diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index 22ac0306b4..403089058c 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -10,14 +10,16 @@ package httptrace import ( "context" "fmt" - "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "net/http" "strconv" "strings" "sync" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/baggage" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal" @@ -107,6 +109,23 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. tracer.WithSpanLinks(spanLinksCtx.SpanLinks())(ssCfg) } tracer.ChildOf(spanParentCtx)(ssCfg) + + var baggageMap map[string]string + spanParentCtx.ForeachBaggageItem(func(k, v string) bool { + // Make the map only if we actually discover any baggage items. + if baggageMap == nil { + baggageMap = make(map[string]string) + } + baggageMap[k] = v + return true + }) + if len(baggageMap) > 0 { + ctx := r.Context() + for k, v := range baggageMap { + ctx = baggage.Set(ctx, k, v) + } + r = r.WithContext(ctx) + } } } diff --git a/contrib/internal/httptrace/httptrace_test.go b/contrib/internal/httptrace/httptrace_test.go index 1f389228c7..8f76f40059 100644 --- a/contrib/internal/httptrace/httptrace_test.go +++ b/contrib/internal/httptrace/httptrace_test.go @@ -368,3 +368,21 @@ func BenchmarkStartRequestSpan(b *testing.B) { StartRequestSpan(r, opts...) } } + +func TestStartRequestSpanWithBaggage(t *testing.T) { + t.Setenv("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext,baggage") + tracer.Start() + defer tracer.Stop() + + r := httptest.NewRequest(http.MethodGet, "/somePath", nil) + r.Header.Set("baggage", "key1=value1,key2=value2") + s, _, _ := StartRequestSpan(r) + s.Finish() + spanBm := make(map[string]string) + s.Context().ForeachBaggageItem(func(k, v string) bool { + spanBm[k] = v + return true + }) + assert.Equal(t, "value1", spanBm["key1"]) + assert.Equal(t, "value2", spanBm["key2"]) +} diff --git a/contrib/net/http/roundtripper.go b/contrib/net/http/roundtripper.go index 79a46828ca..417763800d 100644 --- a/contrib/net/http/roundtripper.go +++ b/contrib/net/http/roundtripper.go @@ -16,6 +16,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http/internal/config" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/baggage" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec" @@ -72,6 +73,9 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er rt.cfg.before(req, span) } r2 := req.Clone(ctx) + for k, v := range baggage.All(ctx) { + span.SetBaggageItem(k, v) + } if rt.cfg.propagation { // inject the span context into the http request copy err = tracer.Inject(span.Context(), tracer.HTTPHeadersCarrier(r2.Header)) diff --git a/contrib/net/http/roundtripper_test.go b/contrib/net/http/roundtripper_test.go index f532371593..0f73e2daa9 100644 --- a/contrib/net/http/roundtripper_test.go +++ b/contrib/net/http/roundtripper_test.go @@ -6,6 +6,7 @@ package http import ( + "context" "encoding/base64" "fmt" "net/http" @@ -22,6 +23,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/baggage" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" @@ -830,3 +832,34 @@ func TestAppsec(t *testing.T) { }) } } + +func TestRoundTripperWithBaggage(t *testing.T) { + t.Setenv("DD_TRACE_PROPAGATION_STYLE", "datadog,tracecontext,baggage") + tracer.Start() + defer tracer.Stop() + + var capturedHeaders http.Header + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeaders = r.Header.Clone() + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Hello with Baggage!")) + })) + defer s.Close() + + rt := WrapRoundTripper(http.DefaultTransport).(*roundTripper) + + ctx := context.Background() + ctx = baggage.Set(ctx, "foo", "bar") + ctx = baggage.Set(ctx, "baz", "qux") + + // Build the HTTP request with that context. + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.URL+"/baggage", nil) + assert.NoError(t, err) + + resp, err := rt.RoundTrip(req) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.NotEmpty(t, capturedHeaders.Get("baggage"), "should have baggage header") +} diff --git a/ddtrace/opentelemetry/telemetry_test.go b/ddtrace/opentelemetry/telemetry_test.go index 573d6ea699..4ed1a90703 100644 --- a/ddtrace/opentelemetry/telemetry_test.go +++ b/ddtrace/opentelemetry/telemetry_test.go @@ -23,21 +23,21 @@ func TestTelemetry(t *testing.T) { }{ { // if nothing is set, DD_TRACE_PROPAGATION_STYLE will be set to datadog,tracecontext - expectedInject: "datadog,tracecontext", - expectedExtract: "datadog,tracecontext", + expectedInject: "datadog,tracecontext,baggage", + expectedExtract: "datadog,tracecontext,baggage", }, { env: map[string]string{ "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "datadog", }, - expectedInject: "datadog,tracecontext", + expectedInject: "datadog,tracecontext,baggage", expectedExtract: "datadog", }, { env: map[string]string{ "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "none", }, - expectedInject: "datadog,tracecontext", + expectedInject: "datadog,tracecontext,baggage", expectedExtract: "", }, { @@ -55,7 +55,7 @@ func TestTelemetry(t *testing.T) { "DD_TRACE_PROPAGATION_STYLE_EXTRACT": "", }, expectedInject: "tracecontext", - expectedExtract: "datadog,tracecontext", + expectedExtract: "datadog,tracecontext,baggage", }, { env: map[string]string{ diff --git a/ddtrace/tracer/log_test.go b/ddtrace/tracer/log_test.go index 0b94b91494..1e036f864a 100644 --- a/ddtrace/tracer/log_test.go +++ b/ddtrace/tracer/log_test.go @@ -33,7 +33,7 @@ func TestStartupLog(t *testing.T) { tp.Ignore("appsec: ", telemetry.LogPrefix) logStartup(tracer) require.Len(t, tp.Logs(), 2) - assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":((true)|(false)),"Stats":((true)|(false)),"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) + assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":((true)|(false)),"Stats":((true)|(false)),"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) }) t.Run("configured", func(t *testing.T) { @@ -65,7 +65,7 @@ func TestStartupLog(t *testing.T) { tp.Ignore("appsec: ", telemetry.LogPrefix) logStartup(tracer) require.Len(t, tp.Logs(), 2) - assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"100","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":true,"metadata":{"version":"v1"}},"feature_flags":\["discovery"\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) + assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"100","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":true,"metadata":{"version":"v1"}},"feature_flags":\["discovery"\],"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) }) t.Run("limit", func(t *testing.T) { @@ -95,7 +95,7 @@ func TestStartupLog(t *testing.T) { tp.Ignore("appsec: ", telemetry.LogPrefix) logStartup(tracer) require.Len(t, tp.Logs(), 2) - assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"1000.001","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) + assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"1000.001","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) }) t.Run("errors", func(t *testing.T) { @@ -110,7 +110,7 @@ func TestStartupLog(t *testing.T) { logStartup(tracer) require.Len(t, tp.Logs(), 2) fmt.Println(tp.Logs()[1]) - assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"100","trace_sampling_rules":\[{"service":"some\.service","sample_rate":0\.234}\],"span_sampling_rules":null,"sampling_rules_error":"\\n\\tat index 1: ignoring rule {Service:other.service Rate:2}: rate is out of \[0\.0, 1\.0] range","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":((true)|(false)),"Stats":((true)|(false)),"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) + assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"100","trace_sampling_rules":\[{"service":"some\.service","sample_rate":0\.234}\],"span_sampling_rules":null,"sampling_rules_error":"\\n\\tat index 1: ignoring rule {Service:other.service Rate:2}: rate is out of \[0\.0, 1\.0] range","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":((true)|(false)),"Stats":((true)|(false)),"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) }) t.Run("lambda", func(t *testing.T) { @@ -123,7 +123,7 @@ func TestStartupLog(t *testing.T) { tp.Ignore("appsec: ", telemetry.LogPrefix) logStartup(tracer) assert.Len(tp.Logs(), 1) - assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"true","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[0]) + assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"true","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[0]) }) t.Run("integrations", func(t *testing.T) { @@ -195,7 +195,7 @@ func TestLogFormat(t *testing.T) { func TestLogPropagators(t *testing.T) { t.Run("default", func(t *testing.T) { assert := assert.New(t) - substring := `"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext"` + substring := `"propagation_style_inject":"datadog,tracecontext,baggage","propagation_style_extract":"datadog,tracecontext,baggage"` log := setup(t, nil) assert.Regexp(substring, log) }) diff --git a/ddtrace/tracer/textmap.go b/ddtrace/tracer/textmap.go index b1698ef48d..949ffd5e2f 100644 --- a/ddtrace/tracer/textmap.go +++ b/ddtrace/tracer/textmap.go @@ -8,9 +8,11 @@ package tracer import ( "fmt" "net/http" + "net/url" "os" "strconv" "strings" + "sync/atomic" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" @@ -90,6 +92,10 @@ const ( // DefaultPriorityHeader specifies the key that will be used in HTTP headers // or text maps to store the sampling priority value. DefaultPriorityHeader = "x-datadog-sampling-priority" + + // DefaultBaggageHeader specifies the key that will be used in HTTP headers + // or text maps to store the baggage value. + DefaultBaggageHeader = "baggage" ) // originHeader specifies the name of the header indicating the origin of the trace. @@ -127,6 +133,10 @@ type PropagatorConfig struct { // B3 specifies if B3 headers should be added for trace propagation. // See https://github.com/openzipkin/b3-propagation B3 bool + + // BaggageHeader specifies the map key that will be used to store the baggage key-value pairs. + // It defaults to DefaultBaggageHeader. + BaggageHeader string } // NewPropagator returns a new propagator which uses TextMap to inject @@ -155,6 +165,9 @@ func NewPropagator(cfg *PropagatorConfig, propagators ...Propagator) Propagator if cfg.PriorityHeader == "" { cfg.PriorityHeader = DefaultPriorityHeader } + if cfg.BaggageHeader == "" { + cfg.BaggageHeader = DefaultBaggageHeader + } cp := new(chainedPropagator) cp.onlyExtractFirst = internal.BoolEnv("DD_TRACE_PROPAGATION_EXTRACT_FIRST", false) if len(propagators) > 0 { @@ -196,8 +209,8 @@ type chainedPropagator struct { // a warning and be ignored. func getPropagators(cfg *PropagatorConfig, ps string) ([]Propagator, string) { dd := &propagator{cfg} - defaultPs := []Propagator{dd, &propagatorW3c{}} - defaultPsName := "datadog,tracecontext" + defaultPs := []Propagator{dd, &propagatorW3c{}, &propagatorBaggage{}} + defaultPsName := "datadog,tracecontext,baggage" if cfg.B3 { defaultPs = append(defaultPs, &propagatorB3{}) defaultPsName += ",b3" @@ -227,6 +240,9 @@ func getPropagators(cfg *PropagatorConfig, ps string) ([]Propagator, string) { case "tracecontext": list = append(list, &propagatorW3c{}) listNames = append(listNames, v) + case "baggage": + list = append(list, &propagatorBaggage{}) + listNames = append(listNames, v) case "b3", "b3multi": if !cfg.B3 { // propagatorB3 hasn't already been added, add a new one. @@ -278,6 +294,14 @@ func (p *chainedPropagator) Extract(carrier interface{}) (ddtrace.SpanContext, e firstExtract := (ctx == nil) // ctx stores the most recently extracted ctx across iterations; if it's nil, no extractor has run yet extractedCtx, err := v.Extract(carrier) + // If the extractor is the baggage propagator and its baggage is empty, + // treat it as if nothing was extracted. + if _, ok := v.(*propagatorBaggage); ok { + if extractedSpan, ok := extractedCtx.(*spanContext); ok && len(extractedSpan.baggage) == 0 { + extractedCtx = nil + } + } + if firstExtract { if err != nil { if p.onlyExtractFirst { // Every error is relevant when we are relying on the first extractor @@ -326,7 +350,17 @@ func (p *chainedPropagator) Extract(carrier interface{}) (ddtrace.SpanContext, e links = append(links, link) } } + + if _, ok := v.(*propagatorBaggage); ok && extractedCtx != nil { + if ctxSpan, ok := ctx.(*spanContext); ok { + if extractedSpan, ok := extractedCtx.(*spanContext); ok && len(extractedSpan.baggage) > 0 { + ctxSpan.baggage = extractedSpan.baggage + atomic.StoreUint32(&ctxSpan.hasBaggage, 1) + } + } + } } + // 0 successful extractions if ctx == nil { return nil, ErrSpanContextNotFound @@ -348,6 +382,8 @@ func getPropagatorName(p Propagator) string { return "b3" case *propagatorW3c: return "tracecontext" + case *propagatorBaggage: + return "baggage" default: return "" } @@ -1276,3 +1312,164 @@ func extractTraceID128(ctx *spanContext, v string) error { } return nil } + +const ( + baggageMaxItems = 64 + baggageMaxBytes = 8192 + safeCharactersKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~" + safeCharactersValue = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()*+-./:<>?@[]^_`{|}~" +) + +// encodeKey encodes a key with the specified safe characters +func encodeKey(key string) string { + return urlEncode(strings.TrimSpace(key), safeCharactersKey) +} + +// encodeValue encodes a value with the specified safe characters +func encodeValue(value string) string { + return urlEncode(strings.TrimSpace(value), safeCharactersValue) +} + +// urlEncode performs percent-encoding while respecting the safe characters +func urlEncode(input string, safeCharacters string) string { + var encoded strings.Builder + for _, c := range input { + if strings.ContainsRune(safeCharacters, c) { + encoded.WriteRune(c) + } else { + encoded.WriteString(url.QueryEscape(string(c))) + } + } + return encoded.String() +} + +// propagatorBaggage implements Propagator and injects/extracts span contexts +// using baggage headers. +type propagatorBaggage struct{} + +func (p *propagatorBaggage) Inject(spanCtx ddtrace.SpanContext, carrier interface{}) error { + switch c := carrier.(type) { + case TextMapWriter: + return p.injectTextMap(spanCtx, c) + default: + return ErrInvalidCarrier + } +} + +// injectTextMap propagates baggage items from the span context into the writer, +// in the format of a single HTTP "baggage" header. Baggage consists of key=value pairs, +// separated by commas. This function enforces a maximum number of baggage items and a maximum overall size. +// If either limit is exceeded, excess items or bytes are dropped, and a warning is logged. +// +// Example of a single "baggage" header: +// baggage: foo=bar,baz=qux +// +// Each key and value pair is encoded and added to the existing baggage header in = format, +// joined together by commas, +func (*propagatorBaggage) injectTextMap(spanCtx ddtrace.SpanContext, writer TextMapWriter) error { + ctx, _ := spanCtx.(*spanContext) + if ctx == nil { + return nil + } + + // Copy the baggage map under the read lock to avoid data races. + ctx.mu.RLock() + baggageCopy := make(map[string]string, len(ctx.baggage)) + for k, v := range ctx.baggage { + baggageCopy[k] = v + } + ctx.mu.RUnlock() + + // If the baggage is empty, do nothing. + if len(baggageCopy) == 0 { + return nil + } + + baggageItems := make([]string, 0, len(baggageCopy)) + totalSize := 0 + count := 0 + + for key, value := range baggageCopy { + if count >= baggageMaxItems { + log.Warn("Baggage item limit exceeded. Only the first %d items will be propagated.", baggageMaxItems) + break + } + + encodedKey := encodeKey(key) + encodedValue := encodeValue(value) + item := fmt.Sprintf("%s=%s", encodedKey, encodedValue) + + itemSize := len(item) + if count > 0 { + itemSize++ // account for the comma separator + } + + if totalSize+itemSize > baggageMaxBytes { + log.Warn("Baggage size limit exceeded. Only the first %d bytes will be propagated.", baggageMaxBytes) + break + } + + baggageItems = append(baggageItems, item) + totalSize += itemSize + count++ + } + + if len(baggageItems) > 0 { + writer.Set("baggage", strings.Join(baggageItems, ",")) + } + + return nil +} + +func (p *propagatorBaggage) Extract(carrier interface{}) (ddtrace.SpanContext, error) { + switch c := carrier.(type) { + case TextMapReader: + return p.extractTextMap(c) + default: + return nil, ErrInvalidCarrier + } +} + +func (*propagatorBaggage) extractTextMap(reader TextMapReader) (ddtrace.SpanContext, error) { + var baggageHeader string + var ctx spanContext + err := reader.ForeachKey(func(k, v string) error { + if strings.ToLower(k) == "baggage" { + baggageHeader = v + } + return nil + }) + if err != nil { + return nil, err + } + + ctx.baggage = make(map[string]string) + + if baggageHeader == "" { + return &ctx, nil + } + + pairs := strings.Split(baggageHeader, ",") + for _, pair := range pairs { + pair = strings.TrimSpace(pair) + if !strings.Contains(pair, "=") { + // If a pair doesn't contain '=', treat it as invalid. + return nil, fmt.Errorf("Invalid baggage item: %s", pair) + } + + keyValue := strings.SplitN(pair, "=", 2) + rawKey := strings.TrimSpace(keyValue[0]) + rawValue := strings.TrimSpace(keyValue[1]) + + decKey, errKey := url.QueryUnescape(rawKey) + decVal, errVal := url.QueryUnescape(rawValue) + if errKey != nil || errVal != nil { + return nil, fmt.Errorf("Invalid baggage item: %s", pair) + } + ctx.baggage[decKey] = decVal + } + if len(ctx.baggage) > 0 { + atomic.StoreUint32(&ctx.hasBaggage, 1) + } + return &ctx, nil +} diff --git a/ddtrace/tracer/textmap_test.go b/ddtrace/tracer/textmap_test.go index 0dd05124c0..6ad8850df8 100644 --- a/ddtrace/tracer/textmap_test.go +++ b/ddtrace/tracer/textmap_test.go @@ -2224,11 +2224,11 @@ func TestOtelPropagator(t *testing.T) { }, { env: "nonesense", - result: "datadog,tracecontext", + result: "datadog,tracecontext,baggage", }, { env: "jaegar", - result: "datadog,tracecontext", + result: "datadog,tracecontext,baggage", }, } for _, test := range tests { @@ -2638,3 +2638,175 @@ func FuzzStringMutator(f *testing.F) { } }) } + +func TestInjectBaggagePropagator(t *testing.T) { + + assert := assert.New(t) + + propagator := NewPropagator(&PropagatorConfig{ + BaggageHeader: "baggage", + TraceHeader: "tid", + ParentHeader: "pid", + }) + tracer := newTracer(WithPropagator(propagator)) + defer tracer.Stop() + + root := tracer.StartSpan("web.request").(*span) + root.SetBaggageItem("foo", "bar") + ctx := root.Context() + headers := http.Header{} + + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + assert.Equal(headers.Get("baggage"), "foo=bar") +} + +func TestExtractBaggagePropagator(t *testing.T) { + tracer := newTracer() + defer tracer.Stop() + headers := TextMapCarrier{ + DefaultTraceIDHeader: "4", + DefaultParentIDHeader: "1", + DefaultBaggageHeader: "foo=bar", + } + s, err := tracer.Extract(headers) + assert.NoError(t, err) + got := make(map[string]string) + s.ForeachBaggageItem(func(k, v string) bool { + got[k] = v + return true + }) + assert.Len(t, got, 1) + assert.Equal(t, "bar", got["foo"]) +} + +func TestInjectBaggagePropagatorEncoding(t *testing.T) { + assert := assert.New(t) + + propagator := NewPropagator(&PropagatorConfig{ + BaggageHeader: "baggage", + TraceHeader: "tid", + ParentHeader: "pid", + }) + tracer := newTracer(WithPropagator(propagator)) + defer tracer.Stop() + + root := tracer.StartSpan("web.request").(*span) + ctx := root.Context() + ctx.(*spanContext).baggage = map[string]string{"userId": "Amélie", "serverNode": "DF 28"} + headers := http.Header{} + + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + actualBaggage := headers.Get("baggage") + // Instead of checking equality of the whole string, assert that both key/value pairs are present. + assert.Contains(actualBaggage, "userId=Am%C3%A9lie") + assert.Contains(actualBaggage, "serverNode=DF+28") +} + +func TestInjectBaggagePropagatorEncodingSpecialCharacters(t *testing.T) { + assert := assert.New(t) + + propagator := NewPropagator(&PropagatorConfig{ + BaggageHeader: "baggage", + TraceHeader: "tid", + ParentHeader: "pid", + }) + tracer := newTracer(WithPropagator(propagator)) + defer tracer.Stop() + + root := tracer.StartSpan("web.request").(*span) + ctx := root.Context() + ctx.(*spanContext).baggage = map[string]string{",;\\()/:<=>?@[]{}": ",;\\"} + headers := http.Header{} + + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + assert.Equal(headers.Get("baggage"), "%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%2C%3B%5C") +} + +func TestExtractBaggagePropagatorDecoding(t *testing.T) { + tracer := newTracer() + defer tracer.Stop() + headers := TextMapCarrier{ + DefaultTraceIDHeader: "4", + DefaultParentIDHeader: "1", + DefaultBaggageHeader: "userId=Am%C3%A9lie,serverNode=DF+28", + } + s, err := tracer.Extract(headers) + assert.NoError(t, err) + got := make(map[string]string) + s.ForeachBaggageItem(func(k, v string) bool { + got[k] = v + return true + }) + assert.Len(t, got, 2) + assert.Equal(t, "Amélie", got["userId"]) + assert.Equal(t, "DF 28", got["serverNode"]) +} + +func TestInjectBaggageMaxItems(t *testing.T) { + assert := assert.New(t) + + propagator := NewPropagator(&PropagatorConfig{ + BaggageHeader: "baggage", + }) + tracer := newTracer(WithPropagator(propagator)) + defer tracer.Stop() + + root := tracer.StartSpan("web.request").(*span) + ctx := root.Context() + + baggageItems := make(map[string]string) + for i := 0; i < baggageMaxItems+2; i++ { + baggageItems[fmt.Sprintf("key%d", i)] = fmt.Sprintf("val%d", i) + } + + ctx.(*spanContext).baggage = baggageItems + headers := http.Header{} + + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + headerValue := headers.Get("baggage") + items := strings.Split(headerValue, ",") + assert.Equal(baggageMaxItems, len(items)) +} + +func TestInjectBaggageMaxBytes(t *testing.T) { + assert := assert.New(t) + + propagator := NewPropagator(&PropagatorConfig{ + BaggageHeader: "baggage", + }) + tracer := newTracer(WithPropagator(propagator)) + defer tracer.Stop() + + root := tracer.StartSpan("web.request").(*span) + ctx := root.Context() + + baggageItems := make(map[string]string) + baggageItems = map[string]string{ + "key0": "o", + "key1": strings.Repeat("a", baggageMaxBytes/3), + "key2": strings.Repeat("b", baggageMaxBytes/3), + "key3": strings.Repeat("c", baggageMaxBytes/3), + } + + ctx.(*spanContext).baggage = baggageItems + headers := http.Header{} + + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + headerValue := headers.Get("baggage") + headerSize := len([]byte(headerValue)) + assert.LessOrEqual(headerSize, baggageMaxBytes) +} diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index ad0b75009f..2c7ddeab75 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -936,6 +936,78 @@ func TestPropagationDefaults(t *testing.T) { assert.Equal(*child.context.trace.priority, -1.) } +func TestPropagationDefaultIncludesBaggage(t *testing.T) { + assert := assert.New(t) + + tracer := newTracer() + defer tracer.Stop() + root := tracer.StartSpan("web.request").(*span) + root.SetBaggageItem("foo", "bar") + root.SetTag(ext.SamplingPriority, -1) + ctx := root.Context().(*spanContext) + headers := http.Header{} + + // inject the spanContext + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + tid := strconv.FormatUint(root.TraceID, 10) + pid := strconv.FormatUint(root.SpanID, 10) + + assert.Equal(headers.Get(DefaultTraceIDHeader), tid) + assert.Equal(headers.Get(DefaultParentIDHeader), pid) + assert.Equal(headers.Get(DefaultPriorityHeader), "-1") + assert.Equal(headers.Get(DefaultBaggageHeader), "foo=bar") + + // retrieve the spanContext + propagated, err := tracer.Extract(carrier) + assert.Nil(err) + pctx := propagated.(*spanContext) + + // compare if there is a Context match + assert.Equal(ctx.traceID, pctx.traceID) + assert.Equal(ctx.spanID, pctx.spanID) + assert.Equal(*ctx.trace.priority, -1.) + assert.Equal(ctx.baggage, pctx.baggage) + + // ensure a child can be created + child := tracer.StartSpan("db.query", ChildOf(propagated)).(*span) + + assert.NotEqual(uint64(0), child.TraceID) + assert.NotEqual(uint64(0), child.SpanID) + assert.Equal(root.SpanID, child.ParentID) + assert.Equal(root.TraceID, child.ParentID) + assert.Equal(*child.context.trace.priority, -1.) +} + +func TestPropagationStyleOnlyBaggage(t *testing.T) { + t.Setenv(headerPropagationStyle, "baggage") + assert := assert.New(t) + + tracer := newTracer() + defer tracer.Stop() + root := tracer.StartSpan("web.request").(*span) + root.SetBaggageItem("foo", "bar") + ctx := root.Context().(*spanContext) + headers := http.Header{} + + // inject the spanContext + carrier := HTTPHeadersCarrier(headers) + err := tracer.Inject(ctx, carrier) + assert.Nil(err) + + assert.Equal(headers.Get(DefaultBaggageHeader), "foo=bar") + + // retrieve the spanContext + propagated, err := tracer.Extract(carrier) + assert.Nil(err) + pctx := propagated.(*spanContext) + + // compare if there is a Context match + assert.Equal(ctx.baggage, pctx.baggage) +} + func TestTracerSamplingPriorityPropagation(t *testing.T) { assert := assert.New(t) tracer := newTracer()