Repository: prometheus/client_python
Pull request #300 (2018-08-29 14:37:39)

Initial work on OpenMetrics
This should handle all valid inputs, but does not yet reject all invalid inputs.


Patch:
diff --git a/README.md b/README.md
index fa4e35b..a8a96b2 100644
--- a/README.md
+++ b/README.md
@@ -67,11 +67,16 @@ Counters go up, and reset when the process restarts.
 
 ```python
 from prometheus_client import Counter
-c = Counter('my_failures_total', 'Description of counter')
+c = Counter('my_failures', 'Description of counter')
 c.inc()     # Increment by 1
 c.inc(1.6)  # Increment by given value
 ```
 
+If there is a suffix of `_total` on the metric name, it will be removed. When
+exposing the time series for counter, a `_total` suffix will be added. This is
+for compatibility between OpenMetrics and the Prometheus text format, as OpenMetrics
+requires the `_total` suffix.
+
 There are utilities to count exceptions raised:
 
 ```python
@@ -169,6 +174,27 @@ with h.time():
   pass
 ```
 
+### Info
+
+Info tracks key-value information, usually about a whole target.
+
+```python
+from prometheus_client import Info
+i = Info('my_build_version', 'Description of info')
+i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
+```
+
+### Enum
+
+Enum tracks which of a set of states something is currently in.
+
+```python
+from prometheus_client import Enum
+e = Enum('my_task_state', 'Description of enum',
+        states=['starting', 'running', 'stopped'])
+e.state('running')
+```
+
 ### Labels
 
 All metrics can have labels, allowing grouping of related time series.
@@ -280,36 +306,6 @@ from prometheus_client import start_wsgi_server
 start_wsgi_server(8000)
 ```
 
-#### Flask
-
-To use Prometheus with [Flask](http://flask.pocoo.org/) we need to serve metrics through a Prometheus WSGI application. This can be achieved using [Flask's application dispatching](http://flask.pocoo.org/docs/latest/patterns/appdispatch/). Below is a working example.
-
-Save the snippet below in a `myapp.py` file
-
-```python
-from flask import Flask
-from werkzeug.wsgi import DispatcherMiddleware
-from prometheus_client import make_wsgi_app
-
-# Create my app
-app = Flask(__name__)
-
-# Add prometheus wsgi middleware to route /metrics requests
-app_dispatch = DispatcherMiddleware(app, {
-    '/metrics': make_wsgi_app()
-})
-```
-
-Run the example web application like this
-
-```bash
-# Install uwsgi if you do not have it
-pip install uwsgi
-uwsgi --http 127.0.0.1:8000 --wsgi-file myapp.py --callable app_dispatch
-```
-
-Visit http://localhost:8000/metrics to see the metrics
-
 ### Node exporter textfile collector
 
 The [textfile collector](https://github.com/prometheus/node_exporter#textfile-collector)
@@ -444,6 +440,7 @@ This comes with a number of limitations:
 
 - Registries can not be used as normal, all instantiated metrics are exported
 - Custom collectors do not work (e.g. cpu and memory metrics)
+- Info and Enum metrics do not work
 - The pushgateway cannot be used
 - Gauges cannot use the `pid` label
 
diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py
index 689a06d..8208d45 100644
--- a/prometheus_client/__init__.py
+++ b/prometheus_client/__init__.py
@@ -5,7 +5,7 @@ from . import exposition
 from . import process_collector
 from . import platform_collector
 
-__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram']
+__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram', 'Info', 'Enum']
 
 CollectorRegistry = core.CollectorRegistry
 REGISTRY = core.REGISTRY
@@ -14,6 +14,8 @@ Counter = core.Counter
 Gauge = core.Gauge
 Summary = core.Summary
 Histogram = core.Histogram
+Info = core.Info
+Enum = core.Enum
 
 CONTENT_TYPE_LATEST = exposition.CONTENT_TYPE_LATEST
 generate_latest = exposition.generate_latest
diff --git a/prometheus_client/bridge/graphite.py b/prometheus_client/bridge/graphite.py
index 6b8f241..c73d493 100755
--- a/prometheus_client/bridge/graphite.py
+++ b/prometheus_client/bridge/graphite.py
@@ -60,16 +60,16 @@ class GraphiteBridge(object):
             prefixstr = prefix + '.'
 
         for metric in self._registry.collect():
-            for name, labels, value in metric.samples:
-                if labels:
+            for s in metric.samples:
+                if s.labels:
                     labelstr = '.' + '.'.join(
                         ['{0}.{1}'.format(
                             _sanitize(k), _sanitize(v))
-                            for k, v in sorted(labels.items())])
+                            for k, v in sorted(s.labels.items())])
                 else:
                     labelstr = ''
                 output.append('{0}{1}{2} {3} {4}\n'.format(
-                    prefixstr, _sanitize(name), labelstr, float(value), now))
+                    prefixstr, _sanitize(s.name), labelstr, float(s.value), now))
 
         conn = socket.create_connection(self._address, self._timeout)
         conn.sendall(''.join(output).encode('ascii'))
diff --git a/prometheus_client/core.py b/prometheus_client/core.py
index a1c7233..da3034d 100644
--- a/prometheus_client/core.py
+++ b/prometheus_client/core.py
@@ -15,6 +15,7 @@ import types
 
 from threading import Lock
 from timeit import default_timer
+from collections import namedtuple
 
 from .decorator import decorate
 
@@ -34,6 +35,39 @@ _pack_double = struct.Struct(b'd').pack_into
 _unpack_integer = struct.Struct(b'i').unpack_from
 _unpack_double = struct.Struct(b'd').unpack_from
 
+# Timestamp and exemplar are optional.
+# Value can be an int or a float.
+# Timestamp can be a float containing a unixtime in seconds,
+# a Timestamp object, or None.
+# Exemplar can be an Exemplar object, or None.
+Sample = namedtuple('Sample', ['name', 'labels', 'value', 'timestamp', 'exemplar'])
+Sample.__new__.__defaults__ = (None, None)
+
+
+class Timestamp(object):
+    '''A nanosecond-resolution timestamp.'''
+    def __init__(self, sec, nsec):
+        if nsec < 0 or nsec >= 1e9:
+            raise ValueError("Invalid value for nanoseconds in Timestamp: {}".format(nsec))
+        self.sec = int(sec)
+        self.nsec = int(nsec)
+
+    def __str__(self):
+        return "{0}.{1:09d}".format(self.sec, self.nsec)
+
+    def __repr__(self):
+        return "Timestamp({0}, {1})".format(self.sec, self.nsec)
+
+    def __float__(self):
+        return float(self.sec) + float(self.nsec) / 1e9
+
+    def __eq__(self, other):
+        return type(self) == type(other) and self.sec == other.sec and self.nsec == other.nsec
+
+
+Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp'])
+Exemplar.__new__.__defaults__ = (None, )
+
 
 class CollectorRegistry(object):
     '''Metric collector registry.
@@ -55,7 +89,7 @@ class CollectorRegistry(object):
             duplicates = set(self._names_to_collectors).intersection(names)
             if duplicates:
                 raise ValueError(
-                    'Duplicated timeseries in CollectorRegistry: {}'.format(
+                    'Duplicated timeseries in CollectorRegistry: {0}'.format(
                         duplicates))
             for name in names:
                 self._names_to_collectors[name] = collector
@@ -85,8 +119,10 @@ class CollectorRegistry(object):
 
         result = []
         type_suffixes = {
-            'summary': ['', '_sum', '_count'],
-            'histogram': ['_bucket', '_sum', '_count']
+            'counter': ['_total', '_created'],
+            'summary': ['', '_sum', '_count', '_created'],
+            'histogram': ['_bucket', '_sum', '_count', '_created'],
+            'info': ['_info'],
         }
         for metric in desc_func():
             for suffix in type_suffixes.get(metric.type, ['']):
@@ -140,16 +176,17 @@ class CollectorRegistry(object):
         if labels is None:
             labels = {}
         for metric in self.collect():
-            for n, l, value in metric.samples:
-                if n == name and l == labels:
-                    return value
+            for s in metric.samples:
+                if s.name == name and s.labels == labels:
+                    return s.value
         return None
 
 
 REGISTRY = CollectorRegistry(auto_describe=True)
 '''The default registry.'''
 
-_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped')
+_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 
+        'gaugehistogram', 'unknown', 'info', 'stateset')
 
 
 class Metric(object):
@@ -160,37 +197,50 @@ class Metric(object):
     Custom collectors should use GaugeMetricFamily, CounterMetricFamily
     and SummaryMetricFamily instead.
     '''
-    def __init__(self, name, documentation, typ):
+    def __init__(self, name, documentation, typ, unit=''):
+        if unit and not name.endswith("_" + unit):
+            name += "_" + unit
+        if not _METRIC_NAME_RE.match(name):
+            raise ValueError('Invalid metric name: ' + name)
         self.name = name
         self.documentation = documentation
+        self.unit = unit
+        if typ == 'untyped':
+            typ = 'unknown'
         if typ not in _METRIC_TYPES:
             raise ValueError('Invalid metric type: ' + typ)
         self.type = typ
+        if unit:
+            if not name.endswith('_' + unit):
+                raise ValueError('Metric name does not end with unit: ' + name)
+        self.unit = unit
         self.samples = []
 
-    def add_sample(self, name, labels, value):
+    def add_sample(self, name, labels, value, timestamp=None, exemplar=None):
         '''Add a sample to the metric.
 
         Internal-only, do not use.'''
-        self.samples.append((name, labels, value))
+        self.samples.append(Sample(name, labels, value, timestamp, exemplar))
 
     def __eq__(self, other):
         return (isinstance(other, Metric) and
                 self.name == other.name and
                 self.documentation == other.documentation and
                 self.type == other.type and
+                self.unit == other.unit and
                 self.samples == other.samples)
 
     def __repr__(self):
-        return "Metric(%s, %s, %s, %s)" % (self.name, self.documentation,
-            self.type, self.samples)
+        return "Metric(%s, %s, %s, %s, %s)" % (self.name, self.documentation,
+            self.type, self.unit, self.samples)
+
 
-class UntypedMetricFamily(Metric):
-    '''A single untyped metric and its samples.
+class UnknownMetricFamily(Metric):
+    '''A single unknwon metric and its samples.
     For use by custom collectors.
     '''
-    def __init__(self, name, documentation, value=None, labels=None):
-        Metric.__init__(self, name, documentation, 'untyped')
+    def __init__(self, name, documentation, value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'unknown', unit)
         if labels is not None and value is not None:
             raise ValueError('Can only specify at most one of value and labels.')
         if labels is None:
@@ -199,38 +249,46 @@ class UntypedMetricFamily(Metric):
         if value is not None:
             self.add_metric([], value)
 
-    def add_metric(self, labels, value):
+    def add_metric(self, labels, value, timestamp=None):
         '''Add a metric to the metric family.
         Args:
         labels: A list of label values
         value: The value of the metric.
         '''
-        self.samples.append((self.name, dict(zip(self._labelnames, labels)), value))
+        self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
 
+# For backward compatibility.
+UntypedMetricFamily = UnknownMetricFamily
 
 class CounterMetricFamily(Metric):
     '''A single counter and its samples.
 
     For use by custom collectors.
     '''
-    def __init__(self, name, documentation, value=None, labels=None):
-        Metric.__init__(self, name, documentation, 'counter')
+    def __init__(self, name, documentation, value=None, labels=None, created=None, unit=''):
+        # Glue code for pre-OpenMetrics metrics.
+        if name.endswith('_total'):
+           name = name[:-6]
+        Metric.__init__(self, name, documentation, 'counter', unit)
         if labels is not None and value is not None:
             raise ValueError('Can only specify at most one of value and labels.')
         if labels is None:
             labels = []
         self._labelnames = tuple(labels)
         if value is not None:
-            self.add_metric([], value)
+            self.add_metric([], value, created)
 
-    def add_metric(self, labels, value):
+    def add_metric(self, labels, value, created=None, timestamp=None):
         '''Add a metric to the metric family.
 
         Args:
           labels: A list of label values
-          value: The value of the metric.
+          value: The value of the metric
+          created: Optional unix timestamp the child was created at.
         '''
-        self.samples.append((self.name, dict(zip(self._labelnames, labels)), value))
+        self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp))
+        if created is not None:
+            self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
 
 
 class GaugeMetricFamily(Metric):
@@ -238,8 +296,8 @@ class GaugeMetricFamily(Metric):
 
     For use by custom collectors.
     '''
-    def __init__(self, name, documentation, value=None, labels=None):
-        Metric.__init__(self, name, documentation, 'gauge')
+    def __init__(self, name, documentation, value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'gauge', unit)
         if labels is not None and value is not None:
             raise ValueError('Can only specify at most one of value and labels.')
         if labels is None:
@@ -248,14 +306,14 @@ class GaugeMetricFamily(Metric):
         if value is not None:
             self.add_metric([], value)
 
-    def add_metric(self, labels, value):
+    def add_metric(self, labels, value, timestamp=None):
         '''Add a metric to the metric family.
 
         Args:
           labels: A list of label values
           value: A float
         '''
-        self.samples.append((self.name, dict(zip(self._labelnames, labels)), value))
+        self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
 
 
 class SummaryMetricFamily(Metric):
@@ -263,8 +321,8 @@ class SummaryMetricFamily(Metric):
 
     For use by custom collectors.
     '''
-    def __init__(self, name, documentation, count_value=None, sum_value=None, labels=None):
-        Metric.__init__(self, name, documentation, 'summary')
+    def __init__(self, name, documentation, count_value=None, sum_value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'summary', unit)
         if (sum_value is None) != (count_value is None):
             raise ValueError('count_value and sum_value must be provided together.')
         if labels is not None and count_value is not None:
@@ -275,7 +333,7 @@ class SummaryMetricFamily(Metric):
         if count_value is not None:
             self.add_metric([], count_value, sum_value)
 
-    def add_metric(self, labels, count_value, sum_value):
+    def add_metric(self, labels, count_value, sum_value, timestamp=None):
         '''Add a metric to the metric family.
 
         Args:
@@ -283,8 +341,8 @@ class SummaryMetricFamily(Metric):
           count_value: The count value of the metric.
           sum_value: The sum value of the metric.
         '''
-        self.samples.append((self.name + '_count', dict(zip(self._labelnames, labels)), count_value))
-        self.samples.append((self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value))
+        self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp))
+        self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
 
 
 class HistogramMetricFamily(Metric):
@@ -292,8 +350,8 @@ class HistogramMetricFamily(Metric):
 
     For use by custom collectors.
     '''
-    def __init__(self, name, documentation, buckets=None, sum_value=None, labels=None):
-        Metric.__init__(self, name, documentation, 'histogram')
+    def __init__(self, name, documentation, buckets=None, sum_value=None, labels=None, unit=''):
+        Metric.__init__(self, name, documentation, 'histogram', unit)
         if (sum_value is None) != (buckets is None):
             raise ValueError('buckets and sum_value must be provided together.')
         if labels is not None and buckets is not None:
@@ -304,20 +362,113 @@ class HistogramMetricFamily(Metric):
         if buckets is not None:
             self.add_metric([], buckets, sum_value)
 
-    def add_metric(self, labels, buckets, sum_value):
+    def add_metric(self, labels, buckets, sum_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.
+          buckets: A list of lists.
+              Each inner list can be a pair of bucket name and value,
+              or a triple of bucket name, value, and exemplar.
               The buckets must be sorted, and +Inf present.
           sum_value: The sum value of the metric.
         '''
-        for bucket, value in buckets:
-            self.samples.append((self.name + '_bucket', dict(list(zip(self._labelnames, labels)) + [('le', bucket)]), value))
+        for b in buckets:
+            bucket, value = b[:2]
+            exemplar = None
+            if len(b) == 3:
+                exemplar = b[2]
+            self.samples.append(Sample(self.name + '_bucket', 
+                dict(list(zip(self._labelnames, labels)) + [('le', bucket)]), 
+                value, timestamp, exemplar))
         # +Inf is last and provides the count value.
-        self.samples.append((self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1]))
-        self.samples.append((self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value))
+        self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
+        self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
+
+
+class GaugeHistogramMetricFamily(Metric):
+    '''A single gauge histogram and its samples.
+
+    For use by custom collectors.
+    '''
+    def __init__(self, name, documentation, buckets=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)
+
+    def add_metric(self, labels, buckets, 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.
+        '''
+        for bucket, value in buckets:
+            self.samples.append(Sample(
+                self.name + '_bucket', 
+                dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
+                value, timestamp))
+
+
+class InfoMetricFamily(Metric):
+    '''A single info and its samples.
+
+    For use by custom collectors.
+    '''
+    def __init__(self, name, documentation, value=None, labels=None):
+        Metric.__init__(self, name, documentation, 'info')
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        '''Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: A dict of labels
+        '''
+        self.samples.append(Sample(self.name + '_info', 
+            dict(dict(zip(self._labelnames, labels)), **value), 1, timestamp))
+
+
+class StateSetMetricFamily(Metric):
+    '''A single stateset and its samples.
+
+    For use by custom collectors.
+    '''
+    def __init__(self, name, documentation, value=None, labels=None):
+        Metric.__init__(self, name, documentation, 'stateset')
+        if labels is not None and value is not None:
+            raise ValueError('Can only specify at most one of value and labels.')
+        if labels is None:
+            labels = []
+        self._labelnames = tuple(labels)
+        if value is not None:
+            self.add_metric([], value)
+
+    def add_metric(self, labels, value, timestamp=None):
+        '''Add a metric to the metric family.
+
+        Args:
+          labels: A list of label values
+          value: A dict of string state names to booleans
+        '''
+        labels = tuple(labels)
+        for state, enabled in value.items():
+            v = (1 if enabled else 0)
+            self.samples.append(Sample(self.name,
+                dict(zip(self._labelnames + (self.name,), labels + (state,))), v, timestamp))
 
 
 class _MutexValue(object):
@@ -584,7 +735,7 @@ class _LabelWrapper(object):
 
 def _MetricWrapper(cls):
     '''Provides common functionality for metrics.'''
-    def init(name, documentation, labelnames=(), namespace='', subsystem='', registry=REGISTRY, **kwargs):
+    def init(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY, **kwargs):
         full_name = ''
         if namespace:
             full_name += namespace + '_'
@@ -592,6 +743,14 @@ def _MetricWrapper(cls):
             full_name += subsystem + '_'
         full_name += name
 
+        if unit and not full_name.endswith("_" + unit):
+            full_name += "_" + unit
+        if unit and cls._type in ('info', 'stateset'):
+            raise ValueError('Metric name is of a type that cannot have a unit: ' + full_name)
+
+        if cls._type == 'counter' and full_name.endswith('_total'):
+            full_name = full_name[:-6]  # Munge to OpenMetrics.
+
         if labelnames:
             labelnames = tuple(labelnames)
             for l in labelnames:
@@ -613,7 +772,7 @@ def _MetricWrapper(cls):
         collector.describe = describe
 
         def collect():
-            metric = Metric(full_name, documentation, cls._type)
+            metric = Metric(full_name, documentation, cls._type, unit)
             for suffix, labels, value in collector._samples():
                 metric.add_sample(full_name + suffix, labels, value)
             return [metric]
@@ -664,7 +823,10 @@ class Counter(object):
     _reserved_labelnames = []
 
     def __init__(self, name, labelnames, labelvalues):
-        self._value = _ValueClass(self._type, name, name, labelnames, labelvalues)
+        if name.endswith('_total'):
+           name = name[:-6]
+        self._value = _ValueClass(self._type, name, name + '_total', labelnames, labelvalues)
+        self._created = time.time()
 
     def inc(self, amount=1):
         '''Increment counter by the given amount.'''
@@ -682,7 +844,8 @@ class Counter(object):
         return _ExceptionCounter(self, exception)
 
     def _samples(self):
-        return (('', {}, self._value.get()), )
+        return (('_total', {}, self._value.get()),
+                ('_created', {}, self._created))
 
 
 @_MetricWrapper
@@ -818,6 +981,7 @@ class Summary(object):
     def __init__(self, name, labelnames, labelvalues):
         self._count = _ValueClass(self._type, name, name + '_count', labelnames, labelvalues)
         self._sum = _ValueClass(self._type, name, name + '_sum', labelnames, labelvalues)
+        self._created = time.time()
 
     def observe(self, amount):
         '''Observe the given amount.'''
@@ -834,7 +998,8 @@ class Summary(object):
     def _samples(self):
         return (
             ('_count', {}, self._count.get()),
-            ('_sum', {}, self._sum.get()))
+            ('_sum', {}, self._sum.get()),
+            ('_created', {}, self._created))
 
 
 def _floatToGoString(d):
@@ -883,13 +1048,12 @@ class Histogram(object):
 
     The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
     They can be overridden by passing `buckets` keyword argument to `Histogram`.
-
-    **NB** The Python client doesn't store or expose quantile information at this time.
     '''
     _type = 'histogram'
-    _reserved_labelnames = ['histogram']
+    _reserved_labelnames = ['le']
 
     def __init__(self, name, labelnames, labelvalues, buckets=(.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, _INF)):
+        self._created = time.time()
         self._sum = _ValueClass(self._type, name, name + '_sum', labelnames, labelvalues)
         buckets = [float(b) for b in buckets]
         if buckets != sorted(buckets):
@@ -929,9 +1093,87 @@ class Histogram(object):
             samples.append(('_bucket', {'le': _floatToGoString(bound)}, acc))
         samples.append(('_count', {}, acc))
         samples.append(('_sum', {}, self._sum.get()))
+        samples.append(('_created', {}, self._created))
         return tuple(samples)
 
 
+@_MetricWrapper
+class Info(object):
+    '''Info metric, key-value pairs.
+
+     Examples of Info include:
+        - Build information
+        - Version information
+        - Potential target metadata
+
+     Example usage:
+        from prometheus_client import Info
+
+        i = Info('my_build', 'Description of info')
+        i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
+
+     Info metrics do not work in multiprocess mode.
+    '''
+    _type = 'info'
+    _reserved_labelnames = []
+
+    def __init__(self, name, labelnames, labelvalues):
+        self._labelnames = set(labelnames)
+        self._lock = Lock()
+        self._value = {}
+
+    def info(self, val):
+        '''Set info metric.'''
+        if self._labelnames.intersection(val.keys()):
+            raise ValueError('Overlapping labels for Info metric, metric: %s child: %s' % (
+                self._labelnames, val))
+        with self._lock:
+            self._value = dict(val)
+
+
+    def _samples(self):
+        with self._lock:
+            return (('_info', self._value, 1.0,), )
+
+
+@_MetricWrapper
+class Enum(object):
+    '''Enum metric, which of a set of states is true.
+
+     Example usage:
+        from prometheus_client import Enum
+
+        e = Enum('task_state', 'Description of enum', 
+          states=['starting', 'running', 'stopped'])
+        e.state('running')
+
+     The first listed state will be the default.
+     Enum metrics do not work in multiprocess mode.
+    '''
+    _type = 'stateset'
+    _reserved_labelnames = []
+
+    def __init__(self, name, labelnames, labelvalues, states=None):
+        if name in labelnames:
+            raise ValueError('Overlapping labels for Enum metric: %s' % (name,))
+        if not states:
+            raise ValueError('No states provided for Enum metric: %s' % (name,))
+        self._name = name
+        self._states = states
+        self._value = 0
+        self._lock = Lock()
+
+    def state(self, state):
+        '''Set enum metric state.'''
+        with self._lock:
+            self._value = self._states.index(state)
+
+    def _samples(self):
+        with self._lock:
+            return [('', {self._name: s}, 1 if i == self._value else 0,)
+                       for i, s in enumerate(self._states)]
+
+
 class _ExceptionCounter(object):
     def __init__(self, counter, exception):
         self._counter = counter
diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py
index 96661e0..1ebeba2 100644
--- a/prometheus_client/exposition.py
+++ b/prometheus_client/exposition.py
@@ -10,7 +10,8 @@ import threading
 from contextlib import closing
 from wsgiref.simple_server import make_server, WSGIRequestHandler
 
-from . import core
+from prometheus_client import core
+from prometheus_client import openmetrics
 try:
     from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
     from SocketServer import ThreadingMixIn
@@ -36,12 +37,13 @@ def make_wsgi_app(registry=core.REGISTRY):
     def prometheus_app(environ, start_response):
         params = parse_qs(environ.get('QUERY_STRING', ''))
         r = registry
+        encoder, content_type = choose_encoder(environ.get['HTTP_ACCEPT'])
         if 'name[]' in params:
             r = r.restricted_registry(params['name[]'])
-        output = generate_latest(r)
+        output = encoder(r)
 
         status = str('200 OK')
-        headers = [(str('Content-type'), CONTENT_TYPE_LATEST)]
+        headers = [(str('Content-type'), content_type)]
         start_response(status, headers)
         return [output]
     return prometheus_app
@@ -67,21 +69,54 @@ def generate_latest(registry=core.REGISTRY):
     '''Returns the metrics from the registry in latest text format as a string.'''
     output = []
     for metric in registry.collect():
+        mname = metric.name
+        mtype = metric.type
+        # Munging from OpenMetrics into Prometheus format.
+        if mtype == 'counter':
+            mname = mname + '_total'
+        elif mtype == 'info':
+            mname = mname + '_info'
+            mtype = 'gauge'
+        elif mtype == 'stateset':
+            mtype = 'gauge'
+        elif mtype == 'gaugehistogram':
+            # A gauge histogram is really a gauge,
+            # but this captures the strucutre better.
+            mtype = 'histogram'
+        elif mtype == 'unknown':
+            mtype = 'untyped'
+
         output.append('# HELP {0} {1}'.format(
-            metric.name, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
-        output.append('\n# TYPE {0} {1}\n'.format(metric.name, metric.type))
-        for name, labels, value in metric.samples:
-            if labels:
+            mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
+        output.append('\n# TYPE {0} {1}\n'.format(mname, mtype))
+        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(labels.items())]))
+                     for k, v in sorted(s.labels.items())]))
             else:
                 labelstr = ''
-            output.append('{0}{1} {2}\n'.format(name, labelstr, core._floatToGoString(value)))
+            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))
     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':
+            return (openmetrics.exposition.generate_latest, 
+                    openmetrics.exposition.CONTENT_TYPE_LATEST)
+    return (generate_latest, CONTENT_TYPE_LATEST)
+
+
 class MetricsHandler(BaseHTTPRequestHandler):
     """HTTP handler that gives metrics from ``core.REGISTRY``."""
     registry = core.REGISTRY
@@ -89,15 +124,16 @@ class MetricsHandler(BaseHTTPRequestHandler):
     def do_GET(self):
         registry = self.registry
         params = parse_qs(urlparse(self.path).query)
+        encoder, content_type = choose_encoder(self.headers.get('Accept'))
         if 'name[]' in params:
             registry = registry.restricted_registry(params['name[]'])
         try:
-            output = generate_latest(registry)
+            output = encoder(registry)
         except:
             self.send_error(500, 'error generating metric output')
             raise
         self.send_response(200)
-        self.send_header('Content-Type', CONTENT_TYPE_LATEST)
+        self.send_header('Content-Type', content_type)
         self.end_headers()
         self.wfile.write(output)
 
diff --git a/prometheus_client/multiprocess.py b/prometheus_client/multiprocess.py
index b605a8f..7dd74b2 100644
--- a/prometheus_client/multiprocess.py
+++ b/prometheus_client/multiprocess.py
@@ -48,17 +48,18 @@ class MultiProcessCollector(object):
         for metric in metrics.values():
             samples = defaultdict(float)
             buckets = {}
-            for name, labels, value in metric.samples:
+            for s in metric.samples:
+                name, labels, value = s.name, s.labels, s.value
                 if metric.type == 'gauge':
                     without_pid = tuple(l for l in labels if l[0] != 'pid')
                     if metric._multiprocess_mode == 'min':
                         current = samples.setdefault((name, without_pid), value)
                         if value < current:
-                            samples[(name, without_pid)] = value
+                            samples[(s.name, without_pid)] = value
                     elif metric._multiprocess_mode == 'max':
                         current = samples.setdefault((name, without_pid), value)
                         if value > current:
-                            samples[(name, without_pid)] = value
+                            samples[(s.name, without_pid)] = value
                     elif metric._multiprocess_mode == 'livesum':
                         samples[(name, without_pid)] += value
                     else:  # all/liveall
@@ -74,11 +75,11 @@ class MultiProcessCollector(object):
                         buckets[without_le][bucket[0]] += value
                     else:
                         # _sum/_count
-                        samples[(name, labels)] += value
+                        samples[(s.name, labels)] += value
 
                 else:
                     # Counter and Summary.
-                    samples[(name, labels)] += value
+                    samples[(s.name, labels)] += value
 
             # Accumulate bucket values.
             if metric.type == 'histogram':
@@ -90,7 +91,7 @@ class MultiProcessCollector(object):
                     samples[(metric.name + '_count', labels)] = acc
 
             # Convert to correct sample format.
-            metric.samples = [(name, dict(labels), value) for (name, labels), value in samples.items()]
+            metric.samples = [core.Sample(name, dict(labels), value) for (name, labels), value in samples.items()]
         return metrics.values()
 
 
diff --git a/prometheus_client/openmetrics/__init__.py b/prometheus_client/openmetrics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py
new file mode 100644
index 0000000..8b37867
--- /dev/null
+++ b/prometheus_client/openmetrics/exposition.py
@@ -0,0 +1,51 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+from .. import core
+
+CONTENT_TYPE_LATEST = str('text/openmetrics; version=0.0.1; charset=utf-8')
+'''Content type of the latest OpenMetrics text format'''
+
+def generate_latest(registry):
+    '''Returns the metrics from the registry in latest text format as a string.'''
+    output = []
+    for metric in registry.collect():
+        mname = metric.name
+        output.append('# HELP {0} {1}\n'.format(
+            mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
+        output.append('# TYPE {0} {1}\n'.format(mname, metric.type))
+        if metric.unit:
+            output.append('# UNIT {0} {1}\n'.format(mname, metric.unit))
+        for s in metric.samples:
+            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 = ''
+            if s.exemplar:
+                if metric.type != 'histogram' 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(
+                     k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+                     for k, v in sorted(s.exemplar.labels.items())]))
+                if s.exemplar.timestamp is not None:
+                    exemplarstr = ' # {0} {1} {2}'.format(labels, 
+                            core._floatToGoString(s.exemplar.value), s.exemplar.timestamp)
+                else:
+                    exemplarstr = ' # {0} {1}'.format(labels, 
+                            core._floatToGoString(s.exemplar.value))
+            else:
+                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))
+    output.append('# EOF\n')
+    return ''.join(output).encode('utf-8')
+
diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py
new file mode 100644
index 0000000..8edf3a6
--- /dev/null
+++ b/prometheus_client/openmetrics/parser.py
@@ -0,0 +1,339 @@
+#!/usr/bin/python
+
+from __future__ import unicode_literals
+
+try:
+    import StringIO
+except ImportError:
+    # Python 3
+    import io as StringIO
+
+from .. import core
+
+
+def text_string_to_metric_families(text):
+    """Parse Openmetrics text format from a unicode string.
+
+    See text_fd_to_metric_families.
+    """
+    for metric_family in text_fd_to_metric_families(StringIO.StringIO(text)):
+        yield metric_family
+
+
+def _unescape_help(text):
+    result = []
+    slash = False
+
+    for char in text:
+        if slash:
+            if char == '\\':
+                result.append('\\')
+            elif char == '"':
+                result.append('"')
+            elif char == 'n':
+                result.append('\n')
+            else:
+                result.append('\\' + char)
+            slash = False
+        else:
+            if char == '\\':
+                slash = True
+            else:
+                result.append(char)
+
+    if slash:
+        result.append('\\')
+
+    return ''.join(result)
+
+
+def _parse_value(value):
+    value = ''.join(value)
+    if value != value.strip():
+        raise ValueError("Invalid value: {0!r}".format(value))
+    try:
+        return int(value)
+    except ValueError:
+        return float(value)
+
+
+def _parse_timestamp(timestamp):
+    timestamp = ''.join(timestamp)
+    if not timestamp:
+        return None
+    if timestamp != timestamp.strip():
+        raise ValueError("Invalid timestamp: {0!r}".format(timestamp))
+    try:
+        # Simple int.
+        return core.Timestamp(int(timestamp), 0)
+    except ValueError:
+        try:
+            # aaaa.bbbb. Nanosecond resolution supported.
+            parts = timestamp.split('.', 1)
+            return core.Timestamp(int(parts[0]), int(parts[1][:9].ljust(9, "0")))
+        except ValueError:
+            # Float.
+            return float(timestamp)
+
+
+def _parse_labels(it, text):
+    # The { has already been parsed.
+    state  = 'startoflabelname'
+    labelname = []
+    labelvalue = []
+    labels = {}
+
+    for char in it:
+        if state == 'startoflabelname':
+            if char == '}':
+                state = 'endoflabels'
+            else:
+                state = 'labelname'
+                labelname.append(char)
+        elif state == 'labelname':
+            if char == '=':
+                state = 'labelvaluequote'
+            else:
+                labelname.append(char)
+        elif state == 'labelvaluequote':
+            if char == '"':
+                state = 'labelvalue'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'labelvalue':
+            if char == '\\':
+                state = 'labelvalueslash'
+            elif char == '"':
+                if not core._METRIC_LABEL_NAME_RE.match(''.join(labelname)):
+                    raise ValueError("Invalid line: " + text)
+                labels[''.join(labelname)] = ''.join(labelvalue)
+                labelname = []
+                labelvalue = []
+                state = 'endoflabelvalue'
+            else:
+                labelvalue.append(char)
+        elif state == 'endoflabelvalue':
+            if char == ',':
+                state = 'labelname'
+            elif char == '}':
+                state = 'endoflabels'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'labelvalueslash':
+            state = 'labelvalue'
+            if char == '\\':
+                labelvalue.append('\\')
+            elif char == 'n':
+                labelvalue.append('\n')
+            elif char == '"':
+                labelvalue.append('"')
+            else:
+                labelvalue.append('\\' + char)
+        elif state == 'endoflabels':
+            if char == ' ':
+                break
+            else:
+                raise ValueError("Invalid line: " + text)
+    return labels
+
+
+def _parse_sample(text):
+    name = []
+    value = []
+    timestamp = []
+    labels = {}
+    exemplar_value = []
+    exemplar_timestamp = []
+    exemplar_labels = None
+
+    state = 'name'
+
+    it = iter(text)
+    for char in it:
+        if state == 'name':
+            if char == '{':
+                labels = _parse_labels(it, text)
+                # Space has already been parsed.
+                state = 'value'
+            elif char == ' ':
+                state = 'value'
+            else:
+                name.append(char)
+        elif state == 'value':
+            if char == ' ':
+                state = 'timestamp'
+            else:
+                value.append(char)
+        elif state == 'timestamp':
+            if char == '#' and not timestamp:
+                state = 'exemplarspace'
+            elif char == ' ':
+                state = 'exemplarhash'
+            else:
+                timestamp.append(char)
+        elif state == 'exemplarhash':
+            if char == '#':
+                state = 'exemplarspace'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarspace':
+            if char == ' ':
+                state = 'exemplarstartoflabels'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarstartoflabels':
+            if char == '{':
+                exemplar_labels = _parse_labels(it, text)
+                # Space has already been parsed.
+                state = 'exemplarvalue'
+            else:
+                raise ValueError("Invalid line: " + text)
+        elif state == 'exemplarvalue':
+            if char == ' ':
+                state = 'exemplartimestamp'
+            else:
+                exemplar_value.append(char)
+        elif state == 'exemplartimestamp':
+            exemplar_timestamp.append(char)
+
+    # Trailing space after value.
+    if state == 'timestamp' and not timestamp:
+        raise ValueError("Invalid line: " + text)
+
+    # Trailing space after value.
+    if state == 'exemplartimestamp' and not exemplar_timestamp:
+        raise ValueError("Invalid line: " + text)
+
+    # Incomplete exemplar.
+    if state in ['exemplarhash', 'exemplarspace', 'exemplarstartoflabels']:
+        raise ValueError("Invalid line: " + text)
+
+    if not value:
+        raise ValueError("Invalid line: " + text)
+    value = ''.join(value)
+    val = _parse_value(value)
+    ts = _parse_timestamp(timestamp)
+    exemplar = None
+    if exemplar_labels is not None:
+        exemplar = core.Exemplar(exemplar_labels,
+                _parse_value(exemplar_value),
+                _parse_timestamp(exemplar_timestamp))
+
+    return core.Sample(''.join(name), labels, val, ts, exemplar)
+    
+
+def text_fd_to_metric_families(fd):
+    """Parse Prometheus text format from a file descriptor.
+
+    This is a laxer parser than the main Go parser,
+    so successful parsing does not imply that the parsed
+    text meets the specification.
+
+    Yields core.Metric's.
+    """
+    name = ''
+    documentation = ''
+    typ = 'untyped'
+    unit = ''
+    samples = []
+    allowed_names = []
+    eof = False
+
+    seen_metrics = set()
+    def build_metric(name, documentation, typ, unit, samples):
+        if name in seen_metrics:
+            raise ValueError("Duplicate metric: " + name)
+        seen_metrics.add(name)
+        if unit and not name.endswith("_" + unit):
+            raise ValueError("Unit does not match metric name: " + name)
+        if unit and typ in ['info', 'stateset']:
+            raise ValueError("Units not allowed for this metric type: " + name)
+        metric = core.Metric(name, documentation, typ, unit)
+        # TODO: check labelvalues are valid utf8
+        # TODO: check only histogram buckets have exemplars.
+        # TODO: check samples are appropriately grouped and ordered
+        # TODO: check info/stateset values are 1/0
+        # TODO: check for metadata in middle of samples
+        # TODO: Check histogram bucket rules being followed
+        # TODO: Check for duplicate metrics
+        # TODO: Check for dupliate samples
+        # TODO: Check for decresing timestamps
+        metric.samples = samples
+        return metric
+
+    for line in fd:
+        if line[-1] == '\n':
+          line = line[:-1]
+
+        if eof:
+            raise ValueError("Received line after # EOF: " + line)
+
+        if line == '# EOF':
+            eof = True
+        elif line.startswith('#'):
+            parts = line.split(' ', 3)
+            if len(parts) < 4:
+                raise ValueError("Invalid line: " + line)
+            if parts[1] == 'HELP':
+                if parts[2] != name:
+                    if name != '':
+                        yield build_metric(name, documentation, typ, unit, samples)
+                    # New metric
+                    name = parts[2]
+                    unit = ''
+                    typ = 'untyped'
+                    samples = []
+                    allowed_names = [parts[2]]
+                if len(parts) == 4:
+                    documentation = _unescape_help(parts[3])
+                elif len(parts) == 3:
+                    raise ValueError("Invalid line: " + line)
+            elif parts[1] == 'TYPE':
+                if parts[2] != name:
+                    if name != '':
+                        yield build_metric(name, documentation, typ, unit, samples)
+                    # New metric
+                    name = parts[2]
+                    documentation = ''
+                    unit = ''
+                    samples = []
+                typ = parts[3]
+                allowed_names = {
+                    'counter': ['_total', '_created'],
+                    'summary': ['_count', '_sum', '', '_created'],
+                    'histogram': ['_count', '_sum', '_bucket', 'created'],
+                    'gaugehistogram': ['_bucket'],
+                }.get(typ, [''])
+                allowed_names = [name + n for n in allowed_names]
+            elif parts[1] == 'UNIT':
+                if parts[2] != name:
+                    if name != '':
+                        yield build_metric(name, documentation, typ, unit, samples)
+                    # New metric
+                    name = parts[2]
+                    typ = 'untyped'
+                    samples = []
+                    allowed_names = [parts[2]]
+                unit = parts[3]
+            else:
+                raise ValueError("Invalid line: " + line)
+        else:
+            sample = _parse_sample(line)
+            if sample[0] not in allowed_names:
+                if name != '':
+                    yield build_metric(name, documentation, typ, unit, samples)
+                # Start an untyped metric.
+                name = sample[0]
+                documentation = ''
+                unit = ''
+                typ = 'untyped'
+                samples = [sample]
+                allowed_names = [sample[0]]
+            else:
+                samples.append(sample)
+
+    if name != '':
+        yield build_metric(name, documentation, typ, unit, samples)
+
+    if not eof:
+        raise ValueError("Missing # EOF at end")
diff --git a/prometheus_client/parser.py b/prometheus_client/parser.py
index 2fea395..06b81eb 100755
--- a/prometheus_client/parser.py
+++ b/prometheus_client/parser.py
@@ -125,7 +125,7 @@ def _parse_sample(text):
         label = text[label_start + 1:label_end]
         # The value is after the label end (ignoring curly brace and space)
         value = float(_parse_value(text[label_end + 2:]))
-        return name, _parse_labels(label), value
+        return core.Sample(name, _parse_labels(label), value)
 
     # We don't have labels
     except ValueError:
@@ -137,7 +137,7 @@ def _parse_sample(text):
         name = text[:name_end]
         # The value is after the name
         value = float(_parse_value(text[name_end:]))
-        return name, {}, value
+        return core.Sample(name, {}, value)
 
 
 def text_fd_to_metric_families(fd):
@@ -156,6 +156,16 @@ def text_fd_to_metric_families(fd):
     allowed_names = []
 
     def build_metric(name, documentation, typ, samples):
+        # Munge counters into OpenMetrics representation
+        # used internally.
+        if typ == 'counter': 
+            if name.endswith('_total'):
+                name = name[:-6]
+            else:
+                new_samples = []
+                for s in samples:
+                    new_samples.append(tuple((s[0] + '_total', ) + s[1:]))
+                    samples = new_samples
         metric = core.Metric(name, documentation, typ)
         metric.samples = samples
         return metric
@@ -204,7 +214,7 @@ def text_fd_to_metric_families(fd):
             pass
         else:
             sample = _parse_sample(line)
-            if sample[0] not in allowed_names:
+            if sample.name not in allowed_names:
                 if name != '':
                     yield build_metric(name, documentation, typ, samples)
                 # New metric, yield immediately as untyped singleton
diff --git a/prometheus_client/twisted/_exposition.py b/prometheus_client/twisted/_exposition.py
index 66c548c..714a7f9 100644
--- a/prometheus_client/twisted/_exposition.py
+++ b/prometheus_client/twisted/_exposition.py
@@ -1,5 +1,5 @@
 from __future__ import absolute_import, unicode_literals
-from .. import REGISTRY, generate_latest, CONTENT_TYPE_LATEST
+from .. import REGISTRY, exposition
 
 from twisted.web.resource import Resource
 
@@ -14,5 +14,6 @@ class MetricsResource(Resource):
         self.registry = registry
 
     def render_GET(self, request):
-        request.setHeader(b'Content-Type', CONTENT_TYPE_LATEST.encode('ascii'))
-        return generate_latest(self.registry)
+        encoder, content_type = exposition.choose_encoder(request.getHeader('Accept'))
+        request.setHeader(b'Content-Type', content_type.encode('ascii'))
+        return encoder(self.registry)
diff --git a/setup.py b/setup.py
index a940a15..0ceb07f 100644
--- a/setup.py
+++ b/setup.py
@@ -15,6 +15,7 @@ setup(
     packages=[
         'prometheus_client',
         'prometheus_client.bridge',
+        'prometheus_client.openmetrics',
         'prometheus_client.twisted',
     ],
     extras_require={


Test patch:
diff --git a/tests/openmetrics/__init__.py b/tests/openmetrics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py
new file mode 100644
index 0000000..5646272
--- /dev/null
+++ b/tests/openmetrics/test_exposition.py
@@ -0,0 +1,228 @@
+from __future__ import unicode_literals
+
+import sys
+import time
+
+if sys.version_info < (2, 7):
+    # We need the skip decorators from unittest2 on Python 2.6.
+    import unittest2 as unittest
+else:
+    import unittest
+
+from prometheus_client import Gauge, Counter, Summary, Histogram, Info, Enum, Metric
+from prometheus_client import CollectorRegistry
+from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp, Exemplar
+from prometheus_client.openmetrics.exposition import (
+        generate_latest,
+)
+
+class TestGenerateText(unittest.TestCase):
+    def setUp(self):
+        self.registry = CollectorRegistry()
+
+        # Mock time so _created values are fixed.
+        self.old_time = time.time
+        time.time = lambda: 123.456
+
+    def tearDown(self):
+        time.time = self.old_time
+
+    def custom_collector(self, metric_family):
+        class CustomCollector(object):
+            def collect(self):
+                return [metric_family]
+        self.registry.register(CustomCollector())
+
+    def test_counter(self):
+        c = Counter('cc', 'A counter', registry=self.registry)
+        c.inc()
+        self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc_total 1.0\ncc_created 123.456\n# EOF\n', generate_latest(self.registry))
+
+    def test_counter_total(self):
+        c = Counter('cc_total', 'A counter', registry=self.registry)
+        c.inc()
+        self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc_total 1.0\ncc_created 123.456\n# EOF\n', generate_latest(self.registry))
+
+    def test_gauge(self):
+        g = Gauge('gg', 'A gauge', registry=self.registry)
+        g.set(17)
+        self.assertEqual(b'# HELP gg A gauge\n# TYPE gg gauge\ngg 17.0\n# EOF\n', generate_latest(self.registry))
+
+    def test_summary(self):
+        s = Summary('ss', 'A summary', ['a', 'b'], registry=self.registry)
+        s.labels('c', 'd').observe(17)
+        self.assertEqual(b'''# HELP ss A summary
+# TYPE ss summary
+ss_count{a="c",b="d"} 1.0
+ss_sum{a="c",b="d"} 17.0
+ss_created{a="c",b="d"} 123.456
+# EOF
+''', generate_latest(self.registry))
+
+    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
+    def test_histogram(self):
+        s = Histogram('hh', 'A histogram', registry=self.registry)
+        s.observe(0.05)
+        self.assertEqual(b'''# HELP hh A histogram
+# TYPE hh histogram
+hh_bucket{le="0.005"} 0.0
+hh_bucket{le="0.01"} 0.0
+hh_bucket{le="0.025"} 0.0
+hh_bucket{le="0.05"} 1.0
+hh_bucket{le="0.075"} 1.0
+hh_bucket{le="0.1"} 1.0
+hh_bucket{le="0.25"} 1.0
+hh_bucket{le="0.5"} 1.0
+hh_bucket{le="0.75"} 1.0
+hh_bucket{le="1.0"} 1.0
+hh_bucket{le="2.5"} 1.0
+hh_bucket{le="5.0"} 1.0
+hh_bucket{le="7.5"} 1.0
+hh_bucket{le="10.0"} 1.0
+hh_bucket{le="+Inf"} 1.0
+hh_count 1.0
+hh_sum 0.05
+hh_created 123.456
+# EOF
+''', generate_latest(self.registry))
+
+    def test_histogram_exemplar(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("hh", "help", 'histogram')
+                # This is not sane, but it covers all the cases.
+                metric.add_sample("hh_bucket", {"le": "1"}, 0, None, Exemplar({'a': 'b'}, 0.5))
+                metric.add_sample("hh_bucket", {"le": "2"}, 0, None, Exemplar({'le': '7'}, 0.5, 12))
+                metric.add_sample("hh_bucket", {"le": "3"}, 0, 123, Exemplar({'a': 'b'}, 2.5, 12))
+                metric.add_sample("hh_bucket", {"le": "4"}, 0, None, Exemplar({'a': '\n"\\'}, 3.5))
+                metric.add_sample("hh_bucket", {"le": "+Inf"}, 0, None, None)
+                yield metric
+
+        self.registry.register(MyCollector())
+        self.assertEqual(b'''# HELP hh help
+# TYPE hh histogram
+hh_bucket{le="1"} 0.0 # {a="b"} 0.5
+hh_bucket{le="2"} 0.0 # {le="7"} 0.5 12
+hh_bucket{le="3"} 0.0 123 # {a="b"} 2.5 12
+hh_bucket{le="4"} 0.0 # {a="\\n\\"\\\\"} 3.5
+hh_bucket{le="+Inf"} 0.0
+# EOF
+''', generate_latest(self.registry))
+
+    def test_nonhistogram_exemplar(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("hh", "help", 'untyped')
+                # This is not sane, but it covers all the cases.
+                metric.add_sample("hh_bucket", {}, 0, None, Exemplar({'a': 'b'}, 0.5))
+                yield metric
+
+        self.registry.register(MyCollector())
+        with self.assertRaises(ValueError):
+            generate_latest(self.registry)
+
+    def test_nonhistogram_bucket_exemplar(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("hh", "help", 'histogram')
+                # This is not sane, but it covers all the cases.
+                metric.add_sample("hh_count", {}, 0, None, Exemplar({'a': 'b'}, 0.5))
+                yield metric
+
+        self.registry.register(MyCollector())
+        with self.assertRaises(ValueError):
+            generate_latest(self.registry)
+
+    def test_gaugehistogram(self):
+        self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))]))
+        self.assertEqual(b'''# HELP gh help
+# TYPE gh gaugehistogram
+gh_bucket{le="1.0"} 4.0
+gh_bucket{le="+Inf"} 5.0
+# EOF
+''', generate_latest(self.registry))
+
+    def test_info(self):
+        i = Info('ii', 'A info', ['a', 'b'], registry=self.registry)
+        i.labels('c', 'd').info({'foo': 'bar'})
+        self.assertEqual(b'''# HELP ii A info
+# TYPE ii info
+ii_info{a="c",b="d",foo="bar"} 1.0
+# EOF
+''', generate_latest(self.registry))
+
+    def test_enum(self):
+        i = Enum('ee', 'An enum', ['a', 'b'], registry=self.registry, states=['foo', 'bar'])
+        i.labels('c', 'd').state('bar')
+        self.assertEqual(b'''# HELP ee An enum
+# TYPE ee stateset
+ee{a="c",b="d",ee="foo"} 0.0
+ee{a="c",b="d",ee="bar"} 1.0
+# EOF
+''', generate_latest(self.registry))
+
+    def test_unicode(self):
+        c = Counter('cc', '\u4500', ['l'], registry=self.registry)
+        c.labels('\u4500').inc()
+        self.assertEqual(b'''# HELP cc \xe4\x94\x80
+# TYPE cc counter
+cc_total{l="\xe4\x94\x80"} 1.0
+cc_created{l="\xe4\x94\x80"} 123.456
+# EOF
+''', generate_latest(self.registry))
+
+    def test_escaping(self):
+        c = Counter('cc', 'A\ncount\\er\"', ['a'], registry=self.registry)
+        c.labels('\\x\n"').inc(1)
+        self.assertEqual(b'''# HELP cc A\\ncount\\\\er\\"
+# TYPE cc counter
+cc_total{a="\\\\x\\n\\""} 1.0
+cc_created{a="\\\\x\\n\\""} 123.456
+# EOF
+''', generate_latest(self.registry))
+
+    def test_nonnumber(self):
+
+        class MyNumber(object):
+            def __repr__(self):
+                return "MyNumber(123)"
+
+            def __float__(self):
+                return 123.0
+
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("nonnumber", "Non number", 'untyped')
+                metric.add_sample("nonnumber", {}, MyNumber())
+                yield metric
+
+        self.registry.register(MyCollector())
+        self.assertEqual(b'# HELP nonnumber Non number\n# TYPE nonnumber unknown\nnonnumber 123.0\n# EOF\n', generate_latest(self.registry))
+
+    def test_timestamp(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("ts", "help", 'unknown')
+                metric.add_sample("ts", {"foo": "a"}, 0, 123.456)
+                metric.add_sample("ts", {"foo": "b"}, 0, -123.456)
+                metric.add_sample("ts", {"foo": "c"}, 0, 123)
+                metric.add_sample("ts", {"foo": "d"}, 0, Timestamp(123, 456000000))
+                metric.add_sample("ts", {"foo": "e"}, 0, Timestamp(123, 456000))
+                metric.add_sample("ts", {"foo": "f"}, 0, Timestamp(123, 456))
+                yield metric
+
+        self.registry.register(MyCollector())
+        self.assertEqual(b'''# HELP ts help
+# TYPE ts unknown
+ts{foo="a"} 0.0 123.456
+ts{foo="b"} 0.0 -123.456
+ts{foo="c"} 0.0 123
+ts{foo="d"} 0.0 123.456000000
+ts{foo="e"} 0.0 123.000456000
+ts{foo="f"} 0.0 123.000000456
+# EOF
+''', generate_latest(self.registry))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py
new file mode 100644
index 0000000..300965a
--- /dev/null
+++ b/tests/openmetrics/test_parser.py
@@ -0,0 +1,415 @@
+from __future__ import unicode_literals
+
+import math
+import sys
+
+if sys.version_info < (2, 7):
+    # We need the skip decorators from unittest2 on Python 2.6.
+    import unittest2 as unittest
+else:
+    import unittest
+
+from prometheus_client.core import (
+    CollectorRegistry,
+    CounterMetricFamily,
+    Exemplar,
+    GaugeMetricFamily,
+    HistogramMetricFamily,
+    Metric,
+    Sample,
+    SummaryMetricFamily,
+    Timestamp,
+)
+from prometheus_client.openmetrics.exposition import (
+    generate_latest,
+)
+from prometheus_client.openmetrics.parser import (
+    text_string_to_metric_families,
+)
+
+
+class TestParse(unittest.TestCase):
+
+    def test_simple_counter(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total 1
+# EOF
+""")
+        self.assertEqual([CounterMetricFamily("a", "help", value=1)], list(families))
+
+    def test_uint64_counter(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total 9223372036854775808
+# EOF
+""")
+        self.assertEqual([CounterMetricFamily("a", "help", value=9223372036854775808)], list(families))
+
+    def test_simple_gauge(self):
+        families = text_string_to_metric_families("""# TYPE a gauge
+# HELP a help
+a 1
+# EOF
+""")
+        self.assertEqual([GaugeMetricFamily("a", "help", value=1)], list(families))
+
+    def test_float_gauge(self):
+        families = text_string_to_metric_families("""# TYPE a gauge
+# HELP a help
+a 1.2
+# EOF
+""")
+        self.assertEqual([GaugeMetricFamily("a", "help", value=1.2)], list(families))
+
+    def test_unit_gauge(self):
+        families = text_string_to_metric_families("""# TYPE a_seconds gauge
+# UNIT a_seconds seconds
+# HELP a_seconds help
+a_seconds 1
+# EOF
+""")
+        self.assertEqual([GaugeMetricFamily("a_seconds", "help", value=1, unit='seconds')], list(families))
+
+    def test_simple_summary(self):
+        families = text_string_to_metric_families("""# TYPE a summary
+# HELP a help
+a_count 1
+a_sum 2
+# EOF
+""")
+        summary = SummaryMetricFamily("a", "help", count_value=1, sum_value=2)
+        self.assertEqual([summary], list(families))
+
+    def test_summary_quantiles(self):
+        families = text_string_to_metric_families("""# TYPE a summary
+# HELP a help
+a_count 1
+a_sum 2
+a{quantile="0.5"} 0.7
+# EOF
+""")
+        # The Python client doesn't support quantiles, but we
+        # still need to be able to parse them.
+        metric_family = SummaryMetricFamily("a", "help", count_value=1, sum_value=2)
+        metric_family.add_sample("a", {"quantile": "0.5"}, 0.7)
+        self.assertEqual([metric_family], list(families))
+
+    def test_simple_histogram(self):
+        families = text_string_to_metric_families("""# TYPE a histogram
+# HELP a help
+a_bucket{le="1"} 0
+a_bucket{le="+Inf"} 3
+a_count 3
+a_sum 2
+# EOF
+""")
+        self.assertEqual([HistogramMetricFamily("a", "help", sum_value=2, buckets=[("1", 0.0), ("+Inf", 3.0)])], list(families))
+
+    def test_histogram_exemplars(self):
+        families = text_string_to_metric_families("""# TYPE a histogram
+# HELP a help
+a_bucket{le="1"} 0 # {a="b"} 0.5
+a_bucket{le="2"} 2 123 # {a="c"} 0.5
+a_bucket{le="+Inf"} 3 # {a="d"} 4 123
+# EOF
+""")
+        hfm = HistogramMetricFamily("a", "help")
+        hfm.add_sample("a_bucket", {"le": "1"}, 0.0, None, Exemplar({"a": "b"}, 0.5))
+        hfm.add_sample("a_bucket", {"le": "2"}, 2.0, Timestamp(123, 0), Exemplar({"a": "c"}, 0.5)), 
+        hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "d"}, 4, Timestamp(123, 0)))
+        self.assertEqual([hfm], list(families))
+
+    def test_no_metadata(self):
+        families = text_string_to_metric_families("""a 1
+# EOF
+""")
+        metric_family = Metric("a", "", "untyped")
+        metric_family.add_sample("a", {}, 1)
+        self.assertEqual([metric_family], list(families))
+
+    def test_untyped(self):
+        # https://github.com/prometheus/client_python/issues/79
+        families = text_string_to_metric_families("""# HELP redis_connected_clients Redis connected clients
+# TYPE redis_connected_clients untyped
+redis_connected_clients{instance="rough-snowflake-web",port="6380"} 10.0
+redis_connected_clients{instance="rough-snowflake-web",port="6381"} 12.0
+# EOF
+""")
+        m = Metric("redis_connected_clients", "Redis connected clients", "untyped")
+        m.samples = [
+            Sample("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6380"}, 10),
+            Sample("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6381"}, 12),
+        ]
+        self.assertEqual([m], list(families))
+
+    def test_type_help_switched(self):
+        families = text_string_to_metric_families("""# HELP a help
+# TYPE a counter
+a_total 1
+# EOF
+""")
+        self.assertEqual([CounterMetricFamily("a", "help", value=1)], list(families))
+
+    def test_labels_with_curly_braces(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{foo="bar",bar="b{a}z"} 1
+# EOF
+""")
+        metric_family = CounterMetricFamily("a", "help", labels=["foo", "bar"])
+        metric_family.add_metric(["bar", "b{a}z"], 1)
+        self.assertEqual([metric_family], list(families))
+
+    def test_empty_help(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a 
+a_total 1
+# EOF
+""")
+        self.assertEqual([CounterMetricFamily("a", "", value=1)], list(families))
+
+    def test_labels_and_infinite(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{foo="bar"} +Inf
+a_total{foo="baz"} -Inf
+# EOF
+""")
+        metric_family = CounterMetricFamily("a", "help", labels=["foo"])
+        metric_family.add_metric(["bar"], float('inf'))
+        metric_family.add_metric(["baz"], float('-inf'))
+        self.assertEqual([metric_family], list(families))
+
+    def test_empty_brackets(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{} 1
+# EOF
+""")
+        self.assertEqual([CounterMetricFamily("a", "help", value=1)], list(families))
+
+    def test_nan(self):
+        families = text_string_to_metric_families("""a NaN
+# EOF
+""")
+        self.assertTrue(math.isnan(list(families)[0].samples[0][2]))
+
+    def test_no_newline_after_eof(self):
+        families = text_string_to_metric_families("""# TYPE a gauge
+# HELP a help
+a 1
+# EOF""")
+        self.assertEqual([GaugeMetricFamily("a", "help", value=1)], list(families))
+
+    def test_empty_label(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{foo="bar"} 1
+a_total{foo=""} 2
+# EOF
+""")
+        metric_family = CounterMetricFamily("a", "help", labels=["foo"])
+        metric_family.add_metric(["bar"], 1)
+        metric_family.add_metric([""], 2)
+        self.assertEqual([metric_family], list(families))
+
+    def test_label_escaping(self):
+        for escaped_val, unescaped_val in [
+                ('foo', 'foo'),
+                ('\\foo', '\\foo'),
+                ('\\\\foo', '\\foo'),
+                ('foo\\\\', 'foo\\'),
+                ('\\\\', '\\'),
+                ('\\n', '\n'),
+                ('\\\\n', '\\n'),
+                ('\\\\\\n', '\\\n'),
+                ('\\"', '"'),
+                ('\\\\\\"', '\\"')]:
+            families = list(text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{foo="%s",bar="baz"} 1
+# EOF
+""" % escaped_val))
+            metric_family = CounterMetricFamily(
+                "a", "help", labels=["foo", "bar"])
+            metric_family.add_metric([unescaped_val, "baz"], 1)
+            self.assertEqual([metric_family], list(families))
+
+    def test_help_escaping(self):
+        for escaped_val, unescaped_val in [
+                ('foo', 'foo'),
+                ('\\foo', '\\foo'),
+                ('\\\\foo', '\\foo'),
+                ('foo\\', 'foo\\'),
+                ('foo\\\\', 'foo\\'),
+                ('\\n', '\n'),
+                ('\\\\n', '\\n'),
+                ('\\\\\\n', '\\\n'),
+                ('\\"', '"'),
+                ('\\\\"', '\\"'),
+                ('\\\\\\"', '\\"')]:
+            families = list(text_string_to_metric_families("""# TYPE a counter
+# HELP a %s
+a_total{foo="bar"} 1
+# EOF
+""" % escaped_val))
+            metric_family = CounterMetricFamily("a", unescaped_val, labels=["foo"])
+            metric_family.add_metric(["bar"], 1)
+            self.assertEqual([metric_family], list(families))
+
+    def test_escaping(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a he\\n\\\\l\\tp
+a_total{foo="b\\"a\\nr"} 1
+a_total{foo="b\\\\a\\z"} 2
+# EOF
+""")
+        metric_family = CounterMetricFamily("a", "he\n\\l\\tp", labels=["foo"])
+        metric_family.add_metric(["b\"a\nr"], 1)
+        metric_family.add_metric(["b\\a\\z"], 2)
+        self.assertEqual([metric_family], list(families))
+
+    def test_timestamps(self):
+        families = text_string_to_metric_families("""# TYPE a counter
+# HELP a help
+a_total{foo="1"} 1 000
+a_total{foo="2"} 1 0.0
+a_total{foo="3"} 1 1.1
+a_total{foo="4"} 1 12345678901234567890.1234567890
+a_total{foo="5"} 1 1.5e3
+# TYPE b counter
+# HELP b help
+b_total 2 1234567890
+# EOF
+""")
+        a = CounterMetricFamily("a", "help", labels=["foo"])
+        a.add_metric(["1"], 1, timestamp=Timestamp(0, 0))
+        a.add_metric(["2"], 1, timestamp=Timestamp(0, 0))
+        a.add_metric(["3"], 1, timestamp=Timestamp(1, 100000000))
+        a.add_metric(["4"], 1, timestamp=Timestamp(12345678901234567890, 123456789))
+        a.add_metric(["5"], 1, timestamp=1500.0)
+        b = CounterMetricFamily("b", "help")
+        b.add_metric([], 2, timestamp=Timestamp(1234567890, 0))
+        self.assertEqual([a, b], list(families))
+
+    @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
+    def test_roundtrip(self):
+        text = """# HELP go_gc_duration_seconds A summary of the GC invocation durations.
+# TYPE go_gc_duration_seconds summary
+go_gc_duration_seconds{quantile="0"} 0.013300656000000001
+go_gc_duration_seconds{quantile="0.25"} 0.013638736
+go_gc_duration_seconds{quantile="0.5"} 0.013759906
+go_gc_duration_seconds{quantile="0.75"} 0.013962066
+go_gc_duration_seconds{quantile="1"} 0.021383540000000003
+go_gc_duration_seconds_sum 56.12904785
+go_gc_duration_seconds_count 7476.0
+# HELP go_goroutines Number of goroutines that currently exist.
+# TYPE go_goroutines gauge
+go_goroutines 166.0
+# HELP prometheus_local_storage_indexing_batch_duration_milliseconds Quantiles for batch indexing duration in milliseconds.
+# TYPE prometheus_local_storage_indexing_batch_duration_milliseconds summary
+prometheus_local_storage_indexing_batch_duration_milliseconds{quantile="0.5"} NaN
+prometheus_local_storage_indexing_batch_duration_milliseconds{quantile="0.9"} NaN
+prometheus_local_storage_indexing_batch_duration_milliseconds{quantile="0.99"} NaN
+prometheus_local_storage_indexing_batch_duration_milliseconds_sum 871.5665949999999
+prometheus_local_storage_indexing_batch_duration_milliseconds_count 229.0
+# HELP process_cpu_seconds Total user and system CPU time spent in seconds.
+# TYPE process_cpu_seconds counter
+process_cpu_seconds_total 29323.4
+# HELP process_virtual_memory_bytes Virtual memory size in bytes.
+# TYPE process_virtual_memory_bytes gauge
+process_virtual_memory_bytes 2478268416.0
+# HELP prometheus_build_info A metric with a constant '1' value labeled by version, revision, and branch from which Prometheus was built.
+# TYPE prometheus_build_info gauge
+prometheus_build_info{branch="HEAD",revision="ef176e5",version="0.16.0rc1"} 1.0
+# HELP prometheus_local_storage_chunk_ops The total number of chunk operations by their type.
+# TYPE prometheus_local_storage_chunk_ops counter
+prometheus_local_storage_chunk_ops_total{type="clone"} 28.0
+prometheus_local_storage_chunk_ops_total{type="create"} 997844.0
+prometheus_local_storage_chunk_ops_total{type="drop"} 1345758.0
+prometheus_local_storage_chunk_ops_total{type="load"} 1641.0
+prometheus_local_storage_chunk_ops_total{type="persist"} 981408.0
+prometheus_local_storage_chunk_ops_total{type="pin"} 32662.0
+prometheus_local_storage_chunk_ops_total{type="transcode"} 980180.0
+prometheus_local_storage_chunk_ops_total{type="unpin"} 32662.0
+# EOF
+"""
+        families = list(text_string_to_metric_families(text))
+
+        class TextCollector(object):
+            def collect(self):
+                return families
+
+        registry = CollectorRegistry()
+        registry.register(TextCollector())
+        self.assertEqual(text.encode('utf-8'), generate_latest(registry))
+
+    def test_invalid_input(self):
+        for case in [
+                # No EOF.
+                (''),
+                # Text after EOF.
+                ('a 1\n# EOF\nblah'),
+                ('a 1\n# EOFblah'),
+                # Missing or wrong quotes on label value.
+                ('a{a=1} 1\n# EOF\n'),
+                ('a{a="1} 1\n# EOF\n'),
+                ('a{a=\'1\'} 1\n# EOF\n'),
+                # Missing or extra commas.
+                ('a{a="1"b="2"} 1\n# EOF\n'),
+                ('a{a="1",,b="2"} 1\n# EOF\n'),
+                ('a{a="1",b="2",} 1\n# EOF\n'),
+                # Missing value.
+                ('a\n# EOF\n'),
+                ('a \n# EOF\n'),
+                # Bad HELP.
+                ('# HELP\n# EOF\n'),
+                ('# HELP \n# EOF\n'),
+                ('# HELP a\n# EOF\n'),
+                ('# HELP a\t\n# EOF\n'),
+                (' # HELP a meh\n# EOF\n'),
+                # Bad TYPE.
+                ('# TYPE\n# EOF\n'),
+                ('# TYPE \n# EOF\n'),
+                ('# TYPE a\n# EOF\n'),
+                ('# TYPE a\t\n# EOF\n'),
+                ('# TYPE a meh\n# EOF\n'),
+                ('# TYPE a meh \n# EOF\n'),
+                ('# TYPE a gauge \n# EOF\n'),
+                # Bad UNIT.
+                ('# UNIT\n# EOF\n'),
+                ('# UNIT \n# EOF\n'),
+                ('# UNIT a\n# EOF\n'),
+                ('# UNIT a\t\n# EOF\n'),
+                ('# UNIT a seconds\n# EOF\n'),
+                ('# UNIT a_seconds seconds \n# EOF\n'),
+                ('# TYPE x_u info\n# UNIT x_u u\n# EOF\n'),
+                ('# TYPE x_u stateset\n# UNIT x_u u\n# EOF\n'),
+                # Bad metric names.
+                ('0a 1\n# EOF\n'),
+                ('a.b 1\n# EOF\n'),
+                ('a-b 1\n# EOF\n'),
+                # Bad value.
+                ('a a\n# EOF\n'),
+                ('a  1\n# EOF\n'),
+                ('a 1\t\n# EOF\n'),
+                ('a 1 \n# EOF\n'),
+                # Bad timestamp.
+                ('a 1 z\n# EOF\n'),
+                ('a 1 1z\n# EOF\n'),
+                ('a 1 1.1.1\n# EOF\n'),
+                # Bad exemplars.
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1 #\n# EOF\n'),
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1# {} 1\n# EOF\n'),
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1 #{} 1\n# EOF\n'),
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {}1\n# EOF\n'),
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 1 \n# EOF\n'),
+                ('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 1 1 \n# EOF\n'),
+                ]:
+            with self.assertRaises(ValueError):
+                list(text_string_to_metric_families(case))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_core.py b/tests/test_core.py
index f85a3e1..fceca7a 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -14,11 +14,17 @@ from prometheus_client.core import (
     CollectorRegistry,
     Counter,
     CounterMetricFamily,
+    Enum,
     Gauge,
+    GaugeHistogramMetricFamily,
     GaugeMetricFamily,
     Histogram,
     HistogramMetricFamily,
+    Info,
+    InfoMetricFamily,
     Metric,
+    StateSetMetricFamily,
+    Sample,
     Summary,
     SummaryMetricFamily,
     UntypedMetricFamily,
@@ -28,14 +34,14 @@ from prometheus_client.core import (
 class TestCounter(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
-        self.counter = Counter('c', 'help', registry=self.registry)
+        self.counter = Counter('c_total', 'help', registry=self.registry)
 
     def test_increment(self):
-        self.assertEqual(0, self.registry.get_sample_value('c'))
+        self.assertEqual(0, self.registry.get_sample_value('c_total'))
         self.counter.inc()
-        self.assertEqual(1, self.registry.get_sample_value('c'))
+        self.assertEqual(1, self.registry.get_sample_value('c_total'))
         self.counter.inc(7)
-        self.assertEqual(8, self.registry.get_sample_value('c'))
+        self.assertEqual(8, self.registry.get_sample_value('c_total'))
 
     def test_negative_increment_raises(self):
         self.assertRaises(ValueError, self.counter.inc, -1)
@@ -54,18 +60,18 @@ class TestCounter(unittest.TestCase):
             f(False)
         except TypeError:
             pass
-        self.assertEqual(0, self.registry.get_sample_value('c'))
+        self.assertEqual(0, self.registry.get_sample_value('c_total'))
 
         try:
             f(True)
         except ValueError:
             pass
-        self.assertEqual(1, self.registry.get_sample_value('c'))
+        self.assertEqual(1, self.registry.get_sample_value('c_total'))
 
     def test_block_decorator(self):
         with self.counter.count_exceptions():
             pass
-        self.assertEqual(0, self.registry.get_sample_value('c'))
+        self.assertEqual(0, self.registry.get_sample_value('c_total'))
 
         raised = False
         try:
@@ -74,7 +80,7 @@ class TestCounter(unittest.TestCase):
         except:
             raised = True
         self.assertTrue(raised)
-        self.assertEqual(1, self.registry.get_sample_value('c'))
+        self.assertEqual(1, self.registry.get_sample_value('c_total'))
 
 
 class TestGauge(unittest.TestCase):
@@ -287,6 +293,8 @@ class TestHistogram(unittest.TestCase):
         self.assertRaises(ValueError, Histogram, 'h', 'help', registry=None, buckets=[3, 1])
 
     def test_labels(self):
+        self.assertRaises(ValueError, Histogram, 'h', 'help', registry=None, labelnames=['le'])
+
         self.labels.labels('a').observe(2)
         self.assertEqual(0, self.registry.get_sample_value('hl_bucket', {'le': '1.0', 'l': 'a'}))
         self.assertEqual(1, self.registry.get_sample_value('hl_bucket', {'le': '2.5', 'l': 'a'}))
@@ -295,6 +303,7 @@ class TestHistogram(unittest.TestCase):
         self.assertEqual(1, self.registry.get_sample_value('hl_count', {'l': 'a'}))
         self.assertEqual(2, self.registry.get_sample_value('hl_sum', {'l': 'a'}))
 
+
     def test_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('h_count'))
         self.assertEqual(0, self.registry.get_sample_value('h_bucket', {'le': '+Inf'}))
@@ -339,26 +348,74 @@ class TestHistogram(unittest.TestCase):
         self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '+Inf'}))
 
 
+class TestInfo(unittest.TestCase):
+    def setUp(self):
+        self.registry = CollectorRegistry()
+        self.info = Info('i', 'help', registry=self.registry)
+        self.labels = Info('il', 'help', ['l'], registry=self.registry)
+
+    def test_info(self):
+        self.assertEqual(1, self.registry.get_sample_value('i_info', {}))
+        self.info.info({'a': 'b', 'c': 'd'})
+        self.assertEqual(None, self.registry.get_sample_value('i_info', {}))
+        self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b', 'c': 'd'}))
+
+    def test_labels(self):
+        self.assertRaises(ValueError, self.labels.labels('a').info, {'l': ''})
+
+        self.labels.labels('a').info({'foo': 'bar'})
+        self.assertEqual(1, self.registry.get_sample_value('il_info', {'l': 'a', 'foo': 'bar'}))
+
+
+class TestEnum(unittest.TestCase):
+    def setUp(self):
+        self.registry = CollectorRegistry()
+        self.enum = Enum('e', 'help', states=['a', 'b', 'c'], registry=self.registry)
+        self.labels = Enum('el', 'help', ['l'], states=['a', 'b', 'c'], registry=self.registry)
+
+    def test_enum(self):
+        self.assertEqual(1, self.registry.get_sample_value('e', {'e': 'a'}))
+        self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'b'}))
+        self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'c'}))
+
+        self.enum.state('b')
+        self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'a'}))
+        self.assertEqual(1, self.registry.get_sample_value('e', {'e': 'b'}))
+        self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'c'}))
+
+        self.assertRaises(ValueError, self.enum.state, 'd')
+        self.assertRaises(ValueError, Enum, 'e', 'help', registry=None)
+
+    def test_labels(self):
+        self.labels.labels('a').state('c')
+        self.assertEqual(0, self.registry.get_sample_value('el', {'l': 'a', 'el': 'a'}))
+        self.assertEqual(0, self.registry.get_sample_value('el', {'l': 'a', 'el': 'b'}))
+        self.assertEqual(1, self.registry.get_sample_value('el', {'l': 'a', 'el': 'c'}))
+
+        e = Enum('e', 'help', registry=None, labelnames=['e'])
+        self.assertRaises(ValueError, e.labels, '')
+
+
 class TestMetricWrapper(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
-        self.counter = Counter('c', 'help', labelnames=['l'], registry=self.registry)
+        self.counter = Counter('c_total', 'help', labelnames=['l'], registry=self.registry)
         self.two_labels = Counter('two', 'help', labelnames=['a', 'b'], registry=self.registry)
 
     def test_child(self):
         self.counter.labels('x').inc()
-        self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
+        self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
         self.two_labels.labels('x', 'y').inc(2)
-        self.assertEqual(2, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
+        self.assertEqual(2, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
 
     def test_remove(self):
         self.counter.labels('x').inc()
         self.counter.labels('y').inc(2)
-        self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
-        self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
+        self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
+        self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))
         self.counter.remove('x')
-        self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'x'}))
-        self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
+        self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'}))
+        self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))
 
     def test_incorrect_label_count_raises(self):
         self.assertRaises(ValueError, self.counter.labels)
@@ -369,10 +426,10 @@ class TestMetricWrapper(unittest.TestCase):
     def test_labels_coerced_to_string(self):
         self.counter.labels(None).inc()
         self.counter.labels(l=None).inc()
-        self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'None'}))
+        self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'None'}))
 
         self.counter.remove(None)
-        self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'None'}))
+        self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'}))
 
     def test_non_string_labels_raises(self):
         class Test(object):
@@ -381,18 +438,18 @@ class TestMetricWrapper(unittest.TestCase):
         self.assertRaises(TypeError, self.counter.labels, l=Test())
 
     def test_namespace_subsystem_concatenated(self):
-        c = Counter('c', 'help', namespace='a', subsystem='b', registry=self.registry)
+        c = Counter('c_total', 'help', namespace='a', subsystem='b', registry=self.registry)
         c.inc()
-        self.assertEqual(1, self.registry.get_sample_value('a_b_c'))
+        self.assertEqual(1, self.registry.get_sample_value('a_b_c_total'))
 
     def test_labels_by_kwarg(self):
         self.counter.labels(l='x').inc()
-        self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
+        self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
         self.assertRaises(ValueError, self.counter.labels, l='x', m='y')
         self.assertRaises(ValueError, self.counter.labels, m='y')
         self.assertRaises(ValueError, self.counter.labels)
         self.two_labels.labels(a='x', b='y').inc()
-        self.assertEqual(1, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
+        self.assertEqual(1, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
         self.assertRaises(ValueError, self.two_labels.labels, a='x', b='y', c='z')
         self.assertRaises(ValueError, self.two_labels.labels, a='x', c='z')
         self.assertRaises(ValueError, self.two_labels.labels, b='y', c='z')
@@ -405,15 +462,26 @@ class TestMetricWrapper(unittest.TestCase):
         self.assertRaises(ValueError, Counter, '^', 'help')
         self.assertRaises(ValueError, Counter, '', 'help', namespace='&')
         self.assertRaises(ValueError, Counter, '', 'help', subsystem='(')
-        self.assertRaises(ValueError, Counter, 'c', '', labelnames=['^'])
-        self.assertRaises(ValueError, Counter, 'c', '', labelnames=['a:b'])
-        self.assertRaises(ValueError, Counter, 'c', '', labelnames=['__reserved'])
-        self.assertRaises(ValueError, Summary, 'c', '', labelnames=['quantile'])
-
+        self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['^'])
+        self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b'])
+        self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved'])
+        self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile'])
     def test_empty_labels_list(self):
         Histogram('h', 'help', [], registry=self.registry)
         self.assertEqual(0, self.registry.get_sample_value('h_sum'))
 
+    def test_unit_appended(self):
+        Histogram('h', 'help', [], registry=self.registry, unit="seconds")
+        self.assertEqual(0, self.registry.get_sample_value('h_seconds_sum'))
+
+    def test_unit_notappended(self):
+        Histogram('h_seconds', 'help', [], registry=self.registry, unit="seconds")
+        self.assertEqual(0, self.registry.get_sample_value('h_seconds_sum'))
+
+    def test_no_units_for_info_enum(self):
+        self.assertRaises(ValueError, Info, 'foo', 'help', unit="x")
+        self.assertRaises(ValueError, Enum, 'foo', 'help', unit="x")
+
     def test_wrapped_original_class(self):
         self.assertEqual(Counter.__wrapped__, Counter('foo', 'bar').__class__)
 
@@ -438,15 +506,23 @@ class TestMetricFamilies(unittest.TestCase):
         self.custom_collector(cmf)
         self.assertEqual(2, self.registry.get_sample_value('u', {'a': 'b', 'c': 'd'}))
 
+    def test_untyped_unit(self):
+        self.custom_collector(UntypedMetricFamily('u', 'help', value=1, unit='unit'))
+        self.assertEqual(1, self.registry.get_sample_value('u_unit', {}))
+
     def test_counter(self):
-        self.custom_collector(CounterMetricFamily('c', 'help', value=1))
-        self.assertEqual(1, self.registry.get_sample_value('c', {}))
+        self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
+        self.assertEqual(1, self.registry.get_sample_value('c_total', {}))
+
+    def test_counter_total(self):
+        self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
+        self.assertEqual(1, self.registry.get_sample_value('c_total', {}))
 
     def test_counter_labels(self):
-        cmf = CounterMetricFamily('c', 'help', labels=['a', 'c'])
+        cmf = CounterMetricFamily('c_total', 'help', labels=['a', 'c_total'])
         cmf.add_metric(['b', 'd'], 2)
         self.custom_collector(cmf)
-        self.assertEqual(2, self.registry.get_sample_value('c', {'a': 'b', 'c': 'd'}))
+        self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': 'b', 'c_total': 'd'}))
 
     def test_gauge(self):
         self.custom_collector(GaugeMetricFamily('g', 'help', value=1))
@@ -486,12 +562,46 @@ class TestMetricFamilies(unittest.TestCase):
         self.assertEqual(2, self.registry.get_sample_value('h_count', {'a': 'b'}))
         self.assertEqual(3, self.registry.get_sample_value('h_sum', {'a': 'b'}))
 
+    def test_gaugehistogram(self):
+        self.custom_collector(GaugeHistogramMetricFamily('h', 'help', buckets=[('0', 1), ('+Inf', 2)]))
+        self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '0'}))
+        self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'le': '+Inf'}))
+
+    def test_gaugehistogram_labels(self):
+        cmf = GaugeHistogramMetricFamily('h', 'help', labels=['a'])
+        cmf.add_metric(['b'], buckets=[('0', 1), ('+Inf', 2)])
+        self.custom_collector(cmf)
+        self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'a': 'b', 'le': '0'}))
+        self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'a': 'b', 'le': '+Inf'}))
+
+    def test_info(self):
+        self.custom_collector(InfoMetricFamily('i', 'help', value={'a': 'b'}))
+        self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b'}))
+
+    def test_info_labels(self):
+        cmf = InfoMetricFamily('i', 'help', labels=['a'])
+        cmf.add_metric(['b'], {'c': 'd'})
+        self.custom_collector(cmf)
+        self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b', 'c': 'd'}))
+
+    def test_stateset(self):
+        self.custom_collector(StateSetMetricFamily('s', 'help', value={'a': True, 'b': True,}))
+        self.assertEqual(1, self.registry.get_sample_value('s', {'s': 'a'}))
+        self.assertEqual(1, self.registry.get_sample_value('s', {'s': 'b'}))
+
+    def test_stateset_labels(self):
+        cmf = StateSetMetricFamily('s', 'help', labels=['foo'])
+        cmf.add_metric(['bar'], {'a': False, 'b': False,})
+        self.custom_collector(cmf)
+        self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'a'}))
+        self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'b'}))
+
     def test_bad_constructors(self):
         self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
         self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])
 
-        self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=[])
-        self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=['a'])
+        self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=[])
+        self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=['a'])
 
         self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=[])
         self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=['a'])
@@ -509,10 +619,16 @@ class TestMetricFamilies(unittest.TestCase):
         self.assertRaises(ValueError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1, labels=['a'])
         self.assertRaises(KeyError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1)
 
+        self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=[])
+        self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=['a'])
+
+        self.assertRaises(ValueError, StateSetMetricFamily, 's', 'help', value={'a': True}, labels=[])
+        self.assertRaises(ValueError, StateSetMetricFamily, 's', 'help', value={'a': True}, labels=['a'])
+
     def test_labelnames(self):
         cmf = UntypedMetricFamily('u', 'help', labels=iter(['a']))
         self.assertEqual(('a',), cmf._labelnames)
-        cmf = CounterMetricFamily('c', 'help', labels=iter(['a']))
+        cmf = CounterMetricFamily('c_total', 'help', labels=iter(['a']))
         self.assertEqual(('a',), cmf._labelnames)
         gmf = GaugeMetricFamily('g', 'help', labels=iter(['a']))
         self.assertEqual(('a',), gmf._labelnames)
@@ -525,16 +641,20 @@ class TestMetricFamilies(unittest.TestCase):
 class TestCollectorRegistry(unittest.TestCase):
     def test_duplicate_metrics_raises(self):
         registry = CollectorRegistry()
-        Counter('c', 'help', registry=registry)
-        self.assertRaises(ValueError, Counter, 'c', 'help', registry=registry)
-        self.assertRaises(ValueError, Gauge, 'c', 'help', registry=registry)
+        Counter('c_total', 'help', registry=registry)
+        self.assertRaises(ValueError, Counter, 'c_total', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'c_total', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'c_created', 'help', registry=registry)
 
-        Gauge('g', 'help', registry=registry)
-        self.assertRaises(ValueError, Gauge, 'g', 'help', registry=registry)
+        Gauge('g_created', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'g_created', 'help', registry=registry)
         self.assertRaises(ValueError, Counter, 'g', 'help', registry=registry)
 
         Summary('s', 'help', registry=registry)
         self.assertRaises(ValueError, Summary, 's', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 's_created', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 's_sum', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
         # We don't currently expose quantiles, but let's prevent future
         # clashes anyway.
         self.assertRaises(ValueError, Gauge, 's', 'help', registry=registry)
@@ -543,18 +663,19 @@ class TestCollectorRegistry(unittest.TestCase):
         self.assertRaises(ValueError, Histogram, 'h', 'help', registry=registry)
         # Clashes aggaint various suffixes.
         self.assertRaises(ValueError, Summary, 'h', 'help', registry=registry)
-        self.assertRaises(ValueError, Counter, 'h_count', 'help', registry=registry)
-        self.assertRaises(ValueError, Counter, 'h_sum', 'help', registry=registry)
-        self.assertRaises(ValueError, Counter, 'h_bucket', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'h_count', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'h_sum', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'h_bucket', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'h_created', 'help', registry=registry)
         # The name of the histogram itself isn't taken.
-        Counter('h', 'help', registry=registry)
+        Gauge('h', 'help', registry=registry)
 
     def test_unregister_works(self):
         registry = CollectorRegistry()
         s = Summary('s', 'help', registry=registry)
-        self.assertRaises(ValueError, Counter, 's_count', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
         registry.unregister(s)
-        Counter('s_count', 'help', registry=registry)
+        Gauge('s_count', 'help', registry=registry)
 
     def custom_collector(self, metric_family, registry):
         class CustomCollector(object):
@@ -564,20 +685,20 @@ class TestCollectorRegistry(unittest.TestCase):
 
     def test_autodescribe_disabled_by_default(self):
         registry = CollectorRegistry()
-        self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
-        self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
+        self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
+        self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
 
         registry = CollectorRegistry(auto_describe=True)
-        self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
-        self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c', 'help', value=1), registry)
+        self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
+        self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c_total', 'help', value=1), registry)
 
     def test_restricted_registry(self):
         registry = CollectorRegistry()
-        Counter('c', 'help', registry=registry)
+        Counter('c_total', 'help', registry=registry)
         Summary('s', 'help', registry=registry).observe(7)
 
         m = Metric('s', 'help', 'summary')
-        m.samples = [('s_sum', {}, 7)]
+        m.samples = [Sample('s_sum', {}, 7)]
         self.assertEquals([m], registry.restricted_registry(['s_sum']).collect())
 
 
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index b94af87..cd5e7eb 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -9,10 +9,11 @@ if sys.version_info < (2, 7):
 else:
     import unittest
 
-from prometheus_client import Gauge, Counter, Summary, Histogram, Metric
+from prometheus_client import Gauge, Counter, Summary, Histogram, Info, Enum, Metric
 from prometheus_client import CollectorRegistry, generate_latest
 from prometheus_client import push_to_gateway, pushadd_to_gateway, delete_from_gateway
 from prometheus_client import CONTENT_TYPE_LATEST, instance_ip_grouping_key
+from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp
 from prometheus_client.exposition import default_handler, basic_auth_handler, MetricsHandler
 
 try:
@@ -28,11 +29,21 @@ class TestGenerateText(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
 
+    def custom_collector(self, metric_family):
+        class CustomCollector(object):
+            def collect(self):
+                return [metric_family]
+        self.registry.register(CustomCollector())
+
     def test_counter(self):
         c = Counter('cc', 'A counter', registry=self.registry)
         c.inc()
-        self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc 1.0\n', generate_latest(self.registry))
+        self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
 
+    def test_counter_total(self):
+        c = Counter('cc_total', 'A counter', registry=self.registry)
+        c.inc()
+        self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
     def test_gauge(self):
         g = Gauge('gg', 'A gauge', registry=self.registry)
         g.set(17)
@@ -68,15 +79,29 @@ hh_count 1.0
 hh_sum 0.05
 ''', generate_latest(self.registry))
 
+    def test_gaugehistogram(self):
+        self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))]))
+        self.assertEqual(b'''# HELP gh help\n# TYPE gh histogram\ngh_bucket{le="1.0"} 4.0\ngh_bucket{le="+Inf"} 5.0\n''', generate_latest(self.registry))
+
+    def test_info(self):
+        i = Info('ii', 'A info', ['a', 'b'], registry=self.registry)
+        i.labels('c', 'd').info({'foo': 'bar'})
+        self.assertEqual(b'# HELP ii_info A info\n# TYPE ii_info gauge\nii_info{a="c",b="d",foo="bar"} 1.0\n', generate_latest(self.registry))
+
+    def test_enum(self):
+        i = Enum('ee', 'An enum', ['a', 'b'], registry=self.registry, states=['foo', 'bar'])
+        i.labels('c', 'd').state('bar')
+        self.assertEqual(b'# HELP ee An enum\n# TYPE ee gauge\nee{a="c",b="d",ee="foo"} 0.0\nee{a="c",b="d",ee="bar"} 1.0\n', generate_latest(self.registry))
+
     def test_unicode(self):
         c = Counter('cc', '\u4500', ['l'], registry=self.registry)
         c.labels('\u4500').inc()
-        self.assertEqual(b'# HELP cc \xe4\x94\x80\n# TYPE cc counter\ncc{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
+        self.assertEqual(b'# HELP cc_total \xe4\x94\x80\n# TYPE cc_total counter\ncc_total{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
 
     def test_escaping(self):
         c = Counter('cc', 'A\ncount\\er', ['a'], registry=self.registry)
         c.labels('\\x\n"').inc(1)
-        self.assertEqual(b'# HELP cc A\\ncount\\\\er\n# TYPE cc counter\ncc{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
+        self.assertEqual(b'# HELP cc_total A\\ncount\\\\er\n# TYPE cc_total counter\ncc_total{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
 
     def test_nonnumber(self):
 
@@ -96,6 +121,29 @@ hh_sum 0.05
         self.registry.register(MyCollector())
         self.assertEqual(b'# HELP nonnumber Non number\n# TYPE nonnumber untyped\nnonnumber 123.0\n', generate_latest(self.registry))
 
+    def test_timestamp(self):
+        class MyCollector(object):
+            def collect(self):
+                metric = Metric("ts", "help", 'untyped')
+                metric.add_sample("ts", {"foo": "a"}, 0, 123.456)
+                metric.add_sample("ts", {"foo": "b"}, 0, -123.456)
+                metric.add_sample("ts", {"foo": "c"}, 0, 123)
+                metric.add_sample("ts", {"foo": "d"}, 0, Timestamp(123, 456000000))
+                metric.add_sample("ts", {"foo": "e"}, 0, Timestamp(123, 456000))
+                metric.add_sample("ts", {"foo": "f"}, 0, Timestamp(123, 456))
+                yield metric
+
+        self.registry.register(MyCollector())
+        self.assertEqual(b'''# HELP ts help
+# TYPE ts untyped
+ts{foo="a"} 0.0 123456
+ts{foo="b"} 0.0 -123456
+ts{foo="c"} 0.0 123000
+ts{foo="d"} 0.0 123456
+ts{foo="e"} 0.0 123000
+ts{foo="f"} 0.0 123000
+''', generate_latest(self.registry))
+
 
 class TestPushGateway(unittest.TestCase):
     def setUp(self):
diff --git a/tests/test_graphite_bridge.py b/tests/test_graphite_bridge.py
index cce368c..66bf720 100644
--- a/tests/test_graphite_bridge.py
+++ b/tests/test_graphite_bridge.py
@@ -5,7 +5,7 @@ try:
 except ImportError:
     import socketserver as SocketServer
 
-from prometheus_client import Counter, CollectorRegistry
+from prometheus_client import Gauge, CollectorRegistry
 from prometheus_client.bridge.graphite import GraphiteBridge
 
 
@@ -38,16 +38,16 @@ class TestGraphiteBridge(unittest.TestCase):
         self.gb = GraphiteBridge(address, self.registry, _timer=fake_timer)
 
     def test_nolabels(self):
-        counter = Counter('c', 'help', registry=self.registry)
-        counter.inc()
+        gauge = Gauge('g', 'help', registry=self.registry)
+        gauge.inc()
 
         self.gb.push()
         self.t.join()
 
-        self.assertEqual(b'c 1.0 1434898897\n', self.data)
+        self.assertEqual(b'g 1.0 1434898897\n', self.data)
 
     def test_labels(self):
-        labels = Counter('labels', 'help', ['a', 'b'], registry=self.registry)
+        labels = Gauge('labels', 'help', ['a', 'b'], registry=self.registry)
         labels.labels('c', 'd').inc()
 
         self.gb.push()
@@ -56,7 +56,7 @@ class TestGraphiteBridge(unittest.TestCase):
         self.assertEqual(b'labels.a.c.b.d 1.0 1434898897\n', self.data)
 
     def test_prefix(self):
-        labels = Counter('labels', 'help', ['a', 'b'], registry=self.registry)
+        labels = Gauge('labels', 'help', ['a', 'b'], registry=self.registry)
         labels.labels('c', 'd').inc()
 
         self.gb.push(prefix='pre.fix')
@@ -65,7 +65,7 @@ class TestGraphiteBridge(unittest.TestCase):
         self.assertEqual(b'pre.fix.labels.a.c.b.d 1.0 1434898897\n', self.data)
 
     def test_sanitizing(self):
-        labels = Counter('labels', 'help', ['a'], registry=self.registry)
+        labels = Gauge('labels', 'help', ['a'], registry=self.registry)
         labels.labels('c.:8').inc()
 
         self.gb.push()
diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py
index db5d0fd..ca84913 100644
--- a/tests/test_multiprocess.py
+++ b/tests/test_multiprocess.py
@@ -36,10 +36,10 @@ class TestMultiProcess(unittest.TestCase):
         c1 = Counter('c', 'help', registry=None)
         core._ValueClass = core._MultiProcessValue(lambda: 456)
         c2 = Counter('c', 'help', registry=None)
-        self.assertEqual(0, self.registry.get_sample_value('c'))
+        self.assertEqual(0, self.registry.get_sample_value('c_total'))
         c1.inc(1)
         c2.inc(2)
-        self.assertEqual(3, self.registry.get_sample_value('c'))
+        self.assertEqual(3, self.registry.get_sample_value('c_total'))
 
     def test_summary_adds(self):
         s1 = Summary('s', 'help', registry=None)
@@ -121,20 +121,20 @@ class TestMultiProcess(unittest.TestCase):
         self.assertEqual(2, self.registry.get_sample_value('g'))
 
     def test_namespace_subsystem(self):
-        c1 = Counter('c', 'help', registry=None, namespace='ns', subsystem='ss')
-        c1.inc(1)
-        self.assertEqual(1, self.registry.get_sample_value('ns_ss_c'))
+         c1 = Counter('c', 'help', registry=None, namespace='ns', subsystem='ss')
+         c1.inc(1)
+         self.assertEqual(1, self.registry.get_sample_value('ns_ss_c_total'))
 
     def test_counter_across_forks(self):
         pid = 0
         core._ValueClass = core._MultiProcessValue(lambda: pid)
         c1 = Counter('c', 'help', registry=None)
-        self.assertEqual(0, self.registry.get_sample_value('c'))
+        self.assertEqual(0, self.registry.get_sample_value('c_total'))
         c1.inc(1)
         c1.inc(1)
         pid = 1
         c1.inc(1)
-        self.assertEqual(3, self.registry.get_sample_value('c'))
+        self.assertEqual(3, self.registry.get_sample_value('c_total'))
         self.assertEqual(1, c1._value.get())
 
 
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 3218a2b..a421678 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -15,6 +15,7 @@ from prometheus_client.core import (
     GaugeMetricFamily,
     HistogramMetricFamily,
     Metric,
+    Sample,
     SummaryMetricFamily,
 )
 from prometheus_client.exposition import (
@@ -89,8 +90,8 @@ redis_connected_clients{instance="rough-snowflake-web",port="6381"} 12.0
 """)
         m = Metric("redis_connected_clients", "Redis connected clients", "untyped")
         m.samples = [
-            ("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6380"}, 10),
-            ("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6381"}, 12),
+            Sample("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6380"}, 10),
+            Sample("redis_connected_clients", {"instance": "rough-snowflake-web", "port": "6381"}, 12),
         ]
         self.assertEqual([m], list(families))
 
diff --git a/tests/test_platform_collector.py b/tests/test_platform_collector.py
index 9eda5bb..529d397 100644
--- a/tests/test_platform_collector.py
+++ b/tests/test_platform_collector.py
@@ -37,9 +37,9 @@ class TestPlatformCollector(unittest.TestCase):
 
     def assertLabels(self, name, labels):
         for metric in self.registry.collect():
-            for n, l, value in metric.samples:
-                if n == name:
-                    assert l == labels
+            for s in metric.samples:
+                if s.name == name:
+                    assert s.labels == labels
                     return
         assert False