Skip to content

Commit 4e551ba

Browse files
metrics: Implement release for handles and observers (#435)
This commit implements a solution for releasing instrument handles and observers. For the handles it is based on a ref count that is increased each time the handled is acquired, when the ref count reaches 0 the handle is removed on collection time. The direct call convention is updated to release the handle after it has been updated. The observer instrument is only updated on collection time, so it can be removed as soon as the user request to do so.
1 parent 6bfc48b commit 4e551ba

File tree

6 files changed

+170
-26
lines changed

6 files changed

+170
-26
lines changed

Diff for: docs/examples/metrics/record.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@
6767
# Therefore, getting a bound metric instrument using the same set of labels
6868
# will yield the same bound metric instrument.
6969
bound_counter = counter.bind(label_set)
70-
bound_counter.add(100)
70+
for i in range(1000):
71+
bound_counter.add(i)
72+
73+
# You can release the bound instrument we you are done
74+
bound_counter.release()
7175

7276
# Direct metric usage
7377
# You can record metrics directly using the metric instrument. You pass in a
@@ -79,4 +83,5 @@
7983
# (metric, value) pairs. The value would be recorded for each metric using the
8084
# specified labelset for each.
8185
meter.record_batch(label_set, [(counter, 50), (counter2, 70)])
82-
time.sleep(100)
86+
87+
time.sleep(10)

Diff for: opentelemetry-api/src/opentelemetry/metrics/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def record(self, value: ValueT) -> None:
6060
value: The value to record to the bound metric instrument.
6161
"""
6262

63+
def release(self) -> None:
64+
"""No-op implementation of release."""
65+
6366

6467
class BoundCounter:
6568
def add(self, value: ValueT) -> None:
@@ -350,6 +353,14 @@ def register_observer(
350353
Returns: A new ``Observer`` metric instrument.
351354
"""
352355

356+
@abc.abstractmethod
357+
def unregister_observer(self, observer: "Observer") -> None:
358+
"""Unregisters an ``Observer`` metric instrument.
359+
360+
Args:
361+
observer: The observer to unregister.
362+
"""
363+
353364
@abc.abstractmethod
354365
def get_label_set(self, labels: Dict[str, str]) -> "LabelSet":
355366
"""Gets a `LabelSet` with the given labels.
@@ -396,6 +407,9 @@ def register_observer(
396407
) -> "Observer":
397408
return DefaultObserver()
398409

410+
def unregister_observer(self, observer: "Observer") -> None:
411+
pass
412+
399413
def get_label_set(self, labels: Dict[str, str]) -> "LabelSet":
400414
# pylint: disable=no-self-use
401415
return DefaultLabelSet()

Diff for: opentelemetry-api/tests/metrics/test_metrics.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ def test_measure_record(self):
5050
measure.record(1, label_set)
5151

5252
def test_default_bound_metric(self):
53-
metrics.DefaultBoundInstrument()
53+
bound_instrument = metrics.DefaultBoundInstrument()
54+
bound_instrument.release()
5455

5556
def test_bound_counter(self):
5657
bound_counter = metrics.BoundCounter()
@@ -59,3 +60,8 @@ def test_bound_counter(self):
5960
def test_bound_measure(self):
6061
bound_measure = metrics.BoundMeasure()
6162
bound_measure.record(1)
63+
64+
def test_observer(self):
65+
observer = metrics.DefaultObserver()
66+
label_set = metrics.LabelSet()
67+
observer.observe(1, label_set)

Diff for: opentelemetry-api/tests/test_implementation.py

+5
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ def test_register_observer(self):
8787
observer = meter.register_observer(callback, "", "", "", int, (), True)
8888
self.assertIsInstance(observer, metrics.DefaultObserver)
8989

90+
def test_unregister_observer(self):
91+
meter = metrics.DefaultMeter()
92+
observer = metrics.DefaultObserver()
93+
meter.unregister_observer(observer)
94+
9095
def test_get_label_set(self):
9196
meter = metrics.DefaultMeter()
9297
label_set = meter.get_label_set({})

Diff for: opentelemetry-sdk/src/opentelemetry/sdk/metrics/__init__.py

+66-22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import logging
16+
import threading
1617
from typing import Dict, Sequence, Tuple, Type
1718

1819
from opentelemetry import metrics as metrics_api
@@ -71,6 +72,8 @@ def __init__(
7172
self.enabled = enabled
7273
self.aggregator = aggregator
7374
self.last_update_timestamp = time_ns()
75+
self._ref_count = 0
76+
self._ref_count_lock = threading.Lock()
7477

7578
def _validate_update(self, value: metrics_api.ValueT) -> bool:
7679
if not self.enabled:
@@ -86,6 +89,21 @@ def update(self, value: metrics_api.ValueT):
8689
self.last_update_timestamp = time_ns()
8790
self.aggregator.update(value)
8891

92+
def release(self):
93+
self.decrease_ref_count()
94+
95+
def decrease_ref_count(self):
96+
with self._ref_count_lock:
97+
self._ref_count -= 1
98+
99+
def increase_ref_count(self):
100+
with self._ref_count_lock:
101+
self._ref_count += 1
102+
103+
def ref_count(self):
104+
with self._ref_count_lock:
105+
return self._ref_count
106+
89107
def __repr__(self):
90108
return '{}(data="{}", last_update_timestamp={})'.format(
91109
type(self).__name__,
@@ -137,18 +155,21 @@ def __init__(
137155
self.label_keys = label_keys
138156
self.enabled = enabled
139157
self.bound_instruments = {}
158+
self.bound_instruments_lock = threading.Lock()
140159

141160
def bind(self, label_set: LabelSet) -> BaseBoundInstrument:
142161
"""See `opentelemetry.metrics.Metric.bind`."""
143-
bound_instrument = self.bound_instruments.get(label_set)
144-
if not bound_instrument:
145-
bound_instrument = self.BOUND_INSTR_TYPE(
146-
self.value_type,
147-
self.enabled,
148-
# Aggregator will be created based off type of metric
149-
self.meter.batcher.aggregator_for(self.__class__),
150-
)
151-
self.bound_instruments[label_set] = bound_instrument
162+
with self.bound_instruments_lock:
163+
bound_instrument = self.bound_instruments.get(label_set)
164+
if bound_instrument is None:
165+
bound_instrument = self.BOUND_INSTR_TYPE(
166+
self.value_type,
167+
self.enabled,
168+
# Aggregator will be created based off type of metric
169+
self.meter.batcher.aggregator_for(self.__class__),
170+
)
171+
self.bound_instruments[label_set] = bound_instrument
172+
bound_instrument.increase_ref_count()
152173
return bound_instrument
153174

154175
def __repr__(self):
@@ -167,7 +188,9 @@ class Counter(Metric, metrics_api.Counter):
167188

168189
def add(self, value: metrics_api.ValueT, label_set: LabelSet) -> None:
169190
"""See `opentelemetry.metrics.Counter.add`."""
170-
self.bind(label_set).add(value)
191+
bound_intrument = self.bind(label_set)
192+
bound_intrument.add(value)
193+
bound_intrument.release()
171194

172195
UPDATE_FUNCTION = add
173196

@@ -179,7 +202,9 @@ class Measure(Metric, metrics_api.Measure):
179202

180203
def record(self, value: metrics_api.ValueT, label_set: LabelSet) -> None:
181204
"""See `opentelemetry.metrics.Measure.record`."""
182-
self.bind(label_set).record(value)
205+
bound_intrument = self.bind(label_set)
206+
bound_intrument.record(value)
207+
bound_intrument.release()
183208

184209
UPDATE_FUNCTION = record
185210

@@ -279,6 +304,7 @@ def __init__(
279304
self.metrics = set()
280305
self.observers = set()
281306
self.batcher = UngroupedBatcher(stateful)
307+
self.observers_lock = threading.Lock()
282308
self.resource = resource
283309

284310
def collect(self) -> None:
@@ -294,26 +320,39 @@ def collect(self) -> None:
294320

295321
def _collect_metrics(self) -> None:
296322
for metric in self.metrics:
297-
if metric.enabled:
323+
if not metric.enabled:
324+
continue
325+
326+
to_remove = []
327+
328+
with metric.bound_instruments_lock:
298329
for label_set, bound_instr in metric.bound_instruments.items():
299330
# TODO: Consider storing records in memory?
300331
record = Record(metric, label_set, bound_instr.aggregator)
301332
# Checkpoints the current aggregators
302333
# Applies different batching logic based on type of batcher
303334
self.batcher.process(record)
304335

336+
if bound_instr.ref_count() == 0:
337+
to_remove.append(label_set)
338+
339+
# Remove handles that were released
340+
for label_set in to_remove:
341+
del metric.bound_instruments[label_set]
342+
305343
def _collect_observers(self) -> None:
306-
for observer in self.observers:
307-
if not observer.enabled:
308-
continue
344+
with self.observers_lock:
345+
for observer in self.observers:
346+
if not observer.enabled:
347+
continue
309348

310-
# TODO: capture timestamp?
311-
if not observer.run():
312-
continue
349+
# TODO: capture timestamp?
350+
if not observer.run():
351+
continue
313352

314-
for label_set, aggregator in observer.aggregators.items():
315-
record = Record(observer, label_set, aggregator)
316-
self.batcher.process(record)
353+
for label_set, aggregator in observer.aggregators.items():
354+
record = Record(observer, label_set, aggregator)
355+
self.batcher.process(record)
317356

318357
def record_batch(
319358
self,
@@ -368,9 +407,14 @@ def register_observer(
368407
label_keys,
369408
enabled,
370409
)
371-
self.observers.add(ob)
410+
with self.observers_lock:
411+
self.observers.add(ob)
372412
return ob
373413

414+
def unregister_observer(self, observer: "Observer") -> None:
415+
with self.observers_lock:
416+
self.observers.remove(observer)
417+
374418
def get_label_set(self, labels: Dict[str, str]):
375419
"""See `opentelemetry.metrics.Meter.create_metric`.
376420

Diff for: opentelemetry-sdk/tests/metrics/test_metrics.py

+71-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def test_collect(self):
4949
)
5050
kvp = {"key1": "value1"}
5151
label_set = meter.get_label_set(kvp)
52-
counter.add(label_set, 1.0)
52+
counter.add(1.0, label_set)
5353
meter.metrics.add(counter)
5454
meter.collect()
5555
self.assertTrue(batcher_mock.process.called)
@@ -179,6 +179,18 @@ def test_register_observer(self):
179179
self.assertEqual(observer.label_keys, ())
180180
self.assertTrue(observer.enabled)
181181

182+
def test_unregister_observer(self):
183+
meter = metrics.MeterProvider().get_meter(__name__)
184+
185+
callback = mock.Mock()
186+
187+
observer = meter.register_observer(
188+
callback, "name", "desc", "unit", int, (), True
189+
)
190+
191+
meter.unregister_observer(observer)
192+
self.assertEqual(len(meter.observers), 0)
193+
182194
def test_get_label_set(self):
183195
meter = metrics.MeterProvider().get_meter(__name__)
184196
kvp = {"environment": "staging", "a": "z"}
@@ -193,6 +205,64 @@ def test_get_label_set_empty(self):
193205
label_set = meter.get_label_set(kvp)
194206
self.assertEqual(label_set, metrics.EMPTY_LABEL_SET)
195207

208+
def test_direct_call_release_bound_instrument(self):
209+
meter = metrics.MeterProvider().get_meter(__name__)
210+
label_keys = ("key1",)
211+
kvp = {"key1": "value1"}
212+
label_set = meter.get_label_set(kvp)
213+
214+
counter = metrics.Counter(
215+
"name", "desc", "unit", float, meter, label_keys
216+
)
217+
meter.metrics.add(counter)
218+
counter.add(4.0, label_set)
219+
220+
measure = metrics.Measure(
221+
"name", "desc", "unit", float, meter, label_keys
222+
)
223+
meter.metrics.add(measure)
224+
measure.record(42.0, label_set)
225+
226+
self.assertEqual(len(counter.bound_instruments), 1)
227+
self.assertEqual(len(measure.bound_instruments), 1)
228+
229+
meter.collect()
230+
231+
self.assertEqual(len(counter.bound_instruments), 0)
232+
self.assertEqual(len(measure.bound_instruments), 0)
233+
234+
def test_release_bound_instrument(self):
235+
meter = metrics.MeterProvider().get_meter(__name__)
236+
label_keys = ("key1",)
237+
kvp = {"key1": "value1"}
238+
label_set = meter.get_label_set(kvp)
239+
240+
counter = metrics.Counter(
241+
"name", "desc", "unit", float, meter, label_keys
242+
)
243+
meter.metrics.add(counter)
244+
bound_counter = counter.bind(label_set)
245+
bound_counter.add(4.0)
246+
247+
measure = metrics.Measure(
248+
"name", "desc", "unit", float, meter, label_keys
249+
)
250+
meter.metrics.add(measure)
251+
bound_measure = measure.bind(label_set)
252+
bound_measure.record(42)
253+
254+
bound_counter.release()
255+
bound_measure.release()
256+
257+
# be sure that bound instruments are only released after collection
258+
self.assertEqual(len(counter.bound_instruments), 1)
259+
self.assertEqual(len(measure.bound_instruments), 1)
260+
261+
meter.collect()
262+
263+
self.assertEqual(len(counter.bound_instruments), 0)
264+
self.assertEqual(len(measure.bound_instruments), 0)
265+
196266

197267
class TestMetric(unittest.TestCase):
198268
def test_bind(self):

0 commit comments

Comments
 (0)