Skip to content

Commit

Permalink
internal: implement new tag for trace source (#3230)
Browse files Browse the repository at this point in the history
  • Loading branch information
genesor authored Feb 28, 2025
1 parent e378af5 commit be2813a
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ jobs:
scenario: APPSEC_LOW_WAF_TIMEOUT
- weblog-variant: net-http
scenario: APPSEC_STANDALONE
- weblog-variant: net-http
scenario: APPSEC_STANDALONE_V2
- weblog-variant: net-http
scenario: APPSEC_META_STRUCT_DISABLED
- weblog-variant: net-http
Expand Down
3 changes: 1 addition & 2 deletions ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,8 +589,7 @@ func newConfig(opts ...StartOption) *config {
globalTagsOrigin := c.globalTags.cfgOrigin
c.initGlobalTags(c.globalTags.get(), globalTagsOrigin)

// TODO: change the name once APM Platform RFC is approved
if internal.BoolEnv("DD_EXPERIMENTAL_APPSEC_STANDALONE_ENABLED", false) {
if !internal.BoolEnv("DD_APM_TRACING_ENABLED", true) {
// Enable tracing as transport layer mode
// This means to stop sending trace metrics, send one trace per minute and those force-kept by other products
// using the tracer as transport layer for their data. And finally adding the _dd.apm.enabled=0 tag to all traces
Expand Down
28 changes: 27 additions & 1 deletion ddtrace/tracer/propagating_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

package tracer

import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

func (t *trace) hasPropagatingTag(k string) bool {
t.mu.RLock()
defer t.mu.RUnlock()
Expand All @@ -25,6 +30,27 @@ func (t *trace) setPropagatingTag(key, value string) {
t.setPropagatingTagLocked(key, value)
}

func (t *trace) setTraceSourcePropagatingTag(key string, value internal.TraceSource) {
t.mu.RLock()
defer t.mu.RUnlock()

// If there is already a TraceSource value set in the trace
// we need to add the new value to the bitmask.
if source := t.propagatingTags[key]; source != "" {
tSource, err := internal.ParseTraceSource(source)
if err != nil {
log.Error("failed to parse trace source tag: %v", err)
}

tSource |= value

t.setPropagatingTagLocked(key, tSource.String())
return
}

t.setPropagatingTagLocked(key, value.String())
}

// setPropagatingTagLocked sets the key/value pair as a trace propagating tag.
// Not safe for concurrent use, setPropagatingTag should be used instead in that case.
func (t *trace) setPropagatingTagLocked(key, value string) {
Expand All @@ -44,7 +70,7 @@ func (t *trace) unsetPropagatingTag(key string) {
// iteratePropagatingTags allows safe iteration through the propagating tags of a trace.
// the trace must not be modified during this call, as it is locked for reading.
//
// f should return whether or not the iteration should continue.
// f should return whether the iteration should continue.
func (t *trace) iteratePropagatingTags(f func(k, v string) bool) {
t.mu.RLock()
defer t.mu.RUnlock()
Expand Down
9 changes: 6 additions & 3 deletions ddtrace/tracer/span.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@ func (s *span) SetTag(key string, value interface{}) {
return
}

// Add this tag to propagating tags and to span tags
// Add this trace source tag to propagating tags and to span tags
// reserved for internal use only
if v, ok := value.(sharedinternal.PropagatingTagValue); ok {
s.context.trace.setPropagatingTag(key, v.Value)
if v, ok := value.(sharedinternal.TraceSourceTagValue); ok {
s.context.trace.setTraceSourcePropagatingTag(key, v.Value)
}
}

Expand Down Expand Up @@ -796,6 +796,9 @@ const (
keySingleSpanSamplingMPS = "_dd.span_sampling.max_per_second"
// keyPropagatedUserID holds the propagated user identifier, if user id propagation is enabled.
keyPropagatedUserID = "_dd.p.usr.id"
// keyPropagatedTraceSource holds a 2 character hexadecimal string representation of the product responsible
// for the span creation.
keyPropagatedTraceSource = "_dd.p.ts"
//keyTracerHostname holds the tracer detected hostname, only present when not connected over UDS to agent.
keyTracerHostname = "_dd.tracer_hostname"
// keyTraceID128 is the lowercase, hex encoded upper 64 bits of a 128-bit trace id, if present.
Expand Down
8 changes: 4 additions & 4 deletions ddtrace/tracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,8 +746,8 @@ func (t *tracer) Inject(ctx ddtrace.SpanContext, carrier interface{}) error {

if t.config.tracingAsTransport {
// in tracing as transport mode, only propagate when there is an upstream appsec event
// TODO: replace with _dd.p.ts in the next iteration standardizing this for other products, comparing enabled products in `t.config` with their corresponding `_dd.p.ts` bitfields
if ctx, ok := ctx.(*spanContext); ok && ctx.trace != nil && ctx.trace.propagatingTag("_dd.p.appsec") != "1" {
if ctx, ok := ctx.(*spanContext); ok && ctx.trace != nil &&
!globalinternal.VerifyTraceSourceEnabled(ctx.trace.propagatingTag(keyPropagatedTraceSource), globalinternal.ASMTraceSource) {
return nil
}
}
Expand Down Expand Up @@ -794,8 +794,8 @@ func (t *tracer) Extract(carrier interface{}) (ddtrace.SpanContext, error) {
ctx, err := t.config.propagator.Extract(carrier)
if t.config.tracingAsTransport {
// in tracing as transport mode, reset upstream sampling decision to make sure we keep 1 trace/minute
// TODO: replace with _dd.p.ts in the next iteration standardizing this for other products, comparing enabled products in `t.config` with their corresponding `_dd.p.ts` bitfields
if ctx, ok := ctx.(*spanContext); ok && ctx.trace.propagatingTag("_dd.p.appsec") != "1" {
if ctx, ok := ctx.(*spanContext); ok && ctx.trace != nil &&
!globalinternal.VerifyTraceSourceEnabled(ctx.trace.propagatingTag(keyPropagatedTraceSource), globalinternal.ASMTraceSource) {
ctx.trace.priority = nil
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/appsec/listener/httpsec/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (

"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
"gopkg.in/DataDog/dd-trace-go.v1/internal"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/waf"
"gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames"
)
Expand Down Expand Up @@ -230,7 +230,7 @@ func TestTags(t *testing.T) {
"manual.keep": true,
"appsec.event": true,
"_dd.origin": "appsec",
"_dd.p.appsec": internal.PropagatingTagValue{Value: "1"},
"_dd.p.ts": internal.TraceSourceTagValue{Value: internal.ASMTraceSource},
})
}

Expand Down
7 changes: 3 additions & 4 deletions internal/appsec/listener/waf/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import (

waf "github.com/DataDog/go-libddwaf/v3"

"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/trace"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames"
)

Expand Down Expand Up @@ -73,5 +72,5 @@ func SetEventSpanTags(span trace.TagSetter) {
span.SetTag("_dd.origin", "appsec")
// Set the appsec.event tag needed by the appsec backend
span.SetTag("appsec.event", true)
span.SetTag("_dd.p.appsec", internal.PropagatingTagValue{Value: "1"})
span.SetTag("_dd.p.ts", internal.TraceSourceTagValue{Value: internal.ASMTraceSource})
}
9 changes: 5 additions & 4 deletions internal/meta_internal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ type MetaStructValue struct {
Value any // TODO: further constraining Value's type, especially if it becomes public
}

// PropagatingTagValue is a custom type wrapper used to create tags that will be propagated
// to downstream distributed traces via the `X-Datadog-Tags` HTTP header for example.
type PropagatingTagValue struct {
Value string
// TraceSourceTagValue is a custom type wrapper used to create the trace source (_dd.p.ts) tag that will
// be propagated to downstream distributed traces via the `X-Datadog-Tags` HTTP header for example.
// It is represented as a 2 character hexadecimal string
type TraceSourceTagValue struct {
Value TraceSource
}
73 changes: 73 additions & 0 deletions internal/trace_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 internal

import (
"fmt"
"strconv"

"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

// TraceSource represents the 8-bit bitmask for the _dd.p.ts tag
type TraceSource uint8

const (
APMTraceSource TraceSource = 0x01
ASMTraceSource TraceSource = 0x02
DSMTraceSource TraceSource = 0x04
DJMTraceSource TraceSource = 0x08
DBMTraceSource TraceSource = 0x10
)

// String converts the bitmask to a two-character hexadecimal string
func (ts TraceSource) String() string {
return fmt.Sprintf("%02X", uint8(ts))
}

// ParseTraceSource parses a hexadecimal string into a TraceSource bitmask
func ParseTraceSource(hexStr string) (TraceSource, error) {
// Ensure at least 2 chars, allowing up to 8 for forward compatibility (32-bit)
if len(hexStr) < 2 || len(hexStr) > 8 {
return 0, fmt.Errorf("invalid length for TraceSource mask, expected 2 to 8 characters")
}

// Parse the full mask as a 32-bit unsigned integer
value, err := strconv.ParseUint(hexStr, 16, 32)
if err != nil {
return 0, fmt.Errorf("invalid hexadecimal format: %w", err)
}

// Extract only the least significant 8 bits (ensuring compliance with 8-bit mask)
return TraceSource(value & 0xFF), nil
}

func VerifyTraceSourceEnabled(hexStr string, target TraceSource) bool {
ts, err := ParseTraceSource(hexStr)
if err != nil {
if len(hexStr) != 0 { // Empty trace source should not trigger an error log.
log.Error("invalid trace-source hex string given for source verification: %v", err)
}
return false
}

return ts.IsSet(target)
}

// Set enables specific TraceSource (bit) in the bitmask
func (ts *TraceSource) Set(src TraceSource) {
*ts |= src
}

// Unset disables specific TraceSource (bit) in the bitmask
func (ts *TraceSource) Unset(src TraceSource) {
*ts &^= src
}

// IsSet checks if a specific TraceSource (bit) is enabled
func (ts TraceSource) IsSet(src TraceSource) bool {
return ts&src != 0
}
Loading

0 comments on commit be2813a

Please sign in to comment.