Skip to content

Commit c6fab7d

Browse files
xrmxemdnetoaabmass
authored
Implement Explicit Bucket Boundaries advisory for Histograms (open-telemetry#4361)
--------- Co-authored-by: Emídio Neto <[email protected]> Co-authored-by: Aaron Abbott <[email protected]>
1 parent e3d39db commit c6fab7d

File tree

13 files changed

+638
-203
lines changed

13 files changed

+638
-203
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- [BREAKING] semantic-conventions: Remove `opentelemetry.semconv.attributes.network_attributes.NETWORK_INTERFACE_NAME`
2121
introduced by mistake in the wrong module.
2222
([#4391](https://github.com/open-telemetry/opentelemetry-python/pull/4391))
23+
- Add support for explicit bucket boundaries advisory for Histograms
24+
([#4361](https://github.com/open-telemetry/opentelemetry-python/pull/4361))
2325
- semantic-conventions: Bump to 1.30.0
2426
([#4337](https://github.com/open-telemetry/opentelemetry-python/pull/4397))
2527

docs/examples/metrics/instruments/example.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,13 @@ def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]:
5858
histogram = meter.create_histogram("histogram")
5959
histogram.record(99.9)
6060

61+
62+
# Histogram with explicit bucket boundaries advisory
63+
histogram = meter.create_histogram(
64+
"histogram_with_advisory",
65+
explicit_bucket_boundaries_advisory=[0.0, 1.0, 2.0],
66+
)
67+
histogram.record(99.9)
68+
6169
# Async Gauge
6270
gauge = meter.create_observable_gauge("gauge", [observable_gauge_func])

opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py

Lines changed: 129 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@
4242

4343
import warnings
4444
from abc import ABC, abstractmethod
45+
from dataclasses import dataclass
4546
from logging import getLogger
4647
from os import environ
4748
from threading import Lock
48-
from typing import List, Optional, Sequence, Set, Tuple, Union, cast
49+
from typing import Dict, List, Optional, Sequence, Union, cast
4950

5051
from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
5152
from opentelemetry.metrics._internal.instrument import (
@@ -64,6 +65,7 @@
6465
ObservableGauge,
6566
ObservableUpDownCounter,
6667
UpDownCounter,
68+
_MetricsHistogramAdvisory,
6769
_ProxyCounter,
6870
_ProxyGauge,
6971
_ProxyHistogram,
@@ -74,7 +76,9 @@
7476
)
7577
from opentelemetry.util._once import Once
7678
from opentelemetry.util._providers import _load_provider
77-
from opentelemetry.util.types import Attributes
79+
from opentelemetry.util.types import (
80+
Attributes,
81+
)
7882

7983
_logger = getLogger(__name__)
8084

@@ -177,6 +181,14 @@ def on_set_meter_provider(self, meter_provider: MeterProvider) -> None:
177181
meter.on_set_meter_provider(meter_provider)
178182

179183

184+
@dataclass
185+
class _InstrumentRegistrationStatus:
186+
instrument_id: str
187+
already_registered: bool
188+
conflict: bool
189+
current_advisory: Optional[_MetricsHistogramAdvisory]
190+
191+
180192
class Meter(ABC):
181193
"""Handles instrument creation.
182194
@@ -194,7 +206,9 @@ def __init__(
194206
self._name = name
195207
self._version = version
196208
self._schema_url = schema_url
197-
self._instrument_ids: Set[str] = set()
209+
self._instrument_ids: Dict[
210+
str, Optional[_MetricsHistogramAdvisory]
211+
] = {}
198212
self._instrument_ids_lock = Lock()
199213

200214
@property
@@ -218,31 +232,68 @@ def schema_url(self) -> Optional[str]:
218232
"""
219233
return self._schema_url
220234

221-
def _is_instrument_registered(
222-
self, name: str, type_: type, unit: str, description: str
223-
) -> Tuple[bool, str]:
235+
def _register_instrument(
236+
self,
237+
name: str,
238+
type_: type,
239+
unit: str,
240+
description: str,
241+
advisory: Optional[_MetricsHistogramAdvisory] = None,
242+
) -> _InstrumentRegistrationStatus:
224243
"""
225-
Check if an instrument with the same name, type, unit and description
226-
has been registered already.
227-
228-
Returns a tuple. The first value is `True` if the instrument has been
229-
registered already, `False` otherwise. The second value is the
230-
instrument id.
244+
Register an instrument with the name, type, unit and description as
245+
identifying keys and the advisory as value.
246+
247+
Returns a tuple. The first value is the instrument id.
248+
The second value is an `_InstrumentRegistrationStatus` where
249+
`already_registered` is `True` if the instrument has been registered
250+
already.
251+
If `conflict` is set to True the `current_advisory` attribute contains
252+
the registered instrument advisory.
231253
"""
232254

233255
instrument_id = ",".join(
234256
[name.strip().lower(), type_.__name__, unit, description]
235257
)
236258

237-
result = False
259+
already_registered = False
260+
conflict = False
261+
current_advisory = None
238262

239263
with self._instrument_ids_lock:
240-
if instrument_id in self._instrument_ids:
241-
result = True
264+
# we are not using get because None is a valid value
265+
already_registered = instrument_id in self._instrument_ids
266+
if already_registered:
267+
current_advisory = self._instrument_ids[instrument_id]
268+
conflict = current_advisory != advisory
242269
else:
243-
self._instrument_ids.add(instrument_id)
270+
self._instrument_ids[instrument_id] = advisory
244271

245-
return (result, instrument_id)
272+
return _InstrumentRegistrationStatus(
273+
instrument_id=instrument_id,
274+
already_registered=already_registered,
275+
conflict=conflict,
276+
current_advisory=current_advisory,
277+
)
278+
279+
@staticmethod
280+
def _log_instrument_registration_conflict(
281+
name: str,
282+
instrumentation_type: str,
283+
unit: str,
284+
description: str,
285+
status: _InstrumentRegistrationStatus,
286+
) -> None:
287+
_logger.warning(
288+
"An instrument with name %s, type %s, unit %s and "
289+
"description %s has been created already with a "
290+
"different advisory value %s and will be used instead.",
291+
name,
292+
instrumentation_type,
293+
unit,
294+
description,
295+
status.current_advisory,
296+
)
246297

247298
@abstractmethod
248299
def create_counter(
@@ -379,6 +430,8 @@ def create_histogram(
379430
name: str,
380431
unit: str = "",
381432
description: str = "",
433+
*,
434+
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
382435
) -> Histogram:
383436
"""Creates a :class:`~opentelemetry.metrics.Histogram` instrument
384437
@@ -526,13 +579,20 @@ def create_histogram(
526579
name: str,
527580
unit: str = "",
528581
description: str = "",
582+
*,
583+
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
529584
) -> Histogram:
530585
with self._lock:
531586
if self._real_meter:
532587
return self._real_meter.create_histogram(
533-
name, unit, description
588+
name,
589+
unit,
590+
description,
591+
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
534592
)
535-
proxy = _ProxyHistogram(name, unit, description)
593+
proxy = _ProxyHistogram(
594+
name, unit, description, explicit_bucket_boundaries_advisory
595+
)
536596
self._instruments.append(proxy)
537597
return proxy
538598

@@ -602,17 +662,18 @@ def create_counter(
602662
description: str = "",
603663
) -> Counter:
604664
"""Returns a no-op Counter."""
605-
if self._is_instrument_registered(
665+
status = self._register_instrument(
606666
name, NoOpCounter, unit, description
607-
)[0]:
608-
_logger.warning(
609-
"An instrument with name %s, type %s, unit %s and "
610-
"description %s has been created already.",
667+
)
668+
if status.conflict:
669+
self._log_instrument_registration_conflict(
611670
name,
612671
Counter.__name__,
613672
unit,
614673
description,
674+
status,
615675
)
676+
616677
return NoOpCounter(name, unit=unit, description=description)
617678

618679
def create_gauge(
@@ -622,16 +683,14 @@ def create_gauge(
622683
description: str = "",
623684
) -> Gauge:
624685
"""Returns a no-op Gauge."""
625-
if self._is_instrument_registered(name, NoOpGauge, unit, description)[
626-
0
627-
]:
628-
_logger.warning(
629-
"An instrument with name %s, type %s, unit %s and "
630-
"description %s has been created already.",
686+
status = self._register_instrument(name, NoOpGauge, unit, description)
687+
if status.conflict:
688+
self._log_instrument_registration_conflict(
631689
name,
632690
Gauge.__name__,
633691
unit,
634692
description,
693+
status,
635694
)
636695
return NoOpGauge(name, unit=unit, description=description)
637696

@@ -642,16 +701,16 @@ def create_up_down_counter(
642701
description: str = "",
643702
) -> UpDownCounter:
644703
"""Returns a no-op UpDownCounter."""
645-
if self._is_instrument_registered(
704+
status = self._register_instrument(
646705
name, NoOpUpDownCounter, unit, description
647-
)[0]:
648-
_logger.warning(
649-
"An instrument with name %s, type %s, unit %s and "
650-
"description %s has been created already.",
706+
)
707+
if status.conflict:
708+
self._log_instrument_registration_conflict(
651709
name,
652710
UpDownCounter.__name__,
653711
unit,
654712
description,
713+
status,
655714
)
656715
return NoOpUpDownCounter(name, unit=unit, description=description)
657716

@@ -663,16 +722,16 @@ def create_observable_counter(
663722
description: str = "",
664723
) -> ObservableCounter:
665724
"""Returns a no-op ObservableCounter."""
666-
if self._is_instrument_registered(
725+
status = self._register_instrument(
667726
name, NoOpObservableCounter, unit, description
668-
)[0]:
669-
_logger.warning(
670-
"An instrument with name %s, type %s, unit %s and "
671-
"description %s has been created already.",
727+
)
728+
if status.conflict:
729+
self._log_instrument_registration_conflict(
672730
name,
673731
ObservableCounter.__name__,
674732
unit,
675733
description,
734+
status,
676735
)
677736
return NoOpObservableCounter(
678737
name,
@@ -686,20 +745,33 @@ def create_histogram(
686745
name: str,
687746
unit: str = "",
688747
description: str = "",
748+
*,
749+
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
689750
) -> Histogram:
690751
"""Returns a no-op Histogram."""
691-
if self._is_instrument_registered(
692-
name, NoOpHistogram, unit, description
693-
)[0]:
694-
_logger.warning(
695-
"An instrument with name %s, type %s, unit %s and "
696-
"description %s has been created already.",
752+
status = self._register_instrument(
753+
name,
754+
NoOpHistogram,
755+
unit,
756+
description,
757+
_MetricsHistogramAdvisory(
758+
explicit_bucket_boundaries=explicit_bucket_boundaries_advisory
759+
),
760+
)
761+
if status.conflict:
762+
self._log_instrument_registration_conflict(
697763
name,
698764
Histogram.__name__,
699765
unit,
700766
description,
767+
status,
701768
)
702-
return NoOpHistogram(name, unit=unit, description=description)
769+
return NoOpHistogram(
770+
name,
771+
unit=unit,
772+
description=description,
773+
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
774+
)
703775

704776
def create_observable_gauge(
705777
self,
@@ -709,16 +781,16 @@ def create_observable_gauge(
709781
description: str = "",
710782
) -> ObservableGauge:
711783
"""Returns a no-op ObservableGauge."""
712-
if self._is_instrument_registered(
784+
status = self._register_instrument(
713785
name, NoOpObservableGauge, unit, description
714-
)[0]:
715-
_logger.warning(
716-
"An instrument with name %s, type %s, unit %s and "
717-
"description %s has been created already.",
786+
)
787+
if status.conflict:
788+
self._log_instrument_registration_conflict(
718789
name,
719790
ObservableGauge.__name__,
720791
unit,
721792
description,
793+
status,
722794
)
723795
return NoOpObservableGauge(
724796
name,
@@ -735,16 +807,16 @@ def create_observable_up_down_counter(
735807
description: str = "",
736808
) -> ObservableUpDownCounter:
737809
"""Returns a no-op ObservableUpDownCounter."""
738-
if self._is_instrument_registered(
810+
status = self._register_instrument(
739811
name, NoOpObservableUpDownCounter, unit, description
740-
)[0]:
741-
_logger.warning(
742-
"An instrument with name %s, type %s, unit %s and "
743-
"description %s has been created already.",
812+
)
813+
if status.conflict:
814+
self._log_instrument_registration_conflict(
744815
name,
745816
ObservableUpDownCounter.__name__,
746817
unit,
747818
description,
819+
status,
748820
)
749821
return NoOpObservableUpDownCounter(
750822
name,

0 commit comments

Comments
 (0)