Skip to content

Commit 7d58c22

Browse files
committed
Enable custom sampler configuration via env vars
1 parent 6f6f8d1 commit 7d58c22

File tree

6 files changed

+191
-37
lines changed

6 files changed

+191
-37
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0...HEAD)
99

10+
- Enabled custom samplers via entry points
11+
([#2972](https://github.com/open-telemetry/opentelemetry-python/pull/2972))
1012
- Update explicit histogram bucket boundaries
1113
([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947))
1214

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

+1-21
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from os import environ
2424
from typing import Dict, Optional, Sequence, Tuple, Type
2525

26-
from pkg_resources import iter_entry_points
2726
from typing_extensions import Literal
2827

2928
from opentelemetry.environment_variables import (
@@ -55,6 +54,7 @@
5554
from opentelemetry.sdk.trace import TracerProvider
5655
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
5756
from opentelemetry.sdk.trace.id_generator import IdGenerator
57+
from opentelemetry.sdk.util import _import_config_components
5858
from opentelemetry.semconv.resource import ResourceAttributes
5959
from opentelemetry.trace import set_tracer_provider
6060

@@ -228,26 +228,6 @@ def _init_logging(
228228
logging.getLogger().addHandler(handler)
229229

230230

231-
def _import_config_components(
232-
selected_components, entry_point_name
233-
) -> Sequence[Tuple[str, object]]:
234-
component_entry_points = {
235-
ep.name: ep for ep in iter_entry_points(entry_point_name)
236-
}
237-
component_impls = []
238-
for selected_component in selected_components:
239-
entry_point = component_entry_points.get(selected_component, None)
240-
if not entry_point:
241-
raise RuntimeError(
242-
f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'"
243-
)
244-
245-
component_impl = entry_point.load()
246-
component_impls.append((selected_component, component_impl))
247-
248-
return component_impls
249-
250-
251231
def _import_exporters(
252232
trace_exporter_names: Sequence[str],
253233
metric_exporter_names: Sequence[str],

opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py

+61-11
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
...
6565
6666
The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable).
67-
The list of known values for ``OTEL_TRACES_SAMPLER`` are:
67+
The list of built-in values for ``OTEL_TRACES_SAMPLER`` are:
6868
6969
* always_on - Sampler that always samples spans, regardless of the parent span's sampling decision.
7070
* always_off - Sampler that never samples spans, regardless of the parent span's sampling decision.
@@ -73,7 +73,24 @@
7373
* parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples.
7474
* parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabalistically based on rate.
7575
76-
Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio, when not provided rate will be set to 1.0 (maximum rate possible).
76+
In order to configure a custom sampler via environment variables, create an entry point for the custom sampler class under the entry point group, ``opentelemtry_traces_sampler``. Then, set the ``OTEL_TRACES_SAMPLER`` environment variable to the key name of the entry point. For example, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5`` after creating the following entry point:
77+
78+
.. code:: python
79+
80+
setup(
81+
...
82+
entry_points={
83+
...
84+
"opentelemtry_traces_sampler": [
85+
"custom_sampler_name = path.to.sampler.module:CustomSampler"
86+
]
87+
}
88+
)
89+
...
90+
class CustomSampler(TraceIdRatioBased):
91+
...
92+
93+
Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is a ``TraceIdRatioBased`` Sampler, such as ``traceidratio`` and ``parentbased_traceidratio``. When not provided rate will be set to 1.0 (maximum rate possible).
7794
7895
7996
Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``.
@@ -111,10 +128,13 @@
111128
OTEL_TRACES_SAMPLER,
112129
OTEL_TRACES_SAMPLER_ARG,
113130
)
131+
from opentelemetry.sdk.util import _import_config_components
114132
from opentelemetry.trace import Link, SpanKind, get_current_span
115133
from opentelemetry.trace.span import TraceState
116134
from opentelemetry.util.types import Attributes
117135

136+
# from opentelemetry.sdk._configuration import _import_config_components
137+
118138
_logger = getLogger(__name__)
119139

120140

@@ -161,6 +181,9 @@ def __init__(
161181
self.trace_state = trace_state
162182

163183

184+
_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemtry_traces_sampler"
185+
186+
164187
class Sampler(abc.ABC):
165188
@abc.abstractmethod
166189
def should_sample(
@@ -350,7 +373,7 @@ def get_description(self):
350373
"""Sampler that respects its parent span's sampling decision, but otherwise always samples."""
351374

352375

353-
class ParentBasedTraceIdRatio(ParentBased):
376+
class ParentBasedTraceIdRatio(ParentBased, TraceIdRatioBased):
354377
"""
355378
Sampler that respects its parent span's sampling decision, but otherwise
356379
samples probabalistically based on `rate`.
@@ -361,37 +384,64 @@ def __init__(self, rate: float):
361384
super().__init__(root=root)
362385

363386

364-
_KNOWN_SAMPLERS = {
387+
_KNOWN_INITIALIZED_SAMPLERS = {
365388
"always_on": ALWAYS_ON,
366389
"always_off": ALWAYS_OFF,
367390
"parentbased_always_on": DEFAULT_ON,
368391
"parentbased_always_off": DEFAULT_OFF,
392+
}
393+
394+
_KNOWN_SAMPLER_CLASSES = {
369395
"traceidratio": TraceIdRatioBased,
370396
"parentbased_traceidratio": ParentBasedTraceIdRatio,
371397
}
372398

373399

374400
def _get_from_env_or_default() -> Sampler:
375-
trace_sampler = os.getenv(
401+
trace_sampler_name = os.getenv(
376402
OTEL_TRACES_SAMPLER, "parentbased_always_on"
377403
).lower()
378-
if trace_sampler not in _KNOWN_SAMPLERS:
379-
_logger.warning("Couldn't recognize sampler %s.", trace_sampler)
380-
trace_sampler = "parentbased_always_on"
381404

382-
if trace_sampler in ("traceidratio", "parentbased_traceidratio"):
405+
if trace_sampler_name in _KNOWN_INITIALIZED_SAMPLERS:
406+
return _KNOWN_INITIALIZED_SAMPLERS[trace_sampler_name]
407+
408+
trace_sampler_impl = None
409+
if trace_sampler_name in _KNOWN_SAMPLER_CLASSES:
410+
trace_sampler_impl = _KNOWN_SAMPLER_CLASSES[trace_sampler_name]
411+
else:
412+
try:
413+
trace_sampler_impl = _import_sampler(trace_sampler_name)
414+
except RuntimeError as err:
415+
_logger.warning(
416+
"Unable to recognize sampler %s: %s", trace_sampler_name, err
417+
)
418+
return _KNOWN_INITIALIZED_SAMPLERS["parentbased_always_on"]
419+
420+
if issubclass(trace_sampler_impl, TraceIdRatioBased):
383421
try:
384422
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG))
385423
except ValueError:
386424
_logger.warning("Could not convert TRACES_SAMPLER_ARG to float.")
387425
rate = 1.0
388-
return _KNOWN_SAMPLERS[trace_sampler](rate)
426+
return trace_sampler_impl(rate)
389427

390-
return _KNOWN_SAMPLERS[trace_sampler]
428+
return trace_sampler_impl()
391429

392430

393431
def _get_parent_trace_state(parent_context) -> Optional["TraceState"]:
394432
parent_span_context = get_current_span(parent_context).get_span_context()
395433
if parent_span_context is None or not parent_span_context.is_valid:
396434
return None
397435
return parent_span_context.trace_state
436+
437+
438+
def _import_sampler(sampler_name: str) -> Sampler:
439+
# pylint: disable=unbalanced-tuple-unpacking
440+
[(sampler_name, sampler_impl)] = _import_config_components(
441+
[sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP
442+
)
443+
444+
if issubclass(sampler_impl, Sampler):
445+
return sampler_impl
446+
447+
raise RuntimeError(f"{sampler_name} is not an Sampler")

opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py

+25-4
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
import datetime
1616
import threading
1717
from collections import OrderedDict, deque
18-
from collections.abc import MutableMapping, Sequence
19-
from typing import Optional
18+
from collections import abc
19+
from typing import Optional, Sequence, Tuple
2020

2121
from deprecated import deprecated
22+
from pkg_resources import iter_entry_points
2223

2324

2425
def ns_to_iso_str(nanoseconds):
@@ -41,7 +42,27 @@ def get_dict_as_key(labels):
4142
)
4243

4344

44-
class BoundedList(Sequence):
45+
def _import_config_components(
46+
selected_components, entry_point_name
47+
) -> Sequence[Tuple[str, object]]:
48+
component_entry_points = {
49+
ep.name: ep for ep in iter_entry_points(entry_point_name)
50+
}
51+
component_impls = []
52+
for selected_component in selected_components:
53+
entry_point = component_entry_points.get(selected_component, None)
54+
if not entry_point:
55+
raise RuntimeError(
56+
f"Requested component '{selected_component}' not found in entry points for '{entry_point_name}'"
57+
)
58+
59+
component_impl = entry_point.load()
60+
component_impls.append((selected_component, component_impl))
61+
62+
return component_impls
63+
64+
65+
class BoundedList(abc.Sequence):
4566
"""An append only list with a fixed max size.
4667
4768
Calls to `append` and `extend` will drop the oldest elements if there is
@@ -92,7 +113,7 @@ def from_seq(cls, maxlen, seq):
92113

93114

94115
@deprecated(version="1.4.0") # type: ignore
95-
class BoundedDict(MutableMapping):
116+
class BoundedDict(abc.MutableMapping):
96117
"""An ordered dict with a fixed max capacity.
97118
98119
Oldest elements are dropped when the dict is full and a new element is

opentelemetry-sdk/tests/test_configurator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def test_trace_init_otlp(self):
257257

258258
@patch.dict(environ, {OTEL_PYTHON_ID_GENERATOR: "custom_id_generator"})
259259
@patch("opentelemetry.sdk._configuration.IdGenerator", new=IdGenerator)
260-
@patch("opentelemetry.sdk._configuration.iter_entry_points")
260+
@patch("opentelemetry.sdk.util.iter_entry_points")
261261
def test_trace_init_custom_id_generator(self, mock_iter_entry_points):
262262
mock_iter_entry_points.configure_mock(
263263
return_value=[

opentelemetry-sdk/tests/trace/test_trace.py

+101
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,63 @@ def test_tracer_provider_accepts_concurrent_multi_span_processor(self):
139139
)
140140

141141

142+
class CustomSampler(sampling.Sampler):
143+
def __init__(self) -> None:
144+
super().__init__()
145+
146+
def get_description(self) -> str:
147+
return super().get_description()
148+
149+
def should_sample(
150+
self,
151+
parent_context,
152+
trace_id,
153+
name,
154+
kind,
155+
attributes,
156+
links,
157+
trace_state,
158+
):
159+
return super().should_sample(
160+
parent_context,
161+
trace_id,
162+
name,
163+
kind,
164+
attributes,
165+
links,
166+
trace_state,
167+
)
168+
169+
170+
class CustomRatioSampler(sampling.TraceIdRatioBased):
171+
def __init__(self, ratio):
172+
self.ratio = ratio
173+
super().__init__(ratio)
174+
175+
def get_description(self) -> str:
176+
return super().get_description()
177+
178+
def should_sample(
179+
self,
180+
parent_context,
181+
trace_id,
182+
name,
183+
kind,
184+
attributes,
185+
links,
186+
trace_state,
187+
):
188+
return super().should_sample(
189+
parent_context,
190+
trace_id,
191+
name,
192+
kind,
193+
attributes,
194+
links,
195+
trace_state,
196+
)
197+
198+
142199
class TestTracerSampling(unittest.TestCase):
143200
def tearDown(self):
144201
reload(trace)
@@ -219,6 +276,50 @@ def test_ratio_sampler_with_env(self):
219276
self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased)
220277
self.assertEqual(tracer_provider.sampler._root.rate, 0.25)
221278

279+
@mock.patch.dict(
280+
"os.environ", {OTEL_TRACES_SAMPLER: "non_existent_entry_point"}
281+
)
282+
def test_sampler_with_env_non_existent_entry_point(self):
283+
# pylint: disable=protected-access
284+
reload(trace)
285+
tracer_provider = trace.TracerProvider()
286+
self.assertIsInstance(tracer_provider.sampler, sampling.ParentBased)
287+
# pylint: disable=protected-access
288+
self.assertEqual(tracer_provider.sampler._root, sampling.ALWAYS_ON)
289+
290+
@mock.patch("opentelemetry.sdk.trace.sampling._import_config_components")
291+
@mock.patch.dict("os.environ", {OTEL_TRACES_SAMPLER: "custom_sampler"})
292+
def test_custom_sampler_with_env(
293+
self, mock_sampling_import_config_components
294+
):
295+
mock_sampling_import_config_components.return_value = [
296+
("custom_sampler", CustomSampler)
297+
]
298+
# pylint: disable=protected-access
299+
reload(trace)
300+
tracer_provider = trace.TracerProvider()
301+
self.assertIsInstance(tracer_provider.sampler, CustomSampler)
302+
303+
@mock.patch("opentelemetry.sdk.trace.sampling._import_config_components")
304+
@mock.patch.dict(
305+
"os.environ",
306+
{
307+
OTEL_TRACES_SAMPLER: "custom_ratio_sampler",
308+
OTEL_TRACES_SAMPLER_ARG: "0.5",
309+
},
310+
)
311+
def test_custom_ratio_sampler_with_env(
312+
self, mock_sampling_import_config_components
313+
):
314+
mock_sampling_import_config_components.return_value = [
315+
("custom_ratio_sampler", CustomRatioSampler)
316+
]
317+
# pylint: disable=protected-access
318+
reload(trace)
319+
tracer_provider = trace.TracerProvider()
320+
self.assertIsInstance(tracer_provider.sampler, CustomRatioSampler)
321+
self.assertEqual(tracer_provider.sampler.ratio, 0.5)
322+
222323

223324
class TestSpanCreation(unittest.TestCase):
224325
def test_start_span_invalid_spancontext(self):

0 commit comments

Comments
 (0)