diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c6c5535ea..258dd7faf2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Support `OTEL_EXPORTER_OTLP_LOGS_INSECURE` and `OTEL_EXPORTER_OTLP_INSECURE` environments in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#5739) +- The `WithResource` option for `NewMeterProvider` now merges the provided resources with the ones from environment variables. (#5773) +- The `WithResource` option for `NewLoggerProvider` now merges the provided resources with the ones from environment variables. (#5773) ### Fixed diff --git a/sdk/log/go.mod b/sdk/log/go.mod index 9aac33fbb2e..2af4e409dea 100644 --- a/sdk/log/go.mod +++ b/sdk/log/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/go-logr/logr v1.4.2 github.com/go-logr/stdr v1.2.2 + github.com/google/go-cmp v0.6.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/log v0.5.0 diff --git a/sdk/log/provider.go b/sdk/log/provider.go index 9d16d801898..14084ed99a8 100644 --- a/sdk/log/provider.go +++ b/sdk/log/provider.go @@ -9,6 +9,7 @@ import ( "sync" "sync/atomic" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/embedded" @@ -196,7 +197,11 @@ func (fn loggerProviderOptionFunc) apply(c providerConfig) providerConfig { // go.opentelemetry.io/otel/sdk/resource package will be used. func WithResource(res *resource.Resource) LoggerProviderOption { return loggerProviderOptionFunc(func(cfg providerConfig) providerConfig { - cfg.resource = res + var err error + cfg.resource, err = resource.Merge(resource.Environment(), res) + if err != nil { + otel.Handle(err) + } return cfg }) } diff --git a/sdk/log/provider_test.go b/sdk/log/provider_test.go index 4dd9cf327e9..cbe355c236b 100644 --- a/sdk/log/provider_test.go +++ b/sdk/log/provider_test.go @@ -12,6 +12,7 @@ import ( "github.com/go-logr/logr" "github.com/go-logr/logr/testr" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,10 +21,13 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/noop" + ottest "go.opentelemetry.io/otel/sdk/internal/internaltest" "go.opentelemetry.io/otel/sdk/log/internal/x" "go.opentelemetry.io/otel/sdk/resource" ) +const envVarResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES" + type processor struct { Name string Err error @@ -172,6 +176,65 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { } } +func mergeResource(t *testing.T, r1, r2 *resource.Resource) *resource.Resource { + r, err := resource.Merge(r1, r2) + assert.NoError(t, err) + return r +} + +func TestWithResource(t *testing.T) { + store, err := ottest.SetEnvVariables(map[string]string{ + envVarResourceAttributes: "key=value,rk5=7", + }) + require.NoError(t, err) + defer func() { require.NoError(t, store.Restore()) }() + + cases := []struct { + name string + options []LoggerProviderOption + want *resource.Resource + msg string + }{ + { + name: "explicitly empty resource", + options: []LoggerProviderOption{WithResource(resource.Empty())}, + want: resource.Environment(), + }, + { + name: "uses default if no resource option", + options: []LoggerProviderOption{}, + want: resource.Default(), + }, + { + name: "explicit resource", + options: []LoggerProviderOption{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5))), + }, + { + name: "last resource wins", + options: []LoggerProviderOption{ + WithResource(resource.NewSchemaless(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))), + WithResource(resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), + }, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), + }, + { + name: "overlapping attributes with environment resource", + options: []LoggerProviderOption{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10))), + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := newProviderConfig(tc.options).resource + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("WithResource:\n -got +want %s", diff) + } + }) + } +} + func TestLoggerProviderConcurrentSafe(t *testing.T) { const goRoutineN = 10 diff --git a/sdk/metric/config.go b/sdk/metric/config.go index bbe7bf671fd..544275a1146 100644 --- a/sdk/metric/config.go +++ b/sdk/metric/config.go @@ -8,6 +8,7 @@ import ( "fmt" "sync" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/sdk/resource" ) @@ -103,7 +104,11 @@ func (o optionFunc) apply(conf config) config { // go.opentelemetry.io/otel/sdk/resource package will be used. func WithResource(res *resource.Resource) Option { return optionFunc(func(conf config) config { - conf.res = res + var err error + conf.res, err = resource.Merge(resource.Environment(), res) + if err != nil { + otel.Handle(err) + } return conf }) } diff --git a/sdk/metric/config_test.go b/sdk/metric/config_test.go index d51ce886097..032337cefe7 100644 --- a/sdk/metric/config_test.go +++ b/sdk/metric/config_test.go @@ -8,9 +8,12 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + ottest "go.opentelemetry.io/otel/sdk/internal/internaltest" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/resource" ) @@ -25,6 +28,8 @@ type reader struct { shutdownFunc func(context.Context) error } +const envVarResourceAttributes = "OTEL_RESOURCE_ATTRIBUTES" + var _ Reader = (*reader)(nil) func (r *reader) aggregation(kind InstrumentKind) Aggregation { // nolint:revive // import-shadow for method scoped by type. @@ -108,10 +113,63 @@ func TestUnifyMultiError(t *testing.T) { assert.Equal(t, unify(funcs)(context.Background()), target) } +func mergeResource(t *testing.T, r1, r2 *resource.Resource) *resource.Resource { + r, err := resource.Merge(r1, r2) + assert.NoError(t, err) + return r +} + func TestWithResource(t *testing.T) { - res := resource.NewSchemaless() - c := newConfig([]Option{WithResource(res)}) - assert.Same(t, res, c.res) + store, err := ottest.SetEnvVariables(map[string]string{ + envVarResourceAttributes: "key=value,rk5=7", + }) + require.NoError(t, err) + defer func() { require.NoError(t, store.Restore()) }() + + cases := []struct { + name string + options []Option + want *resource.Resource + msg string + }{ + { + name: "explicitly empty resource", + options: []Option{WithResource(resource.Empty())}, + want: resource.Environment(), + }, + { + name: "uses default if no resource option", + options: []Option{}, + want: resource.Default(), + }, + { + name: "explicit resource", + options: []Option{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk2", 5))), + }, + { + name: "last resource wins", + options: []Option{ + WithResource(resource.NewSchemaless(attribute.String("rk1", "vk1"), attribute.Int64("rk2", 5))), + WithResource(resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), + }, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk3", "rv3"), attribute.Int64("rk4", 10))), + }, + { + name: "overlapping attributes with environment resource", + options: []Option{WithResource(resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10)))}, + want: mergeResource(t, resource.Environment(), resource.NewSchemaless(attribute.String("rk1", "rv1"), attribute.Int64("rk5", 10))), + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := newConfig(tc.options).res + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("WithResource:\n -got +want %s", diff) + } + }) + } } func TestWithReader(t *testing.T) { diff --git a/sdk/metric/go.mod b/sdk/metric/go.mod index bc38c6d6beb..3a1bd2e2a86 100644 --- a/sdk/metric/go.mod +++ b/sdk/metric/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/go-logr/logr v1.4.2 github.com/go-logr/stdr v1.2.2 + github.com/google/go-cmp v0.6.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/metric v1.29.0