From 2e0d67c3930a778a294784d36b3f486b80d44dd5 Mon Sep 17 00:00:00 2001 From: nubtron Date: Thu, 16 Jul 2026 12:40:45 +0000 Subject: [PATCH 1/2] Add labeled Python agent telemetry bridge --- .../impl/agenttelemetry_test.go | 129 ++++++++++ .../agenttelemetry/impl/defaultProfiles.yaml | 3 + pkg/collector/python/BUILD.bazel | 1 + pkg/collector/python/datadog_agent.go | 221 +++++++++++++----- pkg/collector/python/datadog_agent_test.go | 4 + pkg/collector/python/init.go | 2 + pkg/collector/python/test_datadog_agent.go | 75 +++++- ...ent-telemetry-labels-16633f3a47f24302.yaml | 4 + rtloader/common/builtins/datadog_agent.c | 107 +++++++++ rtloader/common/builtins/datadog_agent.h | 1 + rtloader/include/datadog_agent_rtloader.h | 12 + rtloader/include/rtloader.h | 9 + rtloader/include/rtloader_types.h | 2 + rtloader/rtloader/api.cpp | 5 + rtloader/test/datadog_agent/datadog_agent.go | 47 +++- .../test/datadog_agent/datadog_agent_test.go | 90 +++++++ rtloader/three/three.cpp | 5 + rtloader/three/three.h | 1 + 18 files changed, 659 insertions(+), 59 deletions(-) create mode 100644 releasenotes/notes/python-agent-telemetry-labels-16633f3a47f24302.yaml diff --git a/comp/core/agenttelemetry/impl/agenttelemetry_test.go b/comp/core/agenttelemetry/impl/agenttelemetry_test.go index 215cc00858fb..17f51c484a3e 100644 --- a/comp/core/agenttelemetry/impl/agenttelemetry_test.go +++ b/comp/core/agenttelemetry/impl/agenttelemetry_test.go @@ -2304,6 +2304,119 @@ func TestUsingPayloadCompressionInAgentTelemetrySender(t *testing.T) { assert.True(t, float64(nonCompressBodyLen)/float64(compressBodyLen) > 1.5) } +func TestOpenMetricsMaxReturnedMetricsProfilePreservesOnlyCheckName(t *testing.T) { + var c = ` + agent_telemetry: + enabled: true + profiles: + - name: checks + metric: + metrics: + - name: openmetrics.max_returned_metrics_reached + preserve_tags: + - check_name + ` + + tel := makeTelMock(t) + s := makeSenderImpl(t, nil, c) + r := newRunnerMock() + a := getTestAtel(t, tel, c, s, nil, r) + require.True(t, a.enabled) + + counter := tel.NewCounter("openmetrics", "max_returned_metrics_reached", []string{"check_name", "url"}, "") + counter.AddWithTags(1, map[string]string{"check_name": "openmetrics", "url": "http://one.example"}) + counter.AddWithTags(2, map[string]string{"check_name": "openmetrics", "url": "http://two.example"}) + + families, err := tel.Gather(false) + require.NoError(t, err) + var foundRaw bool + for _, family := range families { + if family.GetName() != "openmetrics__max_returned_metrics_reached" { + continue + } + foundRaw = true + require.NotEmpty(t, family.Metric) + for _, metric := range family.Metric { + labels := map[string]string{} + for _, label := range metric.GetLabel() { + labels[label.GetName()] = label.GetValue() + } + assert.Equal(t, "openmetrics", labels["check_name"]) + } + } + require.True(t, foundRaw) + + metric, ok := getPayloadMetric(a, "openmetrics.max_returned_metrics_reached") + require.True(t, ok) + assert.Equal(t, 3.0, metric.Value) + assert.Equal(t, map[string]interface{}{"check_name": "openmetrics"}, metric.Tags) +} + +func TestOpenMetricsMaxReturnedMetricsProfileDropsMissingCheckName(t *testing.T) { + var c = ` + agent_telemetry: + enabled: true + profiles: + - name: checks + metric: + metrics: + - name: openmetrics.max_returned_metrics_reached + preserve_tags: + - check_name + ` + + tel := makeTelMock(t) + s := makeSenderImpl(t, nil, c) + r := newRunnerMock() + a := getTestAtel(t, tel, c, s, nil, r) + require.True(t, a.enabled) + + counter := tel.NewCounter("openmetrics", "max_returned_metrics_reached", []string{"check_name_typo"}, "") + counter.AddWithTags(1, map[string]string{"check_name_typo": "openmetrics"}) + + payload, err := getPayload(a) + require.NoError(t, err) + payloads, ok := payload.Payload.([]Payload) + require.True(t, ok) + assert.Empty(t, payloads) +} + +func TestOpenMetricsMaxReturnedMetricsCoalescesRegularAndDefaultRegistries(t *testing.T) { + var c = ` + agent_telemetry: + enabled: true + profiles: + - name: checks + metric: + metrics: + - name: openmetrics.max_returned_metrics_reached + preserve_tags: + - check_name + ` + + tel := makeTelMock(t) + s := makeSenderImpl(t, nil, c) + r := newRunnerMock() + a := getTestAtel(t, tel, c, s, nil, r) + require.True(t, a.enabled) + + regularCounter := tel.NewCounterWithOpts("openmetrics", "max_returned_metrics_reached", []string{"check_name"}, "", telemetry.DefaultOptions) + defaultCounter := tel.NewCounterWithOpts("openmetrics", "max_returned_metrics_reached", []string{"check_name"}, "", telemetry.Options{DefaultMetric: true}) + regularCounter.AddWithTags(1, map[string]string{"check_name": "openmetrics"}) + defaultCounter.AddWithTags(2, map[string]string{"check_name": "openmetrics_default"}) + + metrics, ok := getPayloadFilteredMetricList(a, "openmetrics.max_returned_metrics_reached") + require.True(t, ok) + require.Len(t, metrics, 2) + + regularMetric, ok := getPayloadMetricByTagValues(metrics, map[string]interface{}{"check_name": "openmetrics"}) + require.True(t, ok) + assert.Equal(t, 1.0, regularMetric.Value) + defaultMetric, ok := getPayloadMetricByTagValues(metrics, map[string]interface{}{"check_name": "openmetrics_default"}) + require.True(t, ok) + assert.Equal(t, 2.0, defaultMetric.Value) +} + func TestCoalescesDefaultAndNoDefaultMetricFamiliesBeforeAggregation(t *testing.T) { var c = ` agent_telemetry: @@ -2391,6 +2504,22 @@ func TestAgentTelemetryParseDefaultConfiguration(t *testing.T) { assert.True(t, len(atCfg.events) > 0) assert.True(t, len(atCfg.schedule) > 0) assert.True(t, len(atCfg.Profiles) > len(atCfg.events)) + + var checksProfile *Profile + for i := range atCfg.Profiles { + if atCfg.Profiles[i].Name == "checks" { + checksProfile = atCfg.Profiles[i] + break + } + } + require.NotNil(t, checksProfile) + metric, ok := checksProfile.metricsMap["openmetrics_max_returned_metrics_reached"] + require.True(t, ok) + assert.Equal(t, "openmetrics.max_returned_metrics_reached", metric.Name) + assert.Equal(t, []string{"check_name"}, metric.PreserveTags) + assert.True(t, metric.preserveTagsExists) + _, ok = metric.preserveTagsMap["check_name"] + assert.True(t, ok) } func TestAgentTelemetryEventConfiguration(t *testing.T) { diff --git a/comp/core/agenttelemetry/impl/defaultProfiles.yaml b/comp/core/agenttelemetry/impl/defaultProfiles.yaml index 5e18614f43a8..225c0b69fe77 100644 --- a/comp/core/agenttelemetry/impl/defaultProfiles.yaml +++ b/comp/core/agenttelemetry/impl/defaultProfiles.yaml @@ -19,6 +19,9 @@ profiles: preserve_tags: - check_name - state + - name: openmetrics.max_returned_metrics_reached + preserve_tags: + - check_name - name: pymem.inuse - name: health_platform.issues_detected preserve_tags: diff --git a/pkg/collector/python/BUILD.bazel b/pkg/collector/python/BUILD.bazel index 5668eba3532b..a76fdf5354b2 100644 --- a/pkg/collector/python/BUILD.bazel +++ b/pkg/collector/python/BUILD.bazel @@ -166,6 +166,7 @@ go_library( "//pkg/version", "@com_github_datadog_agent_payload_v5//healthplatform", "@com_github_mohae_deepcopy//:deepcopy", + "@com_github_prometheus_client_model//go", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", "@in_yaml_go_yaml_v2//:yaml", diff --git a/pkg/collector/python/datadog_agent.go b/pkg/collector/python/datadog_agent.go index 8ea9bca69d0e..f861f06df1a2 100644 --- a/pkg/collector/python/datadog_agent.go +++ b/pkg/collector/python/datadog_agent.go @@ -12,6 +12,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "sync" "unsafe" @@ -560,92 +561,139 @@ func ObfuscateMongoDBString(cmd *C.char, errResult **C.char) *C.char { } var ( - telemetryMap = map[string]any{} + telemetryMap = map[string]*agentTelemetryMetric{} telemetryLock = sync.Mutex{} ) -func lazyInitTelemetryHistogram(checkName string, metricName string) telemetry.Histogram { - var key = checkName + "." + metricName +type agentTelemetryMetric struct { + metric any + metricType string + labelNames []string +} + +func agentTelemetryMetricKey(checkName string, metricName string) string { + return checkName + "." + metricName +} + +func sortedLabelNames(labels map[string]string) []string { + labelNames := make([]string, 0, len(labels)) + for labelName := range labels { + labelNames = append(labelNames, labelName) + } + sort.Strings(labelNames) + return labelNames +} + +func sameStringSlice(left []string, right []string) bool { + if len(left) != len(right) { + return false + } + for i := range left { + if left[i] != right[i] { + return false + } + } + return true +} + +func lazyInitTelemetryMetric(checkName string, metricName string, metricType string, labelNames []string) (*agentTelemetryMetric, error) { + key := agentTelemetryMetricKey(checkName, metricName) telemetryLock.Lock() defer telemetryLock.Unlock() - histogram, ok := telemetryMap[key] - if !ok { - histogram = telemetryimpl.GetCompatComponent().NewHistogramWithOpts( + if entry, ok := telemetryMap[key]; ok { + if entry.metricType != metricType { + return nil, fmt.Errorf("metric %s for check %s was already emitted as %s when %s was expected", metricName, checkName, entry.metricType, metricType) + } + if !sameStringSlice(entry.labelNames, labelNames) { + return nil, fmt.Errorf("metric %s for check %s was already emitted with labels %v when labels %v were expected", metricName, checkName, entry.labelNames, labelNames) + } + return entry, nil + } + + entry := &agentTelemetryMetric{ + metricType: metricType, + labelNames: append([]string(nil), labelNames...), + } + switch metricType { + case "counter": + entry.metric = telemetryimpl.GetCompatComponent().NewCounterWithOpts( + checkName, + metricName, + entry.labelNames, + fmt.Sprintf("Counter of %s for Python check %s", metricName, checkName), + telemetry.DefaultOptions, + ) + case "histogram": + entry.metric = telemetryimpl.GetCompatComponent().NewHistogramWithOpts( checkName, metricName, - nil, + entry.labelNames, fmt.Sprintf("Histogram of %s for Python check %s", metricName, checkName), []float64{10, 25, 50, 75, 100, 250, 500, 1000, 10000}, telemetry.DefaultOptions, ) - telemetryMap[key] = histogram - } - switch t := histogram.(type) { + case "gauge": + entry.metric = telemetryimpl.GetCompatComponent().NewGaugeWithOpts( + checkName, + metricName, + entry.labelNames, + fmt.Sprintf("Gauge of %s for Python check %s", metricName, checkName), + telemetry.DefaultOptions, + ) default: - // Somehow the same metric was emitted with a different type - log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %s when histogram was expected", metricName, checkName, t) + return nil, fmt.Errorf("unsupported metric type %s requested by %s for %s", metricType, checkName, metricName) + } + + telemetryMap[key] = entry + return entry, nil +} + +func lazyInitTelemetryHistogram(checkName string, metricName string) telemetry.Histogram { + entry, err := lazyInitTelemetryMetric(checkName, metricName, "histogram", nil) + if err != nil { + log.Errorf("EmitAgentTelemetry: %v", err) + return nil + } + histogram, ok := entry.metric.(telemetry.Histogram) + if !ok { + log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %T when histogram was expected", metricName, checkName, entry.metric) return nil - case telemetry.Histogram: - return t } + return histogram } func lazyInitTelemetryCounter(checkName string, metricName string) telemetry.Counter { - var key = checkName + "." + metricName - telemetryLock.Lock() - defer telemetryLock.Unlock() - - counter, ok := telemetryMap[key] - if !ok { - counter = telemetryimpl.GetCompatComponent().NewCounterWithOpts( - checkName, - metricName, - nil, - fmt.Sprintf("Counter of %s for Python check %s", metricName, checkName), - telemetry.DefaultOptions, - ) - telemetryMap[key] = counter + entry, err := lazyInitTelemetryMetric(checkName, metricName, "counter", nil) + if err != nil { + log.Errorf("EmitAgentTelemetry: %v", err) + return nil } - switch t := counter.(type) { - default: - // Somehow the same metric was emitted with a different type - log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %s when counter was expected", metricName, checkName, t) + counter, ok := entry.metric.(telemetry.Counter) + if !ok { + log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %T when counter was expected", metricName, checkName, entry.metric) return nil - case telemetry.Counter: - return t } + return counter } func lazyInitTelemetryGauge(checkName string, metricName string) telemetry.Gauge { - var key = checkName + "." + metricName - telemetryLock.Lock() - defer telemetryLock.Unlock() - - gauge, ok := telemetryMap[key] - if !ok { - gauge = telemetryimpl.GetCompatComponent().NewGaugeWithOpts( - checkName, - metricName, - nil, - fmt.Sprintf("Gauge of %s for Python check %s", metricName, checkName), - telemetry.DefaultOptions, - ) - telemetryMap[key] = gauge + entry, err := lazyInitTelemetryMetric(checkName, metricName, "gauge", nil) + if err != nil { + log.Errorf("EmitAgentTelemetry: %v", err) + return nil } - switch t := gauge.(type) { - default: - // Somehow the same metric was emitted with a different type - log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %s when gauge was expected", metricName, checkName, t) + gauge, ok := entry.metric.(telemetry.Gauge) + if !ok { + log.Errorf("EmitAgentTelemetry: metric %s for check %s was emitted with a different type %T when gauge was expected", metricName, checkName, entry.metric) return nil - case telemetry.Gauge: - return t } + return gauge } -// EmitAgentTelemetry records a telemetry data point for a Python integration +// EmitAgentTelemetry records an unlabeled telemetry data point for a Python integration. // NB: Cross-org agent telemetry needs to be enabled for each metric in -// comp/core/agenttelemetry/impl/config.go defaultProfiles +// comp/core/agenttelemetry/impl/defaultProfiles.yaml. // //export EmitAgentTelemetry func EmitAgentTelemetry(checkName *C.char, metricName *C.char, metricValue C.double, metricType *C.char) { @@ -675,6 +723,67 @@ func EmitAgentTelemetry(checkName *C.char, metricName *C.char, metricValue C.dou } } +func emitAgentTelemetryWithLabels(checkName string, metricName string, metricValue float64, metricType string, labels map[string]string) error { + entry, err := lazyInitTelemetryMetric(checkName, metricName, metricType, sortedLabelNames(labels)) + if err != nil { + return err + } + + switch metricType { + case "counter": + counter, ok := entry.metric.(telemetry.Counter) + if !ok { + return fmt.Errorf("metric %s for check %s was emitted with a different type %T when counter was expected", metricName, checkName, entry.metric) + } + counter.AddWithTags(metricValue, labels) + case "histogram": + histogram, ok := entry.metric.(telemetry.Histogram) + if !ok { + return fmt.Errorf("metric %s for check %s was emitted with a different type %T when histogram was expected", metricName, checkName, entry.metric) + } + histogram.WithTags(labels).Observe(metricValue) + case "gauge": + gauge, ok := entry.metric.(telemetry.Gauge) + if !ok { + return fmt.Errorf("metric %s for check %s was emitted with a different type %T when gauge was expected", metricName, checkName, entry.metric) + } + gauge.WithTags(labels).Set(metricValue) + default: + return fmt.Errorf("unsupported metric type %s requested by %s for %s", metricType, checkName, metricName) + } + + return nil +} + +// EmitAgentTelemetryWithLabels records a labeled telemetry data point for a Python integration. +// labelsJSON must be a JSON object whose keys and values are strings. +// +//export EmitAgentTelemetryWithLabels +func EmitAgentTelemetryWithLabels(checkName *C.char, metricName *C.char, metricValue C.double, metricType *C.char, labelsJSON *C.char, errOut **C.char) { + *errOut = nil + + goCheckName := C.GoString(checkName) + goMetricName := C.GoString(metricName) + goMetricValue := float64(metricValue) + goMetricType := C.GoString(metricType) + goLabelsJSON := C.GoString(labelsJSON) + + var labels map[string]string + if err := json.Unmarshal([]byte(goLabelsJSON), &labels); err != nil { + *errOut = TrackedCString(fmt.Sprintf("invalid labels JSON for %s.%s: %v", goCheckName, goMetricName, err)) + return + } + if labels == nil { + *errOut = TrackedCString(fmt.Sprintf("invalid labels JSON for %s.%s: expected object with string values", goCheckName, goMetricName)) + return + } + + if err := emitAgentTelemetryWithLabels(goCheckName, goMetricName, goMetricValue, goMetricType, labels); err != nil { + *errOut = TrackedCString(err.Error()) + return + } +} + func parseIssueJSON(payload string) (*healthplatformpayload.Issue, error) { t := strings.TrimSpace(payload) if t == "" || t == "null" { diff --git a/pkg/collector/python/datadog_agent_test.go b/pkg/collector/python/datadog_agent_test.go index 3b2e509a51b8..dfd3bf723e81 100644 --- a/pkg/collector/python/datadog_agent_test.go +++ b/pkg/collector/python/datadog_agent_test.go @@ -39,6 +39,10 @@ func TestEmitAgentTelemetry(t *testing.T) { testEmitAgentTelemetry(t) } +func TestEmitAgentTelemetryWithLabels(t *testing.T) { + testEmitAgentTelemetryWithLabels(t) +} + func TestObfuscaterConfig(t *testing.T) { testObfuscaterConfig(t) } diff --git a/pkg/collector/python/init.go b/pkg/collector/python/init.go index 211321157bb8..b77271da3a4a 100644 --- a/pkg/collector/python/init.go +++ b/pkg/collector/python/init.go @@ -97,6 +97,7 @@ char* ObfuscateSQLExecPlan(char *, bool, char **); double getProcessStartTime(); char* ObfuscateMongoDBString(char *, char **); void EmitAgentTelemetry(char *, char *, double, char *); +void EmitAgentTelemetryWithLabels(char *, char *, double, char *, char *, char **); void ReportIssue(char *, char *, char **); void ResolveIssue(char *, char **); @@ -118,6 +119,7 @@ void initDatadogAgentModule(rtloader_t *rtloader) { set_get_process_start_time_cb(rtloader, getProcessStartTime); set_obfuscate_mongodb_string_cb(rtloader, ObfuscateMongoDBString); set_emit_agent_telemetry_cb(rtloader, EmitAgentTelemetry); + set_emit_agent_telemetry_with_labels_cb(rtloader, EmitAgentTelemetryWithLabels); set_report_issue_cb(rtloader, ReportIssue); set_resolve_issue_cb(rtloader, ResolveIssue); } diff --git a/pkg/collector/python/test_datadog_agent.go b/pkg/collector/python/test_datadog_agent.go index f3e08a590215..5bef92c78bea 100644 --- a/pkg/collector/python/test_datadog_agent.go +++ b/pkg/collector/python/test_datadog_agent.go @@ -16,10 +16,12 @@ import ( "time" healthplatformpayload "github.com/DataDog/agent-payload/v5/healthplatform" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" yaml "go.yaml.in/yaml/v2" + telemetryimpl "github.com/DataDog/datadog-agent/comp/core/telemetry/impl" healthplatformmock "github.com/DataDog/datadog-agent/comp/healthplatform/store/mock" "github.com/DataDog/datadog-agent/pkg/collector/externalhost" pkgconfigmock "github.com/DataDog/datadog-agent/pkg/config/mock" @@ -94,6 +96,8 @@ func testSetExternalTags(t *testing.T) { } func testEmitAgentTelemetry(t *testing.T) { + resetAgentTelemetryForTest() + EmitAgentTelemetry(C.CString("test_check"), C.CString("test_metric"), 1.0, C.CString("gauge")) // Test second time for laziness check @@ -125,7 +129,76 @@ func testEmitAgentTelemetry(t *testing.T) { EmitAgentTelemetry(C.CString("test_check"), C.CString("test_histogram"), 1.0, C.CString("counter")) EmitAgentTelemetry(C.CString("test_check"), C.CString("test_histogram"), 1.0, C.CString("gauge")) - assert.True(t, true) + mf := requireTelemetryFamily(t, "test_check__test_metric") + require.Len(t, mf.Metric, 1) + assert.Empty(t, mf.Metric[0].GetLabel()) + assert.Equal(t, 1.0, mf.Metric[0].GetGauge().GetValue()) +} + +func resetAgentTelemetryForTest() { + telemetryLock.Lock() + telemetryMap = map[string]*agentTelemetryMetric{} + telemetryLock.Unlock() + telemetryimpl.GetCompatComponent().Reset() +} + +func callEmitAgentTelemetryWithLabels(checkName string, metricName string, metricValue float64, metricType string, labelsJSON string) string { + var errOut *C.char + EmitAgentTelemetryWithLabels(C.CString(checkName), C.CString(metricName), C.double(metricValue), C.CString(metricType), C.CString(labelsJSON), &errOut) + if errOut == nil { + return "" + } + return C.GoString(errOut) +} + +func requireTelemetryFamily(t *testing.T, name string) *dto.MetricFamily { + t.Helper() + families, err := telemetryimpl.GetCompatComponent().Gather(false) + require.NoError(t, err) + for _, family := range families { + if family.GetName() == name { + return family + } + } + t.Fatalf("metric family %s not found", name) + return nil +} + +func metricLabels(metricIndex int, family *dto.MetricFamily) map[string]string { + labels := map[string]string{} + for _, label := range family.Metric[metricIndex].GetLabel() { + labels[label.GetName()] = label.GetValue() + } + return labels +} + +func testEmitAgentTelemetryWithLabels(t *testing.T) { + resetAgentTelemetryForTest() + + assert.Empty(t, callEmitAgentTelemetryWithLabels("test_check", "test_counter", 1, "counter", `{"check_name":"openmetrics","state":"limited"}`)) + assert.Empty(t, callEmitAgentTelemetryWithLabels("test_check", "test_counter", 2, "counter", `{"state":"limited","check_name":"openmetrics"}`)) + counterFamily := requireTelemetryFamily(t, "test_check__test_counter") + require.Len(t, counterFamily.Metric, 1) + assert.Equal(t, map[string]string{"check_name": "openmetrics", "state": "limited"}, metricLabels(0, counterFamily)) + assert.Equal(t, 3.0, counterFamily.Metric[0].GetCounter().GetValue()) + + assert.Empty(t, callEmitAgentTelemetryWithLabels("test_check", "test_gauge", 7, "gauge", `{"check_name":"openmetrics"}`)) + gaugeFamily := requireTelemetryFamily(t, "test_check__test_gauge") + require.Len(t, gaugeFamily.Metric, 1) + assert.Equal(t, map[string]string{"check_name": "openmetrics"}, metricLabels(0, gaugeFamily)) + assert.Equal(t, 7.0, gaugeFamily.Metric[0].GetGauge().GetValue()) + + assert.Empty(t, callEmitAgentTelemetryWithLabels("test_check", "test_histogram", 25, "histogram", `{"check_name":"openmetrics"}`)) + histogramFamily := requireTelemetryFamily(t, "test_check__test_histogram") + require.Len(t, histogramFamily.Metric, 1) + assert.Equal(t, map[string]string{"check_name": "openmetrics"}, metricLabels(0, histogramFamily)) + assert.Equal(t, uint64(1), histogramFamily.Metric[0].GetHistogram().GetSampleCount()) + + assert.Contains(t, callEmitAgentTelemetryWithLabels("test_check", "bad_json", 1, "counter", `{bad`), "invalid labels JSON") + assert.Contains(t, callEmitAgentTelemetryWithLabels("test_check", "bad_labels", 1, "counter", `{"check_name":1}`), "invalid labels JSON") + assert.Contains(t, callEmitAgentTelemetryWithLabels("test_check", "test_counter", 1, "counter", `{"check_name":"openmetrics"}`), "already emitted with labels") + assert.Contains(t, callEmitAgentTelemetryWithLabels("test_check", "test_counter", 1, "gauge", `{"state":"limited","check_name":"openmetrics"}`), "already emitted as counter") + assert.Contains(t, callEmitAgentTelemetryWithLabels("test_check", "invalid_type", 1, "rate", `{"check_name":"openmetrics"}`), "unsupported metric type") } func testObfuscaterConfig(t *testing.T) { diff --git a/releasenotes/notes/python-agent-telemetry-labels-16633f3a47f24302.yaml b/releasenotes/notes/python-agent-telemetry-labels-16633f3a47f24302.yaml new file mode 100644 index 000000000000..a9c0f27f74b9 --- /dev/null +++ b/releasenotes/notes/python-agent-telemetry-labels-16633f3a47f24302.yaml @@ -0,0 +1,4 @@ +--- +enhancements: + - | + Allow Python integrations to emit labeled Agent telemetry data. diff --git a/rtloader/common/builtins/datadog_agent.c b/rtloader/common/builtins/datadog_agent.c index 2ad8668ebfad..b2dc76f7aba3 100644 --- a/rtloader/common/builtins/datadog_agent.c +++ b/rtloader/common/builtins/datadog_agent.c @@ -27,6 +27,7 @@ static cb_obfuscate_sql_exec_plan_t cb_obfuscate_sql_exec_plan = NULL; static cb_get_process_start_time_t cb_get_process_start_time = NULL; static cb_obfuscate_mongodb_string_t cb_obfuscate_mongodb_string = NULL; static cb_emit_agent_telemetry_t cb_emit_agent_telemetry = NULL; +static cb_emit_agent_telemetry_with_labels_t cb_emit_agent_telemetry_with_labels = NULL; static cb_report_issue_t cb_report_issue = NULL; static cb_resolve_issue_t cb_resolve_issue = NULL; @@ -49,6 +50,7 @@ static PyObject *obfuscate_sql_exec_plan(PyObject *self, PyObject *args, PyObjec static PyObject *get_process_start_time(PyObject *self, PyObject *args, PyObject *kwargs); static PyObject *obfuscate_mongodb_string(PyObject *self, PyObject *args, PyObject *kwargs); static PyObject *emit_agent_telemetry(PyObject *self, PyObject *args, PyObject *kwargs); +static PyObject *emit_agent_telemetry_with_labels(PyObject *self, PyObject *args, PyObject *kwargs); static PyObject *report_issue(PyObject *self, PyObject *args, PyObject *kwargs); static PyObject *resolve_issue(PyObject *self, PyObject *args, PyObject *kwargs); @@ -71,6 +73,7 @@ static PyMethodDef methods[] = { { "get_process_start_time", (PyCFunction)get_process_start_time, METH_NOARGS, "Get agent process startup time, in seconds since the epoch." }, { "obfuscate_mongodb_string", (PyCFunction)obfuscate_mongodb_string, METH_VARARGS|METH_KEYWORDS, "Obfuscate & normalize a MongoDB command string." }, { "emit_agent_telemetry", (PyCFunction)emit_agent_telemetry, METH_VARARGS|METH_KEYWORDS, "Emit agent telemetry." }, + { "emit_agent_telemetry_with_labels", (PyCFunction)emit_agent_telemetry_with_labels, METH_VARARGS|METH_KEYWORDS, "Emit labeled agent telemetry." }, { "report_issue", (PyCFunction)report_issue, METH_VARARGS|METH_KEYWORDS, "Report a health platform issue." }, { "resolve_issue", (PyCFunction)resolve_issue, METH_VARARGS|METH_KEYWORDS, "Resolve a health platform issue by issue id." }, { NULL, NULL } // guards @@ -166,6 +169,10 @@ void _set_emit_agent_telemetry_cb(cb_emit_agent_telemetry_t cb) { cb_emit_agent_telemetry = cb; } +void _set_emit_agent_telemetry_with_labels_cb(cb_emit_agent_telemetry_with_labels_t cb) { + cb_emit_agent_telemetry_with_labels = cb; +} + void _set_report_issue_cb(cb_report_issue_t cb) { cb_report_issue = cb; @@ -983,6 +990,106 @@ static PyObject *emit_agent_telemetry(PyObject *self, PyObject *args, PyObject * Py_RETURN_NONE; } +static int validate_string_mapping(PyObject *mapping) +{ + if (!PyMapping_Check(mapping) || !PyObject_HasAttrString(mapping, "items")) { + PyErr_SetString(PyExc_TypeError, "labels must be a mapping of string keys to string values"); + return 0; + } + + PyObject *items = PyMapping_Items(mapping); + if (items == NULL) { + return 0; + } + + Py_ssize_t size = PySequence_Size(items); + if (size < 0) { + Py_DECREF(items); + return 0; + } + + for (Py_ssize_t i = 0; i < size; i++) { + PyObject *item = PySequence_GetItem(items, i); + if (item == NULL) { + Py_DECREF(items); + return 0; + } + + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + Py_DECREF(item); + Py_DECREF(items); + PyErr_SetString(PyExc_TypeError, "labels must be a mapping of string keys to string values"); + return 0; + } + + PyObject *key = PyTuple_GetItem(item, 0); + PyObject *value = PyTuple_GetItem(item, 1); + if (!(PyUnicode_Check(key) || PyBytes_Check(key)) || !(PyUnicode_Check(value) || PyBytes_Check(value))) { + Py_DECREF(item); + Py_DECREF(items); + PyErr_SetString(PyExc_TypeError, "labels must be a mapping of string keys to string values"); + return 0; + } + + Py_DECREF(item); + } + + Py_DECREF(items); + return 1; +} + +/*! \fn PyObject *emit_agent_telemetry_with_labels(PyObject *self, PyObject *args, PyObject *kwargs) + \brief This function implements the `datadog_agent.emit_agent_telemetry_with_labels` method, emitting labeled agent telemetry + for the provided check, metric, value, type, and labels mapping. +*/ +static PyObject *emit_agent_telemetry_with_labels(PyObject *self, PyObject *args, PyObject *kwargs) +{ + // callback must be set + if (cb_emit_agent_telemetry_with_labels == NULL) { + Py_RETURN_NONE; + } + + PyGILState_STATE gstate = PyGILState_Ensure(); + + char *check_name = NULL; + char *metric_name = NULL; + double metric_value; + char *metric_type = NULL; + PyObject *labels = NULL; + if (!PyArg_ParseTuple(args, "ssdsO", &check_name, &metric_name, &metric_value, &metric_type, &labels)) { + PyGILState_Release(gstate); + return NULL; + } + + if (!validate_string_mapping(labels)) { + PyGILState_Release(gstate); + return NULL; + } + + char *labels_json = as_json(labels); + if (labels_json == NULL) { + PyGILState_Release(gstate); + return NULL; + } + + char *err = NULL; + cb_emit_agent_telemetry_with_labels(check_name, metric_name, metric_value, metric_type, labels_json, &err); + + int has_err = err != NULL; + if (has_err) { + PyErr_SetString(PyExc_RuntimeError, err); + } + + _free(labels_json); + cgo_free(err); + PyGILState_Release(gstate); + + if (has_err) { + return NULL; + } + Py_RETURN_NONE; +} + /*! \fn PyObject *report_issue(PyObject *self, PyObject *args, PyObject *kwargs) \brief Implements `datadog_agent.report_issue`, forwarding to the CGO callback. */ diff --git a/rtloader/common/builtins/datadog_agent.h b/rtloader/common/builtins/datadog_agent.h index ecf9f64f2cbd..3110f55893e2 100644 --- a/rtloader/common/builtins/datadog_agent.h +++ b/rtloader/common/builtins/datadog_agent.h @@ -157,6 +157,7 @@ void _set_obfuscate_sql_exec_plan_cb(cb_obfuscate_sql_exec_plan_t); void _set_get_process_start_time_cb(cb_get_process_start_time_t); void _set_obfuscate_mongodb_string_cb(cb_obfuscate_mongodb_string_t); void _set_emit_agent_telemetry_cb(cb_emit_agent_telemetry_t); +void _set_emit_agent_telemetry_with_labels_cb(cb_emit_agent_telemetry_with_labels_t); void _set_report_issue_cb(cb_report_issue_t); void _set_resolve_issue_cb(cb_resolve_issue_t); diff --git a/rtloader/include/datadog_agent_rtloader.h b/rtloader/include/datadog_agent_rtloader.h index 2f0a898388b3..fa9e8f3a7172 100644 --- a/rtloader/include/datadog_agent_rtloader.h +++ b/rtloader/include/datadog_agent_rtloader.h @@ -690,6 +690,18 @@ DATADOG_AGENT_RTLOADER_API void set_obfuscate_mongodb_string_cb(rtloader_t *, cb */ DATADOG_AGENT_RTLOADER_API void set_emit_agent_telemetry_cb(rtloader_t *, cb_emit_agent_telemetry_t); +/*! \fn void set_emit_agent_telemetry_with_labels_cb(rtloader_t *, cb_emit_agent_telemetry_with_labels_t) + \brief Sets a callback to be used by rtloader to allow emitting a labeled metric for a given + check instance. + \param rtloader_t A rtloader_t * pointer to the RtLoader instance. + \param object A function pointer with cb_emit_agent_telemetry_with_labels_t prototype to the callback + function. + + The callback is expected to be provided by the rtloader caller - in go-context: CGO. +*/ +DATADOG_AGENT_RTLOADER_API void set_emit_agent_telemetry_with_labels_cb(rtloader_t *, + cb_emit_agent_telemetry_with_labels_t); + /*! \fn void set_report_issue_cb(rtloader_t *, cb_report_issue_t) \brief Sets a callback for reporting health platform issues from Python checks. \param rtloader_t A rtloader_t * pointer to the RtLoader instance. diff --git a/rtloader/include/rtloader.h b/rtloader/include/rtloader.h index 8e5c847a48ec..51dd58288e58 100644 --- a/rtloader/include/rtloader.h +++ b/rtloader/include/rtloader.h @@ -522,6 +522,15 @@ class RtLoader */ virtual void setEmitAgentTelemetryCb(cb_emit_agent_telemetry_t) = 0; + //! setEmitAgentTelemetryWithLabelsCb member. + /*! + \param A cb_emit_agent_telemetry_with_labels_t function pointer to the CGO callback. + + This allows us to set the relevant CGO callback that will allow emitting a labeled metric for + specific check instances. + */ + virtual void setEmitAgentTelemetryWithLabelsCb(cb_emit_agent_telemetry_with_labels_t) = 0; + //! setReportIssueCb member. /*! \param A cb_report_issue_t function pointer to the CGO callback. diff --git a/rtloader/include/rtloader_types.h b/rtloader/include/rtloader_types.h index e8799c6f8a84..fb9f965565b1 100644 --- a/rtloader/include/rtloader_types.h +++ b/rtloader/include/rtloader_types.h @@ -140,6 +140,8 @@ typedef double (*cb_get_process_start_time_t)(void); typedef char *(*cb_obfuscate_mongodb_string_t)(char *, char **); // (check_name, metric_name, metric_value, metric_type) typedef void (*cb_emit_agent_telemetry_t)(char *, char *, double, char *); +// (check_name, metric_name, metric_value, metric_type, labels_json, error_message_out) +typedef void (*cb_emit_agent_telemetry_with_labels_t)(char *, char *, double, char *, char *, char **); // (check_name, report_json, error_message_out) typedef void (*cb_report_issue_t)(char *, char *, char **); // (issue_id, error_message_out) diff --git a/rtloader/rtloader/api.cpp b/rtloader/rtloader/api.cpp index 65407da5988d..81e21b0bcab7 100644 --- a/rtloader/rtloader/api.cpp +++ b/rtloader/rtloader/api.cpp @@ -597,6 +597,11 @@ void set_emit_agent_telemetry_cb(rtloader_t *rtloader, cb_emit_agent_telemetry_t AS_TYPE(RtLoader, rtloader)->setEmitAgentTelemetryCb(cb); } +void set_emit_agent_telemetry_with_labels_cb(rtloader_t *rtloader, cb_emit_agent_telemetry_with_labels_t cb) +{ + AS_TYPE(RtLoader, rtloader)->setEmitAgentTelemetryWithLabelsCb(cb); +} + void set_report_issue_cb(rtloader_t *rtloader, cb_report_issue_t cb) { AS_TYPE(RtLoader, rtloader)->setReportIssueCb(cb); diff --git a/rtloader/test/datadog_agent/datadog_agent.go b/rtloader/test/datadog_agent/datadog_agent.go index 5209ef4d9efd..bf9024d8d25e 100644 --- a/rtloader/test/datadog_agent/datadog_agent.go +++ b/rtloader/test/datadog_agent/datadog_agent.go @@ -40,6 +40,7 @@ extern char* obfuscateSQLExecPlan(char*, bool, char**); extern double getProcessStartTime(); extern char* obfuscateMongoDBString(char*, char**); extern void emitAgentTelemetry(char*, char*, double, char*); +extern void emitAgentTelemetryWithLabels(char*, char*, double, char*, char*, char**); extern void reportIssue(char*, char*, char**); extern void resolveIssue(char*, char**); @@ -63,6 +64,7 @@ static void initDatadogAgentTests(rtloader_t *rtloader) { set_get_process_start_time_cb(rtloader, getProcessStartTime); set_obfuscate_mongodb_string_cb(rtloader, obfuscateMongoDBString); set_emit_agent_telemetry_cb(rtloader, emitAgentTelemetry); + set_emit_agent_telemetry_with_labels_cb(rtloader, emitAgentTelemetryWithLabels); set_report_issue_cb(rtloader, reportIssue); set_resolve_issue_cb(rtloader, resolveIssue); } @@ -74,8 +76,10 @@ static inline void call_free(void* ptr) { import "C" var ( - rtloader *C.rtloader_t - tmpfile *os.File + rtloader *C.rtloader_t + tmpfile *os.File + emitAgentTelemetryCalls int + emitAgentTelemetryWithLabelsCalls int ) type message struct { @@ -87,6 +91,8 @@ type message struct { func setUp() error { // Initialize memory tracking helpers.InitMemoryTracker() + emitAgentTelemetryCalls = 0 + emitAgentTelemetryWithLabelsCalls = 0 rtloader = (*C.rtloader_t)(common.GetRtLoader()) if rtloader == nil { @@ -390,6 +396,7 @@ func resolveIssue(issueID *C.char, errOut **C.char) { //export emitAgentTelemetry func emitAgentTelemetry(check *C.char, metric *C.char, value C.double, metricType *C.char) { + emitAgentTelemetryCalls++ checkName := C.GoString(check) metricName := C.GoString(metric) metricValue := float64(value) @@ -409,3 +416,39 @@ func emitAgentTelemetry(check *C.char, metric *C.char, value C.double, metricTyp panic(fmt.Sprintf("unexpected metric value: %f", metricValue)) } } + +//export emitAgentTelemetryWithLabels +func emitAgentTelemetryWithLabels(check *C.char, metric *C.char, value C.double, metricType *C.char, labelsJSON *C.char, errOut **C.char) { + *errOut = nil + emitAgentTelemetryWithLabelsCalls++ + checkName := C.GoString(check) + metricName := C.GoString(metric) + metricValue := float64(value) + metricTypeStr := C.GoString(metricType) + labelsJSONStr := C.GoString(labelsJSON) + + if checkName == "error_check" { + *errOut = (*C.char)(helpers.TrackedCString("stub labeled telemetry failure")) + return + } + if checkName != "test_check" { + panic(fmt.Sprintf("unexpected check name: %s", checkName)) + } + if metricName != "test_metric" { + panic(fmt.Sprintf("unexpected metric name: %s", metricName)) + } + if metricTypeStr != "gauge" && metricTypeStr != "counter" && metricTypeStr != "histogram" { + panic(fmt.Sprintf("unexpected metric type: %s", metricTypeStr)) + } + if fmt.Sprintf("%.1f", metricValue) != "1.0" { + panic(fmt.Sprintf("unexpected metric value: %f", metricValue)) + } + + var labels map[string]string + if err := json.Unmarshal([]byte(labelsJSONStr), &labels); err != nil { + panic(fmt.Sprintf("unexpected labels JSON %q: %v", labelsJSONStr, err)) + } + if labels["check_name"] != "openmetrics" || labels["state"] != "limited" { + panic(fmt.Sprintf("unexpected labels: %v", labels)) + } +} diff --git a/rtloader/test/datadog_agent/datadog_agent_test.go b/rtloader/test/datadog_agent/datadog_agent_test.go index f22c4f407f5a..9da4808dc44f 100644 --- a/rtloader/test/datadog_agent/datadog_agent_test.go +++ b/rtloader/test/datadog_agent/datadog_agent_test.go @@ -732,6 +732,8 @@ func TestObfuscateMongoDBString(t *testing.T) { func TestEmitAgentTelemetry(t *testing.T) { // Reset memory counters helpers.ResetMemoryStats() + emitAgentTelemetryCalls = 0 + emitAgentTelemetryWithLabelsCalls = 0 cases := []string{"counter", "histogram", "gauge"} for _, tc := range cases { @@ -743,11 +745,99 @@ func TestEmitAgentTelemetry(t *testing.T) { t.Fatal(err) } } + requireEqual(t, 3, emitAgentTelemetryCalls) + requireEqual(t, 0, emitAgentTelemetryWithLabelsCalls) // Check for leaks helpers.AssertMemoryUsage(t) } +func TestEmitAgentTelemetryWithLabels(t *testing.T) { + helpers.ResetMemoryStats() + emitAgentTelemetryCalls = 0 + emitAgentTelemetryWithLabelsCalls = 0 + + cases := []string{"counter", "histogram", "gauge"} + for _, tc := range cases { + code := fmt.Sprintf(` + datadog_agent.emit_agent_telemetry_with_labels( + "test_check", + "test_metric", + 1.0, + "%s", + {"state": "limited", "check_name": "openmetrics"}, + ) + `, tc) + out, err := run(code) + if err != nil { + t.Fatal(err) + } + if out != "" { + t.Fatalf("expected no output, got %q", out) + } + } + requireEqual(t, 0, emitAgentTelemetryCalls) + requireEqual(t, 3, emitAgentTelemetryWithLabelsCalls) + + badInputs := []struct { + name string + call string + expected string + callCount int + }{ + { + name: "non-mapping labels", + call: `datadog_agent.emit_agent_telemetry_with_labels("test_check", "test_metric", 1.0, "gauge", [("check_name", "openmetrics")])`, + expected: "TypeError", + }, + { + name: "non-string key", + call: `datadog_agent.emit_agent_telemetry_with_labels("test_check", "test_metric", 1.0, "gauge", {1: "openmetrics"})`, + expected: "TypeError", + }, + { + name: "non-string value", + call: `datadog_agent.emit_agent_telemetry_with_labels("test_check", "test_metric", 1.0, "gauge", {"check_name": 1})`, + expected: "TypeError", + }, + { + name: "serialization failure", + call: `datadog_agent.emit_agent_telemetry_with_labels("test_check", "test_metric", 1.0, "gauge", {"check_name": b"openmetrics"})`, + expected: "TypeError", + }, + { + name: "callback failure", + call: `datadog_agent.emit_agent_telemetry_with_labels("error_check", "test_metric", 1.0, "gauge", {"state": "limited", "check_name": "openmetrics"})`, + expected: "RuntimeError: stub labeled telemetry failure", + callCount: 1, + }, + } + for _, testCase := range badInputs { + t.Run(testCase.name, func(t *testing.T) { + emitAgentTelemetryCalls = 0 + emitAgentTelemetryWithLabelsCalls = 0 + out, err := run(testCase.call) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, testCase.expected) { + t.Fatalf("expected %q in output, got %q", testCase.expected, out) + } + requireEqual(t, 0, emitAgentTelemetryCalls) + requireEqual(t, testCase.callCount, emitAgentTelemetryWithLabelsCalls) + }) + } + + helpers.AssertMemoryUsage(t) +} + +func requireEqual(t *testing.T, expected int, actual int) { + t.Helper() + if expected != actual { + t.Fatalf("expected %d, got %d", expected, actual) + } +} + func TestReportIssue(t *testing.T) { helpers.ResetMemoryStats() diff --git a/rtloader/three/three.cpp b/rtloader/three/three.cpp index f8d6efe789a5..eda429a364b3 100644 --- a/rtloader/three/three.cpp +++ b/rtloader/three/three.cpp @@ -1115,6 +1115,11 @@ void Three::setEmitAgentTelemetryCb(cb_emit_agent_telemetry_t cb) _set_emit_agent_telemetry_cb(cb); } +void Three::setEmitAgentTelemetryWithLabelsCb(cb_emit_agent_telemetry_with_labels_t cb) +{ + _set_emit_agent_telemetry_with_labels_cb(cb); +} + void Three::setReportIssueCb(cb_report_issue_t cb) { _set_report_issue_cb(cb); diff --git a/rtloader/three/three.h b/rtloader/three/three.h index 7da5bfbd16b9..481e4af9cbe6 100644 --- a/rtloader/three/three.h +++ b/rtloader/three/three.h @@ -111,6 +111,7 @@ class Three : public RtLoader void setGetProcessStartTimeCb(cb_get_process_start_time_t); void setObfuscateMongoDBStringCb(cb_obfuscate_mongodb_string_t); void setEmitAgentTelemetryCb(cb_emit_agent_telemetry_t); + void setEmitAgentTelemetryWithLabelsCb(cb_emit_agent_telemetry_with_labels_t); void setReportIssueCb(cb_report_issue_t); void setResolveIssueCb(cb_resolve_issue_t); From 6f6c09d53995e351636aecd9d205cd95dea6e134 Mon Sep 17 00:00:00 2001 From: nubtron Date: Thu, 16 Jul 2026 12:50:14 +0000 Subject: [PATCH 2/2] Avoid Python telemetry cache key collisions --- pkg/collector/python/datadog_agent.go | 13 +++++++---- pkg/collector/python/datadog_agent_test.go | 4 ++++ pkg/collector/python/test_datadog_agent.go | 27 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pkg/collector/python/datadog_agent.go b/pkg/collector/python/datadog_agent.go index f861f06df1a2..9f29d0cfd787 100644 --- a/pkg/collector/python/datadog_agent.go +++ b/pkg/collector/python/datadog_agent.go @@ -561,18 +561,23 @@ func ObfuscateMongoDBString(cmd *C.char, errResult **C.char) *C.char { } var ( - telemetryMap = map[string]*agentTelemetryMetric{} + telemetryMap = map[agentTelemetryMetricKey]*agentTelemetryMetric{} telemetryLock = sync.Mutex{} ) +type agentTelemetryMetricKey struct { + checkName string + metricName string +} + type agentTelemetryMetric struct { metric any metricType string labelNames []string } -func agentTelemetryMetricKey(checkName string, metricName string) string { - return checkName + "." + metricName +func newAgentTelemetryMetricKey(checkName string, metricName string) agentTelemetryMetricKey { + return agentTelemetryMetricKey{checkName: checkName, metricName: metricName} } func sortedLabelNames(labels map[string]string) []string { @@ -597,7 +602,7 @@ func sameStringSlice(left []string, right []string) bool { } func lazyInitTelemetryMetric(checkName string, metricName string, metricType string, labelNames []string) (*agentTelemetryMetric, error) { - key := agentTelemetryMetricKey(checkName, metricName) + key := newAgentTelemetryMetricKey(checkName, metricName) telemetryLock.Lock() defer telemetryLock.Unlock() diff --git a/pkg/collector/python/datadog_agent_test.go b/pkg/collector/python/datadog_agent_test.go index dfd3bf723e81..18e843e450d2 100644 --- a/pkg/collector/python/datadog_agent_test.go +++ b/pkg/collector/python/datadog_agent_test.go @@ -39,6 +39,10 @@ func TestEmitAgentTelemetry(t *testing.T) { testEmitAgentTelemetry(t) } +func TestAgentTelemetryMetricKeyAvoidsDelimitedCollisions(t *testing.T) { + testAgentTelemetryMetricKeyAvoidsDelimitedCollisions(t) +} + func TestEmitAgentTelemetryWithLabels(t *testing.T) { testEmitAgentTelemetryWithLabels(t) } diff --git a/pkg/collector/python/test_datadog_agent.go b/pkg/collector/python/test_datadog_agent.go index 5bef92c78bea..2c926f769392 100644 --- a/pkg/collector/python/test_datadog_agent.go +++ b/pkg/collector/python/test_datadog_agent.go @@ -137,7 +137,7 @@ func testEmitAgentTelemetry(t *testing.T) { func resetAgentTelemetryForTest() { telemetryLock.Lock() - telemetryMap = map[string]*agentTelemetryMetric{} + telemetryMap = map[agentTelemetryMetricKey]*agentTelemetryMetric{} telemetryLock.Unlock() telemetryimpl.GetCompatComponent().Reset() } @@ -172,6 +172,31 @@ func metricLabels(metricIndex int, family *dto.MetricFamily) map[string]string { return labels } +func testAgentTelemetryMetricKeyAvoidsDelimitedCollisions(t *testing.T) { + resetAgentTelemetryForTest() + + leftKey := newAgentTelemetryMetricKey("a.b", "c") + rightKey := newAgentTelemetryMetricKey("a", "b.c") + telemetryMap[leftKey] = &agentTelemetryMetric{ + metric: "left-value", + metricType: "counter", + labelNames: []string{"check_name"}, + } + telemetryMap[rightKey] = &agentTelemetryMetric{ + metric: "right-value", + metricType: "gauge", + labelNames: []string{"state"}, + } + + require.Len(t, telemetryMap, 2) + assert.Equal(t, "counter", telemetryMap[leftKey].metricType) + assert.Equal(t, []string{"check_name"}, telemetryMap[leftKey].labelNames) + assert.Equal(t, "left-value", telemetryMap[leftKey].metric) + assert.Equal(t, "gauge", telemetryMap[rightKey].metricType) + assert.Equal(t, []string{"state"}, telemetryMap[rightKey].labelNames) + assert.Equal(t, "right-value", telemetryMap[rightKey].metric) +} + func testEmitAgentTelemetryWithLabels(t *testing.T) { resetAgentTelemetryForTest()