Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit dbfc325

Browse files
authored
Add BucketOptions (#410)
Replaces DistributionValue's bucket_bounds with a new BucketOptions class following the spec, and fixes #399.
1 parent a711a17 commit dbfc325

File tree

3 files changed

+125
-40
lines changed

3 files changed

+125
-40
lines changed

opencensus/metrics/export/value.py

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
"""
15+
The classes in this module implement the spec for v1 Metrics as of
16+
opencensus-proto release v0.0.2. See opencensus-proto for details:
17+
18+
https://github.com/census-instrumentation/opencensus-proto/blob/24333298e36590ea0716598caacc8959fc393c48/src/opencensus/proto/metrics/v1/metrics.proto
19+
""" # noqa
20+
21+
from copy import copy
1422

1523

1624
class Value(object):
@@ -154,12 +162,48 @@ def exemplar(self):
154162
return self._exemplar
155163

156164

157-
def window(ible, width):
158-
win = []
159-
for x in ible:
160-
win = (win + [x])[-width:]
161-
if len(win) == width:
162-
yield win
165+
class Explicit(object):
166+
"""Set of explicit bucket boundaries.
167+
168+
Specifies a set of buckets with arbitrary upper-bounds. This defines
169+
size(bounds) + 1 (= N) buckets. The boundaries for bucket index i are:
170+
171+
- [0, bounds[i]) for i == 0
172+
- [bounds[i-1], bounds[i]) for 0 < i < N-1
173+
- [bounds[i-1], +infinity) for i == N-1
174+
"""
175+
176+
def __init__(self, bounds):
177+
if not bounds:
178+
raise ValueError("Bounds must not be null or empty")
179+
if bounds != sorted(set(bounds)):
180+
raise ValueError("Bounds must be strictly increasing")
181+
if bounds[0] <= 0:
182+
raise ValueError("Bounds must be positive")
183+
self._bounds = bounds
184+
185+
@property
186+
def bounds(self):
187+
return copy(self._bounds)
188+
189+
190+
class BucketOptions(object):
191+
"""Container for bucket options, including explicit boundaries.
192+
193+
A Distribution may optionally contain a histogram of the values in the
194+
population. The bucket boundaries for that histogram are described by
195+
BucketOptions.
196+
197+
If bucket_options has no type, then there is no histogram associated with
198+
the Distribution.
199+
"""
200+
201+
def __init__(self, type_=None):
202+
self._type = type_
203+
204+
@property
205+
def type_(self):
206+
return self._type
163207

164208

165209
class ValueDistribution(Value):
@@ -179,17 +223,20 @@ class ValueDistribution(Value):
179223
:param sum_of_squared_deviation: The sum of squared deviations from the
180224
mean of the values in the population.
181225
182-
TODO: update to BucketOptions
183-
:type bucket_bounds: list(float)
184-
:param bucket_bounds: Bucket boundaries for the histogram of the values in
226+
:type bucket_options: :class: 'BucketOptions'
227+
:param bucket_options: Bucket boundaries for the histogram of the values in
185228
the population.
186229
187230
:type buckets: list(:class: 'Bucket')
188231
:param buckets: Histogram buckets for the given bucket boundaries.
189232
"""
190233

191-
def __init__(self, count, sum_, sum_of_squared_deviation, bucket_bounds,
192-
buckets):
234+
def __init__(self,
235+
count,
236+
sum_,
237+
sum_of_squared_deviation,
238+
bucket_options,
239+
buckets=None):
193240
if count < 0:
194241
raise ValueError("count must be non-negative")
195242
elif count == 0:
@@ -198,22 +245,27 @@ def __init__(self, count, sum_, sum_of_squared_deviation, bucket_bounds,
198245
if sum_of_squared_deviation != 0:
199246
raise ValueError("sum_of_squared_deviation must be 0 if count "
200247
"is 0")
201-
if not bucket_bounds:
248+
if bucket_options is None:
249+
raise ValueError("bucket_options must not be null")
250+
if bucket_options.type_ is None:
202251
if buckets is not None:
203-
raise ValueError("buckets must be null if bucket_bounds is "
204-
"empty")
252+
raise ValueError("buckets must be null if the distribution has"
253+
"no histogram (i.e. bucket_options.type is "
254+
"null)")
205255
else:
206-
for (lo, hi) in window(bucket_bounds, 2):
207-
if lo >= hi:
208-
raise ValueError("bucket_bounds must be monotonically "
209-
"increasing")
256+
if len(buckets) != len(bucket_options.type_.bounds) + 1:
257+
# Note that this includes the implicit 0 and positive-infinity
258+
# boundaries, so bounds [1, 2] implies three buckets: [[0, 1),
259+
# [1, 2), [2, inf)].
260+
raise ValueError("There must be one bucket for each pair of "
261+
"boundaries")
210262
if count != sum(bucket.count for bucket in buckets):
211263
raise ValueError("The distribution count must equal the sum "
212264
"of bucket counts")
213265
self._count = count
214266
self._sum = sum_
215267
self._sum_of_squared_deviation = sum_of_squared_deviation
216-
self._bucket_bounds = bucket_bounds
268+
self._bucket_options = bucket_options
217269
self._buckets = buckets
218270

219271
@property
@@ -229,8 +281,8 @@ def sum_of_squared_deviation(self):
229281
return self._sum_of_squared_deviation
230282

231283
@property
232-
def bucket_bounds(self):
233-
return self._bucket_bounds
284+
def bucket_options(self):
285+
return self._bucket_options
234286

235287
@property
236288
def buckets(self):

tests/unit/metrics/export/test_point.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def setUp(self):
3232
100,
3333
1000.0,
3434
10.0,
35-
list(range(11)),
35+
value_module.BucketOptions(
36+
value_module.Explicit(list(range(1, 10)))),
3637
[value_module.Bucket(10, None) for ii in range(10)],
3738
)
3839

tests/unit/metrics/export/test_value.py

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,68 +48,79 @@ def test_create_summary_value(self):
4848
VD_COUNT = 100
4949
VD_SUM = 1000.0
5050
VD_SUM_OF_SQUARED_DEVIATION = 10.0
51-
BUCKET_BOUNDS = list(range(11))
51+
BOUNDS = list(range(1, 10))
52+
BUCKET_OPTIONS = value_module.BucketOptions(value_module.Explicit(BOUNDS))
5253
BUCKETS = [value_module.Bucket(10, None) for ii in range(10)]
5354

5455

5556
class TestValueDistribution(unittest.TestCase):
5657
def test_init(self):
5758
distribution = value_module.ValueDistribution(
58-
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, BUCKET_BOUNDS,
59+
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, BUCKET_OPTIONS,
5960
BUCKETS)
6061
self.assertEqual(distribution.count, VD_COUNT)
6162
self.assertEqual(distribution.sum, VD_SUM)
6263
self.assertEqual(distribution.sum_of_squared_deviation,
6364
VD_SUM_OF_SQUARED_DEVIATION)
64-
self.assertEqual(distribution.bucket_bounds, BUCKET_BOUNDS)
65+
self.assertEqual(distribution.bucket_options, BUCKET_OPTIONS)
66+
self.assertEqual(distribution.bucket_options.type_.bounds, BOUNDS)
6567
self.assertEqual(distribution.buckets, BUCKETS)
6668

6769
def test_init_no_histogram(self):
6870
distribution = value_module.ValueDistribution(
69-
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [], None)
71+
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION,
72+
value_module.BucketOptions(), None)
7073
self.assertEqual(distribution.count, VD_COUNT)
7174
self.assertEqual(distribution.sum, VD_SUM)
7275
self.assertEqual(distribution.sum_of_squared_deviation,
7376
VD_SUM_OF_SQUARED_DEVIATION)
74-
self.assertEqual(distribution.bucket_bounds, [])
77+
self.assertIsNone(distribution.bucket_options.type_)
7578
self.assertEqual(distribution.buckets, None)
7679

7780
def test_init_bad_args(self):
81+
value_module.ValueDistribution(VD_COUNT, VD_SUM,
82+
VD_SUM_OF_SQUARED_DEVIATION,
83+
BUCKET_OPTIONS, BUCKETS)
7884

7985
with self.assertRaises(ValueError):
8086
value_module.ValueDistribution(-1, VD_SUM,
8187
VD_SUM_OF_SQUARED_DEVIATION,
82-
BUCKET_BOUNDS, BUCKETS)
88+
BUCKET_OPTIONS, BUCKETS)
8389

8490
with self.assertRaises(ValueError):
85-
value_module.ValueDistribution(
86-
0, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, BUCKET_BOUNDS, BUCKETS)
91+
value_module.ValueDistribution(0, VD_SUM,
92+
VD_SUM_OF_SQUARED_DEVIATION,
93+
BUCKET_OPTIONS, BUCKETS)
8794

8895
with self.assertRaises(ValueError):
8996
value_module.ValueDistribution(0, 0, VD_SUM_OF_SQUARED_DEVIATION,
90-
BUCKET_BOUNDS, BUCKETS)
97+
BUCKET_OPTIONS, BUCKETS)
9198

9299
with self.assertRaises(ValueError):
93100
value_module.ValueDistribution(
94101
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, None, BUCKETS)
95102

96103
with self.assertRaises(ValueError):
97-
value_module.ValueDistribution(
98-
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [], BUCKETS)
104+
value_module.ValueDistribution(0, 0, 0, BUCKET_OPTIONS, BUCKETS)
99105

100106
with self.assertRaises(ValueError):
101-
value_module.ValueDistribution(0, 0, 0, BUCKET_BOUNDS, BUCKETS)
107+
value_module.ValueDistribution(VD_COUNT - 1, VD_SUM,
108+
VD_SUM_OF_SQUARED_DEVIATION,
109+
BUCKET_OPTIONS, BUCKETS)
102110

103111
with self.assertRaises(ValueError):
104-
value_module.ValueDistribution(
105-
VD_COUNT, VD_SUM, VD_SUM_OF_SQUARED_DEVIATION, [1, 1],
106-
[value_module.Bucket(1, None),
107-
value_module.Bucket(1, None)])
112+
value_module.ValueDistribution(VD_COUNT, VD_SUM,
113+
VD_SUM_OF_SQUARED_DEVIATION,
114+
value_module.BucketOptions(),
115+
BUCKETS)
108116

109117
with self.assertRaises(ValueError):
110-
value_module.ValueDistribution(VD_COUNT - 1, VD_SUM,
118+
value_module.ValueDistribution(VD_COUNT, VD_SUM,
111119
VD_SUM_OF_SQUARED_DEVIATION,
112-
BUCKET_BOUNDS, BUCKETS)
120+
BUCKET_OPTIONS, BUCKETS[:-1])
121+
122+
def test_init_empty_buckets_null_bucket_options(self):
123+
pass
113124

114125

115126
EX_VALUE = 1.0
@@ -135,3 +146,24 @@ def test_init(self):
135146
bucket = value_module.Bucket(1, self.exemplar)
136147
self.assertEqual(bucket.count, 1)
137148
self.assertEqual(bucket.exemplar, self.exemplar)
149+
150+
151+
class TestExplicit(unittest.TestCase):
152+
def test_init(self):
153+
bounds = [1, 2]
154+
explicit = value_module.Explicit(bounds)
155+
self.assertEqual(explicit.bounds, bounds)
156+
157+
def test_bad_init(self):
158+
with self.assertRaises(ValueError):
159+
value_module.Explicit(None)
160+
with self.assertRaises(ValueError):
161+
value_module.Explicit([])
162+
with self.assertRaises(ValueError):
163+
value_module.Explicit([-1, 1, 2])
164+
with self.assertRaises(ValueError):
165+
value_module.Explicit([0, 1, 2])
166+
with self.assertRaises(ValueError):
167+
value_module.Explicit([1, 1])
168+
with self.assertRaises(ValueError):
169+
value_module.Explicit([2, 1])

0 commit comments

Comments
 (0)