diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 10db71fc1c4..3dd884f567e 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -13,6 +13,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(configuration): add sampler configuration parsing support [#6409](https://github.com/open-telemetry/opentelemetry-js/pull/6409) @MikeGoldsmith * feat(configuration): add resource detection parsing [#6435](https://github.com/open-telemetry/opentelemetry-js/pull/6435) @MikeGoldsmith * feat(configuration): export interfaces required in other packages [#6462](https://github.com/open-telemetry/opentelemetry-js/pull/6462) @maryliag diff --git a/experimental/packages/configuration/src/EnvironmentConfigFactory.ts b/experimental/packages/configuration/src/EnvironmentConfigFactory.ts index 692d53e9146..bc9f422bfb2 100644 --- a/experimental/packages/configuration/src/EnvironmentConfigFactory.ts +++ b/experimental/packages/configuration/src/EnvironmentConfigFactory.ts @@ -164,6 +164,55 @@ export function setPropagators(config: ConfigurationModel): void { } } +export function setSampler(config: ConfigurationModel): void { + const sampler = getStringFromEnv('OTEL_TRACES_SAMPLER'); + const arg = getStringFromEnv('OTEL_TRACES_SAMPLER_ARG'); + + if (!sampler || !config.tracer_provider) { + return; + } + + const ratio = arg ? parseFloat(arg) : 1.0; + + switch (sampler) { + case 'always_on': + config.tracer_provider.sampler = { always_on: {} }; + break; + + case 'always_off': + config.tracer_provider.sampler = { always_off: {} }; + break; + + case 'traceidratio': + config.tracer_provider.sampler = { + trace_id_ratio_based: { ratio }, + }; + break; + + case 'parentbased_always_on': + config.tracer_provider.sampler = { + parent_based: { root: { always_on: {} } }, + }; + break; + + case 'parentbased_always_off': + config.tracer_provider.sampler = { + parent_based: { root: { always_off: {} } }, + }; + break; + + case 'parentbased_traceidratio': + config.tracer_provider.sampler = { + parent_based: { root: { trace_id_ratio_based: { ratio } } }, + }; + break; + + default: + diag.warn(`Unknown sampler type: ${sampler}`); + break; + } +} + export function setTracerProvider(config: ConfigurationModel): void { const exportersType = Array.from( new Set(getStringListFromEnv('OTEL_TRACES_EXPORTER')) @@ -178,6 +227,7 @@ export function setTracerProvider(config: ConfigurationModel): void { return; } config.tracer_provider = initializeDefaultTracerProviderConfiguration(); + setSampler(config); const attributeValueLengthLimit = getNumberFromEnv( 'OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT' diff --git a/experimental/packages/configuration/src/FileConfigFactory.ts b/experimental/packages/configuration/src/FileConfigFactory.ts index b49d4fe33bf..3d5778b5464 100644 --- a/experimental/packages/configuration/src/FileConfigFactory.ts +++ b/experimental/packages/configuration/src/FileConfigFactory.ts @@ -26,6 +26,8 @@ import { import type { NameStringValuePair } from './models/commonModel'; import { OtlpHttpEncoding, SeverityNumber } from './models/commonModel'; import type { + ExperimentalComposableSampler, + Sampler, SpanExporter, SpanProcessor, TracerProvider, @@ -339,6 +341,137 @@ export function setPropagator( } } +function parseComposableSampler( + sampler: ExperimentalComposableSampler +): ExperimentalComposableSampler { + const samplerType = Object.keys(sampler)[0]; + let parsedSampler: ExperimentalComposableSampler = {}; + + switch (samplerType) { + case 'always_on': + parsedSampler = { always_on: sampler['always_on'] ?? undefined }; + break; + + case 'always_off': + parsedSampler = { always_off: sampler['always_off'] ?? undefined }; + break; + + case 'parent_threshold': { + const s = sampler['parent_threshold']; + if (s?.root) { + parsedSampler = { + parent_threshold: { root: parseComposableSampler(s.root) }, + }; + } + break; + } + + case 'probability': { + const s = sampler['probability']; + parsedSampler = { + probability: { + ratio: getNumberFromConfigFile(s?.ratio) ?? 1.0, + }, + }; + break; + } + + case 'rule_based': { + const rb = sampler['rule_based']; + if (rb) { + parsedSampler = { rule_based: {} }; + if (rb.rules) { + parsedSampler.rule_based!.rules = rb.rules.map(rule => ({ + ...rule, + sampler: rule.sampler ? parseComposableSampler(rule.sampler) : {}, + })); + } + } + break; + } + } + + return parsedSampler; +} + +function parseSampler(sampler: Sampler): Sampler { + const samplerType = Object.keys(sampler)[0]; + let parsedSampler: Sampler = {}; + + switch (samplerType) { + case 'always_on': + parsedSampler = { always_on: sampler['always_on'] ?? undefined }; + break; + + case 'always_off': + parsedSampler = { always_off: sampler['always_off'] ?? undefined }; + break; + + case 'trace_id_ratio_based': { + const s = sampler['trace_id_ratio_based']; + parsedSampler = { + trace_id_ratio_based: { + ratio: getNumberFromConfigFile(s?.ratio) ?? 1.0, + }, + }; + break; + } + + case 'parent_based': { + const s = sampler['parent_based']; + if (s) { + parsedSampler = { parent_based: {} }; + if (s.root) { + parsedSampler.parent_based!.root = parseSampler(s.root); + } + if (s.remote_parent_sampled) { + parsedSampler.parent_based!.remote_parent_sampled = parseSampler( + s.remote_parent_sampled + ); + } + if (s.remote_parent_not_sampled) { + parsedSampler.parent_based!.remote_parent_not_sampled = parseSampler( + s.remote_parent_not_sampled + ); + } + if (s.local_parent_sampled) { + parsedSampler.parent_based!.local_parent_sampled = parseSampler( + s.local_parent_sampled + ); + } + if (s.local_parent_not_sampled) { + parsedSampler.parent_based!.local_parent_not_sampled = parseSampler( + s.local_parent_not_sampled + ); + } + } + break; + } + + case 'probability/development': { + const s = sampler['probability/development']; + parsedSampler = { + 'probability/development': { + ratio: getNumberFromConfigFile(s?.ratio) ?? 1.0, + }, + }; + break; + } + + case 'composite/development': { + const s = sampler['composite/development']; + if (s) { + parsedSampler = { + 'composite/development': parseComposableSampler(s), + }; + } + break; + } + } + + return parsedSampler; +} + function getConfigHeaders( h?: NameStringValuePair[] ): NameStringValuePair[] | null { @@ -552,6 +685,11 @@ export function setTracerProvider( } } + // Sampler + if (tracerProvider['sampler']) { + config.tracer_provider.sampler = parseSampler(tracerProvider['sampler']); + } + // Processors for (let i = 0; i < tracerProvider['processors'].length; i++) { const processorType = Object.keys(tracerProvider['processors'][i])[0]; diff --git a/experimental/packages/configuration/src/models/tracerProviderModel.ts b/experimental/packages/configuration/src/models/tracerProviderModel.ts index 1c8a289aaa1..8667d02fb2f 100644 --- a/experimental/packages/configuration/src/models/tracerProviderModel.ts +++ b/experimental/packages/configuration/src/models/tracerProviderModel.ts @@ -103,6 +103,16 @@ export interface Sampler { * Configure sampler to be trace_id_ratio_based. */ trace_id_ratio_based?: TraceIdRatioBasedSampler; + + /** + * Configure sampler to be probability/development. + */ + 'probability/development'?: ExperimentalProbabilitySampler; + + /** + * Configure sampler to be composite/development. + */ + 'composite/development'?: ExperimentalComposableSampler; } export interface ParentBasedSampler { @@ -140,10 +150,98 @@ export interface ParentBasedSampler { export interface TraceIdRatioBasedSampler { /** * Configure trace_id_ratio. + * If omitted or null, 1.0 is used. + */ + ratio?: number; +} + +export interface ExperimentalProbabilitySampler { + /** + * Configure probability ratio. + * If omitted or null, 1.0 is used. + */ + ratio?: number; +} + +export interface ExperimentalComposableSampler { + /** + * Configure composable sampler to be always_off. + */ + always_off?: object; + + /** + * Configure composable sampler to be always_on. + */ + always_on?: object; + + /** + * Configure composable sampler to be parent_threshold. + */ + parent_threshold?: ExperimentalComposableParentThresholdSampler; + + /** + * Configure composable sampler to be probability. + */ + probability?: ExperimentalComposableProbabilitySampler; + + /** + * Configure composable sampler to be rule_based. + */ + rule_based?: ExperimentalComposableRuleBasedSampler; +} + +export interface ExperimentalComposableParentThresholdSampler { + /** + * Sampler to use when there is no parent. + */ + root: ExperimentalComposableSampler; +} + +export interface ExperimentalComposableProbabilitySampler { + /** + * Configure probability ratio. + * If omitted or null, 1.0 is used. */ ratio?: number; } +export interface ExperimentalComposableRuleBasedSampler { + /** + * The rules for the sampler, matched in order. + */ + rules?: ExperimentalComposableRuleBasedSamplerRule[]; +} + +export interface ExperimentalComposableRuleBasedSamplerRule { + /** + * Values to match against a single attribute. + */ + attribute_values?: { + key: string; + values: string[]; + }; + /** + * Patterns to match against a single attribute. + */ + attribute_patterns?: { + key: string; + included?: string[]; + excluded?: string[]; + }; + /** + * The span kinds to match. + */ + span_kinds?: string[]; + /** + * The parent span types to match. + */ + parent?: string[]; + /** + * The sampler to use for matching spans. + */ + sampler: ExperimentalComposableSampler; +} + export interface SimpleSpanProcessor { /** * Configure exporter. diff --git a/experimental/packages/configuration/test/ConfigFactory.test.ts b/experimental/packages/configuration/test/ConfigFactory.test.ts index 336bf3e44aa..1020f42bb3e 100644 --- a/experimental/packages/configuration/test/ConfigFactory.test.ts +++ b/experimental/packages/configuration/test/ConfigFactory.test.ts @@ -369,8 +369,40 @@ const configFromKitchenSinkFile: ConfigurationModel = { parent_based: { root: { always_on: undefined }, remote_parent_sampled: { always_on: undefined }, - remote_parent_not_sampled: { always_off: undefined }, - local_parent_sampled: { always_on: undefined }, + remote_parent_not_sampled: { + 'probability/development': { ratio: 0.01 }, + }, + local_parent_sampled: { + 'composite/development': { + rule_based: { + rules: [ + { + attribute_values: { + key: 'http.route', + values: ['/healthz', '/livez'], + }, + sampler: { always_off: undefined }, + }, + { + attribute_patterns: { + key: 'http.path', + included: ['/internal/*'], + excluded: ['/internal/special/*'], + }, + sampler: { always_on: undefined }, + }, + { + parent: ['none'], + span_kinds: ['client'], + sampler: { probability: { ratio: 0.05 } }, + }, + { + sampler: { probability: { ratio: 0.001 } }, + }, + ], + }, + }, + }, local_parent_not_sampled: { always_off: undefined }, }, }, @@ -1197,6 +1229,240 @@ describe('ConfigFactory', function () { assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); }); + it('should return config with sampler always_on', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'always_on'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { always_on: {} }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should return config with sampler always_off', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'always_off'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { always_off: {} }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should return config with sampler traceidratio', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'traceidratio'; + process.env.OTEL_TRACES_SAMPLER_ARG = '0.5'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { trace_id_ratio_based: { ratio: 0.5 } }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should return config with sampler parentbased_always_on', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'parentbased_always_on'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { parent_based: { root: { always_on: {} } } }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should return config with sampler parentbased_always_off', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'parentbased_always_off'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { parent_based: { root: { always_off: {} } } }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should return config with sampler parentbased_traceidratio', function () { + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'parentbased_traceidratio'; + process.env.OTEL_TRACES_SAMPLER_ARG = '0.25'; + const expectedConfig: ConfigurationModel = { + ...defaultConfig, + tracer_provider: { + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + encoding: OtlpHttpEncoding.Protobuf, + }, + }, + }, + }, + ], + sampler: { + parent_based: { root: { trace_id_ratio_based: { ratio: 0.25 } } }, + }, + }, + }; + const configFactory = createConfigFactory(); + assert.deepStrictEqual(configFactory.getConfigModel(), expectedConfig); + }); + + it('should warn on unknown sampler type', function () { + const warnSpy = Sinon.spy(diag, 'warn'); + process.env.OTEL_TRACES_EXPORTER = 'otlp'; + process.env.OTEL_TRACES_SAMPLER = 'unknown_sampler'; + createConfigFactory(); + Sinon.assert.calledWith(warnSpy, 'Unknown sampler type: unknown_sampler'); + }); + it('should return config with custom tracer_provider', function () { process.env.OTEL_TRACES_EXPORTER = 'otlp'; process.env.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = '100'; @@ -2193,6 +2459,61 @@ describe('ConfigFactory', function () { ); }); + it('should parse samplers from config file', function () { + process.env.OTEL_CONFIG_FILE = 'test/fixtures/samplers.yaml'; + const configFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + assert.deepStrictEqual(config.tracer_provider?.sampler, { + parent_based: { + root: { + trace_id_ratio_based: { ratio: 0.5 }, + }, + remote_parent_not_sampled: { + 'probability/development': { ratio: 0.1 }, + }, + }, + }); + }); + + it('should parse composite sampler with rule_based rules from config file', function () { + process.env.OTEL_CONFIG_FILE = + 'test/fixtures/composite-sampler-array.yaml'; + const configFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + assert.deepStrictEqual(config.tracer_provider?.sampler, { + 'composite/development': { + rule_based: { + rules: [ + { sampler: { always_on: undefined } }, + { sampler: { probability: { ratio: 0.5 } } }, + ], + }, + }, + }); + }); + + it('should parse composite sampler with rule_based attribute matching from config file', function () { + process.env.OTEL_CONFIG_FILE = + 'test/fixtures/composite-sampler-rulebased-full.yaml'; + const configFactory = createConfigFactory(); + const config = configFactory.getConfigModel(); + assert.deepStrictEqual(config.tracer_provider?.sampler, { + 'composite/development': { + rule_based: { + rules: [ + { + attribute_values: { + key: 'http.method', + values: ['GET'], + }, + sampler: { always_on: undefined }, + }, + ], + }, + }, + }); + }); + it('should return error from invalid config file', function () { const warnSpy = Sinon.spy(diag, 'warn'); process.env.OTEL_CONFIG_FILE = './fixtures/invalid.txt'; diff --git a/experimental/packages/configuration/test/fixtures/composite-sampler-array.yaml b/experimental/packages/configuration/test/fixtures/composite-sampler-array.yaml new file mode 100644 index 00000000000..494aa3c28bd --- /dev/null +++ b/experimental/packages/configuration/test/fixtures/composite-sampler-array.yaml @@ -0,0 +1,15 @@ +file_format: "1.0-rc.3" +tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + composite/development: + rule_based: + rules: + - sampler: + always_on: + - sampler: + probability: + ratio: 0.5 diff --git a/experimental/packages/configuration/test/fixtures/composite-sampler-rulebased-full.yaml b/experimental/packages/configuration/test/fixtures/composite-sampler-rulebased-full.yaml new file mode 100644 index 00000000000..d104a675612 --- /dev/null +++ b/experimental/packages/configuration/test/fixtures/composite-sampler-rulebased-full.yaml @@ -0,0 +1,16 @@ +file_format: "1.0-rc.3" +tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + composite/development: + rule_based: + rules: + - attribute_values: + key: http.method + values: + - GET + sampler: + always_on: diff --git a/experimental/packages/configuration/test/fixtures/samplers.yaml b/experimental/packages/configuration/test/fixtures/samplers.yaml new file mode 100644 index 00000000000..3ffd62f5a75 --- /dev/null +++ b/experimental/packages/configuration/test/fixtures/samplers.yaml @@ -0,0 +1,14 @@ +file_format: "1.0-rc.3" +tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + parent_based: + root: + trace_id_ratio_based: + ratio: 0.5 + remote_parent_not_sampled: + probability/development: + ratio: 0.1