Skip to content

Implement ExplicitBucketBoundaries advisory for Histograms #4361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 42 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c058564
opentelemetry-api: add advisory parameters to Histograms
xrmx Dec 2, 2024
c435220
Update sdk
xrmx Dec 2, 2024
3287503
No need to use Sequence from collections
xrmx Dec 17, 2024
1ee046e
Fix typing in proxy
xrmx Dec 17, 2024
83fd817
Rewrote MetricsInstrumentAdvisory as TypedDict
xrmx Dec 19, 2024
e033083
Rename ExplicitBucketBoundaries to explicit_bucket_boundaries
xrmx Dec 23, 2024
1c90718
Add changelog
xrmx Jan 21, 2025
8114597
Add an example in docs
xrmx Dec 23, 2024
05f974a
parameter validation should just report an error, not fail
xrmx Jan 15, 2025
1012547
Make test reproducible
xrmx Jan 15, 2025
3ebe6df
Fix type of MetricsInstrumentAdvisory.explicit_bucket_boundaries
xrmx Jan 15, 2025
f7af6f4
Make advisory attribute private
xrmx Jan 15, 2025
58d24cf
Permit also a sequence of ints as boundaries
xrmx Jan 15, 2025
2e6be0b
Rework conflicting instrument registration check
xrmx Jan 16, 2025
2c3608d
Fix typing for api instrumentors
xrmx Jan 16, 2025
c996647
Fixup lint
xrmx Jan 16, 2025
dbf8a04
Add integration tests
xrmx Jan 17, 2025
f659394
Make explicit boundaries float only
xrmx Jan 17, 2025
7f3f39a
Stop using assertNoLogs to have green tests on py < 3.10
xrmx Jan 17, 2025
d9fb954
Fix api tests for real
xrmx Jan 17, 2025
70e25bb
Change interface to a dataclass instead of a typed dict
xrmx Jan 17, 2025
76a3181
Add default
xrmx Jan 17, 2025
1ae32b2
Return instrument_id in _InstrumentRegistrationStatus
xrmx Jan 20, 2025
6678f42
Don't use tuple assignment
xrmx Jan 20, 2025
2a3242d
Correct advisory check to match reality
xrmx Jan 20, 2025
cf14e2f
fixup typo
xrmx Jan 20, 2025
b2c1f1b
Centralize instrument registration conflict logging
xrmx Jan 20, 2025
e29d5d5
Add advisory parameter to all instrumentor constructor
xrmx Jan 20, 2025
69f58a2
Accept ints as valid value for advisory explicit bucket boundaries
xrmx Jan 20, 2025
b7b4977
Please mypy again
xrmx Jan 21, 2025
99e123e
Move Advisory types to metrics _internal and export the public one
xrmx Jan 21, 2025
cf2163d
Give an hint on instrument registration conflict that the other one w…
xrmx Jan 21, 2025
93db7ea
Update opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
xrmx Jan 23, 2025
82e2f08
Update opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
emdneto Jan 23, 2025
82536bc
Flattened advisory params
xrmx Jan 24, 2025
6a8b8a7
Keep using _MetricsHistogramAdvisory for storing advisory internally
xrmx Jan 27, 2025
a79030d
Make explicit_bucket_boundaries_advisory kwargs only
xrmx Jan 27, 2025
89c8391
Fix wrong params in create_histogram
xrmx Jan 27, 2025
4bbc222
Merge branch 'main' into histogram-advisory
xrmx Jan 28, 2025
95e77fb
Update CHANGELOG.md
xrmx Jan 28, 2025
9ef6701
Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/aggr…
xrmx Jan 28, 2025
3826348
Consider empty bucket boundaries as valid
xrmx Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [BREAKING] semantic-conventions: Remove `opentelemetry.semconv.attributes.network_attributes.NETWORK_INTERFACE_NAME`
introduced by mistake in the wrong module.
([#4391](https://github.com/open-telemetry/opentelemetry-python/pull/4391))
- Add support for explicit bucket_boundaries advisory for Histograms
([#4361](https://github.com/open-telemetry/opentelemetry-python/pull/4361))
- semantic-conventions: Bump to 1.30.0
([#4337](https://github.com/open-telemetry/opentelemetry-python/pull/4397))

Expand Down
8 changes: 8 additions & 0 deletions docs/examples/metrics/instruments/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,13 @@ def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]:
histogram = meter.create_histogram("histogram")
histogram.record(99.9)


# Histogram with explicit bucket boundaries advisory
histogram = meter.create_histogram(
"histogram_with_advisory",
explicit_bucket_boundaries_advisory=[0.0, 1.0, 2.0],
)
histogram.record(99.9)

# Async Gauge
gauge = meter.create_observable_gauge("gauge", [observable_gauge_func])
186 changes: 129 additions & 57 deletions opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@

import warnings
from abc import ABC, abstractmethod
from dataclasses import dataclass
from logging import getLogger
from os import environ
from threading import Lock
from typing import List, Optional, Sequence, Set, Tuple, Union, cast
from typing import Dict, List, Optional, Sequence, Union, cast

from opentelemetry.environment_variables import OTEL_PYTHON_METER_PROVIDER
from opentelemetry.metrics._internal.instrument import (
Expand All @@ -64,6 +65,7 @@
ObservableGauge,
ObservableUpDownCounter,
UpDownCounter,
_MetricsHistogramAdvisory,
_ProxyCounter,
_ProxyGauge,
_ProxyHistogram,
Expand All @@ -74,7 +76,9 @@
)
from opentelemetry.util._once import Once
from opentelemetry.util._providers import _load_provider
from opentelemetry.util.types import Attributes
from opentelemetry.util.types import (
Attributes,
)

_logger = getLogger(__name__)

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


@dataclass
class _InstrumentRegistrationStatus:
instrument_id: str
already_registered: bool
conflict: bool
current_advisory: Optional[_MetricsHistogramAdvisory]


class Meter(ABC):
"""Handles instrument creation.

Expand All @@ -194,7 +206,9 @@ def __init__(
self._name = name
self._version = version
self._schema_url = schema_url
self._instrument_ids: Set[str] = set()
self._instrument_ids: Dict[
str, Optional[_MetricsHistogramAdvisory]
] = {}
self._instrument_ids_lock = Lock()

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

def _is_instrument_registered(
self, name: str, type_: type, unit: str, description: str
) -> Tuple[bool, str]:
def _register_instrument(
self,
name: str,
type_: type,
unit: str,
description: str,
advisory: Optional[_MetricsHistogramAdvisory] = None,
) -> _InstrumentRegistrationStatus:
"""
Check if an instrument with the same name, type, unit and description
has been registered already.

Returns a tuple. The first value is `True` if the instrument has been
registered already, `False` otherwise. The second value is the
instrument id.
Register an instrument with the name, type, unit and description as
identifying keys and the advisory as value.

Returns a tuple. The first value is the instrument id.
The second value is an `_InstrumentRegistrationStatus` where
`already_registered` is `True` if the instrument has been registered
already.
If `conflict` is set to True the `current_advisory` attribute contains
the registered instrument advisory.
"""

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

result = False
already_registered = False
conflict = False
current_advisory = None

with self._instrument_ids_lock:
if instrument_id in self._instrument_ids:
result = True
# we are not using get because None is a valid value
already_registered = instrument_id in self._instrument_ids
if already_registered:
current_advisory = self._instrument_ids[instrument_id]
conflict = current_advisory != advisory
else:
self._instrument_ids.add(instrument_id)
self._instrument_ids[instrument_id] = advisory

return (result, instrument_id)
return _InstrumentRegistrationStatus(
instrument_id=instrument_id,
already_registered=already_registered,
conflict=conflict,
current_advisory=current_advisory,
)

@staticmethod
def _log_instrument_registration_conflict(
name: str,
instrumentation_type: str,
unit: str,
description: str,
status: _InstrumentRegistrationStatus,
) -> None:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already with a "
"different advisory value %s and will be used instead.",
name,
instrumentation_type,
unit,
description,
status.current_advisory,
)

@abstractmethod
def create_counter(
Expand Down Expand Up @@ -379,6 +430,8 @@ def create_histogram(
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
"""Creates a :class:`~opentelemetry.metrics.Histogram` instrument

Expand Down Expand Up @@ -526,13 +579,20 @@ def create_histogram(
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
with self._lock:
if self._real_meter:
return self._real_meter.create_histogram(
name, unit, description
name,
unit,
description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)
proxy = _ProxyHistogram(name, unit, description)
proxy = _ProxyHistogram(
name, unit, description, explicit_bucket_boundaries_advisory
)
self._instruments.append(proxy)
return proxy

Expand Down Expand Up @@ -602,17 +662,18 @@ def create_counter(
description: str = "",
) -> Counter:
"""Returns a no-op Counter."""
if self._is_instrument_registered(
status = self._register_instrument(
name, NoOpCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Counter.__name__,
unit,
description,
status,
)

return NoOpCounter(name, unit=unit, description=description)

def create_gauge(
Expand All @@ -622,16 +683,14 @@ def create_gauge(
description: str = "",
) -> Gauge:
"""Returns a no-op Gauge."""
if self._is_instrument_registered(name, NoOpGauge, unit, description)[
0
]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
status = self._register_instrument(name, NoOpGauge, unit, description)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Gauge.__name__,
unit,
description,
status,
)
return NoOpGauge(name, unit=unit, description=description)

Expand All @@ -642,16 +701,16 @@ def create_up_down_counter(
description: str = "",
) -> UpDownCounter:
"""Returns a no-op UpDownCounter."""
if self._is_instrument_registered(
status = self._register_instrument(
name, NoOpUpDownCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
UpDownCounter.__name__,
unit,
description,
status,
)
return NoOpUpDownCounter(name, unit=unit, description=description)

Expand All @@ -663,16 +722,16 @@ def create_observable_counter(
description: str = "",
) -> ObservableCounter:
"""Returns a no-op ObservableCounter."""
if self._is_instrument_registered(
status = self._register_instrument(
name, NoOpObservableCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableCounter.__name__,
unit,
description,
status,
)
return NoOpObservableCounter(
name,
Expand All @@ -686,20 +745,33 @@ def create_histogram(
name: str,
unit: str = "",
description: str = "",
*,
explicit_bucket_boundaries_advisory: Optional[Sequence[float]] = None,
) -> Histogram:
"""Returns a no-op Histogram."""
if self._is_instrument_registered(
name, NoOpHistogram, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
status = self._register_instrument(
name,
NoOpHistogram,
unit,
description,
_MetricsHistogramAdvisory(
explicit_bucket_boundaries=explicit_bucket_boundaries_advisory
),
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
Histogram.__name__,
unit,
description,
status,
)
return NoOpHistogram(name, unit=unit, description=description)
return NoOpHistogram(
name,
unit=unit,
description=description,
explicit_bucket_boundaries_advisory=explicit_bucket_boundaries_advisory,
)

def create_observable_gauge(
self,
Expand All @@ -709,16 +781,16 @@ def create_observable_gauge(
description: str = "",
) -> ObservableGauge:
"""Returns a no-op ObservableGauge."""
if self._is_instrument_registered(
status = self._register_instrument(
name, NoOpObservableGauge, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableGauge.__name__,
unit,
description,
status,
)
return NoOpObservableGauge(
name,
Expand All @@ -735,16 +807,16 @@ def create_observable_up_down_counter(
description: str = "",
) -> ObservableUpDownCounter:
"""Returns a no-op ObservableUpDownCounter."""
if self._is_instrument_registered(
status = self._register_instrument(
name, NoOpObservableUpDownCounter, unit, description
)[0]:
_logger.warning(
"An instrument with name %s, type %s, unit %s and "
"description %s has been created already.",
)
if status.conflict:
self._log_instrument_registration_conflict(
name,
ObservableUpDownCounter.__name__,
unit,
description,
status,
)
return NoOpObservableUpDownCounter(
name,
Expand Down
Loading
Loading