Skip to content

Commit 35ba257

Browse files
authored
Enabled custom sampler configuration via env vars (#2972)
1 parent c338eb1 commit 35ba257

File tree

6 files changed

+358
-60
lines changed

6 files changed

+358
-60
lines changed

Diff for: 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 log symbol names
1113
([#2943](https://github.com/open-telemetry/opentelemetry-python/pull/2943))
1214
- Update explicit histogram bucket boundaries

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

+3-24
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

@@ -226,26 +226,6 @@ def _init_logging(
226226
logging.getLogger().addHandler(handler)
227227

228228

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

288268

289269
def _import_id_generator(id_generator_name: str) -> IdGenerator:
290-
# pylint: disable=unbalanced-tuple-unpacking
291-
[(id_generator_name, id_generator_impl)] = _import_config_components(
270+
id_generator_name, id_generator_impl = _import_config_components(
292271
[id_generator_name.strip()], "opentelemetry_id_generator"
293-
)
272+
)[0]
294273

295274
if issubclass(id_generator_impl, IdGenerator):
296275
return id_generator_impl

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py

+74-17
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,8 +73,7 @@
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).
77-
76+
Sampling probability can be set with ``OTEL_TRACES_SAMPLER_ARG`` if the sampler is traceidratio or parentbased_traceidratio. Rate must be in the range [0.0,1.0]. When not provided rate will be set to 1.0 (maximum rate possible).
7877
7978
Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``.
8079
@@ -97,20 +96,53 @@
9796
# created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000.
9897
with trace.get_tracer(__name__).start_as_current_span("Test Span"):
9998
...
99+
100+
In order to create a configurable custom sampler, create an entry point for the custom sampler factory method under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory
101+
method must be of type ``Callable[[str], Sampler]``, taking a single string argument and returning a Sampler object. The single input will come from the string value of the
102+
``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will be an empty string. For example:
103+
104+
.. code:: python
105+
106+
setup(
107+
...
108+
entry_points={
109+
...
110+
"opentelemetry_traces_sampler": [
111+
"custom_sampler_name = path.to.sampler.factory.method:CustomSamplerFactory.get_sampler"
112+
]
113+
}
114+
)
115+
# ...
116+
class CustomRatioSampler(Sampler):
117+
def __init__(rate):
118+
# ...
119+
# ...
120+
class CustomSamplerFactory:
121+
@staticmethod
122+
get_sampler(sampler_argument):
123+
try:
124+
rate = float(sampler_argument)
125+
return CustomSampler(rate)
126+
except ValueError: # In case argument is empty string.
127+
return CustomSampler(0.5)
128+
129+
In order to configure you application with a custom sampler's entry point, set the ``OTEL_TRACES_SAMPLER`` environment variable to the key name of the entry point. For example, to configured the
130+
above sampler, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5``.
100131
"""
101132
import abc
102133
import enum
103134
import os
104135
from logging import getLogger
105136
from types import MappingProxyType
106-
from typing import Optional, Sequence
137+
from typing import Callable, Optional, Sequence
107138

108139
# pylint: disable=unused-import
109140
from opentelemetry.context import Context
110141
from opentelemetry.sdk.environment_variables import (
111142
OTEL_TRACES_SAMPLER,
112143
OTEL_TRACES_SAMPLER_ARG,
113144
)
145+
from opentelemetry.sdk.util import _import_config_components
114146
from opentelemetry.trace import Link, SpanKind, get_current_span
115147
from opentelemetry.trace.span import TraceState
116148
from opentelemetry.util.types import Attributes
@@ -161,6 +193,9 @@ def __init__(
161193
self.trace_state = trace_state
162194

163195

196+
_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler"
197+
198+
164199
class Sampler(abc.ABC):
165200
@abc.abstractmethod
166201
def should_sample(
@@ -372,26 +407,48 @@ def __init__(self, rate: float):
372407

373408

374409
def _get_from_env_or_default() -> Sampler:
375-
trace_sampler = os.getenv(
410+
traces_sampler_name = os.getenv(
376411
OTEL_TRACES_SAMPLER, "parentbased_always_on"
377412
).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"
381-
382-
if trace_sampler in ("traceidratio", "parentbased_traceidratio"):
383-
try:
384-
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG))
385-
except ValueError:
386-
_logger.warning("Could not convert TRACES_SAMPLER_ARG to float.")
387-
rate = 1.0
388-
return _KNOWN_SAMPLERS[trace_sampler](rate)
389413

390-
return _KNOWN_SAMPLERS[trace_sampler]
414+
if traces_sampler_name in _KNOWN_SAMPLERS:
415+
if traces_sampler_name in ("traceidratio", "parentbased_traceidratio"):
416+
try:
417+
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG))
418+
except ValueError:
419+
_logger.warning(
420+
"Could not convert TRACES_SAMPLER_ARG to float."
421+
)
422+
rate = 1.0
423+
return _KNOWN_SAMPLERS[traces_sampler_name](rate)
424+
return _KNOWN_SAMPLERS[traces_sampler_name]
425+
try:
426+
traces_sampler_factory = _import_sampler_factory(traces_sampler_name)
427+
sampler_arg = os.getenv(OTEL_TRACES_SAMPLER_ARG, "")
428+
traces_sampler = traces_sampler_factory(sampler_arg)
429+
if not isinstance(traces_sampler, Sampler):
430+
message = f"Traces sampler factory, {traces_sampler_factory}, produced output, {traces_sampler}, which is not a Sampler object."
431+
_logger.warning(message)
432+
raise ValueError(message)
433+
return traces_sampler
434+
except Exception as exc: # pylint: disable=broad-except
435+
_logger.warning(
436+
"Using default sampler. Failed to initialize custom sampler, %s: %s",
437+
traces_sampler_name,
438+
exc,
439+
)
440+
return _KNOWN_SAMPLERS["parentbased_always_on"]
391441

392442

393443
def _get_parent_trace_state(parent_context) -> Optional["TraceState"]:
394444
parent_span_context = get_current_span(parent_context).get_span_context()
395445
if parent_span_context is None or not parent_span_context.is_valid:
396446
return None
397447
return parent_span_context.trace_state
448+
449+
450+
def _import_sampler_factory(sampler_name: str) -> Callable[[str], Sampler]:
451+
_, sampler_impl = _import_config_components(
452+
[sampler_name.strip()], _OTEL_SAMPLER_ENTRY_POINT_GROUP
453+
)[0]
454+
return sampler_impl

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/util/__init__.py

+25-5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
import datetime
1616
import threading
17-
from collections import OrderedDict, deque
18-
from collections.abc import MutableMapping, Sequence
19-
from typing import Optional
17+
from collections import OrderedDict, abc, deque
18+
from typing import List, Optional, Sequence, Tuple
2019

2120
from deprecated import deprecated
21+
from pkg_resources import iter_entry_points
2222

2323

2424
def ns_to_iso_str(nanoseconds):
@@ -41,7 +41,27 @@ def get_dict_as_key(labels):
4141
)
4242

4343

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

93113

94114
@deprecated(version="1.4.0") # type: ignore
95-
class BoundedDict(MutableMapping):
115+
class BoundedDict(abc.MutableMapping):
96116
"""An ordered dict with a fixed max capacity.
97117
98118
Oldest elements are dropped when the dict is full and a new element is

Diff for: 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=[

0 commit comments

Comments
 (0)