Skip to content

Commit 5f4797a

Browse files
authored
Merge branch 'master' into issue_785
2 parents d3dd646 + 94aabcb commit 5f4797a

File tree

6 files changed

+147
-4
lines changed

6 files changed

+147
-4
lines changed

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

+36-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,17 @@ def add(self, value: ValueT) -> None:
6666
"""Increases the value of the bound counter by ``value``.
6767
6868
Args:
69-
value: The value to add to the bound counter.
69+
value: The value to add to the bound counter. Must be positive.
70+
"""
71+
72+
73+
class BoundUpDownCounter:
74+
def add(self, value: ValueT) -> None:
75+
"""Increases the value of the bound counter by ``value``.
76+
77+
Args:
78+
value: The value to add to the bound counter. Can be positive or
79+
negative.
7080
"""
7181

7282

@@ -137,7 +147,28 @@ def add(self, value: ValueT, labels: Dict[str, str]) -> None:
137147
"""Increases the value of the counter by ``value``.
138148
139149
Args:
140-
value: The value to add to the counter metric.
150+
value: The value to add to the counter metric. Should be positive
151+
or zero. For a Counter that can decrease, use
152+
`UpDownCounter`.
153+
labels: Labels to associate with the bound instrument.
154+
"""
155+
156+
157+
class UpDownCounter(Metric):
158+
"""A counter type metric that expresses the computation of a sum,
159+
allowing negative increments."""
160+
161+
def bind(self, labels: Dict[str, str]) -> "BoundUpDownCounter":
162+
"""Gets a `BoundUpDownCounter`."""
163+
return BoundUpDownCounter()
164+
165+
def add(self, value: ValueT, labels: Dict[str, str]) -> None:
166+
"""Increases the value of the counter by ``value``.
167+
168+
Args:
169+
value: The value to add to the counter metric. Can be positive or
170+
negative. For a Counter that is never decreasing, use
171+
`Counter`.
141172
labels: Labels to associate with the bound instrument.
142173
"""
143174

@@ -268,7 +299,9 @@ def get_meter(
268299

269300

270301
MetricT = TypeVar("MetricT", Counter, ValueRecorder)
271-
InstrumentT = TypeVar("InstrumentT", Counter, Observer, ValueRecorder)
302+
InstrumentT = TypeVar(
303+
"InstrumentT", Counter, UpDownCounter, Observer, ValueRecorder
304+
)
272305
ObserverT = TypeVar("ObserverT", bound=Observer)
273306
ObserverCallbackT = Callable[[Observer], None]
274307

opentelemetry-api/tests/metrics/test_metrics.py

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ def test_counter_add(self):
3535
counter = metrics.Counter()
3636
counter.add(1, {})
3737

38+
def test_updowncounter(self):
39+
counter = metrics.UpDownCounter()
40+
bound_counter = counter.bind({})
41+
self.assertIsInstance(bound_counter, metrics.BoundUpDownCounter)
42+
43+
def test_updowncounter_add(self):
44+
counter = metrics.Counter()
45+
counter.add(1, {})
46+
counter.add(-1, {})
47+
3848
def test_valuerecorder(self):
3949
valuerecorder = metrics.ValueRecorder()
4050
bound_valuerecorder = valuerecorder.bind({})

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

+34
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ def add(self, value: metrics_api.ValueT) -> None:
102102
if self._validate_update(value):
103103
self.update(value)
104104

105+
def _validate_update(self, value: metrics_api.ValueT) -> bool:
106+
if not super()._validate_update(value):
107+
return False
108+
if value < 0:
109+
logger.warning(
110+
"Invalid value %s passed to Counter, value must be non-negative. "
111+
"For a Counter that can decrease, use UpDownCounter.",
112+
value,
113+
)
114+
return False
115+
return True
116+
117+
118+
class BoundUpDownCounter(metrics_api.BoundUpDownCounter, BaseBoundInstrument):
119+
def add(self, value: metrics_api.ValueT) -> None:
120+
"""See `opentelemetry.metrics.BoundUpDownCounter.add`."""
121+
if self._validate_update(value):
122+
self.update(value)
123+
105124

106125
class BoundValueRecorder(metrics_api.BoundValueRecorder, BaseBoundInstrument):
107126
def record(self, value: metrics_api.ValueT) -> None:
@@ -184,6 +203,21 @@ def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None:
184203
UPDATE_FUNCTION = add
185204

186205

206+
class UpDownCounter(Metric, metrics_api.UpDownCounter):
207+
"""See `opentelemetry.metrics.UpDownCounter`.
208+
"""
209+
210+
BOUND_INSTR_TYPE = BoundUpDownCounter
211+
212+
def add(self, value: metrics_api.ValueT, labels: Dict[str, str]) -> None:
213+
"""See `opentelemetry.metrics.UpDownCounter.add`."""
214+
bound_intrument = self.bind(labels)
215+
bound_intrument.add(value)
216+
bound_intrument.release()
217+
218+
UPDATE_FUNCTION = add
219+
220+
187221
class ValueRecorder(Metric, metrics_api.ValueRecorder):
188222
"""See `opentelemetry.metrics.ValueRecorder`."""
189223

opentelemetry-sdk/src/opentelemetry/sdk/metrics/export/batcher.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Counter,
2020
InstrumentT,
2121
SumObserver,
22+
UpDownCounter,
2223
UpDownSumObserver,
2324
ValueObserver,
2425
ValueRecorder,
@@ -55,7 +56,7 @@ def aggregator_for(self, instrument_type: Type[InstrumentT]) -> Aggregator:
5556
Aggregators keep track of and updates values when metrics get updated.
5657
"""
5758
# pylint:disable=R0201
58-
if issubclass(instrument_type, Counter):
59+
if issubclass(instrument_type, (Counter, UpDownCounter)):
5960
return CounterAggregator()
6061
if issubclass(instrument_type, (SumObserver, UpDownSumObserver)):
6162
return LastValueAggregator()

opentelemetry-sdk/tests/metrics/export/test_export.py

+9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ def test_aggregator_for_counter(self):
6969
)
7070
)
7171

72+
def test_aggregator_for_updowncounter(self):
73+
batcher = UngroupedBatcher(True)
74+
self.assertTrue(
75+
isinstance(
76+
batcher.aggregator_for(metrics.UpDownCounter),
77+
CounterAggregator,
78+
)
79+
)
80+
7281
# TODO: Add other aggregator tests
7382

7483
def test_checkpoint_set(self):

opentelemetry-sdk/tests/metrics/test_metrics.py

+56
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ def test_create_metric(self):
174174
self.assertEqual(counter.name, "name")
175175
self.assertIs(counter.meter.resource, resource)
176176

177+
def test_create_updowncounter(self):
178+
meter = metrics.MeterProvider().get_meter(__name__)
179+
updowncounter = meter.create_metric(
180+
"name", "desc", "unit", float, metrics.UpDownCounter, ()
181+
)
182+
self.assertIsInstance(updowncounter, metrics.UpDownCounter)
183+
self.assertEqual(updowncounter.value_type, float)
184+
self.assertEqual(updowncounter.name, "name")
185+
177186
def test_create_valuerecorder(self):
178187
meter = metrics.MeterProvider().get_meter(__name__)
179188
valuerecorder = meter.create_metric(
@@ -296,6 +305,53 @@ def test_add(self):
296305
metric.add(2, labels)
297306
self.assertEqual(bound_counter.aggregator.current, 5)
298307

308+
@mock.patch("opentelemetry.sdk.metrics.logger")
309+
def test_add_non_decreasing_int_error(self, logger_mock):
310+
meter = metrics.MeterProvider().get_meter(__name__)
311+
metric = metrics.Counter("name", "desc", "unit", int, meter, ("key",))
312+
labels = {"key": "value"}
313+
bound_counter = metric.bind(labels)
314+
metric.add(3, labels)
315+
metric.add(0, labels)
316+
metric.add(-1, labels)
317+
self.assertEqual(bound_counter.aggregator.current, 3)
318+
self.assertEqual(logger_mock.warning.call_count, 1)
319+
320+
@mock.patch("opentelemetry.sdk.metrics.logger")
321+
def test_add_non_decreasing_float_error(self, logger_mock):
322+
meter = metrics.MeterProvider().get_meter(__name__)
323+
metric = metrics.Counter(
324+
"name", "desc", "unit", float, meter, ("key",)
325+
)
326+
labels = {"key": "value"}
327+
bound_counter = metric.bind(labels)
328+
metric.add(3.3, labels)
329+
metric.add(0.0, labels)
330+
metric.add(0.1, labels)
331+
metric.add(-0.1, labels)
332+
self.assertEqual(bound_counter.aggregator.current, 3.4)
333+
self.assertEqual(logger_mock.warning.call_count, 1)
334+
335+
336+
class TestUpDownCounter(unittest.TestCase):
337+
@mock.patch("opentelemetry.sdk.metrics.logger")
338+
def test_add(self, logger_mock):
339+
meter = metrics.MeterProvider().get_meter(__name__)
340+
metric = metrics.UpDownCounter(
341+
"name", "desc", "unit", int, meter, ("key",)
342+
)
343+
labels = {"key": "value"}
344+
bound_counter = metric.bind(labels)
345+
metric.add(3, labels)
346+
metric.add(2, labels)
347+
self.assertEqual(bound_counter.aggregator.current, 5)
348+
349+
metric.add(0, labels)
350+
metric.add(-3, labels)
351+
metric.add(-1, labels)
352+
self.assertEqual(bound_counter.aggregator.current, 1)
353+
self.assertEqual(logger_mock.warning.call_count, 0)
354+
299355

300356
class TestValueRecorder(unittest.TestCase):
301357
def test_record(self):

0 commit comments

Comments
 (0)