Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ func (suite *ProviderTestSuite) TestPrometheusFiltering() {
suite.T().Fatalf("error created kubelet mock: %v", err)
}

prometheus.ParseMetricsWithFilterFunc = func(data []byte, filter []string) ([]prom.MetricFamily, error) {
prometheus.ParseMetricsWithFilterFunc = func(data []byte, filter []string, contentType string) ([]prom.MetricFamily, error) {
// We are going to intercept the parsed prometheus metric family data to determine if the configured provider
// has the expected text blacklist settings by default, and that this functionality still works
metrics, err := prom.ParseMetricsWithFilter(data, filter)
metrics, err := prom.ParseMetricsWithFilter(data, filter, contentType)
var found bool
for _, metric := range metrics {
if metric.Name == "container_cpu_usage_seconds_total" {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (p *Provider) Provide(kc kubelet.KubeUtilInterface, sender sender.Sender) e
return nil
}

metrics, err := ParseMetricsWithFilterFunc(data, p.ScraperConfig.TextFilterBlacklist)
metrics, err := ParseMetricsWithFilterFunc(data, p.ScraperConfig.TextFilterBlacklist, "")
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions pkg/collector/python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ go_library(
"//pkg/util/kubernetes/kubelet",
"//pkg/util/log",
"//pkg/util/option",
"//pkg/util/prometheus",
"//pkg/util/retry",
"//pkg/version",
"@com_github_datadog_agent_payload_v5//healthplatform",
Expand Down
29 changes: 29 additions & 0 deletions pkg/collector/python/datadog_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
hostnameUtil "github.com/DataDog/datadog-agent/pkg/util/hostname"
"github.com/DataDog/datadog-agent/pkg/util/kubernetes/clustername"
"github.com/DataDog/datadog-agent/pkg/util/log"
promutil "github.com/DataDog/datadog-agent/pkg/util/prometheus"
"github.com/DataDog/datadog-agent/pkg/version"
)

Expand Down Expand Up @@ -744,6 +745,34 @@ func ResolveIssue(issueID *C.char, errOut **C.char) {
hp.ResolveIssue(id)
}

// ParsePrometheusMetrics parses Prometheus/OpenMetrics text format using the Go parser
// and returns the result as a JSON string.
//
//export ParsePrometheusMetrics
func ParsePrometheusMetrics(rawText *C.char, contentType *C.char, errResult **C.char) *C.char {
data := []byte(C.GoString(rawText))
jsonResult, err := promutil.ParseMetricsToJSON(data, C.GoString(contentType))
if err != nil {
*errResult = TrackedCString(err.Error())
return nil
}
return TrackedCString(jsonResult)
}

// ProcessPrometheusMetrics parses Prometheus/OpenMetrics text format using the Go parser,
// applies label/tag processing based on the provided config, and returns processed results as JSON.
//
//export ProcessPrometheusMetrics
func ProcessPrometheusMetrics(rawText *C.char, contentType *C.char, configJSON *C.char, errResult **C.char) *C.char {
data := []byte(C.GoString(rawText))
jsonResult, err := promutil.ProcessMetricsToJSON(data, C.GoString(contentType), C.GoString(configJSON))
if err != nil {
*errResult = TrackedCString(err.Error())
return nil
}
return TrackedCString(jsonResult)
}

// httpHeaders returns a http headers including various basic information (User-Agent, Content-Type...).
func httpHeaders() map[string]string {
av, _ := version.Agent()
Expand Down
12 changes: 12 additions & 0 deletions pkg/collector/python/datadog_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ func TestEmitAgentTelemetry(t *testing.T) {
testEmitAgentTelemetry(t)
}

func TestParsePrometheusMetrics(t *testing.T) {
testParsePrometheusMetrics(t)
}

func TestParsePrometheusMetricsError(t *testing.T) {
testParsePrometheusMetricsError(t)
}

func TestProcessPrometheusMetrics(t *testing.T) {
testProcessPrometheusMetrics(t)
}

func TestObfuscaterConfig(t *testing.T) {
testObfuscaterConfig(t)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/collector/python/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ char* ObfuscateMongoDBString(char *, char **);
void EmitAgentTelemetry(char *, char *, double, char *);
void ReportIssue(char *, char *, char **);
void ResolveIssue(char *, char **);
char* ParsePrometheusMetrics(char *, char *, char **);
char* ProcessPrometheusMetrics(char *, char *, char *, char **);

void initDatadogAgentModule(rtloader_t *rtloader) {
set_get_clustername_cb(rtloader, GetClusterName);
Expand All @@ -120,6 +122,8 @@ void initDatadogAgentModule(rtloader_t *rtloader) {
set_emit_agent_telemetry_cb(rtloader, EmitAgentTelemetry);
set_report_issue_cb(rtloader, ReportIssue);
set_resolve_issue_cb(rtloader, ResolveIssue);
set_parse_prometheus_metrics_cb(rtloader, ParsePrometheusMetrics);
set_process_prometheus_metrics_cb(rtloader, ProcessPrometheusMetrics);
}

//
Expand Down
74 changes: 74 additions & 0 deletions pkg/collector/python/test_datadog_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,80 @@ func testEmitAgentTelemetry(t *testing.T) {
assert.True(t, true)
}

func testParsePrometheusMetrics(t *testing.T) {
rawText := C.CString(`# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1234
http_requests_total{method="POST",status="500"} 5`)

var errResult *C.char
result := ParsePrometheusMetrics(rawText, nil, &errResult)
require.Nil(t, errResult)
require.NotNil(t, result)

var families []struct {
Name string `json:"name"`
Type string `json:"type"`
Samples []struct {
Labels map[string]string `json:"labels"`
Value float64 `json:"value"`
Timestamp int64 `json:"timestamp"`
} `json:"samples"`
}
err := json.Unmarshal([]byte(C.GoString(result)), &families)
require.NoError(t, err)
require.Len(t, families, 1)
assert.Equal(t, "http_requests", families[0].Name) // _total stripped for counters
assert.Equal(t, "COUNTER", families[0].Type)
require.Len(t, families[0].Samples, 2)
assert.Equal(t, 1234.0, families[0].Samples[0].Value)
assert.Equal(t, "GET", families[0].Samples[0].Labels["method"])
}

func testParsePrometheusMetricsError(t *testing.T) {
// Invalid prometheus text should return an error
rawText := C.CString(`{invalid`)

var errResult *C.char
result := ParsePrometheusMetrics(rawText, nil, &errResult)
assert.Nil(t, result)
assert.NotNil(t, errResult)
}

func testProcessPrometheusMetrics(t *testing.T) {
rawText := C.CString(`# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1234
http_requests_total{method="POST",status="500"} 5`)

configJSON := C.CString(`{"static_tags":["env:test"],"exclude_labels":["__name__"],"rename_labels":{"method":"http_method"}}`)

var errResult *C.char
result := ProcessPrometheusMetrics(rawText, nil, configJSON, &errResult)
require.Nil(t, errResult)
require.NotNil(t, result)

var processResult struct {
Families []struct {
Name string `json:"name"`
Type string `json:"type"`
Samples []struct {
SampleName string `json:"sample_name"`
Value float64 `json:"value"`
Tags []string `json:"tags"`
Hostname string `json:"hostname"`
} `json:"samples"`
} `json:"families"`
}
err := json.Unmarshal([]byte(C.GoString(result)), &processResult)
require.NoError(t, err)
require.Len(t, processResult.Families, 1)
assert.Equal(t, "http_requests", processResult.Families[0].Name) // _total stripped for counters
assert.Equal(t, "COUNTER", processResult.Families[0].Type)
require.Len(t, processResult.Families[0].Samples, 2)
assert.Equal(t, 1234.0, processResult.Families[0].Samples[0].Value)
assert.Contains(t, processResult.Families[0].Samples[0].Tags, "env:test")
assert.Contains(t, processResult.Families[0].Samples[0].Tags, "http_method:GET")
}

func testObfuscaterConfig(t *testing.T) {
pkgconfigmodel.CleanOverride(t)
_ = pkgconfigmock.New(t)
Expand Down
10 changes: 8 additions & 2 deletions pkg/util/prometheus/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ load("//bazel/rules/go:dd_agent_go_test.bzl", "dd_agent_go_test")

go_library(
name = "prometheus",
srcs = ["parse.go"],
srcs = [
"parse.go",
"process.go",
],
importpath = "github.com/DataDog/datadog-agent/pkg/util/prometheus",
visibility = ["//visibility:public"],
deps = [
Expand All @@ -15,7 +18,10 @@ go_library(

dd_agent_go_test(
name = "prometheus_test",
srcs = ["parse_test.go"],
srcs = [
"parse_test.go",
"process_test.go",
],
embed = [":prometheus"],
deps = [
"@com_github_stretchr_testify//assert",
Expand Down
70 changes: 59 additions & 11 deletions pkg/util/prometheus/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ package prometheus

import (
"bytes"
"encoding/json"
"errors"
"io"
"math"
"strings"

"github.com/prometheus/common/model"
Expand All @@ -24,16 +26,24 @@ type Metric map[string]string

// Sample represents a single metric data point.
type Sample struct {
Metric Metric
Value float64
Timestamp int64 // milliseconds since epoch, 0 if not set
Metric Metric `json:"labels"`
Value float64 `json:"value"`
Timestamp int64 `json:"timestamp"` // milliseconds since epoch, 0 if not set
}

// MetricFamily represents a metric family that is returned by a prometheus endpoint.
type MetricFamily struct {
Name string
Type string
Samples []Sample
Name string `json:"name"`
Type string `json:"type"`
Samples []Sample `json:"samples"`
}

// trimCounterSuffix removes the OpenMetrics counter suffix (_total).
func trimCounterSuffix(name string) string {
if trimmed, ok := strings.CutSuffix(name, "_total"); ok {
return trimmed
}
return name
}

// trimHistogramSuffix removes histogram-specific suffixes (_bucket, _sum, _count).
Expand Down Expand Up @@ -78,12 +88,18 @@ func preprocessData(data []byte, filter []string) []byte {
}

// ParseMetricsWithFilter parses prometheus-formatted metrics from the input data, ignoring lines which contain
// text that matches the passed in filter.
func ParseMetricsWithFilter(data []byte, filter []string) ([]MetricFamily, error) {
// text that matches the passed in filter. The contentType selects the parser: "application/openmetrics-text"
// uses the OpenMetrics parser, anything else uses the Prometheus text parser.
func ParseMetricsWithFilter(data []byte, filter []string, contentType string) ([]MetricFamily, error) {
data = preprocessData(data, filter)

st := labels.NewSymbolTable()
parser := textparse.NewPromParser(data, st, false)
var parser textparse.Parser
if strings.HasPrefix(contentType, "application/openmetrics-text") {
parser = textparse.NewOpenMetricsParser(data, st)
} else {
parser = textparse.NewPromParser(data, st, false)
}

var result []MetricFamily
var lbls labels.Labels
Expand Down Expand Up @@ -112,16 +128,27 @@ func ParseMetricsWithFilter(data []byte, filter []string) ([]MetricFamily, error

case textparse.EntrySeries:
_, ts, value := parser.Series()
// Skip NaN/Inf values — they can't be JSON-encoded and the Python
// scraper already drops them, so omitting them here is safe.
if math.IsNaN(value) || math.IsInf(value, 0) {
continue
}
parser.Labels(&lbls)

rawName := lbls.Get(model.MetricNameLabel)

// Fast path: check if raw name matches current family (common for COUNTER/GAUGE)
if len(result) == 0 || result[len(result)-1].Name != rawName {
// Slow path: try trimming suffix based on current family type
// Slow path: try trimming suffix based on current family type.
// For COUNTER this handles OpenMetrics format where the TYPE line uses
// the base name (e.g. "foo") but series are named "foo_total".
// For HISTOGRAM/SUMMARY, sub-series (_bucket, _sum, _count) must be
// mapped back to the base family name.
name := rawName
if len(result) > 0 {
switch result[len(result)-1].Type {
case "COUNTER":
name = trimCounterSuffix(rawName)
case "HISTOGRAM":
name = trimHistogramSuffix(rawName)
case "SUMMARY":
Expand Down Expand Up @@ -172,5 +199,26 @@ func ParseMetricsWithFilter(data []byte, filter []string) ([]MetricFamily, error

// ParseMetrics parses prometheus-formatted metrics from the input data.
func ParseMetrics(data []byte) ([]MetricFamily, error) {
return ParseMetricsWithFilter(data, nil)
return ParseMetricsWithFilter(data, nil, "")
}

// ParseMetricsToJSON parses prometheus-formatted metrics and returns the result as a JSON string.
// This is used by the Python check bridge to avoid Python-side parsing overhead.
// Counter family names have their _total suffix stripped to match Python prometheus_client (>= 0.14).
func ParseMetricsToJSON(data []byte, contentType string) (string, error) {
families, err := ParseMetricsWithFilter(data, nil, contentType)
if err != nil {
return "", err
}
// Strip _total suffix from counter family names to match Python prometheus_client behavior.
for i := range families {
if families[i].Type == "COUNTER" {
families[i].Name = trimCounterSuffix(families[i].Name)
}
}
out, err := json.Marshal(families)
if err != nil {
return "", err
}
return string(out), nil
}
Loading
Loading