diff --git a/CHANGELOG.md b/CHANGELOG.md index b2bf1e8b372..65f49cdc7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - 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) - Support `db.client.operation.duration` metric for `go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo`. (#7983) - Add support for `OTEL_EXPERIMENTAL_CONFIG_FILE` via the `NewSDK` function in `go.opentelemetry.io/contrib/otelconf` (#8106) +- Add support for `container`, `host`, `process` resource detectors in `go.opentelemetry.io/contrib/otelconf` (#8180) ### Changed diff --git a/otelconf/config_json.go b/otelconf/config_json.go index 6e29871047c..08376162b06 100644 --- a/otelconf/config_json.go +++ b/otelconf/config_json.go @@ -122,6 +122,121 @@ func (j *TraceContextPropagator) UnmarshalJSON(b []byte) error { return nil } +// UnmarshalJSON implements json.Unmarshaler. +func (j *ExperimentalContainerResourceDetector) UnmarshalJSON(b []byte) error { + type plain ExperimentalContainerResourceDetector + var p plain + if err := json.Unmarshal(b, &p); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + // If key is present (even if empty object), ensure non-nil value. + if p == nil { + *j = ExperimentalContainerResourceDetector{} + } else { + *j = ExperimentalContainerResourceDetector(p) + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ExperimentalHostResourceDetector) UnmarshalJSON(b []byte) error { + type plain ExperimentalHostResourceDetector + var p plain + if err := json.Unmarshal(b, &p); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + // If key is present (even if empty object), ensure non-nil value. + if p == nil { + *j = ExperimentalHostResourceDetector{} + } else { + *j = ExperimentalHostResourceDetector(p) + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ExperimentalProcessResourceDetector) UnmarshalJSON(b []byte) error { + type plain ExperimentalProcessResourceDetector + var p plain + if err := json.Unmarshal(b, &p); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + // If key is present (even if empty object), ensure non-nil value. + if p == nil { + *j = ExperimentalProcessResourceDetector{} + } else { + *j = ExperimentalProcessResourceDetector(p) + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ExperimentalServiceResourceDetector) UnmarshalJSON(b []byte) error { + type plain ExperimentalServiceResourceDetector + var p plain + if err := json.Unmarshal(b, &p); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + // If key is present (even if empty object), ensure non-nil value. + if p == nil { + *j = ExperimentalServiceResourceDetector{} + } else { + *j = ExperimentalServiceResourceDetector(p) + } + return nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (j *ExperimentalResourceDetector) UnmarshalJSON(b []byte) error { + // Use a shadow struct with a RawMessage field to detect key presence. + type Plain ExperimentalResourceDetector + type shadow struct { + Plain + Container json.RawMessage `json:"container"` + Host json.RawMessage `json:"host"` + Process json.RawMessage `json:"process"` + Service json.RawMessage `json:"service"` + } + var sh shadow + if err := json.Unmarshal(b, &sh); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + + if sh.Container != nil { + var c ExperimentalContainerResourceDetector + if err := json.Unmarshal(sh.Container, &c); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Container = c + } + + if sh.Host != nil { + var c ExperimentalHostResourceDetector + if err := json.Unmarshal(sh.Host, &c); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Host = c + } + + if sh.Process != nil { + var c ExperimentalProcessResourceDetector + if err := json.Unmarshal(sh.Process, &c); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Process = c + } + + if sh.Service != nil { + var c ExperimentalServiceResourceDetector + if err := json.Unmarshal(sh.Service, &c); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + sh.Plain.Service = c + } + *j = ExperimentalResourceDetector(sh.Plain) + return nil +} + // UnmarshalJSON implements json.Unmarshaler. func (j *PushMetricExporter) UnmarshalJSON(b []byte) error { // Use a shadow struct with a RawMessage field to detect key presence. diff --git a/otelconf/config_test.go b/otelconf/config_test.go index 78a61eef0be..fc2b1c424fe 100644 --- a/otelconf/config_test.go +++ b/otelconf/config_test.go @@ -853,11 +853,13 @@ var v10OpenTelemetryConfig = OpenTelemetryConfiguration{ Excluded: []string{"process.command_args"}, Included: []string{"process.*"}, }, - // TODO: implement resource detectors https://github.com/open-telemetry/opentelemetry-go-contrib/issues/7252 - // Detectors: []ExperimentalResourceDetector{} - // }, + Detectors: []ExperimentalResourceDetector{ + {Container: ExperimentalContainerResourceDetector{}}, + {Host: ExperimentalHostResourceDetector{}}, + {Process: ExperimentalProcessResourceDetector{}}, + {Service: ExperimentalServiceResourceDetector{}}, + }, }, - SchemaUrl: ptr("https://opentelemetry.io/schemas/1.16.0"), }, TracerProvider: &TracerProviderJson{ TracerConfiguratorDevelopment: &ExperimentalTracerConfigurator{ @@ -2350,3 +2352,124 @@ func TestUnmarshalPullMetricReader(t *testing.T) { }) } } + +func TestUnmarshalResourceJson(t *testing.T) { + for _, tt := range []struct { + name string + yamlConfig []byte + jsonConfig []byte + wantErrT error + wantResource ResourceJson + }{ + { + name: "valid with all detectors", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"container": null},{"host": null},{"process": null},{"service": null}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - container:\n - host:\n - process:\n - service:"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + { + Container: ExperimentalContainerResourceDetector{}, + }, + { + Host: ExperimentalHostResourceDetector{}, + }, + { + Process: ExperimentalProcessResourceDetector{}, + }, + { + Service: ExperimentalServiceResourceDetector{}, + }, + }, + }, + }, + }, + { + name: "valid non-nil with all detectors", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"container": {}},{"host": {}},{"process": {}},{"service": {}}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - container: {}\n - host: {}\n - process: {}\n - service: {}"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + { + Container: ExperimentalContainerResourceDetector{}, + }, + { + Host: ExperimentalHostResourceDetector{}, + }, + { + Process: ExperimentalProcessResourceDetector{}, + }, + { + Service: ExperimentalServiceResourceDetector{}, + }, + }, + }, + }, + }, + { + name: "invalid container detector", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"container": 1}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - container: 1"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + {}, + }, + }, + }, + wantErrT: newErrUnmarshal(&ExperimentalResourceDetector{}), + }, + { + name: "invalid host detector", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"host": 1}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - host: 1"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + {}, + }, + }, + }, + wantErrT: newErrUnmarshal(&ExperimentalResourceDetector{}), + }, + { + name: "invalid service detector", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"service": 1}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - service: 1"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + {}, + }, + }, + }, + wantErrT: newErrUnmarshal(&ExperimentalResourceDetector{}), + }, + { + name: "invalid process detector", + jsonConfig: []byte(`{"detection/development": {"detectors": [{"process": 1}]}}`), + yamlConfig: []byte("detection/development:\n detectors:\n - process: 1"), + wantResource: ResourceJson{ + DetectionDevelopment: &ExperimentalResourceDetection{ + Detectors: []ExperimentalResourceDetector{ + {}, + }, + }, + }, + wantErrT: newErrUnmarshal(&ExperimentalResourceDetector{}), + }, + } { + t.Run(tt.name, func(t *testing.T) { + r := ResourceJson{} + err := json.Unmarshal(tt.jsonConfig, &r) + assert.ErrorIs(t, err, tt.wantErrT) + assert.Equal(t, tt.wantResource, r) + + r = ResourceJson{} + err = yaml.Unmarshal(tt.yamlConfig, &r) + assert.ErrorIs(t, err, tt.wantErrT) + assert.Equal(t, tt.wantResource, r) + }) + } +} diff --git a/otelconf/config_yaml.go b/otelconf/config_yaml.go index e116dab207c..8c251e83c24 100644 --- a/otelconf/config_yaml.go +++ b/otelconf/config_yaml.go @@ -26,6 +26,33 @@ func hasYAMLMapKey(node *yaml.Node, key string) bool { return false } +// UnmarshalYAML implements yaml.Unmarshaler. +func (j *ExperimentalResourceDetector) UnmarshalYAML(node *yaml.Node) error { + type Plain ExperimentalResourceDetector + var plain Plain + if err := node.Decode(&plain); err != nil { + return errors.Join(newErrUnmarshal(j), err) + } + // container can be nil, must check and set here + if hasYAMLMapKey(node, "container") && plain.Container == nil { + plain.Container = ExperimentalContainerResourceDetector{} + } + // host can be nil, must check and set here + if hasYAMLMapKey(node, "host") && plain.Host == nil { + plain.Host = ExperimentalHostResourceDetector{} + } + // process can be nil, must check and set here + if hasYAMLMapKey(node, "process") && plain.Process == nil { + plain.Process = ExperimentalProcessResourceDetector{} + } + // service can be nil, must check and set here + if hasYAMLMapKey(node, "service") && plain.Service == nil { + plain.Service = ExperimentalServiceResourceDetector{} + } + *j = ExperimentalResourceDetector(plain) + return nil +} + // UnmarshalYAML implements yaml.Unmarshaler. func (j *PushMetricExporter) UnmarshalYAML(node *yaml.Node) error { type Plain PushMetricExporter diff --git a/otelconf/go.mod b/otelconf/go.mod index c6e7ec1330f..781aa0bdd96 100644 --- a/otelconf/go.mod +++ b/otelconf/go.mod @@ -33,13 +33,13 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect diff --git a/otelconf/go.sum b/otelconf/go.sum index 3c035a97e8e..97059fea1de 100644 --- a/otelconf/go.sum +++ b/otelconf/go.sum @@ -4,8 +4,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -29,8 +29,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= diff --git a/otelconf/resource.go b/otelconf/resource.go index 5411bc691ab..f840027e053 100644 --- a/otelconf/resource.go +++ b/otelconf/resource.go @@ -4,12 +4,32 @@ package otelconf // import "go.opentelemetry.io/contrib/otelconf" import ( + "context" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/contrib/otelconf/internal/kv" ) +func resourceOpts(detectors []ExperimentalResourceDetector) []resource.Option { + opts := []resource.Option{} + for _, d := range detectors { + if d.Container != nil { + opts = append(opts, resource.WithContainer()) + } + if d.Host != nil { + opts = append(opts, resource.WithHost(), resource.WithHostID()) + } + if d.Process != nil { + opts = append(opts, resource.WithProcess()) + } + // TODO: implement service: + // Waiting on https://github.com/open-telemetry/opentelemetry-go/pull/7642 + } + return opts +} + func newResource(res OpenTelemetryConfigurationResource) (*resource.Resource, error) { if res == nil { return resource.Default(), nil @@ -25,8 +45,18 @@ func newResource(res OpenTelemetryConfigurationResource) (*resource.Resource, er attrs = append(attrs, kv.FromNameValue(v.Name, v.Value)) } - if r.SchemaUrl == nil { - return resource.NewSchemaless(attrs...), nil + var schema string + if r.SchemaUrl != nil { + schema = *r.SchemaUrl } - return resource.NewWithAttributes(*r.SchemaUrl, attrs...), nil + opts := []resource.Option{ + resource.WithAttributes(attrs...), + resource.WithSchemaURL(schema), + } + + if r.DetectionDevelopment != nil { + opts = append(opts, resourceOpts(r.DetectionDevelopment.Detectors)...) + } + + return resource.New(context.Background(), opts...) } diff --git a/otelconf/testdata/v1.0.0.json b/otelconf/testdata/v1.0.0.json index d9f81f6bd24..ef936596658 100644 --- a/otelconf/testdata/v1.0.0.json +++ b/otelconf/testdata/v1.0.0.json @@ -515,9 +515,14 @@ "excluded": [ "process.command_args" ] - } - }, - "schema_url": "https://opentelemetry.io/schemas/1.16.0" + }, + "detectors": [ + {"container": null}, + {"host": null}, + {"process": null}, + {"service": null} + ] + } }, "instrumentation/development": { "general": { diff --git a/otelconf/testdata/v1.0.0.yaml b/otelconf/testdata/v1.0.0.yaml index 1a7814ab7f0..cad6d14798f 100644 --- a/otelconf/testdata/v1.0.0.yaml +++ b/otelconf/testdata/v1.0.0.yaml @@ -841,19 +841,15 @@ 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. - # TODO: implement resource detectors https://github.com/open-telemetry/opentelemetry-go-contrib/issues/7252 - # 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 + 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 instrumentation. # This type is in development and subject to breaking changes in minor versions. instrumentation/development: