Skip to content

Commit 5c89850

Browse files
authored
Add sampler API, use in SDK tracer (#225)
1 parent e4d8949 commit 5c89850

File tree

9 files changed

+463
-35
lines changed

9 files changed

+463
-35
lines changed

.flake8

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
11
[flake8]
2-
ignore = E501,W503,E203
3-
exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/,ext/opentelemetry-ext-jaeger/build/*
2+
ignore =
3+
E501 # line too long, defer to black
4+
F401 # unused import, defer to pylint
5+
W503 # allow line breaks after binary ops, not after
6+
exclude =
7+
.bzr
8+
.git
9+
.hg
10+
.svn
11+
.tox
12+
CVS
13+
__pycache__
14+
ext/opentelemetry-ext-jaeger/src/opentelemetry/ext/jaeger/gen/
15+
ext/opentelemetry-ext-jaeger/build/*

.pylintrc

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ disable=missing-docstring,
6868
ungrouped-imports, # Leave this up to isort
6969
wrong-import-order, # Leave this up to isort
7070
bad-continuation, # Leave this up to black
71-
line-too-long # Leave this up to black
71+
line-too-long, # Leave this up to black
72+
exec-used
7273

7374
# Enable the message, report, category or checker with the given id(s). You can
7475
# either give multiple identifier separated by comma (,) or put this option

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# -- Project information -----------------------------------------------------
2020

2121
project = "OpenTelemetry"
22-
copyright = "2019, OpenTelemetry Authors"
22+
copyright = "2019, OpenTelemetry Authors" # pylint: disable=redefined-builtin
2323
author = "OpenTelemetry Authors"
2424

2525

opentelemetry-api/src/opentelemetry/trace/__init__.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ def __exit__(
257257
class TraceOptions(int):
258258
"""A bitmask that represents options specific to the trace.
259259
260-
The only supported option is the "recorded" flag (``0x01``). If set, this
261-
flag indicates that the trace may have been recorded upstream.
260+
The only supported option is the "sampled" flag (``0x01``). If set, this
261+
flag indicates that the trace may have been sampled upstream.
262262
263263
See the `W3C Trace Context - Traceparent`_ spec for details.
264264
@@ -267,12 +267,16 @@ class TraceOptions(int):
267267
"""
268268

269269
DEFAULT = 0x00
270-
RECORDED = 0x01
270+
SAMPLED = 0x01
271271

272272
@classmethod
273273
def get_default(cls) -> "TraceOptions":
274274
return cls(cls.DEFAULT)
275275

276+
@property
277+
def sampled(self) -> bool:
278+
return bool(self & TraceOptions.SAMPLED)
279+
276280

277281
DEFAULT_TRACE_OPTIONS = TraceOptions.get_default()
278282

@@ -313,8 +317,8 @@ class SpanContext:
313317
Args:
314318
trace_id: The ID of the trace that this span belongs to.
315319
span_id: This span's ID.
316-
options: Trace options to propagate.
317-
state: Tracing-system-specific info to propagate.
320+
trace_options: Trace options to propagate.
321+
trace_state: Tracing-system-specific info to propagate.
318322
"""
319323

320324
def __init__(
@@ -367,6 +371,9 @@ def __init__(self, context: "SpanContext") -> None:
367371
def get_context(self) -> "SpanContext":
368372
return self._context
369373

374+
def is_recording_events(self) -> bool:
375+
return False
376+
370377

371378
INVALID_SPAN_ID = 0x0000000000000000
372379
INVALID_TRACE_ID = 0x00000000000000000000000000000000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import abc
16+
from typing import Dict, Mapping, Optional, Sequence
17+
18+
# pylint: disable=unused-import
19+
from opentelemetry.trace import Link, SpanContext
20+
from opentelemetry.util.types import AttributeValue
21+
22+
23+
class Decision:
24+
"""A sampling decision as applied to a newly-created Span.
25+
26+
Args:
27+
sampled: Whether the `Span` should be sampled.
28+
attributes: Attributes to add to the `Span`.
29+
"""
30+
31+
def __repr__(self) -> str:
32+
return "{}({}, attributes={})".format(
33+
type(self).__name__, str(self.sampled), str(self.attributes)
34+
)
35+
36+
def __init__(
37+
self,
38+
sampled: bool = False,
39+
attributes: Mapping[str, "AttributeValue"] = None,
40+
) -> None:
41+
self.sampled = sampled # type: bool
42+
if attributes is None:
43+
self.attributes = {} # type: Dict[str, "AttributeValue"]
44+
else:
45+
self.attributes = dict(attributes)
46+
47+
48+
class Sampler(abc.ABC):
49+
@abc.abstractmethod
50+
def should_sample(
51+
self,
52+
parent_context: Optional["SpanContext"],
53+
trace_id: int,
54+
span_id: int,
55+
name: str,
56+
links: Sequence["Link"] = (),
57+
) -> "Decision":
58+
pass
59+
60+
61+
class StaticSampler(Sampler):
62+
"""Sampler that always returns the same decision."""
63+
64+
def __init__(self, decision: "Decision"):
65+
self._decision = decision
66+
67+
def should_sample(
68+
self,
69+
parent_context: Optional["SpanContext"],
70+
trace_id: int,
71+
span_id: int,
72+
name: str,
73+
links: Sequence["Link"] = (),
74+
) -> "Decision":
75+
return self._decision
76+
77+
78+
class ProbabilitySampler(Sampler):
79+
def __init__(self, rate: float):
80+
self._rate = rate
81+
self._bound = self.get_bound_for_rate(self._rate)
82+
83+
# The sampler checks the last 8 bytes of the trace ID to decide whether to
84+
# sample a given trace.
85+
CHECK_BYTES = 0xFFFFFFFFFFFFFFFF
86+
87+
@classmethod
88+
def get_bound_for_rate(cls, rate: float) -> int:
89+
return round(rate * (cls.CHECK_BYTES + 1))
90+
91+
@property
92+
def rate(self) -> float:
93+
return self._rate
94+
95+
@rate.setter
96+
def rate(self, new_rate: float) -> None:
97+
self._rate = new_rate
98+
self._bound = self.get_bound_for_rate(self._rate)
99+
100+
@property
101+
def bound(self) -> int:
102+
return self._bound
103+
104+
def should_sample(
105+
self,
106+
parent_context: Optional["SpanContext"],
107+
trace_id: int,
108+
span_id: int,
109+
name: str,
110+
links: Sequence["Link"] = (),
111+
) -> "Decision":
112+
if parent_context is not None:
113+
return Decision(parent_context.trace_options.sampled)
114+
115+
return Decision(trace_id & self.CHECK_BYTES < self.bound)
116+
117+
118+
# Samplers that ignore the parent sampling decision and never/always sample.
119+
ALWAYS_OFF = StaticSampler(Decision(False))
120+
ALWAYS_ON = StaticSampler(Decision(True))
121+
122+
# Samplers that respect the parent sampling decision, but otherwise
123+
# never/always sample.
124+
DEFAULT_OFF = ProbabilitySampler(0.0)
125+
DEFAULT_ON = ProbabilitySampler(1.0)

0 commit comments

Comments
 (0)