Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add gsum/gcount to GaugeHistogram. #306

Merged
merged 14 commits into from
Oct 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions prometheus_client/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Timestamp(object):
def __init__(self, sec, nsec):
if nsec < 0 or nsec >= 1e9:
raise ValueError("Invalid value for nanoseconds in Timestamp: {}".format(nsec))
if sec < 0:
nsec = -nsec
self.sec = int(sec)
self.nsec = int(nsec)

Expand All @@ -64,6 +66,12 @@ def __float__(self):
def __eq__(self, other):
return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec

def __ne__(self, other):
return not self == other

def __gt__(self, other):
return self.sec > other.sec or self.nsec > other.nsec


Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp'])
Exemplar.__new__.__defaults__ = (None, )
Expand Down Expand Up @@ -122,6 +130,7 @@ def _get_names(self, collector):
'counter': ['_total', '_created'],
'summary': ['', '_sum', '_count', '_created'],
'histogram': ['_bucket', '_sum', '_count', '_created'],
'gaugehistogram': ['_bucket', '_gsum', '_gcount'],
'info': ['_info'],
}
for metric in desc_func():
Expand Down Expand Up @@ -391,29 +400,33 @@ class GaugeHistogramMetricFamily(Metric):

For use by custom collectors.
'''
def __init__(self, name, documentation, buckets=None, labels=None, unit=''):
def __init__(self, name, documentation, buckets=None, gsum_value=None, labels=None, unit=''):
Metric.__init__(self, name, documentation, 'gaugehistogram', unit)
if labels is not None and buckets is not None:
raise ValueError('Can only specify at most one of buckets and labels.')
if labels is None:
labels = []
self._labelnames = tuple(labels)
if buckets is not None:
self.add_metric([], buckets)
self.add_metric([], buckets, gsum_value)

def add_metric(self, labels, buckets, timestamp=None):
def add_metric(self, labels, buckets, gsum_value, timestamp=None):
'''Add a metric to the metric family.

Args:
labels: A list of label values
buckets: A list of pairs of bucket names and values.
The buckets must be sorted, and +Inf present.
gsum_value: The sum value of the metric.
'''
for bucket, value in buckets:
self.samples.append(Sample(
self.name + '_bucket',
dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
value, timestamp))
# +Inf is last and provides the count value.
self.samples.append(Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
self.samples.append(Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp))


class InfoMetricFamily(Metric):
Expand Down Expand Up @@ -465,7 +478,7 @@ def add_metric(self, labels, value, timestamp=None):
value: A dict of string state names to booleans
'''
labels = tuple(labels)
for state, enabled in value.items():
for state, enabled in sorted(value.items()):
v = (1 if enabled else 0)
self.samples.append(Sample(self.name,
dict(zip(self._labelnames + (self.name,), labels + (state,))), v, timestamp))
Expand Down
47 changes: 30 additions & 17 deletions prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ def start_wsgi_server(port, addr='', registry=core.REGISTRY):

def generate_latest(registry=core.REGISTRY):
'''Returns the metrics from the registry in latest text format as a string.'''

def sample_line(s):
if s.labels:
labelstr = '{{{0}}}'.format(','.join(
['{0}="{1}"'.format(
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
for k, v in sorted(s.labels.items())]))
else:
labelstr = ''
timestamp = ''
if s.timestamp is not None:
# Convert to milliseconds.
timestamp = ' {0:d}'.format(int(float(s.timestamp) * 1000))
return '{0}{1} {2}{3}\n'.format(
s.name, labelstr, core._floatToGoString(s.value), timestamp)

output = []
for metric in registry.collect():
mname = metric.name
Expand All @@ -86,32 +102,29 @@ def generate_latest(registry=core.REGISTRY):
elif mtype == 'unknown':
mtype = 'untyped'

output.append('# HELP {0} {1}'.format(
output.append('# HELP {0} {1}\n'.format(
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
output.append('\n# TYPE {0} {1}\n'.format(mname, mtype))
output.append('# TYPE {0} {1}\n'.format(mname, mtype))

om_samples = {}
for s in metric.samples:
if s.name == metric.name + '_created':
continue # Ignore OpenMetrics specific sample. TODO: Make these into a gauge.
if s.labels:
labelstr = '{{{0}}}'.format(','.join(
['{0}="{1}"'.format(
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
for k, v in sorted(s.labels.items())]))
for suffix in ['_created', '_gsum', '_gcount']:
if s.name == metric.name + suffix:
# OpenMetrics specific sample, put in a gauge at the end.
om_samples.setdefault(suffix, []).append(sample_line(s))
break
else:
labelstr = ''
timestamp = ''
if s.timestamp is not None:
# Convert to milliseconds.
timestamp = ' {0:d}'.format(int(float(s.timestamp) * 1000))
output.append('{0}{1} {2}{3}\n'.format(
s.name, labelstr, core._floatToGoString(s.value), timestamp))
output.append(sample_line(s))
for suffix, lines in sorted(om_samples.items()):
output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
output.extend(lines)
return ''.join(output).encode('utf-8')


def choose_encoder(accept_header):
accept_header = accept_header or ''
for accepted in accept_header.split(','):
if accepted == 'text/openmetrics; version=0.0.1':
if accepted == 'application/openmetrics-text; version=0.0.1':
return (openmetrics.exposition.generate_latest,
openmetrics.exposition.CONTENT_TYPE_LATEST)
return (generate_latest, CONTENT_TYPE_LATEST)
Expand Down
5 changes: 2 additions & 3 deletions prometheus_client/openmetrics/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .. import core

CONTENT_TYPE_LATEST = str('text/openmetrics; version=0.0.1; charset=utf-8')
CONTENT_TYPE_LATEST = str('application/openmetrics-text; version=0.0.1; charset=utf-8')
'''Content type of the latest OpenMetrics text format'''

def generate_latest(registry):
Expand All @@ -26,7 +26,7 @@ def generate_latest(registry):
else:
labelstr = ''
if s.exemplar:
if metric.type != 'histogram' or not s.name.endswith('_bucket'):
if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'):
raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name))
labels = '{{{0}}}'.format(','.join(
['{0}="{1}"'.format(
Expand All @@ -42,7 +42,6 @@ def generate_latest(registry):
exemplarstr = ''
timestamp = ''
if s.timestamp is not None:
# Convert to milliseconds.
timestamp = ' {0}'.format(s.timestamp)
output.append('{0}{1} {2}{3}{4}\n'.format(s.name, labelstr,
core._floatToGoString(s.value), timestamp, exemplarstr))
Expand Down
Loading