1616from functools import wraps
1717from threading import Lock
1818
19- __all__ = ['Counter' , 'Gauge' , 'Summary' , 'CollectorRegistry ' ]
19+ __all__ = ['Counter' , 'Gauge' , 'Summary' , 'Histogram ' ]
2020
2121_METRIC_NAME_RE = re .compile (r'^[a-zA-Z_:][a-zA-Z0-9_:]*$' )
2222_METRIC_LABEL_NAME_RE = re .compile (r'^[a-zA-Z_:][a-zA-Z0-9_:]*$' )
2323_RESERVED_METRIC_LABEL_NAME_RE = re .compile (r'^__.*$' )
24+ _INF = float ("inf" )
25+ _MINUS_INF = float ("-inf" )
2426
2527
2628
@@ -71,7 +73,7 @@ def get_sample_value(self, name, labels=None):
7173REGISTRY = CollectorRegistry ()
7274'''The default registry.'''
7375
74- _METRIC_TYPES = ('counter' , 'gauge' , 'summary' , 'untyped' )
76+ _METRIC_TYPES = ('counter' , 'gauge' , 'summary' , 'histogram' , ' untyped' )
7577
7678class Metric (object ):
7779 '''A single metric and it's samples.'''
@@ -90,10 +92,11 @@ def add_sample(self, name, labels, value):
9092
9193class _LabelWrapper (object ):
9294 '''Handles labels for the wrapped metric.'''
93- def __init__ (self , wrappedClass , labelnames ):
95+ def __init__ (self , wrappedClass , labelnames , ** kwargs ):
9496 self ._wrappedClass = wrappedClass
9597 self ._type = wrappedClass ._type
9698 self ._labelnames = labelnames
99+ self ._kwargs = kwargs
97100 self ._lock = Lock ()
98101 self ._metrics = {}
99102
@@ -108,7 +111,7 @@ def labels(self, *labelvalues):
108111 labelvalues = tuple (labelvalues )
109112 with self ._lock :
110113 if labelvalues not in self ._metrics :
111- self ._metrics [labelvalues ] = self ._wrappedClass ()
114+ self ._metrics [labelvalues ] = self ._wrappedClass (** self . _kwargs )
112115 return self ._metrics [labelvalues ]
113116
114117 def remove (self , * labelvalues ):
@@ -129,7 +132,7 @@ def _samples(self):
129132
130133def _MetricWrapper (cls ):
131134 '''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 ):
133136 if labelnames :
134137 for l in labelnames :
135138 if not _METRIC_LABEL_NAME_RE .match (l ):
@@ -138,9 +141,9 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
138141 raise ValueError ('Reserved label metric name: ' + l )
139142 if l in cls ._reserved_labelnames :
140143 raise ValueError ('Reserved label metric name: ' + l )
141- collector = _LabelWrapper (cls , labelnames )
144+ collector = _LabelWrapper (cls , labelnames , ** kwargs )
142145 else :
143- collector = cls ()
146+ collector = cls (** kwargs )
144147
145148 full_name = ''
146149 if namespace :
@@ -159,7 +162,8 @@ def collect():
159162 return [metric ]
160163 collector .collect = collect
161164
162- registry .register (collector )
165+ if registry :
166+ registry .register (collector )
163167 return collector
164168
165169 return init
@@ -300,6 +304,73 @@ def _samples(self):
300304 ('_count' , {}, self ._count ),
301305 ('_sum' , {}, self ._sum ))
302306
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 )
303374
304375
305376CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
@@ -320,7 +391,7 @@ def generate_latest(registry=REGISTRY):
320391 for k , v in labels .items ()]))
321392 else :
322393 labelstr = ''
323- output .append ('{0}{1} {2}\n ' .format (name , labelstr , value ))
394+ output .append ('{0}{1} {2}\n ' .format (name , labelstr , _floatToGoString ( value ) ))
324395 return '' .join (output ).encode ('utf-8' )
325396
326397
@@ -353,6 +424,9 @@ def write_to_textfile(path, registry):
353424 s = Summary ('ss' , 'A summary' , ['a' , 'b' ])
354425 s .labels ('c' , 'd' ).observe (17 )
355426
427+ h = Histogram ('hh' , 'A histogram' )
428+ h .observe (.6 )
429+
356430 from BaseHTTPServer import HTTPServer
357431 server_address = ('' , 8000 )
358432 httpd = HTTPServer (server_address , MetricsHandler )
0 commit comments