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

Commit d59ee42

Browse files
vcasadeisongy23
authored andcommitted
Stats exemplars (#287)
1 parent d473384 commit d59ee42

8 files changed

Lines changed: 426 additions & 44 deletions

File tree

opencensus/stats/aggregation_data.py

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self, sum_data):
4141
super(SumAggregationDataFloat, self).__init__(sum_data)
4242
self._sum_data = sum_data
4343

44-
def add_sample(self, value):
44+
def add_sample(self, value, timestamp=None, attachments=None):
4545
"""Allows the user to add a sample to the Sum Aggregation Data
4646
The value of the sample is then added to the current sum data
4747
"""
@@ -64,7 +64,7 @@ def __init__(self, count_data):
6464
super(CountAggregationData, self).__init__(count_data)
6565
self._count_data = count_data
6666

67-
def add_sample(self, value):
67+
def add_sample(self, value, timestamp=None, attachments=None):
6868
"""Adds a sample to the current Count Aggregation Data and adds 1 to
6969
the count data"""
7070
self._count_data = self._count_data + 1
@@ -97,6 +97,9 @@ class DistributionAggregationData(BaseAggregationData):
9797
:type counts_per_bucket: list(int)
9898
:param counts_per_bucket: the number of occurrences per bucket
9999
100+
:type exemplars: list(Exemplar)
101+
:param: exemplars: the exemplars associated with histogram buckets.
102+
100103
:type bounds: list(float)
101104
:param bounds: the histogram distribution of the values
102105
@@ -108,7 +111,8 @@ def __init__(self,
108111
max_,
109112
sum_of_sqd_deviations,
110113
counts_per_bucket=None,
111-
bounds=None):
114+
bounds=None,
115+
exemplars=None):
112116
super(DistributionAggregationData, self).__init__(mean_data)
113117
self._mean_data = mean_data
114118
self._count_data = count_data
@@ -126,6 +130,13 @@ def __init__(self,
126130
self._counts_per_bucket = counts_per_bucket
127131
self._bounds = bucket_boundaries.BucketBoundaries(
128132
boundaries=bounds).boundaries
133+
bucket = 0
134+
for _ in self.bounds:
135+
bucket = bucket + 1
136+
137+
# If there is no histogram, do not record an exemplar
138+
self._exemplars = \
139+
{bucket: exemplars} if len(self._bounds) > 0 else None
129140

130141
@property
131142
def mean_data(self):
@@ -157,6 +168,11 @@ def counts_per_bucket(self):
157168
"""The current counts per bucket for the distribution"""
158169
return self._counts_per_bucket
159170

171+
@property
172+
def exemplars(self):
173+
"""The current counts per bucket for the distribution"""
174+
return self._exemplars
175+
160176
@property
161177
def bounds(self):
162178
"""The current bounds for the distribution"""
@@ -174,15 +190,17 @@ def variance(self):
174190
return 0
175191
return self.sum_of_sqd_deviations / (self._count_data - 1)
176192

177-
def add_sample(self, value):
193+
def add_sample(self, value, timestamp, attachments):
178194
"""Adding a sample to Distribution Aggregation Data"""
179195
if value < self.min:
180196
self._min = value
181197
if value > self.max:
182198
self._max = value
183199
self._count_data += 1
184-
self.increment_bucket_count(value)
200+
bucket = self.increment_bucket_count(value)
185201

202+
if attachments is not None and self.exemplars is not None:
203+
self.exemplars[bucket] = Exemplar(value, timestamp, attachments)
186204
if self.count_data == 1:
187205
self._mean_data = value
188206
return
@@ -196,22 +214,29 @@ def add_sample(self, value):
196214

197215
def increment_bucket_count(self, value):
198216
"""Increment the bucket count based on a given value from the user"""
199-
if len(self._bounds) == 0:
200-
self._counts_per_bucket[0] += 1
201-
return
202-
203217
i = 0
218+
incremented = False
204219
for b in self._bounds:
205-
if value < b:
220+
if value < b and not incremented:
206221
self._counts_per_bucket[i] += 1
207-
return
222+
incremented = True
208223
i += 1
209224

225+
if incremented:
226+
return i
227+
228+
if len(self._bounds) == 0:
229+
self._counts_per_bucket[0] += 1
230+
return i
231+
210232
self._counts_per_bucket[(len(self._bounds))-1] += 1
211233

234+
return i
235+
212236

213237
class LastValueAggregationData(BaseAggregationData):
214-
"""LastValue Aggregation Data is the value of aggregated data
238+
"""
239+
LastValue Aggregation Data is the value of aggregated data
215240
216241
:type value: long
217242
:param value: represents the current value
@@ -221,15 +246,63 @@ def __init__(self, value):
221246
super(LastValueAggregationData, self).__init__(value)
222247
self._value = value
223248

224-
def add_sample(self, value):
249+
def add_sample(self, value, timestamp=None, attachments=None):
225250
"""Adds a sample to the current
226-
LastValue Aggregation Data and overwrite
227-
the current recorded value
228-
"""
251+
LastValue Aggregation Data and overwrite
252+
the current recorded value"""
229253
self._value = value
230254

231255
@property
232256
def value(self):
233-
"""The current value recorded
234-
"""
257+
"""The current value recorded"""
235258
return self._value
259+
260+
261+
class Exemplar(object):
262+
""" Exemplar represents an example point that may be used to annotate
263+
aggregated distribution values, associated with a histogram bucket.
264+
265+
:type value: double
266+
:param value: value of the Exemplar point.
267+
268+
:type timestamp: time
269+
:param timestamp: the time that this Exemplar's value was recorded.
270+
271+
:type attachments: dict
272+
:param attachments: the contextual information about the example value.
273+
"""
274+
275+
def __init__(self,
276+
value,
277+
timestamp,
278+
attachments):
279+
self._value = value
280+
281+
self._timestamp = timestamp
282+
283+
if attachments is None:
284+
raise TypeError('attachments should not be empty')
285+
286+
for key, value in attachments.items():
287+
if key is None or not isinstance(key, str):
288+
raise TypeError('attachment key should not be '
289+
'empty and should be a string')
290+
if value is None or not isinstance(value, str):
291+
raise TypeError('attachment value should not be '
292+
'empty and should be a string')
293+
self._attachments = attachments
294+
295+
@property
296+
def value(self):
297+
"""The current value of the Exemplar point"""
298+
return self._value
299+
300+
@property
301+
def timestamp(self):
302+
"""The time that this Exemplar's value was recorded"""
303+
return self._timestamp
304+
305+
@property
306+
def attachments(self):
307+
"""The contextual information about the example value"""
308+
return self._attachments

opencensus/stats/measure_to_view_map.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def register_view(self, view, timestamp):
9393
self._measure_to_view_data_list_map[view.measure.name].append(
9494
ViewData(view=view, start_time=timestamp, end_time=timestamp))
9595

96-
def record(self, tags, measurement_map, timestamp):
96+
def record(self, tags, measurement_map, timestamp, attachments=None):
9797
"""records stats with a set of tags"""
9898
for measure, value in measurement_map.items():
9999
if measure != self._registered_measures.get(measure.name):
@@ -105,7 +105,8 @@ def record(self, tags, measurement_map, timestamp):
105105
view_datas.extend(view_data_list)
106106
for view_data in view_datas:
107107
view_data.record(
108-
context=tags, value=value, timestamp=timestamp)
108+
context=tags, value=value, timestamp=timestamp,
109+
attachment=attachments)
109110
self.export(view_datas)
110111

111112
def export(self, view_datas):

opencensus/stats/measurement_map.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ class MeasurementMap(object):
2525
:param measure_to_view_map: the measure to view map that will store the
2626
recorded stats with tags
2727
28+
:type: attachments: dict
29+
:param attachments: the contextual information about the attachment value.
30+
2831
"""
29-
def __init__(self, measure_to_view_map):
32+
def __init__(self, measure_to_view_map, attachments=None):
3033
self._measurement_map = {}
3134
self._measure_to_view_map = measure_to_view_map
35+
self._attachments = attachments
3236

3337
@property
3438
def measurement_map(self):
@@ -40,6 +44,11 @@ def measure_to_view_map(self):
4044
"""the current measure to view map for the measurement map"""
4145
return self._measure_to_view_map
4246

47+
@property
48+
def attachments(self):
49+
"""the current contextual information about the attachment value."""
50+
return self._attachments
51+
4352
def measure_int_put(self, measure, value):
4453
"""associates the measure of type Int with the given value"""
4554
self._measurement_map[measure] = value
@@ -48,6 +57,24 @@ def measure_float_put(self, measure, value):
4857
"""associates the measure of type Float with the given value"""
4958
self._measurement_map[measure] = value
5059

60+
def measure_put_attachment(self, key, value):
61+
"""Associate the contextual information of an Exemplar to this MeasureMap
62+
Contextual information is represented as key - value string pairs.
63+
If this method is called multiple times with the same key,
64+
only the last value will be kept.
65+
"""
66+
if self._attachments is None:
67+
self._attachments = dict()
68+
69+
if key is None or not isinstance(key, str):
70+
raise TypeError('attachment key should not be '
71+
'empty and should be a string')
72+
if value is None or not isinstance(value, str):
73+
raise TypeError('attachment value should not be '
74+
'empty and should be a string')
75+
76+
self._attachments[key] = value
77+
5178
def record(self, tag_map_tags=execution_context.get_current_tag_map()):
5279
"""records all the measures at the same time with a tag_map.
5380
tag_map could either be explicitly passed to the method, or implicitly
@@ -56,5 +83,6 @@ def record(self, tag_map_tags=execution_context.get_current_tag_map()):
5683
self.measure_to_view_map.record(
5784
tags=tag_map_tags,
5885
measurement_map=self.measurement_map,
59-
timestamp=datetime.utcnow().isoformat() + 'Z'
86+
timestamp=datetime.utcnow().isoformat() + 'Z',
87+
attachments=self.attachments
6088
)

opencensus/stats/view_data.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,13 @@ def get_tag_values(self, tags, columns):
7979
i += 1
8080
return tag_values
8181

82-
def record(self, context, value, timestamp):
82+
def record(self, context, value, timestamp, attachments=None):
8383
"""records the view data against context"""
8484
tag_values = self.get_tag_values(tags=context.map,
8585
columns=self.view.columns)
8686
tuple_vals = tuple(tag_values)
8787
if tuple_vals not in self.tag_value_aggregation_data_map:
8888
self.tag_value_aggregation_data_map[tuple_vals] = copy.deepcopy(
8989
self.view.aggregation.aggregation_data)
90-
self.tag_value_aggregation_data_map.get(tuple_vals).add_sample(value)
90+
self.tag_value_aggregation_data_map.get(tuple_vals).\
91+
add_sample(value, timestamp, attachments)

0 commit comments

Comments
 (0)