Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Prometheus metric exporter #378

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions examples/metrics/prometheus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2020, OpenTelemetry Authors
mauriciovasquezbernal marked this conversation as resolved.
Show resolved Hide resolved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
This module serves as an example for a simple application using metrics
Examples show how to recording affects the collection of metrics to be exported
"""
from opentelemetry import metrics
from opentelemetry.ext.prometheus import PrometheusMetricsExporter
from opentelemetry.sdk.metrics import Counter, Meter
from opentelemetry.sdk.metrics.export.controller import PushController

# Meter is responsible for creating and recording metrics
metrics.set_preferred_meter_implementation(lambda _: Meter())
meter = metrics.meter()
# exporter to export metrics to Prometheus
port = 8000
address = "localhost"
prefix = "MyAppPrefix"
exporter = PrometheusMetricsExporter(port, address, prefix)
# controller collects metrics created from meter and exports it via the
# exporter every interval
controller = PushController(meter, exporter, 5)

counter = meter.create_metric(
"requests",
"number of requests",
"requests",
int,
Counter,
("environment",),
)

# Labelsets are used to identify key-values that are associated with a specific
# metric that you want to record. These are useful for pre-aggregation and can
# be used to store custom dimensions pertaining to a metric
label_set = meter.get_label_set({"environment": "staging"})

counter.add(25, label_set)
input("Press any key to exit...")
4 changes: 4 additions & 0 deletions ext/opentelemetry-ext-prometheus/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## Unreleased

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we leaving this blank?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not blank it says unreleased, once is released changelogs will make sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@c24t are we doing changelogs on this fashion? I wonder if there's an easier way to manage these than attempting to update changelogs in all repos as we go along. Then again, this may play better with pypi.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we started the other changelogs. The idea is that each PR that makes a relevant change should add a line under "unreleased", and those lines get moved under a release-specific header when we cut the release.

70 changes: 70 additions & 0 deletions ext/opentelemetry-ext-prometheus/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
OpenTelemetry Prometheus Exporter
=============================

|pypi|

.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-prometheus.svg
:target: https://pypi.org/project/opentelemetry-ext-prometheus/

This library allows to export metrics data to `Prometheus <https://prometheus.io/>`_.

Installation
------------

::

pip install opentelemetry-ext-prometheus


Usage
-----

The **OpenTelemetry Prometheus Exporter** allows to export `OpenTelemetry`_ metrics to `Prometheus`_.


.. _Prometheus: https://prometheus.io/
.. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/

.. code:: python

from opentelemetry import metrics
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
from opentelemetry.ext.prometheus import PrometheusMetricsExporter
from opentelemetry.sdk.metrics import Counter, Meter
from opentelemetry.sdk.metrics.export.controller import PushController

# Meter is responsible for creating and recording metrics
metrics.set_preferred_meter_implementation(lambda _: Meter())
meter = metrics.meter()
# exporter to export metrics to Prometheus
port = 8000
address = "localhost"
prefix = "MyAppPrefix"
exporter = PrometheusMetricsExporter(port, address, prefix)
# controller collects metrics created from meter and exports it via the
# exporter every interval
controller = PushController(meter, exporter, 5)

counter = meter.create_metric(
"requests",
"number of requests",
"requests",
int,
Counter,
("environment",),
)

# Labelsets are used to identify key-values that are associated with a specific
# metric that you want to record. These are useful for pre-aggregation and can
# be used to store custom dimensions pertaining to a metric
label_set = meter.get_label_set({"environment": "staging"})

counter.add(25, label_set)
input("Press any key to exit...")



References
----------

* `Prometheus <https://prometheus.io/>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
47 changes: 47 additions & 0 deletions ext/opentelemetry-ext-prometheus/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright 2020, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[metadata]
name = opentelemetry-ext-prometheus
description = Prometheus Metric Exporter for OpenTelemetry
long_description = file: README.rst
long_description_content_type = text/x-rst
author = OpenTelemetry Authors
author_email = cncf-opentelemetry-contributors@lists.cncf.io
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-prometheus
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7

[options]
python_requires = >=3.4
package_dir=
=src
packages=find_namespace:
install_requires =
prometheus_client >= 0.5.0, < 1.0.0
opentelemetry-api
opentelemetry-sdk
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to have a min version of the sdk that includes the metrics sdk changes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't exist yet, we can add it when is available


[options.packages.find]
where = src
26 changes: 26 additions & 0 deletions ext/opentelemetry-ext-prometheus/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2020, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os

import setuptools

BASE_DIR = os.path.dirname(__file__)
VERSION_FILENAME = os.path.join(
BASE_DIR, "src", "opentelemetry", "ext", "prometheus", "version.py"
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright 2020, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Prometheus Metrics Exporter for OpenTelemetry."""

import collections
import logging
import re
from typing import Sequence

from prometheus_client import start_http_server
from prometheus_client.core import (
REGISTRY,
CollectorRegistry,
CounterMetricFamily,
GaugeMetricFamily,
UnknownMetricFamily,
)

from opentelemetry.metrics import Counter, Gauge, Measure, Metric
from opentelemetry.sdk.metrics.export import (
MetricRecord,
MetricsExporter,
MetricsExportResult,
)

logger = logging.getLogger(__name__)


class PrometheusMetricsExporter(MetricsExporter):
"""Prometheus metric exporter for OpenTelemetry.

Args:
port: Port number to listen.
address: Endpoint address (default is localhost).
prefix: single-word application prefix relevant to the domain the metric belongs to.
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
"""

def __init__(self, port: int = 8000, address: str = "", prefix: str = ""):
self._port = port
self._address = address
self._collector = CustomCollector(prefix)

start_http_server(port=self._port, addr=self._address)
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
REGISTRY.register(self._collector)

def export(
self, metric_records: Sequence[MetricRecord]
) -> MetricsExportResult:
self._collector.add_metrics_data(metric_records)
return MetricsExportResult.SUCCESS

def shutdown(self) -> None:
REGISTRY.unregister(self._collector)


class CustomCollector:
""" CustomCollector represents the Prometheus Collector object
https://github.com/prometheus/client_python#custom-collectors
"""

def __init__(self, prefix: str = ""):
self._prefix = prefix
self._metrics_to_export = collections.deque()
self._non_letters_nor_digits_re = re.compile(
r"[^\w]", re.UNICODE | re.IGNORECASE
)

def add_metrics_data(self, metric_records: Sequence[MetricRecord]):
self._metrics_to_export.append(metric_records)

def collect(self):
"""Collect fetches the metrics from OpenTelemetry
and delivers them as Prometheus Metrics.
Collect is invoked every time a prometheus.Gatherer is run
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a link to some API that shows how this works?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom collector will use same logic as regular Prometheus Collector, pulling mechanism is configured by time intervals but is also triggered by other input like the user refreshing Prometheus visualization UI

https://github.com/prometheus/client_python#custom-collectors
https://github.com/prometheus/client_python/blob/master/prometheus_client/registry.py

for example when the HTTP endpoint is invoked by Prometheus.
"""

while self._metrics_to_export:
for metric_record in self._metrics_to_export.popleft():
prometheus_metric = self._translate_to_prometheus(
metric_record
)
if prometheus_metric is not None:
yield prometheus_metric

def _translate_to_prometheus(self, metric_record: MetricRecord):
prometheus_metric = None
label_values = []
label_keys = []
for label in metric_record.label_set.labels:
for label_tuple_value in label[1::2]:
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
label_values.append(label_tuple_value)

for label_key in metric_record.metric.label_keys:
label_keys.append(self._sanitize(label_key))
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
metric_name = ""
if self._prefix != "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do if self._prefix: I believe

metric_name = self._prefix + "_"
metric_name += self._sanitize(metric_record.metric.name)

if isinstance(metric_record.metric, Counter):
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
prometheus_metric = CounterMetricFamily(
name=metric_name,
documentation=metric_record.metric.description,
labels=label_keys,
)
prometheus_metric.add_metric(
labels=label_values, value=metric_record.aggregator.checkpoint
hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
)

elif isinstance(metric_record.metric, Gauge):
prometheus_metric = GaugeMetricFamily(
name=metric_name,
documentation=metric_record.metric.description,
labels=label_keys,
)
prometheus_metric.add_metric(
labels=label_values, value=metric_record.aggregator.checkpoint
)

# TODO: Add support for histograms when supported in OT
mauriciovasquezbernal marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(metric_record.metric, Measure):
prometheus_metric = UnknownMetricFamily(
name=metric_name,
documentation=metric_record.metric.description,
labels=label_keys,
)
prometheus_metric.add_metric(
labels=label_values, value=metric_record.aggregator.checkpoint
)

else:
logger.warning(
"Unsupported metric type. %s", type(metric_record.metric)
)
return prometheus_metric

def _sanitize(self, key):
""" sanitize the given metric name or label according to Prometheus rule.
Replace all characters other than [A-Za-z0-9_] with '_'.
"""
return self._non_letters_nor_digits_re.sub("_", key)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2020, OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "0.4.dev0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we synchronizing the versions to the api and sdk packages?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we are doing this in all other ext packages

hectorhdzg marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
Loading