diff --git a/CHANGELOG.md b/CHANGELOG.md index 605d7dba8f6..6400a21443e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add unmarshaling and validation for `OTLPHttpExporter`, `OTLPGrpcExporter`, `OTLPGrpcMetricExporter` and `OTLPHttpMetricExporter` to v1.0.0 model in `go.opentelemetry.io/contrib/otelconf`. (#8112) - Add a `WithSpanNameFormatter` option to `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo`. (#7986) - Add unmarshaling and validation for `AttributeType`, `AttributeNameValue`, `SimpleSpanProcessor`, `SimpleLogRecordProcessor`, `ZipkinSpanExporter`, `NameStringValuePair`, `InstrumentType`, `ExperimentalPeerInstrumentationServiceMappingElem`, `ExporterDefaultHistogramAggregation`, `PullMetricReader` to v1.0.0 model in `go.opentelemetry.io/contrib/otelconf`. (#8127) +- Updated `go.opentelemetry.io/contrib/otelconf` to include the [v1.0.0-rc2](https://github.com/open-telemetry/opentelemetry-configuration/releases/tag/v1.0.0-rc.2) release candidate of schema which includes backwards incompatible changes. (#8026) ### Changed diff --git a/otelconf/config.go b/otelconf/config.go new file mode 100644 index 00000000000..db6305803b8 --- /dev/null +++ b/otelconf/config.go @@ -0,0 +1,171 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package otelconf provides an OpenTelemetry declarative configuration SDK. +package otelconf // import "go.opentelemetry.io/contrib/otelconf" + +import ( + "context" + "errors" + + "go.opentelemetry.io/otel/log" + nooplog "go.opentelemetry.io/otel/log/noop" + "go.opentelemetry.io/otel/metric" + noopmetric "go.opentelemetry.io/otel/metric/noop" + sdklog "go.opentelemetry.io/otel/sdk/log" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + nooptrace "go.opentelemetry.io/otel/trace/noop" + yaml "go.yaml.in/yaml/v3" + + "go.opentelemetry.io/contrib/otelconf/internal/provider" +) + +// SDK is a struct that contains all the providers +// configured via the configuration model. +type SDK struct { + meterProvider metric.MeterProvider + tracerProvider trace.TracerProvider + loggerProvider log.LoggerProvider + shutdown shutdownFunc +} + +// TracerProvider returns a configured trace.TracerProvider. +func (s *SDK) TracerProvider() trace.TracerProvider { + return s.tracerProvider +} + +// MeterProvider returns a configured metric.MeterProvider. +func (s *SDK) MeterProvider() metric.MeterProvider { + return s.meterProvider +} + +// LoggerProvider returns a configured log.LoggerProvider. +func (s *SDK) LoggerProvider() log.LoggerProvider { + return s.loggerProvider +} + +// Shutdown calls shutdown on all configured providers. +func (s *SDK) Shutdown(ctx context.Context) error { + return s.shutdown(ctx) +} + +var noopSDK = SDK{ + loggerProvider: nooplog.LoggerProvider{}, + meterProvider: noopmetric.MeterProvider{}, + tracerProvider: nooptrace.TracerProvider{}, + shutdown: func(context.Context) error { return nil }, +} + +// NewSDK creates SDK providers based on the configuration model. +func NewSDK(opts ...ConfigurationOption) (SDK, error) { + o := configOptions{ + ctx: context.Background(), + } + for _, opt := range opts { + o = opt.apply(o) + } + if o.opentelemetryConfig.Disabled != nil && *o.opentelemetryConfig.Disabled { + return noopSDK, nil + } + + r, err := newResource(o.opentelemetryConfig.Resource) + if err != nil { + return noopSDK, err + } + + mp, mpShutdown, err := meterProvider(o, r) + if err != nil { + return noopSDK, err + } + + tp, tpShutdown, err := tracerProvider(o, r) + if err != nil { + return noopSDK, err + } + + lp, lpShutdown, err := loggerProvider(o, r) + if err != nil { + return noopSDK, err + } + + return SDK{ + meterProvider: mp, + tracerProvider: tp, + loggerProvider: lp, + shutdown: func(ctx context.Context) error { + return errors.Join(mpShutdown(ctx), tpShutdown(ctx), lpShutdown(ctx)) + }, + }, nil +} + +// ConfigurationOption configures options for providers. +type ConfigurationOption interface { + apply(configOptions) configOptions +} + +type configurationOptionFunc func(configOptions) configOptions + +func (fn configurationOptionFunc) apply(cfg configOptions) configOptions { + return fn(cfg) +} + +// WithContext sets the context.Context for the SDK. +func WithContext(ctx context.Context) ConfigurationOption { + return configurationOptionFunc(func(c configOptions) configOptions { + c.ctx = ctx + return c + }) +} + +// WithOpenTelemetryConfiguration sets the OpenTelemetryConfiguration used +// to produce the SDK. +func WithOpenTelemetryConfiguration(cfg OpenTelemetryConfiguration) ConfigurationOption { + return configurationOptionFunc(func(c configOptions) configOptions { + c.opentelemetryConfig = cfg + return c + }) +} + +// WithLoggerProviderOptions appends LoggerProviderOptions used for constructing +// the LoggerProvider. OpenTelemetryConfiguration takes precedence over these options. +func WithLoggerProviderOptions(opts ...sdklog.LoggerProviderOption) ConfigurationOption { + return configurationOptionFunc(func(c configOptions) configOptions { + c.loggerProviderOptions = append(c.loggerProviderOptions, opts...) + return c + }) +} + +// WithMeterProviderOptions appends metric.Options used for constructing the +// MeterProvider. OpenTelemetryConfiguration takes precedence over these options. +func WithMeterProviderOptions(opts ...sdkmetric.Option) ConfigurationOption { + return configurationOptionFunc(func(c configOptions) configOptions { + c.meterProviderOptions = append(c.meterProviderOptions, opts...) + return c + }) +} + +// WithTracerProviderOptions appends TracerProviderOptions used for constructing +// the TracerProvider. OpenTelemetryConfiguration takes precedence over these options. +func WithTracerProviderOptions(opts ...sdktrace.TracerProviderOption) ConfigurationOption { + return configurationOptionFunc(func(c configOptions) configOptions { + c.tracerProviderOptions = append(c.tracerProviderOptions, opts...) + return c + }) +} + +// ParseYAML parses a YAML configuration file into an OpenTelemetryConfiguration. +func ParseYAML(file []byte) (*OpenTelemetryConfiguration, error) { + file, err := provider.ReplaceEnvVars(file) + if err != nil { + return nil, err + } + var cfg OpenTelemetryConfiguration + err = yaml.Unmarshal(file, &cfg) + if err != nil { + return nil, err + } + + return &cfg, nil +} diff --git a/otelconf/config_common.go b/otelconf/config_common.go index 246d29e33b4..bb304e07958 100644 --- a/otelconf/config_common.go +++ b/otelconf/config_common.go @@ -50,8 +50,8 @@ var enumValuesOTLPMetricDefaultHistogramAggregation = []any{ type configOptions struct { ctx context.Context opentelemetryConfig OpenTelemetryConfiguration - meterProviderOptions []sdkmetric.Option loggerProviderOptions []sdklog.LoggerProviderOption + meterProviderOptions []sdkmetric.Option tracerProviderOptions []sdktrace.TracerProviderOption } @@ -155,6 +155,30 @@ func newErrInvalid(id string) error { return &errInvalid{Identifier: id} } +// unmarshalSamplerTypes handles always_on and always_off sampler unmarshaling. +func unmarshalSamplerTypes(raw map[string]any, plain *Sampler) { + // always_on can be nil, must check and set here + if _, ok := raw["always_on"]; ok { + plain.AlwaysOn = AlwaysOnSampler{} + } + // always_off can be nil, must check and set here + if _, ok := raw["always_off"]; ok { + plain.AlwaysOff = AlwaysOffSampler{} + } +} + +// unmarshalMetricProducer handles opencensus metric producer unmarshaling. +func unmarshalMetricProducer(raw map[string]any, plain *MetricProducer) { + // opencensus can be nil, must check and set here + if v, ok := raw["opencensus"]; ok && v == nil { + delete(raw, "opencensus") + plain.Opencensus = OpenCensusMetricProducer{} + } + if len(raw) > 0 { + plain.AdditionalProperties = raw + } +} + // validatePeriodicMetricReader handles validation for PeriodicMetricReader. func validatePeriodicMetricReader(plain *PeriodicMetricReader) error { if plain.Timeout != nil && 0 > *plain.Timeout { diff --git a/otelconf/config_json.go b/otelconf/config_json.go index 3c2de10e15c..1d018bf0512 100644 --- a/otelconf/config_json.go +++ b/otelconf/config_json.go @@ -281,8 +281,7 @@ func (j *BatchLogRecordProcessor) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(sh.Exporter, &sh.Plain.Exporter); err != nil { return err } - err := validateBatchLogRecordProcessor((*BatchLogRecordProcessor)(&sh.Plain)) - if err != nil { + if err := validateBatchLogRecordProcessor((*BatchLogRecordProcessor)(&sh.Plain)); err != nil { return err } *j = BatchLogRecordProcessor(sh.Plain) @@ -307,14 +306,122 @@ func (j *BatchSpanProcessor) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(sh.Exporter, &sh.Plain.Exporter); err != nil { return err } - err := validateBatchSpanProcessor((*BatchSpanProcessor)(&sh.Plain)) - if err != nil { + if err := validateBatchSpanProcessor((*BatchSpanProcessor)(&sh.Plain)); err != nil { return err } *j = BatchSpanProcessor(sh.Plain) return nil } +// UnmarshalJSON implements json.Unmarshaler. +func (j *OpenTelemetryConfiguration) UnmarshalJSON(b []byte) error { + type Plain OpenTelemetryConfiguration + type shadow struct { + Plain + FileFormat json.RawMessage `json:"file_format"` + LoggerProvider json.RawMessage `json:"logger_provider"` + MeterProvider json.RawMessage `json:"meter_provider"` + TracerProvider json.RawMessage `json:"tracer_provider"` + Propagator json.RawMessage `json:"propagator"` + Resource json.RawMessage `json:"resource"` + InstrumentationDevelopment json.RawMessage `json:"instrumentation/development"` + AttributeLimits json.RawMessage `json:"attribute_limits"` + Disabled json.RawMessage `json:"disabled"` + LogLevel json.RawMessage `json:"log_level"` + } + var sh shadow + if err := json.Unmarshal(b, &sh); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + + if len(sh.FileFormat) == 0 { + return newErrRequired(j, "file_format") + } + + if err := json.Unmarshal(sh.FileFormat, &sh.Plain.FileFormat); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + + if sh.LoggerProvider != nil { + var l LoggerProviderJson + if err := json.Unmarshal(sh.LoggerProvider, &l); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.LoggerProvider = &l + } + + if sh.MeterProvider != nil { + var m MeterProviderJson + if err := json.Unmarshal(sh.MeterProvider, &m); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.MeterProvider = &m + } + + if sh.TracerProvider != nil { + var t TracerProviderJson + if err := json.Unmarshal(sh.TracerProvider, &t); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.TracerProvider = &t + } + + if sh.Propagator != nil { + var p PropagatorJson + if err := json.Unmarshal(sh.Propagator, &p); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Propagator = &p + } + + if sh.Resource != nil { + var r ResourceJson + if err := json.Unmarshal(sh.Resource, &r); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Resource = &r + } + + if sh.InstrumentationDevelopment != nil { + var r InstrumentationJson + if err := json.Unmarshal(sh.InstrumentationDevelopment, &r); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.InstrumentationDevelopment = &r + } + + if sh.AttributeLimits != nil { + var r AttributeLimits + if err := json.Unmarshal(sh.AttributeLimits, &r); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.AttributeLimits = &r + } + + if sh.Disabled != nil { + if err := json.Unmarshal(sh.Disabled, &sh.Plain.Disabled); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + } else { + // Configure if the SDK is disabled or not. + // If omitted or null, false is used. + sh.Plain.Disabled = ptr(false) + } + + if sh.LogLevel != nil { + if err := json.Unmarshal(sh.LogLevel, &sh.Plain.LogLevel); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + } else { + // Configure the log level of the internal logger used by the SDK. + // If omitted, info is used. + sh.Plain.LogLevel = ptr("info") + } + + *j = OpenTelemetryConfiguration(sh.Plain) + return nil +} + // UnmarshalJSON implements json.Unmarshaler. func (j *PeriodicMetricReader) UnmarshalJSON(b []byte) error { type Plain PeriodicMetricReader @@ -714,3 +821,35 @@ func (j *PullMetricReader) UnmarshalJSON(b []byte) error { *j = PullMetricReader(sh.Plain) return nil } + +// UnmarshalJSON implements json.Unmarshaler. +func (j *Sampler) UnmarshalJSON(b []byte) error { + var raw map[string]any + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + type Plain Sampler + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + unmarshalSamplerTypes(raw, (*Sampler)(&plain)) + *j = Sampler(plain) + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *MetricProducer) UnmarshalJSON(b []byte) error { + var raw map[string]any + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + type Plain MetricProducer + var plain Plain + if err := json.Unmarshal(b, &plain); err != nil { + return err + } + unmarshalMetricProducer(raw, (*MetricProducer)(&plain)) + *j = MetricProducer(plain) + return nil +} diff --git a/otelconf/config_test.go b/otelconf/config_test.go index 99c31e76b63..d73b9376633 100644 --- a/otelconf/config_test.go +++ b/otelconf/config_test.go @@ -4,10 +4,20 @@ package otelconf import ( + "encoding/json" + "fmt" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + lognoop "go.opentelemetry.io/otel/log/noop" + metricnoop "go.opentelemetry.io/otel/metric/noop" + sdklog "go.opentelemetry.io/otel/sdk/log" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + tracenoop "go.opentelemetry.io/otel/trace/noop" "go.yaml.in/yaml/v3" ) @@ -339,6 +349,819 @@ func TestUnmarshalBatchLogRecordProcessor(t *testing.T) { } } +func TestNewSDK(t *testing.T) { + tests := []struct { + name string + cfg []ConfigurationOption + wantTracerProvider any + wantMeterProvider any + wantLoggerProvider any + wantErr error + wantShutdownErr error + }{ + { + name: "no-configuration", + wantTracerProvider: tracenoop.NewTracerProvider(), + wantMeterProvider: metricnoop.NewMeterProvider(), + wantLoggerProvider: lognoop.NewLoggerProvider(), + }, + { + name: "with-configuration", + cfg: []ConfigurationOption{ + WithContext(t.Context()), + WithOpenTelemetryConfiguration(OpenTelemetryConfiguration{ + TracerProvider: &TracerProviderJson{}, + MeterProvider: &MeterProviderJson{}, + LoggerProvider: &LoggerProviderJson{}, + }), + }, + wantTracerProvider: &sdktrace.TracerProvider{}, + wantMeterProvider: &sdkmetric.MeterProvider{}, + wantLoggerProvider: &sdklog.LoggerProvider{}, + }, + { + name: "with-sdk-disabled", + cfg: []ConfigurationOption{ + WithContext(t.Context()), + WithOpenTelemetryConfiguration(OpenTelemetryConfiguration{ + Disabled: ptr(true), + TracerProvider: &TracerProviderJson{}, + MeterProvider: &MeterProviderJson{}, + LoggerProvider: &LoggerProviderJson{}, + }), + }, + wantTracerProvider: tracenoop.NewTracerProvider(), + wantMeterProvider: metricnoop.NewMeterProvider(), + wantLoggerProvider: lognoop.NewLoggerProvider(), + }, + { + name: "invalid resource", + cfg: []ConfigurationOption{ + WithContext(t.Context()), + WithOpenTelemetryConfiguration(OpenTelemetryConfiguration{ + TracerProvider: &TracerProviderJson{}, + MeterProvider: &MeterProviderJson{}, + LoggerProvider: &LoggerProviderJson{}, + Resource: &LoggerProviderJson{}, + }), + }, + wantErr: newErrInvalid("resource"), + wantTracerProvider: tracenoop.NewTracerProvider(), + wantMeterProvider: metricnoop.NewMeterProvider(), + wantLoggerProvider: lognoop.NewLoggerProvider(), + }, + { + name: "invalid logger provider", + cfg: []ConfigurationOption{ + WithContext(t.Context()), + WithOpenTelemetryConfiguration(OpenTelemetryConfiguration{ + TracerProvider: &TracerProviderJson{}, + MeterProvider: &MeterProviderJson{}, + LoggerProvider: &ResourceJson{}, + Resource: &ResourceJson{}, + }), + }, + wantErr: newErrInvalid("logger_provider"), + wantTracerProvider: tracenoop.NewTracerProvider(), + wantMeterProvider: metricnoop.NewMeterProvider(), + wantLoggerProvider: lognoop.NewLoggerProvider(), + }, + { + name: "invalid tracer provider", + cfg: []ConfigurationOption{ + WithContext(t.Context()), + WithOpenTelemetryConfiguration(OpenTelemetryConfiguration{ + TracerProvider: &ResourceJson{}, + }), + }, + wantErr: newErrInvalid("tracer_provider"), + wantTracerProvider: tracenoop.NewTracerProvider(), + wantMeterProvider: metricnoop.NewMeterProvider(), + wantLoggerProvider: lognoop.NewLoggerProvider(), + }, + } + for _, tt := range tests { + sdk, err := NewSDK(tt.cfg...) + require.Equal(t, tt.wantErr, err) + assert.IsType(t, tt.wantTracerProvider, sdk.TracerProvider()) + assert.IsType(t, tt.wantMeterProvider, sdk.MeterProvider()) + assert.IsType(t, tt.wantLoggerProvider, sdk.LoggerProvider()) + require.Equal(t, tt.wantShutdownErr, sdk.Shutdown(t.Context())) + } +} + +var v10OpenTelemetryConfig = OpenTelemetryConfiguration{ + Disabled: ptr(false), + FileFormat: "1.0-rc.2", + AttributeLimits: &AttributeLimits{ + AttributeCountLimit: ptr(128), + AttributeValueLengthLimit: ptr(4096), + }, + InstrumentationDevelopment: &InstrumentationJson{ + Cpp: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Dotnet: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Erlang: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + General: &ExperimentalGeneralInstrumentation{ + Http: &ExperimentalHttpInstrumentation{ + Client: &ExperimentalHttpInstrumentationClient{ + RequestCapturedHeaders: []string{"Content-Type", "Accept"}, + ResponseCapturedHeaders: []string{"Content-Type", "Content-Encoding"}, + }, + Server: &ExperimentalHttpInstrumentationServer{ + RequestCapturedHeaders: []string{"Content-Type", "Accept"}, + ResponseCapturedHeaders: []string{"Content-Type", "Content-Encoding"}, + }, + }, + Peer: &ExperimentalPeerInstrumentation{ + ServiceMapping: []ExperimentalPeerInstrumentationServiceMappingElem{ + {Peer: "1.2.3.4", Service: "FooService"}, + {Peer: "2.3.4.5", Service: "BarService"}, + }, + }, + }, + Go: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Java: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Js: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Php: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Python: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Ruby: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Rust: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + Swift: ExperimentalLanguageSpecificInstrumentation{ + "example": map[string]any{ + "property": "value", + }, + }, + }, + LogLevel: ptr("info"), + LoggerProvider: &LoggerProviderJson{ + LoggerConfiguratorDevelopment: &ExperimentalLoggerConfigurator{ + DefaultConfig: &ExperimentalLoggerConfig{ + Disabled: ptr(true), + }, + Loggers: []ExperimentalLoggerMatcherAndConfig{ + { + Config: &ExperimentalLoggerConfig{ + Disabled: ptr(false), + }, + Name: ptr("io.opentelemetry.contrib.*"), + }, + }, + }, + Limits: &LogRecordLimits{ + AttributeCountLimit: ptr(128), + AttributeValueLengthLimit: ptr(4096), + }, + Processors: []LogRecordProcessor{ + { + Batch: &BatchLogRecordProcessor{ + ExportTimeout: ptr(30000), + Exporter: LogRecordExporter{ + OTLPHttp: &OTLPHttpExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + Encoding: ptr(OTLPHttpEncodingProtobuf), + Endpoint: ptr("http://localhost:4318/v1/logs"), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + Timeout: ptr(10000), + }, + }, + MaxExportBatchSize: ptr(512), + MaxQueueSize: ptr(2048), + ScheduleDelay: ptr(5000), + }, + }, + { + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLPGrpc: &OTLPGrpcExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + Endpoint: ptr("http://localhost:4317"), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + Timeout: ptr(10000), + Insecure: ptr(false), + }, + }, + }, + }, + { + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileExporter{ + OutputStream: ptr("file:///var/log/logs.jsonl"), + }, + }, + }, + }, + { + Batch: &BatchLogRecordProcessor{ + Exporter: LogRecordExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileExporter{ + OutputStream: ptr("stdout"), + }, + }, + }, + }, + { + Simple: &SimpleLogRecordProcessor{ + Exporter: LogRecordExporter{ + Console: ConsoleExporter{}, + }, + }, + }, + }, + }, + MeterProvider: &MeterProviderJson{ + ExemplarFilter: ptr(ExemplarFilter("trace_based")), + MeterConfiguratorDevelopment: &ExperimentalMeterConfigurator{ + DefaultConfig: &ExperimentalMeterConfig{ + Disabled: ptr(true), + }, + Meters: []ExperimentalMeterMatcherAndConfig{ + { + Config: &ExperimentalMeterConfig{ + Disabled: ptr(false), + }, + Name: ptr("io.opentelemetry.contrib.*"), + }, + }, + }, + Readers: []MetricReader{ + { + Pull: &PullMetricReader{ + Producers: []MetricProducer{ + { + Opencensus: OpenCensusMetricProducer{}, + }, + }, + CardinalityLimits: &CardinalityLimits{ + Default: ptr(2000), + Counter: ptr(2000), + Gauge: ptr(2000), + Histogram: ptr(2000), + ObservableCounter: ptr(2000), + ObservableGauge: ptr(2000), + ObservableUpDownCounter: ptr(2000), + UpDownCounter: ptr(2000), + }, + Exporter: PullMetricExporter{ + PrometheusDevelopment: &ExperimentalPrometheusMetricExporter{ + Host: ptr("localhost"), + Port: ptr(9464), + TranslationStrategy: ptr(ExperimentalPrometheusMetricExporterTranslationStrategyUnderscoreEscapingWithSuffixes), + WithResourceConstantLabels: &IncludeExclude{ + Excluded: []string{"service.attr1"}, + Included: []string{"service*"}, + }, + WithoutScopeInfo: ptr(false), + }, + }, + }, + }, + { + Periodic: &PeriodicMetricReader{ + Producers: []MetricProducer{ + { + AdditionalProperties: map[string]any{ + "prometheus": nil, + }, + }, + }, + CardinalityLimits: &CardinalityLimits{ + Default: ptr(2000), + Counter: ptr(2000), + Gauge: ptr(2000), + Histogram: ptr(2000), + ObservableCounter: ptr(2000), + ObservableGauge: ptr(2000), + ObservableUpDownCounter: ptr(2000), + UpDownCounter: ptr(2000), + }, + Exporter: PushMetricExporter{ + OTLPHttp: &OTLPHttpMetricExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + DefaultHistogramAggregation: ptr(ExporterDefaultHistogramAggregationBase2ExponentialBucketHistogram), + Endpoint: ptr("http://localhost:4318/v1/metrics"), + Encoding: ptr(OTLPHttpEncodingProtobuf), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + TemporalityPreference: ptr(ExporterTemporalityPreferenceDelta), + Timeout: ptr(10000), + }, + }, + Interval: ptr(60000), + Timeout: ptr(30000), + }, + }, + { + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLPGrpc: &OTLPGrpcMetricExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + DefaultHistogramAggregation: ptr(ExporterDefaultHistogramAggregationBase2ExponentialBucketHistogram), + Endpoint: ptr("http://localhost:4317"), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + TemporalityPreference: ptr(ExporterTemporalityPreferenceDelta), + Timeout: ptr(10000), + Insecure: ptr(false), + }, + }, + }, + }, + { + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileMetricExporter{ + OutputStream: ptr("file:///var/log/metrics.jsonl"), + DefaultHistogramAggregation: ptr(ExporterDefaultHistogramAggregationBase2ExponentialBucketHistogram), + TemporalityPreference: ptr(ExporterTemporalityPreferenceDelta), + }, + }, + }, + }, + { + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileMetricExporter{ + OutputStream: ptr("stdout"), + DefaultHistogramAggregation: ptr(ExporterDefaultHistogramAggregationBase2ExponentialBucketHistogram), + TemporalityPreference: ptr(ExporterTemporalityPreferenceDelta), + }, + }, + }, + }, + { + Periodic: &PeriodicMetricReader{ + Exporter: PushMetricExporter{ + Console: ConsoleExporter{}, + }, + }, + }, + }, + Views: []View{ + { + Selector: &ViewSelector{ + InstrumentName: ptr("my-instrument"), + InstrumentType: ptr(InstrumentTypeHistogram), + MeterName: ptr("my-meter"), + MeterSchemaUrl: ptr("https://opentelemetry.io/schemas/1.16.0"), + MeterVersion: ptr("1.0.0"), + Unit: ptr("ms"), + }, + Stream: &ViewStream{ + Aggregation: &Aggregation{ + ExplicitBucketHistogram: &ExplicitBucketHistogramAggregation{ + Boundaries: []float64{0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000}, + RecordMinMax: ptr(true), + }, + }, + AggregationCardinalityLimit: ptr(2000), + AttributeKeys: &IncludeExclude{ + Included: []string{"key1", "key2"}, + Excluded: []string{"key3"}, + }, + Description: ptr("new_description"), + Name: ptr("new_instrument_name"), + }, + }, + }, + }, + Propagator: &PropagatorJson{ + Composite: []TextMapPropagator{ + { + Tracecontext: TraceContextPropagator{}, + }, + { + Baggage: BaggagePropagator{}, + }, + { + B3: B3Propagator{}, + }, + { + B3Multi: B3MultiPropagator{}, + }, + { + Jaeger: JaegerPropagator{}, + }, + { + Ottrace: OpenTracingPropagator{}, + }, + }, + CompositeList: ptr("tracecontext,baggage,b3,b3multi,jaeger,ottrace,xray"), + }, + Resource: &ResourceJson{ + Attributes: []AttributeNameValue{ + {Name: "service.name", Value: "unknown_service"}, + {Name: "string_key", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "bool_key", Type: &AttributeType{Value: "bool"}, Value: true}, + {Name: "int_key", Type: &AttributeType{Value: "int"}, Value: 1}, + {Name: "double_key", Type: &AttributeType{Value: "double"}, Value: 1.1}, + {Name: "string_array_key", Type: &AttributeType{Value: "string_array"}, Value: []any{"value1", "value2"}}, + {Name: "bool_array_key", Type: &AttributeType{Value: "bool_array"}, Value: []any{true, false}}, + {Name: "int_array_key", Type: &AttributeType{Value: "int_array"}, Value: []any{1, 2}}, + {Name: "double_array_key", Type: &AttributeType{Value: "double_array"}, Value: []any{1.1, 2.2}}, + }, + AttributesList: ptr("service.namespace=my-namespace,service.version=1.0.0"), + DetectionDevelopment: &ExperimentalResourceDetection{ + Attributes: &IncludeExclude{ + Excluded: []string{"process.command_args"}, + Included: []string{"process.*"}, + }, + // TODO: implement resource detectors + // Detectors: []ExperimentalResourceDetector{} + // }, + }, + SchemaUrl: ptr("https://opentelemetry.io/schemas/1.16.0"), + }, + TracerProvider: &TracerProviderJson{ + TracerConfiguratorDevelopment: &ExperimentalTracerConfigurator{ + DefaultConfig: &ExperimentalTracerConfig{ + Disabled: ptr(true), + }, + Tracers: []ExperimentalTracerMatcherAndConfig{ + { + Config: ptr(ExperimentalTracerConfig{ + Disabled: ptr(false), + }), + Name: ptr("io.opentelemetry.contrib.*"), + }, + }, + }, + + Limits: &SpanLimits{ + AttributeCountLimit: ptr(128), + AttributeValueLengthLimit: ptr(4096), + EventCountLimit: ptr(128), + EventAttributeCountLimit: ptr(128), + LinkCountLimit: ptr(128), + LinkAttributeCountLimit: ptr(128), + }, + Processors: []SpanProcessor{ + { + Batch: &BatchSpanProcessor{ + ExportTimeout: ptr(30000), + Exporter: SpanExporter{ + OTLPHttp: &OTLPHttpExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + Encoding: ptr(OTLPHttpEncodingProtobuf), + Endpoint: ptr("http://localhost:4318/v1/traces"), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + Timeout: ptr(10000), + }, + }, + MaxExportBatchSize: ptr(512), + MaxQueueSize: ptr(2048), + ScheduleDelay: ptr(5000), + }, + }, + { + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLPGrpc: &OTLPGrpcExporter{ + CertificateFile: ptr("/app/cert.pem"), + ClientCertificateFile: ptr("/app/cert.pem"), + ClientKeyFile: ptr("/app/cert.pem"), + Compression: ptr("gzip"), + Endpoint: ptr("http://localhost:4317"), + Headers: []NameStringValuePair{ + {Name: "api-key", Value: ptr("1234")}, + }, + HeadersList: ptr("api-key=1234"), + Timeout: ptr(10000), + Insecure: ptr(false), + }, + }, + }, + }, + { + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileExporter{ + OutputStream: ptr("file:///var/log/traces.jsonl"), + }, + }, + }, + }, + { + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + OTLPFileDevelopment: &ExperimentalOTLPFileExporter{ + OutputStream: ptr("stdout"), + }, + }, + }, + }, + { + Batch: &BatchSpanProcessor{ + Exporter: SpanExporter{ + Zipkin: &ZipkinSpanExporter{ + Endpoint: ptr("http://localhost:9411/api/v2/spans"), + Timeout: ptr(10000), + }, + }, + }, + }, + { + Simple: &SimpleSpanProcessor{ + Exporter: SpanExporter{ + Console: ConsoleExporter{}, + }, + }, + }, + }, + Sampler: &Sampler{ + ParentBased: &ParentBasedSampler{ + LocalParentNotSampled: &Sampler{ + AlwaysOff: AlwaysOffSampler{}, + }, + LocalParentSampled: &Sampler{ + AlwaysOn: AlwaysOnSampler{}, + }, + RemoteParentNotSampled: &Sampler{ + AlwaysOff: AlwaysOffSampler{}, + }, + RemoteParentSampled: &Sampler{ + AlwaysOn: AlwaysOnSampler{}, + }, + Root: &Sampler{ + TraceIDRatioBased: &TraceIDRatioBasedSampler{ + Ratio: ptr(0.0001), + }, + }, + }, + }, + }, +} + +var v100OpenTelemetryConfigEnvParsing = OpenTelemetryConfiguration{ + Disabled: ptr(false), + FileFormat: "1.0", + LogLevel: ptr("info"), + AttributeLimits: &AttributeLimits{ + AttributeCountLimit: ptr(128), + AttributeValueLengthLimit: ptr(4096), + }, + Resource: &ResourceJson{ + Attributes: []AttributeNameValue{ + {Name: "service.name", Value: "unknown_service"}, + {Name: "string_key", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "bool_key", Type: &AttributeType{Value: "bool"}, Value: true}, + {Name: "int_key", Type: &AttributeType{Value: "int"}, Value: 1}, + {Name: "double_key", Type: &AttributeType{Value: "double"}, Value: 1.1}, + {Name: "string_array_key", Type: &AttributeType{Value: "string_array"}, Value: []any{"value1", "value2"}}, + {Name: "bool_array_key", Type: &AttributeType{Value: "bool_array"}, Value: []any{true, false}}, + {Name: "int_array_key", Type: &AttributeType{Value: "int_array"}, Value: []any{1, 2}}, + {Name: "double_array_key", Type: &AttributeType{Value: "double_array"}, Value: []any{1.1, 2.2}}, + {Name: "string_value", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "bool_value", Type: &AttributeType{Value: "bool"}, Value: true}, + {Name: "int_value", Type: &AttributeType{Value: "int"}, Value: 1}, + {Name: "float_value", Type: &AttributeType{Value: "double"}, Value: 1.1}, + {Name: "hex_value", Type: &AttributeType{Value: "int"}, Value: int(48879)}, + {Name: "quoted_string_value", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "quoted_bool_value", Type: &AttributeType{Value: "string"}, Value: "true"}, + {Name: "quoted_int_value", Type: &AttributeType{Value: "string"}, Value: "1"}, + {Name: "quoted_float_value", Type: &AttributeType{Value: "string"}, Value: "1.1"}, + {Name: "quoted_hex_value", Type: &AttributeType{Value: "string"}, Value: "0xbeef"}, + {Name: "alternative_env_syntax", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "invalid_map_value", Type: &AttributeType{Value: "string"}, Value: "value\nkey:value"}, + {Name: "multiple_references_inject", Type: &AttributeType{Value: "string"}, Value: "foo value 1.1"}, + {Name: "undefined_key", Type: &AttributeType{Value: "string"}, Value: nil}, + {Name: "undefined_key_fallback", Type: &AttributeType{Value: "string"}, Value: "fallback"}, + {Name: "env_var_in_key", Type: &AttributeType{Value: "string"}, Value: "value"}, + {Name: "replace_me", Type: &AttributeType{Value: "string"}, Value: "${DO_NOT_REPLACE_ME}"}, + {Name: "undefined_defaults_to_var", Type: &AttributeType{Value: "string"}, Value: "${STRING_VALUE}"}, + {Name: "escaped_does_not_substitute", Type: &AttributeType{Value: "string"}, Value: "${STRING_VALUE}"}, + {Name: "escaped_does_not_substitute_fallback", Type: &AttributeType{Value: "string"}, Value: "${STRING_VALUE:-fallback}"}, + {Name: "escaped_and_substituted_fallback", Type: &AttributeType{Value: "string"}, Value: "${STRING_VALUE:-value}"}, + {Name: "escaped_and_substituted", Type: &AttributeType{Value: "string"}, Value: "$value"}, + {Name: "multiple_escaped_and_not_substituted", Type: &AttributeType{Value: "string"}, Value: "$${STRING_VALUE}"}, + {Name: "undefined_key_with_escape_sequence_in_fallback", Type: &AttributeType{Value: "string"}, Value: "${UNDEFINED_KEY}"}, + {Name: "value_with_escape", Type: &AttributeType{Value: "string"}, Value: "value$$"}, + {Name: "escape_sequence", Type: &AttributeType{Value: "string"}, Value: "a $ b"}, + {Name: "no_escape_sequence", Type: &AttributeType{Value: "string"}, Value: "a $ b"}, + }, + AttributesList: ptr("service.namespace=my-namespace,service.version=1.0.0"), + // Detectors: &Detectors{ + // Attributes: &DetectorsAttributes{ + // Excluded: []string{"process.command_args"}, + // Included: []string{"process.*"}, + // }, + // }, + SchemaUrl: ptr("https://opentelemetry.io/schemas/1.16.0"), + }, +} + +func TestParseFiles(t *testing.T) { + tests := []struct { + name string + input string + wantErr error + wantType *OpenTelemetryConfiguration + }{ + { + name: "invalid nil name", + input: "v1.0.0_invalid_nil_name", + wantErr: newErrRequired(&NameStringValuePair{}, "name"), + wantType: &OpenTelemetryConfiguration{}, + }, + { + name: "invalid nil value", + input: "v1.0.0_invalid_nil_value", + wantErr: newErrRequired(&NameStringValuePair{}, "value"), + wantType: &OpenTelemetryConfiguration{}, + }, + { + name: "valid v0.2 config", + input: "v0.2", + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + wantType: &OpenTelemetryConfiguration{}, + }, + { + name: "valid v0.3 config", + input: "v0.3", + wantErr: newErrUnmarshal(&TextMapPropagator{}), + wantType: &OpenTelemetryConfiguration{}, + }, + { + name: "valid v1.0.0 config", + input: "v1.0.0", + wantType: &v10OpenTelemetryConfig, + }, + } + + for _, tt := range tests { + t.Run("yaml:"+tt.name, func(t *testing.T) { + b, err := os.ReadFile(filepath.Join("testdata", fmt.Sprintf("%s.yaml", tt.input))) + require.NoError(t, err) + + got, err := ParseYAML(b) + require.ErrorIs(t, err, tt.wantErr) + if tt.wantErr == nil { + assert.Equal(t, tt.wantType, got) + } + }) + t.Run("json: "+tt.name, func(t *testing.T) { + b, err := os.ReadFile(filepath.Join("testdata", fmt.Sprintf("%s.json", tt.input))) + require.NoError(t, err) + + var got OpenTelemetryConfiguration + err = json.Unmarshal(b, &got) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantType, &got) + }) + } +} + +func TestUnmarshalOpenTelemetryConfiguration(t *testing.T) { + tests := []struct { + name string + jsonConfig []byte + yamlConfig []byte + wantErr error + wantType OpenTelemetryConfiguration + }{ + { + name: "valid defaults config", + jsonConfig: []byte(`{"file_format": "1.0"}`), + yamlConfig: []byte("file_format: 1.0"), + wantType: OpenTelemetryConfiguration{ + Disabled: ptr(false), + FileFormat: "1.0", + LogLevel: ptr("info"), + }, + }, + { + name: "invalid config missing required file_format", + jsonConfig: []byte(`{"disabled": false}`), + yamlConfig: []byte("disabled: false"), + wantErr: newErrRequired(&OpenTelemetryConfiguration{}, "file_format"), + }, + { + name: "file_format invalid", + jsonConfig: []byte(`{"file_format":[], "disabled": false}`), + yamlConfig: []byte("file_format: []\ndisabled: false"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "invalid config", + jsonConfig: []byte(`{"file_format": "yaml", "disabled": "notabool"}`), + yamlConfig: []byte("file_format: []\ndisabled: notabool"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "invalid data", + jsonConfig: []byte(`{:2000}`), + yamlConfig: []byte("disabled: []\nconsole: {}\nfile_format: str"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "resource invalid", + jsonConfig: []byte(`{"resource":[], "file_format": "1.0"}`), + yamlConfig: []byte("resource: []\nfile_format: 1.0"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "attribute_limits invalid", + jsonConfig: []byte(`{"attribute_limits":[], "file_format": "1.0"}`), + yamlConfig: []byte("attribute_limits: []\nfile_format: 1.0"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "instrumentation invalid", + jsonConfig: []byte(`{"instrumentation/development":[], "file_format": "1.0"}`), + yamlConfig: []byte("instrumentation/development: []\nfile_format: 1.0"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + { + name: "log_level invalid", + jsonConfig: []byte(`{"log_level":[], "file_format": "1.0"}`), + yamlConfig: []byte("log_level: []\nfile_format: 1.0"), + wantErr: newErrUnmarshal(&OpenTelemetryConfiguration{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := OpenTelemetryConfiguration{} + err := got.UnmarshalJSON(tt.jsonConfig) + assert.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantType, got) + + got = OpenTelemetryConfiguration{} + err = yaml.Unmarshal(tt.yamlConfig, &got) + assert.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantType, got) + }) + } +} + func TestUnmarshalBatchSpanProcessor(t *testing.T) { for _, tt := range []struct { name string @@ -440,6 +1263,49 @@ func TestUnmarshalBatchSpanProcessor(t *testing.T) { } } +func TestParseYAMLWithEnvironmentVariables(t *testing.T) { + tests := []struct { + name string + input string + wantErr error + wantType any + }{ + { + name: "valid v1.0.0 config with env vars", + input: "v1.0.0_env_var.yaml", + wantType: &v100OpenTelemetryConfigEnvParsing, + }, + } + + t.Setenv("OTEL_SDK_DISABLED", "false") + t.Setenv("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "4096") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf") + t.Setenv("STRING_VALUE", "value") + t.Setenv("BOOL_VALUE", "true") + t.Setenv("INT_VALUE", "1") + t.Setenv("FLOAT_VALUE", "1.1") + t.Setenv("HEX_VALUE", "0xbeef") // A valid integer value (i.e. 3735928559) written in hexadecimal + t.Setenv("INVALID_MAP_VALUE", "value\\nkey:value") // An invalid attempt to inject a map key into the YAML + t.Setenv("ENV_VAR_IN_KEY", "env_var_in_key") // An env var in key + t.Setenv("DO_NOT_REPLACE_ME", "Never use this value") // An unused environment variable + t.Setenv("REPLACE_ME", "${DO_NOT_REPLACE_ME}") // A valid replacement text, used verbatim, not replaced with "Never use this value" + t.Setenv("VALUE_WITH_ESCAPE", "value$$") // A valid replacement text, used verbatim, not replaced with "Never use this value" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := os.ReadFile(filepath.Join("testdata", tt.input)) + require.NoError(t, err) + + got, err := ParseYAML(b) + if tt.wantErr != nil { + require.Equal(t, tt.wantErr.Error(), err.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantType, got) + } + }) + } +} + func TestUnmarshalPeriodicMetricReader(t *testing.T) { for _, tt := range []struct { name string diff --git a/otelconf/config_yaml.go b/otelconf/config_yaml.go index 4e8bab5b282..e116dab207c 100644 --- a/otelconf/config_yaml.go +++ b/otelconf/config_yaml.go @@ -41,6 +41,74 @@ func (j *PushMetricExporter) UnmarshalYAML(node *yaml.Node) error { return nil } +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *OpenTelemetryConfiguration) UnmarshalYAML(node *yaml.Node) error { + if !hasYAMLMapKey(node, "file_format") { + return newErrRequired(j, "file_format") + } + type Plain OpenTelemetryConfiguration + type shadow struct { + Plain + LogLevel *string `yaml:"log_level,omitempty"` + AttributeLimits *AttributeLimits `yaml:"attribute_limits,omitempty"` + Disabled *bool `yaml:"disabled,omitempty"` + FileFormat string `yaml:"file_format"` + LoggerProvider *LoggerProviderJson `yaml:"logger_provider,omitempty"` + MeterProvider *MeterProviderJson `yaml:"meter_provider,omitempty"` + TracerProvider *TracerProviderJson `yaml:"tracer_provider,omitempty"` + Propagator *PropagatorJson `yaml:"propagator,omitempty"` + Resource *ResourceJson `yaml:"resource,omitempty"` + InstrumentationDevelopment *InstrumentationJson `yaml:"instrumentation/development"` + } + var sh shadow + + if err := node.Decode(&sh); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + + if sh.AttributeLimits != nil { + sh.Plain.AttributeLimits = sh.AttributeLimits + } + + sh.Plain.FileFormat = sh.FileFormat + if sh.Disabled != nil { + sh.Plain.Disabled = sh.Disabled + } else { + // Configure the log level of the internal logger used by the SDK. + // If omitted, info is used. + sh.Plain.Disabled = ptr(false) + } + if sh.LoggerProvider != nil { + sh.Plain.LoggerProvider = sh.LoggerProvider + } + if sh.MeterProvider != nil { + sh.Plain.MeterProvider = sh.MeterProvider + } + if sh.TracerProvider != nil { + sh.Plain.TracerProvider = sh.TracerProvider + } + if sh.Propagator != nil { + sh.Plain.Propagator = sh.Propagator + } + if sh.Resource != nil { + sh.Plain.Resource = sh.Resource + } + if sh.InstrumentationDevelopment != nil { + sh.Plain.InstrumentationDevelopment = sh.InstrumentationDevelopment + } + + if sh.LogLevel != nil { + sh.Plain.LogLevel = sh.LogLevel + } else { + // Configure the log level of the internal logger used by the SDK. + // If omitted, info is used. + sh.Plain.LogLevel = ptr("info") + } + + *j = OpenTelemetryConfiguration(sh.Plain) + return nil +} + // UnmarshalYAML implements yaml.Unmarshaler. func (j *SpanExporter) UnmarshalYAML(node *yaml.Node) error { type Plain SpanExporter @@ -123,6 +191,38 @@ func (j *BatchLogRecordProcessor) UnmarshalYAML(node *yaml.Node) error { return nil } +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *Sampler) UnmarshalYAML(node *yaml.Node) error { + var raw map[string]any + if err := node.Decode(&raw); err != nil { + return err + } + type Plain Sampler + var plain Plain + if err := node.Decode(&plain); err != nil { + return err + } + unmarshalSamplerTypes(raw, (*Sampler)(&plain)) + *j = Sampler(plain) + return nil +} + +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *MetricProducer) UnmarshalYAML(node *yaml.Node) error { + var raw map[string]any + if err := node.Decode(&raw); err != nil { + return err + } + type Plain MetricProducer + var plain Plain + if err := node.Decode(&plain); err != nil { + return err + } + unmarshalMetricProducer(raw, (*MetricProducer)(&plain)) + *j = MetricProducer(plain) + return nil +} + // UnmarshalYAML implements yaml.Unmarshaler. func (j *BatchSpanProcessor) UnmarshalYAML(node *yaml.Node) error { if !hasYAMLMapKey(node, "exporter") { @@ -443,3 +543,14 @@ func (j *PullMetricReader) UnmarshalYAML(node *yaml.Node) error { *j = PullMetricReader(plain) return nil } + +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *ExperimentalLanguageSpecificInstrumentation) UnmarshalYAML(unmarshal func(any) error) error { + var raw map[string]any + if err := unmarshal(&raw); err != nil { + return err + } + + *j = raw + return nil +} diff --git a/otelconf/example_test.go b/otelconf/example_test.go index 8cfdc0c39eb..0d9b67e67f6 100644 --- a/otelconf/example_test.go +++ b/otelconf/example_test.go @@ -12,11 +12,11 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/log/global" - otelconf "go.opentelemetry.io/contrib/otelconf/v0.3.0" + "go.opentelemetry.io/contrib/otelconf" ) func Example() { - b, err := os.ReadFile(filepath.Join("testdata", "v0.3.yaml")) + b, err := os.ReadFile(filepath.Join("testdata", "v1.0.0.yaml")) if err != nil { log.Fatal(err) } diff --git a/otelconf/fuzz_test.go b/otelconf/fuzz_test.go new file mode 100644 index 00000000000..e1c32b48b72 --- /dev/null +++ b/otelconf/fuzz_test.go @@ -0,0 +1,64 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelconf + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func FuzzJSON(f *testing.F) { + b, err := os.ReadFile(filepath.Join("testdata", "v1.0.0.json")) + require.NoError(f, err) + f.Add(b) + + f.Fuzz(func(t *testing.T, data []byte) { + t.Log("JSON:\n" + string(data)) + + var cfg OpenTelemetryConfiguration + err := json.Unmarshal(b, &cfg) + if err != nil { + return + } + + sdk, err := NewSDK(WithOpenTelemetryConfiguration(cfg)) + if err != nil { + return + } + + ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond) + defer cancel() + _ = sdk.Shutdown(ctx) + }) +} + +func FuzzYAML(f *testing.F) { + b, err := os.ReadFile(filepath.Join("testdata", "v1.0.0.yaml")) + require.NoError(f, err) + f.Add(b) + + f.Fuzz(func(t *testing.T, data []byte) { + t.Log("YAML:\n" + string(data)) + + cfg, err := ParseYAML(data) + if err != nil { + return + } + + sdk, err := NewSDK(WithOpenTelemetryConfiguration(*cfg)) + if err != nil { + return + } + + ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond) + defer cancel() + _ = sdk.Shutdown(ctx) + }) +} diff --git a/otelconf/log_test.go b/otelconf/log_test.go index 9bb6119ed7a..11f72146c96 100644 --- a/otelconf/log_test.go +++ b/otelconf/log_test.go @@ -713,33 +713,27 @@ func TestLoggerProviderOptions(t *testing.T) { require.NoError(t, err) res := resource.NewSchemaless(attribute.String("foo", "bar")) - // TODO: re-enable this once NewSDK is added - // sdk, err := NewSDK( - // WithOpenTelemetryConfiguration(cfg), - // WithLoggerProviderOptions(sdklog.WithProcessor(sdklog.NewSimpleProcessor(stdoutlogExporter))), - // WithLoggerProviderOptions(sdklog.WithResource(res)), - // ) - lp, shutdown, err := loggerProvider(configOptions{ - opentelemetryConfig: cfg, - loggerProviderOptions: []sdklog.LoggerProviderOption{sdklog.WithProcessor(sdklog.NewSimpleProcessor(stdoutlogExporter))}, - }, res) + sdk, err := NewSDK( + WithOpenTelemetryConfiguration(cfg), + WithLoggerProviderOptions(sdklog.WithProcessor(sdklog.NewSimpleProcessor(stdoutlogExporter))), + WithLoggerProviderOptions(sdklog.WithResource(res)), + ) require.NoError(t, err) defer func() { - assert.NoError(t, shutdown(t.Context())) + assert.NoError(t, sdk.Shutdown(t.Context())) }() // The exporter, which we passed in as an extra option to NewSDK, // should be wired up to the provider in addition to the // configuration-based OTLP exporter. - logger := lp.Logger("test") + logger := sdk.LoggerProvider().Logger("test") logger.Emit(t.Context(), log.Record{}) assert.NotZero(t, buf) assert.Equal(t, 1, calls) // Options provided by WithMeterProviderOptions may be overridden // by configuration, e.g. the resource is always defined via // configuration. - // TODO: re-enable this once NewSDK is added - // assert.NotContains(t, buf.String(), "foo") + assert.NotContains(t, buf.String(), "foo") } func Test_otlpGRPCLogExporter(t *testing.T) { diff --git a/otelconf/metric_test.go b/otelconf/metric_test.go index 0a4b623a2ab..0dd8d0b2315 100644 --- a/otelconf/metric_test.go +++ b/otelconf/metric_test.go @@ -139,19 +139,14 @@ func TestMeterProviderOptions(t *testing.T) { require.NoError(t, err) res := resource.NewSchemaless(attribute.String("foo", "bar")) - // TODO: re-enable this once NewSDK is added - // sdk, err := NewSDK( - // WithOpenTelemetryConfiguration(cfg), - // WithMeterProviderOptions(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(stdoutmetricExporter))), - // WithMeterProviderOptions(sdkmetric.WithResource(res)), - // ) - mp, shutdown, err := meterProvider(configOptions{ - opentelemetryConfig: cfg, - meterProviderOptions: []sdkmetric.Option{sdkmetric.WithReader(sdkmetric.NewPeriodicReader(stdoutmetricExporter))}, - }, res) + sdk, err := NewSDK( + WithOpenTelemetryConfiguration(cfg), + WithMeterProviderOptions(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(stdoutmetricExporter))), + WithMeterProviderOptions(sdkmetric.WithResource(res)), + ) require.NoError(t, err) defer func() { - assert.NoError(t, shutdown(t.Context())) + assert.NoError(t, sdk.Shutdown(t.Context())) // The exporter, which we passed in as an extra option to NewSDK, // should be wired up to the provider in addition to the // configuration-based OTLP exporter. @@ -161,11 +156,10 @@ func TestMeterProviderOptions(t *testing.T) { // Options provided by WithMeterProviderOptions may be overridden // by configuration, e.g. the resource is always defined via // configuration. - // TODO: re-enable this once NewSDK is added - // assert.NotContains(t, buf.String(), "foo") + assert.NotContains(t, buf.String(), "foo") }() - counter, _ := mp.Meter("test").Int64Counter("counter") + counter, _ := sdk.MeterProvider().Meter("test").Int64Counter("counter") counter.Add(t.Context(), 1) } diff --git a/otelconf/testdata/v1.0.0.json b/otelconf/testdata/v1.0.0.json index f85579dd4b3..fe6a130b3f4 100644 --- a/otelconf/testdata/v1.0.0.json +++ b/otelconf/testdata/v1.0.0.json @@ -515,21 +515,7 @@ "excluded": [ "process.command_args" ] - }, - "detectors": [ - { - "container": null - }, - { - "host": null - }, - { - "process": null - }, - { - "service": null - } - ] + } }, "schema_url": "https://opentelemetry.io/schemas/1.16.0" }, diff --git a/otelconf/testdata/v1.0.0.yaml b/otelconf/testdata/v1.0.0.yaml index 2447b8a71e9..2ebbfd0ee38 100644 --- a/otelconf/testdata/v1.0.0.yaml +++ b/otelconf/testdata/v1.0.0.yaml @@ -841,15 +841,16 @@ resource: # Configure resource detectors. # Resource detector names are dependent on the SDK language ecosystem. Please consult documentation for each respective language. # If omitted or null, no resource detectors are enabled. - detectors: - - # Enable the container resource detector, which populates container.* attributes. - container: - - # Enable the host resource detector, which populates host.* and os.* attributes. - host: - - # Enable the process resource detector, which populates process.* attributes. - process: - - # Enable the service detector, which populates service.name based on the OTEL_SERVICE_NAME environment variable and service.instance.id. - service: + # TODO: implement resource detectors + # detectors: + # - # Enable the container resource detector, which populates container.* attributes. + # container: + # - # Enable the host resource detector, which populates host.* and os.* attributes. + # host: + # - # Enable the process resource detector, which populates process.* attributes. + # process: + # - # Enable the service detector, which populates service.name based on the OTEL_SERVICE_NAME environment variable and service.instance.id. + # service: # Configure resource schema URL. # If omitted or null, no schema URL is used. schema_url: https://opentelemetry.io/schemas/1.16.0 diff --git a/otelconf/testdata/v1.0.0_env_var.yaml b/otelconf/testdata/v1.0.0_env_var.yaml index f3c24f83e9e..2f520a2c6db 100644 --- a/otelconf/testdata/v1.0.0_env_var.yaml +++ b/otelconf/testdata/v1.0.0_env_var.yaml @@ -1,3 +1,4 @@ +file_format: "1.0" disabled: ${OTEL_SDK_DISABLED} attribute_limits: attribute_value_length_limit: ${OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT} diff --git a/otelconf/trace.go b/otelconf/trace.go index 54638a742be..c7e57d2867c 100644 --- a/otelconf/trace.go +++ b/otelconf/trace.go @@ -30,7 +30,7 @@ func tracerProvider(cfg configOptions, res *resource.Resource) (trace.TracerProv } provider, ok := cfg.opentelemetryConfig.TracerProvider.(*TracerProviderJson) if !ok { - return noop.NewTracerProvider(), noopShutdown, newErrInvalid("invalid tracer provider") + return noop.NewTracerProvider(), noopShutdown, newErrInvalid("tracer_provider") } opts := append(cfg.tracerProviderOptions, sdktrace.WithResource(res)) diff --git a/otelconf/trace_test.go b/otelconf/trace_test.go index 760a5d2fe19..5ebd34a76c9 100644 --- a/otelconf/trace_test.go +++ b/otelconf/trace_test.go @@ -157,26 +157,20 @@ func TestTracerProviderOptions(t *testing.T) { require.NoError(t, err) res := resource.NewSchemaless(attribute.String("foo", "bar")) - // TODO: re-enable this once NewSDK is added - // sdk, err := NewSDK( - // WithOpenTelemetryConfiguration(cfg), - // WithTracerProviderOptions(sdktrace.WithSyncer(stdouttraceExporter)), - // WithTracerProviderOptions(sdktrace.WithResource(res)), - // ) - tp, shutdown, err := tracerProvider(configOptions{ - ctx: t.Context(), - opentelemetryConfig: cfg, - tracerProviderOptions: []sdktrace.TracerProviderOption{sdktrace.WithSyncer(stdouttraceExporter)}, - }, res) + sdk, err := NewSDK( + WithOpenTelemetryConfiguration(cfg), + WithTracerProviderOptions(sdktrace.WithSyncer(stdouttraceExporter)), + WithTracerProviderOptions(sdktrace.WithResource(res)), + ) require.NoError(t, err) defer func() { - assert.NoError(t, shutdown(t.Context())) + assert.NoError(t, sdk.Shutdown(t.Context())) }() // The exporter, which we passed in as an extra option to NewSDK, // should be wired up to the provider in addition to the // configuration-based OTLP exporter. - tracer := tp.Tracer("test") + tracer := sdk.TracerProvider().Tracer("test") _, span := tracer.Start(t.Context(), "span") span.End() assert.NotZero(t, buf) @@ -184,8 +178,7 @@ func TestTracerProviderOptions(t *testing.T) { // Options provided by WithMeterProviderOptions may be overridden // by configuration, e.g. the resource is always defined via // configuration. - // TODO: re-enable this once NewSDK is added - // assert.NotContains(t, buf.String(), "foo") + assert.NotContains(t, buf.String(), "foo") } func TestSpanProcessor(t *testing.T) {