Skip to content

Commit 7bbcff5

Browse files
rarguelloFdarccio
andauthored
feat: add process tags to tracing payloads (#3566)
Co-authored-by: Dario Castañé <[email protected]>
1 parent f2441d3 commit 7bbcff5

File tree

5 files changed

+255
-0
lines changed

5 files changed

+255
-0
lines changed

ddtrace/tracer/span.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,8 @@ const (
963963
keyPeerServiceRemappedFrom = "_dd.peer.service.remapped_from"
964964
// keyBaseService contains the globally configured tracer service name. It is only set for spans that override it.
965965
keyBaseService = "_dd.base_service"
966+
// keyProcessTags contains a list of process tags to indentify the service.
967+
keyProcessTags = "_dd.tags.process"
966968
)
967969

968970
// The following set of tags is used for user monitoring and set through calls to span.SetUser().

ddtrace/tracer/spancontext.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/DataDog/dd-trace-go/v2/ddtrace/internal/tracerstats"
2121
sharedinternal "github.com/DataDog/dd-trace-go/v2/internal"
2222
"github.com/DataDog/dd-trace-go/v2/internal/log"
23+
"github.com/DataDog/dd-trace-go/v2/internal/processtags"
2324
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
2425
"github.com/DataDog/dd-trace-go/v2/internal/telemetry"
2526
)
@@ -488,6 +489,9 @@ func (t *trace) setTraceTags(s *Span) {
488489
if s.context != nil && s.context.traceID.HasUpper() {
489490
s.setMeta(keyTraceID128, s.context.traceID.UpperHex())
490491
}
492+
if pTags := processtags.GlobalTags().String(); pTags != "" {
493+
s.setMeta(keyProcessTags, pTags)
494+
}
491495
}
492496

493497
// finishedOne acknowledges that another span in the trace has finished, and checks

ddtrace/tracer/spancontext_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"errors"
1111
"fmt"
1212
"math"
13+
"strconv"
1314
"sync"
1415
"testing"
1516
"time"
1617

1718
"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
1819
"github.com/DataDog/dd-trace-go/v2/internal/globalconfig"
1920
"github.com/DataDog/dd-trace-go/v2/internal/log"
21+
"github.com/DataDog/dd-trace-go/v2/internal/processtags"
2022
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
2123
"github.com/DataDog/dd-trace-go/v2/internal/telemetry"
2224
"github.com/DataDog/dd-trace-go/v2/internal/telemetry/telemetrytest"
@@ -1025,6 +1027,59 @@ func TestSpanIDHexEncoded(t *testing.T) {
10251027
assert.Equal(t, spanIDHexEncoded(math.MaxUint64, 16), sid)
10261028
}
10271029

1030+
func TestSpanProcessTags(t *testing.T) {
1031+
testCases := []struct {
1032+
name string
1033+
enabled bool
1034+
}{
1035+
{
1036+
name: "disabled",
1037+
enabled: false,
1038+
},
1039+
{
1040+
name: "enabled",
1041+
enabled: true,
1042+
},
1043+
}
1044+
1045+
for _, tc := range testCases {
1046+
t.Run(tc.name, func(t *testing.T) {
1047+
t.Setenv("DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", strconv.FormatBool(tc.enabled))
1048+
processtags.Reload()
1049+
tracer, transport, flush, stop, err := startTestTracer(t)
1050+
assert.NoError(t, err)
1051+
t.Cleanup(stop)
1052+
1053+
p := tracer.StartSpan("p")
1054+
c1 := p.StartChild("c1")
1055+
c2 := p.StartChild("c2")
1056+
c11 := c1.StartChild("c1-1")
1057+
1058+
c11.Finish()
1059+
c2.Finish()
1060+
c1.Finish()
1061+
p.Finish()
1062+
1063+
flush(1)
1064+
traces := transport.Traces()
1065+
require.Len(t, traces, 1)
1066+
require.Len(t, traces[0], 4)
1067+
1068+
root := traces[0][0]
1069+
assert.Equal(t, "p", root.name)
1070+
if tc.enabled {
1071+
assert.NotEmpty(t, root.meta["_dd.tags.process"])
1072+
} else {
1073+
assert.NotContains(t, root.meta, "_dd.tags.process")
1074+
}
1075+
1076+
for _, s := range traces[0][1:] {
1077+
assert.NotContains(t, s.meta, "_dd.tags.process")
1078+
}
1079+
})
1080+
}
1081+
}
1082+
10281083
func BenchmarkSpanIDHexEncoded(b *testing.B) {
10291084
for n := 0; n < b.N; n++ {
10301085
_ = spanIDHexEncoded(32, 16)

internal/processtags/processtags.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025 Datadog, Inc.
5+
6+
package processtags
7+
8+
import (
9+
"os"
10+
"path/filepath"
11+
"sort"
12+
"strings"
13+
"sync"
14+
15+
"github.com/DataDog/datadog-agent/pkg/trace/traceutil"
16+
17+
"github.com/DataDog/dd-trace-go/v2/internal"
18+
"github.com/DataDog/dd-trace-go/v2/internal/log"
19+
)
20+
21+
const envProcessTagsEnabled = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED"
22+
23+
const (
24+
tagEntrypointName = "entrypoint.name"
25+
tagEntrypointBasedir = "entrypoint.basedir"
26+
tagEntrypointWorkdir = "entrypoint.workdir"
27+
tagEntrypointType = "entrypoint.type"
28+
)
29+
30+
const (
31+
entrypointTypeExecutable = "executable"
32+
)
33+
34+
var (
35+
enabled bool
36+
pTags *ProcessTags
37+
)
38+
39+
func init() {
40+
Reload()
41+
}
42+
43+
type ProcessTags struct {
44+
mu sync.RWMutex
45+
tags map[string]string
46+
str string
47+
slice []string
48+
}
49+
50+
// String returns the string representation of the process tags.
51+
func (p *ProcessTags) String() string {
52+
if p == nil {
53+
return ""
54+
}
55+
p.mu.RLock()
56+
defer p.mu.RUnlock()
57+
return p.str
58+
}
59+
60+
// Slice returns the string slice representation of the process tags.
61+
func (p *ProcessTags) Slice() []string {
62+
if p == nil {
63+
return nil
64+
}
65+
p.mu.RLock()
66+
defer p.mu.RUnlock()
67+
return p.slice
68+
}
69+
70+
func (p *ProcessTags) merge(newTags map[string]string) {
71+
if len(newTags) == 0 {
72+
return
73+
}
74+
pTags.mu.Lock()
75+
defer pTags.mu.Unlock()
76+
77+
if p.tags == nil {
78+
p.tags = make(map[string]string)
79+
}
80+
for k, v := range newTags {
81+
p.tags[k] = v
82+
}
83+
84+
// loop over the sorted map keys so the resulting string and slice versions are created consistently.
85+
keys := make([]string, 0, len(p.tags))
86+
for k := range p.tags {
87+
keys = append(keys, k)
88+
}
89+
sort.Strings(keys)
90+
91+
tagsSlice := make([]string, 0, len(p.tags))
92+
var b strings.Builder
93+
first := true
94+
for _, k := range keys {
95+
val := p.tags[k]
96+
if !first {
97+
b.WriteByte(',')
98+
}
99+
first = false
100+
keyVal := traceutil.NormalizeTag(k + ":" + val)
101+
b.WriteString(keyVal)
102+
tagsSlice = append(tagsSlice, keyVal)
103+
}
104+
p.slice = tagsSlice
105+
p.str = b.String()
106+
}
107+
108+
// Reload initializes the configuration and process tags collection. This is useful for tests.
109+
func Reload() {
110+
enabled = internal.BoolEnv(envProcessTagsEnabled, false)
111+
if !enabled {
112+
return
113+
}
114+
pTags = &ProcessTags{}
115+
tags := collect()
116+
if len(tags) > 0 {
117+
Add(tags)
118+
}
119+
}
120+
121+
func collect() map[string]string {
122+
tags := make(map[string]string)
123+
execPath, err := os.Executable()
124+
if err != nil {
125+
log.Debug("failed to get binary path: %v", err)
126+
} else {
127+
baseDirName := filepath.Base(filepath.Dir(execPath))
128+
tags[tagEntrypointName] = filepath.Base(execPath)
129+
tags[tagEntrypointBasedir] = baseDirName
130+
tags[tagEntrypointType] = entrypointTypeExecutable
131+
}
132+
wd, err := os.Getwd()
133+
if err != nil {
134+
log.Debug("failed to get working directory: %v", err)
135+
} else {
136+
tags[tagEntrypointWorkdir] = filepath.Base(wd)
137+
}
138+
return tags
139+
}
140+
141+
// GlobalTags returns the global process tags.
142+
func GlobalTags() *ProcessTags {
143+
if !enabled {
144+
return nil
145+
}
146+
return pTags
147+
}
148+
149+
// Add merges the given tags into the global processTags map.
150+
func Add(tags map[string]string) {
151+
if !enabled {
152+
return
153+
}
154+
pTags.merge(tags)
155+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025 Datadog, Inc.
5+
6+
package processtags
7+
8+
import (
9+
"github.com/stretchr/testify/assert"
10+
"regexp"
11+
"strings"
12+
"testing"
13+
)
14+
15+
func TestProcessTags(t *testing.T) {
16+
t.Run("enabled", func(t *testing.T) {
17+
t.Setenv("DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "true")
18+
Reload()
19+
20+
wantTagsRe := regexp.MustCompile(`^entrypoint\.basedir:[a-zA-Z0-9._-]+,entrypoint\.name:[a-zA-Z0-9._-]+,entrypoint.type:executable,entrypoint\.workdir:[a-zA-Z0-9._-]+$`)
21+
p := GlobalTags()
22+
assert.NotNil(t, p)
23+
assert.NotEmpty(t, p.String())
24+
assert.Regexp(t, wantTagsRe, p.String(), "wrong string serialized tags")
25+
26+
assert.NotEmpty(t, p.Slice())
27+
assert.Regexp(t, wantTagsRe, strings.Join(p.Slice(), ","), "wrong slice serialized tags")
28+
})
29+
30+
t.Run("disabled", func(t *testing.T) {
31+
t.Setenv("DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED", "false")
32+
Reload()
33+
34+
p := GlobalTags()
35+
assert.Nil(t, p)
36+
assert.Empty(t, p.String())
37+
assert.Empty(t, p.Slice())
38+
})
39+
}

0 commit comments

Comments
 (0)