From faf2091bbba30f256a21d3204bc6c535286cc24e Mon Sep 17 00:00:00 2001 From: Lev Pakhomov Date: Sun, 15 Feb 2026 21:27:56 +0300 Subject: [PATCH] feat(opentelemetry): add OpenTelemetry OTLP metrics exporter --- Makefile | 2 +- docs/providers/opentelemetry.md | 34 ++++++++++++ samples/config.yaml | 5 ++ setup.cfg | 4 ++ slo_generator/exporters/opentelemetry.py | 69 ++++++++++++++++++++++++ tests/unit/fixtures/exporters.yaml | 4 ++ tests/unit/test_compute.py | 9 +++- tests/unit/test_stubs.py | 2 + 8 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 docs/providers/opentelemetry.md create mode 100644 slo_generator/exporters/opentelemetry.py diff --git a/Makefile b/Makefile index d2b17240..8bf72348 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ develop: install pre-commit install install: clean - $(PIP) install -e ."[api, datadog, prometheus, elasticsearch, opensearch, splunk, pubsub, cloud_monitoring, bigquery, dev]" + $(PIP) install -e ."[api, datadog, prometheus, elasticsearch, opensearch, splunk, pubsub, cloud_monitoring, bigquery, opentelemetry, dev]" uninstall: clean $(PIP) freeze --exclude-editable | xargs $(PIP) uninstall -y diff --git a/docs/providers/opentelemetry.md b/docs/providers/opentelemetry.md new file mode 100644 index 00000000..38e7487c --- /dev/null +++ b/docs/providers/opentelemetry.md @@ -0,0 +1,34 @@ +# OpenTelemetry + +## Exporter + +The `opentelemetry` exporter allows to export SLO metrics via [OTLP gRPC](https://opentelemetry.io/docs/specs/otlp/) to any OpenTelemetry-compatible backend (e.g., Grafana Mimir, Datadog, New Relic, Jaeger, etc.). + +**Installation:** + +```sh +pip install slo-generator[opentelemetry] +``` + +**Config example:** + +```yaml +exporters: + opentelemetry: + endpoint: ${OTLP_ENDPOINT} + headers: + Authorization: Bearer ${OTLP_TOKEN} + insecure: false +``` + +**Required fields:** + +* `endpoint`: OTLP gRPC endpoint (e.g., `localhost:4317`). + +**Optional fields:** + +* `headers`: `dict` - Headers to send with each request (e.g., for authentication). +* `insecure`: `bool` - Use insecure (non-TLS) connection. Defaults to `false`. +* `metrics`: `list` - List of metrics to export ([see docs](../shared/metrics.md)). + +All SLO metrics are exported as gauges with the `slo_` prefix (e.g., `slo_error_budget_burn_rate`). SLO report labels are mapped directly to OpenTelemetry metric attributes. diff --git a/samples/config.yaml b/samples/config.yaml index 2899d2c2..3b4c1d5f 100644 --- a/samples/config.yaml +++ b/samples/config.yaml @@ -52,6 +52,11 @@ exporters: pubsub: project_id: ${PUBSUB_PROJECT_ID} topic_name: ${PUBSUB_TOPIC_NAME} + opentelemetry: + endpoint: ${OTLP_ENDPOINT} + headers: + Authorization: Bearer ${OTLP_TOKEN} + insecure: false prometheus_self: { } error_budget_policies: diff --git a/setup.cfg b/setup.cfg index d0534b43..b0042961 100644 --- a/setup.cfg +++ b/setup.cfg @@ -92,6 +92,10 @@ cloud_storage = google-cloud-storage elasticsearch = elasticsearch +opentelemetry = + opentelemetry-api + opentelemetry-sdk + opentelemetry-exporter-otlp-proto-grpc opensearch = opensearch-py splunk = diff --git a/slo_generator/exporters/opentelemetry.py b/slo_generator/exporters/opentelemetry.py new file mode 100644 index 00000000..8875138e --- /dev/null +++ b/slo_generator/exporters/opentelemetry.py @@ -0,0 +1,69 @@ +# Copyright 2024 Google Inc. +# +# 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. +""" +`opentelemetry.py` +OpenTelemetry exporter class. +""" + +import logging + +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.resources import Resource + +from .base import MetricsExporter + +LOGGER = logging.getLogger(__name__) + + +class OpenTelemetryExporter(MetricsExporter): + """OpenTelemetry exporter class.""" + + REQUIRED_FIELDS = ["endpoint"] + OPTIONAL_FIELDS = ["headers", "insecure"] + METRIC_PREFIX = "slo_" + + def export_metric(self, data): + """Export data via OpenTelemetry OTLP. + + Args: + data (dict): Metric data. + """ + endpoint = data["endpoint"] + headers = data.get("headers") + insecure = data.get("insecure", False) + + name = data["name"] + description = data["description"] + value = data["value"] + labels = data["labels"] + + exporter_kwargs = {"endpoint": endpoint, "insecure": insecure} + if headers: + exporter_kwargs["headers"] = list(headers.items()) + + otlp_exporter = OTLPMetricExporter(**exporter_kwargs) + reader = PeriodicExportingMetricReader( + otlp_exporter, export_interval_millis=1_000_000 + ) + resource = Resource.create({"service.name": "slo-generator"}) + provider = MeterProvider(resource=resource, metric_readers=[reader]) + meter = provider.get_meter("slo-generator") + + gauge = meter.create_gauge(name=name, description=description) + gauge.set(value, attributes=labels) + + provider.force_flush() + provider.shutdown() diff --git a/tests/unit/fixtures/exporters.yaml b/tests/unit/fixtures/exporters.yaml index 8ff67708..406ea916 100644 --- a/tests/unit/fixtures/exporters.yaml +++ b/tests/unit/fixtures/exporters.yaml @@ -44,4 +44,8 @@ - good_events_count - bad_events_count + - class: OpenTelemetry + endpoint: localhost:4317 + insecure: true + - class: PrometheusSelf diff --git a/tests/unit/test_compute.py b/tests/unit/test_compute.py index 768d7018..9cd572bb 100644 --- a/tests/unit/test_compute.py +++ b/tests/unit/test_compute.py @@ -184,12 +184,19 @@ def test_export_prometheus(self, mock): export(SLO_REPORT, EXPORTERS[3]) def test_export_prometheus_self(self): - export(SLO_REPORT, EXPORTERS[7]) + export(SLO_REPORT, EXPORTERS[8]) @patch.object(Metric, "send", mock_dd_metric_send) def test_export_datadog(self): export(SLO_REPORT, EXPORTERS[4]) + @patch( + "slo_generator.exporters.opentelemetry.OTLPMetricExporter", + return_value=MagicMock(), + ) + def test_export_opentelemetry(self, mock): + export(SLO_REPORT, EXPORTERS[7]) + @patch.object(DynatraceClient, "request", side_effect=mock_dt) def test_export_dynatrace(self, mock): export(SLO_REPORT, EXPORTERS[5]) diff --git a/tests/unit/test_stubs.py b/tests/unit/test_stubs.py index 99d67094..42fc8403 100644 --- a/tests/unit/test_stubs.py +++ b/tests/unit/test_stubs.py @@ -63,6 +63,8 @@ "SPLUNK_USER": "fake", "SPLUNK_PWD": "fake", "OPENSEARCH_URL": "http://localhost:9201", + "OTLP_ENDPOINT": "localhost:4317", + "OTLP_TOKEN": "fake", }