Skip to content

Commit f669aa9

Browse files
authored
Metrics: add MetricsProducer and convert stats (census-instrumentation#476)
Add MetricProducer and MetricProducerManager classes and make Stats implement MetricProducer. Also fix a bug in DistributionAggregationData histogram point conversions.
1 parent 520a64b commit f669aa9

9 files changed

Lines changed: 274 additions & 30 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright 2019, OpenCensus 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+
import threading
16+
17+
18+
class MetricProducer(object):
19+
"""Produces a set of metrics for export."""
20+
21+
def get_metrics(self):
22+
"""Get a set of metrics to be exported.
23+
24+
:rtype: set(:class: `opencensus.metrics.export.metric.Metric`)
25+
:return: A set of metrics to be exported.
26+
"""
27+
raise NotImplementedError # pragma: NO COVER
28+
29+
30+
class MetricProducerManager(object):
31+
"""Container class for MetricProducers to be used by exporters.
32+
33+
:type metric_producers: iterable(class: 'MetricProducer')
34+
:param metric_producers: Optional initial metric producers.
35+
"""
36+
37+
def __init__(self, metric_producers=None):
38+
if metric_producers is None:
39+
self.metric_producers = set()
40+
else:
41+
self.metric_producers = set(metric_producers)
42+
self.mp_lock = threading.Lock()
43+
44+
def add(self, metric_producer):
45+
"""Add a metric producer.
46+
47+
:type metric_producer: :class: 'MetricProducer'
48+
:param metric_producer: The metric producer to add.
49+
"""
50+
if metric_producer is None:
51+
raise ValueError
52+
with self.mp_lock:
53+
self.metric_producers.add(metric_producer)
54+
55+
def remove(self, metric_producer):
56+
"""Remove a metric producer.
57+
58+
:type metric_producer: :class: 'MetricProducer'
59+
:param metric_producer: The metric producer to remove.
60+
"""
61+
if metric_producer is None:
62+
raise ValueError
63+
try:
64+
with self.mp_lock:
65+
self.metric_producers.remove(metric_producer)
66+
except KeyError:
67+
pass
68+
69+
def get_all(self):
70+
"""Get the set of all metric producers.
71+
72+
Get a copy of `metric_producers`. Prefer this method to using the
73+
attribute directly to avoid other threads adding/removing producers
74+
while you're reading it.
75+
76+
:rtype: set(:class: `MetricProducer`)
77+
:return: A set of all metric producers at the time of the call.
78+
"""
79+
with self.mp_lock:
80+
mps_copy = set(self.metric_producers)
81+
return mps_copy

opencensus/metrics/export/value.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,9 @@ def __init__(self,
249249
raise ValueError("bucket_options must not be null")
250250
if bucket_options.type_ is None:
251251
if buckets is not None:
252-
raise ValueError("buckets must be null if the distribution has"
253-
"no histogram (i.e. bucket_options.type is "
254-
"null)")
252+
raise ValueError("buckets must be null if the distribution "
253+
"has no histogram (i.e. bucket_options.type "
254+
"is null)")
255255
else:
256256
if len(buckets) != len(bucket_options.type_.bounds) + 1:
257257
# Note that this includes the implicit 0 and positive-infinity

opencensus/stats/aggregation_data.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ def to_point(self, timestamp):
288288
This method creates a :class: `opencensus.metrics.export.point.Point`
289289
with a :class: `opencensus.metrics.export.value.ValueDistribution`
290290
value, and creates buckets and exemplars for that distribution from the
291-
appropriate classes in the `metrics` package.
291+
appropriate classes in the `metrics` package. If the distribution
292+
doesn't have a histogram (i.e. `bounds` is empty) the converted point's
293+
`buckets` attribute will be null.
292294
293295
:type timestamp: :class: `datetime.datetime`
294296
:param timestamp: The time to report the point as having been recorded.
@@ -297,17 +299,22 @@ def to_point(self, timestamp):
297299
:return: a :class: `opencensus.metrics.export.value.ValueDistribution`
298300
-valued Point.
299301
"""
300-
buckets = [None] * len(self.counts_per_bucket)
301-
for ii, count in enumerate(self.counts_per_bucket):
302-
stat_ex = self.exemplars.get(ii, None)
303-
if stat_ex is not None:
304-
metric_ex = value.Exemplar(stat_ex.value, stat_ex.timestamp,
305-
copy.copy(stat_ex.attachments))
306-
buckets[ii] = value.Bucket(count, metric_ex)
307-
else:
308-
buckets[ii] = value.Bucket(count)
302+
if self.bounds:
303+
bucket_options = value.BucketOptions(value.Explicit(self.bounds))
304+
buckets = [None] * len(self.counts_per_bucket)
305+
for ii, count in enumerate(self.counts_per_bucket):
306+
stat_ex = self.exemplars.get(ii) if self.exemplars else None
307+
if stat_ex is not None:
308+
metric_ex = value.Exemplar(stat_ex.value,
309+
stat_ex.timestamp,
310+
copy.copy(stat_ex.attachments))
311+
buckets[ii] = value.Bucket(count, metric_ex)
312+
else:
313+
buckets[ii] = value.Bucket(count)
309314

310-
bucket_options = value.BucketOptions(value.Explicit(self.bounds))
315+
else:
316+
bucket_options = value.BucketOptions()
317+
buckets = None
311318
return point.Point(
312319
value.ValueDistribution(
313320
count=self.count_data,

opencensus/stats/measure_to_view_map.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import copy
1717
import logging
1818

19+
from opencensus.stats import metric_utils
1920
from opencensus.stats import view_data as view_data_module
2021

2122

@@ -126,3 +127,21 @@ def export(self, view_datas):
126127
if len(self.exporters) > 0:
127128
for e in self.exporters:
128129
e.export(view_datas)
130+
131+
def get_metrics(self, timestamp):
132+
"""Get a Metric for each registered view.
133+
134+
Convert each registered view's associated `ViewData` into a `Metric` to
135+
be exported.
136+
137+
:type timestamp: :class: `datetime.datetime`
138+
:param timestamp: The timestamp to use for metric conversions, usually
139+
the current time.
140+
141+
:rtype: Iterator[:class: `opencensus.metrics.export.metric.Metric`]
142+
"""
143+
for vdl in self._measure_to_view_data_list_map.values():
144+
for vd in vdl:
145+
metric = metric_utils.view_data_to_metric(vd, timestamp)
146+
if metric is not None:
147+
yield metric

opencensus/stats/metric_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ def view_data_to_metric(view_data, timestamp):
113113
:rtype: :class: `opencensus.metrics.export.metric.Metric`
114114
:return: A converted Metric.
115115
"""
116+
if not view_data.tag_value_aggregation_data_map:
117+
return None
118+
116119
md = view_data.view.get_metric_descriptor()
117120

118121
# TODO: implement gauges

opencensus/stats/stats.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,29 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from datetime import datetime
16+
17+
from opencensus.metrics.export.metric_producer import MetricProducer
1518
from opencensus.stats.stats_recorder import StatsRecorder
1619
from opencensus.stats.view_manager import ViewManager
1720

1821

19-
class Stats(object):
22+
class Stats(MetricProducer):
2023
"""Stats defines a View Manager and a Stats Recorder in order for the
2124
collection of Stats
2225
"""
26+
2327
def __init__(self):
24-
self._stats_recorder = StatsRecorder()
25-
self._view_manager = ViewManager()
26-
27-
@property
28-
def stats_recorder(self):
29-
"""the current stats recorder for Stats"""
30-
return self._stats_recorder
31-
32-
@property
33-
def view_manager(self):
34-
"""the current view manager for Stats"""
35-
return self._view_manager
28+
self.stats_recorder = StatsRecorder()
29+
self.view_manager = ViewManager()
30+
31+
def get_metrics(self):
32+
"""Get a Metric for each of the view manager's registered views.
33+
34+
Convert each registered view's associated `ViewData` into a `Metric` to
35+
be exported, using the current time for metric conversions.
36+
37+
:rtype: Iterator[:class: `opencensus.metrics.export.metric.Metric`]
38+
"""
39+
return self.view_manager.measure_to_view_map.get_metrics(
40+
datetime.now())
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2019, OpenCensus 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+
try:
16+
from mock import Mock
17+
except ImportError:
18+
from unittest.mock import Mock
19+
20+
import unittest
21+
22+
from opencensus.metrics.export import metric_producer
23+
24+
25+
class TestMetricProducerManager(unittest.TestCase):
26+
def test_init(self):
27+
mpm1 = metric_producer.MetricProducerManager()
28+
self.assertEqual(mpm1.metric_producers, set())
29+
30+
mock_mp = Mock()
31+
mpm2 = metric_producer.MetricProducerManager([mock_mp])
32+
self.assertEqual(mpm2.metric_producers, set([mock_mp]))
33+
34+
def test_add_remove(self):
35+
mpm = metric_producer.MetricProducerManager()
36+
self.assertEqual(mpm.metric_producers, set())
37+
38+
with self.assertRaises(ValueError):
39+
mpm.add(None)
40+
mock_mp = Mock()
41+
mpm.add(mock_mp)
42+
self.assertEqual(mpm.metric_producers, set([mock_mp]))
43+
mpm.add(mock_mp)
44+
self.assertEqual(mpm.metric_producers, set([mock_mp]))
45+
46+
with self.assertRaises(ValueError):
47+
mpm.remove(None)
48+
another_mock_mp = Mock()
49+
mpm.remove(another_mock_mp)
50+
self.assertEqual(mpm.metric_producers, set([mock_mp]))
51+
mpm.remove(mock_mp)
52+
self.assertEqual(mpm.metric_producers, set())
53+
54+
def test_get_all(self):
55+
mp1 = Mock()
56+
mp2 = Mock()
57+
mpm = metric_producer.MetricProducerManager([mp1, mp2])
58+
got = mpm.get_all()
59+
mpm.remove(mp1)
60+
self.assertIn(mp1, got)
61+
self.assertIn(mp2, got)

tests/unit/stats/test_aggregation_data.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,3 +555,22 @@ def test_to_point(self):
555555
exemplars_equal(
556556
ex_99,
557557
converted_point.value.buckets[2].exemplar))
558+
559+
def test_to_point_no_histogram(self):
560+
timestamp = datetime(1970, 1, 1)
561+
dist_agg_data = aggregation_data_module.DistributionAggregationData(
562+
mean_data=50,
563+
count_data=99,
564+
min_=1,
565+
max_=99,
566+
sum_of_sqd_deviations=80850.0,
567+
)
568+
converted_point = dist_agg_data.to_point(timestamp)
569+
self.assertTrue(isinstance(converted_point.value,
570+
value.ValueDistribution))
571+
self.assertEqual(converted_point.value.count, 99)
572+
self.assertEqual(converted_point.value.sum, 4950)
573+
self.assertEqual(converted_point.value.sum_of_squared_deviation,
574+
80850.0)
575+
self.assertIsNone(converted_point.value.buckets)
576+
self.assertIsNone(converted_point.value.bucket_options._type)

tests/unit/stats/test_stats.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,63 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
try:
16+
from mock import Mock
17+
except ImportError:
18+
from unittest.mock import Mock
19+
1520
import unittest
1621

22+
from opencensus.metrics.export import metric_descriptor
23+
from opencensus.metrics.export import value
24+
from opencensus.stats import aggregation
25+
from opencensus.stats import measure
1726
from opencensus.stats import stats as stats_module
27+
from opencensus.stats import view
28+
from opencensus.tags import tag_map
1829

1930

2031
class TestStats(unittest.TestCase):
21-
def test_constructor(self):
32+
def test_get_metrics(self):
33+
"""Test that Stats converts recorded values into metrics."""
34+
2235
stats = stats_module.Stats()
2336

24-
self.assertEqual(stats._view_manager, stats.view_manager)
25-
self.assertEqual(stats._stats_recorder, stats.stats_recorder)
37+
# Check that metrics are empty before view registration
38+
initial_metrics = list(stats.get_metrics())
39+
self.assertEqual(initial_metrics, [])
40+
41+
mock_measure = Mock(spec=measure.MeasureFloat)
42+
43+
mock_md = Mock(spec=metric_descriptor.MetricDescriptor)
44+
mock_md.type =\
45+
metric_descriptor.MetricDescriptorType.CUMULATIVE_DISTRIBUTION
46+
47+
mock_view = Mock(spec=view.View)
48+
mock_view.measure = mock_measure
49+
mock_view.get_metric_descriptor.return_value = mock_md
50+
mock_view.columns = ['k1']
51+
52+
stats.view_manager.measure_to_view_map.register_view(mock_view, Mock())
53+
54+
# Check that metrics are stil empty until we record
55+
empty_metrics = list(stats.get_metrics())
56+
self.assertEqual(empty_metrics, [])
57+
58+
mm = stats.stats_recorder.new_measurement_map()
59+
mm._measurement_map = {mock_measure: 1.0}
60+
61+
mock_view.aggregation = aggregation.DistributionAggregation()
62+
63+
tm = tag_map.TagMap()
64+
tm.insert('k1', 'v1')
65+
mm.record(tm)
66+
67+
metrics = list(stats.get_metrics())
68+
self.assertEqual(len(metrics), 1)
69+
[metric] = metrics
70+
self.assertEqual(len(metric.time_series), 1)
71+
[ts] = metric.time_series
72+
self.assertEqual(len(ts.points), 1)
73+
[point] = ts.points
74+
self.assertTrue(isinstance(point.value, value.ValueDistribution))

0 commit comments

Comments
 (0)