Skip to content

Add initial OSGi support#7964

Open
jack-berg wants to merge 7 commits intoopen-telemetry:mainfrom
jack-berg:osgi-support
Open

Add initial OSGi support#7964
jack-berg wants to merge 7 commits intoopen-telemetry:mainfrom
jack-berg:osgi-support

Conversation

@jack-berg
Copy link
Member

@jack-berg jack-berg commented Jan 8, 2026

Related to #768.

This adds initial OSGi support to all artifacts. Specifically, this uses the biz.aQute.bnd.builder plugin to enrich each artifact's MANIFEST.MF. Here's a before and after of MANIFEST.MF for opentelemetry-api:

Before:

Manifest-Version: 1.0
Automatic-Module-Name: io.opentelemetry.api
Built-By: jberg
Built-JDK: 21.0.8
Implementation-Title: all
Implementation-Version: 1.57.0-SNAPSHOT

After:

Manifest-Version: 1.0
Automatic-Module-Name: io.opentelemetry.api
Built-By: jberg
Built-JDK: 21.0.8
Bundle-ManifestVersion: 2
Bundle-Name: opentelemetry-api
Bundle-SymbolicName: opentelemetry-api
Bundle-Version: 1.58.0.SNAPSHOT
Created-By: 21.0.8 (Eclipse Adoptium)
Export-Package: io.opentelemetry.api;version="1.58.0";uses:="io.opente
 lemetry.api.logs,io.opentelemetry.api.metrics,io.opentelemetry.api.tr
 ace,io.opentelemetry.context.propagation",io.opentelemetry.api.baggag
 e;version="1.58.0";uses:="io.opentelemetry.context,javax.annotation",
 io.opentelemetry.api.baggage.propagation;version="1.58.0";uses:="io.o
 pentelemetry.context,io.opentelemetry.context.propagation,javax.annot
 ation",io.opentelemetry.api.common;version="1.58.0";uses:="javax.anno
 tation",io.opentelemetry.api.internal;version="1.58.0";uses:="io.open
 telemetry.api,io.opentelemetry.api.common,io.opentelemetry.api.trace,
 io.opentelemetry.context,javax.annotation",io.opentelemetry.api.logs;
 version="1.58.0";uses:="io.opentelemetry.api.common,io.opentelemetry.
 context,javax.annotation",io.opentelemetry.api.metrics;version="1.58.
 0";uses:="io.opentelemetry.api.common,io.opentelemetry.context",io.op
 entelemetry.api.trace;version="1.58.0";uses:="io.opentelemetry.api.co
 mmon,io.opentelemetry.context,javax.annotation",io.opentelemetry.api.
 trace.propagation;version="1.58.0";uses:="io.opentelemetry.context,io
 .opentelemetry.context.propagation,javax.annotation",io.opentelemetry
 .api.trace.propagation.internal;version="1.58.0";uses:="io.openteleme
 try.api.trace"
Implementation-Title: all
Implementation-Version: 1.58.0-SNAPSHOT
Import-Package: javax.annotation;version="[3.0,4)";resolution:=optiona
 l,io.opentelemetry.sdk.autoconfigure;resolution:=optional,io.opentele
 metry.api.incubator;resolution:=optional,io.opentelemetry.api.incubat
 or.internal;resolution:=optional,io.opentelemetry.context;version="[1
 .58,2)",io.opentelemetry.context.propagation;version="[1.58,2)",java.
 io,java.lang,java.lang.annotation,java.lang.invoke,java.lang.reflect,
 java.nio,java.nio.charset,java.time,java.util,java.util.concurrent,ja
 va.util.function,java.util.logging,java.util.regex,java.util.stream
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Tool: Bnd-7.2.0.202512261929

The import thing this tool does is specify each artifact's Export-Package and Import-Package. Still coming up to speed on OSGi, but my understanding is that this is a specification for the packages each artifact exports for use by others, and the packages each depends on.

Ideally, we would exclude our *.internal.* packages from our exports, but this is blocked by #6970.

A key bit we have to do is tell OSGi which packages are optional. For example, opentelemetry-api has compileOnly dependencies on opentelemetry-api-incubator and a reflection interaction with opentelemetry-sdk-extension-autoconfigure. If we don't tell the bnd that these are optional, then OSGi environments fail to initialize when these are missing. I've extended our gradle tooling to make it easy to specify optional packages by setting otelJava.osgiOptionalPackages.

Detecting misconfiguration of OSGi around optional packages requires test configurations that exercise a specific set of dependencies. I have an initial test configuration which only includes opentelemetry-sdk and transitive dependencies, and have done an initial pass of specifying optional dependencies, but more test configurations and more OSGi configuration will be needed.

@jack-berg
Copy link
Member Author

jack-berg commented Jan 8, 2026

Would hugely appreciate if @stbischof / @roytee @royteeuwen could give this a sanity check!

@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.12%. Comparing base (ca536b2) to head (3791990).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##               main    #7964   +/-   ##
=========================================
  Coverage     90.12%   90.12%           
  Complexity     7463     7463           
=========================================
  Files           834      834           
  Lines         22585    22585           
  Branches       2239     2239           
=========================================
  Hits          20354    20354           
  Misses         1530     1530           
  Partials        701      701           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@stbischof
Copy link

stbischof commented Jan 8, 2026

Will Look at this at weekend

@jack-berg you May want Thatcher BND also calc your jpms Module info.

https://bnd.bndtools.org/chapters/330-jpms.html

Maybe. Not on this Step and Pr. But this Couleur be an extra

@jack-berg jack-berg changed the title Add initial OSGi Add initial OSGi support Jan 8, 2026
@jack-berg
Copy link
Member Author

@jack-berg you May want Thatcher BND also calc your jpms Module info.

Thanks for the tip! My approach is going to be to write a test case showing a scenario that fails, then update the bnd configuration so it passes. So the key will be knowing which scenarios fail without bnd calculating jpms module info, and encoding that into a test.

@royteeuwen
Copy link

Would hugely appreciate if @stbischof / @roytee could give this a sanity check!

@jack-berg I'm guessing you mean me instead of @roytee 😄 , but sure, will try and find some time in the coming week to test everything out :)!

What caused you to implement it ;)?

@jack-berg
Copy link
Member Author

What caused you to implement it ;)?

Been meaning to for a while given how many 👍 the issue has. Was out on parental leave when this comment was left. Long overdue I gave it some attention!

@jack-berg
Copy link
Member Author

@royteeuwen / @stbischof any chance to take a look at this? 🙂

@stbischof
Copy link

stbischof commented Feb 6, 2026

Thank you for the reminder! Will Look on this!

may its a good first step to post my generates manifest from some months ago.

i would recomment to add:

  • -metainf-services: auto link

helps to declare all serviceloader-services by adding Require-Capability and Provide-Capability Headers automatically:

opentelemetry-context

Require-Capability                 osgi.extender;filter:="(osgi.extender=osgi.serviceloader.processor)"

opentelemetry-exporter-otlp

Provide-Capability                      osgi.service;objectClass:List<String>="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";effective:=active
                                        osgi.service;objectClass:List<String>="io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider";effective:=active
                                        osgi.service;objectClass:List<String>="io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider";effective:=active
                                        osgi.service;objectClass:List<String>="io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider";effective:=active
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpGrpcLogRecordExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpGrpcMetricExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpGrpcSpanExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpHttpLogRecordExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpHttpMetricExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpHttpSpanExporterComponentProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpLogRecordExporterProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpMetricExporterProvider"
                                        osgi.serviceloader;osgi.serviceloader="io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider";register:="io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider"
Require-Capability                      osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
                                        osgi.extender;filter:="(&(osgi.extender=osgi.serviceloader.registrar)(version>=1.0.0)(!(version>=2.0.0)))";resolution:=optional

In my setup I used this because of split-packages. But currently cant find the reason.

  • -split-package: merge-first link

If you plan to have perfect good JMPS modules auto-generated, you should look at this:
https://bnd.bndtools.org/chapters/330-jpms.html

@timothyjward
Would be good if you also can look here

@laeubi
Copy link

laeubi commented Feb 26, 2026

My recommendation would be: Try to come to closure here and add basic OSGi metadata, anything else (jpms, metainf-services) could better be added on top of that with additional changes.

This might make it also easier to look at diff/differences and one can already make some things work.

@jack-berg
Copy link
Member Author

Sorry totally missed the notification for the 2/6 comment and thought this was still stale.

helps to declare all serviceloader-services by adding Require-Capability and Provide-Capability Headers automatically:

I'm new to OSGi so my strategy is to build failing tests to clearly illustrate what fails without a particular feature, then go add that feature to get a passing test. What fails without the Require-Capability / Provide-Capability headers? Seems like SPI mechanisms based on the example?

@stbischof
Copy link

stbischof commented Mar 3, 2026

Ich you use Internal serviceloader mechanism it might be That this Would fail.

But i think first step ist to just do export/Import.

And we can use That an the help you with further tests. This way we do not overengeneer

@jack-berg
Copy link
Member Author

But i think first step ist to just do export/Import.
And we can use That an the help you with further tests. This way we do not overengeneer

Exactly my thought. Next step would be to write a test case which depends on the SPI mechanism (either autoconfigure, or OTLP exporters loading senders via SPI, etc), watching it fail, and then making it pass (probably by adding -metainf-services: auto).

If you agree with the code / approach up to this point, consider giving an approval. Approvals are meaningful in this repo even when they don't come from an official approver. Especially so in cases like this outside our core competency.

Copy link

@laeubi laeubi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets see how this works out!

@royteeuwen
Copy link

royteeuwen commented Mar 18, 2026

@jack-berg I finally had the time to go through them and I have some remarks:

opentelemetry-exporter-common and opentelemetry-exporter-otlp currently has code to work with either okhttp or with grpc. The way the imports are now defined, it would need both the implementations to be available. So ideally we would set both of them as optional and you can choose which one is the one you want to use (I haven't found a working grpc implementation in OSGi yet, so I went for ok http)

To do this, we can set the io.grpc, io.grpc.stub and com.google.common (the packages required for grpc) as resolution:=optional. I'd have to see which ones are the ones for ok http and do the same.

for opentelemetry-sdk-trace I'd also set the sun.misc as optional, Unsafe will be removed and is not available everywhere (#6433)

opentelemetry-common and opentelemetry-sdk-extension-autoconfigure would need a Require-Capability header so that the ServiceLoader mechanism works:

opentelemetry-sdk-extension-autoconfigure, discovers exporters, samplers, resource providers:

  Require-Capability: \                                                                                                                                                                                                                                                                                
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider)"; cardinality:=multiple, \                                                                                                                                        
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider)"; cardinality:=multiple, \                                                                                                                                     
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider)"; cardinality:=multiple, \                                                                                                                                     
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider)"; cardinality:=multiple, \                                                                                                                                                               
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider)"; cardinality:=multiple, \                                                                                                                                                 
    osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)"                                                                                                                                                                                                                              

opentelemetry-exporter-common, discovers HTTP/gRPC senders:

  Require-Capability: \                                                                                                                                                                                                                                                                                
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.exporter.internal.http.HttpSenderProvider)"; cardinality:=multiple, \                                                                                                                                                            
    osgi.serviceloader; filter:="(osgi.serviceloader=io.opentelemetry.exporter.internal.grpc.GrpcSenderProvider)"; cardinality:=multiple, \                                                                                                                                                            
    osgi.extender; filter:="(osgi.extender=osgi.serviceloader.processor)"    

If you want (and agree), I can do these changes on this PR? I could potentially also see if I can add some tests for it :)

@jack-berg
Copy link
Member Author

Thanks for the review @royteeuwen!

The way the imports are now defined, it would need both the implementations to be available. So ideally we would set both of them as optional and you can choose which one is the one you want to use

Under the covers they use SPI mechanism to load and resolve the correct sender implementation, gRPC upstream or okhttp or JDK HttpClient. I think we'll need the Require-Capability headers. (I think you're pointing out the same thing in a later comment)

for opentelemetry-sdk-trace I'd also set the sun.misc as optional, Unsafe will be removed and is not available everywhere (#6433)

👍 all of our usages are resilient to it being unavailable, and have fallback mechanisms.

If you want (and agree), I can do these changes on this PR? I could potentially also see if I can add some tests for it :)

I was going to open a followup PR for these changes: https://github.com/open-telemetry/opentelemetry-java/pull/7964/changes#diff-2a792e194b8d6465c4f4caea2a7f31fc6179a196def02b0dc07f19d97734ba48R35-R39

My idea is roughly:

I'd rather get something landed and iterate than wait for this PR to have everything.

@laeubi
Copy link

laeubi commented Mar 19, 2026

If you want (and agree), I can do these changes on this PR? I could potentially also see if I can add some tests for it :)

I think the general idea is sane. But I can also only again recommend to not make this PR to broad!
So lets say we have the meta-data there but ti is not perfect (e.g. requires to much) this can incrementally improved and would maybe give a nicer git history as well as then its easier why this particular thing was chosen over previous alternative, what the test actually was added for and so on.

In general if you require at least one of http or grpc then the best would be to require such thing as a generic capability and let both of them provide that - the OSGi resolver will then complain if none of them are there instead of something (silently?) fail and users can easily search for what provides it.

Regarding the SPI thing: If you are open to get first-class support for OSGi for the sake of some extra code that is not used otherwise you can do the following:

  1. Do not use the SPI directly but have a class lets say ResourceProviderServices
  2. ResourceProviderServices has a public static Stream<ResourceProvider> providers()
  3. Stream<ResourceProvider> providers() then encapsulates the usual java SPI lookup and additionally concat it with a (CopyOnWriteArray)List
  4. You add two (package protected) methods ResourceProviderServices#addProvider and ResourceProviderServices#removeProvider
  5. You annotate the addProvider with org.osgi.service.component.annotations.Reference and bnd plugin will generate an xml for you that allows automatic injection of providers
  6. Annotate each provider with org.osgi.service.component.annotations.Component and the same happens there

All these annotation are not runtime dependencies so can safely be omitted (that's why the xml is generated) outside the build (provided scope) and you will automatically make things work in OSGi + allow easy extension there.

Again, this is something that might be added on top of inital OSGi manifest headers.

@royteeuwen
Copy link

royteeuwen commented Mar 19, 2026

Under the covers they use SPI mechanism to load and resolve the correct sender implementation, gRPC upstream or okhttp or JDK HttpClient. I think we'll need the Require-Capability headers. (I think you're pointing out the same thing in a later comment)

I'd rather get something landed and iterate than wait for this PR to have everything.

@jack-berg that idea sounds very nice! And thus this would mean that you would have to make both the Import-Package of okhttp and grpc optional, so that if you have a runtime that doesn't have either okhttp or grpc, it would use jdk httpclient, and else can of course also use any of the other two.

I can just say that the current PR is not like that, it requires all implementations to be there:

opentelemetry-exporter-common.jar/META-INF/MANIFEST.MF:

Import-Package: javax.annotation;version="[3.0,4)";resolution:=optiona
 l,com.fasterxml.jackson.core;version="[2.20,3)",com.google.common.io;
 version="[33.5,34)",com.google.common.util.concurrent;version="[33.5,
 34)",io.grpc,io.grpc.stub,io.opentelemetry.api;version="[1.58,2)",io.
 opentelemetry.api.common;version="[1.58,2)",io.opentelemetry.api.incu
 bator.config;version="[1.58,2)",io.opentelemetry.api.internal;version
 ="[1.58,2)",io.opentelemetry.api.metrics;version="[1.58,2)",io.opente
 lemetry.api.trace;version="[1.58,2)",io.opentelemetry.common;version=
 "[1.58,2)",io.opentelemetry.context;version="[1.58,2)",io.opentelemet
 ry.sdk.autoconfigure.spi;version="[1.58,2)",io.opentelemetry.sdk.comm
 on;version="[1.58,2)",io.opentelemetry.sdk.common.export;version="[1.
 58,2)",io.opentelemetry.sdk.internal;version="[1.58,2)",io.openteleme
 try.sdk.metrics;version="[1.58,2)",io.opentelemetry.sdk.metrics.expor
 t;version="[1.58,2)",io.opentelemetry.sdk.metrics.internal.aggregator
 ;version="[1.58,2)",io.opentelemetry.sdk.resources;version="[1.58,2)"
 ,java.io,java.lang,java.lang.invoke,java.lang.reflect,java.net,java.n
 io,java.nio.charset,java.security,java.security.cert,java.security.sp
 ec,java.time,java.util,java.util.concurrent,java.util.concurrent.atom
 ic,java.util.function,java.util.logging,java.util.stream,java.util.zi
 p,javax.net.ssl,org.jspecify.annotations;version="[1.0,2)",sun.misc

opentelemetry-exporter-otlp.jar/META-INF/MANIFEST.MF:

Import-Package: javax.annotation;version="[3.0,4)";resolution:=optiona
 l,com.google.common.util.concurrent;version="[33.5,34)",io.grpc,io.gr
 pc.stub,io.opentelemetry.api.incubator.config;version="[1.58,2)",io.o
 pentelemetry.api.internal;version="[1.58,2)",io.opentelemetry.api.met
 rics;version="[1.58,2)",io.opentelemetry.common;version="[1.58,2)",io
 .opentelemetry.exporter.internal;version="[1.58,2)",io.opentelemetry.
 exporter.internal.grpc;version="[1.58,2)",io.opentelemetry.exporter.i
 nternal.http;version="[1.58,2)",io.opentelemetry.exporter.internal.ma
 rshal;version="[1.58,2)",io.opentelemetry.exporter.internal.otlp.logs
 ;version="[1.58,2)",io.opentelemetry.exporter.internal.otlp.metrics;v
 ersion="[1.58,2)",io.opentelemetry.exporter.internal.otlp.traces;vers
 ion="[1.58,2)",io.opentelemetry.sdk;version="[1.58,2)",io.opentelemet
 ry.sdk.autoconfigure.spi;version="[1.58,2)",io.opentelemetry.sdk.auto
 configure.spi.internal;version="[1.58,2)",io.opentelemetry.sdk.autoco
 nfigure.spi.logs;version="[1.58,2)",io.opentelemetry.sdk.autoconfigur
 e.spi.metrics;version="[1.58,2)",io.opentelemetry.sdk.autoconfigure.s
 pi.traces;version="[1.58,2)",io.opentelemetry.sdk.common;version="[1.
 58,2)",io.opentelemetry.sdk.common.export;version="[1.58,2)",io.opent
 elemetry.sdk.internal;version="[1.58,2)",io.opentelemetry.sdk.logs.da
 ta;version="[1.58,2)",io.opentelemetry.sdk.logs.export;version="[1.58
 ,2)",io.opentelemetry.sdk.metrics;version="[1.58,2)",io.opentelemetry
 .sdk.metrics.data;version="[1.58,2)",io.opentelemetry.sdk.metrics.exp
 ort;version="[1.58,2)",io.opentelemetry.sdk.trace.data;version="[1.58
 ,2)",io.opentelemetry.sdk.trace.export;version="[1.58,2)",java.io,jav
 a.lang,java.lang.invoke,java.net,java.nio.charset,java.time,java.util
 ,java.util.concurrent,java.util.concurrent.atomic,java.util.function,
 java.util.logging,javax.net.ssl

and actually, the opentelemetry-exporter-sender-okhttp even has a dependency on grpc:

Import-Package: javax.annotation;version="[3.0,4)";resolution:=optiona
 l,io.opentelemetry.api.internal;version="[1.58,2)",io.opentelemetry.c
 ontext;version="[1.58,2)",io.opentelemetry.exporter.internal;version=
 "[1.58,2)",io.opentelemetry.exporter.internal.compression;version="[1
 .58,2)",io.opentelemetry.exporter.internal.grpc;version="[1.58,2)",io
 .opentelemetry.exporter.internal.http;version="[1.58,2)",io.opentelem
 etry.exporter.internal.marshal;version="[1.58,2)",io.opentelemetry.sd
 k.common;version="[1.58,2)",io.opentelemetry.sdk.common.export;versio
 n="[1.58,2)",io.opentelemetry.sdk.internal;version="[1.58,2)",java.io
 ,java.lang,java.lang.invoke,java.net,java.nio,java.nio.charset,java.t
 ime,java.util,java.util.concurrent,java.util.function,java.util.loggi
 ng,java.util.stream,javax.net.ssl,okhttp3;version="[5.3,6)",okio;vers
 ion="[3.16,4)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants