Skip to content

[AGTMETRICS-309] add support for tag cardinality#327

Merged
brianna-wang-dd merged 68 commits intomasterfrom
brianna.wang/tag-cardinality
Jul 21, 2025
Merged

[AGTMETRICS-309] add support for tag cardinality#327
brianna-wang-dd merged 68 commits intomasterfrom
brianna.wang/tag-cardinality

Conversation

@brianna-wang-dd
Copy link
Contributor

@brianna-wang-dd brianna-wang-dd commented Jul 7, 2025

This PR implements tag cardinality for this library by sending a cardinality field for each metric as card:<cardinality> field. Possible tag cardinality values are none, low, orchestrator, or high.
system.mangos:21|g#foo:bar,baz:boo|card:high

The tag cardinality can be specified as a global setting (through the user passing the option to the constructor when setting up Statsd or by reading the value from DD_CARDINALITY or DATADOG_CARDINALITY). This can be overridden for each metric when creating one.

In order to ensure backward compatibility, a variadic parameters are added to each metric function that allow the tag cardinality to be passed in as type CardinalityParameter.

This PR was tested with unit test for tag cardinality specific functionality (such as global initialization from the 3 possible sources, checking the validity of inputs, and getting the global tag cardinality) and general metric functions (which included testing the metric-specific overrides for the tag cardinality).

StephenWakely
StephenWakely previously approved these changes Jul 17, 2025
Copy link
Contributor

@StephenWakely StephenWakely left a comment

Choose a reason for hiding this comment

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

Nicely done! I just have a few comments which are pretty pedantic so shouldn't block the PR, but feel free to address!

Copy link
Contributor

@StephenWakely StephenWakely left a comment

Choose a reason for hiding this comment

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

Perfect!

@brianna-wang-dd brianna-wang-dd merged commit a6b9cdf into master Jul 21, 2025
35 checks passed
@brianna-wang-dd brianna-wang-dd deleted the brianna.wang/tag-cardinality branch July 21, 2025 14:35
func (a *aggregator) count(name string, value int64, tags []string) error {
context := getContext(name, tags)
func (a *aggregator) count(name string, value int64, tags []string, cardinality Cardinality) error {
resolvedCardinality := resolveCardinality(cardinality)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is redundant

Comment on lines +81 to +90
func parseTagCardinality(parameters []Parameter) Cardinality {
cardinality := CardinalityNotSet
for _, o := range parameters {
c, ok := o.(Cardinality)
if ok {
cardinality = c
}
}
return resolveCardinality(cardinality)
}
Copy link
Contributor

@AlexanderYastrebov AlexanderYastrebov Dec 2, 2025

Choose a reason for hiding this comment

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

This is called in the hot path and has to obtain global lock to get the default value which has high performance hit. Another problem is that second client with different options (o.tagCardinality) would override global default.

A better approach could be to get rid of global value and store default cardinality from environment inside of the client structure:

diff --git a/statsd/aggregator.go b/statsd/aggregator.go
index 03629d2..283b39b 100644
--- a/statsd/aggregator.go
+++ b/statsd/aggregator.go
@@ -224,8 +224,7 @@ func getContextAndTags(name string, tags []string, cardinality Cardinality) (str
 }
 
 func (a *aggregator) count(name string, value int64, tags []string, cardinality Cardinality) error {
-       resolvedCardinality := resolveCardinality(cardinality)
-       context := getContext(name, tags, resolvedCardinality)
+       context := getContext(name, tags, cardinality)
        a.countsM.RLock()
        if count, found := a.counts[context]; found {
                count.sample(value)
@@ -234,10 +233,10 @@ func (a *aggregator) count(name string, value int64, tags []string, cardinality
        }
        a.countsM.RUnlock()
 
-       metric := newCountMetric(name, value, tags, resolvedCardinality)
+       metric := newCountMetric(name, value, tags, cardinality)
 
        a.countsM.Lock()
-       // Check if another goroutines hasn't created the value betwen the RUnlock and 'Lock'
+       // Check if another goroutines hasn't created the value between the RUnlock and 'Lock'
        if count, found := a.counts[context]; found {
                count.sample(value)
                a.countsM.Unlock()
diff --git a/statsd/statsd_benchmark_test.go b/statsd/statsd_benchmark_test.go
index 35ea6ed..f03566f 100644
--- a/statsd/statsd_benchmark_test.go
+++ b/statsd/statsd_benchmark_test.go
@@ -86,7 +86,7 @@ func benchmarkStatsdDifferentMetrics(b *testing.B, transport string, extraOption
                name := fmt.Sprintf("test.metric%d", testNumber)
                tags := []string{"tag:tag"}
                for pb.Next() {
-                       client.Gauge(name, 1, tags, 1)
+                       client.Count(name, 1, tags, 1)
                }
        })
        client.Flush()
@@ -106,7 +106,7 @@ func benchmarkStatsdSameMetrics(b *testing.B, transport string, extraOptions ...
        b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                        tags := []string{"tag:tag"}
-                       client.Gauge("test.metric", 1, tags, 1)
+                       client.Count("test.metric", 1, tags, 1)
                }
        })
        client.Flush()
diff --git a/statsd/statsdex.go b/statsd/statsdex.go
index d9e9b4e..ffba55f 100644
--- a/statsd/statsdex.go
+++ b/statsd/statsdex.go
@@ -300,6 +300,7 @@ type ClientEx struct {
        errorOnBlockedChannel bool
        errorHandler          ErrorHandler
        originDetection       bool
+       defaultTagCardinality Cardinality
 }
 
 // statsdTelemetry contains telemetry metrics about the client
@@ -479,6 +480,7 @@ func newWithWriter(w Transport, o *Options, writerName string) (*ClientEx, error
 
        // Initializes the global tag cardinality with either the value passed in by the user or the value from the DD_CARDINALITY/DATADOG_CARDINALITY environment variable.
        initTagCardinality(o.tagCardinality)
+       c.defaultTagCardinality = CardinalityNotSet // TODO o.tagCardinality
 
        initContainerID(o.containerID, fillInContainerID(o), isHostCgroupNamespace())
        isUDS := writerName == writerNameUDS
@@ -738,7 +740,7 @@ func (c *ClientEx) Count(name string, value int64, tags []string, rate float64,
                return ErrNoClient
        }
        atomic.AddUint64(&c.telemetry.totalMetricsCount, 1)
-       cardinality := parseTagCardinality(parameters)
+       cardinality := parseTagCardinality2(parameters, c.defaultTagCardinality)
        if c.agg != nil {
                return c.agg.count(name, value, tags, cardinality)
        }
diff --git a/statsd/tag_cardinality.go b/statsd/tag_cardinality.go
index 1289e47..524c013 100644
--- a/statsd/tag_cardinality.go
+++ b/statsd/tag_cardinality.go
@@ -97,3 +97,13 @@ func resolveCardinality(card Cardinality) Cardinality {
        }
        return card
 }
+
+func parseTagCardinality2(parameters []Parameter, defaultCardinality Cardinality) Cardinality {
+       for _, o := range parameters {
+               c, ok := o.(Cardinality)
+               if ok {
+                       return c
+               }
+       }
+       return defaultCardinality
+}

Copy link
Contributor

Choose a reason for hiding this comment

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

Here is benchstat for one of the benchmarks for the above:

goos: darwin
goarch: arm64
pkg: github.com/DataDog/datadog-go/v5/statsd
cpu: Apple M4 Max
                                       │   HEAD~1    │                HEAD                 │
                                       │   sec/op    │   sec/op     vs base                │
StatsdUDPSameMetricMutexAggregation-16   254.8n ± 0%   156.3n ± 0%  -38.65% (p=0.000 n=10)

                                       │   HEAD~1   │              HEAD              │
                                       │ %_dropRate │ %_dropRate  vs base            │
StatsdUDPSameMetricMutexAggregation-16   0.000 ± 0%   0.000 ± 0%  ~ (p=1.000 n=10) ¹
¹ all samples are equal

Copy link
Contributor

Choose a reason for hiding this comment

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

Created #346 to address.

AlexanderYastrebov added a commit to AlexanderYastrebov/datadog-go that referenced this pull request Dec 19, 2025
The change DataDog#327 introduced support for tag cardinality
with global default value that could be overriden per metric call via parameter.

Default cardinality value is stored in a global variable
guarded by a mutex which has two downsides:
 * performance impact - each measure call has to obtain mutex to get the default value
 * two clients created with different default cardinality override the same global value

This change eliminates global default value and uses default value per statsd client
specified during client construction with a fallback to environment variables.

User supplied cardinality value is validated during client construction and
during parameter parsing with a fallback to default value such that all
internal components can assume they always receive a valid value.

Performance improvement (via `Statsd.*Aggregation` benchmarks):
```
goos: darwin
goarch: arm64
pkg: github.com/DataDog/datadog-go/v5/statsd
cpu: Apple M4 Max
                                              │   master    │                HEAD                 │
                                              │   sec/op    │   sec/op     vs base                │
StatsdUDPSameMetricMutexAggregation-16          248.3n ± 1%   142.3n ± 0%  -42.68% (p=0.000 n=10)
StatsdUDPSameMetricChannelAggregation-16        252.3n ± 0%   143.8n ± 1%  -43.02% (p=0.000 n=10)
StatsdUDPDifferentMetricMutexAggregation-16     250.7n ± 1%   148.6n ± 0%  -40.71% (p=0.000 n=10)
StatsdUDPDifferentMetricChannelAggregation-16   252.5n ± 1%   150.3n ± 0%  -40.48% (p=0.000 n=10)
StatsdUDSSameMetricMutexAggregation-16          248.9n ± 1%   143.0n ± 1%  -42.53% (p=0.000 n=10)
StatsdUDSSameMetricChannelAggregation-16        251.9n ± 1%   144.0n ± 1%  -42.87% (p=0.000 n=10)
StatsdUDPSifferentMetricMutexAggregation-16     252.1n ± 0%   148.8n ± 1%  -40.97% (p=0.000 n=10)
StatsdUDSDifferentMetricChannelAggregation-16   252.8n ± 1%   149.5n ± 0%  -40.85% (p=0.000 n=10)
geomean                                         251.2n        146.3n       -41.77%

                                              │    master    │                HEAD                 │
                                              │  %_dropRate  │ %_dropRate  vs base                 │
StatsdUDPSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                    ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```
AlexanderYastrebov added a commit to AlexanderYastrebov/datadog-go that referenced this pull request Jan 8, 2026
The change DataDog#327 introduced support for tag cardinality
with global default value that could be overriden per metric call via parameter.

Default cardinality value is stored in a global variable
guarded by a mutex which has two downsides:
 * performance impact - each measure call has to obtain mutex to get the default value
 * two clients created with different default cardinality override the same global value

This change eliminates global default value and uses default value per statsd client
specified during client construction with a fallback to environment variables.

User supplied cardinality value is validated during client construction and
during parameter parsing with a fallback to default value such that all
internal components can assume they always receive a valid value.

Performance improvement (via `Statsd.*Aggregation` benchmarks):
```
goos: darwin
goarch: arm64
pkg: github.com/DataDog/datadog-go/v5/statsd
cpu: Apple M4 Max
                                              │   master    │                HEAD                 │
                                              │   sec/op    │   sec/op     vs base                │
StatsdUDPSameMetricMutexAggregation-16          248.3n ± 1%   142.3n ± 0%  -42.68% (p=0.000 n=10)
StatsdUDPSameMetricChannelAggregation-16        252.3n ± 0%   143.8n ± 1%  -43.02% (p=0.000 n=10)
StatsdUDPDifferentMetricMutexAggregation-16     250.7n ± 1%   148.6n ± 0%  -40.71% (p=0.000 n=10)
StatsdUDPDifferentMetricChannelAggregation-16   252.5n ± 1%   150.3n ± 0%  -40.48% (p=0.000 n=10)
StatsdUDSSameMetricMutexAggregation-16          248.9n ± 1%   143.0n ± 1%  -42.53% (p=0.000 n=10)
StatsdUDSSameMetricChannelAggregation-16        251.9n ± 1%   144.0n ± 1%  -42.87% (p=0.000 n=10)
StatsdUDPSifferentMetricMutexAggregation-16     252.1n ± 0%   148.8n ± 1%  -40.97% (p=0.000 n=10)
StatsdUDSDifferentMetricChannelAggregation-16   252.8n ± 1%   149.5n ± 0%  -40.85% (p=0.000 n=10)
geomean                                         251.2n        146.3n       -41.77%

                                              │    master    │                HEAD                 │
                                              │  %_dropRate  │ %_dropRate  vs base                 │
StatsdUDPSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                    ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```
StephenWakely pushed a commit that referenced this pull request Jan 8, 2026
* statsd: remove global cardinality

The change #327 introduced support for tag cardinality
with global default value that could be overriden per metric call via parameter.

Default cardinality value is stored in a global variable
guarded by a mutex which has two downsides:
 * performance impact - each measure call has to obtain mutex to get the default value
 * two clients created with different default cardinality override the same global value

This change eliminates global default value and uses default value per statsd client
specified during client construction with a fallback to environment variables.

User supplied cardinality value is validated during client construction and
during parameter parsing with a fallback to default value such that all
internal components can assume they always receive a valid value.

Performance improvement (via `Statsd.*Aggregation` benchmarks):
```
goos: darwin
goarch: arm64
pkg: github.com/DataDog/datadog-go/v5/statsd
cpu: Apple M4 Max
                                              │   master    │                HEAD                 │
                                              │   sec/op    │   sec/op     vs base                │
StatsdUDPSameMetricMutexAggregation-16          248.3n ± 1%   142.3n ± 0%  -42.68% (p=0.000 n=10)
StatsdUDPSameMetricChannelAggregation-16        252.3n ± 0%   143.8n ± 1%  -43.02% (p=0.000 n=10)
StatsdUDPDifferentMetricMutexAggregation-16     250.7n ± 1%   148.6n ± 0%  -40.71% (p=0.000 n=10)
StatsdUDPDifferentMetricChannelAggregation-16   252.5n ± 1%   150.3n ± 0%  -40.48% (p=0.000 n=10)
StatsdUDSSameMetricMutexAggregation-16          248.9n ± 1%   143.0n ± 1%  -42.53% (p=0.000 n=10)
StatsdUDSSameMetricChannelAggregation-16        251.9n ± 1%   144.0n ± 1%  -42.87% (p=0.000 n=10)
StatsdUDPSifferentMetricMutexAggregation-16     252.1n ± 0%   148.8n ± 1%  -40.97% (p=0.000 n=10)
StatsdUDSDifferentMetricChannelAggregation-16   252.8n ± 1%   149.5n ± 0%  -40.85% (p=0.000 n=10)
geomean                                         251.2n        146.3n       -41.77%

                                              │    master    │                HEAD                 │
                                              │  %_dropRate  │ %_dropRate  vs base                 │
StatsdUDPSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricMutexAggregation-16          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSSameMetricChannelAggregation-16        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDPSifferentMetricMutexAggregation-16     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
StatsdUDSDifferentMetricChannelAggregation-16   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                    ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```

* statsd: set CardinalityNotSet to zero

Set CardinalityNotSet to zero to avoid gap between it and CardinalityNone which equals to one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants