feat(config): Add exporter customizers for declarative config (#6576)#8081
feat(config): Add exporter customizers for declarative config (#6576)#8081MikeGoldsmith wants to merge 7 commits intoopen-telemetry:mainfrom
Conversation
- 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
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
…nto mike/exporter-customizers-clean
jack-berg
left a comment
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
Maybe we can just pass the builder to DeclarativeConfigContext to avoid the noise of extra getters / setter for each customizer?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
DeclarativeConfigPropertiesfor 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
…nto mike/exporter-customizers-clean
…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
…nto mike/exporter-customizers-clean
Summary
Adds exporter customizers to
DeclarativeConfigurationCustomizerfor programmatic customization ofSpanExporter,MetricExporter, andLogRecordExporterinstances created from declarative configuration.Fixes #6576
Changes
addSpanExporterCustomizer(),addMetricExporterCustomizer(),addLogRecordExporterCustomizer()toDeclarativeConfigurationCustomizermergeBiFunctionCustomizer()inDeclarativeConfigurationBuilder