454 lines
16 KiB
Python
454 lines
16 KiB
Python
# Copyright The OpenTelemetry Authors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""
|
|
For general information about sampling, see `the specification <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling>`_.
|
|
|
|
OpenTelemetry provides two types of samplers:
|
|
|
|
- `StaticSampler`
|
|
- `TraceIdRatioBased`
|
|
|
|
A `StaticSampler` always returns the same sampling result regardless of the conditions. Both possible StaticSamplers are already created:
|
|
|
|
- Always sample spans: ALWAYS_ON
|
|
- Never sample spans: ALWAYS_OFF
|
|
|
|
A `TraceIdRatioBased` sampler makes a random sampling result based on the sampling probability given.
|
|
|
|
If the span being sampled has a parent, `ParentBased` will respect the parent delegate sampler. Otherwise, it returns the sampling result from the given root sampler.
|
|
|
|
Currently, sampling results are always made during the creation of the span. However, this might not always be the case in the future (see `OTEP #115 <https://github.com/open-telemetry/oteps/pull/115>`_).
|
|
|
|
Custom samplers can be created by subclassing `Sampler` and implementing `Sampler.should_sample` as well as `Sampler.get_description`.
|
|
|
|
Samplers are able to modify the `opentelemetry.trace.span.TraceState` of the parent of the span being created. For custom samplers, it is suggested to implement `Sampler.should_sample` to utilize the
|
|
parent span context's `opentelemetry.trace.span.TraceState` and pass into the `SamplingResult` instead of the explicit trace_state field passed into the parameter of `Sampler.should_sample`.
|
|
|
|
To use a sampler, pass it into the tracer provider constructor. For example:
|
|
|
|
.. code:: python
|
|
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import (
|
|
ConsoleSpanExporter,
|
|
SimpleSpanProcessor,
|
|
)
|
|
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
|
|
# sample 1 in every 1000 traces
|
|
sampler = TraceIdRatioBased(1/1000)
|
|
|
|
# set the sampler onto the global tracer provider
|
|
trace.set_tracer_provider(TracerProvider(sampler=sampler))
|
|
|
|
# set up an exporter for sampled spans
|
|
trace.get_tracer_provider().add_span_processor(
|
|
SimpleSpanProcessor(ConsoleSpanExporter())
|
|
)
|
|
|
|
# created spans will now be sampled by the TraceIdRatioBased sampler
|
|
with trace.get_tracer(__name__).start_as_current_span("Test Span"):
|
|
...
|
|
|
|
The tracer sampler can also be configured via environment variables ``OTEL_TRACES_SAMPLER`` and ``OTEL_TRACES_SAMPLER_ARG`` (only if applicable).
|
|
The list of built-in values for ``OTEL_TRACES_SAMPLER`` are:
|
|
|
|
* always_on - Sampler that always samples spans, regardless of the parent span's sampling decision.
|
|
* always_off - Sampler that never samples spans, regardless of the parent span's sampling decision.
|
|
* traceidratio - Sampler that samples probabilistically based on rate.
|
|
* parentbased_always_on - (default) Sampler that respects its parent span's sampling decision, but otherwise always samples.
|
|
* parentbased_always_off - Sampler that respects its parent span's sampling decision, but otherwise never samples.
|
|
* parentbased_traceidratio - Sampler that respects its parent span's sampling decision, but otherwise samples probabilistically based on rate.
|
|
|
|
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).
|
|
|
|
Prev example but with environment variables. Please make sure to set the env ``OTEL_TRACES_SAMPLER=traceidratio`` and ``OTEL_TRACES_SAMPLER_ARG=0.001``.
|
|
|
|
.. code:: python
|
|
|
|
from opentelemetry import trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import (
|
|
ConsoleSpanExporter,
|
|
SimpleSpanProcessor,
|
|
)
|
|
|
|
trace.set_tracer_provider(TracerProvider())
|
|
|
|
# set up an exporter for sampled spans
|
|
trace.get_tracer_provider().add_span_processor(
|
|
SimpleSpanProcessor(ConsoleSpanExporter())
|
|
)
|
|
|
|
# created spans will now be sampled by the TraceIdRatioBased sampler with rate 1/1000.
|
|
with trace.get_tracer(__name__).start_as_current_span("Test Span"):
|
|
...
|
|
|
|
When utilizing a configurator, you can configure a custom sampler. In order to create a configurable custom sampler, create an entry point for the custom sampler
|
|
factory method or function under the entry point group, ``opentelemetry_traces_sampler``. The custom sampler factory 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 ``OTEL_TRACES_SAMPLER_ARG`` environment variable. If ``OTEL_TRACES_SAMPLER_ARG`` is not configured, the input will
|
|
be an empty string. For example:
|
|
|
|
.. code:: python
|
|
|
|
setup(
|
|
...
|
|
entry_points={
|
|
...
|
|
"opentelemetry_traces_sampler": [
|
|
"custom_sampler_name = path.to.sampler.factory.method:CustomSamplerFactory.get_sampler"
|
|
]
|
|
}
|
|
)
|
|
# ...
|
|
class CustomRatioSampler(Sampler):
|
|
def __init__(rate):
|
|
# ...
|
|
# ...
|
|
class CustomSamplerFactory:
|
|
@staticmethod
|
|
def get_sampler(sampler_argument):
|
|
try:
|
|
rate = float(sampler_argument)
|
|
return CustomSampler(rate)
|
|
except ValueError: # In case argument is empty string.
|
|
return CustomSampler(0.5)
|
|
|
|
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
|
|
above sampler, set ``OTEL_TRACES_SAMPLER=custom_sampler_name`` and ``OTEL_TRACES_SAMPLER_ARG=0.5``.
|
|
"""
|
|
|
|
import abc
|
|
import enum
|
|
import os
|
|
from logging import getLogger
|
|
from types import MappingProxyType
|
|
from typing import Optional, Sequence
|
|
|
|
# pylint: disable=unused-import
|
|
from opentelemetry.context import Context
|
|
from opentelemetry.sdk.environment_variables import (
|
|
OTEL_TRACES_SAMPLER,
|
|
OTEL_TRACES_SAMPLER_ARG,
|
|
)
|
|
from opentelemetry.trace import Link, SpanKind, get_current_span
|
|
from opentelemetry.trace.span import TraceState
|
|
from opentelemetry.util.types import Attributes
|
|
|
|
_logger = getLogger(__name__)
|
|
|
|
|
|
class Decision(enum.Enum):
|
|
# IsRecording() == false, span will not be recorded and all events and attributes will be dropped.
|
|
DROP = 0
|
|
# IsRecording() == true, but Sampled flag MUST NOT be set.
|
|
RECORD_ONLY = 1
|
|
# IsRecording() == true AND Sampled flag` MUST be set.
|
|
RECORD_AND_SAMPLE = 2
|
|
|
|
def is_recording(self):
|
|
return self in (Decision.RECORD_ONLY, Decision.RECORD_AND_SAMPLE)
|
|
|
|
def is_sampled(self):
|
|
return self is Decision.RECORD_AND_SAMPLE
|
|
|
|
|
|
class SamplingResult:
|
|
"""A sampling result as applied to a newly-created Span.
|
|
|
|
Args:
|
|
decision: A sampling decision based off of whether the span is recorded
|
|
and the sampled flag in trace flags in the span context.
|
|
attributes: Attributes to add to the `opentelemetry.trace.Span`.
|
|
trace_state: The tracestate used for the `opentelemetry.trace.Span`.
|
|
Could possibly have been modified by the sampler.
|
|
"""
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{type(self).__name__}({str(self.decision)}, attributes={str(self.attributes)})"
|
|
|
|
def __init__(
|
|
self,
|
|
decision: Decision,
|
|
attributes: "Attributes" = None,
|
|
trace_state: Optional["TraceState"] = None,
|
|
) -> None:
|
|
self.decision = decision
|
|
if attributes is None:
|
|
self.attributes = MappingProxyType({})
|
|
else:
|
|
self.attributes = MappingProxyType(attributes)
|
|
self.trace_state = trace_state
|
|
|
|
|
|
class Sampler(abc.ABC):
|
|
@abc.abstractmethod
|
|
def should_sample(
|
|
self,
|
|
parent_context: Optional["Context"],
|
|
trace_id: int,
|
|
name: str,
|
|
kind: Optional[SpanKind] = None,
|
|
attributes: Attributes = None,
|
|
links: Optional[Sequence["Link"]] = None,
|
|
trace_state: Optional["TraceState"] = None,
|
|
) -> "SamplingResult":
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_description(self) -> str:
|
|
pass
|
|
|
|
|
|
class StaticSampler(Sampler):
|
|
"""Sampler that always returns the same decision."""
|
|
|
|
def __init__(self, decision: "Decision") -> None:
|
|
self._decision = decision
|
|
|
|
def should_sample(
|
|
self,
|
|
parent_context: Optional["Context"],
|
|
trace_id: int,
|
|
name: str,
|
|
kind: Optional[SpanKind] = None,
|
|
attributes: Attributes = None,
|
|
links: Optional[Sequence["Link"]] = None,
|
|
trace_state: Optional["TraceState"] = None,
|
|
) -> "SamplingResult":
|
|
if self._decision is Decision.DROP:
|
|
attributes = None
|
|
return SamplingResult(
|
|
self._decision,
|
|
attributes,
|
|
_get_parent_trace_state(parent_context),
|
|
)
|
|
|
|
def get_description(self) -> str:
|
|
if self._decision is Decision.DROP:
|
|
return "AlwaysOffSampler"
|
|
return "AlwaysOnSampler"
|
|
|
|
|
|
ALWAYS_OFF = StaticSampler(Decision.DROP)
|
|
"""Sampler that never samples spans, regardless of the parent span's sampling decision."""
|
|
|
|
ALWAYS_ON = StaticSampler(Decision.RECORD_AND_SAMPLE)
|
|
"""Sampler that always samples spans, regardless of the parent span's sampling decision."""
|
|
|
|
|
|
class TraceIdRatioBased(Sampler):
|
|
"""
|
|
Sampler that makes sampling decisions probabilistically based on `rate`.
|
|
|
|
Args:
|
|
rate: Probability (between 0 and 1) that a span will be sampled
|
|
"""
|
|
|
|
def __init__(self, rate: float):
|
|
if rate < 0.0 or rate > 1.0:
|
|
raise ValueError("Probability must be in range [0.0, 1.0].")
|
|
self._rate = rate
|
|
self._bound = self.get_bound_for_rate(self._rate)
|
|
|
|
# For compatibility with 64 bit trace IDs, the sampler checks the 64
|
|
# low-order bits of the trace ID to decide whether to sample a given trace.
|
|
TRACE_ID_LIMIT = (1 << 64) - 1
|
|
|
|
@classmethod
|
|
def get_bound_for_rate(cls, rate: float) -> int:
|
|
return round(rate * (cls.TRACE_ID_LIMIT + 1))
|
|
|
|
@property
|
|
def rate(self) -> float:
|
|
return self._rate
|
|
|
|
@property
|
|
def bound(self) -> int:
|
|
return self._bound
|
|
|
|
def should_sample(
|
|
self,
|
|
parent_context: Optional["Context"],
|
|
trace_id: int,
|
|
name: str,
|
|
kind: Optional[SpanKind] = None,
|
|
attributes: Attributes = None,
|
|
links: Optional[Sequence["Link"]] = None,
|
|
trace_state: Optional["TraceState"] = None,
|
|
) -> "SamplingResult":
|
|
decision = Decision.DROP
|
|
if trace_id & self.TRACE_ID_LIMIT < self.bound:
|
|
decision = Decision.RECORD_AND_SAMPLE
|
|
if decision is Decision.DROP:
|
|
attributes = None
|
|
return SamplingResult(
|
|
decision,
|
|
attributes,
|
|
_get_parent_trace_state(parent_context),
|
|
)
|
|
|
|
def get_description(self) -> str:
|
|
return f"TraceIdRatioBased{{{self._rate}}}"
|
|
|
|
|
|
class ParentBased(Sampler):
|
|
"""
|
|
If a parent is set, applies the respective delegate sampler.
|
|
Otherwise, uses the root provided at initialization to make a
|
|
decision.
|
|
|
|
Args:
|
|
root: Sampler called for spans with no parent (root spans).
|
|
remote_parent_sampled: Sampler called for a remote sampled parent.
|
|
remote_parent_not_sampled: Sampler called for a remote parent that is
|
|
not sampled.
|
|
local_parent_sampled: Sampler called for a local sampled parent.
|
|
local_parent_not_sampled: Sampler called for a local parent that is
|
|
not sampled.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
root: Sampler,
|
|
remote_parent_sampled: Sampler = ALWAYS_ON,
|
|
remote_parent_not_sampled: Sampler = ALWAYS_OFF,
|
|
local_parent_sampled: Sampler = ALWAYS_ON,
|
|
local_parent_not_sampled: Sampler = ALWAYS_OFF,
|
|
):
|
|
self._root = root
|
|
self._remote_parent_sampled = remote_parent_sampled
|
|
self._remote_parent_not_sampled = remote_parent_not_sampled
|
|
self._local_parent_sampled = local_parent_sampled
|
|
self._local_parent_not_sampled = local_parent_not_sampled
|
|
|
|
def should_sample(
|
|
self,
|
|
parent_context: Optional["Context"],
|
|
trace_id: int,
|
|
name: str,
|
|
kind: Optional[SpanKind] = None,
|
|
attributes: Attributes = None,
|
|
links: Optional[Sequence["Link"]] = None,
|
|
trace_state: Optional["TraceState"] = None,
|
|
) -> "SamplingResult":
|
|
parent_span_context = get_current_span(
|
|
parent_context
|
|
).get_span_context()
|
|
# default to the root sampler
|
|
sampler = self._root
|
|
# respect the sampling and remote flag of the parent if present
|
|
if parent_span_context is not None and parent_span_context.is_valid:
|
|
if parent_span_context.is_remote:
|
|
if parent_span_context.trace_flags.sampled:
|
|
sampler = self._remote_parent_sampled
|
|
else:
|
|
sampler = self._remote_parent_not_sampled
|
|
else:
|
|
if parent_span_context.trace_flags.sampled:
|
|
sampler = self._local_parent_sampled
|
|
else:
|
|
sampler = self._local_parent_not_sampled
|
|
|
|
return sampler.should_sample(
|
|
parent_context=parent_context,
|
|
trace_id=trace_id,
|
|
name=name,
|
|
kind=kind,
|
|
attributes=attributes,
|
|
links=links,
|
|
)
|
|
|
|
def get_description(self):
|
|
return f"ParentBased{{root:{self._root.get_description()},remoteParentSampled:{self._remote_parent_sampled.get_description()},remoteParentNotSampled:{self._remote_parent_not_sampled.get_description()},localParentSampled:{self._local_parent_sampled.get_description()},localParentNotSampled:{self._local_parent_not_sampled.get_description()}}}"
|
|
|
|
|
|
DEFAULT_OFF = ParentBased(ALWAYS_OFF)
|
|
"""Sampler that respects its parent span's sampling decision, but otherwise never samples."""
|
|
|
|
DEFAULT_ON = ParentBased(ALWAYS_ON)
|
|
"""Sampler that respects its parent span's sampling decision, but otherwise always samples."""
|
|
|
|
|
|
class ParentBasedTraceIdRatio(ParentBased):
|
|
"""
|
|
Sampler that respects its parent span's sampling decision, but otherwise
|
|
samples probabilistically based on `rate`.
|
|
"""
|
|
|
|
def __init__(self, rate: float):
|
|
root = TraceIdRatioBased(rate=rate)
|
|
super().__init__(root=root)
|
|
|
|
|
|
class _AlwaysOff(StaticSampler):
|
|
def __init__(self, _):
|
|
super().__init__(Decision.DROP)
|
|
|
|
|
|
class _AlwaysOn(StaticSampler):
|
|
def __init__(self, _):
|
|
super().__init__(Decision.RECORD_AND_SAMPLE)
|
|
|
|
|
|
class _ParentBasedAlwaysOff(ParentBased):
|
|
def __init__(self, _):
|
|
super().__init__(ALWAYS_OFF)
|
|
|
|
|
|
class _ParentBasedAlwaysOn(ParentBased):
|
|
def __init__(self, _):
|
|
super().__init__(ALWAYS_ON)
|
|
|
|
|
|
_KNOWN_SAMPLERS = {
|
|
"always_on": ALWAYS_ON,
|
|
"always_off": ALWAYS_OFF,
|
|
"parentbased_always_on": DEFAULT_ON,
|
|
"parentbased_always_off": DEFAULT_OFF,
|
|
"traceidratio": TraceIdRatioBased,
|
|
"parentbased_traceidratio": ParentBasedTraceIdRatio,
|
|
}
|
|
|
|
|
|
def _get_from_env_or_default() -> Sampler:
|
|
trace_sampler = os.getenv(
|
|
OTEL_TRACES_SAMPLER, "parentbased_always_on"
|
|
).lower()
|
|
if trace_sampler not in _KNOWN_SAMPLERS:
|
|
_logger.warning("Couldn't recognize sampler %s.", trace_sampler)
|
|
trace_sampler = "parentbased_always_on"
|
|
|
|
if trace_sampler in ("traceidratio", "parentbased_traceidratio"):
|
|
try:
|
|
rate = float(os.getenv(OTEL_TRACES_SAMPLER_ARG, ""))
|
|
except (ValueError, TypeError):
|
|
_logger.warning("Could not convert TRACES_SAMPLER_ARG to float.")
|
|
rate = 1.0
|
|
return _KNOWN_SAMPLERS[trace_sampler](rate)
|
|
|
|
return _KNOWN_SAMPLERS[trace_sampler]
|
|
|
|
|
|
def _get_parent_trace_state(
|
|
parent_context: Optional[Context],
|
|
) -> Optional["TraceState"]:
|
|
parent_span_context = get_current_span(parent_context).get_span_context()
|
|
if parent_span_context is None or not parent_span_context.is_valid:
|
|
return None
|
|
return parent_span_context.trace_state
|