Repository: elastic/apm-agent-go
Pull request #1424 (2023-05-03 10:03:03)

Otel Module: handle resources
As I was digging through traces generated by this module, I would have wanted to see attributes set by it to identify where the traces were coming from. But I only was able to know the agent was sending them.

So this adds support for resources, as supported in the official OpenTelemetry SDK, which allows setting custom attributes on all spans.
By default, the following attributes are set:

* `service.name`: `unknown_service:<executable name>`
* `telemetry.sdk.language`: `go`
* `telemetry.sdk.name`: `apmotel`
* `telemetry.sdk.version`: `apm.AgentVersion`

This closely matches the default resource from the official SDK: https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/resource/resource.go#L203

kruskall: For reviewers: https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#telemetry-sdk

Patch:
diff --git a/module/apmotel/span.go b/module/apmotel/span.go
index 8d7cdd53..c4a6c68b 100644
--- a/module/apmotel/span.go
+++ b/module/apmotel/span.go
@@ -90,6 +90,10 @@ func (s *span) End(options ...trace.SpanEndOption) {
 		outcome = "unknown"
 	}
 
+	for iter := s.provider.resource.Iter(); iter.Next(); {
+		s.attributes = append(s.attributes, iter.Attribute())
+	}
+
 	if s.span != nil {
 		s.setSpanAttributes()
 		s.span.Outcome = outcome
diff --git a/module/apmotel/tracer_provider.go b/module/apmotel/tracer_provider.go
index 6c644c6f..e38cd0ea 100644
--- a/module/apmotel/tracer_provider.go
+++ b/module/apmotel/tracer_provider.go
@@ -23,6 +23,7 @@ package apmotel // import "go.elastic.co/apm/module/apmotel/v2"
 import (
 	"sync"
 
+	"go.opentelemetry.io/otel/sdk/resource"
 	"go.opentelemetry.io/otel/trace"
 
 	"go.elastic.co/apm/v2"
@@ -33,7 +34,8 @@ type tracerProvider struct {
 
 	apmTracer *apm.Tracer
 
-	tracers map[string]*tracer
+	tracers  map[string]*tracer
+	resource *resource.Resource
 }
 
 // NewTracerProvider creates a new tracer provider which acts as a bridge with the Elastic Agent tracer.
@@ -42,6 +44,7 @@ func NewTracerProvider(opts ...TracerProviderOption) (trace.TracerProvider, erro
 	tp := &tracerProvider{
 		apmTracer: cfg.apmTracer,
 		tracers:   map[string]*tracer{},
+		resource:  cfg.resource,
 	}
 
 	return tp, nil
diff --git a/module/apmotel/tracer_provider_config.go b/module/apmotel/tracer_provider_config.go
index 4ee91145..438bd9ba 100644
--- a/module/apmotel/tracer_provider_config.go
+++ b/module/apmotel/tracer_provider_config.go
@@ -21,11 +21,39 @@
 package apmotel // import "go.elastic.co/apm/module/apmotel/v2"
 
 import (
+	"os"
+	"path/filepath"
+
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.18.0"
+
 	"go.elastic.co/apm/v2"
 )
 
+// defaultResource is the resource that will be used by default if none were provided in the config
+func defaultResource() *resource.Resource {
+	return resource.NewWithAttributes(
+		semconv.SchemaURL,
+		semconv.ServiceNameKey.String(getServiceName()),
+		semconv.TelemetrySDKName("apmotel"),
+		semconv.TelemetrySDKLanguageGo,
+		semconv.TelemetrySDKVersion(apm.AgentVersion),
+	)
+}
+
+func getServiceName() string {
+	executable, err := os.Executable()
+	if err != nil {
+		return "unknown_service:go"
+	}
+	return "unknown_service:" + filepath.Base(executable)
+}
+
 type tracerProviderConfig struct {
 	apmTracer *apm.Tracer
+
+	// resource contains attributes representing an entity that produces telemetry.
+	resource *resource.Resource
 }
 
 type TracerProviderOption func(tracerProviderConfig) tracerProviderConfig
@@ -40,6 +68,10 @@ func newTracerProviderConfig(opts ...TracerProviderOption) tracerProviderConfig
 		cfg.apmTracer = apm.DefaultTracer()
 	}
 
+	if cfg.resource == nil {
+		cfg.resource = resource.Default()
+	}
+
 	return cfg
 }
 
@@ -50,3 +82,15 @@ func WithAPMTracer(t *apm.Tracer) TracerProviderOption {
 		return cfg
 	}
 }
+
+// WithResource configures the provided resource, which will be referenced by
+// all tracers this provider creates.
+//
+// If this option is not used, the TracerProvider will use the
+// defaultResource() Resource by default.
+func WithResource(r *resource.Resource) TracerProviderOption {
+	return func(cfg tracerProviderConfig) tracerProviderConfig {
+		cfg.resource, _ = resource.Merge(resource.Environment(), r)
+		return cfg
+	}
+}


Test patch:
diff --git a/module/apmotel/span_test.go b/module/apmotel/span_test.go
index 80b02115..38b2278e 100644
--- a/module/apmotel/span_test.go
+++ b/module/apmotel/span_test.go
@@ -31,6 +31,7 @@ import (
 
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/codes"
+	"go.opentelemetry.io/otel/sdk/resource"
 	"go.opentelemetry.io/otel/trace"
 
 	"go.elastic.co/apm/v2"
@@ -40,8 +41,9 @@ import (
 
 func TestSpanEnd(t *testing.T) {
 	for _, tt := range []struct {
-		name    string
-		getSpan func(context.Context, trace.Tracer) trace.Span
+		name     string
+		getSpan  func(context.Context, trace.Tracer) trace.Span
+		resource *resource.Resource
 
 		expectedSpans        []model.Span
 		expectedTransactions []model.Transaction
@@ -64,6 +66,30 @@ func TestSpanEnd(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "with a root span and the default resource",
+			getSpan: func(ctx context.Context, tracer trace.Tracer) trace.Span {
+				ctx, s := tracer.Start(ctx, "name")
+				return s
+			},
+			resource: defaultResource(),
+			expectedTransactions: []model.Transaction{
+				{
+					Name:    "name",
+					Type:    "unknown",
+					Outcome: "unknown",
+					OTel: &model.OTel{
+						SpanKind: "unspecified",
+						Attributes: map[string]interface{}{
+							"service.name":           "unknown_service:apmotel.test",
+							"telemetry.sdk.language": "go",
+							"telemetry.sdk.name":     "apmotel",
+							"telemetry.sdk.version":  apm.AgentVersion,
+						},
+					},
+				},
+			},
+		},
 		{
 			name: "a root span with a span kind",
 			getSpan: func(ctx context.Context, tracer trace.Tracer) trace.Span {
@@ -222,6 +248,31 @@ func TestSpanEnd(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "with a child span and the default resource",
+			getSpan: func(ctx context.Context, tracer trace.Tracer) trace.Span {
+				ctx, _ = tracer.Start(ctx, "parentSpan")
+				ctx, s := tracer.Start(ctx, "childSpan")
+				return s
+			},
+			resource: defaultResource(),
+			expectedSpans: []model.Span{
+				{
+					Name:    "childSpan",
+					Type:    "custom",
+					Outcome: "unknown",
+					OTel: &model.OTel{
+						SpanKind: "unspecified",
+						Attributes: map[string]interface{}{
+							"service.name":           "unknown_service:apmotel.test",
+							"telemetry.sdk.language": "go",
+							"telemetry.sdk.name":     "apmotel",
+							"telemetry.sdk.version":  apm.AgentVersion,
+						},
+					},
+				},
+			},
+		},
 		{
 			name: "a child span with a span kind",
 			getSpan: func(ctx context.Context, tracer trace.Tracer) trace.Span {
@@ -489,7 +540,10 @@ func TestSpanEnd(t *testing.T) {
 	} {
 		t.Run(tt.name, func(t *testing.T) {
 			apmTracer, recorder := transporttest.NewRecorderTracer()
-			tp, err := NewTracerProvider(WithAPMTracer(apmTracer))
+			tp, err := NewTracerProvider(
+				WithAPMTracer(apmTracer),
+				WithResource(tt.resource),
+			)
 			assert.NoError(t, err)
 			tracer := newTracer(tp.(*tracerProvider))
 
diff --git a/module/apmotel/tracer_provider_config_test.go b/module/apmotel/tracer_provider_config_test.go
index 719a5f0e..104aed82 100644
--- a/module/apmotel/tracer_provider_config_test.go
+++ b/module/apmotel/tracer_provider_config_test.go
@@ -24,6 +24,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
 
 	"go.elastic.co/apm/v2"
 	"go.elastic.co/apm/v2/transport/transporttest"
@@ -42,6 +44,7 @@ func TestNewTracerProviderConfig(t *testing.T) {
 			options: nil,
 			wantConfig: tracerProviderConfig{
 				apmTracer: apm.DefaultTracer(),
+				resource:  resource.Default(),
 			},
 		},
 		{
@@ -51,6 +54,17 @@ func TestNewTracerProviderConfig(t *testing.T) {
 			},
 			wantConfig: tracerProviderConfig{
 				apmTracer: apmTracer,
+				resource:  resource.Default(),
+			},
+		},
+		{
+			name: "WithResource",
+			options: []TracerProviderOption{
+				WithResource(resource.NewWithAttributes(semconv.SchemaURL)),
+			},
+			wantConfig: tracerProviderConfig{
+				apmTracer: apm.DefaultTracer(),
+				resource:  resource.NewWithAttributes(semconv.SchemaURL),
 			},
 		},
 	}