Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/elasticattr/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
CloudProjectName = "cloud.project.name"
ContainerImageTag = "container.image.tag"
DeviceManufacturer = "device.manufacturer"
DataStreamType = "data_stream.type"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note this will require some extra coordination to ensure all modules that use this package update the version of elasticattr they rely on and that a new version of elasticattr is tagged

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remember to change the relevant go.mod files to use the next version of the elasticattr package (it should be v0.38.0). Example commit. Without this external modules will face build errors unfortunately

DataStreamDataset = "data_stream.dataset"
DataStreamNamespace = "data_stream.namespace"
DestinationIP = "destination.ip"
Expand Down
2 changes: 2 additions & 0 deletions processor/elasticapmprocessor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The processor enriches traces, metrics, and logs with elastic specific requireme

The processor configuration embeds all configuration options from [opentelemetry-lib enrichments config](https://github.com/elastic/opentelemetry-lib/tree/main/enrichments/config). All opentelemetry-lib enricher configuration fields are available and can be configured directly in the processor configuration.

The resource enrichment config also includes `resource.default_deployment_environment.enabled`. This controls the fallback `deployment.environment=unset` behavior separately from `resource.deployment_environment.enabled`, which still handles copying `deployment.environment.name` to `deployment.environment`. The fallback is only applied for ECS mapping mode.

### Enable all enrichments

```yaml
Expand Down
66 changes: 7 additions & 59 deletions processor/elasticapmprocessor/internal/ecs/ecs_translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ package ecs // import "github.com/elastic/opentelemetry-collector-components/pro

import (
"strconv"
"strings"

"github.com/elastic/opentelemetry-collector-components/internal/elasticattr"
"github.com/elastic/opentelemetry-collector-components/processor/elasticapmprocessor/internal/sanitize"
"go.opentelemetry.io/collector/pdata/pcommon"
semconv26 "go.opentelemetry.io/otel/semconv/v1.26.0"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"

"github.com/elastic/opentelemetry-collector-components/internal/elasticattr"
)

// Supported ECS resource attributes
Expand All @@ -42,72 +41,21 @@ func TranslateResourceMetadata(resource pcommon.Resource) {
attributes := resource.Attributes()

attributes.Range(func(k string, v pcommon.Value) bool {
if isLabelAttribute(k) {
sanitized := sanitizeLabelAttributeKey(k)
if sanitize.IsLabelAttribute(k) {
sanitized := sanitize.HandleLabelAttributeKey(k)
if sanitized != k {
v.CopyTo(attributes.PutEmpty(sanitized))
attributes.Remove(k)
}
} else if !isSupportedAttribute(k) {
// Other attributes that are not supported by ECS are moved to labels with a "labels." prefix.
setLabelAttributeValue(attributes, sanitizeLabelKey(k), v)
setLabelAttributeValue(attributes, sanitize.HandleAttributeKey(k), v)
attributes.Remove(k)
}
return true
})
}

// sanitizeLabelAttributeKey sanitizes the key portion of a label attribute,
// preserving the "labels." or "numeric_labels." prefix.
func sanitizeLabelAttributeKey(attr string) string {
if strings.HasPrefix(attr, "labels.") {
return "labels." + sanitizeLabelKey(strings.TrimPrefix(attr, "labels."))
}
if strings.HasPrefix(attr, "numeric_labels.") {
return "numeric_labels." + sanitizeLabelKey(strings.TrimPrefix(attr, "numeric_labels."))
}
return attr
}

// sanitizeLabelKey sanitizes a label key, replacing the reserved characters
// '.', '*' and '"' with '_'. This matches the apm-server behavior.
// This matches the logic in the apm-data library here:
// https://github.com/elastic/apm-data/blob/e3e170b/model/modeljson/labels.go.
func sanitizeLabelKey(k string) string {
if strings.ContainsAny(k, ".*\"") {
return strings.Map(replaceReservedLabelKeyRune, k)
}
return k
}

func replaceReservedLabelKeyRune(r rune) rune {
switch r {
case '.', '*', '"':
return '_'
}
return r
}

// isLabelAttribute returns true if the resource attribute is already a prefixed label.
// The elasticapmintake receiver moves labels and numeric_labels into attributes and
// already prefixes those with "labels." and "numeric_labels." respectively and also does de-dotting.
// So for those, we don't want to double prefix - we just leave them as is.
func isLabelAttribute(attr string) bool {
return strings.HasPrefix(attr, "labels.") || strings.HasPrefix(attr, "numeric_labels.")
}

// truncate returns s truncated at n runes, and the number of runes in the resulting string (<= n).
func truncate(s string) string {
var j int
for i := range s {
if j == keywordLength {
return s[:i]
}
j++
}
return s
}

// setLabelAttributeValue maps a value into labels.* / numeric_labels.*.
// Elasticsearch label mappings only support flat scalar values and
// homogeneous arrays thereof; Map, Bytes, and empty types cannot be
Expand All @@ -117,7 +65,7 @@ func truncate(s string) string {
func setLabelAttributeValue(attributes pcommon.Map, key string, value pcommon.Value) {
switch value.Type() {
case pcommon.ValueTypeStr:
attributes.PutStr("labels."+key, truncate(value.Str()))
attributes.PutStr("labels."+key, sanitize.Truncate(value.Str(), keywordLength))
case pcommon.ValueTypeBool:
attributes.PutStr("labels."+key, strconv.FormatBool(value.Bool()))
case pcommon.ValueTypeInt:
Expand All @@ -135,7 +83,7 @@ func setLabelAttributeValue(attributes pcommon.Map, key string, value pcommon.Va
for i := 0; i < slice.Len(); i++ {
item := slice.At(i)
if item.Type() == pcommon.ValueTypeStr {
target.AppendEmpty().SetStr(truncate(item.Str()))
target.AppendEmpty().SetStr(sanitize.Truncate(item.Str(), keywordLength))
}
}
case pcommon.ValueTypeBool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ type Config struct {

// ResourceConfig configures the enrichment of resource attributes.
type ResourceConfig struct {
AgentName AttributeConfig `mapstructure:"agent_name"`
AgentVersion AttributeConfig `mapstructure:"agent_version"`
OverrideHostName AttributeConfig `mapstructure:"override_host_name"`
DeploymentEnvironment AttributeConfig `mapstructure:"deployment_environment"`
ServiceInstanceID AttributeConfig `mapstructure:"service_instance_id"`
HostOSType AttributeConfig `mapstructure:"host_os_type"`
AgentName AttributeConfig `mapstructure:"agent_name"`
AgentVersion AttributeConfig `mapstructure:"agent_version"`
OverrideHostName AttributeConfig `mapstructure:"override_host_name"`
DeploymentEnvironment AttributeConfig `mapstructure:"deployment_environment"`
DefaultDeploymentEnvironment AttributeConfig `mapstructure:"default_deployment_environment"`
ServiceInstanceID AttributeConfig `mapstructure:"service_instance_id"`
ServiceName AttributeConfig `mapstructure:"service_name"`
HostOSType AttributeConfig `mapstructure:"host_os_type"`
}

// ScopeConfig configures the enrichment of scope attributes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ func assertAttributeConfigDefaults(t *testing.T, cfg reflect.Value, expectDisabl

// Fields that are intentionally disabled by default
disabledByDefault := map[string]bool{
"ClearSpanID": true,
"ClearSpanName": true,
"HostOSType": true,
"ClearSpanID": true,
"ClearSpanName": true,
"DefaultDeploymentEnvironment": true,
"ServiceName": true,
"HostOSType": true,
}

assertAttributeConfigDefaultsRecurse(t, cfg, disabled, disabledByDefault)
Expand Down
53 changes: 47 additions & 6 deletions processor/elasticapmprocessor/internal/enrichments/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package enrichments // import "github.com/elastic/opentelemetry-collector-compon
import (
"fmt"

"github.com/elastic/opentelemetry-collector-components/processor/elasticapmprocessor/internal/sanitize"
"go.opentelemetry.io/collector/pdata/pcommon"
semconv25 "go.opentelemetry.io/otel/semconv/v1.25.0"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
Expand Down Expand Up @@ -49,6 +50,7 @@ type resourceEnrichmentContext struct {
deploymentEnvironmentName string

serviceInstanceID string
serviceName string
containerID string

osType string
Expand Down Expand Up @@ -78,6 +80,8 @@ func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config
s.deploymentEnvironmentName = v.Str()
case string(semconv25.ServiceInstanceIDKey):
s.serviceInstanceID = v.Str()
case string(semconv.ServiceNameKey):
s.serviceName = v.Str()
case string(semconv.ContainerIDKey):
s.containerID = v.Str()
case string(semconv.OSTypeKey):
Expand All @@ -100,13 +104,21 @@ func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config
if cfg.OverrideHostName.Enabled {
s.overrideHostNameWithK8sNodeName(resource)
}

if cfg.DeploymentEnvironment.Enabled {
s.setDeploymentEnvironment(resource)
}

if cfg.DefaultDeploymentEnvironment.Enabled {
s.setDefaultDeploymentEnvironment(resource)
}

if cfg.ServiceInstanceID.Enabled {
s.setServiceInstanceID(resource)
}
if cfg.ServiceName.Enabled {
s.sanitizeServiceName(resource)
}
if cfg.HostOSType.Enabled {
s.setHostOSType(resource)
}
Expand All @@ -117,13 +129,32 @@ func (s *resourceEnrichmentContext) Enrich(resource pcommon.Resource, cfg config
// ES currently doesn't allow aliases with multiple targets, so if the new field name is used (SemConv v1.27+),
// we duplicate the value and also send it with the old field name to make the alias work.
func (s *resourceEnrichmentContext) setDeploymentEnvironment(resource pcommon.Resource) {
Comment thread
lanre-ade marked this conversation as resolved.
if s.deploymentEnvironmentName != "" && s.deploymentEnvironment == "" {
attribute.PutStr(
resource.Attributes(),
string(semconv25.DeploymentEnvironmentKey),
s.deploymentEnvironmentName,
)
if s.deploymentEnvironment != "" {
return
}
if s.deploymentEnvironmentName == "" {
return
}

attribute.PutStr(
resource.Attributes(),
string(semconv25.DeploymentEnvironmentKey),
Comment thread
lanre-ade marked this conversation as resolved.
s.deploymentEnvironmentName,
)
}

// setDefaultDeploymentEnvironment sets deployment.environment to "unset"
// when neither deployment environment field is present, matching the
// apm-data/MIS default only where explicitly configured.
func (s *resourceEnrichmentContext) setDefaultDeploymentEnvironment(resource pcommon.Resource) {
if s.deploymentEnvironment != "" || s.deploymentEnvironmentName != "" {
return
}
attribute.PutStr(
resource.Attributes(),
string(semconv25.DeploymentEnvironmentKey),
"unset",
)
}

func (s *resourceEnrichmentContext) setAgentName(resource pcommon.Resource) {
Expand Down Expand Up @@ -225,3 +256,13 @@ func (s *resourceEnrichmentContext) setServiceInstanceID(resource pcommon.Resour
}
attribute.PutStr(resource.Attributes(), string(semconv25.ServiceInstanceIDKey), s.serviceInstanceID)
}

func (s *resourceEnrichmentContext) sanitizeServiceName(resource pcommon.Resource) {
if s.serviceName == "" {
return
}
cleaned := sanitize.CleanServiceName(s.serviceName)
if cleaned != s.serviceName {
resource.Attributes().PutStr(string(semconv.ServiceNameKey), cleaned)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Can we use attribute.PutStr to be consistent ?

@lanre-ade lanre-ade Mar 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attribute.PutStr only inserts the value if one doesn't already exist. Do we not want to always sanitize the service name?

@isaacaflores2 isaacaflores2 Mar 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still sanitize. I meant to just use attribute.PutStr instead of resource.Attributes().PutStr to be consistent

}
}
Loading