Skip to content

statsd: remove global cardinality#346

Merged
StephenWakely merged 2 commits intoDataDog:masterfrom
AlexanderYastrebov:alexander.yastrebov/remove-global-cardinality
Jan 8, 2026
Merged

statsd: remove global cardinality#346
StephenWakely merged 2 commits intoDataDog:masterfrom
AlexanderYastrebov:alexander.yastrebov/remove-global-cardinality

Conversation

@AlexanderYastrebov
Copy link
Contributor

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

@AlexanderYastrebov AlexanderYastrebov requested a review from a team as a code owner December 19, 2025 15:29
func WithCardinality(card Cardinality) Option {
return func(o *Options) error {
o.tagCardinality = card
if !card.isValid() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we ensure user can never pass invalid cardinality to the client during construction.

channelModeErrorsWhenFull bool
errorHandler ErrorHandler
tagCardinality Cardinality
tagCardinality *Cardinality
Copy link
Contributor Author

Choose a reason for hiding this comment

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

nil is default unset

Copy link
Contributor

Choose a reason for hiding this comment

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

If we redefine Cardinality as above so that NotSet is 0, then we don't need to worry about pointers and nil errors. I think this would be cleaner.

Copy link
Contributor Author

@AlexanderYastrebov AlexanderYastrebov Jan 8, 2026

Choose a reason for hiding this comment

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

I agree -1 enum value is annoying.
Here pointer is used to signal that value was explicitly set via options, e.g. for the usecase when there is an env variable DD_CARDINALITY=low and user wants to unset cardinality via WithCardinality(CardinalityNotSet)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's a good point.

Comment on lines +481 to +487
if o.tagCardinality != nil {
c.defaultCardinality = *o.tagCardinality
} else if card, ok := envTagCardinality(); ok {
c.defaultCardinality = card
} else {
c.defaultCardinality = CardinalityNotSet
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we ensure that c.defaultCardinality is always a valid value.

c, ok := o.(Cardinality)
if ok {
cardinality = c
if ok && c.isValid() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we ensure that cardinality is always valid when passed via parameter.

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.

Yes this is much cleaner. Thank you. I just have a couple of minor nits which I think would make things a little cleaner.

)

func (c Cardinality) isValid() bool {
return c >= CardinalityNotSet && c <= CardinalityHigh
Copy link
Contributor

Choose a reason for hiding this comment

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

A Cardinality value of 0 is also not valid, due to the way the consts are setup with iota.

Suggested change
return c >= CardinalityNotSet && c <= CardinalityHigh
return c >= CardinalityNotSet && c <= CardinalityHigh && c != 0

Alternatively, I can't really think of a reason why NotSet has to be -1, so we could possibly just change the definition to make it start at 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A Cardinality value of 0 is also not valid

This method checks user input for options and per metrics call to ensure a supported value is passed; maybe its better to rename into isSupported.

The CardinalityNotSet vs CardinalityNone split is shady and the difference is not clear.

Existing code checks string value for emptiness which is not empty for CardinalityNone 🙈 :

// getContextAndTags returns the context and tags for a metric name, tags, and cardinality.
//
// See getContext for usage for context
// The tags are the tags separated by a separator symbol and can be re-used to pass down to the writer
func getContextAndTags(name string, tags []string, cardinality Cardinality) (string, string) {
cardString := cardinality.String()
if len(tags) == 0 {
if cardString == "" {
return name, ""
}
return name + nameSeparatorSymbol + cardinality.String(), ""
}
n := len(name) + len(nameSeparatorSymbol) + len(tagSeparatorSymbol)*(len(tags)-1)
for _, s := range tags {
n += len(s)
}
var cardStringLen = 0
if cardString != "" {
n += len(cardString) + len(cardSeparatorSymbol)
cardStringLen = len(cardString) + len(cardSeparatorSymbol)
}
var sb strings.Builder
sb.Grow(n)
sb.WriteString(name)
sb.WriteString(nameSeparatorSymbol)
if cardString != "" {
sb.WriteString(cardString)
sb.WriteString(cardSeparatorSymbol)
}
sb.WriteString(tags[0])
for _, s := range tags[1:] {
sb.WriteString(tagSeparatorSymbol)
sb.WriteString(s)
}
s := sb.String()
return s, s[len(name)+len(nameSeparatorSymbol)+cardStringLen:]
}

Is the CardinalityNone ("none") a valid value to use in the aggregation context?
Also CardinalityNotSet is exported so I am not sure we can drop it easily.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A Cardinality value of 0 is also not valid, due to the way the consts are setup with iota.

I see now what you mean
image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made CardinalityNotSet equal to zero to avoid gap between it and CardinalityNone.

channelModeErrorsWhenFull bool
errorHandler ErrorHandler
tagCardinality Cardinality
tagCardinality *Cardinality
Copy link
Contributor

Choose a reason for hiding this comment

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

If we redefine Cardinality as above so that NotSet is 0, then we don't need to worry about pointers and nil errors. I think this would be cleaner.

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 AlexanderYastrebov force-pushed the alexander.yastrebov/remove-global-cardinality branch from f9627ac to a834205 Compare January 8, 2026 10:30
Set CardinalityNotSet to zero to avoid gap between it and CardinalityNone which equals to one.
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.

Thank you!

@StephenWakely StephenWakely merged commit 411ddaf into DataDog:master Jan 8, 2026
42 checks passed
@AlexanderYastrebov AlexanderYastrebov deleted the alexander.yastrebov/remove-global-cardinality branch January 8, 2026 15:56
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.

2 participants