fix(configuration)!: backtrack on removing "| null"s from generated types#6679
Conversation
…ypes TBD: motivation and implications
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6679 +/- ##
==========================================
+ Coverage 94.85% 94.94% +0.09%
==========================================
Files 377 377
Lines 12751 12730 -21
Branches 2887 2893 +6
==========================================
- Hits 12095 12087 -8
+ Misses 656 643 -13
🚀 New features to boost your workflow:
|
| maxExportBatchSize: processor.batch.max_export_batch_size, | ||
| scheduledDelayMillis: processor.batch.schedule_delay, | ||
| exportTimeoutMillis: processor.batch.export_timeout, | ||
| maxQueueSize: processor.batch.max_queue_size ?? 2048, |
There was a problem hiding this comment.
same as above
…e declarative config 'create()' step
…etry-configuration issue/PR
|
Continuing from #6679 (comment), this is my attempt to defend why I think the handling of an unspecified
This const config: ConfigurationModel = {
tracer_provider: {
processors: [
{
batch: {
exporter: { otlp_http: null },
}
}
]
}
}or, at least it is with the ...
exporter: { otlp_http: {} },
...Because the type is: export type OtlpHttpExporter = {
// ...
encoding?: OtlpHttpEncoding;
};The
Using "AttributeLimits": {
"type": "object",
"additionalProperties": false,
"properties": {
"attribute_count_limit": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Configure max attribute count. \nValue must be non-negative.\nIf omitted or null, 128 is used.\n"
}
}
},There is no need for the const tracerProvider = new BasicTracerProvider({
// ...
generalLimits: {
attributeCountLimit:
config.attribute_limits?.attribute_count_limit ?? undefined,
},
});There is a choice here: should a span limit default value live in the declarative configuration handling or in the implementation of the SDK component? I think the latter, when possible. The Configuration spec says there must be a programmatic interface (https://opentelemetry.io/docs/specs/otel/configuration/#programmatic). For example, creating a BatchSpanProcessor programmatically: const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const exporter = ...;
const proc = new BatchSpanProcessor(exporter);This does not currently require the user to specify all options, like We can then avoid the work of adding these default values to the parsed config. So this config file: file_format: "1.0"
tracer_provider:
processors:
- batch:
exporter:
otlp_http:Could parse to this: {
tracer_provider: {
processors: [
{
batch: {
exporter: { otlp_http: ... },
}
}
]
}
}Rather than this: {
tracer_provider: {
processors: [
{
batch: {
exporter: { otlp_http: ... },
schedule_delay: 5000,
export_timeout: 30000,
max_queue_size: 2048,
max_export_batch_size: 512
}
}
]
}
}Back to the
I might be wrong, but I think that this part of the Configuration schema will be the only place where a sub-property (e.g. My (naive, possibly unfair) guess is that this is a side-effect of My point is that while it may feel like a design wart to have Much more important is that the
Now that I've removed all the defaults in this PR, except the If Possibly a future |
|
Sorry for the delay on replying here and thanks for the details of your thinking. LGTM (once merge conflicts are resolved) |
|
That lint failure was unrelated: Aside: our |
b3ca638
Closes: #6576
tl;dr
This backtracks on the
configurationpackage design of attempting to avoid anynullvalues in a parsed JS config. This PR changes configuration parsing to leave the nulls in and move responsibility for handling null values to the consumers of the parsed config object. This solves the general issue partially mentioned by #6576 and results in the TypeScript types more closely matching the Configuration JSON Schema shape.Background
The OpenTelemetry configuration JSON schema intentionally adds an optional "null" type to many fields for two purposes (https://github.com/open-telemetry/opentelemetry-configuration/blob/main/CONTRIBUTING.md#required-and-null-properties):
endpointbelow.OtlpHttpExporterbelow.For example, let's look at this part of the JSON schema:
Without manipulation,
json-schema-to-typescripttranslates this to these TypeScript types:Going back to the two purposes of
null. The first is for envvar substitution, so that this YAML works:If
MY_OTLP_ENDPOINTis not set, then parsing this YAML and doing envvar substitution results in this JS object, withendpoint: null.The second purpose is for objects that do not require any properties, either because they don't have any or because all properties have default values. This allows a YAML config like this:
which yields this parsed config:
How to handle those
nullvalues?There are two options for dealing with these
nullvalues:nullvalues as described in the "description" field for each type. E.g. theendpointproperty above has a description with "If omitted or null, use ...".| nulls so that SDK code using the parsed config can do truthy checks,if (exporter.otlp_http) { ... }, and passendpoint(now of typestring | undefined) directly to an SDK component constructor,new OtlpHttpTraceExporter({ endpoint: exporter.otlp_http.endpoint }).Currently the
configurationpackage attempts option 2, because it seems nicer for SDK code using the resulting config object.However there are problems.
The problems
When the
| nulls have been removed from the TypeScript types, the parsed config file needs to be post-processed to not have thosenullvalues. This involves:Converting
nullvalues to{}for objects that don't require any properties.so that
if (exporter.otlp_http) { ... }works.This should only be done for some nulls.
Then remove the remaining properties that have a null value.
A problem is knowing which properties should have
nullconverted to{}and which should be removed. Currently the code,normalizeYamlNulls(), has a hardcoded allowlist of well-known property names and a heuristic for arrays.First, this is brittle. For example, here is a current case where that heuristic gets it wrong:
The crash here is because that
normalizeYamlNulls()is converting this raw config object:to this:
And then schema validation fails because resource attributes may not be an object.
Second, the hardcoded allowlist cannot handle custom values (like custom detectors, custom processors, etc.). See #6576 that describes the issue for custom propagators.
One considered fix
One option for the brittle case shown above would be to make the
normalizeYamlNulls()andstripNulls()heuristics smarter -- by adding more hardcoded allowlisting of known names in the current schema. That could work, but is an ongoing maintenance burden: as the declarative config schema evolves the@opentelemetry/configurationpackage might have to update its post-processing. The fix would remain brittle.As well, this fix option cannot handle custom YAML sections of the schema: custom processors, detectors, etc.; anything under
instrumentation/development.{language}. So, the result for consumers of the parsed config would always be two flavours of parsed config:nullvalues,nullvaluesProposed fix
The fix I'm proposing is to backtrack on the design and have configuration parsing not remove nulls. It then becomes the responsibility of consumers of the parsed config to handled nulls.
(I'm moving the
configurationpackage design away from having smarts on config parse, and moving those smarts to the create step.)Currently "consumers of the parsed config" is just the
sdk-nodepackage. However, eventually that will also include: (a) instrumentations usingConfigProvider#getInstrumentationConfig()and (b) any PluginComponentProvider, once these things are implemented in OTel JS.Impact of this change
Using the parsed config JS object needs to know about nulls.
For properties like
exporter.otlp_http.endpoint, that means handlingstring | null | undefined. It turns out this isn't so bad, because all optional properties have a default value, so nullish-coalescing works nicely:For objects that can be
null, likeexporter.otlp_http, that means one must not use truthiness. This does require some more care by SDK developers.One the plus side: