Skip to content

Commit 888bed9

Browse files
sdk: Implement observer instrument (open-telemetry#425)
Observer instruments are used to capture a current set of values at a point in time [1]. This commit extends the Meter interface to allow to register an observer instrument by pasing a callback that will be executed at collection time. The logic inside collection is updated to consider these instruments and a new ObserverAggregator is implemented. [1] https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-metrics.md#observer-instruments
1 parent 344d72b commit 888bed9

File tree

13 files changed

+517
-191
lines changed

13 files changed

+517
-191
lines changed

examples/metrics/observer_example.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""
16+
This example shows how the Observer metric instrument can be used to capture
17+
asynchronous metrics data.
18+
"""
19+
import psutil
20+
21+
from opentelemetry import metrics
22+
from opentelemetry.sdk.metrics import LabelSet, MeterProvider
23+
from opentelemetry.sdk.metrics.export import ConsoleMetricsExporter
24+
from opentelemetry.sdk.metrics.export.batcher import UngroupedBatcher
25+
from opentelemetry.sdk.metrics.export.controller import PushController
26+
27+
# Configure a stateful batcher
28+
batcher = UngroupedBatcher(stateful=True)
29+
30+
metrics.set_preferred_meter_provider_implementation(lambda _: MeterProvider())
31+
meter = metrics.get_meter(__name__)
32+
33+
# Exporter to export metrics to the console
34+
exporter = ConsoleMetricsExporter()
35+
36+
# Configure a push controller
37+
controller = PushController(meter=meter, exporter=exporter, interval=2)
38+
39+
40+
# Callback to gather cpu usage
41+
def get_cpu_usage_callback(observer):
42+
for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)):
43+
label_set = meter.get_label_set({"cpu_number": str(number)})
44+
observer.observe(percent, label_set)
45+
46+
47+
meter.register_observer(
48+
callback=get_cpu_usage_callback,
49+
name="cpu_percent",
50+
description="per-cpu usage",
51+
unit="1",
52+
value_type=float,
53+
label_keys=("cpu_number",),
54+
)
55+
56+
57+
# Callback to gather RAM memory usage
58+
def get_ram_usage_callback(observer):
59+
ram_percent = psutil.virtual_memory().percent
60+
observer.observe(ram_percent, LabelSet())
61+
62+
63+
meter.register_observer(
64+
callback=get_ram_usage_callback,
65+
name="ram_percent",
66+
description="RAM memory usage",
67+
unit="1",
68+
value_type=float,
69+
label_keys=(),
70+
)
71+
72+
input("Press a key to finish...\n")

examples/metrics/record.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,25 @@
3232
exporter = ConsoleMetricsExporter()
3333
# controller collects metrics created from meter and exports it via the
3434
# exporter every interval
35-
controller = PushController(meter, exporter, 5)
35+
controller = PushController(meter=meter, exporter=exporter, interval=5)
3636

3737
# Example to show how to record using the meter
3838
counter = meter.create_metric(
39-
"requests", "number of requests", 1, int, Counter, ("environment",)
39+
name="requests",
40+
description="number of requests",
41+
unit="1",
42+
value_type=int,
43+
metric_type=Counter,
44+
label_keys=("environment",),
4045
)
4146

4247
counter2 = meter.create_metric(
43-
"clicks", "number of clicks", 1, int, Counter, ("environment",)
48+
name="clicks",
49+
description="number of clicks",
50+
unit="1",
51+
value_type=int,
52+
metric_type=Counter,
53+
label_keys=("environment",),
4454
)
4555

4656
# Labelsets are used to identify key-values that are associated with a specific

examples/metrics/simple_example.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,30 @@ def usage(argv):
6262

6363
# Metric instruments allow to capture measurements
6464
requests_counter = meter.create_metric(
65-
"requests", "number of requests", 1, int, Counter, ("environment",)
65+
name="requests",
66+
description="number of requests",
67+
unit="1",
68+
value_type=int,
69+
metric_type=Counter,
70+
label_keys=("environment",),
6671
)
6772

6873
clicks_counter = meter.create_metric(
69-
"clicks", "number of clicks", 1, int, Counter, ("environment",)
74+
name="clicks",
75+
description="number of clicks",
76+
unit="1",
77+
value_type=int,
78+
metric_type=Counter,
79+
label_keys=("environment",),
7080
)
7181

7282
requests_size = meter.create_metric(
73-
"requests_size", "size of requests", 1, int, Measure, ("environment",)
83+
name="requests_size",
84+
description="size of requests",
85+
unit="1",
86+
value_type=int,
87+
metric_type=Measure,
88+
label_keys=("environment",),
7489
)
7590

7691
# Labelsets are used to identify key-values that are associated with a specific
@@ -82,21 +97,15 @@ def usage(argv):
8297
# Update the metric instruments using the direct calling convention
8398
requests_size.record(100, staging_label_set)
8499
requests_counter.add(25, staging_label_set)
85-
# Sleep for 5 seconds, exported value should be 25
86100
time.sleep(5)
87101

88102
requests_size.record(5000, staging_label_set)
89103
requests_counter.add(50, staging_label_set)
90-
# Exported value should be 75
91104
time.sleep(5)
92105

93106
requests_size.record(2, testing_label_set)
94107
requests_counter.add(35, testing_label_set)
95-
# There should be two exported values 75 and 35, one for each labelset
96108
time.sleep(5)
97109

98110
clicks_counter.add(5, staging_label_set)
99-
# There should be three exported values, labelsets can be reused for different
100-
# metrics but will be recorded seperately, 75, 35 and 5
101-
102111
time.sleep(5)

ext/opentelemetry-ext-prometheus/src/opentelemetry/ext/prometheus/__init__.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
REGISTRY,
2525
CollectorRegistry,
2626
CounterMetricFamily,
27-
GaugeMetricFamily,
2827
UnknownMetricFamily,
2928
)
3029

31-
from opentelemetry.metrics import Counter, Gauge, Measure, Metric
30+
from opentelemetry.metrics import Counter, Measure, Metric
3231
from opentelemetry.sdk.metrics.export import (
3332
MetricRecord,
3433
MetricsExporter,
@@ -112,17 +111,6 @@ def _translate_to_prometheus(self, metric_record: MetricRecord):
112111
prometheus_metric.add_metric(
113112
labels=label_values, value=metric_record.aggregator.checkpoint
114113
)
115-
116-
elif isinstance(metric_record.metric, Gauge):
117-
prometheus_metric = GaugeMetricFamily(
118-
name=metric_name,
119-
documentation=metric_record.metric.description,
120-
labels=label_keys,
121-
)
122-
prometheus_metric.add_metric(
123-
labels=label_values, value=metric_record.aggregator.checkpoint
124-
)
125-
126114
# TODO: Add support for histograms when supported in OT
127115
elif isinstance(metric_record.metric, Measure):
128116
prometheus_metric = UnknownMetricFamily(

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

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,6 @@ def add(self, value: ValueT) -> None:
5050
value: The value to add to the handle.
5151
"""
5252

53-
def set(self, value: ValueT) -> None:
54-
"""No-op implementation of `GaugeHandle` set.
55-
56-
Args:
57-
value: The value to set to the handle.
58-
"""
59-
6053
def record(self, value: ValueT) -> None:
6154
"""No-op implementation of `MeasureHandle` record.
6255
@@ -74,15 +67,6 @@ def add(self, value: ValueT) -> None:
7467
"""
7568

7669

77-
class GaugeHandle:
78-
def set(self, value: ValueT) -> None:
79-
"""Sets the current value of the handle to ``value``.
80-
81-
Args:
82-
value: The value to set to the handle.
83-
"""
84-
85-
8670
class MeasureHandle:
8771
def record(self, value: ValueT) -> None:
8872
"""Records the given ``value`` to this handle.
@@ -124,7 +108,7 @@ def get_handle(self, label_set: LabelSet) -> "object":
124108
125109
Handles are useful to reduce the cost of repeatedly recording a metric
126110
with a pre-defined set of label values. All metric kinds (counter,
127-
gauge, measure) support declaring a set of required label keys. The
111+
measure) support declaring a set of required label keys. The
128112
values corresponding to these keys should be specified in every handle.
129113
"Unspecified" label values, in cases where a handle is requested but
130114
a value was not provided are permitted.
@@ -153,14 +137,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None:
153137
label_set: `LabelSet` to associate with the returned handle.
154138
"""
155139

156-
def set(self, value: ValueT, label_set: LabelSet) -> None:
157-
"""No-op implementation of `Gauge` set.
158-
159-
Args:
160-
value: The value to set the gauge metric to.
161-
label_set: `LabelSet` to associate with the returned handle.
162-
"""
163-
164140
def record(self, value: ValueT, label_set: LabelSet) -> None:
165141
"""No-op implementation of `Measure` record.
166142
@@ -186,28 +162,6 @@ def add(self, value: ValueT, label_set: LabelSet) -> None:
186162
"""
187163

188164

189-
class Gauge(Metric):
190-
"""A gauge type metric that expresses a pre-calculated value.
191-
192-
Gauge metrics have a value that is either ``Set`` by explicit
193-
instrumentation or observed through a callback. This kind of metric
194-
should be used when the metric cannot be expressed as a sum or because
195-
the measurement interval is arbitrary.
196-
"""
197-
198-
def get_handle(self, label_set: LabelSet) -> "GaugeHandle":
199-
"""Gets a `GaugeHandle`."""
200-
return GaugeHandle()
201-
202-
def set(self, value: ValueT, label_set: LabelSet) -> None:
203-
"""Sets the value of the gauge to ``value``.
204-
205-
Args:
206-
value: The value to set the gauge metric to.
207-
label_set: `LabelSet` to associate with the returned handle.
208-
"""
209-
210-
211165
class Measure(Metric):
212166
"""A measure type metric that represent raw stats that are recorded.
213167
@@ -227,6 +181,37 @@ def record(self, value: ValueT, label_set: LabelSet) -> None:
227181
"""
228182

229183

184+
class Observer(abc.ABC):
185+
"""An observer type metric instrument used to capture a current set of values.
186+
187+
188+
Observer instruments are asynchronous, a callback is invoked with the
189+
observer instrument as argument allowing the user to capture multiple
190+
values per collection interval.
191+
"""
192+
193+
@abc.abstractmethod
194+
def observe(self, value: ValueT, label_set: LabelSet) -> None:
195+
"""Captures ``value`` to the observer.
196+
197+
Args:
198+
value: The value to capture to this observer metric.
199+
label_set: `LabelSet` associated to ``value``.
200+
"""
201+
202+
203+
class DefaultObserver(Observer):
204+
"""No-op implementation of ``Observer``."""
205+
206+
def observe(self, value: ValueT, label_set: LabelSet) -> None:
207+
"""Captures ``value`` to the observer.
208+
209+
Args:
210+
value: The value to capture to this observer metric.
211+
label_set: `LabelSet` associated to ``value``.
212+
"""
213+
214+
230215
class MeterProvider(abc.ABC):
231216
@abc.abstractmethod
232217
def get_meter(
@@ -277,15 +262,16 @@ def get_meter(
277262
return DefaultMeter()
278263

279264

280-
MetricT = TypeVar("MetricT", Counter, Gauge, Measure)
265+
MetricT = TypeVar("MetricT", Counter, Measure, Observer)
266+
ObserverCallbackT = Callable[[Observer], None]
281267

282268

283269
# pylint: disable=unused-argument
284270
class Meter(abc.ABC):
285271
"""An interface to allow the recording of metrics.
286272
287-
`Metric` s are used for recording pre-defined aggregation (gauge and
288-
counter), or raw values (measure) in which the aggregation and labels
273+
`Metric` s are used for recording pre-defined aggregation (counter),
274+
or raw values (measure) in which the aggregation and labels
289275
for the exported metric are deferred.
290276
"""
291277

@@ -325,14 +311,41 @@ def create_metric(
325311
Args:
326312
name: The name of the metric.
327313
description: Human-readable description of the metric.
328-
unit: Unit of the metric values.
314+
unit: Unit of the metric values following the UCUM convention
315+
(https://unitsofmeasure.org/ucum.html).
329316
value_type: The type of values being recorded by the metric.
330317
metric_type: The type of metric being created.
331318
label_keys: The keys for the labels with dynamic values.
332319
enabled: Whether to report the metric by default.
333320
Returns: A new ``metric_type`` metric with values of ``value_type``.
334321
"""
335322

323+
@abc.abstractmethod
324+
def register_observer(
325+
self,
326+
callback: ObserverCallbackT,
327+
name: str,
328+
description: str,
329+
unit: str,
330+
value_type: Type[ValueT],
331+
label_keys: Sequence[str] = (),
332+
enabled: bool = True,
333+
) -> "Observer":
334+
"""Registers an ``Observer`` metric instrument.
335+
336+
Args:
337+
callback: Callback invoked each collection interval with the
338+
observer as argument.
339+
name: The name of the metric.
340+
description: Human-readable description of the metric.
341+
unit: Unit of the metric values following the UCUM convention
342+
(https://unitsofmeasure.org/ucum.html).
343+
value_type: The type of values being recorded by the metric.
344+
label_keys: The keys for the labels with dynamic values.
345+
enabled: Whether to report the metric by default.
346+
Returns: A new ``Observer`` metric instrument.
347+
"""
348+
336349
@abc.abstractmethod
337350
def get_label_set(self, labels: Dict[str, str]) -> "LabelSet":
338351
"""Gets a `LabelSet` with the given labels.
@@ -367,6 +380,18 @@ def create_metric(
367380
# pylint: disable=no-self-use
368381
return DefaultMetric()
369382

383+
def register_observer(
384+
self,
385+
callback: ObserverCallbackT,
386+
name: str,
387+
description: str,
388+
unit: str,
389+
value_type: Type[ValueT],
390+
label_keys: Sequence[str] = (),
391+
enabled: bool = True,
392+
) -> "Observer":
393+
return DefaultObserver()
394+
370395
def get_label_set(self, labels: Dict[str, str]) -> "LabelSet":
371396
# pylint: disable=no-self-use
372397
return DefaultLabelSet()

0 commit comments

Comments
 (0)