diff --git a/CHANGELOG.md b/CHANGELOG.md index 54cf81abc..e1bfbbaf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ OpenTelemetry Go Automatic Instrumentation adheres to [Semantic Versioning](http ## [Unreleased] +### Added + +- Add `WithResourceAttributes` `InstrumentationOption` to configure `Instrumentation` to add additional resource attributes. ([#522](https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/522)) + ### Changed - The instrumentation scope name for the `database/sql` instrumentation is now `go.opentelemtry.io/auto/database/sql`. (#507) diff --git a/instrumentation.go b/instrumentation.go index 192899e3b..57d867152 100644 --- a/instrumentation.go +++ b/instrumentation.go @@ -27,12 +27,14 @@ import ( "github.com/go-logr/logr" "github.com/go-logr/stdr" "github.com/go-logr/zapr" + "go.uber.org/zap" + "go.opentelemetry.io/contrib/exporters/autoexport" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" - "go.uber.org/zap" "go.opentelemetry.io/auto/internal/pkg/instrumentation" "go.opentelemetry.io/auto/internal/pkg/opentelemetry" @@ -163,10 +165,11 @@ type InstrumentationOption interface { } type instConfig struct { - sampler trace.Sampler - traceExp trace.SpanExporter - target process.TargetArgs - serviceName string + sampler trace.Sampler + traceExp trace.SpanExporter + target process.TargetArgs + serviceName string + additionalResAttrs []attribute.KeyValue } func newInstConfig(ctx context.Context, opts []InstrumentationOption) (instConfig, error) { @@ -239,14 +242,22 @@ func (c instConfig) res() *resource.Resource { runVer, runtime.GOOS, runtime.GOARCH, ) - return resource.NewWithAttributes( - semconv.SchemaURL, + attrs := []attribute.KeyValue{ semconv.ServiceNameKey.String(c.serviceName), semconv.TelemetrySDKLanguageGo, semconv.TelemetryAutoVersionKey.String(Version()), semconv.ProcessRuntimeName(runName), semconv.ProcessRuntimeVersion(runVer), semconv.ProcessRuntimeDescription(runDesc), + } + + if len(c.additionalResAttrs) > 0 { + attrs = append(attrs, c.additionalResAttrs...) + } + + return resource.NewWithAttributes( + semconv.SchemaURL, + attrs..., ) } @@ -342,24 +353,27 @@ func WithEnv() InstrumentationOption { c.traceExp, e = autoexport.NewSpanExporter(ctx) err = errors.Join(err, e) } - if v, ok := lookupServiceName(); ok { - c.serviceName = v + if name, attrs, ok := lookupResourceData(); ok { + c.serviceName = name + c.additionalResAttrs = append(c.additionalResAttrs, attrs...) } return c, err }) } -func lookupServiceName() (string, bool) { +func lookupResourceData() (string, []attribute.KeyValue, bool) { // Prioritize OTEL_SERVICE_NAME over OTEL_RESOURCE_ATTRIBUTES value. + svcName := "" if v, ok := lookupEnv(envServiceNameKey); ok { - return v, ok + svcName = v } v, ok := lookupEnv(envResourceAttrKey) if !ok { - return "", false + return svcName, nil, svcName != "" } + var attrs []attribute.KeyValue for _, keyval := range strings.Split(strings.TrimSpace(v), ",") { key, val, found := strings.Cut(keyval, "=") if !found { @@ -367,11 +381,17 @@ func lookupServiceName() (string, bool) { } key = strings.TrimSpace(key) if key == string(semconv.ServiceNameKey) { - return strings.TrimSpace(val), true + svcName = strings.TrimSpace(val) + } else { + attrs = append(attrs, attribute.String(key, strings.TrimSpace(val))) } } - return "", false + if svcName == "" { + return "", nil, false + } + + return svcName, attrs, true } // WithTraceExporter returns an [InstrumentationOption] that will configure an @@ -396,3 +416,12 @@ func WithSampler(sampler trace.Sampler) InstrumentationOption { return c, nil }) } + +// WithResourceAttributes returns an [InstrumentationOption] that will configure +// an [Instrumentation] to add the provided attributes to the OpenTelemetry resource. +func WithResourceAttributes(attrs ...attribute.KeyValue) InstrumentationOption { + return fnOpt(func(_ context.Context, c instConfig) (instConfig, error) { + c.additionalResAttrs = append(c.additionalResAttrs, attrs...) + return c, nil + }) +} diff --git a/instrumentation_test.go b/instrumentation_test.go index 50961f360..a9073ac2e 100644 --- a/instrumentation_test.go +++ b/instrumentation_test.go @@ -21,6 +21,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" ) @@ -127,6 +129,49 @@ func TestOptionPrecedence(t *testing.T) { }) } +func TestWithResourceAttributes(t *testing.T) { + t.Run("By Code", func(t *testing.T) { + attr1 := semconv.K8SContainerName("test_container_name") + attr2 := semconv.K8SPodName("test_pod_name") + attr3 := semconv.K8SNamespaceName("test_namespace_name") + + c, err := newInstConfig(context.Background(), []InstrumentationOption{WithResourceAttributes(attr1, attr2), WithResourceAttributes(attr3)}) + require.NoError(t, err) + assert.Equal(t, []attribute.KeyValue{attr1, attr2, attr3}, c.additionalResAttrs) + }) + + t.Run("By Env", func(t *testing.T) { + nameAttr := semconv.ServiceName("test_service") + attr2 := semconv.K8SPodName("test_pod_name") + attr3 := semconv.K8SNamespaceName("test_namespace_name") + + mockEnv(t, map[string]string{ + "OTEL_RESOURCE_ATTRIBUTES": fmt.Sprintf("%s=%s,%s=%s,%s=%s", nameAttr.Key, nameAttr.Value.AsString(), attr2.Key, attr2.Value.AsString(), attr3.Key, attr3.Value.AsString()), + }) + + c, err := newInstConfig(context.Background(), []InstrumentationOption{WithEnv()}) + require.NoError(t, err) + assert.Equal(t, nameAttr.Value.AsString(), c.serviceName) + assert.Equal(t, []attribute.KeyValue{attr2, attr3}, c.additionalResAttrs) + }) + + t.Run("By Code and Env", func(t *testing.T) { + nameAttr := semconv.ServiceName("test_service") + attr2 := semconv.K8SPodName("test_pod_name") + attr3 := semconv.K8SNamespaceName("test_namespace_name") + + mockEnv(t, map[string]string{ + "OTEL_RESOURCE_ATTRIBUTES": fmt.Sprintf("%s=%s,%s=%s", nameAttr.Key, nameAttr.Value.AsString(), attr2.Key, attr2.Value.AsString()), + }) + + // Use WithResourceAttributes to config the additional resource attributes + c, err := newInstConfig(context.Background(), []InstrumentationOption{WithEnv(), WithResourceAttributes(attr3)}) + require.NoError(t, err) + assert.Equal(t, nameAttr.Value.AsString(), c.serviceName) + assert.Equal(t, []attribute.KeyValue{attr2, attr3}, c.additionalResAttrs) + }) +} + func mockEnv(t *testing.T, env map[string]string) { orig := lookupEnv t.Cleanup(func() { lookupEnv = orig })