16
16
from functools import wraps
17
17
from threading import Lock
18
18
19
- __all__ = ['Counter' , 'Gauge' , 'Summary' , 'CollectorRegistry ' ]
19
+ __all__ = ['Counter' , 'Gauge' , 'Summary' , 'Histogram ' ]
20
20
21
21
_METRIC_NAME_RE = re .compile (r'^[a-zA-Z_:][a-zA-Z0-9_:]*$' )
22
22
_METRIC_LABEL_NAME_RE = re .compile (r'^[a-zA-Z_:][a-zA-Z0-9_:]*$' )
23
23
_RESERVED_METRIC_LABEL_NAME_RE = re .compile (r'^__.*$' )
24
+ _INF = float ("inf" )
25
+ _MINUS_INF = float ("-inf" )
24
26
25
27
26
28
@@ -71,7 +73,7 @@ def get_sample_value(self, name, labels=None):
71
73
REGISTRY = CollectorRegistry ()
72
74
'''The default registry.'''
73
75
74
- _METRIC_TYPES = ('counter' , 'gauge' , 'summary' , 'untyped' )
76
+ _METRIC_TYPES = ('counter' , 'gauge' , 'summary' , 'histogram' , ' untyped' )
75
77
76
78
class Metric (object ):
77
79
'''A single metric and it's samples.'''
@@ -90,10 +92,11 @@ def add_sample(self, name, labels, value):
90
92
91
93
class _LabelWrapper (object ):
92
94
'''Handles labels for the wrapped metric.'''
93
- def __init__ (self , wrappedClass , labelnames ):
95
+ def __init__ (self , wrappedClass , labelnames , ** kwargs ):
94
96
self ._wrappedClass = wrappedClass
95
97
self ._type = wrappedClass ._type
96
98
self ._labelnames = labelnames
99
+ self ._kwargs = kwargs
97
100
self ._lock = Lock ()
98
101
self ._metrics = {}
99
102
@@ -108,7 +111,7 @@ def labels(self, *labelvalues):
108
111
labelvalues = tuple (labelvalues )
109
112
with self ._lock :
110
113
if labelvalues not in self ._metrics :
111
- self ._metrics [labelvalues ] = self ._wrappedClass ()
114
+ self ._metrics [labelvalues ] = self ._wrappedClass (** self . _kwargs )
112
115
return self ._metrics [labelvalues ]
113
116
114
117
def remove (self , * labelvalues ):
@@ -129,7 +132,7 @@ def _samples(self):
129
132
130
133
def _MetricWrapper (cls ):
131
134
'''Provides common functionality for metrics.'''
132
- def init (name , documentation , labelnames = (), namespace = '' , subsystem = '' , registry = REGISTRY ):
135
+ def init (name , documentation , labelnames = (), namespace = '' , subsystem = '' , registry = REGISTRY , ** kwargs ):
133
136
if labelnames :
134
137
for l in labelnames :
135
138
if not _METRIC_LABEL_NAME_RE .match (l ):
@@ -138,9 +141,9 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
138
141
raise ValueError ('Reserved label metric name: ' + l )
139
142
if l in cls ._reserved_labelnames :
140
143
raise ValueError ('Reserved label metric name: ' + l )
141
- collector = _LabelWrapper (cls , labelnames )
144
+ collector = _LabelWrapper (cls , labelnames , ** kwargs )
142
145
else :
143
- collector = cls ()
146
+ collector = cls (** kwargs )
144
147
145
148
full_name = ''
146
149
if namespace :
@@ -159,7 +162,8 @@ def collect():
159
162
return [metric ]
160
163
collector .collect = collect
161
164
162
- registry .register (collector )
165
+ if registry :
166
+ registry .register (collector )
163
167
return collector
164
168
165
169
return init
@@ -300,6 +304,73 @@ def _samples(self):
300
304
('_count' , {}, self ._count ),
301
305
('_sum' , {}, self ._sum ))
302
306
307
+ def _floatToGoString (d ):
308
+ if d == _INF :
309
+ return '+Inf'
310
+ elif d == _MINUS_INF :
311
+ return '-Inf'
312
+ else :
313
+ return repr (d )
314
+
315
+ @_MetricWrapper
316
+ class Histogram (object ):
317
+ _type = 'histogram'
318
+ _reserved_labelnames = ['histogram' ]
319
+ def __init__ (self , buckets = (.005 , .01 , .025 , .05 , .075 , .1 , .25 , .5 , .75 , 1.0 , 2.5 , 5.0 , 7.5 , 10.0 , _INF )):
320
+ self ._sum = 0.0
321
+ self ._lock = Lock ()
322
+ buckets = [float (b ) for b in buckets ]
323
+ if buckets != sorted (buckets ):
324
+ # This is probably an error on the part of the user,
325
+ # so raise rather than sorting for them.
326
+ raise ValueError ('Buckets not in sorted order' )
327
+ if buckets and buckets [- 1 ] != _INF :
328
+ buckets .append (_INF )
329
+ if len (buckets ) < 2 :
330
+ raise ValueError ('Must have at least two buckets' )
331
+ self ._upper_bounds = buckets
332
+ self ._buckets = [0.0 ] * len (buckets )
333
+
334
+ def observe (self , amount ):
335
+ '''Observe the given amount.'''
336
+ with self ._lock :
337
+ self ._sum += amount
338
+ for i , bound in enumerate (self ._upper_bounds ):
339
+ if amount <= bound :
340
+ self ._buckets [i ] += 1
341
+ break
342
+
343
+ def time (self ):
344
+ '''Time a block of code or function, and observe the duration in seconds.
345
+
346
+ Can be used as a function decorator or context manager.
347
+ '''
348
+ class Timer (object ):
349
+ def __init__ (self , histogram ):
350
+ self ._histogram = histogram
351
+ def __enter__ (self ):
352
+ self ._start = time .time ()
353
+ def __exit__ (self , typ , value , traceback ):
354
+ # Time can go backwards.
355
+ self ._histogram .observe (max (time .time () - self ._start , 0 ))
356
+ def __call__ (self , f ):
357
+ @wraps (f )
358
+ def wrapped (* args , ** kwargs ):
359
+ with self :
360
+ return f (* args , ** kwargs )
361
+ return wrapped
362
+ return Timer (self )
363
+
364
+ def _samples (self ):
365
+ with self ._lock :
366
+ samples = []
367
+ acc = 0
368
+ for i , bound in enumerate (self ._upper_bounds ):
369
+ acc += self ._buckets [i ]
370
+ samples .append (('_bucket' , {'le' : _floatToGoString (bound )}, acc ))
371
+ samples .append (('_count' , {}, acc ))
372
+ samples .append (('_sum' , {}, self ._sum ))
373
+ return tuple (samples )
303
374
304
375
305
376
CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
@@ -320,7 +391,7 @@ def generate_latest(registry=REGISTRY):
320
391
for k , v in labels .items ()]))
321
392
else :
322
393
labelstr = ''
323
- output .append ('{0}{1} {2}\n ' .format (name , labelstr , value ))
394
+ output .append ('{0}{1} {2}\n ' .format (name , labelstr , _floatToGoString ( value ) ))
324
395
return '' .join (output ).encode ('utf-8' )
325
396
326
397
@@ -353,6 +424,9 @@ def write_to_textfile(path, registry):
353
424
s = Summary ('ss' , 'A summary' , ['a' , 'b' ])
354
425
s .labels ('c' , 'd' ).observe (17 )
355
426
427
+ h = Histogram ('hh' , 'A histogram' )
428
+ h .observe (.6 )
429
+
356
430
from BaseHTTPServer import HTTPServer
357
431
server_address = ('' , 8000 )
358
432
httpd = HTTPServer (server_address , MetricsHandler )
0 commit comments