chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
# 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.
|
||||
|
||||
__all__ = [
|
||||
"ComposableSampler",
|
||||
"SamplingIntent",
|
||||
"composable_always_off",
|
||||
"composable_always_on",
|
||||
"composable_parent_threshold",
|
||||
"composable_traceid_ratio_based",
|
||||
"composite_sampler",
|
||||
]
|
||||
|
||||
|
||||
from ._always_off import composable_always_off
|
||||
from ._always_on import composable_always_on
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._parent_threshold import composable_parent_threshold
|
||||
from ._sampler import composite_sampler
|
||||
from ._traceid_ratio import composable_traceid_ratio_based
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._util import INVALID_THRESHOLD
|
||||
|
||||
_intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False)
|
||||
|
||||
|
||||
class _ComposableAlwaysOffSampler(ComposableSampler):
|
||||
def sampling_intent(
|
||||
self,
|
||||
parent_ctx: Context | None,
|
||||
name: str,
|
||||
span_kind: SpanKind | None,
|
||||
attributes: Attributes,
|
||||
links: Sequence[Link] | None,
|
||||
trace_state: TraceState | None = None,
|
||||
) -> SamplingIntent:
|
||||
return _intent
|
||||
|
||||
def get_description(self) -> str:
|
||||
return "ComposableAlwaysOff"
|
||||
|
||||
|
||||
_always_off = _ComposableAlwaysOffSampler()
|
||||
|
||||
|
||||
def composable_always_off() -> ComposableSampler:
|
||||
"""Returns a composable sampler that does not sample any span.
|
||||
|
||||
- Always returns a SamplingIntent with no threshold, indicating all spans should be dropped
|
||||
- Sets threshold_reliable to false
|
||||
- Does not add any attributes
|
||||
"""
|
||||
return _always_off
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._util import MIN_THRESHOLD
|
||||
|
||||
_intent = SamplingIntent(threshold=MIN_THRESHOLD)
|
||||
|
||||
|
||||
class _ComposableAlwaysOnSampler(ComposableSampler):
|
||||
def sampling_intent(
|
||||
self,
|
||||
parent_ctx: Context | None,
|
||||
name: str,
|
||||
span_kind: SpanKind | None,
|
||||
attributes: Attributes,
|
||||
links: Sequence[Link] | None,
|
||||
trace_state: TraceState | None = None,
|
||||
) -> SamplingIntent:
|
||||
return _intent
|
||||
|
||||
def get_description(self) -> str:
|
||||
return "ComposableAlwaysOn"
|
||||
|
||||
|
||||
_always_on = _ComposableAlwaysOnSampler()
|
||||
|
||||
|
||||
def composable_always_on() -> ComposableSampler:
|
||||
"""Returns a composable sampler that samples all spans.
|
||||
|
||||
- Always returns a SamplingIntent with threshold set to sample all spans (threshold = 0)
|
||||
- Sets threshold_reliable to true
|
||||
- Does not add any attributes
|
||||
"""
|
||||
return _always_on
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, Protocol, Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SamplingIntent:
|
||||
"""Information to make a consistent sampling decision."""
|
||||
|
||||
threshold: int
|
||||
"""The sampling threshold value. A lower threshold increases the likelihood of sampling."""
|
||||
|
||||
threshold_reliable: bool = field(default=True)
|
||||
"""Indicates whether the threshold is reliable for Span-to-Metrics estimation."""
|
||||
|
||||
attributes: Attributes = field(default=None)
|
||||
"""Any attributes to be added to a sampled span."""
|
||||
|
||||
update_trace_state: Callable[[TraceState], TraceState] = field(
|
||||
default=lambda ts: ts
|
||||
)
|
||||
"""Any updates to be made to trace state."""
|
||||
|
||||
|
||||
class ComposableSampler(Protocol):
|
||||
"""A sampler that can be composed to make a final sampling decision."""
|
||||
|
||||
def sampling_intent(
|
||||
self,
|
||||
parent_ctx: Context | None,
|
||||
name: str,
|
||||
span_kind: SpanKind | None,
|
||||
attributes: Attributes,
|
||||
links: Sequence[Link] | None,
|
||||
trace_state: TraceState | None,
|
||||
) -> SamplingIntent:
|
||||
"""Returns information to make a sampling decision."""
|
||||
... # pylint: disable=unnecessary-ellipsis
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""Returns a description of the sampler."""
|
||||
... # pylint: disable=unnecessary-ellipsis
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._trace_state import OtelTraceState
|
||||
from ._util import (
|
||||
INVALID_THRESHOLD,
|
||||
MIN_THRESHOLD,
|
||||
is_valid_threshold,
|
||||
)
|
||||
|
||||
|
||||
class _ComposableParentThreshold(ComposableSampler):
|
||||
def __init__(self, root_sampler: ComposableSampler):
|
||||
self._root_sampler = root_sampler
|
||||
self._description = f"ComposableParentThreshold{{root={root_sampler.get_description()}}}"
|
||||
|
||||
def sampling_intent(
|
||||
self,
|
||||
parent_ctx: Context | None,
|
||||
name: str,
|
||||
span_kind: SpanKind | None,
|
||||
attributes: Attributes,
|
||||
links: Sequence[Link] | None,
|
||||
trace_state: TraceState | None = None,
|
||||
) -> SamplingIntent:
|
||||
parent_span = get_current_span(parent_ctx)
|
||||
parent_span_ctx = parent_span.get_span_context()
|
||||
is_root = not parent_span_ctx.is_valid
|
||||
if is_root:
|
||||
return self._root_sampler.sampling_intent(
|
||||
parent_ctx, name, span_kind, attributes, links, trace_state
|
||||
)
|
||||
|
||||
ot_trace_state = OtelTraceState.parse(trace_state)
|
||||
|
||||
if is_valid_threshold(ot_trace_state.threshold):
|
||||
return SamplingIntent(
|
||||
threshold=ot_trace_state.threshold,
|
||||
threshold_reliable=True,
|
||||
)
|
||||
|
||||
threshold = (
|
||||
MIN_THRESHOLD
|
||||
if parent_span_ctx.trace_flags.sampled
|
||||
else INVALID_THRESHOLD
|
||||
)
|
||||
return SamplingIntent(threshold=threshold, threshold_reliable=False)
|
||||
|
||||
def get_description(self) -> str:
|
||||
return self._description
|
||||
|
||||
|
||||
def composable_parent_threshold(
|
||||
root_sampler: ComposableSampler,
|
||||
) -> ComposableSampler:
|
||||
"""Returns a consistent sampler that respects the sampling decision of
|
||||
the parent span or falls-back to the given sampler if it is a root span.
|
||||
|
||||
- For spans without a parent context, delegate to the root sampler
|
||||
- For spans with a parent context, returns a SamplingIntent that propagates the parent's sampling decision
|
||||
- Returns the parent's threshold if available; otherwise, if the parent's sampled flag is set,
|
||||
returns threshold=0; otherwise, if the parent's sampled flag is not set, no threshold is returned.
|
||||
- Sets threshold_reliable to match the parent’s reliability, which is true if the parent had a threshold.
|
||||
- Does not add any attributes
|
||||
|
||||
Args:
|
||||
root_sampler: The root sampler to use for spans without a parent context.
|
||||
"""
|
||||
return _ComposableParentThreshold(root_sampler)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._trace_state import OTEL_TRACE_STATE_KEY, OtelTraceState
|
||||
from ._util import INVALID_THRESHOLD, is_valid_random_value, is_valid_threshold
|
||||
|
||||
|
||||
class _CompositeSampler(Sampler):
|
||||
def __init__(self, delegate: ComposableSampler):
|
||||
self._delegate = delegate
|
||||
|
||||
def should_sample(
|
||||
self,
|
||||
parent_context: Context | None,
|
||||
trace_id: int,
|
||||
name: str,
|
||||
kind: SpanKind | None = None,
|
||||
attributes: Attributes | None = None,
|
||||
links: Sequence[Link] | None = None,
|
||||
trace_state: TraceState | None = None,
|
||||
) -> SamplingResult:
|
||||
ot_trace_state = OtelTraceState.parse(trace_state)
|
||||
|
||||
intent = self._delegate.sampling_intent(
|
||||
parent_context, name, kind, attributes, links, trace_state
|
||||
)
|
||||
threshold = intent.threshold
|
||||
|
||||
if is_valid_threshold(threshold):
|
||||
adjusted_count_correct = intent.threshold_reliable
|
||||
if is_valid_random_value(ot_trace_state.random_value):
|
||||
randomness = ot_trace_state.random_value
|
||||
else:
|
||||
# Use last 56 bits of trace_id as randomness
|
||||
randomness = trace_id & 0x00FFFFFFFFFFFFFF
|
||||
sampled = threshold <= randomness
|
||||
else:
|
||||
sampled = False
|
||||
adjusted_count_correct = False
|
||||
|
||||
decision = Decision.RECORD_AND_SAMPLE if sampled else Decision.DROP
|
||||
if sampled and adjusted_count_correct:
|
||||
ot_trace_state.threshold = threshold
|
||||
else:
|
||||
ot_trace_state.threshold = INVALID_THRESHOLD
|
||||
|
||||
return SamplingResult(
|
||||
decision,
|
||||
intent.attributes,
|
||||
_update_trace_state(trace_state, ot_trace_state, intent),
|
||||
)
|
||||
|
||||
def get_description(self) -> str:
|
||||
return self._delegate.get_description()
|
||||
|
||||
|
||||
def _update_trace_state(
|
||||
trace_state: TraceState | None,
|
||||
ot_trace_state: OtelTraceState,
|
||||
intent: SamplingIntent,
|
||||
) -> TraceState | None:
|
||||
otts = ot_trace_state.serialize()
|
||||
if not trace_state:
|
||||
if otts:
|
||||
return TraceState(((OTEL_TRACE_STATE_KEY, otts),))
|
||||
return None
|
||||
new_trace_state = intent.update_trace_state(trace_state)
|
||||
if otts:
|
||||
return new_trace_state.update(OTEL_TRACE_STATE_KEY, otts)
|
||||
return new_trace_state
|
||||
|
||||
|
||||
def composite_sampler(delegate: ComposableSampler) -> Sampler:
|
||||
"""A sampler that uses a a composable sampler to make its decision while
|
||||
handling tracestate.
|
||||
|
||||
Args:
|
||||
delegate: The composable sampler to use for making sampling decisions.
|
||||
"""
|
||||
return _CompositeSampler(delegate)
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.trace import TraceState
|
||||
|
||||
from ._util import (
|
||||
INVALID_RANDOM_VALUE,
|
||||
INVALID_THRESHOLD,
|
||||
MAX_THRESHOLD,
|
||||
is_valid_random_value,
|
||||
is_valid_threshold,
|
||||
)
|
||||
|
||||
OTEL_TRACE_STATE_KEY = "ot"
|
||||
|
||||
_TRACE_STATE_SIZE_LIMIT = 256
|
||||
_MAX_VALUE_LENGTH = 14 # 56 bits, 4 bits per hex digit
|
||||
|
||||
|
||||
@dataclass
|
||||
class OtelTraceState:
|
||||
"""Marshals OpenTelemetry tracestate for sampling parameters.
|
||||
|
||||
https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/
|
||||
"""
|
||||
|
||||
random_value: int
|
||||
threshold: int
|
||||
rest: Sequence[str]
|
||||
|
||||
@staticmethod
|
||||
def invalid() -> "OtelTraceState":
|
||||
return OtelTraceState(INVALID_RANDOM_VALUE, INVALID_THRESHOLD, ())
|
||||
|
||||
@staticmethod
|
||||
def parse(trace_state: TraceState | None) -> "OtelTraceState":
|
||||
if not trace_state:
|
||||
return OtelTraceState.invalid()
|
||||
|
||||
ot = trace_state.get(OTEL_TRACE_STATE_KEY, "")
|
||||
|
||||
if not ot or len(ot) > _TRACE_STATE_SIZE_LIMIT:
|
||||
return OtelTraceState.invalid()
|
||||
|
||||
threshold = INVALID_THRESHOLD
|
||||
random_value = INVALID_RANDOM_VALUE
|
||||
|
||||
members = ot.split(";")
|
||||
rest: list[str] | None = None
|
||||
for member in members:
|
||||
if member.startswith("th:"):
|
||||
threshold = _parse_th(member[len("th:") :], INVALID_THRESHOLD)
|
||||
continue
|
||||
if member.startswith("rv:"):
|
||||
random_value = _parse_rv(
|
||||
member[len("rv:") :], INVALID_RANDOM_VALUE
|
||||
)
|
||||
continue
|
||||
if rest is None:
|
||||
rest = [member]
|
||||
else:
|
||||
rest.append(member)
|
||||
|
||||
return OtelTraceState(
|
||||
random_value=random_value, threshold=threshold, rest=rest or ()
|
||||
)
|
||||
|
||||
def serialize(self) -> str:
|
||||
if (
|
||||
not is_valid_threshold(self.threshold)
|
||||
and not is_valid_random_value(self.random_value)
|
||||
and not self.rest
|
||||
):
|
||||
return ""
|
||||
|
||||
parts: list[str] = []
|
||||
if (
|
||||
is_valid_threshold(self.threshold)
|
||||
and self.threshold != MAX_THRESHOLD
|
||||
):
|
||||
parts.append(f"th:{serialize_th(self.threshold)}")
|
||||
if is_valid_random_value(self.random_value):
|
||||
parts.append(f"rv:{_serialize_rv(self.random_value)}")
|
||||
if self.rest:
|
||||
parts.extend(self.rest)
|
||||
res = ";".join(parts)
|
||||
while len(res) > _TRACE_STATE_SIZE_LIMIT:
|
||||
delim_idx = res.rfind(";")
|
||||
if delim_idx == -1:
|
||||
break
|
||||
res = res[:delim_idx]
|
||||
return res
|
||||
|
||||
|
||||
def _parse_th(value: str, default: int) -> int:
|
||||
if not value or len(value) > _MAX_VALUE_LENGTH:
|
||||
return default
|
||||
|
||||
try:
|
||||
parsed = int(value, 16)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
# th value is compressed by removing all trailing zeros,
|
||||
# so we restore them to get the real value.
|
||||
trailing_zeros = _MAX_VALUE_LENGTH - len(value)
|
||||
return parsed << (trailing_zeros * 4)
|
||||
|
||||
|
||||
def _parse_rv(value: str, default: int) -> int:
|
||||
if not value or len(value) != _MAX_VALUE_LENGTH:
|
||||
return default
|
||||
|
||||
try:
|
||||
return int(value, 16)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def serialize_th(threshold: int) -> str:
|
||||
if not threshold:
|
||||
return "0"
|
||||
return f"{threshold:014x}".rstrip("0")
|
||||
|
||||
|
||||
def _serialize_rv(random_value: int) -> str:
|
||||
return f"{random_value:014x}"
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from opentelemetry.context import Context
|
||||
from opentelemetry.trace import Link, SpanKind, TraceState
|
||||
from opentelemetry.util.types import Attributes
|
||||
|
||||
from ._composable import ComposableSampler, SamplingIntent
|
||||
from ._trace_state import serialize_th
|
||||
from ._util import INVALID_THRESHOLD, MAX_THRESHOLD, calculate_threshold
|
||||
|
||||
|
||||
class ComposableTraceIDRatioBased(ComposableSampler):
|
||||
_threshold: int
|
||||
_description: str
|
||||
|
||||
def __init__(self, ratio: float):
|
||||
threshold = calculate_threshold(ratio)
|
||||
if threshold == MAX_THRESHOLD:
|
||||
threshold_str = "max"
|
||||
else:
|
||||
threshold_str = serialize_th(threshold)
|
||||
if threshold != MAX_THRESHOLD:
|
||||
intent = SamplingIntent(threshold=threshold)
|
||||
else:
|
||||
intent = SamplingIntent(
|
||||
threshold=INVALID_THRESHOLD, threshold_reliable=False
|
||||
)
|
||||
self._intent = intent
|
||||
self._description = f"ComposableTraceIDRatioBased{{threshold={threshold_str}, ratio={ratio}}}"
|
||||
|
||||
def sampling_intent(
|
||||
self,
|
||||
parent_ctx: Context | None,
|
||||
name: str,
|
||||
span_kind: SpanKind | None,
|
||||
attributes: Attributes,
|
||||
links: Sequence[Link] | None,
|
||||
trace_state: TraceState | None,
|
||||
) -> SamplingIntent:
|
||||
return self._intent
|
||||
|
||||
def get_description(self) -> str:
|
||||
return self._description
|
||||
|
||||
|
||||
def composable_traceid_ratio_based(
|
||||
ratio: float,
|
||||
) -> ComposableSampler:
|
||||
"""Returns a composable sampler that samples each span with a fixed ratio.
|
||||
|
||||
- Returns a SamplingIntent with threshold determined by the configured sampling ratio
|
||||
- Sets threshold_reliable to true
|
||||
- Does not add any attributes
|
||||
|
||||
Note:
|
||||
If the ratio is 0, it will behave as an ComposableAlwaysOff sampler instead.
|
||||
|
||||
Args:
|
||||
ratio: The sampling ratio to use (between 0.0 and 1.0).
|
||||
"""
|
||||
if not 0.0 <= ratio <= 1.0:
|
||||
raise ValueError("Sampling ratio must be between 0.0 and 1.0")
|
||||
|
||||
return ComposableTraceIDRatioBased(ratio)
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
|
||||
RANDOM_VALUE_BITS = 56
|
||||
MAX_THRESHOLD = 1 << RANDOM_VALUE_BITS # 0% sampling
|
||||
MIN_THRESHOLD = 0 # 100% sampling
|
||||
MAX_RANDOM_VALUE = MAX_THRESHOLD - 1
|
||||
INVALID_THRESHOLD = -1
|
||||
INVALID_RANDOM_VALUE = -1
|
||||
|
||||
_probability_threshold_scale = float.fromhex("0x1p56")
|
||||
|
||||
|
||||
def calculate_threshold(sampling_probability: float) -> int:
|
||||
return MAX_THRESHOLD - round(
|
||||
sampling_probability * _probability_threshold_scale
|
||||
)
|
||||
|
||||
|
||||
def is_valid_threshold(threshold: int) -> bool:
|
||||
return MIN_THRESHOLD <= threshold <= MAX_THRESHOLD
|
||||
|
||||
|
||||
def is_valid_random_value(random_value: int) -> bool:
|
||||
return 0 <= random_value <= MAX_RANDOM_VALUE
|
||||
@@ -0,0 +1,312 @@
|
||||
# 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.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
from enum import Enum
|
||||
from os import environ, linesep
|
||||
|
||||
from opentelemetry.context import (
|
||||
_SUPPRESS_INSTRUMENTATION_KEY,
|
||||
Context,
|
||||
attach,
|
||||
detach,
|
||||
set_value,
|
||||
)
|
||||
from opentelemetry.sdk._shared_internal import BatchProcessor
|
||||
from opentelemetry.sdk.environment_variables import (
|
||||
OTEL_BSP_EXPORT_TIMEOUT,
|
||||
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
|
||||
OTEL_BSP_MAX_QUEUE_SIZE,
|
||||
OTEL_BSP_SCHEDULE_DELAY,
|
||||
)
|
||||
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
||||
|
||||
_DEFAULT_SCHEDULE_DELAY_MILLIS = 5000
|
||||
_DEFAULT_MAX_EXPORT_BATCH_SIZE = 512
|
||||
_DEFAULT_EXPORT_TIMEOUT_MILLIS = 30000
|
||||
_DEFAULT_MAX_QUEUE_SIZE = 2048
|
||||
_ENV_VAR_INT_VALUE_ERROR_MESSAGE = (
|
||||
"Unable to parse value for %s as integer. Defaulting to %s."
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpanExportResult(Enum):
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
|
||||
|
||||
class SpanExporter:
|
||||
"""Interface for exporting spans.
|
||||
|
||||
Interface to be implemented by services that want to export spans recorded
|
||||
in their own format.
|
||||
|
||||
To export data this MUST be registered to the :class`opentelemetry.sdk.trace.Tracer` using a
|
||||
`SimpleSpanProcessor` or a `BatchSpanProcessor`.
|
||||
"""
|
||||
|
||||
def export(
|
||||
self, spans: typing.Sequence[ReadableSpan]
|
||||
) -> "SpanExportResult":
|
||||
"""Exports a batch of telemetry data.
|
||||
|
||||
Args:
|
||||
spans: The list of `opentelemetry.trace.Span` objects to be exported
|
||||
|
||||
Returns:
|
||||
The result of the export
|
||||
"""
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Shuts down the exporter.
|
||||
|
||||
Called when the SDK is shut down.
|
||||
"""
|
||||
|
||||
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
||||
"""Hint to ensure that the export of any spans the exporter has received
|
||||
prior to the call to ForceFlush SHOULD be completed as soon as possible, preferably
|
||||
before returning from this method.
|
||||
"""
|
||||
|
||||
|
||||
class SimpleSpanProcessor(SpanProcessor):
|
||||
"""Simple SpanProcessor implementation.
|
||||
|
||||
SimpleSpanProcessor is an implementation of `SpanProcessor` that
|
||||
passes ended spans directly to the configured `SpanExporter`.
|
||||
"""
|
||||
|
||||
def __init__(self, span_exporter: SpanExporter):
|
||||
self.span_exporter = span_exporter
|
||||
|
||||
def on_start(
|
||||
self, span: Span, parent_context: typing.Optional[Context] = None
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_end(self, span: ReadableSpan) -> None:
|
||||
if not span.context.trace_flags.sampled:
|
||||
return
|
||||
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
|
||||
try:
|
||||
self.span_exporter.export((span,))
|
||||
# pylint: disable=broad-exception-caught
|
||||
except Exception:
|
||||
logger.exception("Exception while exporting Span.")
|
||||
detach(token)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.span_exporter.shutdown()
|
||||
|
||||
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
||||
# pylint: disable=unused-argument
|
||||
return True
|
||||
|
||||
|
||||
class BatchSpanProcessor(SpanProcessor):
|
||||
"""Batch span processor implementation.
|
||||
|
||||
`BatchSpanProcessor` is an implementation of `SpanProcessor` that
|
||||
batches ended spans and pushes them to the configured `SpanExporter`.
|
||||
|
||||
`BatchSpanProcessor` is configurable with the following environment
|
||||
variables which correspond to constructor parameters:
|
||||
|
||||
- :envvar:`OTEL_BSP_SCHEDULE_DELAY`
|
||||
- :envvar:`OTEL_BSP_MAX_QUEUE_SIZE`
|
||||
- :envvar:`OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
|
||||
- :envvar:`OTEL_BSP_EXPORT_TIMEOUT`
|
||||
|
||||
All the logic for emitting spans, shutting down etc. resides in the `BatchProcessor` class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
span_exporter: SpanExporter,
|
||||
max_queue_size: int | None = None,
|
||||
schedule_delay_millis: float | None = None,
|
||||
max_export_batch_size: int | None = None,
|
||||
export_timeout_millis: float | None = None,
|
||||
):
|
||||
if max_queue_size is None:
|
||||
max_queue_size = BatchSpanProcessor._default_max_queue_size()
|
||||
|
||||
if schedule_delay_millis is None:
|
||||
schedule_delay_millis = (
|
||||
BatchSpanProcessor._default_schedule_delay_millis()
|
||||
)
|
||||
|
||||
if max_export_batch_size is None:
|
||||
max_export_batch_size = (
|
||||
BatchSpanProcessor._default_max_export_batch_size()
|
||||
)
|
||||
|
||||
# Not used. No way currently to pass timeout to export.
|
||||
if export_timeout_millis is None:
|
||||
export_timeout_millis = (
|
||||
BatchSpanProcessor._default_export_timeout_millis()
|
||||
)
|
||||
|
||||
BatchSpanProcessor._validate_arguments(
|
||||
max_queue_size, schedule_delay_millis, max_export_batch_size
|
||||
)
|
||||
|
||||
self._batch_processor = BatchProcessor(
|
||||
span_exporter,
|
||||
schedule_delay_millis,
|
||||
max_export_batch_size,
|
||||
export_timeout_millis,
|
||||
max_queue_size,
|
||||
"Span",
|
||||
)
|
||||
|
||||
# Added for backward compatibility. Not recommended to directly access/use underlying exporter.
|
||||
@property
|
||||
def span_exporter(self):
|
||||
return self._batch_processor._exporter # pylint: disable=protected-access
|
||||
|
||||
def on_start(
|
||||
self, span: Span, parent_context: Context | None = None
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def on_end(self, span: ReadableSpan) -> None:
|
||||
if not span.context.trace_flags.sampled:
|
||||
return
|
||||
self._batch_processor.emit(span)
|
||||
|
||||
def shutdown(self):
|
||||
return self._batch_processor.shutdown()
|
||||
|
||||
def force_flush(self, timeout_millis: typing.Optional[int] = None) -> bool:
|
||||
return self._batch_processor.force_flush(timeout_millis)
|
||||
|
||||
@staticmethod
|
||||
def _default_max_queue_size():
|
||||
try:
|
||||
return int(
|
||||
environ.get(OTEL_BSP_MAX_QUEUE_SIZE, _DEFAULT_MAX_QUEUE_SIZE)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception(
|
||||
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
|
||||
OTEL_BSP_MAX_QUEUE_SIZE,
|
||||
_DEFAULT_MAX_QUEUE_SIZE,
|
||||
)
|
||||
return _DEFAULT_MAX_QUEUE_SIZE
|
||||
|
||||
@staticmethod
|
||||
def _default_schedule_delay_millis():
|
||||
try:
|
||||
return int(
|
||||
environ.get(
|
||||
OTEL_BSP_SCHEDULE_DELAY, _DEFAULT_SCHEDULE_DELAY_MILLIS
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception(
|
||||
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
|
||||
OTEL_BSP_SCHEDULE_DELAY,
|
||||
_DEFAULT_SCHEDULE_DELAY_MILLIS,
|
||||
)
|
||||
return _DEFAULT_SCHEDULE_DELAY_MILLIS
|
||||
|
||||
@staticmethod
|
||||
def _default_max_export_batch_size():
|
||||
try:
|
||||
return int(
|
||||
environ.get(
|
||||
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
|
||||
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception(
|
||||
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
|
||||
OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
|
||||
_DEFAULT_MAX_EXPORT_BATCH_SIZE,
|
||||
)
|
||||
return _DEFAULT_MAX_EXPORT_BATCH_SIZE
|
||||
|
||||
@staticmethod
|
||||
def _default_export_timeout_millis():
|
||||
try:
|
||||
return int(
|
||||
environ.get(
|
||||
OTEL_BSP_EXPORT_TIMEOUT, _DEFAULT_EXPORT_TIMEOUT_MILLIS
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
logger.exception(
|
||||
_ENV_VAR_INT_VALUE_ERROR_MESSAGE,
|
||||
OTEL_BSP_EXPORT_TIMEOUT,
|
||||
_DEFAULT_EXPORT_TIMEOUT_MILLIS,
|
||||
)
|
||||
return _DEFAULT_EXPORT_TIMEOUT_MILLIS
|
||||
|
||||
@staticmethod
|
||||
def _validate_arguments(
|
||||
max_queue_size, schedule_delay_millis, max_export_batch_size
|
||||
):
|
||||
if max_queue_size <= 0:
|
||||
raise ValueError("max_queue_size must be a positive integer.")
|
||||
|
||||
if schedule_delay_millis <= 0:
|
||||
raise ValueError("schedule_delay_millis must be positive.")
|
||||
|
||||
if max_export_batch_size <= 0:
|
||||
raise ValueError(
|
||||
"max_export_batch_size must be a positive integer."
|
||||
)
|
||||
|
||||
if max_export_batch_size > max_queue_size:
|
||||
raise ValueError(
|
||||
"max_export_batch_size must be less than or equal to max_queue_size."
|
||||
)
|
||||
|
||||
|
||||
class ConsoleSpanExporter(SpanExporter):
|
||||
"""Implementation of :class:`SpanExporter` that prints spans to the
|
||||
console.
|
||||
|
||||
This class can be used for diagnostic purposes. It prints the exported
|
||||
spans to the console STDOUT.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
service_name: str | None = None,
|
||||
out: typing.IO = sys.stdout,
|
||||
formatter: typing.Callable[
|
||||
[ReadableSpan], str
|
||||
] = lambda span: span.to_json() + linesep,
|
||||
):
|
||||
self.out = out
|
||||
self.formatter = formatter
|
||||
self.service_name = service_name
|
||||
|
||||
def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult:
|
||||
for span in spans:
|
||||
self.out.write(self.formatter(span))
|
||||
self.out.flush()
|
||||
return SpanExportResult.SUCCESS
|
||||
|
||||
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
||||
return True
|
||||
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from opentelemetry.sdk.trace import ReadableSpan
|
||||
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
||||
|
||||
|
||||
class InMemorySpanExporter(SpanExporter):
|
||||
"""Implementation of :class:`.SpanExporter` that stores spans in memory.
|
||||
|
||||
This class can be used for testing purposes. It stores the exported spans
|
||||
in a list in memory that can be retrieved using the
|
||||
:func:`.get_finished_spans` method.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._finished_spans: typing.List[ReadableSpan] = []
|
||||
self._stopped = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear list of collected spans."""
|
||||
with self._lock:
|
||||
self._finished_spans.clear()
|
||||
|
||||
def get_finished_spans(self) -> typing.Tuple[ReadableSpan, ...]:
|
||||
"""Get list of collected spans."""
|
||||
with self._lock:
|
||||
return tuple(self._finished_spans)
|
||||
|
||||
def export(self, spans: typing.Sequence[ReadableSpan]) -> SpanExportResult:
|
||||
"""Stores a list of spans in memory."""
|
||||
if self._stopped:
|
||||
return SpanExportResult.FAILURE
|
||||
with self._lock:
|
||||
self._finished_spans.extend(spans)
|
||||
return SpanExportResult.SUCCESS
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Shut downs the exporter.
|
||||
|
||||
Calls to export after the exporter has been shut down will fail.
|
||||
"""
|
||||
self._stopped = True
|
||||
|
||||
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
||||
return True
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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.
|
||||
|
||||
import abc
|
||||
import random
|
||||
|
||||
from opentelemetry import trace
|
||||
|
||||
|
||||
class IdGenerator(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def generate_span_id(self) -> int:
|
||||
"""Get a new span ID.
|
||||
|
||||
Returns:
|
||||
A 64-bit int for use as a span ID
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def generate_trace_id(self) -> int:
|
||||
"""Get a new trace ID.
|
||||
|
||||
Implementations should at least make the 64 least significant bits
|
||||
uniformly random. Samplers like the `TraceIdRatioBased` sampler rely on
|
||||
this randomness to make sampling decisions.
|
||||
|
||||
See `the specification on TraceIdRatioBased <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#traceidratiobased>`_.
|
||||
|
||||
Returns:
|
||||
A 128-bit int for use as a trace ID
|
||||
"""
|
||||
|
||||
|
||||
class RandomIdGenerator(IdGenerator):
|
||||
"""The default ID generator for TracerProvider which randomly generates all
|
||||
bits when generating IDs.
|
||||
"""
|
||||
|
||||
def generate_span_id(self) -> int:
|
||||
span_id = random.getrandbits(64)
|
||||
while span_id == trace.INVALID_SPAN_ID:
|
||||
span_id = random.getrandbits(64)
|
||||
return span_id
|
||||
|
||||
def generate_trace_id(self) -> int:
|
||||
trace_id = random.getrandbits(128)
|
||||
while trace_id == trace.INVALID_TRACE_ID:
|
||||
trace_id = random.getrandbits(128)
|
||||
return trace_id
|
||||
@@ -0,0 +1,453 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user