From 11471730c36af35beefd49b15faa61171ed595f5 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 8 Oct 2018 12:21:52 -0700 Subject: [PATCH] Metrics: Add Value, Point and Summary (#337) * Add Value, Point and Summary * Fix reviews --- opencensus/metrics/export/__init__.py | 0 opencensus/metrics/export/point.py | 38 ++++++ opencensus/metrics/export/summary.py | 144 ++++++++++++++++++++++ opencensus/metrics/export/value.py | 90 ++++++++++++++ tests/unit/metrics/export/__init__.py | 0 tests/unit/metrics/export/test_point.py | 64 ++++++++++ tests/unit/metrics/export/test_summary.py | 121 ++++++++++++++++++ tests/unit/metrics/export/test_value.py | 45 +++++++ 8 files changed, 502 insertions(+) create mode 100644 opencensus/metrics/export/__init__.py create mode 100644 opencensus/metrics/export/point.py create mode 100644 opencensus/metrics/export/summary.py create mode 100644 opencensus/metrics/export/value.py create mode 100644 tests/unit/metrics/export/__init__.py create mode 100644 tests/unit/metrics/export/test_point.py create mode 100644 tests/unit/metrics/export/test_summary.py create mode 100644 tests/unit/metrics/export/test_value.py diff --git a/opencensus/metrics/export/__init__.py b/opencensus/metrics/export/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opencensus/metrics/export/point.py b/opencensus/metrics/export/point.py new file mode 100644 index 000000000..16aaa52a8 --- /dev/null +++ b/opencensus/metrics/export/point.py @@ -0,0 +1,38 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Point(object): + """A timestamped measurement of a TimeSeries. + + :type value: Value + :param value: the Value of the Point. + + :type timestamp: time + :param timestamp: the Timestamp when the Point was recorded. + """ + + def __init__(self, value, timestamp): + self._value = value + self._timestamp = timestamp + + @property + def value(self): + """Returns the Value""" + return self._value + + @property + def timestamp(self): + """Returns the Timestamp when this Point was recorded.""" + return self._timestamp diff --git a/opencensus/metrics/export/summary.py b/opencensus/metrics/export/summary.py new file mode 100644 index 000000000..8ed511148 --- /dev/null +++ b/opencensus/metrics/export/summary.py @@ -0,0 +1,144 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Summary(object): + """Implementation of the Summary as a summary of observations. + + :type count: long + :param count: the count of the population values. + + :type sum_data: float + :param sum_data: the sum of the population values. + + :type snapshot: Snapshot + :param snapshot: the values calculated over a sliding time window. + """ + + def __init__(self, count, sum_data, snapshot): + check_count_and_sum(count, sum_data) + self._count = count + self._sum_data = sum_data + + if snapshot is None: + raise ValueError('snapshot must not be none') + + self._snapshot = snapshot + + @property + def count(self): + """Returns the count of the population values""" + return self._count + + @property + def sum_data(self): + """Returns the sum of the population values.""" + return self._sum_data + + @property + def snapshot(self): + """Returns the values calculated over a sliding time window.""" + return self._snapshot + + +class Snapshot(object): + """Represents the summary observation of the recorded events over a + sliding time window. + + :type count: long + :param count: the number of values in the snapshot. + + :type sum_data: float + :param sum_data: the sum of values in the snapshot. + + :type value_at_percentiles: ValueAtPercentile + :param value_at_percentiles: a list of values at different percentiles + of the distribution calculated from the current snapshot. The percentiles + must be strictly increasing. + """ + + def __init__(self, count, sum_data, value_at_percentiles=None): + check_count_and_sum(count, sum_data) + self._count = count + self._sum_data = sum_data + + if value_at_percentiles is None: + value_at_percentiles = [] + + if not isinstance(value_at_percentiles, list): + raise ValueError('value_at_percentiles must be an ' + 'instance of list') + + self._value_at_percentiles = value_at_percentiles + + @property + def count(self): + """Returns the number of values in the snapshot""" + return self._count + + @property + def sum_data(self): + """Returns the sum of values in the snapshot.""" + return self._sum_data + + @property + def value_at_percentiles(self): + """Returns a list of values at different percentiles + of the distribution calculated from the current snapshot. + """ + return self._value_at_percentiles + + +class ValueAtPercentile(object): + """Represents the value at a given percentile of a distribution. + + :type percentile: float + :param percentile: the percentile in the ValueAtPercentile. + + :type value: float + :param value: the value in the ValueAtPercentile. + """ + + def __init__(self, percentile, value): + + if not 0 < percentile <= 100.0: + raise ValueError("percentile must be in the interval (0.0, 100.0]") + + self._percentile = percentile + + if value < 0: + raise ValueError('value must be non-negative') + + self._value = value + + @property + def percentile(self): + """Returns the percentile in the ValueAtPercentile""" + return self._percentile + + @property + def value(self): + """Returns the value in the ValueAtPercentile""" + return self._value + + +def check_count_and_sum(count, sum_data): + if not (count is None or count >= 0): + raise ValueError('count must be non-negative') + + if not (sum_data is None or sum_data >= 0): + raise ValueError('sum_data must be non-negative') + + if count == 0 and sum_data != 0: + raise ValueError('sum_data must be 0 if count is 0') diff --git a/opencensus/metrics/export/value.py b/opencensus/metrics/export/value.py new file mode 100644 index 000000000..995abb8f6 --- /dev/null +++ b/opencensus/metrics/export/value.py @@ -0,0 +1,90 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Value(object): + """The actual point value for a Point. + Currently there are four types of Value: + + Each Point contains exactly one of the four Value types. + """ + def __init__(self, value): + self._value = value + + @staticmethod + def double_value(value): + """Returns a double Value + + :type value: float + :param value: value in double + """ + return ValueDouble(value) + + @staticmethod + def long_value(value): + """Returns a long Value + + :type value: long + :param value: value in long + """ + return ValueLong(value) + + @staticmethod + def summary_value(value): + """Returns a summary Value + + :type value: Summary + :param value: value in Summary + """ + return ValueSummary(value) + + @property + def value(self): + """Returns the value.""" + return self._value + + +class ValueDouble(Value): + """A 64-bit double-precision floating-point number. + + :type value: float + :param value: the value in float. + """ + def __init__(self, value): + super(ValueDouble, self).__init__(value) + + +class ValueLong(Value): + """A 64-bit integer. + + :type value: long + :param value: the value in long. + """ + def __init__(self, value): + super(ValueLong, self).__init__(value) + + +class ValueSummary(Value): + """Represents a snapshot values calculated over an arbitrary time window. + + :type value: summary + :param value: the value in summary. + """ + def __init__(self, value): + super(ValueSummary, self).__init__(value) diff --git a/tests/unit/metrics/export/__init__.py b/tests/unit/metrics/export/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/metrics/export/test_point.py b/tests/unit/metrics/export/test_point.py new file mode 100644 index 000000000..db11eb3fc --- /dev/null +++ b/tests/unit/metrics/export/test_point.py @@ -0,0 +1,64 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from opencensus.metrics.export import point as point_module +from opencensus.metrics.export import summary as summary_module +from opencensus.metrics.export import value as value_module + + +class TestPoint(unittest.TestCase): + + def setUp(self): + self.double_value = value_module.Value.double_value(55.5) + self.long_value = value_module.Value.long_value(9876543210) + self.timestamp = '2018-10-06T17:57:57.936475Z' + + value_at_percentile = [summary_module.ValueAtPercentile(99.5, 10.2)] + snapshot = summary_module.Snapshot(10, 87.07, value_at_percentile) + self.summary = summary_module.Summary(10, 6.6, snapshot) + self.summary_value = value_module.Value.summary_value(self.summary) + + def test_point_with_double_value(self): + point = point_module.Point(self.double_value, self.timestamp) + + self.assertIsNotNone(point) + self.assertEqual(point.timestamp, self.timestamp) + + self.assertIsInstance(point.value, value_module.ValueDouble) + self.assertIsNotNone(point.value) + self.assertEqual(point.value, self.double_value) + self.assertEqual(point.value.value, 55.5) + + def test_point_with_long_value(self): + point = point_module.Point(self.long_value, self.timestamp) + + self.assertIsNotNone(point) + self.assertEqual(point.timestamp, self.timestamp) + + self.assertIsInstance(point.value, value_module.ValueLong) + self.assertIsNotNone(point.value) + self.assertEqual(point.value, self.long_value) + self.assertEqual(point.value.value, 9876543210) + + def test_point_with_summary_value(self): + point = point_module.Point(self.summary_value, self.timestamp) + + self.assertIsNotNone(point) + self.assertEqual(point.timestamp, self.timestamp) + + self.assertIsInstance(point.value, value_module.ValueSummary) + self.assertIsNotNone(point.value) + self.assertEqual(point.value, self.summary_value) + self.assertEqual(point.value.value, self.summary) diff --git a/tests/unit/metrics/export/test_summary.py b/tests/unit/metrics/export/test_summary.py new file mode 100644 index 000000000..960d726a3 --- /dev/null +++ b/tests/unit/metrics/export/test_summary.py @@ -0,0 +1,121 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from opencensus.metrics.export import summary as summary_module + + +class TestSummary(unittest.TestCase): + + def setUp(self): + value_at_percentile = [summary_module.ValueAtPercentile(99.5, 10.2)] + self.snapshot = summary_module.Snapshot(10, 87.07, value_at_percentile) + + def test_constructor(self): + summary = summary_module.Summary(10, 6.6, self.snapshot) + + self.assertIsNotNone(summary) + self.assertEquals(summary.count, 10) + self.assertEquals(summary.sum_data, 6.6) + self.assertIsNotNone(summary.snapshot) + self.assertIsInstance(summary.snapshot, summary_module.Snapshot) + + def test_constructor_with_negative_count(self): + with self.assertRaisesRegexp(ValueError, 'count must be non-negative'): + summary_module.Summary(-10, 87.07, self.snapshot) + + def test_constructor_with_negative_sum_data(self): + with self.assertRaisesRegexp(ValueError, 'sum_data must be non-negative'): + summary_module.Summary(10, -87.07, self.snapshot) + + def test_constructor_with_zero_count_and_sum_data(self): + with self.assertRaisesRegexp(ValueError, 'sum_data must be 0 if count is 0'): + summary_module.Summary(0, 87.07, self.snapshot) + + def test_constructor_with_none_snapshot(self): + with self.assertRaisesRegexp(ValueError, 'snapshot must not be none'): + summary_module.Summary(10, 87.07, None) + + +class TestSnapshot(unittest.TestCase): + + def setUp(self): + self.value_at_percentile = [summary_module.ValueAtPercentile(99.5, 10.2)] + + # Invalid value_at_percentile + self.value_at_percentile1 = summary_module.ValueAtPercentile(99.5, 10.2) + + def test_constructor(self): + snapshot = summary_module.Snapshot(10, 87.07, self.value_at_percentile) + + self.assertIsNotNone(snapshot) + self.assertEquals(snapshot.count, 10) + self.assertEquals(snapshot.sum_data, 87.07) + self.assertIsNotNone(snapshot.value_at_percentiles) + self.assertEquals(len(snapshot.value_at_percentiles), 1) + self.assertEquals(snapshot.value_at_percentiles[0].percentile, 99.5) + self.assertEquals(snapshot.value_at_percentiles[0].value, 10.2) + + def test_constructor_invalid_value_at_percentile(self): + with self.assertRaises(ValueError): + summary_module.Snapshot(10, 87.07, self.value_at_percentile1) + + def test_constructor_empty_value_at_percentile(self): + snapshot = summary_module.Snapshot(10, 87.07) + + self.assertIsNotNone(snapshot) + self.assertIsNotNone(snapshot.value_at_percentiles) + self.assertEquals(len(snapshot.value_at_percentiles), 0) + + def test_constructor_with_negative_count(self): + with self.assertRaisesRegexp(ValueError, 'count must be non-negative'): + summary_module.Snapshot(-10, 87.07, self.value_at_percentile) + + def test_constructor_with_negative_sum_data(self): + with self.assertRaisesRegexp(ValueError, 'sum_data must be non-negative'): + summary_module.Snapshot(10, -87.07, self.value_at_percentile) + + def test_constructor_with_zero_count(self): + with self.assertRaisesRegexp(ValueError, 'sum_data must be 0 if count is 0'): + summary_module.Snapshot(0, 87.07, self.value_at_percentile) + + def test_constructor_with_zero_count_and_sum_data(self): + summary_module.Snapshot(0, 0, self.value_at_percentile) + + def test_constructor_with_none_count_sum(self): + snapshot = summary_module.Snapshot(None, None, self.value_at_percentile) + + self.assertIsNotNone(snapshot) + self.assertIsNone(snapshot.count) + self.assertIsNone(snapshot.sum_data) + self.assertIsNotNone(snapshot.value_at_percentiles) + self.assertEquals(len(snapshot.value_at_percentiles), 1) + + +class TestValueAtPercentile(unittest.TestCase): + + def test_constructor(self): + value_at_percentile = summary_module.ValueAtPercentile(99.5, 10.2) + + self.assertIsNotNone(value_at_percentile) + self.assertEquals(value_at_percentile.value, 10.2) + self.assertEquals(value_at_percentile.percentile, 99.5) + + def test_constructor_invalid_percentile(self): + with self.assertRaises(ValueError): + summary_module.ValueAtPercentile(100.1, 10.2) + + def test_constructor_invalid_value(self): + with self.assertRaisesRegexp(ValueError, 'value must be non-negative'): + summary_module.ValueAtPercentile(99.5, -10.2) diff --git a/tests/unit/metrics/export/test_value.py b/tests/unit/metrics/export/test_value.py new file mode 100644 index 000000000..d6bfb5fae --- /dev/null +++ b/tests/unit/metrics/export/test_value.py @@ -0,0 +1,45 @@ +# Copyright 2018, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from opencensus.metrics.export import summary as summary_module +from opencensus.metrics.export import value as value_module + + +class TestValue(unittest.TestCase): + + def test_create_double_value(self): + double_value = value_module.Value.double_value(-34.56) + + self.assertIsNotNone(double_value) + self.assertIsInstance(double_value, value_module.ValueDouble) + self.assertEqual(double_value.value, -34.56) + + def test_create_long_value(self): + long_value = value_module.Value.long_value(123456789) + + self.assertIsNotNone(long_value) + self.assertIsInstance(long_value, value_module.ValueLong) + self.assertEqual(long_value.value, 123456789) + + def test_create_summary_value(self): + value_at_percentile = [summary_module.ValueAtPercentile(99.5, 10.2)] + snapshot = summary_module.Snapshot(10, 87.07, value_at_percentile) + summary = summary_module.Summary(10, 6.6, snapshot) + + summary_value = value_module.Value.summary_value(summary) + + self.assertIsNotNone(summary_value) + self.assertIsInstance(summary_value, value_module.ValueSummary) + self.assertEqual(summary_value.value, summary)