Skip to content

Commit 84e3e1a

Browse files
committed
Add AlwaysRecordSampler
1 parent 5307dd0 commit 84e3e1a

File tree

9 files changed

+160
-0
lines changed

9 files changed

+160
-0
lines changed

opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
__all__ = [
16+
"AlwaysRecordSampler",
1617
"ComposableSampler",
1718
"SamplingIntent",
1819
"composable_always_off",
@@ -25,6 +26,7 @@
2526

2627
from ._always_off import composable_always_off
2728
from ._always_on import composable_always_on
29+
from ._always_record import AlwaysRecordSampler
2830
from ._composable import ComposableSampler, SamplingIntent
2931
from ._parent_threshold import composable_parent_threshold
3032
from ._sampler import composite_sampler
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Optional, Sequence
16+
17+
from opentelemetry.context import Context
18+
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
19+
from opentelemetry.trace import Link, SpanKind
20+
from opentelemetry.trace.span import TraceState
21+
from opentelemetry.util.types import Attributes
22+
23+
24+
class AlwaysRecordSampler(Sampler):
25+
"""
26+
This sampler will return the sampling result of the provided `_root_sampler`, unless the
27+
sampling result contains the sampling decision `Decision.DROP`, in which case, a
28+
new sampling result will be returned that is functionally equivalent to the original, except that
29+
it contains the sampling decision `SamplingDecision.RECORD_ONLY`. This ensures that all
30+
spans are recorded, with no change to sampling.
31+
32+
The intended use case of this sampler is to provide a means of sending all spans to a
33+
processor without having an impact on the sampling rate. This may be desirable if a user wishes
34+
to count or otherwise measure all spans produced in a service, without incurring the cost of 100%
35+
sampling.
36+
"""
37+
38+
_root_sampler: Sampler
39+
40+
def __init__(self, root_sampler: Sampler):
41+
if not root_sampler:
42+
raise ValueError("root_sampler must not be None")
43+
self._root_sampler = root_sampler
44+
45+
def should_sample(
46+
self,
47+
parent_context: Optional["Context"],
48+
trace_id: int,
49+
name: str,
50+
kind: Optional[SpanKind] = None,
51+
attributes: Attributes = None,
52+
links: Optional[Sequence["Link"]] = None,
53+
trace_state: Optional["TraceState"] = None,
54+
) -> "SamplingResult":
55+
result: SamplingResult = self._root_sampler.should_sample(
56+
parent_context,
57+
trace_id,
58+
name,
59+
kind,
60+
attributes,
61+
links,
62+
trace_state,
63+
)
64+
if result.decision is Decision.DROP:
65+
result = _wrap_result_with_record_only_result(result, attributes)
66+
return result
67+
68+
def get_description(self):
69+
return (
70+
"AlwaysRecordSampler{" + self._root_sampler.get_description() + "}"
71+
)
72+
73+
74+
def _wrap_result_with_record_only_result(
75+
result: SamplingResult, attributes: Attributes
76+
) -> SamplingResult:
77+
return SamplingResult(
78+
Decision.RECORD_ONLY,
79+
attributes,
80+
result.trace_state,
81+
)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from unittest import TestCase
2+
from unittest.mock import MagicMock
3+
4+
from opentelemetry.context import Context
5+
from opentelemetry.sdk.trace._sampling_experimental import AlwaysRecordSampler
6+
from opentelemetry.sdk.trace.sampling import (
7+
Decision,
8+
Sampler,
9+
SamplingResult,
10+
StaticSampler,
11+
)
12+
from opentelemetry.trace import SpanKind
13+
from opentelemetry.trace.span import TraceState
14+
from opentelemetry.util.types import Attributes
15+
16+
17+
class TestAlwaysRecordSampler(TestCase):
18+
def setUp(self):
19+
self.mock_sampler: Sampler = MagicMock()
20+
self.sampler: Sampler = AlwaysRecordSampler(self.mock_sampler)
21+
22+
def test_get_description(self):
23+
static_sampler: Sampler = StaticSampler(Decision.DROP)
24+
test_sampler: Sampler = AlwaysRecordSampler(static_sampler)
25+
self.assertEqual(
26+
"AlwaysRecordSampler{AlwaysOffSampler}",
27+
test_sampler.get_description(),
28+
)
29+
30+
def test_record_and_sample_sampling_decision(self):
31+
self.validate_should_sample(
32+
Decision.RECORD_AND_SAMPLE, Decision.RECORD_AND_SAMPLE
33+
)
34+
35+
def test_record_only_sampling_decision(self):
36+
self.validate_should_sample(Decision.RECORD_ONLY, Decision.RECORD_ONLY)
37+
38+
def test_drop_sampling_decision(self):
39+
self.validate_should_sample(Decision.DROP, Decision.RECORD_ONLY)
40+
41+
def validate_should_sample(
42+
self, root_decision: Decision, expected_decision: Decision
43+
):
44+
root_result: SamplingResult = _build_root_sampling_result(
45+
root_decision
46+
)
47+
self.mock_sampler.should_sample.return_value = root_result
48+
actual_result: SamplingResult = self.sampler.should_sample(
49+
parent_context=Context(),
50+
trace_id=0,
51+
name="name",
52+
kind=SpanKind.CLIENT,
53+
attributes={"key": root_decision.name},
54+
trace_state=TraceState(),
55+
)
56+
57+
if root_decision == expected_decision:
58+
self.assertEqual(actual_result, root_result)
59+
self.assertEqual(actual_result.decision, root_decision)
60+
else:
61+
self.assertNotEqual(actual_result, root_result)
62+
self.assertEqual(actual_result.decision, expected_decision)
63+
64+
self.assertEqual(actual_result.attributes, root_result.attributes)
65+
self.assertEqual(actual_result.trace_state, root_result.trace_state)
66+
67+
68+
def _build_root_sampling_result(sampling_decision: Decision):
69+
sampling_attr: Attributes = {"key": sampling_decision.name}
70+
sampling_trace_state: TraceState = TraceState()
71+
sampling_trace_state.add("key", sampling_decision.name)
72+
sampling_result: SamplingResult = SamplingResult(
73+
decision=sampling_decision,
74+
attributes=sampling_attr,
75+
trace_state=sampling_trace_state,
76+
)
77+
return sampling_result

0 commit comments

Comments
 (0)