diff --git a/elasticattr/attributes.go b/elasticattr/attributes.go index 58fb63a..97a8902 100644 --- a/elasticattr/attributes.go +++ b/elasticattr/attributes.go @@ -19,8 +19,40 @@ package elasticattr const ( // resource s - AgentName = "agent.name" - AgentVersion = "agent.version" + AgentName = "agent.name" + AgentVersion = "agent.version" + AgentEphemeralID = "agent.ephemeral_id" + AgentActivationMethod = "agent.activation_method" + CloudOriginAccountID = "cloud.origin.account.id" + CloudOriginProvider = "cloud.origin.provider" + CloudOriginRegion = "cloud.origin.region" + CloudOriginServiceName = "cloud.origin.service.name" + CloudAccountName = "cloud.account.name" + CloudInstanceID = "cloud.instance.id" + CloudInstanceName = "cloud.instance.name" + CloudMachineType = "cloud.machine.type" + CloudProjectID = "cloud.project.id" + CloudProjectName = "cloud.project.name" + ContainerImageTag = "container.image.tag" + DeviceManufacturer = "device.manufacturer" + DataStreamDataset = "data_stream.dataset" + DataStreamNamespace = "data_stream.namespace" + DestinationIP = "destination.ip" + SourceNATIP = "source.nat.ip" + FaaSExecution = "faas.execution" + FaaSTriggerRequestID = "faas.trigger.request_id" + HostHostName = "host.hostname" + HostOSPlatform = "host.os.platform" + ProcessRuntimeName = "process.runtime.name" + ProcessRuntimeVersion = "process.runtime.version" + ServiceLanguageName = "service.language.name" + ServiceLanguageVersion = "service.language.version" + ServiceRuntimeName = "service.runtime.name" + ServiceRuntimeVersion = "service.runtime.version" + ServiceOriginID = "service.origin.id" + ServiceOriginName = "service.origin.name" + ServiceOriginVersion = "service.origin.version" + UserDomain = "user.domain" // scope s ServiceFrameworkName = "service.framework.name" diff --git a/enrichments/config/config.go b/enrichments/config/config.go index 603cb77..31b2942 100644 --- a/enrichments/config/config.go +++ b/enrichments/config/config.go @@ -34,6 +34,7 @@ type ResourceConfig struct { AgentVersion AttributeConfig `mapstructure:"agent_version"` OverrideHostName AttributeConfig `mapstructure:"override_host_name"` DeploymentEnvironment AttributeConfig `mapstructure:"deployment_environment"` + ServiceInstanceID AttributeConfig `mapstructure:"service_instance_id"` } // ScopeConfig configures the enrichment of scope attributes. @@ -122,6 +123,7 @@ func Enabled() Config { AgentVersion: AttributeConfig{Enabled: true}, OverrideHostName: AttributeConfig{Enabled: true}, DeploymentEnvironment: AttributeConfig{Enabled: true}, + ServiceInstanceID: AttributeConfig{Enabled: true}, }, Scope: ScopeConfig{ ServiceFrameworkName: AttributeConfig{Enabled: true}, diff --git a/enrichments/internal/elastic/resource.go b/enrichments/internal/elastic/resource.go index b38a493..9a560ac 100644 --- a/enrichments/internal/elastic/resource.go +++ b/enrichments/internal/elastic/resource.go @@ -20,11 +20,12 @@ package elastic import ( "fmt" - "github.com/elastic/opentelemetry-lib/elasticattr" - "github.com/elastic/opentelemetry-lib/enrichments/config" "go.opentelemetry.io/collector/pdata/pcommon" semconv25 "go.opentelemetry.io/otel/semconv/v1.25.0" semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + + "github.com/elastic/opentelemetry-lib/elasticattr" + "github.com/elastic/opentelemetry-lib/enrichments/config" ) // EnrichResource derives and adds Elastic specific resource attributes. @@ -45,6 +46,9 @@ type resourceEnrichmentContext struct { deploymentEnvironment string deploymentEnvironmentName string + + serviceInstanceID string + containerID string } func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config.ResourceConfig) { @@ -68,6 +72,10 @@ func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config s.deploymentEnvironment = v.Str() case string(semconv.DeploymentEnvironmentNameKey): s.deploymentEnvironmentName = v.Str() + case string(semconv25.ServiceInstanceIDKey): + s.serviceInstanceID = v.Str() + case string(semconv.ContainerIDKey): + s.containerID = v.Str() } return true }) @@ -91,6 +99,10 @@ func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config if cfg.DeploymentEnvironment.Enabled { s.setDeploymentEnvironment(resource) } + + if cfg.ServiceInstanceID.Enabled { + s.setServiceInstanceID(resource) + } } // SemConv v1.27.0 deprecated `deployment.environment` and added `deployment.environment.name` in favor of it. @@ -161,3 +173,23 @@ func (s *resourceEnrichmentContext) overrideHostNameWithK8sNodeName(resource pco s.k8sNodeName, ) } + +// setServiceInstanceID sets service.instance.id from container.id or host.name +// if service.instance.id is not already set. This follows the existing APM logic for +// `service.node.name`. +func (s *resourceEnrichmentContext) setServiceInstanceID(resource pcommon.Resource) { + if s.serviceInstanceID != "" { + return + } + + switch { + case s.containerID != "": + s.serviceInstanceID = s.containerID + case s.hostName != "": + s.serviceInstanceID = s.hostName + default: + // no instance id could be derived + return + } + resource.Attributes().PutStr(string(semconv25.ServiceInstanceIDKey), s.serviceInstanceID) +} diff --git a/enrichments/internal/elastic/resource_test.go b/enrichments/internal/elastic/resource_test.go index 5833871..c23bc7f 100644 --- a/enrichments/internal/elastic/resource_test.go +++ b/enrichments/internal/elastic/resource_test.go @@ -20,13 +20,14 @@ package elastic import ( "testing" - "github.com/elastic/opentelemetry-lib/elasticattr" - "github.com/elastic/opentelemetry-lib/enrichments/config" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "go.opentelemetry.io/collector/pdata/pcommon" semconv25 "go.opentelemetry.io/otel/semconv/v1.25.0" semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + + "github.com/elastic/opentelemetry-lib/elasticattr" + "github.com/elastic/opentelemetry-lib/enrichments/config" ) func TestResourceEnrich(t *testing.T) { @@ -174,10 +175,11 @@ func TestResourceEnrich(t *testing.T) { }(), config: config.Enabled().Resource, enrichedAttrs: map[string]any{ - string(semconv.HostNameKey): "k8s-node", - string(semconv.K8SNodeNameKey): "k8s-node", - elasticattr.AgentName: "otlp", - elasticattr.AgentVersion: "unknown", + string(semconv.HostNameKey): "k8s-node", + string(semconv.K8SNodeNameKey): "k8s-node", + elasticattr.AgentName: "otlp", + elasticattr.AgentVersion: "unknown", + string(semconv25.ServiceInstanceIDKey): string("test-host"), }, }, { @@ -245,6 +247,58 @@ func TestResourceEnrich(t *testing.T) { elasticattr.AgentVersion: "unknown", }, }, + { + name: "service_instance_id_derived_from_container_id", + input: func() pcommon.Resource { + res := pcommon.NewResource() + res.Attributes().PutStr(string(semconv.ServiceInstanceIDKey), "") + res.Attributes().PutStr(string(semconv25.ContainerIDKey), "container-id") + res.Attributes().PutStr(string(semconv25.HostNameKey), "k8s-node") + return res + }(), + config: config.Enabled().Resource, + enrichedAttrs: map[string]any{ + string(semconv25.ServiceInstanceIDKey): "container-id", + string(semconv.ContainerIDKey): "container-id", + string(semconv.HostNameKey): "k8s-node", + elasticattr.AgentName: "otlp", + elasticattr.AgentVersion: "unknown", + }, + }, + { + name: "service_instance_id_derived_from_host_name", + input: func() pcommon.Resource { + res := pcommon.NewResource() + res.Attributes().PutStr(string(semconv.ServiceInstanceIDKey), "") + res.Attributes().PutStr(string(semconv25.HostNameKey), "k8s-node") + return res + }(), + config: config.Enabled().Resource, + enrichedAttrs: map[string]any{ + string(semconv25.ServiceInstanceIDKey): "k8s-node", + string(semconv.HostNameKey): "k8s-node", + elasticattr.AgentName: "otlp", + elasticattr.AgentVersion: "unknown", + }, + }, + { + name: "service_instance_id_already_set", + input: func() pcommon.Resource { + res := pcommon.NewResource() + res.Attributes().PutStr(string(semconv.ServiceInstanceIDKey), "node-name") + res.Attributes().PutStr(string(semconv25.ContainerIDKey), "container-id") + res.Attributes().PutStr(string(semconv25.HostNameKey), "k8s-node") + return res + }(), + config: config.Enabled().Resource, + enrichedAttrs: map[string]any{ + string(semconv25.ServiceInstanceIDKey): "node-name", + string(semconv.ContainerIDKey): "container-id", + string(semconv.HostNameKey): "k8s-node", + elasticattr.AgentName: "otlp", + elasticattr.AgentVersion: "unknown", + }, + }, } { t.Run(tc.name, func(t *testing.T) { // Merge existing resource attrs with the attrs added diff --git a/enrichments/internal/elastic/span.go b/enrichments/internal/elastic/span.go index 136798b..94e7772 100644 --- a/enrichments/internal/elastic/span.go +++ b/enrichments/internal/elastic/span.go @@ -29,8 +29,6 @@ import ( "net/url" "strconv" - "github.com/elastic/opentelemetry-lib/elasticattr" - "github.com/elastic/opentelemetry-lib/enrichments/config" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling" "github.com/ua-parser/uap-go/uaparser" "go.opentelemetry.io/collector/pdata/pcommon" @@ -40,6 +38,9 @@ import ( semconv37 "go.opentelemetry.io/otel/semconv/v1.37.0" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" "google.golang.org/grpc/codes" + + "github.com/elastic/opentelemetry-lib/elasticattr" + "github.com/elastic/opentelemetry-lib/enrichments/config" ) // defaultRepresentativeCount is the representative count to use for adjusting @@ -444,9 +445,14 @@ func (s *spanEnrichmentContext) setSpanTypeSubtype(span ptrace.Span) (spanType s } } - span.Attributes().PutStr(elasticattr.SpanType, spanType) + // do not overwrite existing span.type and span.subtype attributes + if existingSpanType, _ := span.Attributes().Get(elasticattr.SpanType); existingSpanType.Str() == "" { + span.Attributes().PutStr(elasticattr.SpanType, spanType) + } if spanSubtype != "" { - span.Attributes().PutStr(elasticattr.SpanSubtype, spanSubtype) + if existingSpanSubtype, _ := span.Attributes().Get(elasticattr.SpanSubtype); existingSpanSubtype.Str() == "" { + span.Attributes().PutStr(elasticattr.SpanSubtype, spanSubtype) + } } return spanType, spanSubtype @@ -494,9 +500,15 @@ func (s *spanEnrichmentContext) setServiceTarget(span ptrace.Span) { } } + // set either target.type or target.name if at least one is available if targetType != "" || targetName != "" { - span.Attributes().PutStr(elasticattr.ServiceTargetType, targetType) - span.Attributes().PutStr(elasticattr.ServiceTargetName, targetName) + // do not overwrite existing target.type and target.name attributes + if existingTargetType, _ := span.Attributes().Get(elasticattr.ServiceTargetType); existingTargetType.Str() == "" { + span.Attributes().PutStr(elasticattr.ServiceTargetType, targetType) + } + if existingTargetName, _ := span.Attributes().Get(elasticattr.ServiceTargetName); existingTargetName.Str() == "" { + span.Attributes().PutStr(elasticattr.ServiceTargetName, targetName) + } } } @@ -536,7 +548,10 @@ func (s *spanEnrichmentContext) setDestinationService(span ptrace.Span) { } if destnResource != "" { - span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, destnResource) + // do not overwrite existing span.destination.service.resource attribute + if existingDestnResource, _ := span.Attributes().Get(elasticattr.SpanDestinationServiceResource); existingDestnResource.Str() == "" { + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, destnResource) + } } } diff --git a/enrichments/internal/elastic/span_test.go b/enrichments/internal/elastic/span_test.go index c403df4..8c574a8 100644 --- a/enrichments/internal/elastic/span_test.go +++ b/enrichments/internal/elastic/span_test.go @@ -24,8 +24,6 @@ import ( "testing" "time" - "github.com/elastic/opentelemetry-lib/elasticattr" - "github.com/elastic/opentelemetry-lib/enrichments/config" "github.com/google/go-cmp/cmp" "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest/ptracetest" "github.com/stretchr/testify/assert" @@ -37,6 +35,9 @@ import ( semconv37 "go.opentelemetry.io/otel/semconv/v1.37.0" tracepb "go.opentelemetry.io/proto/otlp/trace/v1" "google.golang.org/grpc/codes" + + "github.com/elastic/opentelemetry-lib/elasticattr" + "github.com/elastic/opentelemetry-lib/enrichments/config" ) // Tests the enrichment logic for elastic's transaction definition. @@ -931,6 +932,39 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SpanDestinationServiceResource: "testsvc", }, }, + { + name: "http_span_with_existing_attributes_are_preserved", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.Attributes().PutStr(string(semconv25.PeerServiceKey), "testsvc") + span.Attributes().PutInt( + string(semconv25.HTTPResponseStatusCodeKey), + http.StatusOK, + ) + span.Attributes().PutStr(elasticattr.SpanType, "external-test") + span.Attributes().PutStr(elasticattr.SpanSubtype, "http-test") + span.Attributes().PutStr(elasticattr.ServiceTargetName, "api.example.com") + span.Attributes().PutStr(elasticattr.ServiceTargetType, "http-test") + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, "api.example.com:443") + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "external-test", + elasticattr.SpanSubtype: "http-test", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.ServiceTargetType: "http-test", + elasticattr.ServiceTargetName: "api.example.com", + elasticattr.SpanDestinationServiceResource: "api.example.com:443", + }, + }, { name: "http_span_full_url", input: func() ptrace.Span { @@ -1059,6 +1093,39 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SpanDestinationServiceResource: "testsvc", }, }, + { + name: "rpc_span_grpc_with_existing_attributes_are_preserved", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.Attributes().PutStr(string(semconv25.PeerServiceKey), "testsvc") + span.Attributes().PutInt( + string(semconv25.RPCGRPCStatusCodeKey), + int64(codes.OK), + ) + span.Attributes().PutStr(elasticattr.SpanType, "external-test") + span.Attributes().PutStr(elasticattr.SpanSubtype, "grpc-test") + span.Attributes().PutStr(elasticattr.ServiceTargetName, "myservice.EchoService") + span.Attributes().PutStr(elasticattr.ServiceTargetType, "grpc-test") + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, "api.example.com:443") + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "external-test", + elasticattr.SpanSubtype: "grpc-test", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.ServiceTargetType: "grpc-test", + elasticattr.ServiceTargetName: "myservice.EchoService", + elasticattr.SpanDestinationServiceResource: "api.example.com:443", + }, + }, { name: "rpc_span_system", input: func() ptrace.Span { @@ -1187,6 +1254,36 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SpanDestinationServiceResource: "testsvc", }, }, + { + name: "messaging_with_existing_attributes_are_preserved", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.Attributes().PutStr(string(semconv25.PeerServiceKey), "testsvc") + span.Attributes().PutStr(string(semconv25.MessagingSystemKey), "kafka") + span.Attributes().PutStr(elasticattr.SpanType, "messaging-test") + span.Attributes().PutStr(elasticattr.SpanSubtype, "kafka-test") + span.Attributes().PutStr(elasticattr.ServiceTargetName, "user-events") + span.Attributes().PutStr(elasticattr.ServiceTargetType, "kafka-test") + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, "broker.com:443") + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "messaging-test", + elasticattr.SpanSubtype: "kafka-test", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.ServiceTargetType: "kafka-test", + elasticattr.ServiceTargetName: "user-events", + elasticattr.SpanDestinationServiceResource: "broker.com:443", + }, + }, { name: "messaging_destination", input: func() ptrace.Span { @@ -1265,6 +1362,40 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SpanDestinationServiceResource: "testsvc", }, }, + { + name: "db_with_existing_attributes_are_preserved", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.Attributes().PutStr(string(semconv25.PeerServiceKey), "testsvc") + span.Attributes().PutStr( + string(semconv25.URLFullKey), + "https://localhost:5432", + ) + span.Attributes().PutStr(string(semconv25.DBSystemKey), "postgresql") + span.Attributes().PutStr(elasticattr.SpanType, "db") + span.Attributes().PutStr(elasticattr.SpanSubtype, "postgresql") + span.Attributes().PutStr(elasticattr.ServiceTargetName, "customers") + span.Attributes().PutStr(elasticattr.ServiceTargetType, "postgresql") + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, "postgresql/testdb") + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "db", + elasticattr.SpanSubtype: "postgresql", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.ServiceTargetType: "postgresql", + elasticattr.ServiceTargetName: "customers", + elasticattr.SpanDestinationServiceResource: "postgresql/testdb", + }, + }, { name: "db_over_rpc", input: func() ptrace.Span { @@ -1374,6 +1505,36 @@ func TestElasticSpanEnrich(t *testing.T) { elasticattr.SuccessCount: int64(1), }, }, + { + name: "genai_with_existing_attributes_are_preserved", + input: func() ptrace.Span { + span := getElasticSpan() + span.SetName("testspan") + span.SetSpanID([8]byte{1}) + span.Attributes().PutStr(string(semconv37.GenAIProviderNameKey), "openai") + span.Attributes().PutStr(elasticattr.SpanType, "genai") + span.Attributes().PutStr(elasticattr.SpanSubtype, "openai-test") + span.Attributes().PutStr(elasticattr.ServiceTargetName, "openai-api") + span.Attributes().PutStr(elasticattr.ServiceTargetType, "genai") + span.Attributes().PutStr(elasticattr.SpanDestinationServiceResource, "api.openai.com:443") + return span + }(), + config: config.Enabled().Span, + enrichedAttrs: map[string]any{ + elasticattr.TimestampUs: startTs.AsTime().UnixMicro(), + elasticattr.SpanName: "testspan", + elasticattr.ProcessorEvent: "span", + elasticattr.SpanRepresentativeCount: float64(1), + elasticattr.SpanType: "genai", + elasticattr.SpanSubtype: "openai-test", + elasticattr.SpanDurationUs: expectedDuration.Microseconds(), + elasticattr.EventOutcome: "success", + elasticattr.SuccessCount: int64(1), + elasticattr.ServiceTargetType: "genai", + elasticattr.ServiceTargetName: "openai-api", + elasticattr.SpanDestinationServiceResource: "api.openai.com:443", + }, + }, { name: "rpc_span_with_only_rpc_sevice_attr", input: func() ptrace.Span {