diff --git a/contrib/internal/httptrace/httptrace.go b/contrib/internal/httptrace/httptrace.go index 22ac0306b4..9b33c18105 100644 --- a/contrib/internal/httptrace/httptrace.go +++ b/contrib/internal/httptrace/httptrace.go @@ -10,8 +10,6 @@ package httptrace import ( "context" "fmt" - "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "net/http" "strconv" "strings" @@ -22,7 +20,9 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) var ( @@ -44,10 +44,8 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer. // we cannot track the configuration in newConfig because it's called during init() and the the telemetry client // is not initialized yet reportTelemetryConfigOnce.Do(func() { - telemetry.GlobalClient.ConfigChange([]telemetry.Configuration{ - {Name: "inferred_proxy_services_enabled", Value: cfg.inferredProxyServicesEnabled}, - }) - log.Debug("internal/httptrace: telemetry.ConfigChange called with cfg: %v:", cfg) + telemetry.RegisterAppConfig("inferred_proxy_services_enabled", cfg.inferredProxyServicesEnabled, telemetry.OriginEnvVar) + log.Debug("internal/httptrace: telemetry.RegisterAppConfig called with cfg: %v", cfg) }) var ipTags map[string]string diff --git a/contrib/internal/telemetrytest/telemetry_test.go b/contrib/internal/telemetrytest/telemetry_test.go index 123b2a73ca..d82146d018 100644 --- a/contrib/internal/telemetrytest/telemetry_test.go +++ b/contrib/internal/telemetrytest/telemetry_test.go @@ -12,10 +12,12 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/gorilla/mux" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/telemetrytest" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,13 +25,12 @@ import ( // sends the correct data to the telemetry client. func TestIntegrationInfo(t *testing.T) { // mux.NewRouter() uses the net/http and gorilla/mux integration - mux.NewRouter() - integrations := telemetry.Integrations() - require.Len(t, integrations, 2) - assert.Equal(t, integrations[0].Name, "net/http") - assert.True(t, integrations[0].Enabled) - assert.Equal(t, integrations[1].Name, "gorilla/mux") - assert.True(t, integrations[1].Enabled) + client := new(telemetrytest.RecordClient) + telemetry.StartApp(client) + _ = mux.NewRouter() + + assert.Contains(t, client.Integrations, telemetry.Integration{Name: "net/http", Version: "", Error: ""}) + assert.Contains(t, client.Integrations, telemetry.Integration{Name: "gorilla/mux", Version: "", Error: ""}) } type contribPkg struct { diff --git a/ddtrace/opentelemetry/telemetry_test.go b/ddtrace/opentelemetry/telemetry_test.go index 573d6ea699..05f6e62e20 100644 --- a/ddtrace/opentelemetry/telemetry_test.go +++ b/ddtrace/opentelemetry/telemetry_test.go @@ -73,16 +73,15 @@ func TestTelemetry(t *testing.T) { for k, v := range test.env { t.Setenv(k, v) } - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() p := NewTracerProvider() p.Tracer("") defer p.Shutdown() - assert.True(t, telemetryClient.Started) - telemetry.Check(t, telemetryClient.Configuration, "trace_propagation_style_inject", test.expectedInject) - telemetry.Check(t, telemetryClient.Configuration, "trace_propagation_style_extract", test.expectedExtract) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_propagation_style_inject", Value: test.expectedInject}) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_propagation_style_extract", Value: test.expectedExtract}) }) } diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index 42df8e2b6f..bf050b72bf 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -50,7 +50,7 @@ func (t *oteltracer) Start(ctx context.Context, spanName string, opts ...oteltra if k := ssConfig.SpanKind(); k != 0 { ddopts = append(ddopts, tracer.Tag(ext.SpanKind, k.String())) } - telemetry.GlobalClient.Count(telemetry.NamespaceTracers, "spans_created", 1.0, telemetryTags, true) + telemetry.Count(telemetry.NamespaceTracers, "spans_created", telemetryTags).Submit(1.0) var cfg ddtrace.StartSpanConfig cfg.Tags = make(map[string]interface{}) for _, attr := range ssConfig.Attributes() { diff --git a/ddtrace/opentelemetry/tracer_test.go b/ddtrace/opentelemetry/tracer_test.go index 9099fc132e..95982efc4f 100644 --- a/ddtrace/opentelemetry/tracer_test.go +++ b/ddtrace/opentelemetry/tracer_test.go @@ -195,14 +195,13 @@ func TestShutdownOnce(t *testing.T) { } func TestSpanTelemetry(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tp := NewTracerProvider() otel.SetTracerProvider(tp) tr := otel.Tracer("") _, _ = tr.Start(context.Background(), "otel.span") - telemetryClient.AssertCalled(t, "Count", telemetry.NamespaceTracers, "spans_created", 1.0, telemetryTags, true) - telemetryClient.AssertNumberOfCalls(t, "Count", 1) + assert.NotZero(t, telemetryClient.Count(telemetry.NamespaceTracers, "spans_created", telemetryTags).Get()) } func TestConcurrentSetAttributes(_ *testing.T) { diff --git a/ddtrace/opentracer/tracer.go b/ddtrace/opentracer/tracer.go index d91191ebe0..8764d625b4 100644 --- a/ddtrace/opentracer/tracer.go +++ b/ddtrace/opentracer/tracer.go @@ -32,7 +32,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" - opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go" ) // New creates, instantiates and returns an Opentracing compatible version of the @@ -67,7 +67,7 @@ func (t *opentracer) StartSpan(operationName string, options ...opentracing.Star for k, v := range sso.Tags { opts = append(opts, tracer.Tag(k, v)) } - telemetry.GlobalClient.Count(telemetry.NamespaceTracers, "spans_created", 1.0, telemetryTags, true) + telemetry.Count(telemetry.NamespaceTracers, "spans_created", telemetryTags).Submit(1.0) return &span{ Span: t.Tracer.StartSpan(operationName, opts...), opentracer: t, diff --git a/ddtrace/opentracer/tracer_test.go b/ddtrace/opentracer/tracer_test.go index 6a57d9a6d3..4cb0ee4269 100644 --- a/ddtrace/opentracer/tracer_test.go +++ b/ddtrace/opentracer/tracer_test.go @@ -115,10 +115,9 @@ func TestExtractError(t *testing.T) { } func TestSpanTelemetry(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() opentracing.SetGlobalTracer(New()) _ = opentracing.StartSpan("opentracing.span") - telemetryClient.AssertCalled(t, "Count", telemetry.NamespaceTracers, "spans_created", 1.0, telemetryTags, true) - telemetryClient.AssertNumberOfCalls(t, "Count", 1) + assert.NotZero(t, telemetryClient.Count(telemetry.NamespaceTracers, "spans_created", telemetryTags).Get()) } diff --git a/ddtrace/tracer/dynamic_config.go b/ddtrace/tracer/dynamic_config.go index db48be5a73..e0bb4928ed 100644 --- a/ddtrace/tracer/dynamic_config.go +++ b/ddtrace/tracer/dynamic_config.go @@ -26,11 +26,12 @@ type dynamicConfig[T any] struct { func newDynamicConfig[T any](name string, val T, apply func(T) bool, equal func(x, y T) bool) dynamicConfig[T] { return dynamicConfig[T]{ - cfgName: name, - current: val, - startup: val, - apply: apply, - equal: equal, + cfgName: name, + current: val, + startup: val, + cfgOrigin: telemetry.OriginDefault, + apply: apply, + equal: equal, } } @@ -79,11 +80,11 @@ func (dc *dynamicConfig[T]) handleRC(val *T) bool { func (dc *dynamicConfig[T]) toTelemetry() telemetry.Configuration { dc.RLock() defer dc.RUnlock() - return telemetry.Sanitize(telemetry.Configuration{ + return telemetry.Configuration{ Name: dc.cfgName, Value: dc.current, Origin: dc.cfgOrigin, - }) + } } func equal[T comparable](x, y T) bool { diff --git a/ddtrace/tracer/log.go b/ddtrace/tracer/log.go index 14bf075a9d..5a2cdc156e 100644 --- a/ddtrace/tracer/log.go +++ b/ddtrace/tracer/log.go @@ -18,6 +18,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo" + telemetrylog "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) @@ -170,4 +171,5 @@ func logStartup(t *tracer) { return } log.Info("DATADOG TRACER CONFIGURATION %s\n", string(bs)) + telemetrylog.Debug("DATADOG TRACER CONFIGURATION %s\n", string(bs)) } diff --git a/ddtrace/tracer/log_test.go b/ddtrace/tracer/log_test.go index 0b94b91494..b04fd8b2b3 100644 --- a/ddtrace/tracer/log_test.go +++ b/ddtrace/tracer/log_test.go @@ -14,7 +14,6 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,7 +29,7 @@ func TestStartupLog(t *testing.T) { defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":((true)|(false)),"Stats":((true)|(false)),"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) @@ -62,7 +61,7 @@ func TestStartupLog(t *testing.T) { defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"100","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":true,"metadata":{"version":"v1"}},"feature_flags":\["discovery"\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) @@ -92,7 +91,7 @@ func TestStartupLog(t *testing.T) { defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sample_rate_limit":"1000.001","trace_sampling_rules":\[{"service":"mysql","sample_rate":0\.75}\],"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":{"initial_service":"new_service"},"tags":{"runtime-id":"[^"]*","tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service","lambda_mode":"false","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[1]) @@ -106,7 +105,7 @@ func TestStartupLog(t *testing.T) { defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) fmt.Println(tp.Logs()[1]) @@ -120,7 +119,7 @@ func TestStartupLog(t *testing.T) { defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) assert.Len(tp.Logs(), 1) assert.Regexp(logPrefixRegexp+` INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test(\.exe)?","agent_url":"http://localhost:9/v0.4/traces","agent_error":"","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sample_rate_limit":"disabled","trace_sampling_rules":null,"span_sampling_rules":null,"sampling_rules_error":"","service_mappings":null,"tags":{"runtime-id":"[^"]*"},"runtime_metrics_enabled":false,"runtime_metrics_v2_enabled":false,"profiler_code_hotspots_enabled":((false)|(true)),"profiler_endpoints_enabled":((false)|(true)),"dd_version":"","architecture":"[^"]*","global_service":"","lambda_mode":"true","appsec":((true)|(false)),"agent_features":{"DropP0s":false,"Stats":false,"StatsdPort":(0|8125)},"integrations":{.*},"partial_flush_enabled":false,"partial_flush_min_spans":1000,"orchestrion":{"enabled":false},"feature_flags":\[\],"propagation_style_inject":"datadog,tracecontext","propagation_style_extract":"datadog,tracecontext","tracing_as_transport":false,"dogstatsd_address":"localhost:8125"}`, tp.Logs()[0]) @@ -132,7 +131,7 @@ func TestStartupLog(t *testing.T) { tracer, _, _, stop := startTestTracer(t, WithLogger(tp)) defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) @@ -146,7 +145,7 @@ func TestStartupLog(t *testing.T) { func TestLogSamplingRules(t *testing.T) { assert := assert.New(t) tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") t.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "some.service", "sample_rate": 0.234}, {"service": "other.service"}, {"service": "last.service", "sample_rate": 0.56}, {"odd": "pairs"}, {"sample_rate": 9.10}]`) _, _, _, stop := startTestTracer(t, WithLogger(tp), WithEnv("test")) defer stop() @@ -158,7 +157,7 @@ func TestLogSamplingRules(t *testing.T) { func TestLogDefaultSampleRate(t *testing.T) { assert := assert.New(t) tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") log.UseLogger(tp) t.Setenv("DD_TRACE_SAMPLE_RATE", ``) _, _, _, stop := startTestTracer(t, WithLogger(tp), WithEnv("test")) @@ -173,7 +172,7 @@ func TestLogAgentReachable(t *testing.T) { tracer, _, _, stop := startTestTracer(t, WithLogger(tp)) defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) assert.Regexp(logPrefixRegexp+` WARN: DIAGNOSTICS Unable to reach agent intake: Post`, tp.Logs()[0]) @@ -186,7 +185,7 @@ func TestLogFormat(t *testing.T) { tracer, _, _, stop := startTestTracer(t, WithLogger(tp), WithRuntimeMetrics(), WithDebugMode(true)) defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") tracer.StartSpan("test", ServiceName("test-service"), ResourceName("/"), WithSpanID(12345)) assert.Len(tp.Logs(), 1) assert.Regexp(logPrefixRegexp+` DEBUG: Started Span: dd.trace_id="12345" dd.span_id="12345" dd.parent_id="0", Operation: test, Resource: /, Tags: map.*, map.*`, tp.Logs()[0]) @@ -257,7 +256,7 @@ func setup(t *testing.T, customProp Propagator) string { } defer stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) require.Len(t, tp.Logs(), 2) return tp.Logs()[1] @@ -278,7 +277,7 @@ func TestAgentURL(t *testing.T) { tracer := newTracer(WithLogger(tp), WithUDS("var/run/datadog/apm.socket")) defer tracer.Stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) logEntry, found := findLogEntry(tp.Logs(), `"agent_url":"unix://var/run/datadog/apm.socket"`) if !found { @@ -294,7 +293,7 @@ func TestAgentURLFromEnv(t *testing.T) { tracer := newTracer(WithLogger(tp)) defer tracer.Stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) logEntry, found := findLogEntry(tp.Logs(), `"agent_url":"unix://var/run/datadog/apm.socket"`) if !found { @@ -311,7 +310,7 @@ func TestInvalidAgentURL(t *testing.T) { tracer := newTracer(WithLogger(tp)) defer tracer.Stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) logEntry, found := findLogEntry(tp.Logs(), `"agent_url":"http://localhost:8126/v0.4/traces"`) if !found { @@ -328,7 +327,7 @@ func TestAgentURLConflict(t *testing.T) { tracer := newTracer(WithLogger(tp), WithUDS("var/run/datadog/apm.socket"), WithAgentAddr("localhost:8126")) defer tracer.Stop() tp.Reset() - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") logStartup(tracer) logEntry, found := findLogEntry(tp.Logs(), `"agent_url":"http://localhost:8126/v0.4/traces"`) if !found { diff --git a/ddtrace/tracer/otel_dd_mappings.go b/ddtrace/tracer/otel_dd_mappings.go index 2fe6278c20..681d7e9ba7 100644 --- a/ddtrace/tracer/otel_dd_mappings.go +++ b/ddtrace/tracer/otel_dd_mappings.go @@ -94,13 +94,13 @@ func getDDorOtelConfig(configName string) string { if val != "" { log.Warn("Both %v and %v are set, using %v=%v", config.ot, config.dd, config.dd, val) telemetryTags := []string{ddPrefix + strings.ToLower(config.dd), otelPrefix + strings.ToLower(config.ot)} - telemetry.GlobalClient.Count(telemetry.NamespaceTracers, "otel.env.hiding", 1.0, telemetryTags, true) + telemetry.Count(telemetry.NamespaceTracers, "otel.env.hiding", telemetryTags).Submit(1) } else { v, err := config.remapper(otVal) if err != nil { log.Warn("%v", err) telemetryTags := []string{ddPrefix + strings.ToLower(config.dd), otelPrefix + strings.ToLower(config.ot)} - telemetry.GlobalClient.Count(telemetry.NamespaceTracers, "otel.env.invalid", 1.0, telemetryTags, true) + telemetry.Count(telemetry.NamespaceTracers, "otel.env.invalid", telemetryTags).Submit(1) } val = v } diff --git a/ddtrace/tracer/otel_dd_mappings_test.go b/ddtrace/tracer/otel_dd_mappings_test.go index 3f85dffa50..4427a02a3b 100644 --- a/ddtrace/tracer/otel_dd_mappings_test.go +++ b/ddtrace/tracer/otel_dd_mappings_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/telemetrytest" ) @@ -29,21 +30,21 @@ func TestAssessSource(t *testing.T) { assert.Equal(t, "abc", v) }) t.Run("both", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() // DD_SERVICE prevails t.Setenv("DD_SERVICE", "abc") t.Setenv("OTEL_SERVICE_NAME", "123") v := getDDorOtelConfig("service") assert.Equal(t, "abc", v) - telemetryClient.AssertCalled(t, "Count", telemetry.NamespaceTracers, "otel.env.hiding", 1.0, []string{"config_datadog:dd_service", "config_opentelemetry:otel_service_name"}, true) + assert.NotZero(t, telemetryClient.Count(telemetry.NamespaceTracers, "otel.env.hiding", []string{"config_datadog:dd_service", "config_opentelemetry:otel_service_name"}).Get()) }) t.Run("invalid-ot", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("OTEL_LOG_LEVEL", "nonesense") v := getDDorOtelConfig("debugMode") assert.Equal(t, "", v) - telemetryClient.AssertCalled(t, "Count", telemetry.NamespaceTracers, "otel.env.invalid", 1.0, []string{"config_datadog:dd_trace_debug", "config_opentelemetry:otel_log_level"}, true) + assert.NotZero(t, telemetryClient.Count(telemetry.NamespaceTracers, "otel.env.invalid", []string{"config_datadog:dd_trace_debug", "config_opentelemetry:otel_log_level"}).Get()) }) } diff --git a/ddtrace/tracer/remote_config.go b/ddtrace/tracer/remote_config.go index eaa8641730..ea5f6ff4bb 100644 --- a/ddtrace/tracer/remote_config.go +++ b/ddtrace/tracer/remote_config.go @@ -186,7 +186,7 @@ func (t *tracer) onRemoteConfigUpdate(u remoteconfig.ProductUpdate) map[string]s } if len(telemConfigs) > 0 { log.Debug("Reporting %d configuration changes to telemetry", len(telemConfigs)) - telemetry.GlobalClient.ConfigChange(telemConfigs) + telemetry.RegisterAppConfigs(telemConfigs...) } return statuses } @@ -244,7 +244,7 @@ func (t *tracer) onRemoteConfigUpdate(u remoteconfig.ProductUpdate) map[string]s } if len(telemConfigs) > 0 { log.Debug("Reporting %d configuration changes to telemetry", len(telemConfigs)) - telemetry.GlobalClient.ConfigChange(telemConfigs) + telemetry.RegisterAppConfigs(telemConfigs...) } return statuses } diff --git a/ddtrace/tracer/remote_config_test.go b/ddtrace/tracer/remote_config_test.go index d64c2666fe..f54a6f21e0 100644 --- a/ddtrace/tracer/remote_config_test.go +++ b/ddtrace/tracer/remote_config_test.go @@ -22,10 +22,18 @@ import ( "github.com/stretchr/testify/require" ) +func assertCalled(t *testing.T, client *telemetrytest.RecordClient, cfgs []telemetry.Configuration) { + t.Helper() + + for _, cfg := range cfgs { + assert.Contains(t, client.Configuration, cfg) + } +} + func TestOnRemoteConfigUpdate(t *testing.T) { t.Run("RC sampling rate = 0.5 is applied and can be reverted", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) defer stop() @@ -45,12 +53,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, 0.5, s.Metrics[keyRulesSamplerAppliedRate]) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{{Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}}, - ) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}) //Apply RC with sampling rules. Assert _dd.rule_psr shows the corresponding rule matched rate. input = remoteconfig.ProductUpdate{ @@ -88,15 +91,13 @@ func TestOnRemoteConfigUpdate(t *testing.T) { s.Finish() require.NotContains(t, keyRulesSamplerAppliedRate, s.Metrics) - // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 3) - // Not calling AssertCalled because the configuration contains a math.NaN() - // as value which cannot be asserted see https://github.com/stretchr/testify/issues/624 + // assert telemetry config contains trace_sample_rate with Nan (marshalled as nil) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_sample_rate", Value: nil, Origin: telemetry.OriginDefault}) }) t.Run("DD_TRACE_SAMPLE_RATE=0.1 and RC sampling rate = 0.2", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_SAMPLE_RATE", "0.1") tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) @@ -116,13 +117,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { s.Finish() require.Equal(t, 0.2, s.Metrics[keyRulesSamplerAppliedRate]) - // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{{Name: "trace_sample_rate", Value: 0.2, Origin: telemetry.OriginRemoteConfig}}, - ) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_sample_rate", Value: 0.2, Origin: telemetry.OriginRemoteConfig}) // Unset RC. Assert _dd.rule_psr shows the previous sampling rate (0.1) is applied input = remoteconfig.ProductUpdate{ @@ -134,18 +129,12 @@ func TestOnRemoteConfigUpdate(t *testing.T) { s.Finish() require.Equal(t, 0.1, s.Metrics[keyRulesSamplerAppliedRate]) - // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{{Name: "trace_sample_rate", Value: 0.1, Origin: telemetry.OriginDefault}}, - ) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_sample_rate", Value: 0.1, Origin: telemetry.OriginDefault}) }) t.Run("DD_TRACE_SAMPLING_RULES rate=0.1 and RC trace sampling rules rate = 1.0", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_SAMPLING_RULES", `[{ "service": "my-service", @@ -192,20 +181,17 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, samplerToDM(samplernames.RuleRate), s.context.trace.propagatingTags[keyDecisionMaker]) } - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ - {Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}, - { - Name: "trace_sample_rules", - Value: `[{"service":"my-service","name":"web.request","resource":"abc","sample_rate":1,"provenance":"customer"}]`, - Origin: telemetry.OriginRemoteConfig, - }, + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{ + Name: "trace_sample_rules", + Value: `[{"service":"my-service","name":"web.request","resource":"abc","sample_rate":1,"provenance":"customer"}]`, + Origin: telemetry.OriginRemoteConfig, }) }) t.Run("DD_TRACE_SAMPLING_RULES=0.1 and RC rule rate=1.0 and revert", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_SAMPLING_RULES", `[{ "service": "my-service", @@ -270,13 +256,13 @@ func TestOnRemoteConfigUpdate(t *testing.T) { if p, ok := s.context.trace.samplingPriority(); ok && p > 0 { require.Equal(t, samplerToDM(samplernames.RuleRate), s.context.trace.propagatingTags[keyDecisionMaker]) } - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ + + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}, {Name: "trace_sample_rules", Value: `[{"service":"my-service","name":"web.request","resource":"abc","sample_rate":1,"provenance":"customer"} {"service":"my-service","name":"web.request","resource":"*","sample_rate":0.3,"provenance":"dynamic"}]`, Origin: telemetry.OriginRemoteConfig}, }) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_sample_rate", Value: nil, Origin: telemetry.OriginDefault}, { Name: "trace_sample_rules", @@ -287,8 +273,8 @@ func TestOnRemoteConfigUpdate(t *testing.T) { }) t.Run("RC rule with tags", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) defer stop() @@ -301,7 +287,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { "provenance": "customer", "sample_rate": 1.0, "tags": [{"key": "tag-a", "value_glob": "tv-a??"}] - }]}, + }]}, "service_target": {"service": "my-service", "env": "my-env"}}`), } applyStatus := tracer.onRemoteConfigUpdate(input) @@ -320,8 +306,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { s.Finish() require.Equal(t, 0.5, s.Metrics[keyRulesSamplerAppliedRate]) - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_sample_rate", Value: 0.5, Origin: telemetry.OriginRemoteConfig}, { Name: "trace_sample_rules", @@ -333,8 +318,8 @@ func TestOnRemoteConfigUpdate(t *testing.T) { t.Run("RC header tags = X-Test-Header:my-tag-name is applied and can be reverted", func(t *testing.T) { defer globalconfig.ClearHeaderTags() - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) defer stop() @@ -352,14 +337,9 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, 1, globalconfig.HeaderTagsLen()) require.Equal(t, "my-tag-name", globalconfig.HeaderTag("X-Test-Header")) - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{ - {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name", Origin: telemetry.OriginRemoteConfig}, - }, - ) + assertCalled(t, telemetryClient, []telemetry.Configuration{ + {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name", Origin: telemetry.OriginRemoteConfig}, + }) // Unset RC. Assert header tags are not set input = remoteconfig.ProductUpdate{ @@ -370,17 +350,14 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, 0, globalconfig.HeaderTagsLen()) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled( - t, - "ConfigChange", + assertCalled(t, telemetryClient, []telemetry.Configuration{{Name: "trace_header_tags", Value: "", Origin: telemetry.OriginDefault}}, ) }) t.Run("RC tracing_enabled = false is applied", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tr, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) defer stop() @@ -428,16 +405,14 @@ func TestOnRemoteConfigUpdate(t *testing.T) { applyStatus = tr.onRemoteConfigUpdate(input) require.Equal(t, state.ApplyStateAcknowledged, applyStatus["path"].State) require.Equal(t, false, tr.config.enabled.current) - - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) }) t.Run( "DD_TRACE_HEADER_TAGS=X-Test-Header:my-tag-name-from-env and RC header tags = X-Test-Header:my-tag-name-from-rc", func(t *testing.T) { defer globalconfig.ClearHeaderTags() - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_HEADER_TAGS", "X-Test-Header:my-tag-name-from-env") tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) @@ -455,10 +430,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "my-tag-name-from-rc", globalconfig.HeaderTag("X-Test-Header")) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name-from-rc", Origin: telemetry.OriginRemoteConfig}, }, @@ -474,10 +446,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "my-tag-name-from-env", globalconfig.HeaderTag("X-Test-Header")) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled( - t, - "ConfigChange", + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name-from-env", Origin: telemetry.OriginDefault}, }, @@ -489,8 +458,8 @@ func TestOnRemoteConfigUpdate(t *testing.T) { "In code header tags = X-Test-Header:my-tag-name-in-code and RC header tags = X-Test-Header:my-tag-name-from-rc", func(t *testing.T) { defer globalconfig.ClearHeaderTags() - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer( t, @@ -512,10 +481,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "my-tag-name-from-rc", globalconfig.HeaderTag("X-Test-Header")) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name-from-rc", Origin: telemetry.OriginRemoteConfig}, }, @@ -531,10 +497,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "my-tag-name-in-code", globalconfig.HeaderTag("X-Test-Header")) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled( - t, - "ConfigChange", + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-name-in-code", Origin: telemetry.OriginDefault}, }, @@ -543,8 +506,8 @@ func TestOnRemoteConfigUpdate(t *testing.T) { ) t.Run("Invalid payload", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithService("my-service"), WithEnv("my-env")) defer stop() @@ -561,12 +524,14 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.NotEmpty(t, applyStatus["path"].Error) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 0) + for _, cfg := range telemetryClient.Configuration { + assert.NotEqual(t, "trace_sample_rate", cfg.Name) + } }) t.Run("Service mismatch", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithServiceName("my-service"), WithEnv("my-env")) defer stop() @@ -581,12 +546,14 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "service mismatch", applyStatus["path"].Error) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 0) + for _, cfg := range telemetryClient.Configuration { + assert.NotEqual(t, "trace_sample_rate", cfg.Name) + } }) t.Run("Env mismatch", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer, _, _, stop := startTestTracer(t, WithServiceName("my-service"), WithEnv("my-env")) defer stop() @@ -601,12 +568,14 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "env mismatch", applyStatus["path"].Error) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 0) + for _, cfg := range telemetryClient.Configuration { + assert.NotEqual(t, "trace_sample_rate", cfg.Name) + } }) t.Run("DD_TAGS=key0:val0,key1:val1, WithGlobalTag=key2:val2 and RC tags = key3:val3,key4:val4", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TAGS", "key0:val0,key1:val1") tracer, _, _, stop := startTestTracer( @@ -639,13 +608,9 @@ func TestOnRemoteConfigUpdate(t *testing.T) { runtimeIDTag := ext.RuntimeID + ":" + globalconfig.RuntimeID() // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{ - {Name: "trace_tags", Value: "key3:val3,key4:val4," + runtimeIDTag, Origin: telemetry.OriginRemoteConfig}, - }, + assertCalled(t, telemetryClient, []telemetry.Configuration{ + {Name: "trace_tags", Value: "key3:val3,key4:val4," + runtimeIDTag, Origin: telemetry.OriginRemoteConfig}, + }, ) // Unset RC. Assert config shows the original DD_TAGS + WithGlobalTag + runtime ID @@ -664,20 +629,16 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, globalconfig.RuntimeID(), s.Meta[ext.RuntimeID]) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled( - t, - "ConfigChange", - []telemetry.Configuration{ - {Name: "trace_tags", Value: "key0:val0,key1:val1,key2:val2," + runtimeIDTag, Origin: telemetry.OriginDefault}, - }, + assertCalled(t, telemetryClient, []telemetry.Configuration{ + {Name: "trace_tags", Value: "key0:val0,key1:val1,key2:val2," + runtimeIDTag, Origin: telemetry.OriginDefault}, + }, ) }) t.Run("Deleted config", func(t *testing.T) { defer globalconfig.ClearHeaderTags() - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_SAMPLE_RATE", "0.1") t.Setenv("DD_TRACE_HEADER_TAGS", "X-Test-Header:my-tag-from-env") @@ -702,8 +663,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "from-rc", s.Meta["ddtag"]) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 1) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_sample_rate", Value: 0.2, Origin: telemetry.OriginRemoteConfig}, {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-from-rc", Origin: telemetry.OriginRemoteConfig}, { @@ -724,8 +684,7 @@ func TestOnRemoteConfigUpdate(t *testing.T) { require.Equal(t, "from-env", s.Meta["ddtag"]) // Telemetry - telemetryClient.AssertNumberOfCalls(t, "ConfigChange", 2) - telemetryClient.AssertCalled(t, "ConfigChange", []telemetry.Configuration{ + assertCalled(t, telemetryClient, []telemetry.Configuration{ {Name: "trace_sample_rate", Value: 0.1, Origin: telemetry.OriginDefault}, {Name: "trace_header_tags", Value: "X-Test-Header:my-tag-from-env", Origin: telemetry.OriginDefault}, {Name: "trace_tags", Value: "ddtag:from-env," + ext.RuntimeID + ":" + globalconfig.RuntimeID(), Origin: telemetry.OriginDefault}, diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index 5b51677867..4a3b0cf9c0 100644 --- a/ddtrace/tracer/spancontext.go +++ b/ddtrace/tracer/spancontext.go @@ -497,7 +497,7 @@ func (t *trace) finishedOne(s *span) { return // The trace hasn't completed and partial flushing will not occur } log.Debug("Partial flush triggered with %d finished spans", t.finished) - telemetry.GlobalClient.Count(telemetry.NamespaceTracers, "trace_partial_flush.count", 1, []string{"reason:large_trace"}, true) + telemetry.Count(telemetry.NamespaceTracers, "trace_partial_flush.count", []string{"reason:large_trace"}).Submit(1) finishedSpans := make([]*span, 0, t.finished) leftoverSpans := make([]*span, 0, len(t.spans)-t.finished) for _, s2 := range t.spans { @@ -507,9 +507,8 @@ func (t *trace) finishedOne(s *span) { leftoverSpans = append(leftoverSpans, s2) } } - // TODO: (Support MetricKindDist) Re-enable these when we actually support `MetricKindDist` - //telemetry.GlobalClient.Record(telemetry.NamespaceTracers, telemetry.MetricKindDist, "trace_partial_flush.spans_closed", float64(len(finishedSpans)), nil, true) - //telemetry.GlobalClient.Record(telemetry.NamespaceTracers, telemetry.MetricKindDist, "trace_partial_flush.spans_remaining", float64(len(leftoverSpans)), nil, true) + telemetry.Distribution(telemetry.NamespaceTracers, "trace_partial_flush.spans_closed", nil).Submit(float64(len(finishedSpans))) + telemetry.Distribution(telemetry.NamespaceTracers, "trace_partial_flush.spans_remaining", nil).Submit(float64(len(leftoverSpans))) finishedSpans[0].setMetric(keySamplingPriority, *t.priority) if s != t.spans[0] { // Make sure the first span in the chunk has the trace-level tags diff --git a/ddtrace/tracer/spancontext_test.go b/ddtrace/tracer/spancontext_test.go index 7e86d9fdb1..0265fbd4a5 100644 --- a/ddtrace/tracer/spancontext_test.go +++ b/ddtrace/tracer/spancontext_test.go @@ -39,7 +39,7 @@ func TestNewSpanContextPushError(t *testing.T) { defer setupteardown(2, 2)() tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") _, _, _, stop := startTestTracer(t, WithLogger(tp), WithLambdaMode(true), WithEnv("testEnv")) defer stop() parent := newBasicSpan("test1") // 1st span in trace @@ -172,9 +172,9 @@ func TestPartialFlush(t *testing.T) { t.Setenv("DD_TRACE_PARTIAL_FLUSH_ENABLED", "true") t.Setenv("DD_TRACE_PARTIAL_FLUSH_MIN_SPANS", "2") t.Run("WithFlush", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - telemetryClient.ProductChange(telemetry.NamespaceTracers, true, nil) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + telemetryClient.ProductStarted(telemetry.NamespaceTracers) + defer telemetry.MockClient(telemetryClient)() tracer, transport, flush, stop := startTestTracer(t) defer stop() @@ -198,10 +198,9 @@ func TestPartialFlush(t *testing.T) { comparePayloadSpans(t, children[0], ts[0][0]) comparePayloadSpans(t, children[1], ts[0][1]) - telemetryClient.AssertCalled(t, "Count", telemetry.NamespaceTracers, "trace_partial_flush.count", 1.0, []string{"reason:large_trace"}, true) - // TODO: (Support MetricKindDist) Re-enable these when we actually support `MetricKindDist` - //telemetryClient.AssertCalled(t, "Record", telemetry.NamespaceTracers, "trace_partial_flush.spans_closed", 2.0, []string(nil), true) // Typed-nil here to not break usage of reflection in `mock` library. - //telemetryClient.AssertCalled(t, "Record", telemetry.NamespaceTracers, "trace_partial_flush.spans_remaining", 1.0, []string(nil), true) + assert.Equal(t, 1.0, telemetryClient.Count(telemetry.NamespaceTracers, "trace_partial_flush.count", []string{"reason:large_trace"}).Get()) + assert.Equal(t, 2.0, telemetryClient.Distribution(telemetry.NamespaceTracers, "trace_partial_flush.spans_closed", nil).Get()) + assert.Equal(t, 1.0, telemetryClient.Distribution(telemetry.NamespaceTracers, "trace_partial_flush.spans_remaining", nil).Get()) root.Finish() flush(1) @@ -214,9 +213,6 @@ func TestPartialFlush(t *testing.T) { assert.Equal(t, 1.0, ts[0][1].Metrics[keySamplingPriority]) // the tag should only be on the first span in the chunk comparePayloadSpans(t, root.(*span), tsRoot[0][0]) comparePayloadSpans(t, children[2], tsRoot[0][1]) - telemetryClient.AssertNumberOfCalls(t, "Count", 1) - // TODO: (Support MetricKindDist) Re-enable this when we actually support `MetricKindDist` - // telemetryClient.AssertNumberOfCalls(t, "Record", 2) }) // This test covers an issue where partial flushing + a rate sampler would panic @@ -242,7 +238,7 @@ func TestSpanTracePushNoFinish(t *testing.T) { assert := assert.New(t) tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") _, _, _, stop := startTestTracer(t, WithLogger(tp), WithLambdaMode(true), WithEnv("testEnv")) defer stop() @@ -756,7 +752,7 @@ func TestSpanContextPushFull(t *testing.T) { defer func(old int) { traceMaxSize = old }(traceMaxSize) traceMaxSize = 2 tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") _, _, _, stop := startTestTracer(t, WithLogger(tp), WithLambdaMode(true), WithEnv("testEnv")) defer stop() diff --git a/ddtrace/tracer/telemetry.go b/ddtrace/tracer/telemetry.go index e4dc21fc4d..8e809fabe5 100644 --- a/ddtrace/tracer/telemetry.go +++ b/ddtrace/tracer/telemetry.go @@ -7,8 +7,10 @@ package tracer import ( "fmt" + "os" "strings" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) @@ -31,15 +33,8 @@ func startTelemetry(c *config) { // Do not do extra work populating config data if instrumentation telemetry is disabled. return } - telemetry.GlobalClient.ApplyOps( - telemetry.WithService(c.serviceName), - telemetry.WithEnv(c.env), - telemetry.WithHTTPClient(c.httpClient), - // c.logToStdout is true if serverless is turned on - // c.ciVisibilityAgentless is true if ci visibility mode is turned on and agentless writer is configured - telemetry.WithURL(c.logToStdout || c.ciVisibilityAgentless, c.agentURL.String()), - telemetry.WithVersion(c.version), - ) + + telemetry.ProductStarted(telemetry.NamespaceTracers) telemetryConfigs := []telemetry.Configuration{ {Name: "trace_debug_enabled", Value: c.debug}, {Name: "agent_feature_drop_p0s", Value: c.agent.DropP0s}, @@ -70,7 +65,7 @@ func startTelemetry(c *config) { c.headerAsTags.toTelemetry(), c.globalTags.toTelemetry(), c.traceSampleRules.toTelemetry(), - telemetry.Sanitize(telemetry.Configuration{Name: "span_sample_rules", Value: c.spanRules}), + {Name: "span_sample_rules", Value: c.spanRules}, } var peerServiceMapping []string for key, value := range c.peerServiceMappings { @@ -114,5 +109,18 @@ func startTelemetry(c *config) { } } telemetryConfigs = append(telemetryConfigs, additionalConfigs...) - telemetry.GlobalClient.ProductChange(telemetry.NamespaceTracers, true, telemetryConfigs) + telemetry.RegisterAppConfigs(telemetryConfigs...) + cfg := telemetry.ClientConfig{ + HTTPClient: c.httpClient, + AgentURL: c.agentURL.String(), + } + if c.logToStdout || c.ciVisibilityAgentless { + cfg.APIKey = os.Getenv("DD_API_KEY") + } + client, err := telemetry.NewClient(c.serviceName, c.env, c.version, cfg) + if err != nil { + log.Debug("tracer: failed to create telemetry client: %v", err) + return + } + telemetry.StartApp(client) } diff --git a/ddtrace/tracer/telemetry_test.go b/ddtrace/tracer/telemetry_test.go index 90e82c851e..fc83df9e4b 100644 --- a/ddtrace/tracer/telemetry_test.go +++ b/ddtrace/tracer/telemetry_test.go @@ -19,8 +19,8 @@ import ( func TestTelemetryEnabled(t *testing.T) { t.Run("tracer start", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() Start( WithDebugMode(true), @@ -41,32 +41,24 @@ func TestTelemetryEnabled(t *testing.T) { defer globalconfig.SetServiceName("") defer Stop() - assert.True(t, telemetryClient.Started) - telemetryClient.AssertNumberOfCalls(t, "ApplyOps", 1) - telemetry.Check(t, telemetryClient.Configuration, "trace_debug_enabled", true) - telemetry.Check(t, telemetryClient.Configuration, "service", "test-serv") - telemetry.Check(t, telemetryClient.Configuration, "env", "test-env") - telemetry.Check(t, telemetryClient.Configuration, "runtime_metrics_enabled", true) - telemetry.Check(t, telemetryClient.Configuration, "stats_computation_enabled", false) - telemetry.Check(t, telemetryClient.Configuration, "trace_enabled", true) - telemetry.Check(t, telemetryClient.Configuration, "trace_span_attribute_schema", 0) - telemetry.Check(t, telemetryClient.Configuration, "trace_peer_service_defaults_enabled", true) - telemetry.Check(t, telemetryClient.Configuration, "trace_peer_service_mapping", "key:val") - telemetry.Check(t, telemetryClient.Configuration, "debug_stack_enabled", false) - telemetry.Check(t, telemetryClient.Configuration, "orchestrion_enabled", false) - telemetry.Check(t, telemetryClient.Configuration, "trace_sample_rate", nil) // default value is NaN which is sanitized to nil - telemetry.Check(t, telemetryClient.Configuration, "trace_header_tags", "key:val,key2:val2") - telemetry.Check(t, telemetryClient.Configuration, "trace_sample_rules", + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_debug_enabled", true) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "service", "test-serv") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "env", "test-env") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "runtime_metrics_enabled", true) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "stats_computation_enabled", false) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_enabled", true) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_span_attribute_schema", 0) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_peer_service_defaults_enabled", true) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_peer_service_mapping", "key:val") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "debug_stack_enabled", false) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "orchestrion_enabled", false) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_sample_rate", nil) // default value is NaN which is sanitized to nil + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_header_tags", "key:val,key2:val2") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_sample_rules", `[{"service":"test-serv","name":"op-name","resource":"resource-*","sample_rate":0.1,"tags":{"tag-a":"tv-a??"}}]`) - telemetry.Check(t, telemetryClient.Configuration, "span_sample_rules", "[]") - if metrics, ok := telemetryClient.Metrics[telemetry.NamespaceGeneral]; ok { - if initTime, ok := metrics["init_time"]; ok { - assert.True(t, initTime > 0, "expected positive init time, but got %f", initTime) - return - } - t.Fatalf("could not find general init time in telemetry client metrics") - } - t.Fatalf("could not find tracer namespace in telemetry client metrics") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "span_sample_rules", "[]") + + assert.NotZero(t, telemetryClient.Distribution(telemetry.NamespaceGeneral, "init_time", nil).Get()) }) t.Run("telemetry customer or dynamic rules", func(t *testing.T) { @@ -78,16 +70,15 @@ func TestTelemetryEnabled(t *testing.T) { } rule.Provenance = prov - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() Start(WithService("test-serv"), WithSamplingRules([]SamplingRule{rule}), ) defer globalconfig.SetServiceName("") defer Stop() - assert.True(t, telemetryClient.Started) - telemetry.Check(t, telemetryClient.Configuration, "trace_sample_rules", + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_sample_rules", fmt.Sprintf(`[{"service":"test-serv","name":"op-name","resource":"resource-*","sample_rate":0.1,"tags":{"tag-a":"tv-a??"},"provenance":"%s"}]`, prov.String())) } }) @@ -103,24 +94,23 @@ func TestTelemetryEnabled(t *testing.T) { rules[i].Provenance = Local } - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() Start(WithService("test-serv"), WithSamplingRules(rules), ) defer globalconfig.SetServiceName("") defer Stop() - assert.True(t, telemetryClient.Started) - telemetry.Check(t, telemetryClient.Configuration, "trace_sample_rules", + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_sample_rules", `[{"service":"test-serv","name":"op-name","resource":"resource-*","sample_rate":0.1,"tags":{"tag-a":"tv-a??"}}]`) - telemetry.Check(t, telemetryClient.Configuration, "span_sample_rules", + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "span_sample_rules", `[{"service":"test-serv","name":"op-name","sample_rate":0.1}]`) }) t.Run("tracer start with empty rules", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() t.Setenv("DD_TRACE_SAMPLING_RULES", "") t.Setenv("DD_SPAN_SAMPLING_RULES", "") @@ -128,18 +118,13 @@ func TestTelemetryEnabled(t *testing.T) { defer globalconfig.SetServiceName("") defer Stop() - assert.True(t, telemetryClient.Started) - var cfgs []telemetry.Configuration - for _, c := range telemetryClient.Configuration { - cfgs = append(cfgs, telemetry.Sanitize(c)) - } - telemetry.Check(t, cfgs, "trace_sample_rules", "[]") - telemetry.Check(t, cfgs, "span_sample_rules", "[]") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "trace_sample_rules", "[]") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "span_sample_rules", "[]") }) t.Run("profiler start, tracer start", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() profiler.Start() defer profiler.Stop() Start( @@ -147,18 +132,17 @@ func TestTelemetryEnabled(t *testing.T) { ) defer globalconfig.SetServiceName("") defer Stop() - telemetry.Check(t, telemetryClient.Configuration, "service", "test-serv") - telemetryClient.AssertNumberOfCalls(t, "ApplyOps", 2) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "service", "test-serv") }) t.Run("orchestrion telemetry", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() Start(WithOrchestrion(map[string]string{"k1": "v1", "k2": "v2"})) defer Stop() - telemetry.Check(t, telemetryClient.Configuration, "orchestrion_enabled", true) - telemetry.Check(t, telemetryClient.Configuration, "orchestrion_k1", "v1") - telemetry.Check(t, telemetryClient.Configuration, "orchestrion_k2", "v2") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "orchestrion_enabled", true) + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "orchestrion_k1", "v1") + telemetrytest.CheckConfig(t, telemetryClient.Configuration, "orchestrion_k2", "v2") }) } diff --git a/ddtrace/tracer/textmap_test.go b/ddtrace/tracer/textmap_test.go index 0dd05124c0..90784852bc 100644 --- a/ddtrace/tracer/textmap_test.go +++ b/ddtrace/tracer/textmap_test.go @@ -16,16 +16,14 @@ import ( "sync" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/httpmem" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/DataDog/datadog-go/v5/statsd" ) @@ -2092,7 +2090,7 @@ func TestNonePropagator(t *testing.T) { t.Run("inject/none,b3", func(t *testing.T) { t.Setenv(headerPropagationStyleInject, "none,b3") tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") tracer := newTracer(WithLogger(tp), WithEnv("test")) defer tracer.Stop() // reinitializing to capture log output, since propagators are parsed before logger is set @@ -2421,7 +2419,7 @@ func FuzzComposeTracestate(f *testing.F) { if strings.HasSuffix(v, " ") { t.Skipf("Skipping invalid tags") } - totalLen += (len(k) + len(v)) + totalLen += len(k) + len(v) if totalLen > 128 { break } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 316ed56270..f189d8f8aa 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -149,7 +149,9 @@ func Start(opts ...StartOption) { if internal.Testing { return // mock tracer active } - defer telemetry.Time(telemetry.NamespaceGeneral, "init_time", nil, true)() + defer func(now time.Time) { + telemetry.Distribution(telemetry.NamespaceGeneral, "init_time", nil).Submit(float64(time.Since(now).Milliseconds())) + }(time.Now()) t := newTracer(opts...) if !t.config.enabled.current { // TODO: instrumentation telemetry client won't get started diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index dd1820621e..42658d6dc1 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -26,6 +26,10 @@ import ( pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace" "github.com/DataDog/datadog-go/v5/statsd" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tinylib/msgp/msgp" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal" @@ -33,11 +37,6 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/statsdtest" - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tinylib/msgp/msgp" ) func (t *tracer) newEnvSpan(service, env string) *span { @@ -712,7 +711,7 @@ func TestSamplingDecision(t *testing.T) { func TestTracerRuntimeMetrics(t *testing.T) { t.Run("on", func(t *testing.T) { tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") tracer := newTracer(WithRuntimeMetrics(), WithLogger(tp), WithDebugMode(true), WithEnv("test")) defer tracer.Stop() assert.Contains(t, tp.Logs()[0], "DEBUG: Runtime metrics enabled") @@ -721,7 +720,7 @@ func TestTracerRuntimeMetrics(t *testing.T) { t.Run("dd-env", func(t *testing.T) { t.Setenv("DD_RUNTIME_METRICS_ENABLED", "true") tp := new(log.RecordLogger) - tp.Ignore("appsec: ", telemetry.LogPrefix) + tp.Ignore("appsec: ", "telemetry") tracer := newTracer(WithLogger(tp), WithDebugMode(true), WithEnv("test")) defer tracer.Stop() assert.Contains(t, tp.Logs()[0], "DEBUG: Runtime metrics enabled") @@ -1486,7 +1485,7 @@ func TestTracerRace(t *testing.T) { // different orders, and modifying spans after creation. for n := 0; n < total; n++ { i := n // keep local copy - odd := ((i % 2) != 0) + odd := (i % 2) != 0 go func() { if i%11 == 0 { time.Sleep(time.Microsecond) diff --git a/internal/appsec/config/config.go b/internal/appsec/config/config.go index fe34fa4332..d019ad9ccc 100644 --- a/internal/appsec/config/config.go +++ b/internal/appsec/config/config.go @@ -19,25 +19,20 @@ import ( ) func init() { - registerAppConfigTelemetry() -} - -// Register the global app telemetry configuration. -func registerAppConfigTelemetry() { - registerSCAAppConfigTelemetry(telemetry.GlobalClient) + registerSCAAppConfigTelemetry() } // Register the global app telemetry configuration related to the Software Composition Analysis (SCA) product. // Report over telemetry whether SCA's enablement env var was set or not along with its value. Nothing is reported in // case of an error or if the env var is not set. -func registerSCAAppConfigTelemetry(client telemetry.Client) { +func registerSCAAppConfigTelemetry() { val, defined, err := parseBoolEnvVar(EnvSCAEnabled) if err != nil { log.Error("appsec: %v", err) return } if defined { - client.RegisterAppConfig(EnvSCAEnabled, val, telemetry.OriginEnvVar) + telemetry.RegisterAppConfig(EnvSCAEnabled, val, telemetry.OriginEnvVar) } } diff --git a/internal/appsec/config/config_test.go b/internal/appsec/config/config_test.go index 77d29e36df..9dc26262e0 100644 --- a/internal/appsec/config/config_test.go +++ b/internal/appsec/config/config_test.go @@ -51,8 +51,9 @@ func TestSCAEnabled(t *testing.T) { telemetryClient := new(telemetrytest.MockClient) telemetryClient.On("RegisterAppConfig", EnvSCAEnabled, tc.expectedValue, telemetry.OriginEnvVar).Return() + defer telemetry.MockClient(telemetryClient)() - registerSCAAppConfigTelemetry(telemetryClient) + registerSCAAppConfigTelemetry() if tc.telemetryExpected { telemetryClient.AssertCalled(t, "RegisterAppConfig", EnvSCAEnabled, tc.expectedValue, telemetry.OriginEnvVar) diff --git a/internal/appsec/features.go b/internal/appsec/features.go index ca286de742..e9ef073378 100644 --- a/internal/appsec/features.go +++ b/internal/appsec/features.go @@ -66,8 +66,12 @@ func (a *appsec) SwapRootOperation() error { oldFeatures := a.features a.features = newFeatures - log.Debug("appsec: stopping the following features: %v", oldFeatures) - log.Debug("appsec: starting the following features: %v", newFeatures) + if len(oldFeatures) > 0 { + log.Debug("appsec: stopping the following features: %v", oldFeatures) + } + if len(newFeatures) > 0 { + log.Debug("appsec: starting the following features: %v", newFeatures) + } dyngo.SwapRootOperation(newRoot) diff --git a/internal/appsec/telemetry.go b/internal/appsec/telemetry.go index 2a8b0b42d3..9be83469cf 100644 --- a/internal/appsec/telemetry.go +++ b/internal/appsec/telemetry.go @@ -87,5 +87,11 @@ func (a *appsecTelemetry) emit() { return } - telemetry.GlobalClient.ProductChange(telemetry.NamespaceAppSec, a.enabled, a.configs) + if a.enabled { + telemetry.ProductStarted(telemetry.NamespaceAppSec) + } else { + telemetry.ProductStopped(telemetry.NamespaceAppSec) + } + + telemetry.RegisterAppConfigs(a.configs...) } diff --git a/internal/civisibility/integrations/civisibility.go b/internal/civisibility/integrations/civisibility.go index 6dd3caa3b4..cd5e8f6407 100644 --- a/internal/civisibility/integrations/civisibility.go +++ b/internal/civisibility/integrations/civisibility.go @@ -131,7 +131,7 @@ func ExitCiVisibility() { log.Debug("civisibility: flushing and stopping tracer") tracer.Flush() tracer.Stop() - telemetry.GlobalClient.Stop() + telemetry.StopApp() log.Debug("civisibility: done.") }() for _, v := range closeActions { diff --git a/internal/civisibility/utils/net/client.go b/internal/civisibility/utils/net/client.go index 3bcfcdd847..c823f6a0f6 100644 --- a/internal/civisibility/utils/net/client.go +++ b/internal/civisibility/utils/net/client.go @@ -126,11 +126,13 @@ func NewClientWithServiceNameAndSubdomain(serviceName, subdomain string) Client defaultHeaders := map[string]string{} var baseURL string var requestHandler *RequestHandler + var agentURL *url.URL + var APIKeyValue string agentlessEnabled := internal.BoolEnv(constants.CIVisibilityAgentlessEnabledEnvironmentVariable, false) if agentlessEnabled { // Agentless mode is enabled. - APIKeyValue := os.Getenv(constants.APIKeyEnvironmentVariable) + APIKeyValue = os.Getenv(constants.APIKeyEnvironmentVariable) if APIKeyValue == "" { log.Error("An API key is required for agentless mode. Use the DD_API_KEY env variable to set it") return nil @@ -159,7 +161,7 @@ func NewClientWithServiceNameAndSubdomain(serviceName, subdomain string) Client // Use agent mode with the EVP proxy. defaultHeaders["X-Datadog-EVP-Subdomain"] = subdomain - agentURL := internal.AgentURLFromEnv() + agentURL = internal.AgentURLFromEnv() if agentURL.Scheme == "unix" { // If we're connecting over UDS we can just rely on the agent to provide the hostname log.Debug("connecting to agent over unix, do not set hostname on any traces") @@ -205,19 +207,23 @@ func NewClientWithServiceNameAndSubdomain(serviceName, subdomain string) Client if !telemetry.Disabled() { telemetryInit.Do(func() { - telemetry.GlobalClient.ApplyOps( - telemetry.WithService(serviceName), - telemetry.WithEnv(environment), - telemetry.WithHTTPClient(requestHandler.Client), - telemetry.WithURL(agentlessEnabled, baseURL), - telemetry.SyncFlushOnStop(), - ) - telemetry.GlobalClient.ProductChange(telemetry.NamespaceCiVisibility, true, []telemetry.Configuration{ - telemetry.StringConfig("service", serviceName), - telemetry.StringConfig("env", environment), - telemetry.BoolConfig("agentless", agentlessEnabled), - telemetry.StringConfig("test_session_name", ciTags[constants.TestSessionName]), - }) + telemetry.ProductStarted(telemetry.NamespaceTracers) + if telemetry.GlobalClient() != nil { + return + } + cfg := telemetry.ClientConfig{ + HTTPClient: requestHandler.Client, + APIKey: APIKeyValue, + } + if agentURL != nil { + cfg.AgentURL = agentURL.String() + } + client, err := telemetry.NewClient(serviceName, environment, os.Getenv("DD_VERSION"), cfg) + if err != nil { + log.Debug("civisibility: failed to create telemetry client: %v", err) + return + } + telemetry.StartApp(client) }) } diff --git a/internal/civisibility/utils/telemetry/telemetry_count.go b/internal/civisibility/utils/telemetry/telemetry_count.go index 0e1bfaa931..d1b1e175a1 100644 --- a/internal/civisibility/utils/telemetry/telemetry_count.go +++ b/internal/civisibility/utils/telemetry/telemetry_count.go @@ -48,165 +48,165 @@ func GetErrorTypeFromStatusCode(statusCode int) ErrorType { func EventCreated(testingFramework string, eventType TestingEventType) { tags := []string{string(getTestingFramework(testingFramework))} tags = append(tags, eventType...) - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "event_created", 1.0, removeEmptyStrings(tags), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "event_created", removeEmptyStrings(tags)).Submit(1.0) } // EventFinished the number of events finished by CI Visibility func EventFinished(testingFramework string, eventType TestingEventType) { tags := []string{string(getTestingFramework(testingFramework))} tags = append(tags, eventType...) - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "event_finished", 1.0, removeEmptyStrings(tags), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "event_finished", removeEmptyStrings(tags)).Submit(1.0) } // CodeCoverageStarted the number of code coverage start calls by CI Visibility func CodeCoverageStarted(testingFramework string, coverageLibraryType CoverageLibraryType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage_started", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "code_coverage_started", removeEmptyStrings([]string{ string(getTestingFramework(testingFramework)), string(coverageLibraryType), - }), true) + })).Submit(1.0) } // CodeCoverageFinished the number of code coverage finished calls by CI Visibility func CodeCoverageFinished(testingFramework string, coverageLibraryType CoverageLibraryType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage_finished", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "code_coverage_finished", removeEmptyStrings([]string{ string(getTestingFramework(testingFramework)), string(coverageLibraryType), - }), true) + })).Submit(1.0) } // EventsEnqueueForSerialization the number of events enqueued for serialization by CI Visibility func EventsEnqueueForSerialization() { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "events_enqueued_for_serialization", 1.0, nil, true) + telemetry.Count(telemetry.NamespaceCIVisibility, "events_enqueued_for_serialization", nil).Submit(1.0) } // EndpointPayloadRequests the number of requests sent to the endpoint, regardless of success, tagged by endpoint type func EndpointPayloadRequests(endpointType EndpointType, requestCompressedType RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.requests", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "endpoint_payload.requests", removeEmptyStrings([]string{ string(endpointType), string(requestCompressedType), - }), true) + })).Submit(1.0) } // EndpointPayloadRequestsErrors the number of requests sent to the endpoint that errored, tagget by the error type and endpoint type and status code func EndpointPayloadRequestsErrors(endpointType EndpointType, errorType ErrorType) { tags := []string{string(endpointType)} tags = append(tags, errorType...) - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.requests_errors", 1.0, removeEmptyStrings(tags), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "endpoint_payload.requests_errors", removeEmptyStrings(tags)).Submit(1.0) } // EndpointPayloadDropped the number of payloads dropped after all retries by CI Visibility func EndpointPayloadDropped(endpointType EndpointType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "endpoint_payload.dropped", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "endpoint_payload.dropped", removeEmptyStrings([]string{ string(endpointType), - }), true) + })).Submit(1.0) } // GitCommand the number of git commands executed by CI Visibility func GitCommand(commandType CommandType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git.command", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "git.command", removeEmptyStrings([]string{ string(commandType), - }), true) + })).Submit(1.0) } // GitCommandErrors the number of git command that errored by CI Visibility func GitCommandErrors(commandType CommandType, exitCode CommandExitCodeType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git.command_errors", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "git.command_errors", removeEmptyStrings([]string{ string(commandType), string(exitCode), - }), true) + })).Submit(1.0) } // GitRequestsSearchCommits the number of requests sent to the search commit endpoint, regardless of success. func GitRequestsSearchCommits(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.search_commits", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.search_commits", removeEmptyStrings([]string{ string(requestCompressed), - }), true) + })).Submit(1.0) } // GitRequestsSearchCommitsErrors the number of requests sent to the search commit endpoint that errored, tagged by the error type. func GitRequestsSearchCommitsErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.search_commits_errors", 1.0, removeEmptyStrings(errorType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.search_commits_errors", removeEmptyStrings(errorType)).Submit(1.0) } // GitRequestsObjectsPack the number of requests sent to the objects pack endpoint, tagged by the request compressed type. func GitRequestsObjectsPack(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.objects_pack", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.objects_pack", removeEmptyStrings([]string{ string(requestCompressed), - }), true) + })).Submit(1.0) } // GitRequestsObjectsPackErrors the number of requests sent to the objects pack endpoint that errored, tagged by the error type. func GitRequestsObjectsPackErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.objects_pack_errors", 1.0, removeEmptyStrings(errorType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.objects_pack_errors", removeEmptyStrings(errorType)).Submit(1.0) } // GitRequestsSettings the number of requests sent to the settings endpoint, tagged by the request compressed type. func GitRequestsSettings(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.settings", removeEmptyStrings([]string{ string(requestCompressed), - }), true) + })).Submit(1.0) } // GitRequestsSettingsErrors the number of requests sent to the settings endpoint that errored, tagged by the error type. func GitRequestsSettingsErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings_errors", 1.0, removeEmptyStrings(errorType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.settings_errors", removeEmptyStrings(errorType)).Submit(1.0) } // GitRequestsSettingsResponse the number of settings responses received by CI Visibility, tagged by the settings response type. func GitRequestsSettingsResponse(settingsResponseType SettingsResponseType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "git_requests.settings_response", 1.0, removeEmptyStrings(settingsResponseType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "git_requests.settings_response", removeEmptyStrings(settingsResponseType)).Submit(1.0) } // ITRSkippableTestsRequest the number of requests sent to the ITR skippable tests endpoint, tagged by the request compressed type. func ITRSkippableTestsRequest(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.request", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_skippable_tests.request", removeEmptyStrings([]string{ string(requestCompressed), - }), true) + })).Submit(1.0) } // ITRSkippableTestsRequestErrors the number of requests sent to the ITR skippable tests endpoint that errored, tagged by the error type. func ITRSkippableTestsRequestErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.request_errors", 1.0, removeEmptyStrings(errorType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_skippable_tests.request_errors", removeEmptyStrings(errorType)).Submit(1.0) } // ITRSkippableTestsResponseTests the number of tests received in the ITR skippable tests response by CI Visibility. func ITRSkippableTestsResponseTests(value float64) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skippable_tests.response_tests", value, nil, true) + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_skippable_tests.response_tests", nil).Submit(value) } // ITRSkipped the number of ITR tests skipped by CI Visibility, tagged by the event type. func ITRSkipped(eventType TestingEventType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_skipped", 1.0, removeEmptyStrings(eventType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_skipped", removeEmptyStrings(eventType)).Submit(1.0) } // ITRUnskippable the number of ITR tests unskippable by CI Visibility, tagged by the event type. func ITRUnskippable(eventType TestingEventType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_unskippable", 1.0, removeEmptyStrings(eventType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_unskippable", removeEmptyStrings(eventType)).Submit(1.0) } // ITRForcedRun the number of tests or test suites that would've been skipped by ITR but were forced to run because of their unskippable status by CI Visibility. func ITRForcedRun(eventType TestingEventType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "itr_forced_run", 1.0, removeEmptyStrings(eventType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "itr_forced_run", removeEmptyStrings(eventType)).Submit(1.0) } // CodeCoverageIsEmpty the number of code coverage payloads that are empty by CI Visibility. func CodeCoverageIsEmpty() { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.is_empty", 1.0, nil, true) + telemetry.Count(telemetry.NamespaceCIVisibility, "code_coverage.is_empty", nil).Submit(1.0) } // CodeCoverageErrors the number of errors while processing code coverage by CI Visibility. func CodeCoverageErrors() { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "code_coverage.errors", 1.0, nil, true) + telemetry.Count(telemetry.NamespaceCIVisibility, "code_coverage.errors", nil).Submit(1.0) } // KnownTestsRequest the number of requests sent to the known tests endpoint, tagged by the request compressed type. func KnownTestsRequest(requestCompressed RequestCompressedType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request", 1.0, removeEmptyStrings([]string{ + telemetry.Count(telemetry.NamespaceCIVisibility, "known_tests.request", removeEmptyStrings([]string{ string(requestCompressed), - }), true) + })).Submit(1.0) } // KnownTestsRequestErrors the number of requests sent to the known tests endpoint that errored, tagged by the error type. func KnownTestsRequestErrors(errorType ErrorType) { - telemetry.GlobalClient.Count(telemetry.NamespaceCiVisibility, "known_tests.request_errors", 1.0, removeEmptyStrings(errorType), true) + telemetry.Count(telemetry.NamespaceCIVisibility, "known_tests.request_errors", removeEmptyStrings(errorType)).Submit(1.0) } diff --git a/internal/civisibility/utils/telemetry/telemetry_distribution.go b/internal/civisibility/utils/telemetry/telemetry_distribution.go index 20c5786eb0..93b3228f5c 100644 --- a/internal/civisibility/utils/telemetry/telemetry_distribution.go +++ b/internal/civisibility/utils/telemetry/telemetry_distribution.go @@ -9,96 +9,96 @@ import "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" // EndpointPayloadBytes records the size in bytes of the serialized payload by CI Visibility. func EndpointPayloadBytes(endpointType EndpointType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.bytes", value, removeEmptyStrings([]string{ - (string)(endpointType), - }), true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "endpoint_payload.bytes", removeEmptyStrings([]string{ + string(endpointType), + })).Submit(value) } // EndpointPayloadRequestsMs records the time it takes to send the payload sent to the endpoint in ms by CI Visibility. func EndpointPayloadRequestsMs(endpointType EndpointType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.requests_ms", value, removeEmptyStrings([]string{ - (string)(endpointType), - }), true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "endpoint_payload.requests_ms", removeEmptyStrings([]string{ + string(endpointType), + })).Submit(value) } // EndpointPayloadEventsCount records the number of events in the payload sent to the endpoint by CI Visibility. func EndpointPayloadEventsCount(endpointType EndpointType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.events_count", value, removeEmptyStrings([]string{ - (string)(endpointType), - }), true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "endpoint_payload.events_count", removeEmptyStrings([]string{ + string(endpointType), + })).Submit(value) } // EndpointEventsSerializationMs records the time it takes to serialize the events in the payload sent to the endpoint in ms by CI Visibility. func EndpointEventsSerializationMs(endpointType EndpointType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "endpoint_payload.events_serialization_ms", value, removeEmptyStrings([]string{ - (string)(endpointType), - }), true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "endpoint_payload.events_serialization_ms", removeEmptyStrings([]string{ + string(endpointType), + })).Submit(value) } // GitCommandMs records the time it takes to execute a git command in ms by CI Visibility. func GitCommandMs(commandType CommandType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git.command_ms", value, removeEmptyStrings([]string{ + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git.command_ms", removeEmptyStrings([]string{ (string)(commandType), - }), true) + })).Submit(value) } // GitRequestsSearchCommitsMs records the time it takes to get the response of the search commit quest in ms by CI Visibility. func GitRequestsSearchCommitsMs(responseCompressedType ResponseCompressedType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.search_commits_ms", value, removeEmptyStrings([]string{ + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git_requests.search_commits_ms", removeEmptyStrings([]string{ (string)(responseCompressedType), - }), true) + })).Submit(value) } // GitRequestsObjectsPackMs records the time it takes to get the response of the objects pack request in ms by CI Visibility. func GitRequestsObjectsPackMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_ms", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git_requests.objects_pack_ms", nil).Submit(value) } // GitRequestsObjectsPackBytes records the sum of the sizes of the object pack files inside a single payload by CI Visibility func GitRequestsObjectsPackBytes(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_bytes", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git_requests.objects_pack_bytes", nil).Submit(value) } // GitRequestsObjectsPackFiles records the number of files sent in the object pack payload by CI Visibility. func GitRequestsObjectsPackFiles(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.objects_pack_files", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git_requests.objects_pack_files", nil).Submit(value) } // GitRequestsSettingsMs records the time it takes to get the response of the settings endpoint request in ms by CI Visibility. func GitRequestsSettingsMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "git_requests.settings_ms", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "git_requests.settings_ms", nil).Submit(value) } // ITRSkippableTestsRequestMs records the time it takes to get the response of the itr skippable tests endpoint request in ms by CI Visibility. func ITRSkippableTestsRequestMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "itr_skippable_tests.request_ms", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "itr_skippable_tests.request_ms", nil).Submit(value) } -// ITRSkippableTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. +// ITRSkippableTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set t if response body is compressed. func ITRSkippableTestsResponseBytes(responseCompressedType ResponseCompressedType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "itr_skippable_tests.response_bytes", value, removeEmptyStrings([]string{ + telemetry.Distribution(telemetry.NamespaceCIVisibility, "itr_skippable_tests.response_bytes", removeEmptyStrings([]string{ (string)(responseCompressedType), - }), true) + })).Submit(value) } // CodeCoverageFiles records the number of files in the code coverage report by CI Visibility. func CodeCoverageFiles(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "code_coverage.files", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "code_coverage.files", nil).Submit(value) } // KnownTestsRequestMs records the time it takes to get the response of the known tests endpoint request in ms by CI Visibility. func KnownTestsRequestMs(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.request_ms", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "known_tests.request_ms", nil).Submit(value) } // KnownTestsResponseBytes records the number of bytes received by the endpoint. Tagged with a boolean flag set to true if response body is compressed. func KnownTestsResponseBytes(responseCompressedType ResponseCompressedType, value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_bytes", value, removeEmptyStrings([]string{ - (string)(responseCompressedType), - }), true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "known_tests.response_bytes", removeEmptyStrings([]string{ + string(responseCompressedType), + })).Submit(value) } // KnownTestsResponseTests records the number of tests in the response of the known tests endpoint by CI Visibility. func KnownTestsResponseTests(value float64) { - telemetry.GlobalClient.Record(telemetry.NamespaceCiVisibility, telemetry.MetricKindDist, "known_tests.response_tests", value, nil, true) + telemetry.Distribution(telemetry.NamespaceCIVisibility, "known_tests.response_tests", nil).Submit(value) } diff --git a/internal/newtelemetry/client.go b/internal/newtelemetry/client.go deleted file mode 100644 index a37d25629d..0000000000 --- a/internal/newtelemetry/client.go +++ /dev/null @@ -1,344 +0,0 @@ -// 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 2024 Datadog, Inc. - -package newtelemetry - -import ( - "errors" - "os" - "strconv" - "sync" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/mapper" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" -) - -// NewClient creates a new telemetry client with the given service, environment, and version and config. -func NewClient(service, env, version string, config ClientConfig) (Client, error) { - if service == "" { - return nil, errors.New("service name must not be empty") - } - - config = defaultConfig(config) - if err := config.validateConfig(); err != nil { - return nil, err - } - - return newClient(internal.TracerConfig{Service: service, Env: env, Version: version}, config) -} - -func newClient(tracerConfig internal.TracerConfig, config ClientConfig) (*client, error) { - writerConfig, err := newWriterConfig(config, tracerConfig) - if err != nil { - return nil, err - } - - writer, err := internal.NewWriter(writerConfig) - if err != nil { - return nil, err - } - - client := &client{ - tracerConfig: tracerConfig, - writer: writer, - clientConfig: config, - flushMapper: mapper.NewDefaultMapper(config.HeartbeatInterval, config.ExtendedHeartbeatInterval), - payloadQueue: internal.NewRingQueue[transport.Payload](config.PayloadQueueSize), - - dependencies: dependencies{ - DependencyLoader: config.DependencyLoader, - }, - metrics: metrics{ - skipAllowlist: config.Debug, - pool: internal.NewSyncPool(func() *metricPoint { return &metricPoint{} }), - }, - distributions: distributions{ - skipAllowlist: config.Debug, - queueSize: config.DistributionsSize, - pool: internal.NewSyncPool(func() []float64 { return make([]float64, config.DistributionsSize.Min) }), - }, - } - - client.dataSources = append(client.dataSources, - &client.integrations, - &client.products, - &client.configuration, - &client.dependencies, - ) - - if config.LogsEnabled { - client.dataSources = append(client.dataSources, &client.logger) - } - - if config.MetricsEnabled { - client.dataSources = append(client.dataSources, &client.metrics, &client.distributions) - } - - client.flushTicker = internal.NewTicker(client.Flush, config.FlushInterval) - - return client, nil -} - -// dataSources is where the data that will be flushed is coming from. I.e metrics, logs, configurations, etc. -type dataSource interface { - Payload() transport.Payload -} - -type client struct { - tracerConfig internal.TracerConfig - clientConfig ClientConfig - - // Data sources - dataSources []dataSource - integrations integrations - products products - configuration configuration - dependencies dependencies - logger logger - metrics metrics - distributions distributions - - // flushMapper is the transformer to use for the next flush on the gathered bodies on this tick - flushMapper mapper.Mapper - flushMapperMu sync.Mutex - - // flushTicker is the ticker that triggers a call to client.Flush every flush interval - flushTicker *internal.Ticker - - // writer is the writer to use to send the payloads to the backend or the agent - writer internal.Writer - - // payloadQueue is used when we cannot flush previously built payload for multiple reasons. - payloadQueue *internal.RingQueue[transport.Payload] -} - -func (c *client) Log(level LogLevel, text string, options ...LogOption) { - if !c.clientConfig.LogsEnabled { - return - } - - c.logger.Add(level, text, options...) -} - -func (c *client) MarkIntegrationAsLoaded(integration Integration) { - c.integrations.Add(integration) -} - -func (c *client) Count(namespace Namespace, name string, tags []string) MetricHandle { - if !c.clientConfig.MetricsEnabled { - return noopMetricHandle{} - } - return c.metrics.LoadOrStore(namespace, transport.CountMetric, name, tags) -} - -func (c *client) Rate(namespace Namespace, name string, tags []string) MetricHandle { - if !c.clientConfig.MetricsEnabled { - return noopMetricHandle{} - } - return c.metrics.LoadOrStore(namespace, transport.RateMetric, name, tags) -} - -func (c *client) Gauge(namespace Namespace, name string, tags []string) MetricHandle { - if !c.clientConfig.MetricsEnabled { - return noopMetricHandle{} - } - return c.metrics.LoadOrStore(namespace, transport.GaugeMetric, name, tags) -} - -func (c *client) Distribution(namespace Namespace, name string, tags []string) MetricHandle { - if !c.clientConfig.MetricsEnabled { - return noopMetricHandle{} - } - return c.distributions.LoadOrStore(namespace, name, tags) -} - -func (c *client) ProductStarted(product Namespace) { - c.products.Add(product, true, nil) -} - -func (c *client) ProductStopped(product Namespace) { - c.products.Add(product, false, nil) -} - -func (c *client) ProductStartError(product Namespace, err error) { - c.products.Add(product, false, err) -} - -func (c *client) RegisterAppConfig(key string, value any, origin Origin) { - c.configuration.Add(Configuration{key, value, origin}) -} - -func (c *client) RegisterAppConfigs(kvs ...Configuration) { - for _, value := range kvs { - c.configuration.Add(value) - } -} - -func (c *client) Config() ClientConfig { - return c.clientConfig -} - -// Flush sends all the data sources before calling flush -// This function is called by the flushTicker so it should not panic, or it will crash the whole customer application. -// If a panic occurs, we stop the telemetry and log the error. -func (c *client) Flush() { - defer func() { - r := recover() - if r == nil { - return - } - log.Warn("panic while flushing telemetry data, stopping telemetry: %v", r) - telemetryClientDisabled = true - if gc, ok := GlobalClient().(*client); ok && gc == c { - SwapClient(nil) - } - }() - - payloads := make([]transport.Payload, 0, 8) - for _, ds := range c.dataSources { - if payload := ds.Payload(); payload != nil { - payloads = append(payloads, payload) - } - } - - nbBytes, err := c.flush(payloads) - if err != nil { - log.Warn("telemetry: error while flushing: %v", err) - } - - if c.clientConfig.Debug { - log.Debug("telemetry: flushed %d bytes of data", nbBytes) - } -} - -func (c *client) transform(payloads []transport.Payload) []transport.Payload { - c.flushMapperMu.Lock() - defer c.flushMapperMu.Unlock() - payloads, c.flushMapper = c.flushMapper.Transform(payloads) - return payloads -} - -// flush sends all the data sources to the writer after having sent them through the [transform] function. -// It returns the amount of bytes sent to the writer. -func (c *client) flush(payloads []transport.Payload) (int, error) { - payloads = c.transform(payloads) - - if c.payloadQueue.IsEmpty() && len(payloads) == 0 { - return 0, nil - } - - emptyQueue := c.payloadQueue.IsEmpty() - // We enqueue the new payloads to preserve the order of the payloads - c.payloadQueue.Enqueue(payloads...) - payloads = c.payloadQueue.Flush() - - var ( - nbBytes int - speedIncreased bool - failedCalls []internal.EndpointRequestResult - ) - - for i, payload := range payloads { - results, err := c.writer.Flush(payload) - c.computeFlushMetrics(results, err) - if err != nil { - // We stop flushing when we encounter a fatal error, put the bodies in the queue and return the error - if results[len(results)-1].StatusCode == 413 { // If the payload is too large we have no way to divide it, we can only skip it... - log.Warn("telemetry: tried sending a payload that was too large, dropping it") - continue - } - c.payloadQueue.Enqueue(payloads[i:]...) - return nbBytes, err - } - - failedCalls = append(failedCalls, results[:len(results)-1]...) - successfulCall := results[len(results)-1] - - if !speedIncreased && successfulCall.PayloadByteSize > c.clientConfig.EarlyFlushPayloadSize { - // We increase the speed of the flushTicker to try to flush the remaining bodies faster as we are at risk of sending too large bodies to the backend - c.flushTicker.CanIncreaseSpeed() - speedIncreased = true - } - - nbBytes += successfulCall.PayloadByteSize - } - - if emptyQueue && !speedIncreased { // If we did not send a very big payload, and we have no payloads - c.flushTicker.CanDecreaseSpeed() - } - - if len(failedCalls) > 0 { - var errs []error - for _, call := range failedCalls { - errs = append(errs, call.Error) - } - log.Debug("non-fatal error(s) while flushing telemetry data: %v", errors.Join(errs...)) - } - - return nbBytes, nil -} - -// computeFlushMetrics computes and submits the metrics for the flush operation using the output from the writer.Flush method. -// It will submit the number of requests, responses, errors, the number of bytes sent and the duration of the call that was successful. -func (c *client) computeFlushMetrics(results []internal.EndpointRequestResult, reason error) { - if !c.clientConfig.internalMetricsEnabled { - return - } - - indexToEndpoint := func(i int) string { - if i == 0 && c.clientConfig.AgentURL != "" { - return "agent" - } - return "agentless" - } - - for i, result := range results { - endpoint := "endpoint:" + indexToEndpoint(i) - c.Count(transport.NamespaceTelemetry, "telemetry_api.requests", []string{endpoint}).Submit(1) - if result.StatusCode != 0 { - c.Count(transport.NamespaceTelemetry, "telemetry_api.responses", []string{endpoint, "status_code:" + strconv.Itoa(result.StatusCode)}).Submit(1) - } - - if result.Error != nil { - typ := "type:network" - if os.IsTimeout(result.Error) { - typ = "type:timeout" - } - var writerStatusCodeError *internal.WriterStatusCodeError - if errors.As(result.Error, &writerStatusCodeError) { - typ = "type:status_code" - } - c.Count(transport.NamespaceTelemetry, "telemetry_api.errors", []string{endpoint, typ}).Submit(1) - } - } - - if reason != nil { - return - } - - successfulCall := results[len(results)-1] - endpoint := "endpoint:" + indexToEndpoint(len(results)-1) - c.Distribution(transport.NamespaceTelemetry, "telemetry_api.bytes", []string{endpoint}).Submit(float64(successfulCall.PayloadByteSize)) - c.Distribution(transport.NamespaceTelemetry, "telemetry_api.ms", []string{endpoint}).Submit(float64(successfulCall.CallDuration.Milliseconds())) -} - -func (c *client) AppStart() { - c.flushMapperMu.Lock() - defer c.flushMapperMu.Unlock() - c.flushMapper = mapper.NewAppStartedMapper(c.flushMapper) -} - -func (c *client) AppStop() { - c.flushMapperMu.Lock() - defer c.flushMapperMu.Unlock() - c.flushMapper = mapper.NewAppClosingMapper(c.flushMapper) -} - -func (c *client) Close() error { - c.flushTicker.Stop() - return nil -} diff --git a/internal/newtelemetry/client_test.go b/internal/newtelemetry/client_test.go deleted file mode 100644 index 66e2a508d5..0000000000 --- a/internal/newtelemetry/client_test.go +++ /dev/null @@ -1,1592 +0,0 @@ -// 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 newtelemetry - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "runtime" - "runtime/debug" - "slices" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" - "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo" - "gopkg.in/DataDog/dd-trace-go.v1/internal/version" -) - -func TestNewClient(t *testing.T) { - for _, test := range []struct { - name string - tracerConfig internal.TracerConfig - clientConfig ClientConfig - newErr string - }{ - { - name: "nominal", - tracerConfig: internal.TracerConfig{ - Service: "test-service", - Env: "test-env", - Version: "1.0.0", - }, - clientConfig: ClientConfig{ - AgentURL: "http://localhost:8126", - }, - }, - { - name: "empty service", - tracerConfig: internal.TracerConfig{}, - newErr: "service name must not be empty", - }, - { - name: "empty agent url", - tracerConfig: internal.TracerConfig{ - Service: "test-service", - }, - clientConfig: ClientConfig{}, - newErr: "could not build any endpoint", - }, - { - name: "invalid agent url", - tracerConfig: internal.TracerConfig{ - Service: "test-service", - }, - clientConfig: ClientConfig{ - AgentURL: "toto_protocol://localhost:8126", - }, - newErr: "invalid agent URL", - }, - { - name: "Too big payload size", - tracerConfig: internal.TracerConfig{ - Service: "test-service", - }, - clientConfig: ClientConfig{ - AgentURL: "http://localhost:8126", - EarlyFlushPayloadSize: 64 * 1024 * 1024, // 64MB - }, - newErr: "EarlyFlushPayloadSize must be between 0 and 5MB", - }, - } { - t.Run(test.name, func(t *testing.T) { - c, err := NewClient(test.tracerConfig.Service, test.tracerConfig.Env, test.tracerConfig.Version, test.clientConfig) - if err == nil { - defer c.Close() - } - - if test.newErr == "" { - require.NoError(t, err) - } else { - require.ErrorContains(t, err, test.newErr) - } - }) - } -} - -func TestClientFlush(t *testing.T) { - tracerConfig := internal.TracerConfig{ - Service: "test-service", - Env: "test-env", - Version: "1.0.0", - } - - type testParams struct { - name string - clientConfig ClientConfig - when func(c *client) - expect func(*testing.T, []transport.Payload) - } - - testcases := []testParams{ - { - name: "heartbeat", - clientConfig: ClientConfig{ - HeartbeatInterval: time.Nanosecond, - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppHeartbeat{}, payload) - assert.Equal(t, payload.RequestType(), transport.RequestTypeAppHeartbeat) - }, - }, - { - name: "extended-heartbeat-config", - clientConfig: ClientConfig{ - HeartbeatInterval: time.Nanosecond, - ExtendedHeartbeatInterval: time.Nanosecond, - }, - when: func(c *client) { - c.RegisterAppConfig("key", "value", OriginDefault) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.MessageBatch{}, payload) - batch := payload.(transport.MessageBatch) - require.Len(t, batch, 2) - assert.Equal(t, transport.RequestTypeAppClientConfigurationChange, batch[0].RequestType) - assert.Equal(t, transport.RequestTypeAppExtendedHeartBeat, batch[1].RequestType) - - assert.Len(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Configuration, 0) - }, - }, - { - name: "extended-heartbeat-integrations", - clientConfig: ClientConfig{ - HeartbeatInterval: time.Nanosecond, - ExtendedHeartbeatInterval: time.Nanosecond, - }, - when: func(c *client) { - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.MessageBatch{}, payload) - batch := payload.(transport.MessageBatch) - require.Len(t, batch, 2) - assert.Equal(t, transport.RequestTypeAppIntegrationsChange, batch[0].RequestType) - assert.Equal(t, transport.RequestTypeAppExtendedHeartBeat, batch[1].RequestType) - assert.Len(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations, 1) - assert.Equal(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations[0].Name, "test-integration") - assert.Equal(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations[0].Version, "1.0.0") - }, - }, - { - name: "configuration-default", - when: func(c *client) { - c.RegisterAppConfig("key", "value", OriginDefault) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppClientConfigurationChange{}, payload) - config := payload.(transport.AppClientConfigurationChange) - assert.Len(t, config.Configuration, 1) - assert.Equal(t, config.Configuration[0].Name, "key") - assert.Equal(t, config.Configuration[0].Value, "value") - assert.Equal(t, config.Configuration[0].Origin, OriginDefault) - }, - }, - { - name: "configuration-default", - when: func(c *client) { - c.RegisterAppConfig("key", "value", OriginDefault) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppClientConfigurationChange{}, payload) - config := payload.(transport.AppClientConfigurationChange) - assert.Len(t, config.Configuration, 1) - assert.Equal(t, config.Configuration[0].Name, "key") - assert.Equal(t, config.Configuration[0].Value, "value") - assert.Equal(t, config.Configuration[0].Origin, OriginDefault) - }, - }, - { - name: "configuration-complex-values", - when: func(c *client) { - c.RegisterAppConfigs( - Configuration{Name: "key1", Value: []string{"value1", "value2"}, Origin: OriginDefault}, - Configuration{Name: "key2", Value: map[string]string{"key": "value", "key2": "value2"}, Origin: OriginCode}, - Configuration{Name: "key3", Value: []int{1, 2, 3}, Origin: OriginDDConfig}, - Configuration{Name: "key4", Value: struct { - A string - }{A: "1"}, Origin: OriginEnvVar}, - Configuration{Name: "key5", Value: map[int]struct{ X int }{1: {X: 1}}, Origin: OriginRemoteConfig}, - ) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppClientConfigurationChange{}, payload) - config := payload.(transport.AppClientConfigurationChange) - - slices.SortStableFunc(config.Configuration, func(a, b transport.ConfKeyValue) int { - return strings.Compare(a.Name, b.Name) - }) - - assert.Len(t, config.Configuration, 5) - assert.Equal(t, "key1", config.Configuration[0].Name) - assert.Equal(t, "value1,value2", config.Configuration[0].Value) - assert.Equal(t, OriginDefault, config.Configuration[0].Origin) - assert.Equal(t, "key2", config.Configuration[1].Name) - assert.Equal(t, "key:value,key2:value2", config.Configuration[1].Value) - assert.Equal(t, OriginCode, config.Configuration[1].Origin) - assert.Equal(t, "key3", config.Configuration[2].Name) - assert.Equal(t, "[1 2 3]", config.Configuration[2].Value) - assert.Equal(t, OriginDDConfig, config.Configuration[2].Origin) - assert.Equal(t, "key4", config.Configuration[3].Name) - assert.Equal(t, "{1}", config.Configuration[3].Value) - assert.Equal(t, OriginEnvVar, config.Configuration[3].Origin) - assert.Equal(t, "key5", config.Configuration[4].Name) - assert.Equal(t, "1:{1}", config.Configuration[4].Value) - assert.Equal(t, OriginRemoteConfig, config.Configuration[4].Origin) - }, - }, - { - name: "product-start", - when: func(c *client) { - c.ProductStarted("test-product") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppProductChange{}, payload) - productChange := payload.(transport.AppProductChange) - assert.Len(t, productChange.Products, 1) - assert.True(t, productChange.Products[Namespace("test-product")].Enabled) - }, - }, - { - name: "product-start-error", - when: func(c *client) { - c.ProductStartError("test-product", errors.New("test-error")) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppProductChange{}, payload) - productChange := payload.(transport.AppProductChange) - assert.Len(t, productChange.Products, 1) - assert.False(t, productChange.Products[Namespace("test-product")].Enabled) - assert.Equal(t, "test-error", productChange.Products[Namespace("test-product")].Error.Message) - }, - }, - { - name: "product-stop", - when: func(c *client) { - c.ProductStopped("test-product") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppProductChange{}, payload) - productChange := payload.(transport.AppProductChange) - assert.Len(t, productChange.Products, 1) - assert.False(t, productChange.Products[Namespace("test-product")].Enabled) - }, - }, - { - name: "integration", - when: func(c *client) { - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppIntegrationChange{}, payload) - integrationChange := payload.(transport.AppIntegrationChange) - assert.Len(t, integrationChange.Integrations, 1) - assert.Equal(t, integrationChange.Integrations[0].Name, "test-integration") - assert.Equal(t, integrationChange.Integrations[0].Version, "1.0.0") - assert.True(t, integrationChange.Integrations[0].Enabled) - }, - }, - { - name: "integration-error", - when: func(c *client) { - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0", Error: "test-error"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppIntegrationChange{}, payload) - integrationChange := payload.(transport.AppIntegrationChange) - assert.Len(t, integrationChange.Integrations, 1) - assert.Equal(t, integrationChange.Integrations[0].Name, "test-integration") - assert.Equal(t, integrationChange.Integrations[0].Version, "1.0.0") - assert.False(t, integrationChange.Integrations[0].Enabled) - assert.Equal(t, integrationChange.Integrations[0].Error, "test-error") - }, - }, - { - name: "product+integration", - when: func(c *client) { - c.ProductStarted("test-product") - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.MessageBatch{}, payload) - batch := payload.(transport.MessageBatch) - assert.Len(t, batch, 2) - for _, payload := range batch { - switch p := payload.Payload.(type) { - case transport.AppProductChange: - assert.Equal(t, transport.RequestTypeAppProductChange, payload.RequestType) - assert.Len(t, p.Products, 1) - assert.True(t, p.Products[Namespace("test-product")].Enabled) - case transport.AppIntegrationChange: - assert.Equal(t, transport.RequestTypeAppIntegrationsChange, payload.RequestType) - assert.Len(t, p.Integrations, 1) - assert.Equal(t, p.Integrations[0].Name, "test-integration") - assert.Equal(t, p.Integrations[0].Version, "1.0.0") - assert.True(t, p.Integrations[0].Enabled) - default: - t.Fatalf("unexpected payload type: %T", p) - } - } - }, - }, - { - name: "product+integration+heartbeat", - clientConfig: ClientConfig{ - HeartbeatInterval: time.Nanosecond, - }, - when: func(c *client) { - c.ProductStarted("test-product") - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.MessageBatch{}, payload) - batch := payload.(transport.MessageBatch) - assert.Len(t, batch, 3) - for _, payload := range batch { - switch p := payload.Payload.(type) { - case transport.AppProductChange: - assert.Equal(t, transport.RequestTypeAppProductChange, payload.RequestType) - assert.Len(t, p.Products, 1) - assert.True(t, p.Products[Namespace("test-product")].Enabled) - case transport.AppIntegrationChange: - assert.Equal(t, transport.RequestTypeAppIntegrationsChange, payload.RequestType) - assert.Len(t, p.Integrations, 1) - assert.Equal(t, p.Integrations[0].Name, "test-integration") - assert.Equal(t, p.Integrations[0].Version, "1.0.0") - assert.True(t, p.Integrations[0].Enabled) - case transport.AppHeartbeat: - assert.Equal(t, transport.RequestTypeAppHeartbeat, payload.RequestType) - default: - t.Fatalf("unexpected payload type: %T", p) - } - } - }, - }, - { - name: "app-started", - when: func(c *client) { - c.AppStart() - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppStarted{}, payload) - appStart := payload.(transport.AppStarted) - assert.Equal(t, appStart.InstallSignature.InstallID, globalconfig.InstrumentationInstallID()) - assert.Equal(t, appStart.InstallSignature.InstallType, globalconfig.InstrumentationInstallType()) - assert.Equal(t, appStart.InstallSignature.InstallTime, globalconfig.InstrumentationInstallTime()) - }, - }, - { - name: "app-started-with-product", - when: func(c *client) { - c.AppStart() - c.ProductStarted("test-product") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppStarted{}, payload) - appStart := payload.(transport.AppStarted) - assert.Equal(t, appStart.Products[Namespace("test-product")].Enabled, true) - }, - }, - { - name: "app-started-with-configuration", - when: func(c *client) { - c.AppStart() - c.RegisterAppConfig("key", "value", OriginDefault) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppStarted{}, payload) - appStart := payload.(transport.AppStarted) - require.Len(t, appStart.Configuration, 1) - assert.Equal(t, appStart.Configuration[0].Name, "key") - assert.Equal(t, appStart.Configuration[0].Value, "value") - }, - }, - { - name: "app-started+integrations", - when: func(c *client) { - c.AppStart() - c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppStarted{}, payload) - appStart := payload.(transport.AppStarted) - assert.Equal(t, globalconfig.InstrumentationInstallID(), appStart.InstallSignature.InstallID) - assert.Equal(t, globalconfig.InstrumentationInstallType(), appStart.InstallSignature.InstallType) - assert.Equal(t, globalconfig.InstrumentationInstallTime(), appStart.InstallSignature.InstallTime) - - payload = payloads[1] - require.IsType(t, transport.AppIntegrationChange{}, payload) - p := payload.(transport.AppIntegrationChange) - - assert.Len(t, p.Integrations, 1) - assert.Equal(t, p.Integrations[0].Name, "test-integration") - assert.Equal(t, p.Integrations[0].Version, "1.0.0") - }, - }, - { - name: "app-started+heartbeat", - clientConfig: ClientConfig{ - HeartbeatInterval: time.Nanosecond, - }, - when: func(c *client) { - c.AppStart() - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppStarted{}, payload) - appStart := payload.(transport.AppStarted) - assert.Equal(t, globalconfig.InstrumentationInstallID(), appStart.InstallSignature.InstallID) - assert.Equal(t, globalconfig.InstrumentationInstallType(), appStart.InstallSignature.InstallType) - assert.Equal(t, globalconfig.InstrumentationInstallTime(), appStart.InstallSignature.InstallTime) - - payload = payloads[1] - require.IsType(t, transport.AppHeartbeat{}, payload) - }, - }, - { - name: "app-stopped", - when: func(c *client) { - c.AppStop() - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppClosing{}, payload) - }, - }, - { - name: "app-dependencies-loaded", - clientConfig: ClientConfig{ - DependencyLoader: func() (*debug.BuildInfo, bool) { - return &debug.BuildInfo{ - Deps: []*debug.Module{ - {Path: "test", Version: "v1.0.0"}, - {Path: "test2", Version: "v2.0.0"}, - {Path: "test3", Version: "3.0.0"}, - }, - }, true - }, - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppDependenciesLoaded{}, payload) - deps := payload.(transport.AppDependenciesLoaded) - - assert.Len(t, deps.Dependencies, 3) - assert.Equal(t, deps.Dependencies[0].Name, "test") - assert.Equal(t, deps.Dependencies[0].Version, "1.0.0") - assert.Equal(t, deps.Dependencies[1].Name, "test2") - assert.Equal(t, deps.Dependencies[1].Version, "2.0.0") - assert.Equal(t, deps.Dependencies[2].Name, "test3") - assert.Equal(t, deps.Dependencies[2].Version, "3.0.0") - }, - }, - { - name: "app-many-dependencies-loaded", - clientConfig: ClientConfig{ - DependencyLoader: func() (*debug.BuildInfo, bool) { - modules := make([]*debug.Module, 2001) - for i := range modules { - modules[i] = &debug.Module{ - Path: fmt.Sprintf("test-%d", i), - Version: fmt.Sprintf("v%d.0.0", i), - } - } - return &debug.BuildInfo{ - Deps: modules, - }, true - }, - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.AppDependenciesLoaded{}, payload) - deps := payload.(transport.AppDependenciesLoaded) - - if len(deps.Dependencies) != 2000 && len(deps.Dependencies) != 1 { - t.Fatalf("expected 2000 and 1 dependencies, got %d", len(deps.Dependencies)) - } - - if len(deps.Dependencies) == 1 { - assert.Equal(t, deps.Dependencies[0].Name, "test-0") - assert.Equal(t, deps.Dependencies[0].Version, "0.0.0") - return - } - - for i := range deps.Dependencies { - assert.Equal(t, deps.Dependencies[i].Name, fmt.Sprintf("test-%d", i)) - assert.Equal(t, deps.Dependencies[i].Version, fmt.Sprintf("%d.0.0", i)) - } - }, - }, - { - name: "single-log-debug", - when: func(c *client) { - c.Log(LogDebug, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelDebug, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - }, - }, - { - name: "single-log-warn", - when: func(c *client) { - c.Log(LogWarn, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelWarn, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - }, - }, - { - name: "single-log-error", - when: func(c *client) { - c.Log(LogError, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - }, - }, - { - name: "multiple-logs-same-key", - when: func(c *client) { - c.Log(LogError, "test") - c.Log(LogError, "test") - c.Log(LogError, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Equal(t, uint32(3), logs.Logs[0].Count) - }, - }, - { - name: "single-log-with-tag", - when: func(c *client) { - c.Log(LogError, "test", WithTags([]string{"key:value"})) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Equal(t, "key:value", logs.Logs[0].Tags) - }, - }, - { - name: "single-log-with-tags", - when: func(c *client) { - c.Log(LogError, "test", WithTags([]string{"key:value", "key2:value2"})) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - tags := strings.Split(logs.Logs[0].Tags, ",") - assert.Contains(t, tags, "key:value") - assert.Contains(t, tags, "key2:value2") - }, - }, - { - name: "single-log-with-tags-and-without", - when: func(c *client) { - c.Log(LogError, "test", WithTags([]string{"key:value", "key2:value2"})) - c.Log(LogError, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 2) - - slices.SortStableFunc(logs.Logs, func(i, j transport.LogMessage) int { - return strings.Compare(i.Tags, j.Tags) - }) - - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Equal(t, uint32(1), logs.Logs[0].Count) - assert.Empty(t, logs.Logs[0].Tags) - - assert.Equal(t, transport.LogLevelError, logs.Logs[1].Level) - assert.Equal(t, "test", logs.Logs[1].Message) - assert.Equal(t, uint32(1), logs.Logs[1].Count) - tags := strings.Split(logs.Logs[1].Tags, ",") - assert.Contains(t, tags, "key:value") - assert.Contains(t, tags, "key2:value2") - }, - }, - { - name: "single-log-with-stacktrace", - when: func(c *client) { - c.Log(LogError, "test", WithStacktrace()) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Contains(t, logs.Logs[0].StackTrace, "internal/newtelemetry/client_test.go") - }, - }, - { - name: "single-log-with-stacktrace-and-tags", - when: func(c *client) { - c.Log(LogError, "test", WithStacktrace(), WithTags([]string{"key:value", "key2:value2"})) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Contains(t, logs.Logs[0].StackTrace, "internal/newtelemetry/client_test.go") - tags := strings.Split(logs.Logs[0].Tags, ",") - assert.Contains(t, tags, "key:value") - assert.Contains(t, tags, "key2:value2") - - }, - }, - { - name: "multiple-logs-different-levels", - when: func(c *client) { - c.Log(LogError, "test") - c.Log(LogWarn, "test") - c.Log(LogDebug, "test") - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.Logs{}, payload) - logs := payload.(transport.Logs) - require.Len(t, logs.Logs, 3) - - slices.SortStableFunc(logs.Logs, func(i, j transport.LogMessage) int { - return strings.Compare(string(i.Level), string(j.Level)) - }) - - assert.Equal(t, transport.LogLevelDebug, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) - assert.Equal(t, uint32(1), logs.Logs[0].Count) - assert.Equal(t, transport.LogLevelError, logs.Logs[1].Level) - assert.Equal(t, "test", logs.Logs[1].Message) - assert.Equal(t, uint32(1), logs.Logs[1].Count) - assert.Equal(t, transport.LogLevelWarn, logs.Logs[2].Level) - assert.Equal(t, "test", logs.Logs[2].Message) - assert.Equal(t, uint32(1), logs.Logs[2].Count) - }, - }, - { - name: "simple-count", - when: func(c *client) { - c.Count(NamespaceTracers, "init_time", nil).Submit(1) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 1) - assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - assert.Equal(t, 1.0, metrics.Series[0].Points[0][1]) - }, - }, - { - name: "count-multiple-call-same-handle", - when: func(c *client) { - handle1 := c.Count(NamespaceTracers, "init_time", nil) - handle2 := c.Count(NamespaceTracers, "init_time", nil) - - handle2.Submit(1) - handle1.Submit(1) - handle1.Submit(3) - handle2.Submit(2) - handle2.Submit(10) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 1) - assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - assert.Equal(t, 17.0, metrics.Series[0].Points[0][1]) - }, - }, - { - name: "multiple-count-by-name", - when: func(c *client) { - c.Count(NamespaceTracers, "init_time_1", nil).Submit(1) - c.Count(NamespaceTracers, "init_time_2", nil).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 2) - - assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64))), metrics.Series[0].Metric) - - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - - assert.Equal(t, transport.CountMetric, metrics.Series[1].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) - assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64))), metrics.Series[1].Metric) - assert.Empty(t, metrics.Series[1].Tags) - assert.NotZero(t, metrics.Series[1].Points[0][0]) - }, - }, - { - name: "multiple-count-by-tags", - when: func(c *client) { - c.Count(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) - c.Count(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 2) - - assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - - assert.Contains(t, metrics.Series[0].Tags, "test:"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64)))) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - - assert.Equal(t, transport.CountMetric, metrics.Series[1].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) - assert.Equal(t, "init_time", metrics.Series[1].Metric) - assert.Contains(t, metrics.Series[1].Tags, "test:"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64)))) - assert.NotZero(t, metrics.Series[1].Points[0][0]) - }, - }, - { - name: "simple-gauge", - when: func(c *client) { - c.Gauge(NamespaceTracers, "init_time", nil).Submit(1) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 1) - assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - assert.Equal(t, 1.0, metrics.Series[0].Points[0][1]) - }, - }, - { - name: "multiple-gauge-by-name", - when: func(c *client) { - c.Gauge(NamespaceTracers, "init_time_1", nil).Submit(1) - c.Gauge(NamespaceTracers, "init_time_2", nil).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 2) - - assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64))), metrics.Series[0].Metric) - - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - - assert.Equal(t, transport.GaugeMetric, metrics.Series[1].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) - assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64))), metrics.Series[1].Metric) - assert.Empty(t, metrics.Series[1].Tags) - assert.NotZero(t, metrics.Series[1].Points[0][0]) - }, - }, - { - name: "multiple-gauge-by-tags", - when: func(c *client) { - c.Gauge(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) - c.Gauge(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 2) - - assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - - assert.Contains(t, metrics.Series[0].Tags, "test:"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64)))) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - - assert.Equal(t, transport.GaugeMetric, metrics.Series[1].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) - assert.Equal(t, "init_time", metrics.Series[1].Metric) - assert.Contains(t, metrics.Series[1].Tags, "test:"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64)))) - assert.NotZero(t, metrics.Series[1].Points[0][0]) - }, - }, - { - name: "simple-rate", - when: func(c *client) { - handle := c.Rate(NamespaceTracers, "init_time", nil) - handle.Submit(1) - - rate := handle.(*rate) - // So the rate is not +Infinity because the interval is zero - now := rate.intervalStart.Load() - sub := now.Add(-time.Second) - rate.intervalStart.Store(&sub) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, transport.GenerateMetrics{}, payload) - metrics := payload.(transport.GenerateMetrics) - require.Len(t, metrics.Series, 1) - assert.Equal(t, transport.RateMetric, metrics.Series[0].Type) - assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) - assert.Equal(t, "init_time", metrics.Series[0].Metric) - assert.Empty(t, metrics.Series[0].Tags) - assert.NotZero(t, metrics.Series[0].Interval) - assert.NotZero(t, metrics.Series[0].Points[0][0]) - assert.LessOrEqual(t, metrics.Series[0].Points[0][1], 1.1) - }, - }, - { - name: "simple-distribution", - when: func(c *client) { - c.Distribution(NamespaceGeneral, "init_time", nil).Submit(1) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, payload, transport.Distributions{}) - distributions := payload.(transport.Distributions) - require.Len(t, distributions.Series, 1) - assert.Equal(t, NamespaceGeneral, distributions.Series[0].Namespace) - assert.Equal(t, "init_time", distributions.Series[0].Metric) - assert.Empty(t, distributions.Series[0].Tags) - require.Len(t, distributions.Series[0].Points, 1) - assert.Equal(t, 1.0, distributions.Series[0].Points[0]) - }, - }, - { - name: "multiple-distribution-by-name", - when: func(c *client) { - c.Distribution(NamespaceTracers, "init_time_1", nil).Submit(1) - c.Distribution(NamespaceTracers, "init_time_2", nil).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, payload, transport.Distributions{}) - distributions := payload.(transport.Distributions) - require.Len(t, distributions.Series, 2) - - assert.Equal(t, "init_time_"+strconv.Itoa(int(distributions.Series[0].Points[0])), distributions.Series[0].Metric) - assert.Equal(t, "init_time_"+strconv.Itoa(int(distributions.Series[1].Points[0])), distributions.Series[1].Metric) - }, - }, - { - name: "multiple-distribution-by-tags", - when: func(c *client) { - c.Distribution(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) - c.Distribution(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, payload, transport.Distributions{}) - distributions := payload.(transport.Distributions) - require.Len(t, distributions.Series, 2) - - assert.Contains(t, distributions.Series[0].Tags, "test:"+strconv.Itoa(int(distributions.Series[0].Points[0]))) - assert.Contains(t, distributions.Series[1].Tags, "test:"+strconv.Itoa(int(distributions.Series[1].Points[0]))) - }, - }, - { - name: "distribution-overflow", - when: func(c *client) { - handler := c.Distribution(NamespaceGeneral, "init_time", nil) - for i := 0; i < 1<<16; i++ { - handler.Submit(float64(i)) - } - }, - expect: func(t *testing.T, payloads []transport.Payload) { - payload := payloads[0] - require.IsType(t, payload, transport.Distributions{}) - distributions := payload.(transport.Distributions) - require.Len(t, distributions.Series, 1) - assert.Equal(t, NamespaceGeneral, distributions.Series[0].Namespace) - assert.Equal(t, "init_time", distributions.Series[0].Metric) - assert.Empty(t, distributions.Series[0].Tags) - - // Should not contain the first passed point - assert.NotContains(t, distributions.Series[0].Points, 0.0) - }, - }, - } - - for _, test := range testcases { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - config := defaultConfig(test.clientConfig) - config.AgentURL = "http://localhost:8126" - config.DependencyLoader = test.clientConfig.DependencyLoader // Don't use the default dependency loader - config.internalMetricsEnabled = test.clientConfig.internalMetricsEnabled // only enabled internal metrics when explicitly set - config.internalMetricsEnabled = false - c, err := newClient(tracerConfig, config) - require.NoError(t, err) - defer c.Close() - - recordWriter := &internal.RecordWriter{} - c.writer = recordWriter - - if test.when != nil { - test.when(c) - } - c.Flush() - - payloads := recordWriter.Payloads() - require.LessOrEqual(t, 1, len(payloads)) - test.expect(t, payloads) - }) - } -} - -func TestMetricsDisabled(t *testing.T) { - t.Setenv("DD_TELEMETRY_METRICS_ENABLED", "false") - - c, err := NewClient("test-service", "test-env", "1.0.0", ClientConfig{AgentURL: "http://localhost:8126"}) - require.NoError(t, err) - - recordWriter := &internal.RecordWriter{} - c.(*client).writer = recordWriter - - defer c.Close() - - assert.NotNil(t, c.Gauge(NamespaceTracers, "init_time", nil)) - assert.NotNil(t, c.Count(NamespaceTracers, "init_time", nil)) - assert.NotNil(t, c.Rate(NamespaceTracers, "init_time", nil)) - assert.NotNil(t, c.Distribution(NamespaceGeneral, "init_time", nil)) - - c.Flush() - - payloads := recordWriter.Payloads() - require.Len(t, payloads, 0) -} - -type testRoundTripper struct { - t *testing.T - roundTrip func(*http.Request) (*http.Response, error) - bodies []transport.Body -} - -func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - defer req.Body.Close() - body, err := io.ReadAll(req.Body) - require.NoError(t.t, err) - req.Body = io.NopCloser(bytes.NewReader(body)) - t.bodies = append(t.bodies, parseRequest(t.t, req.Header, body)) - return t.roundTrip(req) -} - -func parseRequest(t *testing.T, headers http.Header, raw []byte) transport.Body { - t.Helper() - - assert.Equal(t, "v2", headers.Get("DD-Telemetry-API-Version")) - assert.Equal(t, "application/json", headers.Get("Content-Type")) - assert.Equal(t, "go", headers.Get("DD-Client-Library-Language")) - assert.Equal(t, "test-env", headers.Get("DD-Agent-Env")) - assert.Equal(t, version.Tag, headers.Get("DD-Client-Library-Version")) - assert.Equal(t, globalconfig.InstrumentationInstallID(), headers.Get("DD-Agent-Install-Id")) - assert.Equal(t, globalconfig.InstrumentationInstallType(), headers.Get("DD-Agent-Install-Type")) - assert.Equal(t, globalconfig.InstrumentationInstallTime(), headers.Get("DD-Agent-Install-Time")) - - assert.NotEmpty(t, headers.Get("DD-Agent-Hostname")) - - var body transport.Body - require.NoError(t, json.Unmarshal(raw, &body)) - - assert.Equal(t, string(body.RequestType), headers.Get("DD-Telemetry-Request-Type")) - assert.Equal(t, "test-service", body.Application.ServiceName) - assert.Equal(t, "test-env", body.Application.Env) - assert.Equal(t, "1.0.0", body.Application.ServiceVersion) - assert.Equal(t, "go", body.Application.LanguageName) - assert.Equal(t, runtime.Version(), body.Application.LanguageVersion) - - assert.NotEmpty(t, body.Host.Hostname) - assert.Equal(t, osinfo.OSName(), body.Host.OS) - assert.Equal(t, osinfo.OSVersion(), body.Host.OSVersion) - assert.Equal(t, osinfo.Architecture(), body.Host.Architecture) - assert.Equal(t, osinfo.KernelName(), body.Host.KernelName) - assert.Equal(t, osinfo.KernelRelease(), body.Host.KernelRelease) - assert.Equal(t, osinfo.KernelVersion(), body.Host.KernelVersion) - - assert.Equal(t, "v2", body.APIVersion) - assert.NotZero(t, body.TracerTime) - assert.LessOrEqual(t, int64(1), body.SeqID) - assert.Equal(t, globalconfig.RuntimeID(), body.RuntimeID) - - return body -} - -func TestClientEnd2End(t *testing.T) { - tracerConfig := internal.TracerConfig{ - Service: "test-service", - Env: "test-env", - Version: "1.0.0", - } - - for _, test := range []struct { - name string - when func(*client) - roundtrip func(*testing.T, *http.Request) (*http.Response, error) - expect func(*testing.T, []transport.Body) - }{ - { - name: "app-start", - when: func(c *client) { - c.AppStart() - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 1) - assert.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) - }, - }, - { - name: "app-stop", - when: func(c *client) { - c.AppStop() - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 1) - assert.Equal(t, transport.RequestTypeAppClosing, bodies[0].RequestType) - }, - }, - { - name: "app-start+app-stop", - when: func(c *client) { - c.AppStart() - c.AppStop() - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 2) - assert.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) - assert.Equal(t, transport.RequestTypeAppClosing, bodies[1].RequestType) - }, - }, - { - name: "message-batch", - when: func(c *client) { - c.RegisterAppConfig("key", "value", OriginCode) - c.ProductStarted(NamespaceAppSec) - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 1) - assert.Equal(t, transport.RequestTypeMessageBatch, bodies[0].RequestType) - batch := bodies[0].Payload.(transport.MessageBatch) - require.Len(t, batch, 2) - assert.Equal(t, transport.RequestTypeAppProductChange, batch[0].RequestType) - assert.Equal(t, transport.RequestTypeAppClientConfigurationChange, batch[1].RequestType) - }, - }, - { - name: "fail-agent-endpoint", - when: func(c *client) { - c.AppStart() - }, - roundtrip: func(_ *testing.T, req *http.Request) (*http.Response, error) { - if strings.Contains(req.URL.Host, "localhost") { - return nil, errors.New("failed") - } - return &http.Response{StatusCode: http.StatusOK}, nil - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 2) - require.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) - require.Equal(t, transport.RequestTypeAppStarted, bodies[1].RequestType) - }, - }, - { - name: "fail-all-endpoint", - when: func(c *client) { - c.AppStart() - }, - roundtrip: func(_ *testing.T, _ *http.Request) (*http.Response, error) { - return nil, errors.New("failed") - }, - expect: func(t *testing.T, bodies []transport.Body) { - require.Len(t, bodies, 2) - require.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) - require.Equal(t, transport.RequestTypeAppStarted, bodies[1].RequestType) - }, - }, - } { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - rt := &testRoundTripper{ - t: t, - roundTrip: func(req *http.Request) (*http.Response, error) { - if test.roundtrip != nil { - return test.roundtrip(t, req) - } - return &http.Response{StatusCode: http.StatusOK}, nil - }, - } - clientConfig := ClientConfig{ - AgentURL: "http://localhost:8126", - APIKey: "apikey", - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: rt, - }, - Debug: true, - } - - clientConfig = defaultConfig(clientConfig) - clientConfig.DependencyLoader = nil - - c, err := newClient(tracerConfig, clientConfig) - require.NoError(t, err) - defer c.Close() - - test.when(c) - c.Flush() - test.expect(t, rt.bodies) - }) - } -} - -func TestHeartBeatInterval(t *testing.T) { - startTime := time.Now() - payloadtimes := make([]time.Duration, 0, 32) - c, err := NewClient("test-service", "test-env", "1.0.0", ClientConfig{ - AgentURL: "http://localhost:8126", - HeartbeatInterval: 2 * time.Second, - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &testRoundTripper{ - t: t, - roundTrip: func(_ *http.Request) (*http.Response, error) { - payloadtimes = append(payloadtimes, time.Since(startTime)) - startTime = time.Now() - return &http.Response{ - StatusCode: http.StatusOK, - }, nil - }, - }, - }, - }) - require.NoError(t, err) - defer c.Close() - - for i := 0; i < 10; i++ { - c.Log(LogError, "test") - time.Sleep(1 * time.Second) - } - - // 10 seconds have passed, we should have sent 5 heartbeats - - c.Flush() - c.Close() - - require.InDelta(t, 5, len(payloadtimes), 1) - sum := 0.0 - for _, d := range payloadtimes { - sum += d.Seconds() - } - - assert.InDelta(t, 2, sum/5, 0.1) -} - -func TestSendingFailures(t *testing.T) { - cfg := ClientConfig{ - AgentURL: "http://localhost:8126", - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: &testRoundTripper{ - t: t, - roundTrip: func(_ *http.Request) (*http.Response, error) { - return nil, errors.New("failed") - }, - }, - }, - } - - c, err := newClient(internal.TracerConfig{ - Service: "test-service", - Env: "test-env", - Version: "1.0.0", - }, defaultConfig(cfg)) - - require.NoError(t, err) - defer c.Close() - - c.Log(LogError, "test") - c.Flush() - - require.False(t, c.payloadQueue.IsEmpty()) - payload := c.payloadQueue.ReversePeek() - require.NotNil(t, payload) - - assert.Equal(t, transport.RequestTypeLogs, payload.RequestType()) - logs := payload.(transport.Logs) - assert.Len(t, logs.Logs, 1) - assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) - assert.Equal(t, "test", logs.Logs[0].Message) -} - -func BenchmarkLogs(b *testing.B) { - clientConfig := ClientConfig{ - HeartbeatInterval: time.Hour, - ExtendedHeartbeatInterval: time.Hour, - AgentURL: "http://localhost:8126", - } - - b.Run("simple", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Log(LogDebug, "this is supposed to be a DEBUG log of representative length with a variable message: "+strconv.Itoa(i%10)) - } - }) - - b.Run("with-tags", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Log(LogWarn, "this is supposed to be a WARN log of representative length", WithTags([]string{"key:" + strconv.Itoa(i%10)})) - } - }) - - b.Run("with-stacktrace", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Log(LogError, "this is supposed to be a ERROR log of representative length", WithStacktrace()) - } - }) -} - -type noopTransport struct{} - -func (noopTransport) RoundTrip(*http.Request) (*http.Response, error) { - return &http.Response{StatusCode: http.StatusOK}, nil -} - -func BenchmarkWorstCaseScenarioFloodLogging(b *testing.B) { - b.ReportAllocs() - nbSameLogs := 10 - nbDifferentLogs := 100 - nbGoroutines := 25 - - clientConfig := ClientConfig{ - HeartbeatInterval: time.Hour, - ExtendedHeartbeatInterval: time.Hour, - FlushInterval: internal.Range[time.Duration]{Min: time.Second, Max: time.Second}, - AgentURL: "http://localhost:8126", - - // Empty transport to avoid sending data to the agent - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: noopTransport{}, - }, - } - - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - - for x := 0; x < b.N; x++ { - var wg sync.WaitGroup - - for i := 0; i < nbGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < nbDifferentLogs; j++ { - for k := 0; k < nbSameLogs; k++ { - c.Log(LogDebug, "this is supposed to be a DEBUG log of representative length"+strconv.Itoa(i), WithTags([]string{"key:" + strconv.Itoa(j)})) - } - } - }() - } - - wg.Wait() - } - - b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentLogs*nbSameLogs*b.N)), "ns/log") -} - -func BenchmarkMetrics(b *testing.B) { - b.ReportAllocs() - clientConfig := ClientConfig{ - HeartbeatInterval: time.Hour, - ExtendedHeartbeatInterval: time.Hour, - AgentURL: "http://localhost:8126", - } - - b.Run("count+get-handle", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Count(NamespaceGeneral, "logs_created", nil).Submit(1) - } - }) - - b.Run("count+handle-reused", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - handle := c.Count(NamespaceGeneral, "logs_created", nil) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - handle.Submit(1) - } - }) - - b.Run("gauge+get-handle", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Gauge(NamespaceTracers, "stats_buckets", nil).Submit(1) - } - }) - - b.Run("gauge+handle-reused", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - handle := c.Gauge(NamespaceTracers, "stats_buckets", nil) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - handle.Submit(1) - } - }) - - b.Run("rate+get-handle", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Rate(NamespaceTracers, "init_time", nil).Submit(1) - } - }) - - b.Run("rate+handle-reused", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - handle := c.Rate(NamespaceTracers, "init_time", nil) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - handle.Submit(1) - } - }) - - b.Run("distribution+get-handle", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.Distribution(NamespaceGeneral, "init_time", nil).Submit(1) - } - }) - - b.Run("distribution+handle-reused", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - handle := c.Distribution(NamespaceGeneral, "init_time", nil) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - handle.Submit(1) - } - }) -} - -func BenchmarkWorstCaseScenarioFloodMetrics(b *testing.B) { - nbSameMetric := 1000 - nbDifferentMetrics := 50 - nbGoroutines := 50 - - clientConfig := ClientConfig{ - HeartbeatInterval: time.Hour, - ExtendedHeartbeatInterval: time.Hour, - FlushInterval: internal.Range[time.Duration]{Min: time.Second, Max: time.Second}, - DistributionsSize: internal.Range[int]{Min: 256, Max: -1}, - AgentURL: "http://localhost:8126", - - // Empty transport to avoid sending data to the agent - HTTPClient: &http.Client{ - Timeout: 5 * time.Second, - Transport: noopTransport{}, - }, - } - - b.Run("get-handle", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - - for x := 0; x < b.N; x++ { - var wg sync.WaitGroup - - for i := 0; i < nbGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < nbDifferentMetrics; j++ { - metricName := "init_time_" + strconv.Itoa(j) - for k := 0; k < nbSameMetric; k++ { - c.Count(NamespaceGeneral, metricName, []string{"test:1"}).Submit(1) - } - } - }() - } - - wg.Wait() - } - - b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentMetrics*nbSameMetric*b.N)), "ns/point") - }) - - b.Run("handle-reused", func(b *testing.B) { - b.ReportAllocs() - c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) - require.NoError(b, err) - - defer c.Close() - - b.ResetTimer() - - for x := 0; x < b.N; x++ { - var wg sync.WaitGroup - - handles := make([]MetricHandle, nbDifferentMetrics) - for i := 0; i < nbDifferentMetrics; i++ { - handles[i] = c.Count(NamespaceGeneral, "init_time_"+strconv.Itoa(i), []string{"test:1"}) - } - for i := 0; i < nbGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < nbDifferentMetrics; j++ { - for k := 0; k < nbSameMetric; k++ { - handles[j].Submit(1) - } - } - }() - } - - wg.Wait() - } - - b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentMetrics*nbSameMetric*b.N)), "ns/point") - }) - -} diff --git a/internal/newtelemetry/telemetrytest/mock.go b/internal/newtelemetry/telemetrytest/mock.go deleted file mode 100644 index fdf9855084..0000000000 --- a/internal/newtelemetry/telemetrytest/mock.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 telemetrytest provides a mock implementation of the telemetry client for testing purposes -package telemetrytest - -import ( - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry" - - "github.com/stretchr/testify/mock" -) - -// MockClient implements Client and is used for testing purposes outside the telemetry package, -// e.g. the tracer and profiler. -type MockClient struct { - mock.Mock -} - -func (m *MockClient) Close() error { - return nil -} - -type MockMetricHandle struct { - mock.Mock -} - -func (m *MockMetricHandle) Submit(value float64) { - m.Called(value) -} - -func (m *MockMetricHandle) Get() float64 { - return m.Called().Get(0).(float64) -} - -func (m *MockClient) Count(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { - return m.Called(namespace, name, tags).Get(0).(newtelemetry.MetricHandle) -} - -func (m *MockClient) Rate(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { - return m.Called(namespace, name, tags).Get(0).(newtelemetry.MetricHandle) -} - -func (m *MockClient) Gauge(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { - return m.Called(namespace, name, tags).Get(0).(newtelemetry.MetricHandle) -} - -func (m *MockClient) Distribution(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { - return m.Called(namespace, name, tags).Get(0).(newtelemetry.MetricHandle) -} - -func (m *MockClient) Log(level newtelemetry.LogLevel, text string, options ...newtelemetry.LogOption) { - m.Called(level, text, options) -} - -func (m *MockClient) ProductStarted(product newtelemetry.Namespace) { - m.Called(product) -} - -func (m *MockClient) ProductStopped(product newtelemetry.Namespace) { - m.Called(product) -} - -func (m *MockClient) ProductStartError(product newtelemetry.Namespace, err error) { - m.Called(product, err) -} - -func (m *MockClient) RegisterAppConfig(key string, value any, origin newtelemetry.Origin) { - m.Called(key, value, origin) -} - -func (m *MockClient) RegisterAppConfigs(kvs ...newtelemetry.Configuration) { - m.Called(kvs) -} - -func (m *MockClient) MarkIntegrationAsLoaded(integration newtelemetry.Integration) { - m.Called(integration) -} - -func (m *MockClient) Flush() { - m.Called() -} - -func (m *MockClient) AppStart() { - m.Called() -} - -func (m *MockClient) AppStop() { - m.Called() -} - -var _ newtelemetry.Client = (*MockClient)(nil) diff --git a/internal/newtelemetry/api.go b/internal/telemetry/api.go similarity index 98% rename from internal/newtelemetry/api.go rename to internal/telemetry/api.go index 41cba3e6c7..275d3f23ec 100644 --- a/internal/newtelemetry/api.go +++ b/internal/telemetry/api.go @@ -3,12 +3,12 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2024 Datadog, Inc. -package newtelemetry +package telemetry import ( "io" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // Namespace describes a product to distinguish telemetry coming from diff --git a/internal/telemetry/client.go b/internal/telemetry/client.go index 4e55b77a82..6779e4edc9 100644 --- a/internal/telemetry/client.go +++ b/internal/telemetry/client.go @@ -1,618 +1,344 @@ // 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 2022 Datadog, Inc. +// Copyright 2024 Datadog, Inc. -// Package telemetry implements a client for sending telemetry information to -// Datadog regarding usage of an APM library such as tracing or profiling. package telemetry import ( - "bytes" - "encoding/json" - "fmt" - "net" - "net/http" + "errors" "os" - "runtime" - "runtime/debug" - "strings" + "strconv" "sync" - "time" - "gopkg.in/DataDog/dd-trace-go.v1/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" - logger "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo" - "gopkg.in/DataDog/dd-trace-go.v1/internal/version" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/mapper" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) -// Client buffers and sends telemetry messages to Datadog (possibly through an -// agent). -type Client interface { - RegisterAppConfig(name string, val interface{}, origin Origin) - ProductChange(namespace Namespace, enabled bool, configuration []Configuration) - ConfigChange(configuration []Configuration) - Record(namespace Namespace, metric MetricKind, name string, value float64, tags []string, common bool) - Count(namespace Namespace, name string, value float64, tags []string, common bool) - ApplyOps(opts ...Option) - Stop() +// NewClient creates a new telemetry client with the given service, environment, and version and config. +func NewClient(service, env, version string, config ClientConfig) (Client, error) { + if service == "" { + return nil, errors.New("service name must not be empty") + } + + config = defaultConfig(config) + if err := config.validateConfig(); err != nil { + return nil, err + } + + return newClient(internal.TracerConfig{Service: service, Env: env, Version: version}, config) } -var ( - // GlobalClient acts as a global telemetry client that the - // tracer, profiler, and appsec products will use - GlobalClient Client - globalClient sync.Mutex - - // integrations tracks the integrations enabled - contribPackages []Integration - contrib sync.Mutex - - // copied from dd-trace-go/profiler - defaultHTTPClient = &http.Client{ - // We copy the transport to avoid using the default one, as it might be - // augmented with tracing and we don't want these calls to be recorded. - // See https://golang.org/pkg/net/http/#DefaultTransport . - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - Timeout: 5 * time.Second, +func newClient(tracerConfig internal.TracerConfig, config ClientConfig) (*client, error) { + writerConfig, err := newWriterConfig(config, tracerConfig) + if err != nil { + return nil, err } - // protects agentlessURL, which may be changed for testing purposes - agentlessEndpointLock sync.RWMutex - // agentlessURL is the endpoint used to send telemetry in an agentless environment. It is - // also the default URL in case connecting to the agent URL fails. - agentlessURL = "https://instrumentation-telemetry-intake.datadoghq.com/api/v2/apmtelemetry" + writer, err := internal.NewWriter(writerConfig) + if err != nil { + return nil, err + } - defaultHeartbeatInterval = 60.0 // seconds + client := &client{ + tracerConfig: tracerConfig, + writer: writer, + clientConfig: config, + flushMapper: mapper.NewDefaultMapper(config.HeartbeatInterval, config.ExtendedHeartbeatInterval), + payloadQueue: internal.NewRingQueue[transport.Payload](config.PayloadQueueSize), - // LogPrefix specifies the prefix for all telemetry logging - LogPrefix = "Instrumentation telemetry: " + dependencies: dependencies{ + DependencyLoader: config.DependencyLoader, + }, + metrics: metrics{ + skipAllowlist: config.Debug, + pool: internal.NewSyncPool(func() *metricPoint { return &metricPoint{} }), + }, + distributions: distributions{ + skipAllowlist: config.Debug, + queueSize: config.DistributionsSize, + pool: internal.NewSyncPool(func() []float64 { return make([]float64, config.DistributionsSize.Min) }), + }, + } - hostname string -) + client.dataSources = append(client.dataSources, + &client.integrations, + &client.products, + &client.configuration, + &client.dependencies, + ) -func init() { - var err error - hostname, err = os.Hostname() - if err != nil { - hostname = "unknown" + if config.LogsEnabled { + client.dataSources = append(client.dataSources, &client.logger) } - GlobalClient = new(client) -} -// client implements Client interface. Client.Start should be called before any other methods. -// -// Client is safe to use from multiple goroutines concurrently. The client will -// send all telemetry requests in the background, in order to avoid blocking the -// caller since telemetry should not disrupt an application. Metrics are -// aggregated by the Client. -type client struct { - // URL for the Datadog agent or Datadog telemetry endpoint - URL string - // APIKey should be supplied if the endpoint is not a Datadog agent, - // i.e. you are sending telemetry directly to Datadog - APIKey string - // The interval for sending a heartbeat signal to the backend. - // Configurable with DD_TELEMETRY_HEARTBEAT_INTERVAL. Default 60s. - heartbeatInterval time.Duration - - // e.g. "tracers", "profilers", "appsec" - Namespace Namespace - - // App-specific information - Service string - Env string - Version string - - // Client will be used for telemetry uploads. This http.Client, if - // provided, should be the same as would be used for any other - // interaction with the Datadog agent, e.g. if the agent is accessed - // over UDS, or if the user provides their own http.Client to the - // profiler/tracer to access the agent over a proxy. - // - // If Client is nil, an http.Client with the same Transport settings as - // http.DefaultTransport and a 5 second timeout will be used. - Client *http.Client - - // mu guards all of the following fields - mu sync.Mutex - - // debug enables the debug flag for all requests, see - // https://dtdg.co/3bv2MMv. - // DD_INSTRUMENTATION_TELEMETRY_DEBUG configures this field. - debug bool - // started is true in between when Start() returns and the next call to - // Stop() - started bool - // seqID is a sequence number used to order telemetry messages by - // the back end. - seqID int64 - // heartbeatT is used to schedule heartbeat messages - heartbeatT *time.Timer - // requests hold all messages which don't need to be immediately sent - requests []*Request - // metrics holds un-sent metrics that will be aggregated the next time - // metrics are sent - metrics map[Namespace]map[string]*metric - newMetrics bool - - // syncFlushOnStop forces a sync flush to ensure all metrics are sent before stopping the client - syncFlushOnStop bool - - // Globally registered application configuration sent in the app-started request, along with the locally-defined - // configuration of the event. - globalAppConfig []Configuration + if config.MetricsEnabled { + client.dataSources = append(client.dataSources, &client.metrics, &client.distributions) + } + + client.flushTicker = internal.NewTicker(client.Flush, config.FlushInterval) + + return client, nil } -func log(msg string, args ...interface{}) { - // Debug level so users aren't spammed with telemetry info. - logger.Debug(LogPrefix+msg, args...) +// dataSources is where the data that will be flushed is coming from. I.e metrics, logs, configurations, etc. +type dataSource interface { + Payload() transport.Payload } -// RegisterAppConfig allows to register a globally-defined application configuration. -// This configuration will be sent when the telemetry client is started and over related configuration updates. -func (c *client) RegisterAppConfig(name string, value interface{}, origin Origin) { - c.globalAppConfig = append(c.globalAppConfig, Configuration{ - Name: name, - Value: value, - Origin: origin, - }) +type client struct { + tracerConfig internal.TracerConfig + clientConfig ClientConfig + + // Data sources + dataSources []dataSource + integrations integrations + products products + configuration configuration + dependencies dependencies + logger logger + metrics metrics + distributions distributions + + // flushMapper is the transformer to use for the next flush on the gathered bodies on this tick + flushMapper mapper.Mapper + flushMapperMu sync.Mutex + + // flushTicker is the ticker that triggers a call to client.Flush every flush interval + flushTicker *internal.Ticker + + // writer is the writer to use to send the payloads to the backend or the agent + writer internal.Writer + + // payloadQueue is used when we cannot flush previously built payload for multiple reasons. + payloadQueue *internal.RingQueue[transport.Payload] } -// start registers that the app has begun running with the app-started event. -// Must be called with c.mu locked. -// start also configures the telemetry client based on the following telemetry -// environment variables: DD_INSTRUMENTATION_TELEMETRY_ENABLED, -// DD_TELEMETRY_HEARTBEAT_INTERVAL, DD_INSTRUMENTATION_TELEMETRY_DEBUG, -// and DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED. -// TODO: implement passing in error information about tracer start -func (c *client) start(configuration []Configuration, namespace Namespace, flush bool) { - if Disabled() { - return - } - if c.started { - log("attempted to start telemetry client when client has already started - ignoring attempt") +func (c *client) Log(level LogLevel, text string, options ...LogOption) { + if !c.clientConfig.LogsEnabled { return } - // Don't start the telemetry client if there is some error configuring the client with fallback - // options, e.g. an API key was not found but agentless telemetry is expected. - if err := c.fallbackOps(); err != nil { - log(err.Error()) - return - } - - c.started = true - c.metrics = make(map[Namespace]map[string]*metric) - c.debug = internal.BoolEnv("DD_INSTRUMENTATION_TELEMETRY_DEBUG", false) - productInfo := Products{ - AppSec: ProductDetails{ - Version: version.Tag, - // if appsec is the one starting the telemetry client, - // then AppSec is enabled - Enabled: namespace == NamespaceAppSec, - }, - Profiler: ProductDetails{ - Version: version.Tag, - // if the profiler is the one starting the telemetry client, - // then profiling is enabled. - Enabled: namespace == NamespaceProfilers, - }, - } - - var cfg []Configuration - cfg = append(cfg, c.globalAppConfig...) - cfg = append(cfg, configuration...) - - // State whether the app has its Go dependencies available or not - deps, ok := debug.ReadBuildInfo() - if !ok { - deps = nil // because not guaranteed to be nil by the public doc when !ok - } - cfg = append(cfg, BoolConfig("dependencies_available", ok)) - collectDependenciesEnabled := collectDependencies() - cfg = append(cfg, BoolConfig("DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED", collectDependenciesEnabled)) // TODO: report all the possible telemetry config option automatically - if !collectDependenciesEnabled { - deps = nil // to simplify the condition below to `deps != nil` - } + c.logger.Add(level, text, options...) +} - payload := &AppStarted{ - Configuration: cfg, - Products: productInfo, - } - appStarted := c.newRequest(RequestTypeAppStarted) - appStarted.Body.Payload = payload - c.scheduleSubmit(appStarted) - - if deps != nil { - var depPayload Dependencies - for _, dep := range deps.Deps { - depPayload.Dependencies = append(depPayload.Dependencies, - Dependency{ - Name: dep.Path, - Version: strings.TrimPrefix(dep.Version, "v"), - }, - ) - } - // Send the telemetry request if and only if the dependencies are actually present in the binary. - // For instance, bazel doesn't include them out of the box (cf. https://github.com/bazelbuild/rules_go/issues/3090), - // which would result in an empty list of dependencies. - dep := c.newRequest(RequestTypeDependenciesLoaded) - dep.Body.Payload = depPayload - c.scheduleSubmit(dep) - } +func (c *client) MarkIntegrationAsLoaded(integration Integration) { + c.integrations.Add(integration) +} - if len(contribPackages) > 0 { - req := c.newRequest(RequestTypeAppIntegrationsChange) - req.Body.Payload = IntegrationsChange{Integrations: contribPackages} - c.scheduleSubmit(req) +func (c *client) Count(namespace Namespace, name string, tags []string) MetricHandle { + if !c.clientConfig.MetricsEnabled { + return noopMetricHandle{} } + return c.metrics.LoadOrStore(namespace, transport.CountMetric, name, tags) +} - if flush { - c.flush(false) +func (c *client) Rate(namespace Namespace, name string, tags []string) MetricHandle { + if !c.clientConfig.MetricsEnabled { + return noopMetricHandle{} } - c.heartbeatInterval = heartbeatInterval() - c.heartbeatT = time.AfterFunc(c.heartbeatInterval, c.backgroundHeartbeat) + return c.metrics.LoadOrStore(namespace, transport.RateMetric, name, tags) } -func heartbeatInterval() time.Duration { - heartbeat := internal.FloatEnv("DD_TELEMETRY_HEARTBEAT_INTERVAL", defaultHeartbeatInterval) - if heartbeat <= 0 || heartbeat > 3600 { - log("DD_TELEMETRY_HEARTBEAT_INTERVAL=%d not in [1,3600] range, setting to default of %f", heartbeat, defaultHeartbeatInterval) - heartbeat = defaultHeartbeatInterval +func (c *client) Gauge(namespace Namespace, name string, tags []string) MetricHandle { + if !c.clientConfig.MetricsEnabled { + return noopMetricHandle{} } - return time.Duration(heartbeat * float64(time.Second)) + return c.metrics.LoadOrStore(namespace, transport.GaugeMetric, name, tags) } -// Stop notifies the telemetry endpoint that the app is closing. All outstanding -// messages will also be sent. No further messages will be sent until the client -// is started again -func (c *client) Stop() { - c.mu.Lock() - defer c.mu.Unlock() - if !c.started { - return +func (c *client) Distribution(namespace Namespace, name string, tags []string) MetricHandle { + if !c.clientConfig.MetricsEnabled { + return noopMetricHandle{} } - c.started = false - c.heartbeatT.Stop() - // close request types have no body - r := c.newRequest(RequestTypeAppClosing) - c.scheduleSubmit(r) - c.flush(c.syncFlushOnStop) + return c.distributions.LoadOrStore(namespace, name, tags) } -// Disabled returns whether instrumentation telemetry is disabled -// according to the DD_INSTRUMENTATION_TELEMETRY_ENABLED env var -func Disabled() bool { - return !internal.BoolEnv("DD_INSTRUMENTATION_TELEMETRY_ENABLED", true) +func (c *client) ProductStarted(product Namespace) { + c.products.Add(product, true, nil) } -// collectDependencies returns whether dependencies telemetry information is sent -func collectDependencies() bool { - return internal.BoolEnv("DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED", true) +func (c *client) ProductStopped(product Namespace) { + c.products.Add(product, false, nil) } -// MetricKind specifies the type of metric being reported. -// Metric types mirror Datadog metric types - for a more detailed -// description of metric types, see: -// https://docs.datadoghq.com/metrics/types/?tab=count#metric-types -type MetricKind string - -var ( - // MetricKindGauge represents a gauge type metric - MetricKindGauge MetricKind = "gauge" - // MetricKindCount represents a count type metric - MetricKindCount MetricKind = "count" - // MetricKindDist represents a distribution type metric - MetricKindDist MetricKind = "distribution" -) - -type metric struct { - name string - kind MetricKind - value float64 - // Unix timestamp - ts float64 - tags []string - common bool +func (c *client) ProductStartError(product Namespace, err error) { + c.products.Add(product, false, err) } -// TODO: Can there be identically named/tagged metrics with a "common" and "not -// common" variant? +func (c *client) RegisterAppConfig(key string, value any, origin Origin) { + c.configuration.Add(Configuration{key, value, origin}) +} -func newMetric(name string, kind MetricKind, tags []string, common bool) *metric { - return &metric{ - name: name, - kind: kind, - tags: append([]string{}, tags...), - common: common, +func (c *client) RegisterAppConfigs(kvs ...Configuration) { + for _, value := range kvs { + c.configuration.Add(value) } } -func metricKey(name string, tags []string, kind MetricKind) string { - return name + string(kind) + strings.Join(tags, "-") +func (c *client) Config() ClientConfig { + return c.clientConfig } -// Record sets the value for a gauge or distribution metric type -// with the given name and tags. If the metric is not language-specific, common should be set to true -func (c *client) Record(namespace Namespace, kind MetricKind, name string, value float64, tags []string, common bool) { - c.mu.Lock() - defer c.mu.Unlock() - if !c.started { - return +// Flush sends all the data sources before calling flush +// This function is called by the flushTicker so it should not panic, or it will crash the whole customer application. +// If a panic occurs, we stop the telemetry and log the error. +func (c *client) Flush() { + defer func() { + r := recover() + if r == nil { + return + } + log.Warn("panic while flushing telemetry data, stopping telemetry: %v", r) + telemetryClientDisabled = true + if gc, ok := GlobalClient().(*client); ok && gc == c { + SwapClient(nil) + } + }() + + payloads := make([]transport.Payload, 0, 8) + for _, ds := range c.dataSources { + if payload := ds.Payload(); payload != nil { + payloads = append(payloads, payload) + } } - if _, ok := c.metrics[namespace]; !ok { - c.metrics[namespace] = map[string]*metric{} + + nbBytes, err := c.flush(payloads) + if err != nil { + log.Warn("telemetry: error while flushing: %v", err) } - key := metricKey(name, tags, kind) - m, ok := c.metrics[namespace][key] - if !ok { - m = newMetric(name, kind, tags, common) - c.metrics[namespace][key] = m + + if c.clientConfig.Debug { + log.Debug("telemetry: flushed %d bytes of data", nbBytes) } - m.value = value - m.ts = float64(time.Now().Unix()) - c.newMetrics = true } -// Count adds the value to a count with the given name and tags. If the metric -// is not language-specific, common should be set to true -func (c *client) Count(namespace Namespace, name string, value float64, tags []string, common bool) { - c.mu.Lock() - defer c.mu.Unlock() - if !c.started { - return - } - if _, ok := c.metrics[namespace]; !ok { - c.metrics[namespace] = map[string]*metric{} - } - key := metricKey(name, tags, MetricKindCount) - m, ok := c.metrics[namespace][key] - if !ok { - m = newMetric(name, MetricKindCount, tags, common) - c.metrics[namespace][key] = m - } - m.value += value - m.ts = float64(time.Now().Unix()) - c.newMetrics = true +func (c *client) transform(payloads []transport.Payload) []transport.Payload { + c.flushMapperMu.Lock() + defer c.flushMapperMu.Unlock() + payloads, c.flushMapper = c.flushMapper.Transform(payloads) + return payloads } -// flush sends any outstanding telemetry messages and aggregated metrics to be -// sent to the backend. Requests are sent in the background. Must be called -// with c.mu locked -func (c *client) flush(sync bool) { - // initialize submissions slice of capacity len(c.requests) + 2 - // to hold all the new events, plus two potential metric events - submissions := make([]*Request, 0, len(c.requests)+2) - - // copy over requests so we can do the actual submission without holding - // the lock. Zero out the old stuff so we don't leak references - for i, r := range c.requests { - submissions = append(submissions, r) - c.requests[i] = nil - } - c.requests = c.requests[:0] - - if c.newMetrics { - c.newMetrics = false - for namespace := range c.metrics { - // metrics can either be request type generate-metrics or distributions - dPayload := &DistributionMetrics{ - Namespace: namespace, - } - gPayload := &Metrics{ - Namespace: namespace, - } - for _, m := range c.metrics[namespace] { - if m.kind == MetricKindDist { - dPayload.Series = append(dPayload.Series, DistributionSeries{ - Metric: m.name, - Tags: m.tags, - Common: m.common, - Points: []float64{m.value}, - }) - } else { - gPayload.Series = append(gPayload.Series, Series{ - Metric: m.name, - Type: string(m.kind), - Tags: m.tags, - Common: m.common, - Points: [][2]float64{{m.ts, m.value}}, - }) - } - } - if len(dPayload.Series) > 0 { - distributions := c.newRequest(RequestTypeDistributions) - distributions.Body.Payload = dPayload - submissions = append(submissions, distributions) - } - if len(gPayload.Series) > 0 { - generateMetrics := c.newRequest(RequestTypeGenerateMetrics) - generateMetrics.Body.Payload = gPayload - submissions = append(submissions, generateMetrics) +// flush sends all the data sources to the writer after having sent them through the [transform] function. +// It returns the amount of bytes sent to the writer. +func (c *client) flush(payloads []transport.Payload) (int, error) { + payloads = c.transform(payloads) + + if c.payloadQueue.IsEmpty() && len(payloads) == 0 { + return 0, nil + } + + emptyQueue := c.payloadQueue.IsEmpty() + // We enqueue the new payloads to preserve the order of the payloads + c.payloadQueue.Enqueue(payloads...) + payloads = c.payloadQueue.Flush() + + var ( + nbBytes int + speedIncreased bool + failedCalls []internal.EndpointRequestResult + ) + + for i, payload := range payloads { + results, err := c.writer.Flush(payload) + c.computeFlushMetrics(results, err) + if err != nil { + // We stop flushing when we encounter a fatal error, put the bodies in the queue and return the error + if results[len(results)-1].StatusCode == 413 { // If the payload is too large we have no way to divide it, we can only skip it... + log.Warn("telemetry: tried sending a payload that was too large, dropping it") + continue } + c.payloadQueue.Enqueue(payloads[i:]...) + return nbBytes, err } - } - submit := func() { - for _, r := range submissions { - err := r.submit() - if err != nil { - log("submission error: %s", err.Error()) - } - } - } + failedCalls = append(failedCalls, results[:len(results)-1]...) + successfulCall := results[len(results)-1] - if sync { - submit() - } else { - go submit() - } -} + if !speedIncreased && successfulCall.PayloadByteSize > c.clientConfig.EarlyFlushPayloadSize { + // We increase the speed of the flushTicker to try to flush the remaining bodies faster as we are at risk of sending too large bodies to the backend + c.flushTicker.CanIncreaseSpeed() + speedIncreased = true + } -// newRequests populates a request with the common fields shared by all requests -// sent through this Client -func (c *client) newRequest(t RequestType) *Request { - c.seqID++ - body := &Body{ - APIVersion: "v2", - RequestType: t, - TracerTime: time.Now().Unix(), - RuntimeID: globalconfig.RuntimeID(), - SeqID: c.seqID, - Debug: c.debug, - Application: Application{ - ServiceName: c.Service, - Env: c.Env, - ServiceVersion: c.Version, - TracerVersion: version.Tag, - LanguageName: "go", - LanguageVersion: runtime.Version(), - }, - Host: Host{ - Hostname: hostname, - OS: osinfo.OSName(), - OSVersion: osinfo.OSVersion(), - Architecture: osinfo.Architecture(), - KernelName: osinfo.KernelName(), - KernelRelease: osinfo.KernelRelease(), - KernelVersion: osinfo.KernelVersion(), - }, + nbBytes += successfulCall.PayloadByteSize } - header := &http.Header{ - "Content-Type": {"application/json"}, - "DD-Telemetry-API-Version": {"v2"}, - "DD-Telemetry-Request-Type": {string(t)}, - "DD-Client-Library-Language": {"go"}, - "DD-Client-Library-Version": {version.Tag}, - "DD-Agent-Env": {c.Env}, - "DD-Agent-Hostname": {hostname}, - } - if cid := internal.ContainerID(); cid != "" { - header.Set("Datadog-Container-ID", cid) - } - if eid := internal.EntityID(); eid != "" { - header.Set("Datadog-Entity-ID", eid) - } - if c.URL == getAgentlessURL() { - header.Set("DD-API-KEY", c.APIKey) - } - client := c.Client - if client == nil { - client = defaultHTTPClient - } - return &Request{Body: body, - Header: header, - HTTPClient: client, - URL: c.URL, + if emptyQueue && !speedIncreased { // If we did not send a very big payload, and we have no payloads + c.flushTicker.CanDecreaseSpeed() } -} -// submit sends a telemetry request -func (r *Request) submit() error { - retry, err := r.trySubmit() - if retry { - // retry telemetry submissions in instances where the telemetry client has trouble - // connecting with the agent - log("telemetry submission failed, retrying with agentless: %s", err) - r.URL = getAgentlessURL() - r.Header.Set("DD-API-KEY", defaultAPIKey()) - if _, err := r.trySubmit(); err == nil { - return nil + if len(failedCalls) > 0 { + var errs []error + for _, call := range failedCalls { + errs = append(errs, call.Error) } - log("retrying with agentless telemetry failed: %s", err) + log.Debug("non-fatal error(s) while flushing telemetry data: %v", errors.Join(errs...)) } - return err -} -// agentlessRetry determines if we should retry a failed a request with -// by submitting to the agentless endpoint -func agentlessRetry(req *Request, resp *http.Response, err error) bool { - if req.URL == getAgentlessURL() { - // no need to retry with agentless endpoint if it already failed - return false - } - if err != nil { - // we didn't get a response which might signal a connectivity problem with - // agent - retry with agentless - return true - } - // TODO: add more status codes we do not want to retry on - doNotRetry := []int{http.StatusBadRequest, http.StatusTooManyRequests, http.StatusUnauthorized, http.StatusForbidden} - for status := range doNotRetry { - if resp.StatusCode == status { - return false - } - } - return true + return nbBytes, nil } -// trySubmit submits a telemetry request to the specified URL -// in the Request struct. If submission fails, return whether or not -// this submission should be re-tried with the agentless endpoint -// as well as the error that occurred -func (r *Request) trySubmit() (retry bool, err error) { - b, err := json.Marshal(r.Body) - if err != nil { - return false, err +// computeFlushMetrics computes and submits the metrics for the flush operation using the output from the writer.Flush method. +// It will submit the number of requests, responses, errors, the number of bytes sent and the duration of the call that was successful. +func (c *client) computeFlushMetrics(results []internal.EndpointRequestResult, reason error) { + if !c.clientConfig.internalMetricsEnabled { + return } - req, err := http.NewRequest(http.MethodPost, r.URL, bytes.NewReader(b)) - if err != nil { - return false, err + indexToEndpoint := func(i int) string { + if i == 0 && c.clientConfig.AgentURL != "" { + return "agent" + } + return "agentless" } - req.Header = *r.Header - req.ContentLength = int64(len(b)) + for i, result := range results { + endpoint := "endpoint:" + indexToEndpoint(i) + c.Count(transport.NamespaceTelemetry, "telemetry_api.requests", []string{endpoint}).Submit(1) + if result.StatusCode != 0 { + c.Count(transport.NamespaceTelemetry, "telemetry_api.responses", []string{endpoint, "status_code:" + strconv.Itoa(result.StatusCode)}).Submit(1) + } - client := r.HTTPClient - if client == nil { - client = defaultHTTPClient - } - resp, err := client.Do(req) - if err != nil { - return agentlessRetry(r, resp, err), err + if result.Error != nil { + typ := "type:network" + if os.IsTimeout(result.Error) { + typ = "type:timeout" + } + var writerStatusCodeError *internal.WriterStatusCodeError + if errors.As(result.Error, &writerStatusCodeError) { + typ = "type:status_code" + } + c.Count(transport.NamespaceTelemetry, "telemetry_api.errors", []string{endpoint, typ}).Submit(1) + } } - defer resp.Body.Close() - if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK { - return agentlessRetry(r, resp, err), errBadStatus(resp.StatusCode) + + if reason != nil { + return } - return false, nil -} -type errBadStatus int + successfulCall := results[len(results)-1] + endpoint := "endpoint:" + indexToEndpoint(len(results)-1) + c.Distribution(transport.NamespaceTelemetry, "telemetry_api.bytes", []string{endpoint}).Submit(float64(successfulCall.PayloadByteSize)) + c.Distribution(transport.NamespaceTelemetry, "telemetry_api.ms", []string{endpoint}).Submit(float64(successfulCall.CallDuration.Milliseconds())) +} -func (e errBadStatus) Error() string { return fmt.Sprintf("bad HTTP response status %d", e) } +func (c *client) AppStart() { + c.flushMapperMu.Lock() + defer c.flushMapperMu.Unlock() + c.flushMapper = mapper.NewAppStartedMapper(c.flushMapper) +} -// scheduleSubmit queues a request to be sent to the backend. Should be called -// with c.mu locked -func (c *client) scheduleSubmit(r *Request) { - c.requests = append(c.requests, r) +func (c *client) AppStop() { + c.flushMapperMu.Lock() + defer c.flushMapperMu.Unlock() + c.flushMapper = mapper.NewAppClosingMapper(c.flushMapper) } -// backgroundHeartbeat is invoked at every heartbeat interval, -// sending the app-heartbeat event and flushing any outstanding -// telemetry messages -func (c *client) backgroundHeartbeat() { - c.mu.Lock() - defer c.mu.Unlock() - if !c.started { - return - } - c.scheduleSubmit(c.newRequest(RequestTypeAppHeartbeat)) - c.flush(false) - c.heartbeatT.Reset(c.heartbeatInterval) +func (c *client) Close() error { + c.flushTicker.Stop() + return nil } diff --git a/internal/newtelemetry/client_config.go b/internal/telemetry/client_config.go similarity index 99% rename from internal/newtelemetry/client_config.go rename to internal/telemetry/client_config.go index d0688e4434..b6ff546c15 100644 --- a/internal/newtelemetry/client_config.go +++ b/internal/telemetry/client_config.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2024 Datadog, Inc. -package newtelemetry +package telemetry import ( "fmt" @@ -15,7 +15,7 @@ import ( globalinternal "gopkg.in/DataDog/dd-trace-go.v1/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" ) type ClientConfig struct { diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 45cd4172a3..2baaa42252 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -1,484 +1,1592 @@ // 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 2022 Datadog, Inc. +// Copyright 2025 Datadog, Inc. package telemetry import ( - "context" + "bytes" "encoding/json" + "errors" + "fmt" + "io" "net/http" - "net/http/httptest" - "reflect" "runtime" - "sort" + "runtime/debug" + "slices" + "strconv" + "strings" "sync" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "gopkg.in/DataDog/dd-trace-go.v1/internal" - logging "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) -func TestClient(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - heartbeat := make(chan struct{}) +func TestNewClient(t *testing.T) { + for _, test := range []struct { + name string + tracerConfig internal.TracerConfig + clientConfig ClientConfig + newErr string + }{ + { + name: "nominal", + tracerConfig: internal.TracerConfig{ + Service: "test-service", + Env: "test-env", + Version: "1.0.0", + }, + clientConfig: ClientConfig{ + AgentURL: "http://localhost:8126", + }, + }, + { + name: "empty service", + tracerConfig: internal.TracerConfig{}, + newErr: "service name must not be empty", + }, + { + name: "empty agent url", + tracerConfig: internal.TracerConfig{ + Service: "test-service", + }, + clientConfig: ClientConfig{}, + newErr: "could not build any endpoint", + }, + { + name: "invalid agent url", + tracerConfig: internal.TracerConfig{ + Service: "test-service", + }, + clientConfig: ClientConfig{ + AgentURL: "toto_protocol://localhost:8126", + }, + newErr: "invalid agent URL", + }, + { + name: "Too big payload size", + tracerConfig: internal.TracerConfig{ + Service: "test-service", + }, + clientConfig: ClientConfig{ + AgentURL: "http://localhost:8126", + EarlyFlushPayloadSize: 64 * 1024 * 1024, // 64MB + }, + newErr: "EarlyFlushPayloadSize must be between 0 and 5MB", + }, + } { + t.Run(test.name, func(t *testing.T) { + c, err := NewClient(test.tracerConfig.Service, test.tracerConfig.Env, test.tracerConfig.Version, test.clientConfig) + if err == nil { + defer c.Close() + } - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - h := r.Header.Get("DD-Telemetry-Request-Type") - if len(h) == 0 { - t.Fatal("didn't get telemetry request type header") - } - if RequestType(h) == RequestTypeAppHeartbeat { - select { - case heartbeat <- struct{}{}: - default: + if test.newErr == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.newErr) } - } - })) - defer server.Close() + }) + } +} - client := &client{ - URL: server.URL, +func TestClientFlush(t *testing.T) { + tracerConfig := internal.TracerConfig{ + Service: "test-service", + Env: "test-env", + Version: "1.0.0", } - client.mu.Lock() - client.start(nil, NamespaceTracers, true) - client.start(nil, NamespaceTracers, true) // test idempotence - client.mu.Unlock() - defer client.Stop() - - timeout := time.After(30 * time.Second) - select { - case <-timeout: - t.Fatal("Heartbeat took more than 30 seconds. Should have been ~1 second") - case <-heartbeat: + + type testParams struct { + name string + clientConfig ClientConfig + when func(c *client) + expect func(*testing.T, []transport.Payload) } -} + testcases := []testParams{ + { + name: "heartbeat", + clientConfig: ClientConfig{ + HeartbeatInterval: time.Nanosecond, + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppHeartbeat{}, payload) + assert.Equal(t, payload.RequestType(), transport.RequestTypeAppHeartbeat) + }, + }, + { + name: "extended-heartbeat-config", + clientConfig: ClientConfig{ + HeartbeatInterval: time.Nanosecond, + ExtendedHeartbeatInterval: time.Nanosecond, + }, + when: func(c *client) { + c.RegisterAppConfig("key", "value", OriginDefault) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.MessageBatch{}, payload) + batch := payload.(transport.MessageBatch) + require.Len(t, batch, 2) + assert.Equal(t, transport.RequestTypeAppClientConfigurationChange, batch[0].RequestType) + assert.Equal(t, transport.RequestTypeAppExtendedHeartBeat, batch[1].RequestType) -func TestMetrics(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - var ( - mu sync.Mutex - got []Series - ) - closed := make(chan struct{}, 1) - - // we will try to set three metrics that the server must receive - expectedMetrics := 3 - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rType := RequestType(r.Header.Get("DD-Telemetry-Request-Type")) - if rType != RequestTypeGenerateMetrics { - return - } - req := Body{ - Payload: new(Metrics), - } - dec := json.NewDecoder(r.Body) - err := dec.Decode(&req) - if err != nil { - t.Fatal(err) - } - v, ok := req.Payload.(*Metrics) - if !ok { - t.Fatal("payload set metrics but didn't get metrics") - } - for _, s := range v.Series { - for i, p := range s.Points { - // zero out timestamps - s.Points[i] = [2]float64{0, p[1]} - } - } - mu.Lock() - defer mu.Unlock() - got = append(got, v.Series...) - if len(got) == expectedMetrics { - select { - case closed <- struct{}{}: - default: - } - return - } - })) - defer server.Close() + assert.Len(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Configuration, 0) + }, + }, + { + name: "extended-heartbeat-integrations", + clientConfig: ClientConfig{ + HeartbeatInterval: time.Nanosecond, + ExtendedHeartbeatInterval: time.Nanosecond, + }, + when: func(c *client) { + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.MessageBatch{}, payload) + batch := payload.(transport.MessageBatch) + require.Len(t, batch, 2) + assert.Equal(t, transport.RequestTypeAppIntegrationsChange, batch[0].RequestType) + assert.Equal(t, transport.RequestTypeAppExtendedHeartBeat, batch[1].RequestType) + assert.Len(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations, 1) + assert.Equal(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations[0].Name, "test-integration") + assert.Equal(t, batch[1].Payload.(transport.AppExtendedHeartbeat).Integrations[0].Version, "1.0.0") + }, + }, + { + name: "configuration-default", + when: func(c *client) { + c.RegisterAppConfig("key", "value", OriginDefault) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppClientConfigurationChange{}, payload) + config := payload.(transport.AppClientConfigurationChange) + assert.Len(t, config.Configuration, 1) + assert.Equal(t, config.Configuration[0].Name, "key") + assert.Equal(t, config.Configuration[0].Value, "value") + assert.Equal(t, config.Configuration[0].Origin, OriginDefault) + }, + }, + { + name: "configuration-default", + when: func(c *client) { + c.RegisterAppConfig("key", "value", OriginDefault) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppClientConfigurationChange{}, payload) + config := payload.(transport.AppClientConfigurationChange) + assert.Len(t, config.Configuration, 1) + assert.Equal(t, config.Configuration[0].Name, "key") + assert.Equal(t, config.Configuration[0].Value, "value") + assert.Equal(t, config.Configuration[0].Origin, OriginDefault) + }, + }, + { + name: "configuration-complex-values", + when: func(c *client) { + c.RegisterAppConfigs( + Configuration{Name: "key1", Value: []string{"value1", "value2"}, Origin: OriginDefault}, + Configuration{Name: "key2", Value: map[string]string{"key": "value", "key2": "value2"}, Origin: OriginCode}, + Configuration{Name: "key3", Value: []int{1, 2, 3}, Origin: OriginDDConfig}, + Configuration{Name: "key4", Value: struct { + A string + }{A: "1"}, Origin: OriginEnvVar}, + Configuration{Name: "key5", Value: map[int]struct{ X int }{1: {X: 1}}, Origin: OriginRemoteConfig}, + ) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppClientConfigurationChange{}, payload) + config := payload.(transport.AppClientConfigurationChange) - go func() { - client := &client{ - URL: server.URL, - } - client.start(nil, NamespaceTracers, true) - - // Records should have the most recent value - client.Record(NamespaceTracers, MetricKindGauge, "foobar", 1, nil, false) - client.Record(NamespaceTracers, MetricKindGauge, "foobar", 2, nil, false) - // Counts should be aggregated - client.Count(NamespaceTracers, "baz", 3, nil, true) - client.Count(NamespaceTracers, "baz", 1, nil, true) - // Tags should be passed through - client.Count(NamespaceTracers, "bonk", 4, []string{"org:1"}, false) - - client.mu.Lock() - client.flush(false) - client.mu.Unlock() - }() - - <-closed - - want := []Series{ - {Metric: "baz", Type: "count", Interval: 0, Points: [][2]float64{{0, 4}}, Tags: []string{}, Common: true}, - {Metric: "bonk", Type: "count", Interval: 0, Points: [][2]float64{{0, 4}}, Tags: []string{"org:1"}}, - {Metric: "foobar", Type: "gauge", Interval: 0, Points: [][2]float64{{0, 2}}, Tags: []string{}}, - } - sort.Slice(got, func(i, j int) bool { - return got[i].Metric < got[j].Metric - }) - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %+v, got %+v", want, got) - } -} + slices.SortStableFunc(config.Configuration, func(a, b transport.ConfKeyValue) int { + return strings.Compare(a.Name, b.Name) + }) -func TestDistributionMetrics(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - var ( - mu sync.Mutex - got []DistributionSeries - ) - closed := make(chan struct{}, 1) - - // we will try to set one metric that the server must receive - expectedMetrics := 1 - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rType := RequestType(r.Header.Get("DD-Telemetry-Request-Type")) - if rType != RequestTypeDistributions { - return - } - req := Body{ - Payload: new(DistributionMetrics), - } - dec := json.NewDecoder(r.Body) - err := dec.Decode(&req) - if err != nil { - t.Fatal(err) - } - v, ok := req.Payload.(*DistributionMetrics) - if !ok { - t.Fatal("payload set metrics but didn't get metrics") - } - mu.Lock() - defer mu.Unlock() - got = append(got, v.Series...) - if len(got) == expectedMetrics { - select { - case closed <- struct{}{}: - default: - } - return - } - })) - defer server.Close() + assert.Len(t, config.Configuration, 5) + assert.Equal(t, "key1", config.Configuration[0].Name) + assert.Equal(t, "value1,value2", config.Configuration[0].Value) + assert.Equal(t, OriginDefault, config.Configuration[0].Origin) + assert.Equal(t, "key2", config.Configuration[1].Name) + assert.Equal(t, "key:value,key2:value2", config.Configuration[1].Value) + assert.Equal(t, OriginCode, config.Configuration[1].Origin) + assert.Equal(t, "key3", config.Configuration[2].Name) + assert.Equal(t, "[1 2 3]", config.Configuration[2].Value) + assert.Equal(t, OriginDDConfig, config.Configuration[2].Origin) + assert.Equal(t, "key4", config.Configuration[3].Name) + assert.Equal(t, "{1}", config.Configuration[3].Value) + assert.Equal(t, OriginEnvVar, config.Configuration[3].Origin) + assert.Equal(t, "key5", config.Configuration[4].Name) + assert.Equal(t, "1:{1}", config.Configuration[4].Value) + assert.Equal(t, OriginRemoteConfig, config.Configuration[4].Origin) + }, + }, + { + name: "product-start", + when: func(c *client) { + c.ProductStarted("test-product") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppProductChange{}, payload) + productChange := payload.(transport.AppProductChange) + assert.Len(t, productChange.Products, 1) + assert.True(t, productChange.Products[Namespace("test-product")].Enabled) + }, + }, + { + name: "product-start-error", + when: func(c *client) { + c.ProductStartError("test-product", errors.New("test-error")) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppProductChange{}, payload) + productChange := payload.(transport.AppProductChange) + assert.Len(t, productChange.Products, 1) + assert.False(t, productChange.Products[Namespace("test-product")].Enabled) + assert.Equal(t, "test-error", productChange.Products[Namespace("test-product")].Error.Message) + }, + }, + { + name: "product-stop", + when: func(c *client) { + c.ProductStopped("test-product") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppProductChange{}, payload) + productChange := payload.(transport.AppProductChange) + assert.Len(t, productChange.Products, 1) + assert.False(t, productChange.Products[Namespace("test-product")].Enabled) + }, + }, + { + name: "integration", + when: func(c *client) { + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppIntegrationChange{}, payload) + integrationChange := payload.(transport.AppIntegrationChange) + assert.Len(t, integrationChange.Integrations, 1) + assert.Equal(t, integrationChange.Integrations[0].Name, "test-integration") + assert.Equal(t, integrationChange.Integrations[0].Version, "1.0.0") + assert.True(t, integrationChange.Integrations[0].Enabled) + }, + }, + { + name: "integration-error", + when: func(c *client) { + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0", Error: "test-error"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppIntegrationChange{}, payload) + integrationChange := payload.(transport.AppIntegrationChange) + assert.Len(t, integrationChange.Integrations, 1) + assert.Equal(t, integrationChange.Integrations[0].Name, "test-integration") + assert.Equal(t, integrationChange.Integrations[0].Version, "1.0.0") + assert.False(t, integrationChange.Integrations[0].Enabled) + assert.Equal(t, integrationChange.Integrations[0].Error, "test-error") + }, + }, + { + name: "product+integration", + when: func(c *client) { + c.ProductStarted("test-product") + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.MessageBatch{}, payload) + batch := payload.(transport.MessageBatch) + assert.Len(t, batch, 2) + for _, payload := range batch { + switch p := payload.Payload.(type) { + case transport.AppProductChange: + assert.Equal(t, transport.RequestTypeAppProductChange, payload.RequestType) + assert.Len(t, p.Products, 1) + assert.True(t, p.Products[Namespace("test-product")].Enabled) + case transport.AppIntegrationChange: + assert.Equal(t, transport.RequestTypeAppIntegrationsChange, payload.RequestType) + assert.Len(t, p.Integrations, 1) + assert.Equal(t, p.Integrations[0].Name, "test-integration") + assert.Equal(t, p.Integrations[0].Version, "1.0.0") + assert.True(t, p.Integrations[0].Enabled) + default: + t.Fatalf("unexpected payload type: %T", p) + } + } + }, + }, + { + name: "product+integration+heartbeat", + clientConfig: ClientConfig{ + HeartbeatInterval: time.Nanosecond, + }, + when: func(c *client) { + c.ProductStarted("test-product") + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.MessageBatch{}, payload) + batch := payload.(transport.MessageBatch) + assert.Len(t, batch, 3) + for _, payload := range batch { + switch p := payload.Payload.(type) { + case transport.AppProductChange: + assert.Equal(t, transport.RequestTypeAppProductChange, payload.RequestType) + assert.Len(t, p.Products, 1) + assert.True(t, p.Products[Namespace("test-product")].Enabled) + case transport.AppIntegrationChange: + assert.Equal(t, transport.RequestTypeAppIntegrationsChange, payload.RequestType) + assert.Len(t, p.Integrations, 1) + assert.Equal(t, p.Integrations[0].Name, "test-integration") + assert.Equal(t, p.Integrations[0].Version, "1.0.0") + assert.True(t, p.Integrations[0].Enabled) + case transport.AppHeartbeat: + assert.Equal(t, transport.RequestTypeAppHeartbeat, payload.RequestType) + default: + t.Fatalf("unexpected payload type: %T", p) + } + } + }, + }, + { + name: "app-started", + when: func(c *client) { + c.AppStart() + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppStarted{}, payload) + appStart := payload.(transport.AppStarted) + assert.Equal(t, appStart.InstallSignature.InstallID, globalconfig.InstrumentationInstallID()) + assert.Equal(t, appStart.InstallSignature.InstallType, globalconfig.InstrumentationInstallType()) + assert.Equal(t, appStart.InstallSignature.InstallTime, globalconfig.InstrumentationInstallTime()) + }, + }, + { + name: "app-started-with-product", + when: func(c *client) { + c.AppStart() + c.ProductStarted("test-product") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppStarted{}, payload) + appStart := payload.(transport.AppStarted) + assert.Equal(t, appStart.Products[Namespace("test-product")].Enabled, true) + }, + }, + { + name: "app-started-with-configuration", + when: func(c *client) { + c.AppStart() + c.RegisterAppConfig("key", "value", OriginDefault) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppStarted{}, payload) + appStart := payload.(transport.AppStarted) + require.Len(t, appStart.Configuration, 1) + assert.Equal(t, appStart.Configuration[0].Name, "key") + assert.Equal(t, appStart.Configuration[0].Value, "value") + }, + }, + { + name: "app-started+integrations", + when: func(c *client) { + c.AppStart() + c.MarkIntegrationAsLoaded(Integration{Name: "test-integration", Version: "1.0.0"}) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppStarted{}, payload) + appStart := payload.(transport.AppStarted) + assert.Equal(t, globalconfig.InstrumentationInstallID(), appStart.InstallSignature.InstallID) + assert.Equal(t, globalconfig.InstrumentationInstallType(), appStart.InstallSignature.InstallType) + assert.Equal(t, globalconfig.InstrumentationInstallTime(), appStart.InstallSignature.InstallTime) - go func() { - client := &client{ - URL: server.URL, - } - client.start(nil, NamespaceTracers, true) - // Records should have the most recent value - client.Record(NamespaceTracers, MetricKindDist, "soobar", 1, nil, false) - client.Record(NamespaceTracers, MetricKindDist, "soobar", 3, nil, false) - client.mu.Lock() - client.flush(false) - client.mu.Unlock() - }() - - <-closed - - want := []DistributionSeries{ - // Distributions do not record metric types since it is its own event - {Metric: "soobar", Points: []float64{3}, Tags: []string{}}, - } - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %+v, got %+v", want, got) - } -} + payload = payloads[1] + require.IsType(t, transport.AppIntegrationChange{}, payload) + p := payload.(transport.AppIntegrationChange) -func TestDisabledClient(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - t.Setenv("DD_INSTRUMENTATION_TELEMETRY_ENABLED", "0") + assert.Len(t, p.Integrations, 1) + assert.Equal(t, p.Integrations[0].Name, "test-integration") + assert.Equal(t, p.Integrations[0].Version, "1.0.0") + }, + }, + { + name: "app-started+heartbeat", + clientConfig: ClientConfig{ + HeartbeatInterval: time.Nanosecond, + }, + when: func(c *client) { + c.AppStart() + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppStarted{}, payload) + appStart := payload.(transport.AppStarted) + assert.Equal(t, globalconfig.InstrumentationInstallID(), appStart.InstallSignature.InstallID) + assert.Equal(t, globalconfig.InstrumentationInstallType(), appStart.InstallSignature.InstallType) + assert.Equal(t, globalconfig.InstrumentationInstallTime(), appStart.InstallSignature.InstallTime) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("shouldn't have got any requests") - })) - defer server.Close() + payload = payloads[1] + require.IsType(t, transport.AppHeartbeat{}, payload) + }, + }, + { + name: "app-stopped", + when: func(c *client) { + c.AppStop() + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppClosing{}, payload) + }, + }, + { + name: "app-dependencies-loaded", + clientConfig: ClientConfig{ + DependencyLoader: func() (*debug.BuildInfo, bool) { + return &debug.BuildInfo{ + Deps: []*debug.Module{ + {Path: "test", Version: "v1.0.0"}, + {Path: "test2", Version: "v2.0.0"}, + {Path: "test3", Version: "3.0.0"}, + }, + }, true + }, + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppDependenciesLoaded{}, payload) + deps := payload.(transport.AppDependenciesLoaded) - client := &client{ - URL: server.URL, + assert.Len(t, deps.Dependencies, 3) + assert.Equal(t, deps.Dependencies[0].Name, "test") + assert.Equal(t, deps.Dependencies[0].Version, "1.0.0") + assert.Equal(t, deps.Dependencies[1].Name, "test2") + assert.Equal(t, deps.Dependencies[1].Version, "2.0.0") + assert.Equal(t, deps.Dependencies[2].Name, "test3") + assert.Equal(t, deps.Dependencies[2].Version, "3.0.0") + }, + }, + { + name: "app-many-dependencies-loaded", + clientConfig: ClientConfig{ + DependencyLoader: func() (*debug.BuildInfo, bool) { + modules := make([]*debug.Module, 2001) + for i := range modules { + modules[i] = &debug.Module{ + Path: fmt.Sprintf("test-%d", i), + Version: fmt.Sprintf("v%d.0.0", i), + } + } + return &debug.BuildInfo{ + Deps: modules, + }, true + }, + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.AppDependenciesLoaded{}, payload) + deps := payload.(transport.AppDependenciesLoaded) + + if len(deps.Dependencies) != 2000 && len(deps.Dependencies) != 1 { + t.Fatalf("expected 2000 and 1 dependencies, got %d", len(deps.Dependencies)) + } + + if len(deps.Dependencies) == 1 { + assert.Equal(t, deps.Dependencies[0].Name, "test-0") + assert.Equal(t, deps.Dependencies[0].Version, "0.0.0") + return + } + + for i := range deps.Dependencies { + assert.Equal(t, deps.Dependencies[i].Name, fmt.Sprintf("test-%d", i)) + assert.Equal(t, deps.Dependencies[i].Version, fmt.Sprintf("%d.0.0", i)) + } + }, + }, + { + name: "single-log-debug", + when: func(c *client) { + c.Log(LogDebug, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelDebug, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + }, + }, + { + name: "single-log-warn", + when: func(c *client) { + c.Log(LogWarn, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelWarn, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + }, + }, + { + name: "single-log-error", + when: func(c *client) { + c.Log(LogError, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + }, + }, + { + name: "multiple-logs-same-key", + when: func(c *client) { + c.Log(LogError, "test") + c.Log(LogError, "test") + c.Log(LogError, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Equal(t, uint32(3), logs.Logs[0].Count) + }, + }, + { + name: "single-log-with-tag", + when: func(c *client) { + c.Log(LogError, "test", WithTags([]string{"key:value"})) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Equal(t, "key:value", logs.Logs[0].Tags) + }, + }, + { + name: "single-log-with-tags", + when: func(c *client) { + c.Log(LogError, "test", WithTags([]string{"key:value", "key2:value2"})) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + tags := strings.Split(logs.Logs[0].Tags, ",") + assert.Contains(t, tags, "key:value") + assert.Contains(t, tags, "key2:value2") + }, + }, + { + name: "single-log-with-tags-and-without", + when: func(c *client) { + c.Log(LogError, "test", WithTags([]string{"key:value", "key2:value2"})) + c.Log(LogError, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 2) + + slices.SortStableFunc(logs.Logs, func(i, j transport.LogMessage) int { + return strings.Compare(i.Tags, j.Tags) + }) + + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Equal(t, uint32(1), logs.Logs[0].Count) + assert.Empty(t, logs.Logs[0].Tags) + + assert.Equal(t, transport.LogLevelError, logs.Logs[1].Level) + assert.Equal(t, "test", logs.Logs[1].Message) + assert.Equal(t, uint32(1), logs.Logs[1].Count) + tags := strings.Split(logs.Logs[1].Tags, ",") + assert.Contains(t, tags, "key:value") + assert.Contains(t, tags, "key2:value2") + }, + }, + { + name: "single-log-with-stacktrace", + when: func(c *client) { + c.Log(LogError, "test", WithStacktrace()) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Contains(t, logs.Logs[0].StackTrace, "internal/telemetry/client_test.go") + }, + }, + { + name: "single-log-with-stacktrace-and-tags", + when: func(c *client) { + c.Log(LogError, "test", WithStacktrace(), WithTags([]string{"key:value", "key2:value2"})) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Contains(t, logs.Logs[0].StackTrace, "internal/telemetry/client_test.go") + tags := strings.Split(logs.Logs[0].Tags, ",") + assert.Contains(t, tags, "key:value") + assert.Contains(t, tags, "key2:value2") + + }, + }, + { + name: "multiple-logs-different-levels", + when: func(c *client) { + c.Log(LogError, "test") + c.Log(LogWarn, "test") + c.Log(LogDebug, "test") + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.Logs{}, payload) + logs := payload.(transport.Logs) + require.Len(t, logs.Logs, 3) + + slices.SortStableFunc(logs.Logs, func(i, j transport.LogMessage) int { + return strings.Compare(string(i.Level), string(j.Level)) + }) + + assert.Equal(t, transport.LogLevelDebug, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) + assert.Equal(t, uint32(1), logs.Logs[0].Count) + assert.Equal(t, transport.LogLevelError, logs.Logs[1].Level) + assert.Equal(t, "test", logs.Logs[1].Message) + assert.Equal(t, uint32(1), logs.Logs[1].Count) + assert.Equal(t, transport.LogLevelWarn, logs.Logs[2].Level) + assert.Equal(t, "test", logs.Logs[2].Message) + assert.Equal(t, uint32(1), logs.Logs[2].Count) + }, + }, + { + name: "simple-count", + when: func(c *client) { + c.Count(NamespaceTracers, "init_time", nil).Submit(1) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 1) + assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + assert.Equal(t, 1.0, metrics.Series[0].Points[0][1]) + }, + }, + { + name: "count-multiple-call-same-handle", + when: func(c *client) { + handle1 := c.Count(NamespaceTracers, "init_time", nil) + handle2 := c.Count(NamespaceTracers, "init_time", nil) + + handle2.Submit(1) + handle1.Submit(1) + handle1.Submit(3) + handle2.Submit(2) + handle2.Submit(10) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 1) + assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + assert.Equal(t, 17.0, metrics.Series[0].Points[0][1]) + }, + }, + { + name: "multiple-count-by-name", + when: func(c *client) { + c.Count(NamespaceTracers, "init_time_1", nil).Submit(1) + c.Count(NamespaceTracers, "init_time_2", nil).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 2) + + assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64))), metrics.Series[0].Metric) + + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + + assert.Equal(t, transport.CountMetric, metrics.Series[1].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) + assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64))), metrics.Series[1].Metric) + assert.Empty(t, metrics.Series[1].Tags) + assert.NotZero(t, metrics.Series[1].Points[0][0]) + }, + }, + { + name: "multiple-count-by-tags", + when: func(c *client) { + c.Count(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) + c.Count(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 2) + + assert.Equal(t, transport.CountMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + + assert.Contains(t, metrics.Series[0].Tags, "test:"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64)))) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + + assert.Equal(t, transport.CountMetric, metrics.Series[1].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) + assert.Equal(t, "init_time", metrics.Series[1].Metric) + assert.Contains(t, metrics.Series[1].Tags, "test:"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64)))) + assert.NotZero(t, metrics.Series[1].Points[0][0]) + }, + }, + { + name: "simple-gauge", + when: func(c *client) { + c.Gauge(NamespaceTracers, "init_time", nil).Submit(1) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 1) + assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + assert.Equal(t, 1.0, metrics.Series[0].Points[0][1]) + }, + }, + { + name: "multiple-gauge-by-name", + when: func(c *client) { + c.Gauge(NamespaceTracers, "init_time_1", nil).Submit(1) + c.Gauge(NamespaceTracers, "init_time_2", nil).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 2) + + assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64))), metrics.Series[0].Metric) + + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + + assert.Equal(t, transport.GaugeMetric, metrics.Series[1].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) + assert.Equal(t, "init_time_"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64))), metrics.Series[1].Metric) + assert.Empty(t, metrics.Series[1].Tags) + assert.NotZero(t, metrics.Series[1].Points[0][0]) + }, + }, + { + name: "multiple-gauge-by-tags", + when: func(c *client) { + c.Gauge(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) + c.Gauge(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 2) + + assert.Equal(t, transport.GaugeMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + + assert.Contains(t, metrics.Series[0].Tags, "test:"+strconv.Itoa(int(metrics.Series[0].Points[0][1].(float64)))) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + + assert.Equal(t, transport.GaugeMetric, metrics.Series[1].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[1].Namespace) + assert.Equal(t, "init_time", metrics.Series[1].Metric) + assert.Contains(t, metrics.Series[1].Tags, "test:"+strconv.Itoa(int(metrics.Series[1].Points[0][1].(float64)))) + assert.NotZero(t, metrics.Series[1].Points[0][0]) + }, + }, + { + name: "simple-rate", + when: func(c *client) { + handle := c.Rate(NamespaceTracers, "init_time", nil) + handle.Submit(1) + + rate := handle.(*rate) + // So the rate is not +Infinity because the interval is zero + now := rate.intervalStart.Load() + sub := now.Add(-time.Second) + rate.intervalStart.Store(&sub) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, transport.GenerateMetrics{}, payload) + metrics := payload.(transport.GenerateMetrics) + require.Len(t, metrics.Series, 1) + assert.Equal(t, transport.RateMetric, metrics.Series[0].Type) + assert.Equal(t, NamespaceTracers, metrics.Series[0].Namespace) + assert.Equal(t, "init_time", metrics.Series[0].Metric) + assert.Empty(t, metrics.Series[0].Tags) + assert.NotZero(t, metrics.Series[0].Interval) + assert.NotZero(t, metrics.Series[0].Points[0][0]) + assert.LessOrEqual(t, metrics.Series[0].Points[0][1], 1.1) + }, + }, + { + name: "simple-distribution", + when: func(c *client) { + c.Distribution(NamespaceGeneral, "init_time", nil).Submit(1) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, payload, transport.Distributions{}) + distributions := payload.(transport.Distributions) + require.Len(t, distributions.Series, 1) + assert.Equal(t, NamespaceGeneral, distributions.Series[0].Namespace) + assert.Equal(t, "init_time", distributions.Series[0].Metric) + assert.Empty(t, distributions.Series[0].Tags) + require.Len(t, distributions.Series[0].Points, 1) + assert.Equal(t, 1.0, distributions.Series[0].Points[0]) + }, + }, + { + name: "multiple-distribution-by-name", + when: func(c *client) { + c.Distribution(NamespaceTracers, "init_time_1", nil).Submit(1) + c.Distribution(NamespaceTracers, "init_time_2", nil).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, payload, transport.Distributions{}) + distributions := payload.(transport.Distributions) + require.Len(t, distributions.Series, 2) + + assert.Equal(t, "init_time_"+strconv.Itoa(int(distributions.Series[0].Points[0])), distributions.Series[0].Metric) + assert.Equal(t, "init_time_"+strconv.Itoa(int(distributions.Series[1].Points[0])), distributions.Series[1].Metric) + }, + }, + { + name: "multiple-distribution-by-tags", + when: func(c *client) { + c.Distribution(NamespaceTracers, "init_time", []string{"test:1"}).Submit(1) + c.Distribution(NamespaceTracers, "init_time", []string{"test:2"}).Submit(2) + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, payload, transport.Distributions{}) + distributions := payload.(transport.Distributions) + require.Len(t, distributions.Series, 2) + + assert.Contains(t, distributions.Series[0].Tags, "test:"+strconv.Itoa(int(distributions.Series[0].Points[0]))) + assert.Contains(t, distributions.Series[1].Tags, "test:"+strconv.Itoa(int(distributions.Series[1].Points[0]))) + }, + }, + { + name: "distribution-overflow", + when: func(c *client) { + handler := c.Distribution(NamespaceGeneral, "init_time", nil) + for i := 0; i < 1<<16; i++ { + handler.Submit(float64(i)) + } + }, + expect: func(t *testing.T, payloads []transport.Payload) { + payload := payloads[0] + require.IsType(t, payload, transport.Distributions{}) + distributions := payload.(transport.Distributions) + require.Len(t, distributions.Series, 1) + assert.Equal(t, NamespaceGeneral, distributions.Series[0].Namespace) + assert.Equal(t, "init_time", distributions.Series[0].Metric) + assert.Empty(t, distributions.Series[0].Tags) + + // Should not contain the first passed point + assert.NotContains(t, distributions.Series[0].Points, 0.0) + }, + }, } - client.start(nil, NamespaceTracers, true) - client.Record(NamespaceTracers, MetricKindGauge, "foobar", 1, nil, false) - client.Count(NamespaceTracers, "bonk", 4, []string{"org:1"}, false) - client.Stop() -} -func TestNonStartedClient(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("shouldn't have got any requests") - })) - defer server.Close() + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + config := defaultConfig(test.clientConfig) + config.AgentURL = "http://localhost:8126" + config.DependencyLoader = test.clientConfig.DependencyLoader // Don't use the default dependency loader + config.internalMetricsEnabled = test.clientConfig.internalMetricsEnabled // only enabled internal metrics when explicitly set + config.internalMetricsEnabled = false + c, err := newClient(tracerConfig, config) + require.NoError(t, err) + defer c.Close() + + recordWriter := &internal.RecordWriter{} + c.writer = recordWriter + + if test.when != nil { + test.when(c) + } + c.Flush() - client := &client{ - URL: server.URL, + payloads := recordWriter.Payloads() + require.LessOrEqual(t, 1, len(payloads)) + test.expect(t, payloads) + }) } - client.Record(NamespaceTracers, MetricKindGauge, "foobar", 1, nil, false) - client.Count(NamespaceTracers, "bonk", 4, []string{"org:1"}, false) - client.Stop() } -func TestConcurrentClient(t *testing.T) { - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - var ( - mu sync.Mutex - got []Series - ) - closed := make(chan struct{}, 1) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Log("foo") - req := Body{ - Payload: new(Metrics), - } - dec := json.NewDecoder(r.Body) - err := dec.Decode(&req) - if err != nil { - t.Fatal(err) - } - if req.RequestType != RequestTypeGenerateMetrics { - return - } - v, ok := req.Payload.(*Metrics) - if !ok { - t.Fatal("payload set metrics but didn't get metrics") - } - for _, s := range v.Series { - for i, p := range s.Points { - // zero out timestamps - s.Points[i] = [2]float64{0, p[1]} - } - } - mu.Lock() - defer mu.Unlock() - got = append(got, v.Series...) - select { - case closed <- struct{}{}: - default: - return - } - })) - defer server.Close() +func TestMetricsDisabled(t *testing.T) { + t.Setenv("DD_TELEMETRY_METRICS_ENABLED", "false") - go func() { - client := &client{ - URL: server.URL, - } - client.start(nil, NamespaceTracers, true) - defer client.Stop() + c, err := NewClient("test-service", "test-env", "1.0.0", ClientConfig{AgentURL: "http://localhost:8126"}) + require.NoError(t, err) - var wg sync.WaitGroup - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 10; j++ { - client.Count(NamespaceTracers, "foobar", 1, []string{"tag"}, false) - } - }() - } - wg.Wait() - }() + recordWriter := &internal.RecordWriter{} + c.(*client).writer = recordWriter - <-closed + defer c.Close() - want := []Series{ - {Metric: "foobar", Type: "count", Points: [][2]float64{{0, 80}}, Tags: []string{"tag"}}, - } - sort.Slice(got, func(i, j int) bool { - return got[i].Metric < got[j].Metric - }) - if !reflect.DeepEqual(want, got) { - t.Fatalf("want %+v, got %+v", want, got) - } + assert.NotNil(t, c.Gauge(NamespaceTracers, "init_time", nil)) + assert.NotNil(t, c.Count(NamespaceTracers, "init_time", nil)) + assert.NotNil(t, c.Rate(NamespaceTracers, "init_time", nil)) + assert.NotNil(t, c.Distribution(NamespaceGeneral, "init_time", nil)) + + c.Flush() + + payloads := recordWriter.Payloads() + require.Len(t, payloads, 0) } -// fakeAgentless is a helper function for TestAgentlessRetry. It replaces the agentless -// endpoint in the telemetry package with a custom server URL and returns -// 1. a function that waits for a telemetry request to that server -// 2. a cleanup function that closes the server and resets the agentless endpoint to -// its original value -func fakeAgentless(ctx context.Context, t *testing.T) (wait func(), cleanup func()) { - received := make(chan struct{}, 1) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("DD-Telemetry-Request-Type") == string(RequestTypeAppStarted) { - select { - case received <- struct{}{}: - default: - } - } - })) - prevEndpoint := SetAgentlessEndpoint(server.URL) - return func() { - select { - case <-ctx.Done(): - t.Fatalf("fake agentless endpoint timed out waiting for telemetry") - case <-received: - return - } - }, func() { - server.Close() - SetAgentlessEndpoint(prevEndpoint) - } +type testRoundTripper struct { + t *testing.T + roundTrip func(*http.Request) (*http.Response, error) + bodies []transport.Body } -// TestAgentlessRetry tests the behavior of the telemetry client in the case where -// the client cannot connect to the agent. The client should re-try the request -// with the agentless endpoint. -func TestAgentlessRetry(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() +func (t *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + defer req.Body.Close() + body, err := io.ReadAll(req.Body) + require.NoError(t.t, err) + req.Body = io.NopCloser(bytes.NewReader(body)) + t.bodies = append(t.bodies, parseRequest(t.t, req.Header, body)) + return t.roundTrip(req) +} - waitAgentlessEndpoint, cleanup := fakeAgentless(ctx, t) - defer cleanup() +func parseRequest(t *testing.T, headers http.Header, raw []byte) transport.Body { + t.Helper() - brokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })) - brokenServer.Close() + assert.Equal(t, "v2", headers.Get("DD-Telemetry-API-Version")) + assert.Equal(t, "application/json", headers.Get("Content-Type")) + assert.Equal(t, "go", headers.Get("DD-Client-Library-Language")) + assert.Equal(t, "test-env", headers.Get("DD-Agent-Env")) + assert.Equal(t, version.Tag, headers.Get("DD-Client-Library-Version")) + assert.Equal(t, globalconfig.InstrumentationInstallID(), headers.Get("DD-Agent-Install-Id")) + assert.Equal(t, globalconfig.InstrumentationInstallType(), headers.Get("DD-Agent-Install-Type")) + assert.Equal(t, globalconfig.InstrumentationInstallTime(), headers.Get("DD-Agent-Install-Time")) - client := &client{ - URL: brokenServer.URL, - } - client.start(nil, NamespaceTracers, true) - waitAgentlessEndpoint() + assert.NotEmpty(t, headers.Get("DD-Agent-Hostname")) + + var body transport.Body + require.NoError(t, json.Unmarshal(raw, &body)) + + assert.Equal(t, string(body.RequestType), headers.Get("DD-Telemetry-Request-Type")) + assert.Equal(t, "test-service", body.Application.ServiceName) + assert.Equal(t, "test-env", body.Application.Env) + assert.Equal(t, "1.0.0", body.Application.ServiceVersion) + assert.Equal(t, "go", body.Application.LanguageName) + assert.Equal(t, runtime.Version(), body.Application.LanguageVersion) + + assert.NotEmpty(t, body.Host.Hostname) + assert.Equal(t, osinfo.OSName(), body.Host.OS) + assert.Equal(t, osinfo.OSVersion(), body.Host.OSVersion) + assert.Equal(t, osinfo.Architecture(), body.Host.Architecture) + assert.Equal(t, osinfo.KernelName(), body.Host.KernelName) + assert.Equal(t, osinfo.KernelRelease(), body.Host.KernelRelease) + assert.Equal(t, osinfo.KernelVersion(), body.Host.KernelVersion) + + assert.Equal(t, "v2", body.APIVersion) + assert.NotZero(t, body.TracerTime) + assert.LessOrEqual(t, int64(1), body.SeqID) + assert.Equal(t, globalconfig.RuntimeID(), body.RuntimeID) + + return body } -func TestCollectDependencies(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - received := make(chan *Dependencies) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("DD-Telemetry-Request-Type") == string(RequestTypeDependenciesLoaded) { - var body Body - body.Payload = new(Dependencies) - err := json.NewDecoder(r.Body).Decode(&body) - if err != nil { - t.Errorf("bad body: %s", err) - } - select { - case received <- body.Payload.(*Dependencies): - default: - } - } - })) - defer server.Close() - client := &client{ - URL: server.URL, - } - client.start(nil, NamespaceTracers, true) - select { - case <-received: - case <-ctx.Done(): - t.Fatalf("Timed out waiting for dependency payload") +func TestClientEnd2End(t *testing.T) { + tracerConfig := internal.TracerConfig{ + Service: "test-service", + Env: "test-env", + Version: "1.0.0", } -} -func Test_heartbeatInterval(t *testing.T) { - defaultInterval := time.Second * time.Duration(defaultHeartbeatInterval) - tests := []struct { - name string - setup func(t *testing.T) - want time.Duration + for _, test := range []struct { + name string + when func(*client) + roundtrip func(*testing.T, *http.Request) (*http.Response, error) + expect func(*testing.T, []transport.Body) }{ { - name: "default", - setup: func(t *testing.T) {}, - want: defaultInterval, + name: "app-start", + when: func(c *client) { + c.AppStart() + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 1) + assert.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) + }, }, { - name: "float", - setup: func(t *testing.T) { t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "0.2") }, - want: time.Millisecond * 200, + name: "app-stop", + when: func(c *client) { + c.AppStop() + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 1) + assert.Equal(t, transport.RequestTypeAppClosing, bodies[0].RequestType) + }, }, { - name: "integer", - setup: func(t *testing.T) { t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "2") }, - want: time.Second * 2, + name: "app-start+app-stop", + when: func(c *client) { + c.AppStart() + c.AppStop() + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 2) + assert.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) + assert.Equal(t, transport.RequestTypeAppClosing, bodies[1].RequestType) + }, }, { - name: "negative", - setup: func(t *testing.T) { t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "-1") }, - want: defaultInterval, + name: "message-batch", + when: func(c *client) { + c.RegisterAppConfig("key", "value", OriginCode) + c.ProductStarted(NamespaceAppSec) + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 1) + assert.Equal(t, transport.RequestTypeMessageBatch, bodies[0].RequestType) + batch := bodies[0].Payload.(transport.MessageBatch) + require.Len(t, batch, 2) + assert.Equal(t, transport.RequestTypeAppProductChange, batch[0].RequestType) + assert.Equal(t, transport.RequestTypeAppClientConfigurationChange, batch[1].RequestType) + }, }, { - name: "zero", - setup: func(t *testing.T) { t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "0") }, - want: defaultInterval, + name: "fail-agent-endpoint", + when: func(c *client) { + c.AppStart() + }, + roundtrip: func(_ *testing.T, req *http.Request) (*http.Response, error) { + if strings.Contains(req.URL.Host, "localhost") { + return nil, errors.New("failed") + } + return &http.Response{StatusCode: http.StatusOK}, nil + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 2) + require.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) + require.Equal(t, transport.RequestTypeAppStarted, bodies[1].RequestType) + }, }, { - name: "long", - setup: func(t *testing.T) { t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "4000") }, - want: defaultInterval, + name: "fail-all-endpoint", + when: func(c *client) { + c.AppStart() + }, + roundtrip: func(_ *testing.T, _ *http.Request) (*http.Response, error) { + return nil, errors.New("failed") + }, + expect: func(t *testing.T, bodies []transport.Body) { + require.Len(t, bodies, 2) + require.Equal(t, transport.RequestTypeAppStarted, bodies[0].RequestType) + require.Equal(t, transport.RequestTypeAppStarted, bodies[1].RequestType) + }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.setup(t) - assert.Equal(t, tt.want, heartbeatInterval()) + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + rt := &testRoundTripper{ + t: t, + roundTrip: func(req *http.Request) (*http.Response, error) { + if test.roundtrip != nil { + return test.roundtrip(t, req) + } + return &http.Response{StatusCode: http.StatusOK}, nil + }, + } + clientConfig := ClientConfig{ + AgentURL: "http://localhost:8126", + APIKey: "apikey", + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: rt, + }, + Debug: true, + } + + clientConfig = defaultConfig(clientConfig) + clientConfig.DependencyLoader = nil + + c, err := newClient(tracerConfig, clientConfig) + require.NoError(t, err) + defer c.Close() + + test.when(c) + c.Flush() + test.expect(t, rt.bodies) }) } } -func TestNoEmptyHeaders(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping test on non-linux OS") +func TestHeartBeatInterval(t *testing.T) { + startTime := time.Now() + payloadtimes := make([]time.Duration, 0, 32) + c, err := NewClient("test-service", "test-env", "1.0.0", ClientConfig{ + AgentURL: "http://localhost:8126", + HeartbeatInterval: 2 * time.Second, + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &testRoundTripper{ + t: t, + roundTrip: func(_ *http.Request) (*http.Response, error) { + payloadtimes = append(payloadtimes, time.Since(startTime)) + startTime = time.Now() + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + }, + }, + }) + require.NoError(t, err) + defer c.Close() + + for i := 0; i < 10; i++ { + c.Log(LogError, "test") + time.Sleep(1 * time.Second) + } + + // 10 seconds have passed, we should have sent 5 heartbeats + + c.Flush() + c.Close() + + require.InDelta(t, 5, len(payloadtimes), 1) + sum := 0.0 + for _, d := range payloadtimes { + sum += d.Seconds() + } + + assert.InDelta(t, 2, sum/5, 0.1) +} + +func TestSendingFailures(t *testing.T) { + cfg := ClientConfig{ + AgentURL: "http://localhost:8126", + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: &testRoundTripper{ + t: t, + roundTrip: func(_ *http.Request) (*http.Response, error) { + return nil, errors.New("failed") + }, + }, + }, } - if internal.EntityID() == "" || internal.ContainerID() == "" { - t.Skip("skipping test when entity ID and container ID are not available") + + c, err := newClient(internal.TracerConfig{ + Service: "test-service", + Env: "test-env", + Version: "1.0.0", + }, defaultConfig(cfg)) + + require.NoError(t, err) + defer c.Close() + + c.Log(LogError, "test") + c.Flush() + + require.False(t, c.payloadQueue.IsEmpty()) + payload := c.payloadQueue.ReversePeek() + require.NotNil(t, payload) + + assert.Equal(t, transport.RequestTypeLogs, payload.RequestType()) + logs := payload.(transport.Logs) + assert.Len(t, logs.Logs, 1) + assert.Equal(t, transport.LogLevelError, logs.Logs[0].Level) + assert.Equal(t, "test", logs.Logs[0].Message) +} + +func BenchmarkLogs(b *testing.B) { + clientConfig := ClientConfig{ + HeartbeatInterval: time.Hour, + ExtendedHeartbeatInterval: time.Hour, + AgentURL: "http://localhost:8126", + } + + b.Run("simple", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Log(LogDebug, "this is supposed to be a DEBUG log of representative length with a variable message: "+strconv.Itoa(i%10)) + } + }) + + b.Run("with-tags", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Log(LogWarn, "this is supposed to be a WARN log of representative length", WithTags([]string{"key:" + strconv.Itoa(i%10)})) + } + }) + + b.Run("with-stacktrace", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Log(LogError, "this is supposed to be a ERROR log of representative length", WithStacktrace()) + } + }) +} + +type noopTransport struct{} + +func (noopTransport) RoundTrip(*http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK}, nil +} + +func BenchmarkWorstCaseScenarioFloodLogging(b *testing.B) { + b.ReportAllocs() + nbSameLogs := 10 + nbDifferentLogs := 100 + nbGoroutines := 25 + + clientConfig := ClientConfig{ + HeartbeatInterval: time.Hour, + ExtendedHeartbeatInterval: time.Hour, + FlushInterval: internal.Range[time.Duration]{Min: time.Second, Max: time.Second}, + AgentURL: "http://localhost:8126", + + // Empty transport to avoid sending data to the agent + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: noopTransport{}, + }, } - c := &client{} - req := c.newRequest(RequestTypeAppStarted) - assertNotEmpty := func(header string) { - headers := *req.Header - vals := headers[header] - assert.Greater(t, len(vals), 0, "header %s should not be empty", header) - for _, v := range vals { - assert.NotEmpty(t, v, "%s header should not be empty", header) + + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + + for x := 0; x < b.N; x++ { + var wg sync.WaitGroup + + for i := 0; i < nbGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < nbDifferentLogs; j++ { + for k := 0; k < nbSameLogs; k++ { + c.Log(LogDebug, "this is supposed to be a DEBUG log of representative length"+strconv.Itoa(i), WithTags([]string{"key:" + strconv.Itoa(j)})) + } + } + }() } + + wg.Wait() + } + + b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentLogs*nbSameLogs*b.N)), "ns/log") +} + +func BenchmarkMetrics(b *testing.B) { + b.ReportAllocs() + clientConfig := ClientConfig{ + HeartbeatInterval: time.Hour, + ExtendedHeartbeatInterval: time.Hour, + AgentURL: "http://localhost:8126", } - assertNotEmpty("Datadog-Container-ID") - assertNotEmpty("Datadog-Entity-ID") + + b.Run("count+get-handle", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Count(NamespaceGeneral, "logs_created", nil).Submit(1) + } + }) + + b.Run("count+handle-reused", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + handle := c.Count(NamespaceGeneral, "logs_created", nil) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handle.Submit(1) + } + }) + + b.Run("gauge+get-handle", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Gauge(NamespaceTracers, "stats_buckets", nil).Submit(1) + } + }) + + b.Run("gauge+handle-reused", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + handle := c.Gauge(NamespaceTracers, "stats_buckets", nil) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handle.Submit(1) + } + }) + + b.Run("rate+get-handle", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Rate(NamespaceTracers, "init_time", nil).Submit(1) + } + }) + + b.Run("rate+handle-reused", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + handle := c.Rate(NamespaceTracers, "init_time", nil) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handle.Submit(1) + } + }) + + b.Run("distribution+get-handle", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + c.Distribution(NamespaceGeneral, "init_time", nil).Submit(1) + } + }) + + b.Run("distribution+handle-reused", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + handle := c.Distribution(NamespaceGeneral, "init_time", nil) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handle.Submit(1) + } + }) } -func TestTelementryClientLogging(t *testing.T) { - var ( - rl = new(logging.RecordLogger) - oldLevel = logging.GetLevel() - ) - logging.SetLevel(logging.LevelDebug) - undo := logging.UseLogger(rl) - defer func() { - logging.SetLevel(oldLevel) - undo() - }() - - // We simulate a client that has already started - c := &client{ - started: true, +func BenchmarkWorstCaseScenarioFloodMetrics(b *testing.B) { + nbSameMetric := 1000 + nbDifferentMetrics := 50 + nbGoroutines := 50 + + clientConfig := ClientConfig{ + HeartbeatInterval: time.Hour, + ExtendedHeartbeatInterval: time.Hour, + FlushInterval: internal.Range[time.Duration]{Min: time.Second, Max: time.Second}, + DistributionsSize: internal.Range[int]{Min: 256, Max: -1}, + AgentURL: "http://localhost:8126", + + // Empty transport to avoid sending data to the agent + HTTPClient: &http.Client{ + Timeout: 5 * time.Second, + Transport: noopTransport{}, + }, } - c.start(nil, NamespaceTracers, true) - assert := assert.New(t) - assert.Contains(rl.Logs()[0], LogPrefix+"attempted to start telemetry client when client has already started - ignoring attempt") + b.Run("get-handle", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + + for x := 0; x < b.N; x++ { + var wg sync.WaitGroup + + for i := 0; i < nbGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < nbDifferentMetrics; j++ { + metricName := "init_time_" + strconv.Itoa(j) + for k := 0; k < nbSameMetric; k++ { + c.Count(NamespaceGeneral, metricName, []string{"test:1"}).Submit(1) + } + } + }() + } + + wg.Wait() + } + + b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentMetrics*nbSameMetric*b.N)), "ns/point") + }) + + b.Run("handle-reused", func(b *testing.B) { + b.ReportAllocs() + c, err := NewClient("test-service", "test-env", "1.0.0", clientConfig) + require.NoError(b, err) + + defer c.Close() + + b.ResetTimer() + + for x := 0; x < b.N; x++ { + var wg sync.WaitGroup + + handles := make([]MetricHandle, nbDifferentMetrics) + for i := 0; i < nbDifferentMetrics; i++ { + handles[i] = c.Count(NamespaceGeneral, "init_time_"+strconv.Itoa(i), []string{"test:1"}) + } + for i := 0; i < nbGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < nbDifferentMetrics; j++ { + for k := 0; k < nbSameMetric; k++ { + handles[j].Submit(1) + } + } + }() + } + + wg.Wait() + } + + b.ReportMetric(float64(b.Elapsed().Nanoseconds()/int64(nbGoroutines*nbDifferentMetrics*nbSameMetric*b.N)), "ns/point") + }) + } diff --git a/internal/newtelemetry/configuration.go b/internal/telemetry/configuration.go similarity index 97% rename from internal/newtelemetry/configuration.go rename to internal/telemetry/configuration.go index 3cdbe76477..4a14c4f443 100644 --- a/internal/newtelemetry/configuration.go +++ b/internal/telemetry/configuration.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "encoding/json" @@ -14,7 +14,7 @@ import ( "strings" "sync" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type configuration struct { diff --git a/internal/newtelemetry/dependencies.go b/internal/telemetry/dependencies.go similarity index 95% rename from internal/newtelemetry/dependencies.go rename to internal/telemetry/dependencies.go index 10e2bee3d3..b881aa3ac3 100644 --- a/internal/newtelemetry/dependencies.go +++ b/internal/telemetry/dependencies.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "runtime/debug" @@ -11,7 +11,7 @@ import ( "sync" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type dependencies struct { diff --git a/internal/newtelemetry/distributions.go b/internal/telemetry/distributions.go similarity index 90% rename from internal/newtelemetry/distributions.go rename to internal/telemetry/distributions.go index a556936731..6405c67ae7 100644 --- a/internal/newtelemetry/distributions.go +++ b/internal/telemetry/distributions.go @@ -3,15 +3,15 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "sync" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/knownmetrics" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/knownmetrics" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type distributions struct { diff --git a/internal/newtelemetry/globalclient.go b/internal/telemetry/globalclient.go similarity index 98% rename from internal/newtelemetry/globalclient.go rename to internal/telemetry/globalclient.go index efd156bb0c..bf99d62d8d 100644 --- a/internal/newtelemetry/globalclient.go +++ b/internal/telemetry/globalclient.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2024 Datadog, Inc. -package newtelemetry +package telemetry import ( "sync" @@ -11,8 +11,8 @@ import ( globalinternal "gopkg.in/DataDog/dd-trace-go.v1/internal" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) var ( diff --git a/internal/newtelemetry/integration.go b/internal/telemetry/integration.go similarity index 90% rename from internal/newtelemetry/integration.go rename to internal/telemetry/integration.go index d77ef116cd..ff45af0494 100644 --- a/internal/newtelemetry/integration.go +++ b/internal/telemetry/integration.go @@ -3,12 +3,12 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "sync" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type integrations struct { diff --git a/internal/newtelemetry/internal/knownmetrics/common_metrics.json b/internal/telemetry/internal/knownmetrics/common_metrics.json similarity index 100% rename from internal/newtelemetry/internal/knownmetrics/common_metrics.json rename to internal/telemetry/internal/knownmetrics/common_metrics.json diff --git a/internal/newtelemetry/internal/knownmetrics/generator/generator.go b/internal/telemetry/internal/knownmetrics/generator/generator.go similarity index 96% rename from internal/newtelemetry/internal/knownmetrics/generator/generator.go rename to internal/telemetry/internal/knownmetrics/generator/generator.go index 1cdf13ff38..ddce6fc147 100644 --- a/internal/newtelemetry/internal/knownmetrics/generator/generator.go +++ b/internal/telemetry/internal/knownmetrics/generator/generator.go @@ -19,8 +19,8 @@ import ( "slices" "strings" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/knownmetrics" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/knownmetrics" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // This represents the base64-encoded URL of api.github.com to download the configuration file. diff --git a/internal/newtelemetry/internal/knownmetrics/golang_metrics.json b/internal/telemetry/internal/knownmetrics/golang_metrics.json similarity index 100% rename from internal/newtelemetry/internal/knownmetrics/golang_metrics.json rename to internal/telemetry/internal/knownmetrics/golang_metrics.json diff --git a/internal/newtelemetry/internal/knownmetrics/known_metrics.go b/internal/telemetry/internal/knownmetrics/known_metrics.go similarity index 92% rename from internal/newtelemetry/internal/knownmetrics/known_metrics.go rename to internal/telemetry/internal/knownmetrics/known_metrics.go index 4fb1c6b079..fd6a90edcf 100644 --- a/internal/newtelemetry/internal/knownmetrics/known_metrics.go +++ b/internal/telemetry/internal/knownmetrics/known_metrics.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -//go:generate go run gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/knownmetrics/generator +//go:generate go run gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/knownmetrics/generator package knownmetrics @@ -13,7 +13,7 @@ import ( "slices" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) //go:embed common_metrics.json diff --git a/internal/newtelemetry/internal/mapper/app_closing.go b/internal/telemetry/internal/mapper/app_closing.go similarity index 90% rename from internal/newtelemetry/internal/mapper/app_closing.go rename to internal/telemetry/internal/mapper/app_closing.go index aeba9c7adc..9a570d3ee2 100644 --- a/internal/newtelemetry/internal/mapper/app_closing.go +++ b/internal/telemetry/internal/mapper/app_closing.go @@ -6,7 +6,7 @@ package mapper import ( - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // NewAppClosingMapper returns a new Mapper that appends an AppClosing payload to the given payloads and calls the underlying Mapper with it. diff --git a/internal/newtelemetry/internal/mapper/app_started.go b/internal/telemetry/internal/mapper/app_started.go similarity index 95% rename from internal/newtelemetry/internal/mapper/app_started.go rename to internal/telemetry/internal/mapper/app_started.go index f281672a17..64ce18b46e 100644 --- a/internal/newtelemetry/internal/mapper/app_started.go +++ b/internal/telemetry/internal/mapper/app_started.go @@ -7,7 +7,7 @@ package mapper import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type appStartedReducer struct { diff --git a/internal/newtelemetry/internal/mapper/default.go b/internal/telemetry/internal/mapper/default.go similarity index 97% rename from internal/newtelemetry/internal/mapper/default.go rename to internal/telemetry/internal/mapper/default.go index 12f75a5fb8..6576821a96 100644 --- a/internal/newtelemetry/internal/mapper/default.go +++ b/internal/telemetry/internal/mapper/default.go @@ -10,7 +10,7 @@ import ( "golang.org/x/time/rate" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // NewDefaultMapper returns a Mapper that transforms payloads into a MessageBatch and adds a heartbeat message. diff --git a/internal/newtelemetry/internal/mapper/mapper.go b/internal/telemetry/internal/mapper/mapper.go similarity index 89% rename from internal/newtelemetry/internal/mapper/mapper.go rename to internal/telemetry/internal/mapper/mapper.go index 84a376fee6..eaee0b95a6 100644 --- a/internal/newtelemetry/internal/mapper/mapper.go +++ b/internal/telemetry/internal/mapper/mapper.go @@ -6,7 +6,7 @@ package mapper import ( - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // Mapper is an interface for transforming payloads to comply with different types of lifecycle events in the application. diff --git a/internal/newtelemetry/internal/range.go b/internal/telemetry/internal/range.go similarity index 100% rename from internal/newtelemetry/internal/range.go rename to internal/telemetry/internal/range.go diff --git a/internal/newtelemetry/internal/recorder.go b/internal/telemetry/internal/recorder.go similarity index 100% rename from internal/newtelemetry/internal/recorder.go rename to internal/telemetry/internal/recorder.go diff --git a/internal/newtelemetry/internal/ringbuffer.go b/internal/telemetry/internal/ringbuffer.go similarity index 100% rename from internal/newtelemetry/internal/ringbuffer.go rename to internal/telemetry/internal/ringbuffer.go diff --git a/internal/newtelemetry/internal/ringbuffer_test.go b/internal/telemetry/internal/ringbuffer_test.go similarity index 100% rename from internal/newtelemetry/internal/ringbuffer_test.go rename to internal/telemetry/internal/ringbuffer_test.go diff --git a/internal/newtelemetry/internal/syncmap.go b/internal/telemetry/internal/syncmap.go similarity index 100% rename from internal/newtelemetry/internal/syncmap.go rename to internal/telemetry/internal/syncmap.go diff --git a/internal/newtelemetry/internal/syncpool.go b/internal/telemetry/internal/syncpool.go similarity index 100% rename from internal/newtelemetry/internal/syncpool.go rename to internal/telemetry/internal/syncpool.go diff --git a/internal/newtelemetry/internal/ticker.go b/internal/telemetry/internal/ticker.go similarity index 100% rename from internal/newtelemetry/internal/ticker.go rename to internal/telemetry/internal/ticker.go diff --git a/internal/newtelemetry/internal/tracerconfig.go b/internal/telemetry/internal/tracerconfig.go similarity index 100% rename from internal/newtelemetry/internal/tracerconfig.go rename to internal/telemetry/internal/tracerconfig.go diff --git a/internal/newtelemetry/internal/transport/app_closing.go b/internal/telemetry/internal/transport/app_closing.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_closing.go rename to internal/telemetry/internal/transport/app_closing.go diff --git a/internal/newtelemetry/internal/transport/app_configuration_change.go b/internal/telemetry/internal/transport/app_configuration_change.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_configuration_change.go rename to internal/telemetry/internal/transport/app_configuration_change.go diff --git a/internal/newtelemetry/internal/transport/app_dependencies_loaded.go b/internal/telemetry/internal/transport/app_dependencies_loaded.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_dependencies_loaded.go rename to internal/telemetry/internal/transport/app_dependencies_loaded.go diff --git a/internal/newtelemetry/internal/transport/app_extended_heartbeat.go b/internal/telemetry/internal/transport/app_extended_heartbeat.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_extended_heartbeat.go rename to internal/telemetry/internal/transport/app_extended_heartbeat.go diff --git a/internal/newtelemetry/internal/transport/app_heartbeat.go b/internal/telemetry/internal/transport/app_heartbeat.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_heartbeat.go rename to internal/telemetry/internal/transport/app_heartbeat.go diff --git a/internal/newtelemetry/internal/transport/app_integration_change.go b/internal/telemetry/internal/transport/app_integration_change.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_integration_change.go rename to internal/telemetry/internal/transport/app_integration_change.go diff --git a/internal/newtelemetry/internal/transport/app_product_change.go b/internal/telemetry/internal/transport/app_product_change.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_product_change.go rename to internal/telemetry/internal/transport/app_product_change.go diff --git a/internal/newtelemetry/internal/transport/app_started.go b/internal/telemetry/internal/transport/app_started.go similarity index 100% rename from internal/newtelemetry/internal/transport/app_started.go rename to internal/telemetry/internal/transport/app_started.go diff --git a/internal/newtelemetry/internal/transport/body.go b/internal/telemetry/internal/transport/body.go similarity index 100% rename from internal/newtelemetry/internal/transport/body.go rename to internal/telemetry/internal/transport/body.go diff --git a/internal/newtelemetry/internal/transport/conf_key_value.go b/internal/telemetry/internal/transport/conf_key_value.go similarity index 100% rename from internal/newtelemetry/internal/transport/conf_key_value.go rename to internal/telemetry/internal/transport/conf_key_value.go diff --git a/internal/newtelemetry/internal/transport/distributions.go b/internal/telemetry/internal/transport/distributions.go similarity index 100% rename from internal/newtelemetry/internal/transport/distributions.go rename to internal/telemetry/internal/transport/distributions.go diff --git a/internal/newtelemetry/internal/transport/error.go b/internal/telemetry/internal/transport/error.go similarity index 100% rename from internal/newtelemetry/internal/transport/error.go rename to internal/telemetry/internal/transport/error.go diff --git a/internal/newtelemetry/internal/transport/generate-metrics.go b/internal/telemetry/internal/transport/generate-metrics.go similarity index 100% rename from internal/newtelemetry/internal/transport/generate-metrics.go rename to internal/telemetry/internal/transport/generate-metrics.go diff --git a/internal/newtelemetry/internal/transport/logs.go b/internal/telemetry/internal/transport/logs.go similarity index 100% rename from internal/newtelemetry/internal/transport/logs.go rename to internal/telemetry/internal/transport/logs.go diff --git a/internal/newtelemetry/internal/transport/message_batch.go b/internal/telemetry/internal/transport/message_batch.go similarity index 100% rename from internal/newtelemetry/internal/transport/message_batch.go rename to internal/telemetry/internal/transport/message_batch.go diff --git a/internal/newtelemetry/internal/transport/namespace.go b/internal/telemetry/internal/transport/namespace.go similarity index 100% rename from internal/newtelemetry/internal/transport/namespace.go rename to internal/telemetry/internal/transport/namespace.go diff --git a/internal/newtelemetry/internal/transport/origin.go b/internal/telemetry/internal/transport/origin.go similarity index 100% rename from internal/newtelemetry/internal/transport/origin.go rename to internal/telemetry/internal/transport/origin.go diff --git a/internal/newtelemetry/internal/transport/payload.go b/internal/telemetry/internal/transport/payload.go similarity index 100% rename from internal/newtelemetry/internal/transport/payload.go rename to internal/telemetry/internal/transport/payload.go diff --git a/internal/newtelemetry/internal/transport/requesttype.go b/internal/telemetry/internal/transport/requesttype.go similarity index 100% rename from internal/newtelemetry/internal/transport/requesttype.go rename to internal/telemetry/internal/transport/requesttype.go diff --git a/internal/newtelemetry/internal/writer.go b/internal/telemetry/internal/writer.go similarity index 99% rename from internal/newtelemetry/internal/writer.go rename to internal/telemetry/internal/writer.go index b26c476748..4ed5a91262 100644 --- a/internal/newtelemetry/internal/writer.go +++ b/internal/telemetry/internal/writer.go @@ -22,8 +22,8 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "gopkg.in/DataDog/dd-trace-go.v1/internal/hostname" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) diff --git a/internal/newtelemetry/internal/writer_test.go b/internal/telemetry/internal/writer_test.go similarity index 98% rename from internal/newtelemetry/internal/writer_test.go rename to internal/telemetry/internal/writer_test.go index bacf6accf7..33f772f029 100644 --- a/internal/newtelemetry/internal/writer_test.go +++ b/internal/telemetry/internal/writer_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) func TestNewWriter_ValidConfig(t *testing.T) { diff --git a/internal/newtelemetry/log/log.go b/internal/telemetry/log/log.go similarity index 65% rename from internal/newtelemetry/log/log.go rename to internal/telemetry/log/log.go index eabe43e9e1..1421f439d3 100644 --- a/internal/newtelemetry/log/log.go +++ b/internal/telemetry/log/log.go @@ -8,18 +8,18 @@ package log import ( "fmt" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) -func divideArgs(args []any) ([]newtelemetry.LogOption, []any) { +func divideArgs(args []any) ([]telemetry.LogOption, []any) { if len(args) == 0 { return nil, nil } - var options []newtelemetry.LogOption + var options []telemetry.LogOption var fmtArgs []any for _, arg := range args { - if opt, ok := arg.(newtelemetry.LogOption); ok { + if opt, ok := arg.(telemetry.LogOption); ok { options = append(options, opt) } else { fmtArgs = append(fmtArgs, arg) @@ -30,20 +30,20 @@ func divideArgs(args []any) ([]newtelemetry.LogOption, []any) { // Debug sends a telemetry payload with a debug log message to the backend. func Debug(format string, args ...any) { - log(newtelemetry.LogDebug, format, args) + log(telemetry.LogDebug, format, args) } // Warn sends a telemetry payload with a warning log message to the backend. func Warn(format string, args ...any) { - log(newtelemetry.LogWarn, format, args) + log(telemetry.LogWarn, format, args) } // Error sends a telemetry payload with an error log message to the backend. func Error(format string, args ...any) { - log(newtelemetry.LogError, format, args) + log(telemetry.LogError, format, args) } -func log(lvl newtelemetry.LogLevel, format string, args []any) { +func log(lvl telemetry.LogLevel, format string, args []any) { opts, fmtArgs := divideArgs(args) - newtelemetry.Log(lvl, fmt.Sprintf(format, fmtArgs...), opts...) + telemetry.Log(lvl, fmt.Sprintf(format, fmtArgs...), opts...) } diff --git a/internal/newtelemetry/logger.go b/internal/telemetry/logger.go similarity index 93% rename from internal/newtelemetry/logger.go rename to internal/telemetry/logger.go index b9a424d7d4..a316d3d452 100644 --- a/internal/newtelemetry/logger.go +++ b/internal/telemetry/logger.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "runtime" @@ -11,8 +11,8 @@ import ( "sync/atomic" "time" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // WithTags returns a LogOption that sets the tags for the telemetry log message. Tags are key-value pairs that are then diff --git a/internal/telemetry/message.go b/internal/telemetry/message.go deleted file mode 100644 index 7da6a9fa24..0000000000 --- a/internal/telemetry/message.go +++ /dev/null @@ -1,305 +0,0 @@ -// 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 2022 Datadog, Inc. - -package telemetry - -import ( - "bytes" - "fmt" - "net/http" -) - -// Request captures all necessary information for a telemetry event submission -type Request struct { - Body *Body - Header *http.Header - HTTPClient *http.Client - URL string -} - -// Body is the common high-level structure encapsulating a telemetry request body -type Body struct { - APIVersion string `json:"api_version"` - RequestType RequestType `json:"request_type"` - TracerTime int64 `json:"tracer_time"` - RuntimeID string `json:"runtime_id"` - SeqID int64 `json:"seq_id"` - Debug bool `json:"debug"` - Payload interface{} `json:"payload"` - Application Application `json:"application"` - Host Host `json:"host"` -} - -// RequestType determines how the Payload of a request should be handled -type RequestType string - -const ( - // RequestTypeAppStarted is the first message sent by the telemetry - // client, containing the configuration loaded at startup - RequestTypeAppStarted RequestType = "app-started" - // RequestTypeAppHeartbeat is sent periodically by the client to indicate - // that the app is still running - RequestTypeAppHeartbeat RequestType = "app-heartbeat" - // RequestTypeGenerateMetrics contains count, gauge, or rate metrics accumulated by the - // client, and is sent periodically along with the heartbeat - RequestTypeGenerateMetrics RequestType = "generate-metrics" - // RequestTypeDistributions is to send distribution type metrics accumulated by the - // client, and is sent periodically along with the heartbeat - RequestTypeDistributions RequestType = "distributions" - // RequestTypeAppClosing is sent when the telemetry client is stopped - RequestTypeAppClosing RequestType = "app-closing" - // RequestTypeDependenciesLoaded is sent if DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED - // is enabled. Sent when Start is called for the telemetry client. - RequestTypeDependenciesLoaded RequestType = "app-dependencies-loaded" - // RequestTypeAppClientConfigurationChange is sent if there are changes - // to the client library configuration - RequestTypeAppClientConfigurationChange RequestType = "app-client-configuration-change" - // RequestTypeAppProductChange is sent when products are enabled/disabled - RequestTypeAppProductChange RequestType = "app-product-change" - // RequestTypeAppIntegrationsChange is sent when the telemetry client starts - // with info on which integrations are used. - RequestTypeAppIntegrationsChange RequestType = "app-integrations-change" -) - -// Namespace describes an APM product to distinguish telemetry coming from -// different products used by the same application -type Namespace string - -const ( - // NamespaceGeneral is for general use - NamespaceGeneral Namespace = "general" - // NamespaceTracers is for distributed tracing - NamespaceTracers Namespace = "tracers" - // NamespaceProfilers is for continuous profiling - NamespaceProfilers Namespace = "profilers" - // NamespaceAppSec is for application security management - NamespaceAppSec Namespace = "appsec" - // NamespaceCiVisibility is for CI Visibility - NamespaceCiVisibility Namespace = "civisibility" -) - -// Application is identifying information about the app itself -type Application struct { - ServiceName string `json:"service_name"` - Env string `json:"env"` - ServiceVersion string `json:"service_version"` - TracerVersion string `json:"tracer_version"` - LanguageName string `json:"language_name"` - LanguageVersion string `json:"language_version"` - RuntimeName string `json:"runtime_name"` - RuntimeVersion string `json:"runtime_version"` - RuntimePatches string `json:"runtime_patches,omitempty"` -} - -// Host is identifying information about the host on which the app -// is running -type Host struct { - Hostname string `json:"hostname"` - OS string `json:"os"` - OSVersion string `json:"os_version,omitempty"` - Architecture string `json:"architecture"` - KernelName string `json:"kernel_name"` - KernelRelease string `json:"kernel_release"` - KernelVersion string `json:"kernel_version"` -} - -// AppStarted corresponds to the "app-started" request type -type AppStarted struct { - Configuration []Configuration `json:"configuration,omitempty"` - Products Products `json:"products,omitempty"` - AdditionalPayload []AdditionalPayload `json:"additional_payload,omitempty"` - Error Error `json:"error,omitempty"` - RemoteConfig *RemoteConfig `json:"remote_config,omitempty"` -} - -// IntegrationsChange corresponds to the app-integrations-change requesty type -type IntegrationsChange struct { - Integrations []Integration `json:"integrations"` -} - -// Integration is an integration that is configured to be traced automatically. -type Integration struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Version string `json:"version,omitempty"` - AutoEnabled bool `json:"auto_enabled,omitempty"` - Compatible bool `json:"compatible,omitempty"` - Error string `json:"error,omitempty"` -} - -// ConfigurationChange corresponds to the `AppClientConfigurationChange` event -// that contains information about configuration changes since the app-started event -type ConfigurationChange struct { - Configuration []Configuration `json:"configuration"` - RemoteConfig *RemoteConfig `json:"remote_config,omitempty"` -} - -type Origin int - -const ( - OriginDefault Origin = iota - OriginCode - OriginDDConfig - OriginEnvVar - OriginRemoteConfig -) - -func (o Origin) String() string { - switch o { - case OriginDefault: - return "default" - case OriginCode: - return "code" - case OriginDDConfig: - return "dd_config" - case OriginEnvVar: - return "env_var" - case OriginRemoteConfig: - return "remote_config" - default: - return fmt.Sprintf("unknown origin %d", o) - } -} - -func (o Origin) MarshalJSON() ([]byte, error) { - var b bytes.Buffer - b.WriteString(`"`) - b.WriteString(o.String()) - b.WriteString(`"`) - return b.Bytes(), nil -} - -// Configuration is a library-specific configuration value -// that should be initialized through StringConfig, IntConfig, FloatConfig, or BoolConfig -type Configuration struct { - Name string `json:"name"` - Value interface{} `json:"value"` - // origin is the source of the config. It is one of {default, env_var, code, dd_config, remote_config}. - Origin Origin `json:"origin"` - Error Error `json:"error"` - IsOverriden bool `json:"is_overridden"` -} - -// TODO: be able to pass in origin, error, isOverriden info to config -// constructors - -// StringConfig returns a Configuration struct with a string value -func StringConfig(key string, val string) Configuration { - return Configuration{Name: key, Value: val} -} - -// IntConfig returns a Configuration struct with a int value -func IntConfig(key string, val int) Configuration { - return Configuration{Name: key, Value: val} -} - -// FloatConfig returns a Configuration struct with a float value -func FloatConfig(key string, val float64) Configuration { - return Configuration{Name: key, Value: val} -} - -// BoolConfig returns a Configuration struct with a bool value -func BoolConfig(key string, val bool) Configuration { - return Configuration{Name: key, Value: val} -} - -// ProductsPayload is the top-level key for the app-product-change payload. -type ProductsPayload struct { - Products Products `json:"products"` -} - -// Products specifies information about available products. -type Products struct { - AppSec ProductDetails `json:"appsec,omitempty"` - Profiler ProductDetails `json:"profiler,omitempty"` -} - -// ProductDetails specifies details about a product. -type ProductDetails struct { - Enabled bool `json:"enabled"` - Version string `json:"version,omitempty"` - Error Error `json:"error,omitempty"` -} - -// Dependencies stores a list of dependencies -type Dependencies struct { - Dependencies []Dependency `json:"dependencies"` -} - -// Dependency is a Go module on which the application depends. This information -// can be accesed at run-time through the runtime/debug.ReadBuildInfo API. -type Dependency struct { - Name string `json:"name"` - Version string `json:"version"` -} - -// RemoteConfig contains information about remote-config -type RemoteConfig struct { - UserEnabled string `json:"user_enabled"` // whether the library has made a request to fetch remote-config - ConfigsRecieved bool `json:"configs_received"` // whether the library receives a valid config response - RcID string `json:"rc_id,omitempty"` - RcRevision string `json:"rc_revision,omitempty"` - RcVersion string `json:"rc_version,omitempty"` - Error Error `json:"error,omitempty"` -} - -// Error stores error information about various tracer events -type Error struct { - Code int `json:"code"` - Message string `json:"message"` -} - -// AdditionalPayload can be used to add extra information to the app-started -// event -type AdditionalPayload struct { - Name string `json:"name"` - Value interface{} `json:"value"` -} - -// Metrics corresponds to the "generate-metrics" request type -type Metrics struct { - Namespace Namespace `json:"namespace"` - Series []Series `json:"series"` -} - -// DistributionMetrics corresponds to the "distributions" request type -type DistributionMetrics struct { - Namespace Namespace `json:"namespace"` - Series []DistributionSeries `json:"series"` -} - -// Series is a sequence of observations for a single named metric. -// The `Points` field will store a timestamp and value. -type Series struct { - Metric string `json:"metric"` - Points [][2]float64 `json:"points"` - // Interval is required for gauge and rate metrics - Interval int `json:"interval,omitempty"` - Type string `json:"type,omitempty"` - Tags []string `json:"tags"` - // Common distinguishes metrics which are cross-language vs. - // language-specific. - // - // NOTE: If this field isn't present in the request, the API assumes - // the metric is common. So we can't "omitempty" even though the - // field is technically optional. - Common bool `json:"common"` - Namespace string `json:"namespace"` -} - -// DistributionSeries is a sequence of observations for a distribution metric. -// Unlike `Series`, DistributionSeries does not store timestamps in `Points` -type DistributionSeries struct { - Metric string `json:"metric"` - Points []float64 `json:"points"` - Tags []string `json:"tags"` - // Common distinguishes metrics which are cross-language vs. - // language-specific. - // - // NOTE: If this field isn't present in the request, the API assumes - // the metric is common. So we can't "omitempty" even though the - // field is technically optional. - Common bool `json:"common"` -} diff --git a/internal/newtelemetry/metrichandle.go b/internal/telemetry/metrichandle.go similarity index 95% rename from internal/newtelemetry/metrichandle.go rename to internal/telemetry/metrichandle.go index a191c953f2..fe91398d03 100644 --- a/internal/newtelemetry/metrichandle.go +++ b/internal/telemetry/metrichandle.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "math" @@ -11,7 +11,7 @@ import ( "sync/atomic" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" ) // noopMetricHandle is a no-op implementation of a metric handle. diff --git a/internal/newtelemetry/metrics.go b/internal/telemetry/metrics.go similarity index 96% rename from internal/newtelemetry/metrics.go rename to internal/telemetry/metrics.go index 0ab4c28cc0..02ec013e54 100644 --- a/internal/newtelemetry/metrics.go +++ b/internal/telemetry/metrics.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "fmt" @@ -14,9 +14,9 @@ import ( "time" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/knownmetrics" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/knownmetrics" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) // metricKey is used as a key in the metrics store hash map. diff --git a/internal/telemetry/option.go b/internal/telemetry/option.go deleted file mode 100644 index 8320d1a5fb..0000000000 --- a/internal/telemetry/option.go +++ /dev/null @@ -1,150 +0,0 @@ -// 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 2023 Datadog, Inc. - -package telemetry - -import ( - "errors" - "net/http" - "net/url" - "os" - "path/filepath" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" -) - -// An Option is used to configure the telemetry client's settings -type Option func(*client) - -// ApplyOps sets various fields of the client. -// To be called before starting any product. -func (c *client) ApplyOps(opts ...Option) { - c.mu.Lock() - defer c.mu.Unlock() - for _, opt := range opts { - opt(c) - } -} - -// WithNamespace sets name as the telemetry client's namespace (tracer, profiler, appsec) -func WithNamespace(name Namespace) Option { - return func(client *client) { - client.Namespace = name - } -} - -// WithEnv sets the app specific environment for the telemetry client -func WithEnv(env string) Option { - return func(client *client) { - client.Env = env - } -} - -// WithService sets the app specific service for the telemetry client -func WithService(service string) Option { - return func(client *client) { - client.Service = service - } -} - -// WithVersion sets the app specific version for the telemetry client -func WithVersion(version string) Option { - return func(client *client) { - client.Version = version - } -} - -// WithHTTPClient specifies the http client for the telemetry client -func WithHTTPClient(httpClient *http.Client) Option { - return func(client *client) { - client.Client = httpClient - } -} - -// SyncFlushOnStop forces a sync flush on client stop -func SyncFlushOnStop() Option { - return func(client *client) { - client.syncFlushOnStop = true - } -} - -func defaultAPIKey() string { - return os.Getenv("DD_API_KEY") -} - -// WithAPIKey sets the DD API KEY for the telemetry client -func WithAPIKey(v string) Option { - return func(client *client) { - client.APIKey = v - } -} - -// WithURL sets the URL for where telemetry information is flushed to. -// For the URL, uploading through agent goes through -// -// ${AGENT_URL}/telemetry/proxy/api/v2/apmtelemetry -// -// for agentless: -// -// https://instrumentation-telemetry-intake.datadoghq.com/api/v2/apmtelemetry -// -// with an API key -func WithURL(agentless bool, agentURL string) Option { - return func(client *client) { - if agentless { - client.URL = getAgentlessURL() - } else { - u, err := url.Parse(agentURL) - if err == nil { - u.Path = "/telemetry/proxy/api/v2/apmtelemetry" - client.URL = u.String() - } else { - log("Agent URL %s is invalid, switching to agentless telemetry endpoint", agentURL) - client.URL = getAgentlessURL() - } - } - } -} - -func getAgentlessURL() string { - agentlessEndpointLock.RLock() - defer agentlessEndpointLock.RUnlock() - return agentlessURL -} - -// configEnvFallback returns the value of environment variable with the -// given key if def == "" -func configEnvFallback(key, def string) string { - if def != "" { - return def - } - return os.Getenv(key) -} - -// fallbackOps populates missing fields of the client with environment variables -// or default values. -func (c *client) fallbackOps() error { - if c.Client == nil { - WithHTTPClient(defaultHTTPClient)(c) - } - if len(c.APIKey) == 0 && c.URL == getAgentlessURL() { - WithAPIKey(defaultAPIKey())(c) - if c.APIKey == "" { - return errors.New("agentless is turned on, but valid DD API key was not found") - } - } - c.Service = configEnvFallback("DD_SERVICE", c.Service) - if len(c.Service) == 0 { - if name := globalconfig.ServiceName(); len(name) != 0 { - c.Service = name - } else { - c.Service = filepath.Base(os.Args[0]) - - } - } - c.Env = configEnvFallback("DD_ENV", c.Env) - c.Version = configEnvFallback("DD_VERSION", c.Version) - return nil -} diff --git a/internal/newtelemetry/product.go b/internal/telemetry/product.go similarity index 90% rename from internal/newtelemetry/product.go rename to internal/telemetry/product.go index 84c53e8242..6f98876c6d 100644 --- a/internal/newtelemetry/product.go +++ b/internal/telemetry/product.go @@ -3,12 +3,12 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2025 Datadog, Inc. -package newtelemetry +package telemetry import ( "sync" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type products struct { diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go deleted file mode 100644 index 994dee89a8..0000000000 --- a/internal/telemetry/telemetry.go +++ /dev/null @@ -1,126 +0,0 @@ -// 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 2023 Datadog, Inc. - -// Package telemetry implements a client for sending telemetry information to -// Datadog regarding usage of an APM library such as tracing or profiling. -package telemetry - -import ( - "time" -) - -// ProductChange signals that the product has changed with some configuration -// information. It will start the telemetry client if it is not already started, -// unless enabled is false (in which case the call does nothing). ProductChange -// assumes that the telemetry client has been configured already by the caller -// using the ApplyOps method. -// If the client is already started, it will send any necessary -// app-product-change events to indicate whether the product is enabled, as well -// as an app-client-configuration-change event in case any new configuration -// information is available. -func (c *client) ProductChange(namespace Namespace, enabled bool, configuration []Configuration) { - c.mu.Lock() - defer c.mu.Unlock() - if !c.started { - if !enabled { - // Namespace is not enabled & telemetry isn't started, won't start it now. - return - } - c.start(configuration, namespace, true) - return - } - - var cfg []Configuration - cfg = append(cfg, c.globalAppConfig...) - cfg = append(cfg, configuration...) - c.configChange(cfg) - - switch namespace { - case NamespaceTracers, NamespaceProfilers, NamespaceAppSec, NamespaceCiVisibility: - c.productChange(namespace, enabled) - default: - log("unknown product namespace %q provided to ProductChange", namespace) - } -} - -// ConfigChange is a thread-safe method to enqueue an app-client-configuration-change event. -func (c *client) ConfigChange(configuration []Configuration) { - c.mu.Lock() - defer c.mu.Unlock() - c.configChange(configuration) -} - -// configChange enqueues an app-client-configuration-change event to be flushed. -// Must be called with c.mu locked. -func (c *client) configChange(configuration []Configuration) { - if !c.started { - log("attempted to send config change event, but telemetry client has not started") - return - } - if len(configuration) > 0 { - configChange := new(ConfigurationChange) - configChange.Configuration = configuration - configReq := c.newRequest(RequestTypeAppClientConfigurationChange) - configReq.Body.Payload = configChange - c.scheduleSubmit(configReq) - } -} - -// productChange enqueues an app-product-change event that signals a product has been `enabled`. -// Must be called with c.mu locked. An app-product-change event with enabled=true indicates -// that a certain product has been used for this application. -func (c *client) productChange(namespace Namespace, enabled bool) { - if !c.started { - log("attempted to send product change event, but telemetry client has not started") - return - } - products := new(ProductsPayload) - switch namespace { - case NamespaceAppSec: - products.Products.AppSec = ProductDetails{Enabled: enabled} - case NamespaceProfilers: - products.Products.Profiler = ProductDetails{Enabled: enabled} - case NamespaceTracers, NamespaceCiVisibility: - // Nothing to do - default: - log("unknown product namespace: %q. The app-product-change telemetry event will not send", namespace) - return - } - productReq := c.newRequest(RequestTypeAppProductChange) - productReq.Body.Payload = products - c.scheduleSubmit(productReq) -} - -// Integrations returns which integrations are tracked by telemetry. -func Integrations() []Integration { - contrib.Lock() - defer contrib.Unlock() - return contribPackages -} - -// LoadIntegration notifies telemetry that an integration is being used. -func LoadIntegration(name string) { - if Disabled() { - return - } - contrib.Lock() - defer contrib.Unlock() - contribPackages = append(contribPackages, Integration{Name: name, Enabled: true}) -} - -// Time is used to track a distribution metric that measures the time (ms) -// of some portion of code. It returns a function that should be called when -// the desired code finishes executing. -// For example, by adding: -// defer Time(namespace, "init_time", nil, true)() -// at the beginning of the tracer Start function, the tracer start time is measured -// and stored as a metric to be flushed by the global telemetry client. -func Time(namespace Namespace, name string, tags []string, common bool) (finish func()) { - start := time.Now() - return func() { - elapsed := time.Since(start) - GlobalClient.Record(namespace, MetricKindDist, name, float64(elapsed.Milliseconds()), tags, common) - } -} diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go deleted file mode 100644 index 87d40ff02a..0000000000 --- a/internal/telemetry/telemetry_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// 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 2023 Datadog, Inc. - -// Package telemetry implements a client for sending telemetry information to -// Datadog regarding usage of an APM library such as tracing or profiling. - -package telemetry - -import ( - "context" - "net/http" - "net/http/httptest" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestProductEnabled(t *testing.T) { - client := new(client) - client.start(nil, NamespaceTracers, true) - client.productChange(NamespaceProfilers, true) - // should just contain app-product-change - require.Len(t, client.requests, 1) - body := client.requests[0].Body - - assert.Equal(t, RequestTypeAppProductChange, body.RequestType) - var productsPayload = body.Payload.(*ProductsPayload) - assert.True(t, productsPayload.Products.Profiler.Enabled) -} - -func TestConfigChange(t *testing.T) { - client := new(client) - client.start(nil, NamespaceTracers, true) - client.configChange([]Configuration{BoolConfig("delta_profiles", true)}) - require.Len(t, client.requests, 1) - - body := client.requests[0].Body - assert.Equal(t, RequestTypeAppClientConfigurationChange, body.RequestType) - var configPayload = client.requests[0].Body.Payload.(*ConfigurationChange) - require.Len(t, configPayload.Configuration, 1) - - Check(t, configPayload.Configuration, "delta_profiles", true) -} - -// mockServer initializes a server that expects a strict amount of telemetry events. It saves these -// events in a slice until the expected number of events is reached. -// the `genTelemetry` argument accepts a function that should generate the expected telemetry events via calls to the global client -// the `expectedHits` argument specifies the number of telemetry events the server should expect. -func mockServer(ctx context.Context, t *testing.T, expectedHits int, genTelemetry func(), exclude ...RequestType) (waitForEvents func() []RequestType, cleanup func()) { - messages := make([]RequestType, expectedHits) - hits := 0 - done := make(chan struct{}) - mu := sync.Mutex{} - excludeEvent := make(map[RequestType]struct{}) - for _, event := range exclude { - excludeEvent[event] = struct{}{} - } - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/telemetry/proxy/api/v2/apmtelemetry" { - return - } - rType := RequestType(r.Header.Get("DD-Telemetry-Request-Type")) - if _, ok := excludeEvent[rType]; ok { - return - } - mu.Lock() - defer mu.Unlock() - if hits == expectedHits { - t.Fatalf("too many telemetry messages (expected %d)", expectedHits) - } - messages[hits] = rType - if hits++; hits == expectedHits { - done <- struct{}{} - } - })) - GlobalClient.ApplyOps(WithURL(false, server.URL)) - - return func() []RequestType { - genTelemetry() - select { - case <-ctx.Done(): - t.Fatal("TestProductChange timed out") - case <-done: - } - return messages - }, func() { - server.Close() - GlobalClient.Stop() - } -} - -func TestProductChange(t *testing.T) { - // this test is meant to ensure that a given sequence of ProductStart/ProductStop calls - // emits the expected telemetry events. - t.Setenv("DD_TELEMETRY_HEARTBEAT_INTERVAL", "1") - t.Setenv("DD_TRACE_STARTUP_LOGS", "0") - tests := []struct { - name string - wantedMessages []RequestType - genTelemetry func() - }{ - { - name: "tracer start, profiler start", - wantedMessages: []RequestType{RequestTypeAppStarted, RequestTypeDependenciesLoaded, RequestTypeAppClientConfigurationChange, RequestTypeAppProductChange}, - genTelemetry: func() { - GlobalClient.ProductChange(NamespaceTracers, true, nil) - GlobalClient.ProductChange(NamespaceProfilers, true, []Configuration{{Name: "key", Value: "value"}}) - }, - }, - /* This case is flaky (see #2688) - { - name: "profiler start, tracer start", - wantedMessages: []RequestType{RequestTypeAppStarted, RequestTypeDependenciesLoaded, RequestTypeAppClientConfigurationChange}, - genTelemetry: func() { - GlobalClient.ProductChange(NamespaceProfilers, true, nil) - GlobalClient.ProductChange(NamespaceTracers, true, []Configuration{{Name: "key", Value: "value"}}) - }, - }, - */ - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - telemetryClient := new(client) - defer MockGlobalClient(telemetryClient)() - excludedEvents := []RequestType{RequestTypeAppHeartbeat, RequestTypeGenerateMetrics, RequestTypeAppClosing} - waitForEvents, cleanup := mockServer(ctx, t, len(test.wantedMessages), test.genTelemetry, excludedEvents...) - defer cleanup() - messages := waitForEvents() - for i := range messages { - assert.Equal(t, test.wantedMessages[i], messages[i]) - } - }) - } -} - -// Test that globally registered app config is sent in telemetry requests including the configuration state. -func TestRegisterAppConfig(t *testing.T) { - client := new(client) - client.RegisterAppConfig("key1", "val1", OriginDefault) - - // Test that globally registered app config is sent in app-started payloads - client.start([]Configuration{{Name: "key2", Value: "val2", Origin: OriginDDConfig}}, NamespaceTracers, false) - - req := client.requests[0].Body - require.Equal(t, RequestTypeAppStarted, req.RequestType) - appStarted := req.Payload.(*AppStarted) - cfg := appStarted.Configuration - require.Contains(t, cfg, Configuration{Name: "key1", Value: "val1", Origin: OriginDefault}) - require.Contains(t, cfg, Configuration{Name: "key2", Value: "val2", Origin: OriginDDConfig}) - - // Test that globally registered app config is sent in app-client-configuration-change payloads - client.ProductChange(NamespaceTracers, true, []Configuration{{Name: "key3", Value: "val3", Origin: OriginCode}}) - - req = client.requests[2].Body - require.Equal(t, RequestTypeAppClientConfigurationChange, req.RequestType) - appConfigChange := req.Payload.(*ConfigurationChange) - cfg = appConfigChange.Configuration - require.Len(t, cfg, 2) - require.Contains(t, cfg, Configuration{Name: "key1", Value: "val1", Origin: OriginDefault}) - require.Contains(t, cfg, Configuration{Name: "key3", Value: "val3", Origin: OriginCode}) -} diff --git a/internal/newtelemetry/telemetrytest/globalclient_test.go b/internal/telemetry/telemetrytest/globalclient_test.go similarity index 51% rename from internal/newtelemetry/telemetrytest/globalclient_test.go rename to internal/telemetry/telemetrytest/globalclient_test.go index 1f6b86983b..779449bbf0 100644 --- a/internal/newtelemetry/telemetrytest/globalclient_test.go +++ b/internal/telemetry/telemetrytest/globalclient_test.go @@ -12,76 +12,76 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) func TestGlobalClient(t *testing.T) { t.Run("config", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.RegisterAppConfig("key", "value", newtelemetry.OriginCode) + telemetry.RegisterAppConfig("key", "value", telemetry.OriginCode) assert.Len(t, recorder.Configuration, 1) assert.Equal(t, "key", recorder.Configuration[0].Name) assert.Equal(t, "value", recorder.Configuration[0].Value) - assert.Equal(t, newtelemetry.OriginCode, recorder.Configuration[0].Origin) + assert.Equal(t, telemetry.OriginCode, recorder.Configuration[0].Origin) }) t.Run("configs", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.RegisterAppConfigs(newtelemetry.Configuration{Name: "key", Value: "value", Origin: newtelemetry.OriginCode}, newtelemetry.Configuration{Name: "key2", Value: "value2", Origin: newtelemetry.OriginRemoteConfig}) + telemetry.RegisterAppConfigs(telemetry.Configuration{Name: "key", Value: "value", Origin: telemetry.OriginCode}, telemetry.Configuration{Name: "key2", Value: "value2", Origin: telemetry.OriginRemoteConfig}) assert.Len(t, recorder.Configuration, 2) assert.Equal(t, "key", recorder.Configuration[0].Name) assert.Equal(t, "value", recorder.Configuration[0].Value) - assert.Equal(t, newtelemetry.OriginCode, recorder.Configuration[0].Origin) + assert.Equal(t, telemetry.OriginCode, recorder.Configuration[0].Origin) assert.Equal(t, "key2", recorder.Configuration[1].Name) assert.Equal(t, "value2", recorder.Configuration[1].Value) - assert.Equal(t, newtelemetry.OriginRemoteConfig, recorder.Configuration[1].Origin) + assert.Equal(t, telemetry.OriginRemoteConfig, recorder.Configuration[1].Origin) }) t.Run("app-stop", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.StopApp() + telemetry.StopApp() assert.True(t, recorder.Stopped) }) t.Run("product-start", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.ProductStarted(newtelemetry.NamespaceAppSec) + telemetry.ProductStarted(telemetry.NamespaceAppSec) assert.Len(t, recorder.Products, 1) - assert.True(t, recorder.Products[newtelemetry.NamespaceAppSec]) + assert.True(t, recorder.Products[telemetry.NamespaceAppSec]) }) t.Run("product-stopped", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.ProductStopped(newtelemetry.NamespaceAppSec) + telemetry.ProductStopped(telemetry.NamespaceAppSec) assert.Len(t, recorder.Products, 1) - assert.False(t, recorder.Products[newtelemetry.NamespaceAppSec]) + assert.False(t, recorder.Products[telemetry.NamespaceAppSec]) }) t.Run("integration-loaded", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.LoadIntegration("test-integration") + telemetry.LoadIntegration("test-integration") assert.Len(t, recorder.Integrations, 1) assert.Equal(t, "test-integration", recorder.Integrations[0].Name) }) t.Run("mark-integration-as-loaded", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.MarkIntegrationAsLoaded(newtelemetry.Integration{Name: "test-integration", Version: "1.0.0"}) + telemetry.MarkIntegrationAsLoaded(telemetry.Integration{Name: "test-integration", Version: "1.0.0"}) assert.Len(t, recorder.Integrations, 1) assert.Equal(t, "test-integration", recorder.Integrations[0].Name) assert.Equal(t, "1.0.0", recorder.Integrations[0].Version) @@ -89,42 +89,42 @@ func TestGlobalClient(t *testing.T) { t.Run("count", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.Count(newtelemetry.NamespaceTracers, "init_time", nil).Submit(1) + telemetry.Count(telemetry.NamespaceTracers, "init_time", nil).Submit(1) assert.Len(t, recorder.Metrics, 1) - require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.CountMetric)}) - assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.CountMetric)}].Get()) + require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.CountMetric)}) + assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.CountMetric)}].Get()) }) t.Run("gauge", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.Gauge(newtelemetry.NamespaceTracers, "init_time", nil).Submit(1) + telemetry.Gauge(telemetry.NamespaceTracers, "init_time", nil).Submit(1) assert.Len(t, recorder.Metrics, 1) - require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.GaugeMetric)}) - assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.GaugeMetric)}].Get()) + require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.GaugeMetric)}) + assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.GaugeMetric)}].Get()) }) t.Run("rate", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.Rate(newtelemetry.NamespaceTracers, "init_time", nil).Submit(1) + telemetry.Rate(telemetry.NamespaceTracers, "init_time", nil).Submit(1) assert.Len(t, recorder.Metrics, 1) - require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.RateMetric)}) - assert.False(t, math.IsNaN(recorder.Metrics[MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceTracers, Kind: string(transport.RateMetric)}].Get())) + require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.RateMetric)}) + assert.False(t, math.IsNaN(recorder.Metrics[MetricKey{Name: "init_time", Namespace: telemetry.NamespaceTracers, Kind: string(transport.RateMetric)}].Get())) }) t.Run("distribution", func(t *testing.T) { recorder := new(RecordClient) - defer newtelemetry.MockClient(recorder)() + defer telemetry.MockClient(recorder)() - newtelemetry.Distribution(newtelemetry.NamespaceGeneral, "init_time", nil).Submit(1) + telemetry.Distribution(telemetry.NamespaceGeneral, "init_time", nil).Submit(1) assert.Len(t, recorder.Metrics, 1) - require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceGeneral, Kind: string(transport.DistMetric)}) - assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: newtelemetry.NamespaceGeneral, Kind: string(transport.DistMetric)}].Get()) + require.Contains(t, recorder.Metrics, MetricKey{Name: "init_time", Namespace: telemetry.NamespaceGeneral, Kind: string(transport.DistMetric)}) + assert.Equal(t, 1.0, recorder.Metrics[MetricKey{Name: "init_time", Namespace: telemetry.NamespaceGeneral, Kind: string(transport.DistMetric)}].Get()) }) } diff --git a/internal/telemetry/telemetrytest/mock.go b/internal/telemetry/telemetrytest/mock.go new file mode 100644 index 0000000000..2991101f37 --- /dev/null +++ b/internal/telemetry/telemetrytest/mock.go @@ -0,0 +1,93 @@ +// 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 telemetrytest provides a mock implementation of the telemetry client for testing purposes +package telemetrytest + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + + "github.com/stretchr/testify/mock" +) + +// MockClient implements Client and is used for testing purposes outside the telemetry package, +// e.g. the tracer and profiler. +type MockClient struct { + mock.Mock +} + +func (m *MockClient) Close() error { + return nil +} + +type MockMetricHandle struct { + mock.Mock +} + +func (m *MockMetricHandle) Submit(value float64) { + m.Called(value) +} + +func (m *MockMetricHandle) Get() float64 { + return m.Called().Get(0).(float64) +} + +func (m *MockClient) Count(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { + return m.Called(namespace, name, tags).Get(0).(telemetry.MetricHandle) +} + +func (m *MockClient) Rate(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { + return m.Called(namespace, name, tags).Get(0).(telemetry.MetricHandle) +} + +func (m *MockClient) Gauge(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { + return m.Called(namespace, name, tags).Get(0).(telemetry.MetricHandle) +} + +func (m *MockClient) Distribution(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { + return m.Called(namespace, name, tags).Get(0).(telemetry.MetricHandle) +} + +func (m *MockClient) Log(level telemetry.LogLevel, text string, options ...telemetry.LogOption) { + m.Called(level, text, options) +} + +func (m *MockClient) ProductStarted(product telemetry.Namespace) { + m.Called(product) +} + +func (m *MockClient) ProductStopped(product telemetry.Namespace) { + m.Called(product) +} + +func (m *MockClient) ProductStartError(product telemetry.Namespace, err error) { + m.Called(product, err) +} + +func (m *MockClient) RegisterAppConfig(key string, value any, origin telemetry.Origin) { + m.Called(key, value, origin) +} + +func (m *MockClient) RegisterAppConfigs(kvs ...telemetry.Configuration) { + m.Called(kvs) +} + +func (m *MockClient) MarkIntegrationAsLoaded(integration telemetry.Integration) { + m.Called(integration) +} + +func (m *MockClient) Flush() { + m.Called() +} + +func (m *MockClient) AppStart() { + m.Called() +} + +func (m *MockClient) AppStop() { + m.Called() +} + +var _ telemetry.Client = (*MockClient)(nil) diff --git a/internal/newtelemetry/telemetrytest/record.go b/internal/telemetry/telemetrytest/record.go similarity index 63% rename from internal/newtelemetry/telemetrytest/record.go rename to internal/telemetry/telemetrytest/record.go index b73d398f54..daee5f3e4a 100644 --- a/internal/newtelemetry/telemetrytest/record.go +++ b/internal/telemetry/telemetrytest/record.go @@ -12,12 +12,12 @@ import ( "testing" "time" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry" - "gopkg.in/DataDog/dd-trace-go.v1/internal/newtelemetry/internal/transport" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry/internal/transport" ) type MetricKey struct { - Namespace newtelemetry.Namespace + Namespace telemetry.Namespace Name string Tags string Kind string @@ -27,10 +27,10 @@ type RecordClient struct { mu sync.Mutex Started bool Stopped bool - Configuration []newtelemetry.Configuration - Logs map[newtelemetry.LogLevel]string - Integrations []newtelemetry.Integration - Products map[newtelemetry.Namespace]bool + Configuration []telemetry.Configuration + Logs map[telemetry.LogLevel]string + Integrations []telemetry.Integration + Products map[telemetry.Namespace]bool Metrics map[MetricKey]*RecordMetricHandle } @@ -62,7 +62,7 @@ func (m *RecordMetricHandle) Get() float64 { return m.get(m) } -func (r *RecordClient) metric(kind string, namespace newtelemetry.Namespace, name string, tags []string, submit func(handle *RecordMetricHandle, value float64), get func(handle *RecordMetricHandle) float64) *RecordMetricHandle { +func (r *RecordClient) metric(kind string, namespace telemetry.Namespace, name string, tags []string, submit func(handle *RecordMetricHandle, value float64), get func(handle *RecordMetricHandle) float64) *RecordMetricHandle { r.mu.Lock() defer r.mu.Unlock() if r.Metrics == nil { @@ -76,7 +76,7 @@ func (r *RecordClient) metric(kind string, namespace newtelemetry.Namespace, nam return r.Metrics[key] } -func (r *RecordClient) Count(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { +func (r *RecordClient) Count(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { return r.metric(string(transport.CountMetric), namespace, name, tags, func(handle *RecordMetricHandle, value float64) { handle.count += value }, func(handle *RecordMetricHandle) float64 { @@ -84,7 +84,7 @@ func (r *RecordClient) Count(namespace newtelemetry.Namespace, name string, tags }) } -func (r *RecordClient) Rate(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { +func (r *RecordClient) Rate(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { handle := r.metric(string(transport.RateMetric), namespace, name, tags, func(handle *RecordMetricHandle, value float64) { handle.count += value handle.rate = float64(handle.count) / time.Since(handle.rateStart).Seconds() @@ -96,7 +96,7 @@ func (r *RecordClient) Rate(namespace newtelemetry.Namespace, name string, tags return handle } -func (r *RecordClient) Gauge(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { +func (r *RecordClient) Gauge(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { return r.metric(string(transport.GaugeMetric), namespace, name, tags, func(handle *RecordMetricHandle, value float64) { handle.gauge = value }, func(handle *RecordMetricHandle) float64 { @@ -104,7 +104,7 @@ func (r *RecordClient) Gauge(namespace newtelemetry.Namespace, name string, tags }) } -func (r *RecordClient) Distribution(namespace newtelemetry.Namespace, name string, tags []string) newtelemetry.MetricHandle { +func (r *RecordClient) Distribution(namespace telemetry.Namespace, name string, tags []string) telemetry.MetricHandle { return r.metric(string(transport.DistMetric), namespace, name, tags, func(handle *RecordMetricHandle, value float64) { handle.distrib = append(handle.distrib, value) }, func(handle *RecordMetricHandle) float64 { @@ -116,59 +116,60 @@ func (r *RecordClient) Distribution(namespace newtelemetry.Namespace, name strin }) } -func (r *RecordClient) Log(level newtelemetry.LogLevel, text string, _ ...newtelemetry.LogOption) { +func (r *RecordClient) Log(level telemetry.LogLevel, text string, _ ...telemetry.LogOption) { r.mu.Lock() defer r.mu.Unlock() if r.Logs == nil { - r.Logs = make(map[newtelemetry.LogLevel]string) + r.Logs = make(map[telemetry.LogLevel]string) } r.Logs[level] = text } -func (r *RecordClient) ProductStarted(product newtelemetry.Namespace) { +func (r *RecordClient) ProductStarted(product telemetry.Namespace) { r.mu.Lock() defer r.mu.Unlock() if r.Products == nil { - r.Products = make(map[newtelemetry.Namespace]bool) + r.Products = make(map[telemetry.Namespace]bool) } r.Products[product] = true } -func (r *RecordClient) ProductStopped(product newtelemetry.Namespace) { +func (r *RecordClient) ProductStopped(product telemetry.Namespace) { r.mu.Lock() defer r.mu.Unlock() if r.Products == nil { - r.Products = make(map[newtelemetry.Namespace]bool) + r.Products = make(map[telemetry.Namespace]bool) } r.Products[product] = false } -func (r *RecordClient) ProductStartError(product newtelemetry.Namespace, _ error) { +func (r *RecordClient) ProductStartError(product telemetry.Namespace, _ error) { r.mu.Lock() defer r.mu.Unlock() if r.Products == nil { - r.Products = make(map[newtelemetry.Namespace]bool) + r.Products = make(map[telemetry.Namespace]bool) } r.Products[product] = false } -func (r *RecordClient) RegisterAppConfig(key string, value any, origin newtelemetry.Origin) { - r.mu.Lock() - defer r.mu.Unlock() - r.Configuration = append(r.Configuration, newtelemetry.Configuration{Name: key, Value: value, Origin: origin}) +func (r *RecordClient) RegisterAppConfig(key string, value any, origin telemetry.Origin) { + r.RegisterAppConfigs(telemetry.Configuration{Name: key, Value: value, Origin: origin}) } -func (r *RecordClient) RegisterAppConfigs(kvs ...newtelemetry.Configuration) { +func (r *RecordClient) RegisterAppConfigs(kvs ...telemetry.Configuration) { r.mu.Lock() defer r.mu.Unlock() + for i := range kvs { + kvs[i].Value = telemetry.SanitizeConfigValue(kvs[i].Value) + } r.Configuration = append(r.Configuration, kvs...) } -func (r *RecordClient) MarkIntegrationAsLoaded(integration newtelemetry.Integration) { +func (r *RecordClient) MarkIntegrationAsLoaded(integration telemetry.Integration) { r.mu.Lock() defer r.mu.Unlock() r.Integrations = append(r.Integrations, integration) @@ -188,7 +189,8 @@ func (r *RecordClient) AppStop() { r.Stopped = true } -func CheckConfig(t *testing.T, cfgs []newtelemetry.Configuration, key string, value any) { +func CheckConfig(t *testing.T, cfgs []telemetry.Configuration, key string, value any) { + t.Helper() for _, c := range cfgs { if c.Name == key && reflect.DeepEqual(c.Value, value) { return diff --git a/internal/telemetry/telemetrytest/telemetrytest.go b/internal/telemetry/telemetrytest/telemetrytest.go deleted file mode 100644 index 0348d39454..0000000000 --- a/internal/telemetry/telemetrytest/telemetrytest.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 2022 Datadog, Inc. - -// Package telemetrytest provides a mock implementation of the telemetry client for testing purposes -package telemetrytest - -import ( - "sync" - - "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" - - "github.com/stretchr/testify/mock" -) - -// MockClient implements Client and is used for testing purposes outside the telemetry package, -// e.g. the tracer and profiler. -type MockClient struct { - mock.Mock - mu sync.Mutex - Started bool - Configuration []telemetry.Configuration - Integrations []string - ProfilerEnabled bool - AsmEnabled bool - Metrics map[telemetry.Namespace]map[string]float64 -} - -func (c *MockClient) RegisterAppConfig(name string, val interface{}, origin telemetry.Origin) { - _ = c.Called(name, val, origin) -} - -// ProductChange starts and adds configuration data to the mock client. -func (c *MockClient) ProductChange(namespace telemetry.Namespace, enabled bool, configuration []telemetry.Configuration) { - c.mu.Lock() - defer c.mu.Unlock() - c.Started = true - c.Configuration = append(c.Configuration, configuration...) - if len(c.Metrics) == 0 { - c.Metrics = make(map[telemetry.Namespace]map[string]float64) - } - c.productChange(namespace, enabled) -} - -// ProductStop signals a product has stopped and disables that product in the mock client. -// ProductStop is NOOP for the tracer namespace, since the tracer is not considered a product. -func (c *MockClient) ProductStop(namespace telemetry.Namespace) { - c.mu.Lock() - defer c.mu.Unlock() - if namespace == telemetry.NamespaceTracers { - return - } - c.productChange(namespace, false) -} - -// ProductChange signals that a certain product is enabled or disabled for the mock client. -func (c *MockClient) productChange(namespace telemetry.Namespace, enabled bool) { - switch namespace { - case telemetry.NamespaceAppSec: - c.AsmEnabled = enabled - case telemetry.NamespaceProfilers: - c.ProfilerEnabled = enabled - case telemetry.NamespaceTracers: - return - default: - panic("invalid product namespace") - } -} - -// Record stores the value for the given metric. It is currently mocked for `Gauge` and `Distribution` metric types. -func (c *MockClient) Record(ns telemetry.Namespace, _ telemetry.MetricKind, name string, val float64, tags []string, common bool) { - c.On("Gauge", ns, name, val, tags, common).Return() - c.On("Record", ns, name, val, tags, common).Return() - _ = c.Called(ns, name, val, tags, common) - - c.mu.Lock() - defer c.mu.Unlock() - // record the val for tests that assert based on the value - if _, ok := c.Metrics[ns]; !ok { - c.Metrics[ns] = map[string]float64{} - } - c.Metrics[ns][name] = val -} - -// Count counts the value for the given metric -func (c *MockClient) Count(ns telemetry.Namespace, name string, val float64, tags []string, common bool) { - c.On("Count", ns, name, val, tags, common).Return() - _ = c.Called(ns, name, val, tags, common) -} - -// Stop is NOOP for the mock client. -func (c *MockClient) Stop() { -} - -// ApplyOps is used to record the number of ApplyOps method calls. -func (c *MockClient) ApplyOps(args ...telemetry.Option) { - c.On("ApplyOps", args).Return() - _ = c.Called(args) -} - -// ConfigChange is a mock for the ConfigChange method. -func (c *MockClient) ConfigChange(args []telemetry.Configuration) { - c.On("ConfigChange", args).Return() - _ = c.Called(args) -} diff --git a/internal/telemetry/utils.go b/internal/telemetry/utils.go deleted file mode 100644 index d2e454ecc1..0000000000 --- a/internal/telemetry/utils.go +++ /dev/null @@ -1,101 +0,0 @@ -// 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 2023 Datadog, Inc. - -// Package telemetry implements a client for sending telemetry information to -// Datadog regarding usage of an APM library such as tracing or profiling. -package telemetry - -import ( - "fmt" - "math" - "sort" - "strings" -) - -// MockGlobalClient replaces the global telemetry client with a custom -// implementation of TelemetryClient. It returns a function that can be deferred -// to reset the global telemetry client to its previous value. -func MockGlobalClient(client Client) func() { - globalClient.Lock() - defer globalClient.Unlock() - oldClient := GlobalClient - GlobalClient = client - return func() { - globalClient.Lock() - defer globalClient.Unlock() - GlobalClient = oldClient - } -} - -// Errorfer is an interface that allows to call the `Errorf` method. -// This is the same interface as `testing.T` because the goal of this -// interface is to remove the need to import `testing` in this package -// that is actually imported by users. -type Errorfer interface { - Errorf(format string, args ...any) -} - -// Check is a testing utility to assert that a target key in config contains the expected value -func Check(t Errorfer, configuration []Configuration, key string, expected interface{}) { - for _, kv := range configuration { - if kv.Name == key { - if kv.Value != expected { - t.Errorf("configuration %s: wanted '%v' type:%T, got '%v' type:%T", key, expected, expected, kv.Value, kv.Value) - } - return - } - } - t.Errorf("missing configuration %s", key) -} - -// SetAgentlessEndpoint is used for testing purposes to replace the real agentless -// endpoint with a custom one -func SetAgentlessEndpoint(endpoint string) string { - agentlessEndpointLock.Lock() - defer agentlessEndpointLock.Unlock() - prev := agentlessURL - agentlessURL = endpoint - return prev -} - -// Sanitize ensures the configuration values are valid and compatible. -// It removes NaN and Inf values and converts string slices and maps into comma-separated strings. -func Sanitize(c Configuration) Configuration { - switch val := c.Value.(type) { - case float64: - if math.IsNaN(val) || math.IsInf(val, 0) { - // Those values cause marshalling errors. - // https://github.com/golang/go/issues/59627 - c.Value = nil - } - case []string: - // The telemetry API only supports primitive types. - c.Value = strings.Join(val, ",") - case map[string]interface{}: - // The telemetry API only supports primitive types. - // Sort the keys to ensure the order is deterministic. - // This is technically not required but makes testing easier + it's not in a hot path. - keys := make([]string, 0, len(val)) - for k := range val { - keys = append(keys, k) - } - sort.Strings(keys) - var sb strings.Builder - for _, k := range keys { - if sb.Len() > 0 { - sb.WriteString(",") - } - sb.WriteString(k) - sb.WriteString(":") - sb.WriteString(fmt.Sprint(val[k])) - } - c.Value = sb.String() - default: - var sb strings.Builder - sb.WriteString(fmt.Sprint(val)) - c.Value = sb.String() - } - return c -} diff --git a/profiler/telemetry.go b/profiler/telemetry.go index a53367f33a..0fb63f9405 100644 --- a/profiler/telemetry.go +++ b/profiler/telemetry.go @@ -6,6 +6,7 @@ package profiler import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" ) @@ -24,15 +25,8 @@ func startTelemetry(c *config) { _, ok := c.types[t] return ok } - telemetry.GlobalClient.ApplyOps( - telemetry.WithService(c.service), - telemetry.WithEnv(c.env), - telemetry.WithHTTPClient(c.httpClient), - telemetry.WithURL(c.agentless, c.agentURL), - ) - telemetry.GlobalClient.ProductChange( - telemetry.NamespaceProfilers, - true, + telemetry.ProductStarted(telemetry.NamespaceProfilers) + telemetry.RegisterAppConfigs( []telemetry.Configuration{ {Name: "delta_profiles", Value: c.deltaProfiles}, {Name: "agentless", Value: c.agentless}, @@ -56,6 +50,18 @@ func startTelemetry(c *config) { {Name: "num_custom_profiler_label_keys", Value: len(c.customProfilerLabels)}, {Name: "enabled", Value: c.enabled}, {Name: "flush_on_exit", Value: c.flushOnExit}, - }, + }..., ) + if telemetry.GlobalClient() == nil { + client, err := telemetry.NewClient(c.service, c.env, c.version, telemetry.ClientConfig{ + HTTPClient: c.httpClient, + APIKey: c.apiKey, + AgentURL: c.agentURL, + }) + if err != nil { + log.Debug("profiler: failed to create telemetry client: %v", err) + return + } + telemetry.StartApp(client) + } } diff --git a/profiler/telemetry_test.go b/profiler/telemetry_test.go index 155b16d33c..81a4fca36e 100644 --- a/profiler/telemetry_test.go +++ b/profiler/telemetry_test.go @@ -18,8 +18,8 @@ import ( // Test that the profiler sends the correct telemetry information func TestTelemetryEnabled(t *testing.T) { t.Run("tracer start, profiler start", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() tracer.Start() defer tracer.Stop() @@ -31,13 +31,12 @@ func TestTelemetryEnabled(t *testing.T) { ) defer Stop() - assert.True(t, telemetryClient.ProfilerEnabled) - telemetry.Check(t, telemetryClient.Configuration, "heap_profile_enabled", true) - telemetryClient.AssertNumberOfCalls(t, "ApplyOps", 2) + assert.True(t, telemetryClient.Products[telemetry.NamespaceProfilers]) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "heap_profile_enabled", Value: true}) }) t.Run("only profiler start", func(t *testing.T) { - telemetryClient := new(telemetrytest.MockClient) - defer telemetry.MockGlobalClient(telemetryClient)() + telemetryClient := new(telemetrytest.RecordClient) + defer telemetry.MockClient(telemetryClient)() Start( WithProfileTypes( HeapProfile, @@ -45,8 +44,7 @@ func TestTelemetryEnabled(t *testing.T) { ) defer Stop() - assert.True(t, telemetryClient.ProfilerEnabled) - telemetry.Check(t, telemetryClient.Configuration, "heap_profile_enabled", true) - telemetryClient.AssertNumberOfCalls(t, "ApplyOps", 1) + assert.True(t, telemetryClient.Products[telemetry.NamespaceProfilers]) + assert.Contains(t, telemetryClient.Configuration, telemetry.Configuration{Name: "heap_profile_enabled", Value: true}) }) }