Skip to content

Commit 76ea42f

Browse files
anuraagaxrmx
andauthored
feat(trace): implement span start/end metrics (#4880)
* feat(trace): implement span start/end metrics * changelog * Filter in TestBase * Format * Fix scope check * Cleanup * Fix * Temporarily switch contrib tests to PR branch * Bump * Public symbols * Restore CI * Cleanup * Fix mismerge * Fix merge --------- Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent 550fc09 commit 76ea42f

File tree

4 files changed

+351
-0
lines changed

4 files changed

+351
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
([#4806](https://github.com/open-telemetry/opentelemetry-python/pull/4806))
3535
- Prevent possible endless recursion from happening in `SimpleLogRecordProcessor.on_emit`,
3636
([#4799](https://github.com/open-telemetry/opentelemetry-python/pull/4799)) and ([#4867](https://github.com/open-telemetry/opentelemetry-python/pull/4867)).
37+
- Implement span start/end metrics
38+
([#4880](https://github.com/open-telemetry/opentelemetry-python/pull/4880))
3739
- Add environment variable carriers to API
3840
([#4609](https://github.com/open-telemetry/opentelemetry-python/pull/4609))
3941
- Add experimental composable rule based sampler

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from typing_extensions import deprecated
4848

4949
from opentelemetry import context as context_api
50+
from opentelemetry import metrics as metrics_api
5051
from opentelemetry import trace as trace_api
5152
from opentelemetry.attributes import BoundedAttributes
5253
from opentelemetry.sdk import util
@@ -80,6 +81,8 @@
8081
from opentelemetry.util import types
8182
from opentelemetry.util._decorator import _agnosticcontextmanager
8283

84+
from ._tracer_metrics import TracerMetrics
85+
8386
logger = logging.getLogger(__name__)
8487

8588
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
@@ -814,6 +817,7 @@ def __init__(
814817
set_status_on_exception: bool = True,
815818
limits=_UnsetLimits,
816819
instrumentation_scope: Optional[InstrumentationScope] = None,
820+
record_end_metrics: Optional[Callable[[], None]] = None,
817821
) -> None:
818822
if resource is None:
819823
resource = Resource.create({})
@@ -851,6 +855,8 @@ def __init__(
851855

852856
self._links = self._new_links(links)
853857

858+
self._record_end_metrics = record_end_metrics
859+
854860
def __repr__(self):
855861
return f'{type(self).__name__}(name="{self._name}", context={self._context})'
856862

@@ -980,6 +986,8 @@ def end(self, end_time: Optional[int] = None) -> None:
980986

981987
self._end_time = end_time if end_time is not None else time_ns()
982988

989+
if self._record_end_metrics:
990+
self._record_end_metrics()
983991
# pylint: disable=protected-access
984992
self._span_processor._on_ending(self)
985993
self._span_processor.on_end(self._readable_span())
@@ -1106,6 +1114,7 @@ def __init__(
11061114
instrumentation_info: InstrumentationInfo,
11071115
span_limits: SpanLimits,
11081116
instrumentation_scope: InstrumentationScope,
1117+
meter_provider: Optional[metrics_api.MeterProvider] = None,
11091118
*,
11101119
_tracer_provider: Optional["TracerProvider"] = None,
11111120
) -> None:
@@ -1118,6 +1127,9 @@ def __init__(
11181127
self._instrumentation_scope = instrumentation_scope
11191128
self._tracer_provider = _tracer_provider
11201129

1130+
meter_provider = meter_provider or metrics_api.get_meter_provider()
1131+
self._tracer_metrics = TracerMetrics(meter_provider)
1132+
11211133
def _is_enabled(self) -> bool:
11221134
"""If the tracer is not enabled, start_span will create a NonRecordingSpan"""
11231135

@@ -1214,6 +1226,10 @@ def start_span( # pylint: disable=too-many-locals
12141226
trace_state=sampling_result.trace_state,
12151227
)
12161228

1229+
record_end_metrics = self._tracer_metrics.start_span(
1230+
parent_span_context, sampling_result.decision
1231+
)
1232+
12171233
# Only record if is_recording() is true
12181234
if sampling_result.decision.is_recording():
12191235
# pylint:disable=protected-access
@@ -1232,6 +1248,7 @@ def start_span( # pylint: disable=too-many-locals
12321248
set_status_on_exception=set_status_on_exception,
12331249
limits=self._span_limits,
12341250
instrumentation_scope=self._instrumentation_scope,
1251+
record_end_metrics=record_end_metrics,
12351252
)
12361253
span.start(start_time=start_time, parent_context=context)
12371254
else:
@@ -1313,6 +1330,7 @@ def __init__(
13131330
] = None,
13141331
id_generator: Optional[IdGenerator] = None,
13151332
span_limits: Optional[SpanLimits] = None,
1333+
meter_provider: Optional[metrics_api.MeterProvider] = None,
13161334
*,
13171335
_tracer_configurator: Optional[_TracerConfiguratorT] = None,
13181336
) -> None:
@@ -1334,6 +1352,7 @@ def __init__(
13341352
disabled = environ.get(OTEL_SDK_DISABLED, "")
13351353
self._disabled = disabled.lower().strip() == "true"
13361354
self._atexit_handler = None
1355+
self._meter_provider = meter_provider
13371356

13381357
if shutdown_on_exit:
13391358
self._atexit_handler = atexit.register(self.shutdown)
@@ -1406,6 +1425,7 @@ def get_tracer(
14061425
schema_url,
14071426
attributes,
14081427
),
1428+
self._meter_provider,
14091429
_tracer_provider=self,
14101430
)
14111431

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright The 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+
from __future__ import annotations
16+
17+
from collections.abc import Callable
18+
19+
from opentelemetry import metrics as metrics_api
20+
from opentelemetry.sdk.trace.sampling import Decision
21+
from opentelemetry.semconv._incubating.attributes.otel_attributes import (
22+
OTEL_SPAN_PARENT_ORIGIN,
23+
OTEL_SPAN_SAMPLING_RESULT,
24+
OtelSpanSamplingResultValues,
25+
)
26+
from opentelemetry.semconv._incubating.metrics.otel_metrics import (
27+
create_otel_sdk_span_live,
28+
create_otel_sdk_span_started,
29+
)
30+
from opentelemetry.trace.span import SpanContext
31+
32+
33+
class TracerMetrics:
34+
def __init__(self, meter_provider: metrics_api.MeterProvider) -> None:
35+
meter = meter_provider.get_meter("opentelemetry-sdk")
36+
37+
self._started_spans = create_otel_sdk_span_started(meter)
38+
self._live_spans = create_otel_sdk_span_live(meter)
39+
40+
def start_span(
41+
self,
42+
parent_span_context: SpanContext | None,
43+
sampling_decision: Decision,
44+
) -> Callable[[], None]:
45+
sampling_result_value = sampling_result(sampling_decision)
46+
self._started_spans.add(
47+
1,
48+
{
49+
OTEL_SPAN_PARENT_ORIGIN: parent_origin(parent_span_context),
50+
OTEL_SPAN_SAMPLING_RESULT: sampling_result_value,
51+
},
52+
)
53+
54+
if not sampling_decision.is_recording():
55+
return noop
56+
57+
live_span_attrs = {
58+
OTEL_SPAN_SAMPLING_RESULT: sampling_result_value,
59+
}
60+
self._live_spans.add(1, live_span_attrs)
61+
62+
def end_span() -> None:
63+
self._live_spans.add(-1, live_span_attrs)
64+
65+
return end_span
66+
67+
68+
def noop() -> None:
69+
pass
70+
71+
72+
def parent_origin(span_ctx: SpanContext | None) -> str:
73+
if span_ctx is None:
74+
return "none"
75+
if span_ctx.is_remote:
76+
return "remote"
77+
return "local"
78+
79+
80+
def sampling_result(decision: Decision) -> str:
81+
if decision == Decision.RECORD_AND_SAMPLE:
82+
return OtelSpanSamplingResultValues.RECORD_AND_SAMPLE.value
83+
if decision == Decision.RECORD_ONLY:
84+
return OtelSpanSamplingResultValues.RECORD_ONLY.value
85+
return OtelSpanSamplingResultValues.DROP.value

0 commit comments

Comments
 (0)