Skip to content

Commit

Permalink
Simplify set_gauge implementation
Browse files Browse the repository at this point in the history
The `set_gauge` now uses OpenTelemetry's newer helper which creates a
sync gauge.

Our own implementation of `create_gauge` around the async
OpenTelemetry's implementation has been removed in favor of the newer
sync helper.
  • Loading branch information
luismiramirez committed Aug 21, 2024
1 parent 3bdd10e commit f9b17a7
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 89 deletions.
6 changes: 6 additions & 0 deletions .changesets/simplify-the-implementation-of-set_gauge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: patch
type: change
---

Simplify the implementation of `set_gauge` in favor of the newer OpenTelemetry's sync implementation.
47 changes: 9 additions & 38 deletions src/appsignal/metrics.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Iterable
from typing import TYPE_CHECKING, Any

from opentelemetry.metrics import (
CallbackOptions,
Histogram,
Observation,
UpDownCounter,
get_meter,
)
from opentelemetry.metrics import Histogram, UpDownCounter, _Gauge, get_meter


if TYPE_CHECKING:
Expand Down Expand Up @@ -42,37 +36,14 @@ def add_distribution_value(name: str, value: int | float, tags: Tags = None) ->
histogram.record(value, tags)


_gauges: dict[str, dict[TagsKey, int | float]] = {}


def _create_gauge(name: str) -> None:
def gauge_callback(options: CallbackOptions) -> Iterable[Observation]:
gauge_entries = _gauges.get(name)
if gauge_entries is None:
return []

observations = []
for key, value in gauge_entries.items():
tags = None if key is None else dict(key)

observations.append(Observation(value, tags))

_gauges[name] = {}

return observations

_meter.create_observable_gauge(
name,
callbacks=[gauge_callback],
)
_gauges: dict[str, _Gauge] = {}


def set_gauge(name: str, value: float, tags: Tags = None) -> None:
if name not in _gauges:
# Create dict for every tag combination
_gauges[name] = {}
_create_gauge(name)

key = (frozenset(tags.items())) if tags is not None else None
if name in _gauges:
gauge = _gauges[name]
else:
gauge = _meter.create_gauge(name)
_gauges[name] = gauge

_gauges[name][key] = value
gauge.set(value, tags)
66 changes: 15 additions & 51 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

from typing import Any
from unittest.mock import call

from opentelemetry.metrics import CallbackOptions, Histogram, UpDownCounter
from opentelemetry.metrics import Histogram, UpDownCounter, _Gauge

from appsignal import add_distribution_value, increment_counter, set_gauge
from appsignal.metrics import _counters, _gauges, _histograms, _meter
Expand Down Expand Up @@ -77,73 +78,36 @@ def test_add_distribution_value_with_tags(mocker):


def test_set_gauge_creates_new_gauge(mocker):
meter_spy = mocker.spy(_meter, "create_observable_gauge")
meter_spy = mocker.spy(_meter, "create_gauge")
assert "metric_name" not in _gauges

set_gauge("metric_name", 10)

# Registers the gauges internally
assert _gauges["metric_name"][None] == 10
assert isinstance(_gauges["metric_name"], _Gauge)

# Check if it was registered on the meter
meter_spy.assert_called_once_with("metric_name", callbacks=mocker.ANY)
callbacks = meter_spy.call_args.kwargs["callbacks"]
assert len(callbacks) == 1
meter_spy.assert_called_once_with("metric_name")

# Mock the ObservableGauge's async task calling the callbacks
return_values = callbacks[0](CallbackOptions())
assert len(return_values) == 1
return_value = return_values[0]
assert return_value.value == 10
assert return_value.attributes is None

# Resets gauge values
assert _gauges["metric_name"] == {}
def test_set_gauge_updates_existing_gauge(mocker):
_gauges["existing"] = _meter.create_gauge("existing")
gauge1_spy = mocker.spy(_gauges["existing"], "set")

return_values = callbacks[0](CallbackOptions())
assert len(return_values) == 0


def test_set_gauge_updates_existing_gauge():
set_gauge("existing", 10)
set_gauge("existing", 11, None) # None is also the default

assert _gauges["existing"][None] == 11
gauge1_spy.assert_has_calls([call(10, None), call(11, None)])

_gauges["existing_with_tags"] = _meter.create_gauge("existing_with_tags")
gauge2_spy = mocker.spy(_gauges["existing_with_tags"], "set")

set_gauge("existing_with_tags", 10, {"tag1": "value1"})
set_gauge("existing_with_tags", 11, {"tag1": "value1"})

assert _gauges["existing_with_tags"][tags_key({"tag1": "value1"})] == 11


def test_set_gauge_updates_with_tags(mocker):
meter_spy = mocker.spy(_meter, "create_observable_gauge")
set_gauge("with_tags1", 10, {"tag1": "value1"})
set_gauge("with_tags1", 11, {"tag1": "value1"})
set_gauge("with_tags1", 55, {"other": "value2"})

assert _gauges["with_tags1"][tags_key({"tag1": "value1"})] == 11
assert _gauges["with_tags1"][tags_key({"other": "value2"})] == 55

# Mock the ObservableGauge's async task calling the callbacks
callbacks = meter_spy.call_args.kwargs["callbacks"]
return_values = callbacks[0](CallbackOptions())
assert len(return_values) == 2

obs1 = return_values[0]
assert obs1.value == 11
assert obs1.attributes == {"tag1": "value1"}

obs2 = return_values[1]
assert obs2.value == 55
assert obs2.attributes == {"other": "value2"}

# Resets gauge values
assert _gauges["with_tags1"] == {}

callbacks = meter_spy.call_args.kwargs["callbacks"]
return_values = callbacks[0](CallbackOptions())
assert len(return_values) == 0
gauge2_spy.assert_has_calls(
[call(10, {"tag1": "value1"}), call(11, {"tag1": "value1"})]
)


def tags_key(tags: dict[str, str]) -> frozenset[Any]:
Expand Down

0 comments on commit f9b17a7

Please sign in to comment.