Skip to content

Commit 30c57b1

Browse files
committed
implement DD_TRACE_INTEGRATION_TAGS + contrib/net/http support
1 parent 137578f commit 30c57b1

File tree

8 files changed

+282
-35
lines changed

8 files changed

+282
-35
lines changed

contrib/net/http/internal/config/config.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ const (
2323
)
2424

2525
type Config struct {
26-
SpanOpts []ddtrace.StartSpanOption
27-
FinishOpts []ddtrace.FinishOption
28-
IgnoreRequest func(*http.Request) bool
29-
ResourceNamer func(*http.Request) string
30-
IsStatusError func(int) bool
31-
HeaderTags *internal.LockMap
32-
ServiceName string
33-
AnalyticsRate float64
26+
SpanOpts []ddtrace.StartSpanOption
27+
FinishOpts []ddtrace.FinishOption
28+
IgnoreRequest func(*http.Request) bool
29+
ResourceNamer func(*http.Request) string
30+
IsStatusError func(int) bool
31+
HeaderTags *internal.LockMap
32+
ServiceName string
33+
AnalyticsRate float64
34+
IntegrationTags *internal.IntegrationTags
3435
}
3536

3637
// Option represents an option that can be passed to NewServeMux or WrapHandler.
@@ -52,6 +53,7 @@ func Default() *Config {
5253
}
5354
cfg.IgnoreRequest = func(_ *http.Request) bool { return false }
5455
cfg.ResourceNamer = func(_ *http.Request) string { return "" }
56+
cfg.IntegrationTags = globalconfig.IntegrationTags()
5557

5658
return cfg
5759
}

contrib/net/http/internal/wrap/mux.go

+32
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package wrap
77

88
import (
9+
"net"
910
"net/http"
1011

1112
"gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/httptrace"
@@ -55,6 +56,11 @@ func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5556
so := make([]ddtrace.StartSpanOption, len(mux.cfg.SpanOpts), len(mux.cfg.SpanOpts)+1)
5657
copy(so, mux.cfg.SpanOpts)
5758
so = append(so, httptrace.HeaderTagsFromRequest(r, mux.cfg.HeaderTags))
59+
60+
for k, v := range serverIntegrationTags(mux.cfg, r) {
61+
so = append(so, tracer.Tag(k, v))
62+
}
63+
5864
TraceAndServe(mux.ServeMux, w, r, &httptrace.ServeConfig{
5965
Service: mux.cfg.ServiceName,
6066
Resource: resource,
@@ -64,3 +70,29 @@ func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6470
RouteParams: patternValues(pattern, r),
6571
})
6672
}
73+
74+
func serverIntegrationTags(cfg *config.Config, req *http.Request) map[string]string {
75+
host, port := serverHostPort(req)
76+
77+
q := map[string]string{
78+
"span.kind": ext.SpanKindServer,
79+
"server.address": host,
80+
"server.port": port,
81+
"url.path": req.URL.Path,
82+
"http.request.method": req.Method,
83+
}
84+
return cfg.IntegrationTags.Get(config.ComponentName, q)
85+
}
86+
87+
func serverHostPort(req *http.Request) (string, string) {
88+
ctxLocalAddr := req.Context().Value(http.LocalAddrContextKey)
89+
if ctxLocalAddr == nil {
90+
return "", ""
91+
}
92+
addr, ok := ctxLocalAddr.(net.Addr)
93+
if !ok {
94+
return "", ""
95+
}
96+
host, port, _ := net.SplitHostPort(addr.String())
97+
return host, port
98+
}

contrib/net/http/option.go

+22-20
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,19 @@ type RoundTripperBeforeFunc func(*http.Request, ddtrace.Span)
114114
// RoundTrip is made. It is possible for the http Response to be nil.
115115
type RoundTripperAfterFunc func(*http.Response, ddtrace.Span)
116116
type roundTripperConfig struct {
117-
before RoundTripperBeforeFunc
118-
after RoundTripperAfterFunc
119-
analyticsRate float64
120-
serviceName string
121-
resourceNamer func(req *http.Request) string
122-
spanNamer func(req *http.Request) string
123-
ignoreRequest func(*http.Request) bool
124-
spanOpts []ddtrace.StartSpanOption
125-
propagation bool
126-
errCheck func(err error) bool
127-
queryString bool // reports whether the query string is included in the URL tag for http client spans
128-
isStatusError func(statusCode int) bool
117+
before RoundTripperBeforeFunc
118+
after RoundTripperAfterFunc
119+
analyticsRate float64
120+
serviceName string
121+
resourceNamer func(req *http.Request) string
122+
spanNamer func(req *http.Request) string
123+
ignoreRequest func(*http.Request) bool
124+
spanOpts []ddtrace.StartSpanOption
125+
propagation bool
126+
errCheck func(err error) bool
127+
queryString bool // reports whether the query string is included in the URL tag for http client spans
128+
isStatusError func(statusCode int) bool
129+
integrationTags *internal.IntegrationTags
129130
}
130131

131132
func newRoundTripperConfig() *roundTripperConfig {
@@ -138,14 +139,15 @@ func newRoundTripperConfig() *roundTripperConfig {
138139
}
139140

140141
c := &roundTripperConfig{
141-
serviceName: namingschema.ServiceNameOverrideV0("", ""),
142-
analyticsRate: globalconfig.AnalyticsRate(),
143-
resourceNamer: defaultResourceNamer,
144-
propagation: true,
145-
spanNamer: defaultSpanNamer,
146-
ignoreRequest: func(_ *http.Request) bool { return false },
147-
queryString: internal.BoolEnv(envClientQueryStringEnabled, true),
148-
isStatusError: isClientError,
142+
serviceName: namingschema.ServiceNameOverrideV0("", ""),
143+
analyticsRate: globalconfig.AnalyticsRate(),
144+
resourceNamer: defaultResourceNamer,
145+
propagation: true,
146+
spanNamer: defaultSpanNamer,
147+
ignoreRequest: func(_ *http.Request) bool { return false },
148+
queryString: internal.BoolEnv(envClientQueryStringEnabled, true),
149+
isStatusError: isClientError,
150+
integrationTags: globalconfig.IntegrationTags(),
149151
}
150152
v := os.Getenv(envClientErrorStatuses)
151153
if fn := httptrace.GetErrorCodesFromInput(v); fn != nil {

contrib/net/http/roundtripper.go

+16
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er
5757
if len(rt.cfg.spanOpts) > 0 {
5858
opts = append(opts, rt.cfg.spanOpts...)
5959
}
60+
if tags := rt.clientIntegrationTags(req); tags != nil {
61+
for k, v := range tags {
62+
opts = append(opts, tracer.Tag(k, v))
63+
}
64+
}
6065
span, ctx := tracer.StartSpanFromContext(req.Context(), spanName, opts...)
6166
defer func() {
6267
if rt.cfg.after != nil {
@@ -108,6 +113,17 @@ func (rt *roundTripper) Unwrap() http.RoundTripper {
108113
return rt.base
109114
}
110115

116+
func (rt *roundTripper) clientIntegrationTags(req *http.Request) map[string]string {
117+
q := map[string]string{
118+
"span.kind": "client",
119+
"server.address": req.URL.Hostname(),
120+
"server.port": req.URL.Port(),
121+
"url.path": req.URL.Path,
122+
"http.request.method": req.Method,
123+
}
124+
return rt.cfg.integrationTags.Get(config.ComponentName, q)
125+
}
126+
111127
// WrapRoundTripper returns a new RoundTripper which traces all requests sent
112128
// over the transport.
113129
func WrapRoundTripper(rt http.RoundTripper, opts ...RoundTripperOption) http.RoundTripper {

contrib/net/http/roundtripper_test.go

+93
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
2929
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/waf/addresses"
3030
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
31+
internallog "gopkg.in/DataDog/dd-trace-go.v1/internal/log"
3132

3233
"github.com/stretchr/testify/assert"
3334
"github.com/stretchr/testify/require"
@@ -830,3 +831,95 @@ func TestAppsec(t *testing.T) {
830831
})
831832
}
832833
}
834+
835+
func TestIntegrationTags(t *testing.T) {
836+
srv1 := httptest.NewServer(nil)
837+
defer srv1.Close()
838+
srv1URL, err := url.Parse(srv1.URL)
839+
require.NoError(t, err)
840+
841+
srv2 := httptest.NewServer(nil)
842+
defer srv2.Close()
843+
srv2URL, err := url.Parse(srv2.URL)
844+
require.NoError(t, err)
845+
846+
integrationTagsEnv := `[
847+
{
848+
"component": "net/http",
849+
"query": [
850+
{"span.kind": "server", "server.port": "%s"},
851+
{"span.kind": "client", "server.port": "%s", "http.request.method": "GET"}
852+
],
853+
"tags": {
854+
"tag1": "val1"
855+
}
856+
},
857+
{
858+
"component": "net/http",
859+
"query": [
860+
{"span.kind": "server", "server.port": "%s"}
861+
],
862+
"tags": {
863+
"tag2": "val2"
864+
}
865+
}
866+
]`
867+
t.Setenv("DD_TRACE_INTEGRATION_TAGS", fmt.Sprintf(integrationTagsEnv, srv1URL.Port(), srv2URL.Port(), srv2URL.Port()))
868+
869+
// force start-stop the real tracer to load the config
870+
tracer.Start(tracer.WithLogger(internallog.DiscardLogger{}))
871+
tracer.Stop()
872+
873+
mt := mocktracer.Start()
874+
defer mt.Stop()
875+
876+
mux := NewServeMux()
877+
mux.HandleFunc("/200", handler200)
878+
mux.HandleFunc("/500", handler500)
879+
880+
srv1.Config.Handler = mux
881+
srv2.Config.Handler = mux
882+
883+
client := WrapClient(&http.Client{})
884+
885+
req, err := http.NewRequest(http.MethodGet, srv1.URL+"/200", nil)
886+
require.NoError(t, err)
887+
resp, err := client.Do(req)
888+
require.NoError(t, err)
889+
defer resp.Body.Close()
890+
891+
req, err = http.NewRequest(http.MethodGet, srv2.URL+"/200", nil)
892+
require.NoError(t, err)
893+
resp, err = client.Do(req)
894+
require.NoError(t, err)
895+
defer resp.Body.Close()
896+
897+
spans := mt.FinishedSpans()
898+
require.Len(t, spans, 4)
899+
900+
spanSrv1 := spans[0]
901+
spanClient1 := spans[1]
902+
spanSrv2 := spans[2]
903+
spanClient2 := spans[3]
904+
905+
assert.Equal(t, "server", spanSrv1.Tag("span.kind"))
906+
assert.Equal(t, "server", spanSrv2.Tag("span.kind"))
907+
assert.Equal(t, "client", spanClient1.Tag("span.kind"))
908+
assert.Equal(t, "client", spanClient2.Tag("span.kind"))
909+
910+
// server1
911+
assert.Equal(t, "val1", spanSrv1.Tag("tag1"), "server1 missing tag1")
912+
assert.Nil(t, spanSrv1.Tag("tag2"), "server1 should not have tag2")
913+
914+
// client1
915+
assert.Nil(t, spanClient1.Tag("tag1"), "client should not have tag1")
916+
assert.Nil(t, spanClient1.Tag("tag2"), "client should not have tag2")
917+
918+
// server2
919+
assert.Nil(t, spanSrv2.Tag("tag1"), "server2 should not have tag1")
920+
assert.Equal(t, "val2", spanSrv2.Tag("tag2"), "server2 missing tag2")
921+
922+
// client2
923+
assert.Equal(t, "val1", spanClient2.Tag("tag1"), "client2 missing tag1")
924+
assert.Nil(t, spanClient2.Tag("tag2"), "client2 should not have tag2")
925+
}

ddtrace/tracer/option.go

+13
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,9 @@ type config struct {
308308

309309
// traceRateLimitPerSecond specifies the rate limit for traces.
310310
traceRateLimitPerSecond float64
311+
312+
// integrationTags allows to set custom tags on integrations.
313+
integrationTags []internal.IntegrationTagsRule
311314
}
312315

313316
// orchestrionConfig contains Orchestrion configuration.
@@ -605,6 +608,16 @@ func newConfig(opts ...StartOption) *config {
605608
c.runtimeMetricsV2 = false
606609
}
607610

611+
if iTagsStr := os.Getenv("DD_TRACE_INTEGRATION_TAGS"); iTagsStr != "" {
612+
var iTagsRules []internal.IntegrationTagsRule
613+
if err := json.Unmarshal([]byte(iTagsStr), &iTagsRules); err != nil {
614+
log.Warn("failed to parse DD_TRACE_INTEGRATION_TAGS: %v", err)
615+
} else {
616+
c.integrationTags = iTagsRules
617+
globalconfig.SetIntegrationTagsRules(iTagsRules)
618+
}
619+
}
620+
608621
return c
609622
}
610623

internal/globalconfig/globalconfig.go

+24-7
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@ var cfg = &config{
2020
analyticsRate: math.NaN(),
2121
runtimeID: uuid.New().String(),
2222
headersAsTags: internal.NewLockMap(map[string]string{}),
23+
integrationTags: &internal.IntegrationTags{
24+
Rules: nil,
25+
Cache: make(map[string]map[string]string),
26+
},
2327
}
2428

2529
type config struct {
26-
mu sync.RWMutex
27-
analyticsRate float64
28-
serviceName string
29-
runtimeID string
30-
headersAsTags *internal.LockMap
31-
dogstatsdAddr string
32-
statsTags []string
30+
mu sync.RWMutex
31+
analyticsRate float64
32+
serviceName string
33+
runtimeID string
34+
headersAsTags *internal.LockMap
35+
dogstatsdAddr string
36+
statsTags []string
37+
integrationTags *internal.IntegrationTags
3338
}
3439

3540
// AnalyticsRate returns the sampling rate at which events should be marked. It uses
@@ -130,3 +135,15 @@ func HeaderTagsLen() int {
130135
func ClearHeaderTags() {
131136
cfg.headersAsTags.Clear()
132137
}
138+
139+
func SetIntegrationTagsRules(rules []internal.IntegrationTagsRule) {
140+
cfg.mu.Lock()
141+
defer cfg.mu.Unlock()
142+
cfg.integrationTags.Rules = rules
143+
}
144+
145+
func IntegrationTags() *internal.IntegrationTags {
146+
cfg.mu.RLock()
147+
defer cfg.mu.RUnlock()
148+
return cfg.integrationTags
149+
}

0 commit comments

Comments
 (0)