Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[exporter/datadog]: Add datadog span operation name remapping config option #3444

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
17 changes: 17 additions & 0 deletions exporter/datadogexporter/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ type TracesConfig struct {
// all entries must be surrounded by double quotes and separated by commas.
// ignore_resources: ["(GET|POST) /healthcheck"]
IgnoreResources []string `mapstructure:"ignore_resources"`

// SpanNameRemappings is the list of datadog span names and preferred names to map to. This can be used to
// automatically map Datadog Span Operation Names to an updated value, and is useful when a user wants to
// shorten or modify span names to something more user friendly in the case of instrumentation libraries with
// particularly verbose names. All entries must be surrounded by double quotes and be separated by commas, with each entry
// between old and updated name separated by a space.
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
// span_name_remappings: ["io.opentelemetry.javaagent.spring.client spring.client", "instrumentation::express.server express"]
ericmustin marked this conversation as resolved.
Show resolved Hide resolved
SpanNameRemappings []string `mapstructure:"span_name_remappings"`
mx-psi marked this conversation as resolved.
Show resolved Hide resolved
}

// TagsConfig defines the tag-related configuration
Expand Down Expand Up @@ -243,5 +251,14 @@ func (c *Config) Validate() error {
}
}
}

if c.Traces.SpanNameRemappings != nil {
for _, entry := range c.Traces.SpanNameRemappings {
SpanNameAndUpdatedName := strings.Split(entry, " ")
if len(SpanNameAndUpdatedName) != 2 {
return fmt.Errorf("'%s' is not valid entry for span name remapping, entry should consist of two space separated strings", entry)
}
}
}
return nil
}
10 changes: 10 additions & 0 deletions exporter/datadogexporter/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,13 @@ func TestIgnoreResourcesValidation(t *testing.T) {
require.NoError(t, noErr)
require.Error(t, err)
}

func TestSpanNameRemappingsValidation(t *testing.T) {
validCfg := Config{Traces: TracesConfig{SpanNameRemappings: []string{"old.opentelemetryspan.name updated.name"}}}
invalidCfg := Config{Traces: TracesConfig{SpanNameRemappings: []string{"oldname:newname"}}}

noErr := validCfg.Validate()
err := invalidCfg.Validate()
require.NoError(t, noErr)
require.Error(t, err)
}
12 changes: 11 additions & 1 deletion exporter/datadogexporter/example/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,17 @@ exporters:
## A blacklist of regular expressions can be provided to disable certain traces based on their resource name
## all entries must be surrounded by double quotes and separated by commas.
#
# ignore_resources: ["(GET|POST) /healthcheck"]
# ignore_resources: ["(GET|POST) /healthcheck"]

## @param span_name_remappings - list of strings - optional
## A list of Datadog span operation names and preferred names to update those names to. This can be used to
## automatically map Datadog Span Operation Names to an updated value, and is useful when a user wants to
## shorten or modify span names to something more user friendly in the case of instrumentation libraries with
## particularly verbose names. All entries must be surrounded by double quotes and be separated by commas, with each entry
## between old and updated name separated by a space.
#
# span_name_remappings: ["io.opentelemetry.javaagent.spring.client spring.client", "instrumentation::express.server express"]


service:
pipelines:
Expand Down
36 changes: 32 additions & 4 deletions exporter/datadogexporter/translate_traces.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,17 @@ func convertToDatadogTd(td pdata.Traces, fallbackHost string, calculator *sublay

var runningMetrics []datadog.Metric
pushTime := pdata.TimestampFromTime(time.Now())

spanNameMap := generateSpanNameMap(cfg)

for i := 0; i < resourceSpans.Len(); i++ {
rs := resourceSpans.At(i)
host, ok := metadata.HostnameFromAttributes(rs.Resource().Attributes())
if !ok {
host = fallbackHost
}

payload := resourceSpansToDatadogSpans(rs, calculator, host, cfg, blk)
payload := resourceSpansToDatadogSpans(rs, calculator, host, cfg, blk, spanNameMap)
traces = append(traces, &payload)

ms := metrics.DefaultMetrics("traces", host, uint64(pushTime))
Expand Down Expand Up @@ -118,7 +121,7 @@ func aggregateTracePayloadsByEnv(tracePayloads []*pb.TracePayload) []*pb.TracePa
}

// converts a Trace's resource spans into a trace payload
func resourceSpansToDatadogSpans(rs pdata.ResourceSpans, calculator *sublayerCalculator, hostname string, cfg *config.Config, blk *Denylister) pb.TracePayload {
func resourceSpansToDatadogSpans(rs pdata.ResourceSpans, calculator *sublayerCalculator, hostname string, cfg *config.Config, blk *Denylister, spanNameMap map[string]string) pb.TracePayload {
// get env tag
env := utils.NormalizeTag(cfg.Env)

Expand Down Expand Up @@ -151,7 +154,7 @@ func resourceSpansToDatadogSpans(rs pdata.ResourceSpans, calculator *sublayerCal
extractInstrumentationLibraryTags(ils.InstrumentationLibrary(), datadogTags)
spans := ils.Spans()
for j := 0; j < spans.Len(); j++ {
span := spanToDatadogSpan(spans.At(j), resourceServiceName, datadogTags, cfg)
span := spanToDatadogSpan(spans.At(j), resourceServiceName, datadogTags, cfg, spanNameMap)
var apiTrace *pb.APITrace
var ok bool

Expand Down Expand Up @@ -214,6 +217,7 @@ func spanToDatadogSpan(s pdata.Span,
serviceName string,
datadogTags map[string]string,
cfg *config.Config,
spanNameMap map[string]string,
) *pb.Span {

tags := aggregateSpanTags(s, datadogTags)
Expand Down Expand Up @@ -275,7 +279,7 @@ func spanToDatadogSpan(s pdata.Span,
span := &pb.Span{
TraceID: decodeAPMTraceID(s.TraceID().Bytes()),
SpanID: decodeAPMSpanID(s.SpanID().Bytes()),
Name: getDatadogSpanName(s, tags),
Name: remapDatadogSpanName(getDatadogSpanName(s, tags), spanNameMap),
Resource: resourceName,
Service: normalizedServiceName,
Start: int64(startTime),
Expand Down Expand Up @@ -630,3 +634,27 @@ func eventsToString(evts pdata.SpanEventSlice) string {
eventArrayBytes, _ := json.Marshal(&eventArray)
return string(eventArrayBytes)
}

func generateSpanNameMap(cfg *config.Config) map[string]string {
// we can preset the size to be at most the number of remapping entries
spanNameMap := make(map[string]string, len(cfg.Traces.SpanNameRemappings))
for _, entry := range cfg.Traces.SpanNameRemappings {
spanNameRemapTuple := strings.Split(entry, " ")
spanNameMap[spanNameRemapTuple[0]] = spanNameRemapTuple[1]
}

return spanNameMap
}

// remapDatadogSpanName allows users to map their datadog span operation names to
// another string as they see fit. It's unclear if this should be a set of regexes or simple string
// matching. It's also unclear if we should expose arbitrary remapping, ie, allowing users to map
// to some other dyanmic attribute or value on the span. This would probably cause issues in metric
// computation
ericmustin marked this conversation as resolved.
Show resolved Hide resolved
func remapDatadogSpanName(name string, spanNameMap map[string]string) string {
if updatedSpanName := spanNameMap[name]; updatedSpanName != "" {
return updatedSpanName
}

return name
}
78 changes: 64 additions & 14 deletions exporter/datadogexporter/translate_traces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func TestBasicTracesTranslation(t *testing.T) {
rs := NewResourceSpansData(mockTraceID, mockSpanID, mockParentSpanID, pdata.StatusCodeUnset, false, mockEndTime)

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -308,7 +308,7 @@ func TestBasicTracesDenylist(t *testing.T) {
rs := NewResourceSpansData(mockTraceID, mockSpanID, mockParentSpanID, pdata.StatusCodeUnset, false, mockEndTime)

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -340,7 +340,7 @@ func TestTracesTranslationErrorsAndResource(t *testing.T) {
},
}

datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -422,7 +422,7 @@ func TestTracesTranslationErrorsFromEventsUsesLast(t *testing.T) {
},
}

datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})

// Ensure the error type is copied over from the last error event logged
assert.Equal(t, attribs[conventions.AttributeExceptionType].StringVal(), datadogPayload.Traces[0].Spans[0].Meta[ext.ErrorType])
Expand Down Expand Up @@ -477,7 +477,7 @@ func TestTracesTranslationErrorsFromEventsBounds(t *testing.T) {
},
}

datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})

// Ensure the error type is copied over
assert.Equal(t, attribs[conventions.AttributeExceptionType].StringVal(), datadogPayload.Traces[0].Spans[0].Meta[ext.ErrorType])
Expand Down Expand Up @@ -534,7 +534,7 @@ func TestTracesTranslationOkStatus(t *testing.T) {
},
}

datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -585,7 +585,7 @@ func TestTracesTranslationConfig(t *testing.T) {
}

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -624,7 +624,7 @@ func TestTracesTranslationNoIls(t *testing.T) {
}

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -675,9 +675,9 @@ func TestTracesTranslationInvalidService(t *testing.T) {
}

// translate mocks to datadog traces
datadogPayloadInvalidService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgInvalidService, denylister)
datadogPayloadEmptyService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgEmptyService, denylister)
datadogPayloadStartWithInvalidService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgStartWithInvalidService, denylister)
datadogPayloadInvalidService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgInvalidService, denylister, map[string]string{})
datadogPayloadEmptyService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgEmptyService, denylister, map[string]string{})
datadogPayloadStartWithInvalidService := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfgStartWithInvalidService, denylister, map[string]string{})

// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayloadInvalidService)
Expand Down Expand Up @@ -712,7 +712,7 @@ func TestTracesTranslationServicePeerName(t *testing.T) {
span.Attributes().InsertString(conventions.AttributePeerService, "my_peer_service_name")

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -788,7 +788,7 @@ func TestTracesTranslationTruncatetag(t *testing.T) {
span.Attributes().InsertString(conventions.AttributeExceptionStacktrace, RandStringBytes(5500))

// translate mocks to datadog traces
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &config.Config{}, denylister, map[string]string{})
// ensure we return the correct type
assert.IsType(t, pb.TracePayload{}, datadogPayload)

Expand Down Expand Up @@ -1184,7 +1184,7 @@ func TestStatsAggregations(t *testing.T) {
// translate mocks to datadog traces
cfg := config.Config{}

datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister)
datadogPayload := resourceSpansToDatadogSpans(rs, calculator, hostname, &cfg, denylister, map[string]string{})

statsOutput := computeAPMStats(&datadogPayload, calculator, time.Now().UTC().UnixNano())

Expand Down Expand Up @@ -1290,3 +1290,53 @@ func TestNormalizeTag(t *testing.T) {
})
}
}

// ensure that sanitization of trace payloads occurs
func TestSpanNameMapping(t *testing.T) {
mockTraceID := [16]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}
mockSpanID := [8]byte{0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8}
mockParentSpanID := [8]byte{0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8}
endTime := time.Now().Round(time.Second)
pdataEndTime := pdata.TimestampFromTime(endTime)
startTime := endTime.Add(-90 * time.Second)
pdataStartTime := pdata.TimestampFromTime(startTime)

calculator := newSublayerCalculator()
denylister := NewDenylister([]string{})

traces := pdata.NewTraces()
traces.ResourceSpans().Resize(1)
rs := traces.ResourceSpans().At(0)
resource := rs.Resource()
resource.Attributes().InitFromMap(map[string]pdata.AttributeValue{
"deployment.environment": pdata.NewAttributeValueString("UpperCase"),
})
rs.InstrumentationLibrarySpans().Resize(1)
ilss := rs.InstrumentationLibrarySpans().At(0)
instrumentationLibrary := ilss.InstrumentationLibrary()
instrumentationLibrary.SetName("flash")
instrumentationLibrary.SetVersion("v1")
span := ilss.Spans().AppendEmpty()

traceID := pdata.NewTraceID(mockTraceID)
spanID := pdata.NewSpanID(mockSpanID)
parentSpanID := pdata.NewSpanID(mockParentSpanID)
span.SetTraceID(traceID)
span.SetSpanID(spanID)
span.SetParentSpanID(parentSpanID)
span.SetName("End-To-End Here")
span.SetKind(pdata.SpanKindServer)
span.SetStartTimestamp(pdataStartTime)
span.SetEndTimestamp(pdataEndTime)

config := config.Config{Traces: config.TracesConfig{SpanNameRemappings: []string{"flash.server bang.client"}}}

outputTraces, _ := convertToDatadogTd(traces, "test-host", calculator, &config, denylister)
aggregatedTraces := aggregateTracePayloadsByEnv(outputTraces)

obfuscator := obfuscate.NewObfuscator(obfuscatorConfig)
obfuscatePayload(obfuscator, aggregatedTraces)
assert.Equal(t, 1, len(aggregatedTraces))

assert.Equal(t, "bang.client", aggregatedTraces[0].Traces[0].Spans[0].Name)
}