Skip to content

feat(config): Add exporter customizers for declarative config (#6576)#8081

Open
MikeGoldsmith wants to merge 7 commits intoopen-telemetry:mainfrom
honeycombio:mike/exporter-customizers-clean
Open

feat(config): Add exporter customizers for declarative config (#6576)#8081
MikeGoldsmith wants to merge 7 commits intoopen-telemetry:mainfrom
honeycombio:mike/exporter-customizers-clean

Conversation

@MikeGoldsmith
Copy link
Member

Summary

Adds exporter customizers to DeclarativeConfigurationCustomizer for programmatic customization of SpanExporter, MetricExporter, and LogRecordExporter instances created from declarative configuration.

Fixes #6576

Changes

  • Added addSpanExporterCustomizer(), addMetricExporterCustomizer(), addLogRecordExporterCustomizer() to DeclarativeConfigurationCustomizer
  • Customizers compose in SPI registration order via mergeBiFunctionCustomizer() in DeclarativeConfigurationBuilder
  • Factories apply customizers after component creation with fail-fast null checks
  • Comprehensive tests: builder composition, factory application, integration end-to-end

- Add SpanExporter/MetricExporter/LogRecordExporter customizers to DeclarativeConfigurationCustomizer
- Customizers compose in SPI registration order
- Apply in factories after component creation, before return
- Null returns throw DeclarativeConfigException
- Tests: factory unit tests, builder composition tests, integration tests
@MikeGoldsmith MikeGoldsmith requested a review from a team as a code owner February 13, 2026 14:49
@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 89.39394% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.33%. Comparing base (1188cee) to head (3e7f252).

Files with missing lines Patch % Lines
...incubator/fileconfig/LogRecordExporterFactory.java 80.00% 1 Missing and 2 partials ⚠️
...on/incubator/fileconfig/MetricExporterFactory.java 78.57% 1 Missing and 2 partials ⚠️
...sion/incubator/fileconfig/SpanExporterFactory.java 92.85% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8081      +/-   ##
============================================
- Coverage     90.33%   90.33%   -0.01%     
- Complexity     7591     7606      +15     
============================================
  Files           839      839              
  Lines         22828    22891      +63     
  Branches       2279     2291      +12     
============================================
+ Hits          20622    20678      +56     
- Misses         1498     1500       +2     
- Partials        708      713       +5     

☔ 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.

Copy link
Member

@jack-berg jack-berg left a comment

Choose a reason for hiding this comment

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

I'm supportive of these customizers because even though we want to encourage modeling all properties that matter in the opentelemetry-configuration schema, it will never be possible to cover all cases (e.g. think the executor service used to eval async requests), and we'll want an escape hatch to leverage when a a property hasn't yet been modeled in declarative config but will be.

The exporters are a good place to start because there are known gaps in the what can be expressed in declarative config (i.e. authenticators).

Let's hold off modeling additional customizers (e.g. span processors, propagators, etc) until there are concrete requests by end users or distributions which require them.

}

// Pass exporter customizers to context
context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer());
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can just pass the builder to DeclarativeConfigContext to avoid the noise of extra getters / setter for each customizer?

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated in c95e072 - let me know what you think 😄

*
* @param customizer function receiving (exporterName, exporter) and returning customized exporter
*/
void addSpanExporterCustomizer(BiFunction<String, SpanExporter, SpanExporter> customizer);
Copy link
Member

Choose a reason for hiding this comment

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

This contract is different than the env var / system prop equivalent:

AutoConfigurationCustomizer addSpanExporterCustomizer(BiFunction<SpanExporter, ConfigProperties, SpanExporter>)

Comments:

  • There are mechanisms for a customizer to identify the exporter and decide whether to participate: the exporterName and the class type of the exporter. We don't technically need both. What's the advantage (if any) of the exporter name in addition to the type?
  • The customizer doesn't have access to the DeclarativeConfigProperties for the exporter. We need to add this.
  • One annoying part about this and the existing AutoConfigurationCustomizer pattern is that the customizer implementation has to filter all span exporter and only operate on the instances they care about. I wonder if we could improve on this by decomposing it into a predicate and a customizer: addSpanExporterCustomizer(Predicate<SpanExporter>, BiFunction<SpanExporter, DeclarativeConfigProperties, SpanExporter>, or even something like: <T extends SpanExporter> addSpanExporterCustomizer(Class<T>, BiFunction<T, DeclarativeConfigProperties, T>) to remove the need to do this filtering and type casting entirely.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've been look at this, and have found a couple of options:

Option 1: Class filtering

API:

<T extends SpanExporter> void addSpanExporterCustomizer(
    Class<T> exporterType,
    BiFunction<T, DeclarativeConfigProperties, T> customizer);

User code:

customizer.addSpanExporterCustomizer(
    OtlpGrpcSpanExporter.class,
    (exporter, props) -> exporter.toBuilder().setTimeout(...).build());

User: Does not need to cast
Framework: Cast needed (factories OR constructor)
Requires @SuppressWarnings("unchecked") in 3 factories or constructor


Option 2: Predicate filtering

API:

void addSpanExporterCustomizer(
    Predicate<SpanExporter> predicate,
    BiFunction<SpanExporter, DeclarativeConfigProperties, SpanExporter> customizer);

User code:

customizer.addSpanExporterCustomizer(
    exporter -> exporter instanceof OtlpGrpcSpanExporter,
    (exporter, props) -> {
      OtlpGrpcSpanExporter otlp = (OtlpGrpcSpanExporter) exporter;
      return otlp.toBuilder().setTimeout(...).build();
    });

User: Must cast
Framework: No casting
Does not require @SuppressWarnings("unchecked") in factories / construct


What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

I think we try out option 1. Its different than the existing AutoConfigurationCustomizer but having used that API, I'm not a huge fan of the ergonomics and so willing to try something new. We can still review / adjust before stabilizing these APIs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated in eab2d56.

I decided to go with a log and return the original exporter if the customizer returns null.

Reduces noise of 3 separate setter calls for exporter customizers.
Context stores builder ref and delegates customizer access.

Note: Built using Claude Sonnet 4.5
…nfig

- Change customizer signature to use Class<T> for type-safe filtering
- Add DeclarativeConfigProperties parameter for access to config
- Consolidate type cast in ExporterCustomizer constructor
- Handle null returns gracefully with logging instead of exception
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.

How should AutoConfigurationCustomizer work when file configuration is used

2 participants

Comments