Skip to content

Commit 4ead3f4

Browse files
lzchenc24t
authored andcommitted
Implement LabelSet for metrics (#258)
The primary purpose of LabelSets are to have an optimal way of re-using handles with the same label values. We achieve this by having the keys and values of the labels encoded and stored in each LabelSet instance, so we can have an easy lookup to the corresponding handle for each metric instrument.
1 parent d3bb228 commit 4ead3f4

File tree

7 files changed

+195
-82
lines changed

7 files changed

+195
-82
lines changed

examples/opentelemetry-example-app/src/opentelemetry_example_app/metrics_example.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,17 @@
3030
("environment",),
3131
)
3232

33-
label_values = ("staging",)
33+
label_set = meter.get_label_set({"environment": "staging"})
3434

3535
# Direct metric usage
36-
counter.add(label_values, 25)
36+
counter.add(label_set, 25)
3737

3838
# Handle usage
39-
counter_handle = counter.get_handle(label_values)
39+
counter_handle = counter.get_handle(label_set)
4040
counter_handle.add(100)
4141

4242
# Record batch usage
43-
meter.record_batch(label_values, [(counter, 50)])
43+
meter.record_batch(label_set, [(counter, 50)])
4444
print(counter_handle.data)
4545

4646
# TODO: exporters

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

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
2727
2828
"""
29-
from typing import Callable, Optional, Sequence, Tuple, Type, TypeVar
29+
import abc
30+
from typing import Callable, Dict, Optional, Sequence, Tuple, Type, TypeVar
3031

3132
from opentelemetry.util import loader
3233

@@ -67,14 +68,33 @@ def record(self, value: ValueT) -> None:
6768
"""
6869

6970

71+
class LabelSet(abc.ABC):
72+
"""A canonicalized set of labels useful for preaggregation
73+
74+
Re-usable LabelSet objects provide a potential optimization for scenarios
75+
where handles might not be effective. For example, if the LabelSet will be
76+
re-used but only used once per metrics, handles do not offer any
77+
optimization. It may best to pre-compute a canonicalized LabelSet once and
78+
re-use it with the direct calling convention. LabelSets are immutable and
79+
should be opaque in implementation.
80+
"""
81+
82+
83+
class DefaultLabelSet(LabelSet):
84+
"""The default LabelSet.
85+
86+
Used when no LabelSet implementation is available.
87+
"""
88+
89+
7090
class Metric:
7191
"""Base class for various types of metrics.
7292
7393
Metric class that inherit from this class are specialized with the type of
7494
handle that the metric holds.
7595
"""
7696

77-
def get_handle(self, label_values: Sequence[str]) -> "object":
97+
def get_handle(self, label_set: LabelSet) -> "object":
7898
"""Gets a handle, used for repeated-use of metrics instruments.
7999
80100
Handles are useful to reduce the cost of repeatedly recording a metric
@@ -85,34 +105,34 @@ def get_handle(self, label_values: Sequence[str]) -> "object":
85105
a value was not provided are permitted.
86106
87107
Args:
88-
label_values: Values to associate with the returned handle.
108+
label_set: `LabelSet` to associate with the returned handle.
89109
"""
90110

91111

92112
class DefaultMetric(Metric):
93113
"""The default Metric used when no Metric implementation is available."""
94114

95-
def get_handle(self, label_values: Sequence[str]) -> "DefaultMetricHandle":
115+
def get_handle(self, label_set: LabelSet) -> "DefaultMetricHandle":
96116
"""Gets a `DefaultMetricHandle`.
97117
98118
Args:
99-
label_values: The label values associated with the handle.
119+
label_set: `LabelSet` to associate with the returned handle.
100120
"""
101121
return DefaultMetricHandle()
102122

103123

104124
class Counter(Metric):
105125
"""A counter type metric that expresses the computation of a sum."""
106126

107-
def get_handle(self, label_values: Sequence[str]) -> "CounterHandle":
127+
def get_handle(self, label_set: LabelSet) -> "CounterHandle":
108128
"""Gets a `CounterHandle`."""
109129
return CounterHandle()
110130

111-
def add(self, label_values: Sequence[str], value: ValueT) -> None:
131+
def add(self, label_set: LabelSet, value: ValueT) -> None:
112132
"""Increases the value of the counter by ``value``.
113133
114134
Args:
115-
label_values: The label values associated with the metric.
135+
label_set: `LabelSet` to associate with the returned handle.
116136
value: The value to add to the counter metric.
117137
"""
118138

@@ -126,15 +146,15 @@ class Gauge(Metric):
126146
the measurement interval is arbitrary.
127147
"""
128148

129-
def get_handle(self, label_values: Sequence[str]) -> "GaugeHandle":
149+
def get_handle(self, label_set: LabelSet) -> "GaugeHandle":
130150
"""Gets a `GaugeHandle`."""
131151
return GaugeHandle()
132152

133-
def set(self, label_values: Sequence[str], value: ValueT) -> None:
153+
def set(self, label_set: LabelSet, value: ValueT) -> None:
134154
"""Sets the value of the gauge to ``value``.
135155
136156
Args:
137-
label_values: The label values associated with the metric.
157+
label_set: `LabelSet` to associate with the returned handle.
138158
value: The value to set the gauge metric to.
139159
"""
140160

@@ -147,15 +167,15 @@ class Measure(Metric):
147167
Negative inputs will be discarded when monotonic is True.
148168
"""
149169

150-
def get_handle(self, label_values: Sequence[str]) -> "MeasureHandle":
170+
def get_handle(self, label_set: LabelSet) -> "MeasureHandle":
151171
"""Gets a `MeasureHandle` with a float value."""
152172
return MeasureHandle()
153173

154-
def record(self, label_values: Sequence[str], value: ValueT) -> None:
174+
def record(self, label_set: LabelSet, value: ValueT) -> None:
155175
"""Records the ``value`` to the measure.
156176
157177
Args:
158-
label_values: The label values associated with the metric.
178+
label_set: `LabelSet` to associate with the returned handle.
159179
value: The value to record to this measure metric.
160180
"""
161181

@@ -174,7 +194,7 @@ class Meter:
174194

175195
def record_batch(
176196
self,
177-
label_values: Sequence[str],
197+
label_set: LabelSet,
178198
record_tuples: Sequence[Tuple["Metric", ValueT]],
179199
) -> None:
180200
"""Atomically records a batch of `Metric` and value pairs.
@@ -184,7 +204,7 @@ def record_batch(
184204
match the key-value pairs in the label tuples.
185205
186206
Args:
187-
label_values: The label values associated with all measurements in
207+
label_set: The `LabelSet` associated with all measurements in
188208
the batch. A measurement is a tuple, representing the `Metric`
189209
being recorded and the corresponding value to record.
190210
record_tuples: A sequence of pairs of `Metric` s and the
@@ -211,8 +231,6 @@ def create_metric(
211231
value_type: The type of values being recorded by the metric.
212232
metric_type: The type of metric being created.
213233
label_keys: The keys for the labels with dynamic values.
214-
Order of the sequence is important as the same order must be
215-
used on recording when suppling values for these labels.
216234
enabled: Whether to report the metric by default.
217235
monotonic: Whether to only allow non-negative values.
218236
@@ -221,6 +239,17 @@ def create_metric(
221239
# pylint: disable=no-self-use
222240
return DefaultMetric()
223241

242+
def get_label_set(self, labels: Dict[str, str]) -> "LabelSet":
243+
"""Gets a `LabelSet` with the given labels.
244+
245+
Args:
246+
labels: A dictionary representing label key to label value pairs.
247+
248+
Returns: A `LabelSet` object canonicalized using the given input.
249+
"""
250+
# pylint: disable=no-self-use
251+
return DefaultLabelSet()
252+
224253

225254
# Once https://github.com/python/mypy/issues/7092 is resolved,
226255
# the following type definition should be replaced with

opentelemetry-api/tests/metrics/test_metrics.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,57 @@ def setUp(self):
2424

2525
def test_record_batch(self):
2626
counter = metrics.Counter()
27-
self.meter.record_batch(("values"), ((counter, 1),))
27+
label_set = metrics.LabelSet()
28+
self.meter.record_batch(label_set, ((counter, 1),))
2829

2930
def test_create_metric(self):
3031
metric = self.meter.create_metric("", "", "", float, metrics.Counter)
3132
self.assertIsInstance(metric, metrics.DefaultMetric)
3233

34+
def test_get_label_set(self):
35+
metric = self.meter.get_label_set({})
36+
self.assertIsInstance(metric, metrics.DefaultLabelSet)
37+
3338

3439
class TestMetrics(unittest.TestCase):
3540
def test_default(self):
3641
default = metrics.DefaultMetric()
37-
handle = default.get_handle(("test", "test1"))
42+
default_ls = metrics.DefaultLabelSet()
43+
handle = default.get_handle(default_ls)
3844
self.assertIsInstance(handle, metrics.DefaultMetricHandle)
3945

4046
def test_counter(self):
4147
counter = metrics.Counter()
42-
handle = counter.get_handle(("test", "test1"))
48+
label_set = metrics.LabelSet()
49+
handle = counter.get_handle(label_set)
4350
self.assertIsInstance(handle, metrics.CounterHandle)
4451

4552
def test_counter_add(self):
4653
counter = metrics.Counter()
47-
counter.add(("value",), 1)
54+
label_set = metrics.LabelSet()
55+
counter.add(label_set, 1)
4856

4957
def test_gauge(self):
5058
gauge = metrics.Gauge()
51-
handle = gauge.get_handle(("test", "test1"))
59+
label_set = metrics.LabelSet()
60+
handle = gauge.get_handle(label_set)
5261
self.assertIsInstance(handle, metrics.GaugeHandle)
5362

5463
def test_gauge_set(self):
5564
gauge = metrics.Gauge()
56-
gauge.set(("value",), 1)
65+
label_set = metrics.LabelSet()
66+
gauge.set(label_set, 1)
5767

5868
def test_measure(self):
5969
measure = metrics.Measure()
60-
handle = measure.get_handle(("test", "test1"))
70+
label_set = metrics.LabelSet()
71+
handle = measure.get_handle(label_set)
6172
self.assertIsInstance(handle, metrics.MeasureHandle)
6273

6374
def test_measure_record(self):
6475
measure = metrics.Measure()
65-
measure.record(("value",), 1)
76+
label_set = metrics.LabelSet()
77+
measure.record(label_set, 1)
6678

6779
def test_default_handle(self):
6880
metrics.DefaultMetricHandle()

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

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,23 @@
1313
# limitations under the License.
1414

1515
import logging
16-
from typing import Sequence, Tuple, Type
16+
from collections import OrderedDict
17+
from typing import Dict, Sequence, Tuple, Type
1718

1819
from opentelemetry import metrics as metrics_api
1920
from opentelemetry.util import time_ns
2021

2122
logger = logging.getLogger(__name__)
2223

2324

25+
# pylint: disable=redefined-outer-name
26+
class LabelSet(metrics_api.LabelSet):
27+
"""See `opentelemetry.metrics.LabelSet."""
28+
29+
def __init__(self, labels: Dict[str, str] = None):
30+
self.labels = labels
31+
32+
2433
class BaseHandle:
2534
def __init__(
2635
self,
@@ -107,14 +116,14 @@ def __init__(
107116
self.monotonic = monotonic
108117
self.handles = {}
109118

110-
def get_handle(self, label_values: Sequence[str]) -> BaseHandle:
119+
def get_handle(self, label_set: LabelSet) -> BaseHandle:
111120
"""See `opentelemetry.metrics.Metric.get_handle`."""
112-
handle = self.handles.get(label_values)
121+
handle = self.handles.get(label_set)
113122
if not handle:
114123
handle = self.HANDLE_TYPE(
115124
self.value_type, self.enabled, self.monotonic
116125
)
117-
self.handles[label_values] = handle
126+
self.handles[label_set] = handle
118127
return handle
119128

120129
def __repr__(self):
@@ -155,11 +164,9 @@ def __init__(
155164
monotonic=monotonic,
156165
)
157166

158-
def add(
159-
self, label_values: Sequence[str], value: metrics_api.ValueT
160-
) -> None:
167+
def add(self, label_set: LabelSet, value: metrics_api.ValueT) -> None:
161168
"""See `opentelemetry.metrics.Counter.add`."""
162-
self.get_handle(label_values).add(value)
169+
self.get_handle(label_set).add(value)
163170

164171
UPDATE_FUNCTION = add
165172

@@ -193,11 +200,9 @@ def __init__(
193200
monotonic=monotonic,
194201
)
195202

196-
def set(
197-
self, label_values: Sequence[str], value: metrics_api.ValueT
198-
) -> None:
203+
def set(self, label_set: LabelSet, value: metrics_api.ValueT) -> None:
199204
"""See `opentelemetry.metrics.Gauge.set`."""
200-
self.get_handle(label_values).set(value)
205+
self.get_handle(label_set).set(value)
201206

202207
UPDATE_FUNCTION = set
203208

@@ -231,26 +236,31 @@ def __init__(
231236
monotonic=monotonic,
232237
)
233238

234-
def record(
235-
self, label_values: Sequence[str], value: metrics_api.ValueT
236-
) -> None:
239+
def record(self, label_set: LabelSet, value: metrics_api.ValueT) -> None:
237240
"""See `opentelemetry.metrics.Measure.record`."""
238-
self.get_handle(label_values).record(value)
241+
self.get_handle(label_set).record(value)
239242

240243
UPDATE_FUNCTION = record
241244

242245

246+
# Used when getting a LabelSet with no key/values
247+
EMPTY_LABEL_SET = LabelSet()
248+
249+
243250
class Meter(metrics_api.Meter):
244251
"""See `opentelemetry.metrics.Meter`."""
245252

253+
def __init__(self):
254+
self.labels = {}
255+
246256
def record_batch(
247257
self,
248-
label_values: Sequence[str],
258+
label_set: LabelSet,
249259
record_tuples: Sequence[Tuple[metrics_api.Metric, metrics_api.ValueT]],
250260
) -> None:
251261
"""See `opentelemetry.metrics.Meter.record_batch`."""
252262
for metric, value in record_tuples:
253-
metric.UPDATE_FUNCTION(label_values, value)
263+
metric.UPDATE_FUNCTION(label_set, value)
254264

255265
def create_metric(
256266
self,
@@ -275,5 +285,22 @@ def create_metric(
275285
monotonic=monotonic,
276286
)
277287

288+
def get_label_set(self, labels: Dict[str, str]):
289+
"""See `opentelemetry.metrics.Meter.create_metric`.
290+
291+
This implementation encodes the labels to use as a map key.
292+
293+
Args:
294+
labels: The dictionary of label keys to label values.
295+
"""
296+
if len(labels) == 0:
297+
return EMPTY_LABEL_SET
298+
# Use simple encoding for now until encoding API is implemented
299+
encoded = tuple(sorted(labels.items()))
300+
# If LabelSet exists for this meter in memory, use existing one
301+
if encoded not in self.labels:
302+
self.labels[encoded] = LabelSet(labels=labels)
303+
return self.labels[encoded]
304+
278305

279306
meter = Meter()

0 commit comments

Comments
 (0)