Skip to content

Commit 26e0576

Browse files
authored
Prometheus metric exporter (open-telemetry#378)
prometheus-exporter: initial commit
1 parent 0a8ecd1 commit 26e0576

File tree

10 files changed

+542
-0
lines changed

10 files changed

+542
-0
lines changed

examples/metrics/prometheus.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""
16+
This module serves as an example for a simple application using metrics
17+
Examples show how to recording affects the collection of metrics to be exported
18+
"""
19+
20+
from prometheus_client import start_http_server
21+
22+
from opentelemetry import metrics
23+
from opentelemetry.ext.prometheus import PrometheusMetricsExporter
24+
from opentelemetry.sdk.metrics import Counter, Meter
25+
from opentelemetry.sdk.metrics.export.controller import PushController
26+
27+
# Start Prometheus client
28+
start_http_server(port=8000, addr="localhost")
29+
30+
# Meter is responsible for creating and recording metrics
31+
metrics.set_preferred_meter_implementation(lambda _: Meter())
32+
meter = metrics.meter()
33+
# exporter to export metrics to Prometheus
34+
prefix = "MyAppPrefix"
35+
exporter = PrometheusMetricsExporter(prefix)
36+
# controller collects metrics created from meter and exports it via the
37+
# exporter every interval
38+
controller = PushController(meter, exporter, 5)
39+
40+
counter = meter.create_metric(
41+
"requests",
42+
"number of requests",
43+
"requests",
44+
int,
45+
Counter,
46+
("environment",),
47+
)
48+
49+
# Labelsets are used to identify key-values that are associated with a specific
50+
# metric that you want to record. These are useful for pre-aggregation and can
51+
# be used to store custom dimensions pertaining to a metric
52+
label_set = meter.get_label_set({"environment": "staging"})
53+
54+
counter.add(25, label_set)
55+
input("Press any key to exit...")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## Unreleased
4+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
OpenTelemetry Prometheus Exporter
2+
=============================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-prometheus.svg
7+
:target: https://pypi.org/project/opentelemetry-ext-prometheus/
8+
9+
This library allows to export metrics data to `Prometheus <https://prometheus.io/>`_.
10+
11+
Installation
12+
------------
13+
14+
::
15+
16+
pip install opentelemetry-ext-prometheus
17+
18+
19+
Usage
20+
-----
21+
22+
The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metrics to `Prometheus`_.
23+
24+
25+
.. _Prometheus: https://prometheus.io/
26+
.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/
27+
28+
.. code:: python
29+
30+
from opentelemetry import metrics
31+
from opentelemetry.ext.prometheus import PrometheusMetricsExporter
32+
from opentelemetry.sdk.metrics import Counter, Meter
33+
from opentelemetry.sdk.metrics.export.controller import PushController
34+
from prometheus_client import start_http_server
35+
36+
# Start Prometheus client
37+
start_http_server(port=8000, addr="localhost")
38+
39+
# Meter is responsible for creating and recording metrics
40+
metrics.set_preferred_meter_implementation(lambda _: Meter())
41+
meter = metrics.meter()
42+
# exporter to export metrics to Prometheus
43+
prefix = "MyAppPrefix"
44+
exporter = PrometheusMetricsExporter(prefix)
45+
# controller collects metrics created from meter and exports it via the
46+
# exporter every interval
47+
controller = PushController(meter, exporter, 5)
48+
49+
counter = meter.create_metric(
50+
"requests",
51+
"number of requests",
52+
"requests",
53+
int,
54+
Counter,
55+
("environment",),
56+
)
57+
58+
# Labelsets are used to identify key-values that are associated with a specific
59+
# metric that you want to record. These are useful for pre-aggregation and can
60+
# be used to store custom dimensions pertaining to a metric
61+
label_set = meter.get_label_set({"environment": "staging"})
62+
63+
counter.add(25, label_set)
64+
input("Press any key to exit...")
65+
66+
67+
68+
References
69+
----------
70+
71+
* `Prometheus <https://prometheus.io/>`_
72+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
[metadata]
16+
name = opentelemetry-ext-prometheus
17+
description = Prometheus Metric Exporter for OpenTelemetry
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = cncf-opentelemetry-contributors@lists.cncf.io
22+
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-prometheus
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 3 - Alpha
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.4
32+
Programming Language :: Python :: 3.5
33+
Programming Language :: Python :: 3.6
34+
Programming Language :: Python :: 3.7
35+
36+
[options]
37+
python_requires = >=3.4
38+
package_dir=
39+
=src
40+
packages=find_namespace:
41+
install_requires =
42+
prometheus_client >= 0.5.0, < 1.0.0
43+
opentelemetry-api
44+
opentelemetry-sdk
45+
46+
[options.packages.find]
47+
where = src
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR, "src", "opentelemetry", "ext", "prometheus", "version.py"
21+
)
22+
PACKAGE_INFO = {}
23+
with open(VERSION_FILENAME) as f:
24+
exec(f.read(), PACKAGE_INFO)
25+
26+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Prometheus Metrics Exporter for OpenTelemetry."""
16+
17+
import collections
18+
import logging
19+
import re
20+
from typing import Sequence
21+
22+
from prometheus_client import start_http_server
23+
from prometheus_client.core import (
24+
REGISTRY,
25+
CollectorRegistry,
26+
CounterMetricFamily,
27+
GaugeMetricFamily,
28+
UnknownMetricFamily,
29+
)
30+
31+
from opentelemetry.metrics import Counter, Gauge, Measure, Metric
32+
from opentelemetry.sdk.metrics.export import (
33+
MetricRecord,
34+
MetricsExporter,
35+
MetricsExportResult,
36+
)
37+
38+
logger = logging.getLogger(__name__)
39+
40+
41+
class PrometheusMetricsExporter(MetricsExporter):
42+
"""Prometheus metric exporter for OpenTelemetry.
43+
44+
Args:
45+
prefix: single-word application prefix relevant to the domain
46+
the metric belongs to.
47+
"""
48+
49+
def __init__(self, prefix: str = ""):
50+
self._collector = CustomCollector(prefix)
51+
REGISTRY.register(self._collector)
52+
53+
def export(
54+
self, metric_records: Sequence[MetricRecord]
55+
) -> MetricsExportResult:
56+
self._collector.add_metrics_data(metric_records)
57+
return MetricsExportResult.SUCCESS
58+
59+
def shutdown(self) -> None:
60+
REGISTRY.unregister(self._collector)
61+
62+
63+
class CustomCollector:
64+
""" CustomCollector represents the Prometheus Collector object
65+
https://github.com/prometheus/client_python#custom-collectors
66+
"""
67+
68+
def __init__(self, prefix: str = ""):
69+
self._prefix = prefix
70+
self._metrics_to_export = collections.deque()
71+
self._non_letters_nor_digits_re = re.compile(
72+
r"[^\w]", re.UNICODE | re.IGNORECASE
73+
)
74+
75+
def add_metrics_data(self, metric_records: Sequence[MetricRecord]):
76+
self._metrics_to_export.append(metric_records)
77+
78+
def collect(self):
79+
"""Collect fetches the metrics from OpenTelemetry
80+
and delivers them as Prometheus Metrics.
81+
Collect is invoked every time a prometheus.Gatherer is run
82+
for example when the HTTP endpoint is invoked by Prometheus.
83+
"""
84+
85+
while self._metrics_to_export:
86+
for metric_record in self._metrics_to_export.popleft():
87+
prometheus_metric = self._translate_to_prometheus(
88+
metric_record
89+
)
90+
if prometheus_metric is not None:
91+
yield prometheus_metric
92+
93+
def _translate_to_prometheus(self, metric_record: MetricRecord):
94+
prometheus_metric = None
95+
label_values = []
96+
label_keys = []
97+
for label_tuple in metric_record.label_set.labels:
98+
label_keys.append(self._sanitize(label_tuple[0]))
99+
label_values.append(label_tuple[1])
100+
101+
metric_name = ""
102+
if self._prefix != "":
103+
metric_name = self._prefix + "_"
104+
metric_name += self._sanitize(metric_record.metric.name)
105+
106+
if isinstance(metric_record.metric, Counter):
107+
prometheus_metric = CounterMetricFamily(
108+
name=metric_name,
109+
documentation=metric_record.metric.description,
110+
labels=label_keys,
111+
)
112+
prometheus_metric.add_metric(
113+
labels=label_values, value=metric_record.aggregator.checkpoint
114+
)
115+
116+
elif isinstance(metric_record.metric, Gauge):
117+
prometheus_metric = GaugeMetricFamily(
118+
name=metric_name,
119+
documentation=metric_record.metric.description,
120+
labels=label_keys,
121+
)
122+
prometheus_metric.add_metric(
123+
labels=label_values, value=metric_record.aggregator.checkpoint
124+
)
125+
126+
# TODO: Add support for histograms when supported in OT
127+
elif isinstance(metric_record.metric, Measure):
128+
prometheus_metric = UnknownMetricFamily(
129+
name=metric_name,
130+
documentation=metric_record.metric.description,
131+
labels=label_keys,
132+
)
133+
prometheus_metric.add_metric(
134+
labels=label_values, value=metric_record.aggregator.checkpoint
135+
)
136+
137+
else:
138+
logger.warning(
139+
"Unsupported metric type. %s", type(metric_record.metric)
140+
)
141+
return prometheus_metric
142+
143+
def _sanitize(self, key):
144+
""" sanitize the given metric name or label according to Prometheus rule.
145+
Replace all characters other than [A-Za-z0-9_] with '_'.
146+
"""
147+
return self._non_letters_nor_digits_re.sub("_", key)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.4.dev0"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2020, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.

0 commit comments

Comments
 (0)