Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: implement new tag for trace source #3230

Merged
merged 9 commits into from
Feb 28, 2025
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

Choose a reason for hiding this comment

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

- 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
Loading