Skip to content

Commit 9f0a3db

Browse files
jmacdjpkrohling
andauthored
[processor/probabilisticsampling] encoded sampling probability (support OTEP 235) (#31894)
**Description:** Creates new sampler modes named "equalizing" and "proportional". Preserves existing functionality under the mode named "hash_seed". Fixes #31918 This is the final step in a sequence, the whole of this work was factored into 3+ PRs, including the new `pkg/sampling` and the previous step, #31946. The two new Sampler modes enable mixing OTel sampling SDKs with Collectors in a consistent way. The existing hash_seed mode is also a consistent sampling mode, which makes it possible to have a 1:1 mapping between its decisions and the OTEP 235 randomness and threshold values. Specifically, the 14-bit hash value and sampling probability are mapped into 56-bit R-value and T-value encodings, so that all sampling decisions in all modes include threshold information. This implements the semantic conventions of open-telemetry/semantic-conventions#793, namely the `sampling.randomness` and `sampling.threshold` attributes used for logs where there is no tracestate. The default sampling mode remains HashSeed. We consider a future change of default to Proportional to be desirable, because: 1. Sampling probability is the same, only the hashing algorithm changes 2. Proportional respects and preserves information about earlier sampling decisions, which HashSeed can't do, so it has greater interoperability with OTel SDKs which may also adopt OTEP 235 samplers. **Link to tracking Issue:** Draft for open-telemetry/opentelemetry-specification#3602. Previously #24811, see also open-telemetry/oteps#235 Part of #29738 **Testing:** New testing has been added. **Documentation:** ✅ --------- Co-authored-by: Juraci Paixão Kröhling <[email protected]>
1 parent 2d661a7 commit 9f0a3db

17 files changed

+1807
-155
lines changed
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: probabilisticsamplerprocessor
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add Proportional and Equalizing sampling modes
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [31918]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: Both the existing hash_seed mode and the two new modes use OTEP 235 semantic conventions to encode sampling probability.
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

processor/probabilisticsamplerprocessor/README.md

+154-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# Probabilistic Sampling Processor
23

34
<!-- status autogenerated section -->
@@ -115,7 +116,9 @@ interpreted as a percentage, with values >= 100 equal to 100%
115116
sampling. The logs sampling priority attribute is configured via
116117
`sampling_priority`.
117118

118-
## Sampling algorithm
119+
## Mode Selection
120+
121+
There are three sampling modes available. All modes are consistent.
119122

120123
### Hash seed
121124

@@ -135,7 +138,154 @@ In order for hashing to be consistent, all collectors for a given tier
135138
at different collector tiers to support additional sampling
136139
requirements.
137140

138-
This mode uses 14 bits of sampling precision.
141+
This mode uses 14 bits of information in its sampling decision; the
142+
default `sampling_precision`, which is 4 hexadecimal digits, exactly
143+
encodes this information.
144+
145+
This mode is selected by default.
146+
147+
#### Hash seed: Use-cases
148+
149+
The hash seed mode is most useful in logs sampling, because it can be
150+
applied to units of telemetry other than TraceID. For example, a
151+
deployment consisting of 100 pods can be sampled according to the
152+
`service.instance.id` resource attribute. In this case, 10% sampling
153+
implies collecting log records from an expected value of 10 pods.
154+
155+
### Proportional
156+
157+
OpenTelemetry specifies a consistent sampling mechanism using 56 bits
158+
of randomness, which may be obtained from the Trace ID according to
159+
the W3C Trace Context Level 2 specification. Randomness can also be
160+
explicly encoding in the OpenTelemetry `tracestate` field, where it is
161+
known as the R-value.
162+
163+
This mode is named because it reduces the number of items transmitted
164+
proportionally, according to the sampling probability. In this mode,
165+
items are selected for sampling without considering how much they were
166+
already sampled by preceding samplers.
167+
168+
This mode uses 56 bits of information in its calculations. The
169+
default `sampling_precision` (4) will cause thresholds to be rounded
170+
in some cases when they contain more than 16 significant bits.
171+
172+
#### Proportional: Use-cases
173+
174+
The proportional mode is generally applicable in trace sampling,
175+
because it is based on OpenTelemetry and W3C specifications. This
176+
mode is selected by default, because it enforces a predictable
177+
(probabilistic) ratio between incoming items and outgoing items of
178+
telemetry. No matter how SDKs and other sources of telemetry have
179+
been configured with respect to sampling, a collector configured with
180+
25% proportional sampling will output (an expected value of) 1 item
181+
for every 4 items input.
182+
183+
### Equalizing
184+
185+
This mode uses the same randomness mechanism as the propotional
186+
sampling mode, in this case considering how much each item was already
187+
sampled by preceding samplers. This mode can be used to lower
188+
sampling probability to a minimum value across a whole pipeline,
189+
making it possible to conditionally adjust sampling probabilities.
190+
191+
This mode compares a 56 bit threshold against the configured sampling
192+
probability and updates when the threshold is larger. The default
193+
`sampling_precision` (4) will cause updated thresholds to be rounded
194+
in some cases when they contain more than 16 significant bits.
195+
196+
#### Equalizing: Use-cases
197+
198+
The equalizing mode is useful in collector deployments where client
199+
SDKs have mixed sampling configuration and the user wants to apply a
200+
uniform sampling probability across the system. For example, a user's
201+
system consists of mostly components developed in-house, but also some
202+
third-party software. Seeking to lower the overall cost of tracing,
203+
the configures 10% sampling in the samplers for all of their in-house
204+
components. This leaves third-party software components unsampled,
205+
making the savings less than desired. In this case, the user could
206+
configure a 10% equalizing probabilistic sampler. Already-sampled
207+
items of telemetry from the in-house components will pass-through one
208+
for one in this scenario, while items of telemetry from third-party
209+
software will be sampled by the intended amount.
210+
211+
## Sampling threshold information
212+
213+
In all modes, information about the effective sampling probability is
214+
added into the item of telemetry. The random variable that was used
215+
may also be recorded, in case it was not derived from the TraceID
216+
using a standard algorithm.
217+
218+
For traces, threshold and optional randomness information are encoded
219+
in the W3C Trace Context `tracestate` fields. The tracestate is
220+
divided into sections according to a two-character vendor code;
221+
OpenTelemetry uses "ot" as its section designator. Within the
222+
OpenTelemetry section, the sampling threshold is encoded using "th"
223+
and the optional random variable is encoded using "rv".
224+
225+
For example, 25% sampling is encoded in a tracing Span as:
226+
227+
```
228+
tracestate: ot=th:c
229+
```
230+
231+
Users can randomness values in this way, independently, making it
232+
possible to apply consistent sampling across traces for example. If
233+
the Trace was initialized with pre-determined randomness value
234+
`9b8233f7e3a151` and 100% sampling, it would read:
235+
236+
```
237+
tracestate: ot=th:0;rv:9b8233f7e3a151
238+
```
239+
240+
This component, using either proportional or equalizing modes, could
241+
apply 50% sampling the Span. This span with randomness value
242+
`9b8233f7e3a151` is consistently sampled at 50% because the threshold,
243+
when zero padded (i.e., `80000000000000`), is less than the randomess
244+
value. The resulting span will have the following tracestate:
245+
246+
```
247+
tracestate: ot=th:8;rv:9b8233f7e3a151
248+
```
249+
250+
For log records, threshold and randomness information are encoded in
251+
the log record itself, using attributes. For example, 25% sampling
252+
with an explicit randomness value is encoded as:
253+
254+
```
255+
sampling.threshold: c
256+
sampling.randomness: e05a99c8df8d32
257+
```
258+
259+
### Sampling precision
260+
261+
When encoding sampling probability in the form of a threshold,
262+
variable precision is permitted making it possible for the user to
263+
restrict sampling probabilities to rounded numbers of fixed width.
264+
265+
Because the threshold is encoded using hexadecimal digits, each digit
266+
contributes 4 bits of information. One digit of sampling precision
267+
can express exact sampling probabilities 1/16, 2/16, ... through
268+
16/16. Two digits of sampling precision can express exact sampling
269+
probabilities 1/256, 2/256, ... through 256/256. With N digits of
270+
sampling precision, there are exactly `(2^N)-1` exactly representable
271+
probabilities.
272+
273+
Depending on the mode, there are different maximum reasonable settings
274+
for this parameter.
275+
276+
- The `hash_seed` mode uses a 14-bit hash function, therefore
277+
precision 4 completely captures the available information.
278+
- The `equalizing` mode configures a sampling probability after
279+
parsing a `float32` value, which contains 20 bits of precision,
280+
therefore precision 5 completely captures the available information.
281+
- The `proportional` mode configures its ratio using a `float32`
282+
value, however it carries out the arithmetic using 56-bits of
283+
precision. In this mode, increasing precision has the effect
284+
of preserving precision applied by preceding samplers.
285+
286+
In cases where larger precision is configured than is actually
287+
available, the added precision has no effect because trailing zeros
288+
are eliminated by the encoding.
139289

140290
### Error handling
141291

@@ -153,9 +303,11 @@ false, in which case erroneous data will pass through the processor.
153303

154304
The following configuration options can be modified:
155305

306+
- `mode` (string, optional): One of "proportional", "equalizing", or "hash_seed"; the default is "proportional" unless either `hash_seed` is configured or `attribute_source` is set to `record`.
156307
- `sampling_percentage` (32-bit floating point, required): Percentage at which items are sampled; >= 100 samples all items, 0 rejects all items.
157308
- `hash_seed` (32-bit unsigned integer, optional, default = 0): An integer used to compute the hash algorithm. Note that all collectors for a given tier (e.g. behind the same load balancer) should have the same hash_seed.
158309
- `fail_closed` (boolean, optional, default = true): Whether to reject items with sampling-related errors.
310+
- `sampling_precision` (integer, optional, default = 4): Determines the number of hexadecimal digits used to encode the sampling threshold. Permitted values are 1..14.
159311

160312
### Logs-specific configuration
161313

processor/probabilisticsamplerprocessor/config.go

+63-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ package probabilisticsamplerprocessor // import "github.com/open-telemetry/opent
55

66
import (
77
"fmt"
8+
"math"
89

910
"go.opentelemetry.io/collector/component"
11+
12+
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling"
1013
)
1114

1215
type AttributeSource string
@@ -35,6 +38,33 @@ type Config struct {
3538
// different sampling rates, configuring different seeds avoids that.
3639
HashSeed uint32 `mapstructure:"hash_seed"`
3740

41+
// Mode selects the sampling behavior. Supported values:
42+
//
43+
// - "hash_seed": the legacy behavior of this processor.
44+
// Using an FNV hash combined with the HashSeed value, this
45+
// sampler performs a non-consistent probabilistic
46+
// downsampling. The number of spans output is expected to
47+
// equal SamplingPercentage (as a ratio) times the number of
48+
// spans inpout, assuming good behavior from FNV and good
49+
// entropy in the hashed attributes or TraceID.
50+
//
51+
// - "equalizing": Using an OTel-specified consistent sampling
52+
// mechanism, this sampler selectively reduces the effective
53+
// sampling probability of arriving spans. This can be
54+
// useful to select a small fraction of complete traces from
55+
// a stream with mixed sampling rates. The rate of spans
56+
// passing through depends on how much sampling has already
57+
// been applied. If an arriving span was head sampled at
58+
// the same probability it passes through. If the span
59+
// arrives with lower probability, a warning is logged
60+
// because it means this sampler is configured with too
61+
// large a sampling probability to ensure complete traces.
62+
//
63+
// - "proportional": Using an OTel-specified consistent sampling
64+
// mechanism, this sampler reduces the effective sampling
65+
// probability of each span by `SamplingProbability`.
66+
Mode SamplerMode `mapstructure:"mode"`
67+
3868
// FailClosed indicates to not sample data (the processor will
3969
// fail "closed") in case of error, such as failure to parse
4070
// the tracestate field or missing the randomness attribute.
@@ -45,6 +75,14 @@ type Config struct {
4575
// despite errors using priority.
4676
FailClosed bool `mapstructure:"fail_closed"`
4777

78+
// SamplingPrecision is how many hex digits of sampling
79+
// threshold will be encoded, from 1 up to 14. Default is 4.
80+
// 0 is treated as full precision.
81+
SamplingPrecision int `mapstructure:"sampling_precision"`
82+
83+
///////
84+
// Logs only fields below.
85+
4886
// AttributeSource (logs only) defines where to look for the attribute in from_attribute. The allowed values are
4987
// `traceID` or `record`. Default is `traceID`.
5088
AttributeSource `mapstructure:"attribute_source"`
@@ -61,11 +99,34 @@ var _ component.Config = (*Config)(nil)
6199

62100
// Validate checks if the processor configuration is valid
63101
func (cfg *Config) Validate() error {
64-
if cfg.SamplingPercentage < 0 {
65-
return fmt.Errorf("negative sampling rate: %.2f", cfg.SamplingPercentage)
102+
pct := float64(cfg.SamplingPercentage)
103+
104+
if math.IsInf(pct, 0) || math.IsNaN(pct) {
105+
return fmt.Errorf("sampling rate is invalid: %f%%", cfg.SamplingPercentage)
106+
}
107+
ratio := pct / 100.0
108+
109+
switch {
110+
case ratio < 0:
111+
return fmt.Errorf("sampling rate is negative: %f%%", cfg.SamplingPercentage)
112+
case ratio == 0:
113+
// Special case
114+
case ratio < sampling.MinSamplingProbability:
115+
// Too-small case
116+
return fmt.Errorf("sampling rate is too small: %g%%", cfg.SamplingPercentage)
117+
default:
118+
// Note that ratio > 1 is specifically allowed by the README, taken to mean 100%
66119
}
120+
67121
if cfg.AttributeSource != "" && !validAttributeSource[cfg.AttributeSource] {
68122
return fmt.Errorf("invalid attribute source: %v. Expected: %v or %v", cfg.AttributeSource, traceIDAttributeSource, recordAttributeSource)
69123
}
124+
125+
if cfg.SamplingPrecision == 0 {
126+
return fmt.Errorf("invalid sampling precision: 0")
127+
} else if cfg.SamplingPrecision > sampling.NumHexDigits {
128+
return fmt.Errorf("sampling precision is too great, should be <= 14: %d", cfg.SamplingPrecision)
129+
}
130+
70131
return nil
71132
}

processor/probabilisticsamplerprocessor/config_test.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ func TestLoadConfig(t *testing.T) {
2626
id: component.NewIDWithName(metadata.Type, ""),
2727
expected: &Config{
2828
SamplingPercentage: 15.3,
29+
SamplingPrecision: 4,
30+
Mode: "proportional",
2931
AttributeSource: "traceID",
3032
FailClosed: true,
3133
},
@@ -34,7 +36,9 @@ func TestLoadConfig(t *testing.T) {
3436
id: component.NewIDWithName(metadata.Type, "logs"),
3537
expected: &Config{
3638
SamplingPercentage: 15.3,
39+
SamplingPrecision: defaultPrecision,
3740
HashSeed: 22,
41+
Mode: "",
3842
AttributeSource: "record",
3943
FromAttribute: "foo",
4044
SamplingPriority: "bar",
@@ -68,7 +72,11 @@ func TestLoadInvalidConfig(t *testing.T) {
6872
file string
6973
contains string
7074
}{
71-
{"invalid_negative.yaml", "negative sampling rate"},
75+
{"invalid_negative.yaml", "sampling rate is negative"},
76+
{"invalid_small.yaml", "sampling rate is too small"},
77+
{"invalid_inf.yaml", "sampling rate is invalid: +Inf%"},
78+
{"invalid_prec.yaml", "sampling precision is too great"},
79+
{"invalid_zero.yaml", "invalid sampling precision"},
7280
} {
7381
t.Run(test.file, func(t *testing.T) {
7482
factories, err := otelcoltest.NopFactories()

processor/probabilisticsamplerprocessor/factory.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ func NewFactory() processor.Factory {
4040

4141
func createDefaultConfig() component.Config {
4242
return &Config{
43-
AttributeSource: defaultAttributeSource,
44-
FailClosed: true,
43+
AttributeSource: defaultAttributeSource,
44+
FailClosed: true,
45+
Mode: modeUnset,
46+
SamplingPrecision: defaultPrecision,
4547
}
4648
}
4749

processor/probabilisticsamplerprocessor/internal/metadata/generated_telemetry_test.go

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)