Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions ddtrace/tracer/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,8 @@ const (
keyPeerServiceRemappedFrom = "_dd.peer.service.remapped_from"
// keyBaseService contains the globally configured tracer service name. It is only set for spans that override it.
keyBaseService = "_dd.base_service"
// keyProcessTags contains a list of process tags to indentify the service.
Copy link
Member

Choose a reason for hiding this comment

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

small typo. fix here: #3823

keyProcessTags = "_dd.tags.process"
)

// The following set of tags is used for user monitoring and set through calls to span.SetUser().
Expand Down
4 changes: 4 additions & 0 deletions ddtrace/tracer/spancontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/DataDog/dd-trace-go/v2/ddtrace/internal/tracerstats"
sharedinternal "github.com/DataDog/dd-trace-go/v2/internal"
"github.com/DataDog/dd-trace-go/v2/internal/log"
"github.com/DataDog/dd-trace-go/v2/internal/processtags"
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
"github.com/DataDog/dd-trace-go/v2/internal/telemetry"
)
Expand Down Expand Up @@ -488,6 +489,9 @@ func (t *trace) setTraceTags(s *Span) {
if s.context != nil && s.context.traceID.HasUpper() {
s.setMeta(keyTraceID128, s.context.traceID.UpperHex())
}
if pTags := processtags.Get().String(); pTags != "" {
s.setMeta(keyProcessTags, pTags)
}
}

// finishedOne acknowledges that another span in the trace has finished, and checks
Expand Down
55 changes: 55 additions & 0 deletions ddtrace/tracer/spancontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
"errors"
"fmt"
"math"
"strconv"
"sync"
"testing"
"time"

"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
"github.com/DataDog/dd-trace-go/v2/internal/globalconfig"
"github.com/DataDog/dd-trace-go/v2/internal/log"
"github.com/DataDog/dd-trace-go/v2/internal/processtags"
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
"github.com/DataDog/dd-trace-go/v2/internal/telemetry"
"github.com/DataDog/dd-trace-go/v2/internal/telemetry/telemetrytest"
Expand Down Expand Up @@ -1025,6 +1027,59 @@ func TestSpanIDHexEncoded(t *testing.T) {
assert.Equal(t, spanIDHexEncoded(math.MaxUint64, 16), sid)
}

func TestSpanProcessTags(t *testing.T) {
testCases := []struct {
name string
enabled bool
}{
{
name: "disabled",
enabled: false,
},
{
name: "enabled",
enabled: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED", strconv.FormatBool(tc.enabled))
processtags.ResetConfig()
tracer, transport, flush, stop, err := startTestTracer(t)
assert.NoError(t, err)
t.Cleanup(stop)

p := tracer.StartSpan("p")
c1 := p.StartChild("c1")
c2 := p.StartChild("c2")
c11 := c1.StartChild("c1-1")

c11.Finish()
c2.Finish()
c1.Finish()
p.Finish()

flush(1)
traces := transport.Traces()
require.Len(t, traces, 1)
require.Len(t, traces[0], 4)

root := traces[0][0]
assert.Equal(t, "p", root.name)
if tc.enabled {
assert.NotEmpty(t, root.meta["_dd.tags.process"])
} else {
assert.NotContains(t, root.meta, "_dd.tags.process")
}

for _, s := range traces[0][1:] {
assert.NotContains(t, s.meta, "_dd.tags.process")
}
})
}
}

func BenchmarkSpanIDHexEncoded(b *testing.B) {
for n := 0; n < b.N; n++ {
_ = spanIDHexEncoded(32, 16)
Expand Down
138 changes: 138 additions & 0 deletions internal/processtags/processtags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package processtags

import (
"os"
"path/filepath"
"strings"
"sync"

"github.com/DataDog/datadog-agent/pkg/trace/traceutil"

"github.com/DataDog/dd-trace-go/v2/internal"
"github.com/DataDog/dd-trace-go/v2/internal/log"
)

const (
tagEntrypointName = "entrypoint.name"
tagEntrypointBasedir = "entrypoint.basedir"
tagEntrypointWorkdir = "entrypoint.workdir"
)

var (
enabled bool
pTags *ProcessTags
)

func init() {
ResetConfig()
}

type ProcessTags struct {
mu sync.RWMutex
tags map[string]string
str string
slice []string
}

// String returns the string representation of the process tags.
func (p *ProcessTags) String() string {
if p == nil {
return ""
}
p.mu.RLock()
defer p.mu.RUnlock()
return p.str
}

// Slice returns the string slice representation of the process tags.
func (p *ProcessTags) Slice() []string {
if p == nil {
return nil
}
p.mu.RLock()
defer p.mu.RUnlock()
return p.slice
}

func (p *ProcessTags) mergeTags(newTags map[string]string) {
if len(newTags) == 0 {
return
}
pTags.mu.Lock()
defer pTags.mu.Unlock()

if p.tags == nil {
p.tags = make(map[string]string)
}
for k, v := range newTags {
p.tags[k] = v
}

tagsSlice := make([]string, 0, len(p.tags))
var b strings.Builder
first := true
for k, val := range p.tags {
if !first {
b.WriteByte(',')
}
first = false
keyVal := traceutil.NormalizeTag(k + ":" + val)
b.WriteString(keyVal)
tagsSlice = append(tagsSlice, keyVal)
}
p.slice = tagsSlice
p.str = b.String()
}

// ResetConfig initializes the configuration and process tags collection. This is useful for tests.
func ResetConfig() {
enabled = internal.BoolEnv("DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED", false)
if !enabled {
return
}
pTags = &ProcessTags{}
tags := collectInitialProcessTags()
if len(tags) > 0 {
AddTags(tags)
}
}

func collectInitialProcessTags() map[string]string {
tags := make(map[string]string)
execPath, err := os.Executable()
if err != nil {
log.Debug("failed to get binary path: %v", err)
} else {
baseDirName := filepath.Base(filepath.Dir(execPath))
tags[tagEntrypointName] = filepath.Base(execPath)
tags[tagEntrypointBasedir] = baseDirName
}
wd, err := os.Getwd()
if err != nil {
log.Debug("failed to get working directory: %v", err)
} else {
tags[tagEntrypointWorkdir] = filepath.Base(wd)
}
return tags
}

// Get returns the global process tags.
func Get() *ProcessTags {
if !enabled {
return nil
}
return pTags
}

// AddTags merges the given tags into the global processTags map.
func AddTags(tags map[string]string) {
if !enabled {
return
}
pTags.mergeTags(tags)
}
33 changes: 33 additions & 0 deletions internal/processtags/processtags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025 Datadog, Inc.

package processtags

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestProcessTags(t *testing.T) {
t.Run("enabled", func(t *testing.T) {
t.Setenv("DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED", "true")
ResetConfig()

p := Get()
assert.NotNil(t, p)
assert.NotEmpty(t, p.String())
assert.NotEmpty(t, p.Slice())
})

t.Run("disabled", func(t *testing.T) {
t.Setenv("DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED", "false")
ResetConfig()

p := Get()
assert.Nil(t, p)
assert.Empty(t, p.String())
assert.Empty(t, p.Slice())
})
}
Loading