Update prom rw exporter (#1359)
This commit is contained in:
		
							parent
							
								
									26d3343428
								
							
						
					
					
						commit
						0dc16a4118
					
				
							
								
								
									
										1
									
								
								.flake8
								
								
								
								
							
							
						
						
									
										1
									
								
								.flake8
								
								
								
								
							|  | @ -16,6 +16,7 @@ exclude = | |||
|   target | ||||
|   __pycache__ | ||||
|   exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ | ||||
|   exporter/opentelemetry-exporter-prometheus-remote-write/src/opentelemetry/exporter/prometheus_remote_write/gen/ | ||||
|   exporter/opentelemetry-exporter-jaeger/build/* | ||||
|   docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ | ||||
|   docs/examples/opentelemetry-example-app/build/* | ||||
|  |  | |||
|  | @ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
|   ([#1413](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1413)) | ||||
| - `opentelemetry-instrumentation-pyramid` Add support for regular expression matching and sanitization of HTTP headers. | ||||
|   ([#1414](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1414)) | ||||
| - Add metric exporter for Prometheus Remote Write | ||||
|   ([#1359](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1359)) | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
|  | @ -62,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
| - Add metric instrumentation in starlette | ||||
|   ([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327)) | ||||
| 
 | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies. | ||||
|  |  | |||
|  | @ -0,0 +1,29 @@ | |||
| OpenTelemetry Prometheus Remote Write Exporter | ||||
| ============================================== | ||||
| 
 | ||||
| |pypi| | ||||
| 
 | ||||
| .. |pypi| image:: https://badge.fury.io/py/opentelemetry-exporter-prometheus-remote-write.svg | ||||
|    :target: https://pypi.org/project/opentelemetry-exporter-prometheus-remote-write/ | ||||
| 
 | ||||
| This package contains an exporter to send metrics from the OpenTelemetry Python SDK directly to a Prometheus Remote Write integrated backend | ||||
| (such as Cortex or Thanos) without having to run an instance of the Prometheus server. | ||||
| 
 | ||||
| 
 | ||||
| Installation | ||||
| ------------ | ||||
| 
 | ||||
| :: | ||||
| 
 | ||||
|     pip install opentelemetry-exporter-prometheus-remote-write | ||||
| 
 | ||||
| 
 | ||||
| .. _OpenTelemetry: https://github.com/open-telemetry/opentelemetry-python/ | ||||
| .. _Prometheus Remote Write integrated backend: https://prometheus.io/docs/operating/integrations/ | ||||
| 
 | ||||
| 
 | ||||
| References | ||||
| ---------- | ||||
| 
 | ||||
| * `OpenTelemetry Project <https://opentelemetry.io/>`_ | ||||
| * `Prometheus Remote Write Integration <https://prometheus.io/docs/operating/integrations/>`_ | ||||
|  | @ -0,0 +1,11 @@ | |||
| FROM python:3.8 | ||||
| 
 | ||||
| RUN apt-get update -y && apt-get install libsnappy-dev -y | ||||
| 
 | ||||
| WORKDIR /code | ||||
| COPY . . | ||||
| 
 | ||||
| RUN pip install -e . | ||||
| RUN pip install -r ./examples/requirements.txt | ||||
| 
 | ||||
| CMD ["python", "./examples/sampleapp.py"] | ||||
|  | @ -0,0 +1,42 @@ | |||
| # Prometheus Remote Write Exporter Example | ||||
| This example uses [Docker Compose](https://docs.docker.com/compose/) to set up: | ||||
| 
 | ||||
| 1. A Python program that creates 5 instruments with 5 unique | ||||
| aggregators and a randomized load generator | ||||
| 2. An instance of [Cortex](https://cortexmetrics.io/) to receive the metrics | ||||
| data | ||||
| 3. An instance of [Grafana](https://grafana.com/) to visualizse the exported | ||||
| data | ||||
| 
 | ||||
| ## Requirements | ||||
| * Have Docker Compose [installed](https://docs.docker.com/compose/install/) | ||||
| 
 | ||||
| *Users do not need to install Python as the app will be run in the Docker Container* | ||||
| 
 | ||||
| ## Instructions | ||||
| 1. Run `docker-compose up -d` in the the `examples/` directory | ||||
| 
 | ||||
| The `-d` flag causes all services to run in detached mode and frees up your | ||||
| terminal session. This also causes no logs to show up. Users can attach themselves to the service's logs manually using `docker logs ${CONTAINER_ID} --follow` | ||||
| 
 | ||||
| 2. Log into the Grafana instance at [http://localhost:3000](http://localhost:3000) | ||||
|    * login credentials are `username: admin` and `password: admin` | ||||
|    * There may be an additional screen on setting a new password. This can be skipped and is optional | ||||
| 
 | ||||
| 3. Navigate to the `Data Sources` page | ||||
|    * Look for a gear icon on the left sidebar and select `Data Sources` | ||||
| 
 | ||||
| 4. Add a new Prometheus Data Source | ||||
|    * Use `http://cortex:9009/api/prom` as the URL | ||||
|    * (OPTIONAl) set the scrape interval to `2s` to make updates appear quickly | ||||
|    * click `Save & Test` | ||||
| 
 | ||||
| 5. Go to `Metrics Explore` to query metrics | ||||
|    * Look for a compass icon on the left sidebar | ||||
|    * click `Metrics` for a dropdown list of all the available metrics | ||||
|    * (OPTIONAL) Adjust time range by clicking the `Last 6 hours` button on the upper right side of the graph | ||||
|    * (OPTIONAL) Set up auto-refresh by selecting an option under the dropdown next to the refresh button on the upper right side of the graph | ||||
|    * Click the refresh button and data should show up on the graph | ||||
| 
 | ||||
| 6. Shutdown the services when finished | ||||
|    * Run `docker-compose down` in the examples directory | ||||
|  | @ -0,0 +1,101 @@ | |||
| # This Cortex Config is copied from the Cortex Project documentation | ||||
| # Source: https://github.com/cortexproject/cortex/blob/master/docs/configuration/single-process-config.yaml | ||||
| 
 | ||||
| # Configuration for running Cortex in single-process mode. | ||||
| # This configuration should not be used in production. | ||||
| # It is only for getting started and development. | ||||
| 
 | ||||
| # Disable the requirement that every request to Cortex has a | ||||
| # X-Scope-OrgID header. `fake` will be substituted in instead. | ||||
| # pylint: skip-file | ||||
| auth_enabled: false | ||||
| 
 | ||||
| server: | ||||
|   http_listen_port: 9009 | ||||
| 
 | ||||
|   # Configure the server to allow messages up to 100MB. | ||||
|   grpc_server_max_recv_msg_size: 104857600 | ||||
|   grpc_server_max_send_msg_size: 104857600 | ||||
|   grpc_server_max_concurrent_streams: 1000 | ||||
| 
 | ||||
| distributor: | ||||
|   shard_by_all_labels: true | ||||
|   pool: | ||||
|     health_check_ingesters: true | ||||
| 
 | ||||
| ingester_client: | ||||
|   grpc_client_config: | ||||
|     # Configure the client to allow messages up to 100MB. | ||||
|     max_recv_msg_size: 104857600 | ||||
|     max_send_msg_size: 104857600 | ||||
|     use_gzip_compression: true | ||||
| 
 | ||||
| ingester: | ||||
|   # We want our ingesters to flush chunks at the same time to optimise | ||||
|   # deduplication opportunities. | ||||
|   spread_flushes: true | ||||
|   chunk_age_jitter: 0 | ||||
| 
 | ||||
|   walconfig: | ||||
|     wal_enabled: true | ||||
|     recover_from_wal: true | ||||
|     wal_dir: /tmp/cortex/wal | ||||
| 
 | ||||
|   lifecycler: | ||||
|     # The address to advertise for this ingester.  Will be autodiscovered by | ||||
|     # looking up address on eth0 or en0; can be specified if this fails. | ||||
|     # address: 127.0.0.1 | ||||
| 
 | ||||
|     # We want to start immediately and flush on shutdown. | ||||
|     join_after: 0 | ||||
|     min_ready_duration: 0s | ||||
|     final_sleep: 0s | ||||
|     num_tokens: 512 | ||||
|     tokens_file_path: /tmp/cortex/wal/tokens | ||||
| 
 | ||||
|     # Use an in memory ring store, so we don't need to launch a Consul. | ||||
|     ring: | ||||
|       kvstore: | ||||
|         store: inmemory | ||||
|       replication_factor: 1 | ||||
| 
 | ||||
| # Use local storage - BoltDB for the index, and the filesystem | ||||
| # for the chunks. | ||||
| schema: | ||||
|   configs: | ||||
|     - from: 2019-07-29 | ||||
|       store: boltdb | ||||
|       object_store: filesystem | ||||
|       schema: v10 | ||||
|       index: | ||||
|         prefix: index_ | ||||
|         period: 1w | ||||
| 
 | ||||
| storage: | ||||
|   boltdb: | ||||
|     directory: /tmp/cortex/index | ||||
| 
 | ||||
|   filesystem: | ||||
|     directory: /tmp/cortex/chunks | ||||
| 
 | ||||
|   delete_store: | ||||
|     store: boltdb | ||||
| 
 | ||||
| purger: | ||||
|   object_store_type: filesystem | ||||
| 
 | ||||
| frontend_worker: | ||||
|   # Configure the frontend worker in the querier to match worker count | ||||
|   # to max_concurrent on the queriers. | ||||
|   match_max_concurrent: true | ||||
| 
 | ||||
| # Configure the ruler to scan the /tmp/cortex/rules directory for prometheus | ||||
| # rules: https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules | ||||
| ruler: | ||||
|   enable_api: true | ||||
|   enable_sharding: false | ||||
|   storage: | ||||
|     type: local | ||||
|     local: | ||||
|       directory: /tmp/cortex/rules | ||||
| 
 | ||||
|  | @ -0,0 +1,33 @@ | |||
| # Copyright The 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: "3.8" | ||||
| 
 | ||||
| services: | ||||
|   cortex: | ||||
|     image: quay.io/cortexproject/cortex:v1.5.0 | ||||
|     command: | ||||
|       - -config.file=./config/cortex-config.yml | ||||
|     volumes: | ||||
|       - ./cortex-config.yml:/config/cortex-config.yml:ro | ||||
|     ports: | ||||
|       - 9009:9009 | ||||
|   grafana: | ||||
|     image: grafana/grafana:latest | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|   sample_app: | ||||
|     build: | ||||
|       context: ../ | ||||
|       dockerfile: ./examples/Dockerfile | ||||
|  | @ -0,0 +1,7 @@ | |||
| psutil | ||||
| protobuf>=3.13.0 | ||||
| requests>=2.25.0 | ||||
| python-snappy>=0.5.4 | ||||
| opentelemetry-api | ||||
| opentelemetry-sdk | ||||
| opentelemetry-proto | ||||
|  | @ -0,0 +1,114 @@ | |||
| # Copyright The 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 logging | ||||
| import random | ||||
| import sys | ||||
| import time | ||||
| from logging import INFO | ||||
| 
 | ||||
| import psutil | ||||
| 
 | ||||
| from opentelemetry import metrics | ||||
| from opentelemetry.exporter.prometheus_remote_write import ( | ||||
|     PrometheusRemoteWriteMetricsExporter, | ||||
| ) | ||||
| from opentelemetry.metrics import Observation | ||||
| from opentelemetry.sdk.metrics import MeterProvider | ||||
| from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader | ||||
| 
 | ||||
| logging.basicConfig(stream=sys.stdout, level=logging.INFO) | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| testing_labels = {"environment": "testing"} | ||||
| 
 | ||||
| exporter = PrometheusRemoteWriteMetricsExporter( | ||||
|     endpoint="http://cortex:9009/api/prom/push", | ||||
|     headers={"X-Scope-Org-ID": "5"}, | ||||
| ) | ||||
| reader = PeriodicExportingMetricReader(exporter, 1000) | ||||
| provider = MeterProvider(metric_readers=[reader]) | ||||
| metrics.set_meter_provider(provider) | ||||
| meter = metrics.get_meter(__name__) | ||||
| 
 | ||||
| 
 | ||||
| # Callback to gather cpu usage | ||||
| def get_cpu_usage_callback(observer): | ||||
|     for (number, percent) in enumerate(psutil.cpu_percent(percpu=True)): | ||||
|         labels = {"cpu_number": str(number)} | ||||
|         yield Observation(percent, labels) | ||||
| 
 | ||||
| 
 | ||||
| # Callback to gather RAM usage | ||||
| def get_ram_usage_callback(observer): | ||||
|     ram_percent = psutil.virtual_memory().percent | ||||
|     yield Observation(ram_percent, {}) | ||||
| 
 | ||||
| 
 | ||||
| requests_counter = meter.create_counter( | ||||
|     name="requests", | ||||
|     description="number of requests", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| request_min_max = meter.create_counter( | ||||
|     name="requests_min_max", | ||||
|     description="min max sum count of requests", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| request_last_value = meter.create_counter( | ||||
|     name="requests_last_value", | ||||
|     description="last value number of requests", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| requests_active = meter.create_up_down_counter( | ||||
|     name="requests_active", | ||||
|     description="number of active requests", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| meter.create_observable_counter( | ||||
|     callbacks=[get_ram_usage_callback], | ||||
|     name="ram_usage", | ||||
|     description="ram usage", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| meter.create_observable_up_down_counter( | ||||
|     callbacks=[get_cpu_usage_callback], | ||||
|     name="cpu_percent", | ||||
|     description="per-cpu usage", | ||||
|     unit="1", | ||||
| ) | ||||
| 
 | ||||
| request_latency = meter.create_histogram("request_latency") | ||||
| 
 | ||||
| # Load generator | ||||
| num = random.randint(0, 1000) | ||||
| while True: | ||||
|     # counters | ||||
|     requests_counter.add(num % 131 + 200, testing_labels) | ||||
|     request_min_max.add(num % 181 + 200, testing_labels) | ||||
|     request_last_value.add(num % 101 + 200, testing_labels) | ||||
| 
 | ||||
|     # updown counter | ||||
|     requests_active.add(num % 7231 + 200, testing_labels) | ||||
| 
 | ||||
|     request_latency.record(num % 92, testing_labels) | ||||
|     logger.log(level=INFO, msg="completed metrics collection cycle") | ||||
|     time.sleep(1) | ||||
|     num += 9791 | ||||
|  | @ -0,0 +1 @@ | |||
| opentelemetry | ||||
|  | @ -0,0 +1,3 @@ | |||
| ## Instructions | ||||
| 1. Install protobuf tools. Can use your package manager or download from [GitHub](https://github.com/protocolbuffers/protobuf/releases/tag/v21.7) | ||||
| 2. Run `generate-proto-py.sh` from inside the `proto/` directory | ||||
|  | @ -0,0 +1,57 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| PROM_VERSION=v2.39.0 | ||||
| PROTO_VERSION=v1.3.2 | ||||
| 
 | ||||
| # SRC_DIR is from protoc perspective. ie its the destination for our checkouts/clones | ||||
| SRC_DIR=opentelemetry/exporter/prometheus_remote_write/gen/ | ||||
| DST_DIR=../src/opentelemetry/exporter/prometheus_remote_write/gen/ | ||||
| 
 | ||||
| #TODO: | ||||
| # Check that black & protoc are installed properly | ||||
| echo "Creating our destination directory" | ||||
| mkdir -p ${SRC_DIR}/gogoproto | ||||
| 
 | ||||
| # Clone prometheus | ||||
| echo "Grabbing Prometheus protobuf files" | ||||
| git clone --filter=blob:none --sparse https://github.com/prometheus/prometheus.git | ||||
| cd prometheus | ||||
| git checkout ${PROM_VERSION} | ||||
| git sparse-checkout set prompb | ||||
| cd .. | ||||
| 
 | ||||
| 
 | ||||
| # We also need gogo.proto which is in the protobuf Repo | ||||
| # Could also try to pull this locally from the install location of protobuf | ||||
| # but that will be harder in a platform agnostic way. | ||||
| echo "Grabbing gogo.proto" | ||||
| git clone --filter=blob:none --sparse https://github.com/gogo/protobuf.git | ||||
| cd protobuf | ||||
| git checkout ${PROTO_VERSION} | ||||
| git sparse-checkout set /gogoproto/gogo.proto | ||||
| cd .. | ||||
| 
 | ||||
| # Move the proto files into our structure | ||||
| echo "Moving proto files to ${SRC_DIR}" | ||||
| cp prometheus/prompb/remote.proto prometheus/prompb/types.proto ${SRC_DIR} | ||||
| cp protobuf/gogoproto/gogo.proto ${SRC_DIR}/gogoproto/ | ||||
| 
 | ||||
| 
 | ||||
| # A bit of a hack, but we need to fix the imports to fit the python structure. | ||||
| # using sed to find the 3 files and point them at each other using OUR structure | ||||
| echo "Fixing imports" | ||||
| sed -i 's/import "types.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/types.proto";/' ${SRC_DIR}/remote.proto | ||||
| sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/remote.proto | ||||
| sed -i 's/import "gogoproto\/gogo.proto";/import "opentelemetry\/exporter\/prometheus_remote_write\/gen\/gogoproto\/gogo.proto";/' ${SRC_DIR}/types.proto | ||||
| 
 | ||||
| 
 | ||||
| # Cleanup the repos | ||||
| echo "Removing clones..." | ||||
| rm -rf protobuf prometheus | ||||
| 
 | ||||
| # Used libprotoc 3.21.1 & protoc 21.7 | ||||
| echo "Compiling proto files to Python" | ||||
| protoc -I .  --python_out=../src ${SRC_DIR}/gogoproto/gogo.proto ${SRC_DIR}/remote.proto ${SRC_DIR}/types.proto | ||||
| 
 | ||||
| echo "Running formatting on the generated files" | ||||
| ../../../scripts/eachdist.py format --path $PWD/.. | ||||
|  | @ -0,0 +1,51 @@ | |||
| [build-system] | ||||
| requires = ["hatchling"] | ||||
| build-backend = "hatchling.build" | ||||
| 
 | ||||
| 
 | ||||
| [project] | ||||
| name = "opentelemetry-exporter-prometheus-remote-write" | ||||
| dynamic = ["version"] | ||||
| description = "Prometheus Remote Write Metrics Exporter for OpenTelemetry" | ||||
| readme = "README.rst" | ||||
| license = "Apache-2.0" | ||||
| requires-python = ">=3.7" | ||||
| authors = [ | ||||
|   { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, | ||||
| ] | ||||
| classifiers = [ | ||||
|   "Development Status :: 4 - Beta", | ||||
|   "Intended Audience :: Developers", | ||||
|   "License :: OSI Approved :: Apache Software License", | ||||
|   "Programming Language :: Python", | ||||
|   "Programming Language :: Python :: 3", | ||||
|   "Programming Language :: Python :: 3.7", | ||||
|   "Programming Language :: Python :: 3.8", | ||||
|   "Programming Language :: Python :: 3.9", | ||||
|   "Programming Language :: Python :: 3.10", | ||||
| ] | ||||
| dependencies = [ | ||||
|   "protobuf ~= 4.21", | ||||
|   "requests ~= 2.28", | ||||
|   "opentelemetry-api ~= 1.12", | ||||
|   "opentelemetry-sdk ~= 1.12", | ||||
|   "python-snappy ~= 0.6", | ||||
| ] | ||||
| 
 | ||||
| [project.optional-dependencies] | ||||
| test = [] | ||||
| 
 | ||||
| [project.urls] | ||||
| Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/exporter/opentelemetry-exporter-prometheus-remote-write" | ||||
| 
 | ||||
| [tool.hatch.version] | ||||
| path = "src/opentelemetry/exporter/prometheus_remote_write/version.py" | ||||
| 
 | ||||
| [tool.hatch.build.targets.sdist] | ||||
| include = [ | ||||
|   "/src", | ||||
|   "/tests", | ||||
| ] | ||||
| 
 | ||||
| [tool.hatch.build.targets.wheel] | ||||
| packages = ["src/opentelemetry"] | ||||
|  | @ -0,0 +1,414 @@ | |||
| # Copyright The 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 logging | ||||
| import re | ||||
| from collections import defaultdict | ||||
| from itertools import chain | ||||
| from typing import Dict, Sequence | ||||
| 
 | ||||
| import requests | ||||
| import snappy | ||||
| 
 | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2 import (  # pylint: disable=no-name-in-module | ||||
|     WriteRequest, | ||||
| ) | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import (  # pylint: disable=no-name-in-module | ||||
|     Label, | ||||
|     Sample, | ||||
|     TimeSeries, | ||||
| ) | ||||
| from opentelemetry.sdk.metrics import Counter | ||||
| from opentelemetry.sdk.metrics import Histogram as ClientHistogram | ||||
| from opentelemetry.sdk.metrics import ( | ||||
|     ObservableCounter, | ||||
|     ObservableGauge, | ||||
|     ObservableUpDownCounter, | ||||
|     UpDownCounter, | ||||
| ) | ||||
| from opentelemetry.sdk.metrics.export import ( | ||||
|     AggregationTemporality, | ||||
|     Gauge, | ||||
|     Histogram, | ||||
|     Metric, | ||||
|     MetricExporter, | ||||
|     MetricExportResult, | ||||
|     MetricsData, | ||||
|     Sum, | ||||
| ) | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| PROMETHEUS_NAME_REGEX = re.compile(r"^\d|[^\w:]") | ||||
| PROMETHEUS_LABEL_REGEX = re.compile(r"^\d|[^\w]") | ||||
| UNDERSCORE_REGEX = re.compile(r"_+") | ||||
| 
 | ||||
| 
 | ||||
| class PrometheusRemoteWriteMetricsExporter(MetricExporter): | ||||
|     """ | ||||
|     Prometheus remote write metric exporter for OpenTelemetry. | ||||
| 
 | ||||
|     Args: | ||||
|         endpoint: url where data will be sent (Required) | ||||
|         basic_auth: username and password for authentication (Optional) | ||||
|         headers: additional headers for remote write request (Optional) | ||||
|         timeout: timeout for remote write requests in seconds, defaults to 30 (Optional) | ||||
|         proxies: dict mapping request proxy protocols to proxy urls (Optional) | ||||
|         tls_config: configuration for remote write TLS settings (Optional) | ||||
|     """ | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         endpoint: str, | ||||
|         basic_auth: Dict = None, | ||||
|         headers: Dict = None, | ||||
|         timeout: int = 30, | ||||
|         tls_config: Dict = None, | ||||
|         proxies: Dict = None, | ||||
|         resources_as_labels: bool = True, | ||||
|         preferred_temporality: Dict[type, AggregationTemporality] = None, | ||||
|         preferred_aggregation: Dict = None, | ||||
|     ): | ||||
|         self.endpoint = endpoint | ||||
|         self.basic_auth = basic_auth | ||||
|         self.headers = headers | ||||
|         self.timeout = timeout | ||||
|         self.tls_config = tls_config | ||||
|         self.proxies = proxies | ||||
|         self.resources_as_labels = resources_as_labels | ||||
| 
 | ||||
|         if not preferred_temporality: | ||||
|             preferred_temporality = { | ||||
|                 Counter: AggregationTemporality.CUMULATIVE, | ||||
|                 UpDownCounter: AggregationTemporality.CUMULATIVE, | ||||
|                 ClientHistogram: AggregationTemporality.CUMULATIVE, | ||||
|                 ObservableCounter: AggregationTemporality.CUMULATIVE, | ||||
|                 ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, | ||||
|                 ObservableGauge: AggregationTemporality.CUMULATIVE, | ||||
|             } | ||||
| 
 | ||||
|         super().__init__(preferred_temporality, preferred_aggregation) | ||||
| 
 | ||||
|     @property | ||||
|     def endpoint(self): | ||||
|         return self._endpoint | ||||
| 
 | ||||
|     @endpoint.setter | ||||
|     def endpoint(self, endpoint: str): | ||||
|         if endpoint == "": | ||||
|             raise ValueError("endpoint required") | ||||
|         self._endpoint = endpoint | ||||
| 
 | ||||
|     @property | ||||
|     def basic_auth(self): | ||||
|         return self._basic_auth | ||||
| 
 | ||||
|     @basic_auth.setter | ||||
|     def basic_auth(self, basic_auth: Dict): | ||||
|         if basic_auth: | ||||
|             if "username" not in basic_auth: | ||||
|                 raise ValueError("username required in basic_auth") | ||||
|             if "password_file" in basic_auth: | ||||
|                 if "password" in basic_auth: | ||||
|                     raise ValueError( | ||||
|                         "basic_auth cannot contain password and password_file" | ||||
|                     ) | ||||
|                 with open(  # pylint: disable=unspecified-encoding | ||||
|                     basic_auth["password_file"] | ||||
|                 ) as file: | ||||
|                     basic_auth["password"] = file.readline().strip() | ||||
|             elif "password" not in basic_auth: | ||||
|                 raise ValueError("password required in basic_auth") | ||||
|         self._basic_auth = basic_auth | ||||
| 
 | ||||
|     @property | ||||
|     def timeout(self): | ||||
|         return self._timeout | ||||
| 
 | ||||
|     @timeout.setter | ||||
|     def timeout(self, timeout: int): | ||||
|         if timeout <= 0: | ||||
|             raise ValueError("timeout must be greater than 0") | ||||
|         self._timeout = timeout | ||||
| 
 | ||||
|     @property | ||||
|     def tls_config(self): | ||||
|         return self._tls_config | ||||
| 
 | ||||
|     @tls_config.setter | ||||
|     def tls_config(self, tls_config: Dict): | ||||
|         if tls_config: | ||||
|             new_config = {} | ||||
|             if "ca_file" in tls_config: | ||||
|                 new_config["ca_file"] = tls_config["ca_file"] | ||||
|             if "cert_file" in tls_config and "key_file" in tls_config: | ||||
|                 new_config["cert_file"] = tls_config["cert_file"] | ||||
|                 new_config["key_file"] = tls_config["key_file"] | ||||
|             elif "cert_file" in tls_config or "key_file" in tls_config: | ||||
|                 raise ValueError( | ||||
|                     "tls_config requires both cert_file and key_file" | ||||
|                 ) | ||||
|             if "insecure_skip_verify" in tls_config: | ||||
|                 new_config["insecure_skip_verify"] = tls_config[ | ||||
|                     "insecure_skip_verify" | ||||
|                 ] | ||||
|         self._tls_config = tls_config | ||||
| 
 | ||||
|     @property | ||||
|     def proxies(self): | ||||
|         return self._proxies | ||||
| 
 | ||||
|     @proxies.setter | ||||
|     def proxies(self, proxies: Dict): | ||||
|         self._proxies = proxies | ||||
| 
 | ||||
|     @property | ||||
|     def headers(self): | ||||
|         return self._headers | ||||
| 
 | ||||
|     @headers.setter | ||||
|     def headers(self, headers: Dict): | ||||
|         self._headers = headers | ||||
| 
 | ||||
|     def export( | ||||
|         self, | ||||
|         metrics_data: MetricsData, | ||||
|         timeout_millis: float = 10_000, | ||||
|         **kwargs, | ||||
|     ) -> MetricExportResult: | ||||
|         if not metrics_data: | ||||
|             return MetricExportResult.SUCCESS | ||||
|         timeseries = self._translate_data(metrics_data) | ||||
|         if not timeseries: | ||||
|             logger.error( | ||||
|                 "All records contain unsupported aggregators, export aborted" | ||||
|             ) | ||||
|             return MetricExportResult.FAILURE | ||||
|         message = self._build_message(timeseries) | ||||
|         headers = self._build_headers() | ||||
|         return self._send_message(message, headers) | ||||
| 
 | ||||
|     def _translate_data(self, data: MetricsData) -> Sequence[TimeSeries]: | ||||
|         rw_timeseries = [] | ||||
| 
 | ||||
|         for resource_metrics in data.resource_metrics: | ||||
|             resource = resource_metrics.resource | ||||
|             # OTLP Data model suggests combining some attrs into  job/instance | ||||
|             # Should we do that here? | ||||
|             if self.resources_as_labels: | ||||
|                 resource_labels = [ | ||||
|                     (n, str(v)) for n, v in resource.attributes.items() | ||||
|                 ] | ||||
|             else: | ||||
|                 resource_labels = [] | ||||
|             # Scope name/version probably not too useful from a labeling perspective | ||||
|             for scope_metrics in resource_metrics.scope_metrics: | ||||
|                 for metric in scope_metrics.metrics: | ||||
|                     rw_timeseries.extend( | ||||
|                         self._parse_metric(metric, resource_labels) | ||||
|                     ) | ||||
|         return rw_timeseries | ||||
| 
 | ||||
|     def _parse_metric( | ||||
|         self, metric: Metric, resource_labels: Sequence | ||||
|     ) -> Sequence[TimeSeries]: | ||||
|         """ | ||||
|         Parses the Metric & lower objects, then converts the output into | ||||
|         OM TimeSeries. Returns a List of TimeSeries objects based on one Metric | ||||
|         """ | ||||
| 
 | ||||
|         # Create the metric name, will be a label later | ||||
|         if metric.unit: | ||||
|             # Prom. naming guidelines add unit to the name | ||||
|             name = f"{metric.name}_{metric.unit}" | ||||
|         else: | ||||
|             name = metric.name | ||||
| 
 | ||||
|         # datapoints have attributes associated with them. these would be sent | ||||
|         # to RW as different metrics: name & labels is a unique time series | ||||
|         sample_sets = defaultdict(list) | ||||
|         if isinstance(metric.data, (Gauge, Sum)): | ||||
|             for dp in metric.data.data_points: | ||||
|                 attrs, sample = self._parse_data_point(dp, name) | ||||
|                 sample_sets[attrs].append(sample) | ||||
|         elif isinstance(metric.data, Histogram): | ||||
|             for dp in metric.data.data_points: | ||||
|                 dp_result = self._parse_histogram_data_point(dp, name) | ||||
|                 for attrs, sample in dp_result: | ||||
|                     sample_sets[attrs].append(sample) | ||||
|         else: | ||||
|             logger.warning("Unsupported Metric Type: %s", type(metric.data)) | ||||
|             return [] | ||||
|         return self._convert_to_timeseries(sample_sets, resource_labels) | ||||
| 
 | ||||
|     def _convert_to_timeseries( | ||||
|         self, sample_sets: Sequence[tuple], resource_labels: Sequence | ||||
|     ) -> Sequence[TimeSeries]: | ||||
|         timeseries = [] | ||||
|         for labels, samples in sample_sets.items(): | ||||
|             ts = TimeSeries() | ||||
|             for label_name, label_value in chain(resource_labels, labels): | ||||
|                 # Previous implementation did not str() the names... | ||||
|                 ts.labels.append(self._label(label_name, str(label_value))) | ||||
|             for value, timestamp in samples: | ||||
|                 ts.samples.append(self._sample(value, timestamp)) | ||||
|             timeseries.append(ts) | ||||
|         return timeseries | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _sample(value: int, timestamp: int) -> Sample: | ||||
|         sample = Sample() | ||||
|         sample.value = value | ||||
|         sample.timestamp = timestamp | ||||
|         return sample | ||||
| 
 | ||||
|     def _label(self, name: str, value: str) -> Label: | ||||
|         label = Label() | ||||
|         label.name = self._sanitize_string(name, "label") | ||||
|         label.value = value | ||||
|         return label | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _sanitize_string(string: str, type_: str) -> str: | ||||
|         # I Think Prometheus requires names to NOT start with a number this | ||||
|         # would not catch that, but do cover the other cases. The naming rules | ||||
|         # don't explicit say this, but the supplied regex implies it. | ||||
|         # Got a little weird trying to do substitution with it, but can be | ||||
|         # fixed if we allow numeric beginnings to metric names | ||||
|         if type_ == "name": | ||||
|             sanitized = PROMETHEUS_NAME_REGEX.sub("_", string) | ||||
|         elif type_ == "label": | ||||
|             sanitized = PROMETHEUS_LABEL_REGEX.sub("_", string) | ||||
|         else: | ||||
|             raise TypeError(f"Unsupported string type: {type_}") | ||||
| 
 | ||||
|         # Remove consecutive underscores | ||||
|         # TODO: Unfortunately this clobbbers __name__ | ||||
|         # sanitized = UNDERSCORE_REGEX.sub("_",sanitized) | ||||
| 
 | ||||
|         return sanitized | ||||
| 
 | ||||
|     def _parse_histogram_data_point(self, data_point, name): | ||||
| 
 | ||||
|         sample_attr_pairs = [] | ||||
| 
 | ||||
|         base_attrs = list(data_point.attributes.items()) | ||||
|         timestamp = data_point.time_unix_nano // 1_000_000 | ||||
| 
 | ||||
|         def handle_bucket(value, bound=None, name_override=None): | ||||
|             # Metric Level attributes + the bucket boundary attribute + name | ||||
|             ts_attrs = base_attrs.copy() | ||||
|             ts_attrs.append( | ||||
|                 ( | ||||
|                     "__name__", | ||||
|                     self._sanitize_string(name_override or name, "name"), | ||||
|                 ) | ||||
|             ) | ||||
|             if bound: | ||||
|                 ts_attrs.append(("le", str(bound))) | ||||
|             # Value is count of values in each bucket | ||||
|             ts_sample = (value, timestamp) | ||||
|             return tuple(ts_attrs), ts_sample | ||||
| 
 | ||||
|         for bound_pos, bound in enumerate(data_point.explicit_bounds): | ||||
|             sample_attr_pairs.append( | ||||
|                 handle_bucket(data_point.bucket_counts[bound_pos], bound) | ||||
|             ) | ||||
| 
 | ||||
|         # Add the last label for implicit +inf bucket | ||||
|         sample_attr_pairs.append( | ||||
|             handle_bucket(data_point.bucket_counts[-1], bound="+Inf") | ||||
|         ) | ||||
| 
 | ||||
|         # Lastly, add series for count & sum | ||||
|         sample_attr_pairs.append( | ||||
|             handle_bucket(data_point.sum, name_override=f"{name}_sum") | ||||
|         ) | ||||
|         sample_attr_pairs.append( | ||||
|             handle_bucket(data_point.count, name_override=f"{name}_count") | ||||
|         ) | ||||
|         return sample_attr_pairs | ||||
| 
 | ||||
|     def _parse_data_point(self, data_point, name=None): | ||||
| 
 | ||||
|         attrs = tuple(data_point.attributes.items()) + ( | ||||
|             ("__name__", self._sanitize_string(name, "name")), | ||||
|         ) | ||||
|         sample = (data_point.value, (data_point.time_unix_nano // 1_000_000)) | ||||
|         return attrs, sample | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def _build_message(timeseries: Sequence[TimeSeries]) -> bytes: | ||||
|         write_request = WriteRequest() | ||||
|         write_request.timeseries.extend(timeseries) | ||||
|         serialized_message = write_request.SerializeToString() | ||||
|         return snappy.compress(serialized_message) | ||||
| 
 | ||||
|     def _build_headers(self) -> Dict: | ||||
|         headers = { | ||||
|             "Content-Encoding": "snappy", | ||||
|             "Content-Type": "application/x-protobuf", | ||||
|             "X-Prometheus-Remote-Write-Version": "0.1.0", | ||||
|         } | ||||
|         if self.headers: | ||||
|             for header_name, header_value in self.headers.items(): | ||||
|                 headers[header_name] = header_value | ||||
|         return headers | ||||
| 
 | ||||
|     def _send_message( | ||||
|         self, message: bytes, headers: Dict | ||||
|     ) -> MetricExportResult: | ||||
|         auth = None | ||||
|         if self.basic_auth: | ||||
|             auth = (self.basic_auth["username"], self.basic_auth["password"]) | ||||
| 
 | ||||
|         cert = None | ||||
|         verify = True | ||||
|         if self.tls_config: | ||||
|             if "ca_file" in self.tls_config: | ||||
|                 verify = self.tls_config["ca_file"] | ||||
|             elif "insecure_skip_verify" in self.tls_config: | ||||
|                 verify = self.tls_config["insecure_skip_verify"] | ||||
| 
 | ||||
|             if ( | ||||
|                 "cert_file" in self.tls_config | ||||
|                 and "key_file" in self.tls_config | ||||
|             ): | ||||
|                 cert = ( | ||||
|                     self.tls_config["cert_file"], | ||||
|                     self.tls_config["key_file"], | ||||
|                 ) | ||||
|         try: | ||||
|             response = requests.post( | ||||
|                 self.endpoint, | ||||
|                 data=message, | ||||
|                 headers=headers, | ||||
|                 auth=auth, | ||||
|                 timeout=self.timeout, | ||||
|                 proxies=self.proxies, | ||||
|                 cert=cert, | ||||
|                 verify=verify, | ||||
|             ) | ||||
|             if not response.ok: | ||||
|                 response.raise_for_status() | ||||
|         except requests.exceptions.RequestException as err: | ||||
|             logger.error("Export POST request failed with reason: %s", err) | ||||
|             return MetricExportResult.FAILURE | ||||
|         return MetricExportResult.SUCCESS | ||||
| 
 | ||||
|     def force_flush(self, timeout_millis: float = 10_000) -> bool: | ||||
|         return True | ||||
| 
 | ||||
|     def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: | ||||
|         pass | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -0,0 +1,59 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by the protocol buffer compiler.  DO NOT EDIT! | ||||
| # source: opentelemetry/exporter/prometheus_remote_write/gen/remote.proto | ||||
| """Generated protocol buffer code.""" | ||||
| from google.protobuf.internal import builder as _builder | ||||
| from google.protobuf import descriptor as _descriptor | ||||
| from google.protobuf import descriptor_pool as _descriptor_pool | ||||
| from google.protobuf import symbol_database as _symbol_database | ||||
| 
 | ||||
| # @@protoc_insertion_point(imports) | ||||
| 
 | ||||
| _sym_db = _symbol_database.Default() | ||||
| 
 | ||||
| 
 | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen import ( | ||||
|     types_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_types__pb2, | ||||
| ) | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import ( | ||||
|     gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( | ||||
|     b'\n?opentelemetry/exporter/prometheus_remote_write/gen/remote.proto\x12\nprometheus\x1a>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto"z\n\x0cWriteRequest\x12\x30\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeriesB\x04\xc8\xde\x1f\x00\x12\x32\n\x08metadata\x18\x03 \x03(\x0b\x32\x1a.prometheus.MetricMetadataB\x04\xc8\xde\x1f\x00J\x04\x08\x02\x10\x03"\xae\x01\n\x0bReadRequest\x12"\n\x07queries\x18\x01 \x03(\x0b\x32\x11.prometheus.Query\x12\x45\n\x17\x61\x63\x63\x65pted_response_types\x18\x02 \x03(\x0e\x32$.prometheus.ReadRequest.ResponseType"4\n\x0cResponseType\x12\x0b\n\x07SAMPLES\x10\x00\x12\x17\n\x13STREAMED_XOR_CHUNKS\x10\x01"8\n\x0cReadResponse\x12(\n\x07results\x18\x01 \x03(\x0b\x32\x17.prometheus.QueryResult"\x8f\x01\n\x05Query\x12\x1a\n\x12start_timestamp_ms\x18\x01 \x01(\x03\x12\x18\n\x10\x65nd_timestamp_ms\x18\x02 \x01(\x03\x12*\n\x08matchers\x18\x03 \x03(\x0b\x32\x18.prometheus.LabelMatcher\x12$\n\x05hints\x18\x04 \x01(\x0b\x32\x15.prometheus.ReadHints"9\n\x0bQueryResult\x12*\n\ntimeseries\x18\x01 \x03(\x0b\x32\x16.prometheus.TimeSeries"]\n\x13\x43hunkedReadResponse\x12\x31\n\x0e\x63hunked_series\x18\x01 \x03(\x0b\x32\x19.prometheus.ChunkedSeries\x12\x13\n\x0bquery_index\x18\x02 \x01(\x03\x42\x08Z\x06prompbb\x06proto3' | ||||
| ) | ||||
| 
 | ||||
| _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) | ||||
| _builder.BuildTopDescriptorsAndMessages( | ||||
|     DESCRIPTOR, | ||||
|     "opentelemetry.exporter.prometheus_remote_write.gen.remote_pb2", | ||||
|     globals(), | ||||
| ) | ||||
| if _descriptor._USE_C_DESCRIPTORS == False: | ||||
| 
 | ||||
|     DESCRIPTOR._options = None | ||||
|     DESCRIPTOR._serialized_options = b"Z\006prompb" | ||||
|     _WRITEREQUEST.fields_by_name["timeseries"]._options = None | ||||
|     _WRITEREQUEST.fields_by_name[ | ||||
|         "timeseries" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _WRITEREQUEST.fields_by_name["metadata"]._options = None | ||||
|     _WRITEREQUEST.fields_by_name[ | ||||
|         "metadata" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _WRITEREQUEST._serialized_start = 216 | ||||
|     _WRITEREQUEST._serialized_end = 338 | ||||
|     _READREQUEST._serialized_start = 341 | ||||
|     _READREQUEST._serialized_end = 515 | ||||
|     _READREQUEST_RESPONSETYPE._serialized_start = 463 | ||||
|     _READREQUEST_RESPONSETYPE._serialized_end = 515 | ||||
|     _READRESPONSE._serialized_start = 517 | ||||
|     _READRESPONSE._serialized_end = 573 | ||||
|     _QUERY._serialized_start = 576 | ||||
|     _QUERY._serialized_end = 719 | ||||
|     _QUERYRESULT._serialized_start = 721 | ||||
|     _QUERYRESULT._serialized_end = 778 | ||||
|     _CHUNKEDREADRESPONSE._serialized_start = 780 | ||||
|     _CHUNKEDREADRESPONSE._serialized_end = 873 | ||||
| # @@protoc_insertion_point(module_scope) | ||||
|  | @ -0,0 +1,86 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| # Generated by the protocol buffer compiler.  DO NOT EDIT! | ||||
| # source: opentelemetry/exporter/prometheus_remote_write/gen/types.proto | ||||
| """Generated protocol buffer code.""" | ||||
| from google.protobuf.internal import builder as _builder | ||||
| from google.protobuf import descriptor as _descriptor | ||||
| from google.protobuf import descriptor_pool as _descriptor_pool | ||||
| from google.protobuf import symbol_database as _symbol_database | ||||
| 
 | ||||
| # @@protoc_insertion_point(imports) | ||||
| 
 | ||||
| _sym_db = _symbol_database.Default() | ||||
| 
 | ||||
| 
 | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen.gogoproto import ( | ||||
|     gogo_pb2 as opentelemetry_dot_exporter_dot_prometheus__remote__write_dot_gen_dot_gogoproto_dot_gogo__pb2, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( | ||||
|     b'\n>opentelemetry/exporter/prometheus_remote_write/gen/types.proto\x12\nprometheus\x1aGopentelemetry/exporter/prometheus_remote_write/gen/gogoproto/gogo.proto"\xf8\x01\n\x0eMetricMetadata\x12\x33\n\x04type\x18\x01 \x01(\x0e\x32%.prometheus.MetricMetadata.MetricType\x12\x1a\n\x12metric_family_name\x18\x02 \x01(\t\x12\x0c\n\x04help\x18\x04 \x01(\t\x12\x0c\n\x04unit\x18\x05 \x01(\t"y\n\nMetricType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07\x43OUNTER\x10\x01\x12\t\n\x05GAUGE\x10\x02\x12\r\n\tHISTOGRAM\x10\x03\x12\x12\n\x0eGAUGEHISTOGRAM\x10\x04\x12\x0b\n\x07SUMMARY\x10\x05\x12\x08\n\x04INFO\x10\x06\x12\x0c\n\x08STATESET\x10\x07"*\n\x06Sample\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x11\n\ttimestamp\x18\x02 \x01(\x03"U\n\x08\x45xemplar\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\r\n\x05value\x18\x02 \x01(\x01\x12\x11\n\ttimestamp\x18\x03 \x01(\x03"\x8f\x01\n\nTimeSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12)\n\x07samples\x18\x02 \x03(\x0b\x32\x12.prometheus.SampleB\x04\xc8\xde\x1f\x00\x12-\n\texemplars\x18\x03 \x03(\x0b\x32\x14.prometheus.ExemplarB\x04\xc8\xde\x1f\x00"$\n\x05Label\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t"1\n\x06Labels\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00"\x82\x01\n\x0cLabelMatcher\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.prometheus.LabelMatcher.Type\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t"(\n\x04Type\x12\x06\n\x02\x45Q\x10\x00\x12\x07\n\x03NEQ\x10\x01\x12\x06\n\x02RE\x10\x02\x12\x07\n\x03NRE\x10\x03"|\n\tReadHints\x12\x0f\n\x07step_ms\x18\x01 \x01(\x03\x12\x0c\n\x04\x66unc\x18\x02 \x01(\t\x12\x10\n\x08start_ms\x18\x03 \x01(\x03\x12\x0e\n\x06\x65nd_ms\x18\x04 \x01(\x03\x12\x10\n\x08grouping\x18\x05 \x03(\t\x12\n\n\x02\x62y\x18\x06 \x01(\x08\x12\x10\n\x08range_ms\x18\x07 \x01(\x03"\x8b\x01\n\x05\x43hunk\x12\x13\n\x0bmin_time_ms\x18\x01 \x01(\x03\x12\x13\n\x0bmax_time_ms\x18\x02 \x01(\x03\x12(\n\x04type\x18\x03 \x01(\x0e\x32\x1a.prometheus.Chunk.Encoding\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c" \n\x08\x45ncoding\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x07\n\x03XOR\x10\x01"a\n\rChunkedSeries\x12\'\n\x06labels\x18\x01 \x03(\x0b\x32\x11.prometheus.LabelB\x04\xc8\xde\x1f\x00\x12\'\n\x06\x63hunks\x18\x02 \x03(\x0b\x32\x11.prometheus.ChunkB\x04\xc8\xde\x1f\x00\x42\x08Z\x06prompbb\x06proto3' | ||||
| ) | ||||
| 
 | ||||
| _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) | ||||
| _builder.BuildTopDescriptorsAndMessages( | ||||
|     DESCRIPTOR, | ||||
|     "opentelemetry.exporter.prometheus_remote_write.gen.types_pb2", | ||||
|     globals(), | ||||
| ) | ||||
| if _descriptor._USE_C_DESCRIPTORS == False: | ||||
| 
 | ||||
|     DESCRIPTOR._options = None | ||||
|     DESCRIPTOR._serialized_options = b"Z\006prompb" | ||||
|     _EXEMPLAR.fields_by_name["labels"]._options = None | ||||
|     _EXEMPLAR.fields_by_name[ | ||||
|         "labels" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _TIMESERIES.fields_by_name["labels"]._options = None | ||||
|     _TIMESERIES.fields_by_name[ | ||||
|         "labels" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _TIMESERIES.fields_by_name["samples"]._options = None | ||||
|     _TIMESERIES.fields_by_name[ | ||||
|         "samples" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _TIMESERIES.fields_by_name["exemplars"]._options = None | ||||
|     _TIMESERIES.fields_by_name[ | ||||
|         "exemplars" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _LABELS.fields_by_name["labels"]._options = None | ||||
|     _LABELS.fields_by_name["labels"]._serialized_options = b"\310\336\037\000" | ||||
|     _CHUNKEDSERIES.fields_by_name["labels"]._options = None | ||||
|     _CHUNKEDSERIES.fields_by_name[ | ||||
|         "labels" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _CHUNKEDSERIES.fields_by_name["chunks"]._options = None | ||||
|     _CHUNKEDSERIES.fields_by_name[ | ||||
|         "chunks" | ||||
|     ]._serialized_options = b"\310\336\037\000" | ||||
|     _METRICMETADATA._serialized_start = 152 | ||||
|     _METRICMETADATA._serialized_end = 400 | ||||
|     _METRICMETADATA_METRICTYPE._serialized_start = 279 | ||||
|     _METRICMETADATA_METRICTYPE._serialized_end = 400 | ||||
|     _SAMPLE._serialized_start = 402 | ||||
|     _SAMPLE._serialized_end = 444 | ||||
|     _EXEMPLAR._serialized_start = 446 | ||||
|     _EXEMPLAR._serialized_end = 531 | ||||
|     _TIMESERIES._serialized_start = 534 | ||||
|     _TIMESERIES._serialized_end = 677 | ||||
|     _LABEL._serialized_start = 679 | ||||
|     _LABEL._serialized_end = 715 | ||||
|     _LABELS._serialized_start = 717 | ||||
|     _LABELS._serialized_end = 766 | ||||
|     _LABELMATCHER._serialized_start = 769 | ||||
|     _LABELMATCHER._serialized_end = 899 | ||||
|     _LABELMATCHER_TYPE._serialized_start = 859 | ||||
|     _LABELMATCHER_TYPE._serialized_end = 899 | ||||
|     _READHINTS._serialized_start = 901 | ||||
|     _READHINTS._serialized_end = 1025 | ||||
|     _CHUNK._serialized_start = 1028 | ||||
|     _CHUNK._serialized_end = 1167 | ||||
|     _CHUNK_ENCODING._serialized_start = 1135 | ||||
|     _CHUNK_ENCODING._serialized_end = 1167 | ||||
|     _CHUNKEDSERIES._serialized_start = 1169 | ||||
|     _CHUNKEDSERIES._serialized_end = 1266 | ||||
| # @@protoc_insertion_point(module_scope) | ||||
|  | @ -0,0 +1,15 @@ | |||
| # Copyright The 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.34b0" | ||||
|  | @ -0,0 +1,13 @@ | |||
| # Copyright The 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. | ||||
|  | @ -0,0 +1,66 @@ | |||
| import random | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| import opentelemetry.test.metrictestutil as metric_util | ||||
| from opentelemetry.exporter.prometheus_remote_write import ( | ||||
|     PrometheusRemoteWriteMetricsExporter, | ||||
| ) | ||||
| from opentelemetry.sdk.metrics.export import ( | ||||
|     AggregationTemporality, | ||||
|     Histogram, | ||||
|     HistogramDataPoint, | ||||
|     Metric, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def prom_rw(): | ||||
|     return PrometheusRemoteWriteMetricsExporter( | ||||
|         "http://victoria:8428/api/v1/write" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def metric(request): | ||||
|     if hasattr(request, "param"): | ||||
|         type_ = request.param | ||||
|     else: | ||||
|         type_ = random.choice(["gauge", "sum"]) | ||||
| 
 | ||||
|     if type_ == "gauge": | ||||
|         return metric_util._generate_gauge( | ||||
|             "test.gauge", random.randint(0, 100) | ||||
|         ) | ||||
|     if type_ == "sum": | ||||
|         return metric_util._generate_sum( | ||||
|             "test.sum", random.randint(0, 9_999_999_999) | ||||
|         ) | ||||
|     if type_ == "histogram": | ||||
|         return _generate_histogram("test_histogram") | ||||
| 
 | ||||
|     raise ValueError(f"Unsupported metric type '{type_}'.") | ||||
| 
 | ||||
| 
 | ||||
| def _generate_histogram(name): | ||||
|     dp = HistogramDataPoint( | ||||
|         attributes={"foo": "bar", "baz": 42}, | ||||
|         start_time_unix_nano=1641946016139533244, | ||||
|         time_unix_nano=1641946016139533244, | ||||
|         count=5, | ||||
|         sum=420, | ||||
|         bucket_counts=[1, 4], | ||||
|         explicit_bounds=[10.0], | ||||
|         min=8, | ||||
|         max=80, | ||||
|     ) | ||||
|     data = Histogram( | ||||
|         [dp], | ||||
|         AggregationTemporality.CUMULATIVE, | ||||
|     ) | ||||
|     return Metric( | ||||
|         name, | ||||
|         "foo", | ||||
|         "tu", | ||||
|         data=data, | ||||
|     ) | ||||
|  | @ -0,0 +1,309 @@ | |||
| # Copyright The 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 unittest | ||||
| from unittest.mock import patch | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from opentelemetry.exporter.prometheus_remote_write import ( | ||||
|     PrometheusRemoteWriteMetricsExporter, | ||||
| ) | ||||
| from opentelemetry.exporter.prometheus_remote_write.gen.types_pb2 import (  # pylint: disable=E0611 | ||||
|     TimeSeries, | ||||
| ) | ||||
| from opentelemetry.sdk.metrics.export import ( | ||||
|     Histogram, | ||||
|     HistogramDataPoint, | ||||
|     MetricExportResult, | ||||
|     MetricsData, | ||||
|     NumberDataPoint, | ||||
|     ResourceMetrics, | ||||
|     ScopeMetrics, | ||||
| ) | ||||
| from opentelemetry.sdk.resources import Resource | ||||
| from opentelemetry.sdk.util.instrumentation import InstrumentationScope | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "name,result", | ||||
|     [ | ||||
|         ("abc.124", "abc_124"), | ||||
|         (":abc", ":abc"), | ||||
|         ("abc.name.hi", "abc_name_hi"), | ||||
|         ("service.name...", "service_name___"), | ||||
|         ("4hellowor:ld5∂©∑", "_hellowor:ld5___"), | ||||
|     ], | ||||
| ) | ||||
| def test_regex(name, result, prom_rw): | ||||
|     assert prom_rw._sanitize_string(name, "name") == result | ||||
| 
 | ||||
| 
 | ||||
| def test_regex_invalid(prom_rw): | ||||
|     with pytest.raises(TypeError): | ||||
|         prom_rw("foo_bar", "A random type") | ||||
| 
 | ||||
| 
 | ||||
| def test_parse_data_point(prom_rw): | ||||
| 
 | ||||
|     attrs = {"Foo": "Bar", "Baz": 42} | ||||
|     timestamp = 1641946016139533244 | ||||
|     value = 242.42 | ||||
|     dp = NumberDataPoint(attrs, 0, timestamp, value) | ||||
|     name = "abc.123_42" | ||||
|     labels, sample = prom_rw._parse_data_point(dp, name) | ||||
| 
 | ||||
|     name = "abc_123_42" | ||||
|     assert labels == (("Foo", "Bar"), ("Baz", 42), ("__name__", name)) | ||||
|     assert sample == (value, timestamp // 1_000_000) | ||||
| 
 | ||||
| 
 | ||||
| def test_parse_histogram_dp(prom_rw): | ||||
|     attrs = {"foo": "bar", "baz": 42} | ||||
|     timestamp = 1641946016139533244 | ||||
|     bounds = [10.0, 20.0] | ||||
|     dp = HistogramDataPoint( | ||||
|         attributes=attrs, | ||||
|         start_time_unix_nano=1641946016139533244, | ||||
|         time_unix_nano=timestamp, | ||||
|         count=9, | ||||
|         sum=180, | ||||
|         bucket_counts=[1, 4, 4], | ||||
|         explicit_bounds=bounds, | ||||
|         min=8, | ||||
|         max=80, | ||||
|     ) | ||||
|     name = "foo_histogram" | ||||
|     label_sample_pairs = prom_rw._parse_histogram_data_point(dp, name) | ||||
|     timestamp = timestamp // 1_000_000 | ||||
|     bounds.append("+Inf") | ||||
|     for pos, bound in enumerate(bounds): | ||||
|         # We have to attributes, we kinda assume the bucket label is last... | ||||
|         assert ("le", str(bound)) == label_sample_pairs[pos][0][-1] | ||||
|         # Check and make sure we are putting the bucket counts in there | ||||
|         assert (dp.bucket_counts[pos], timestamp) == label_sample_pairs[pos][1] | ||||
| 
 | ||||
|     # Last two are the sum & total count | ||||
|     assert ("__name__", f"{name}_sum") in label_sample_pairs[-2][0] | ||||
|     assert (dp.sum, timestamp) == label_sample_pairs[-2][1] | ||||
| 
 | ||||
|     assert ("__name__", f"{name}_count") in label_sample_pairs[-1][0] | ||||
|     assert (dp.count, timestamp) == label_sample_pairs[-1][1] | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "metric", | ||||
|     [ | ||||
|         "gauge", | ||||
|         "sum", | ||||
|         "histogram", | ||||
|     ], | ||||
|     indirect=["metric"], | ||||
| ) | ||||
| def test_parse_metric(metric, prom_rw): | ||||
|     """ | ||||
|     Ensures output from parse_metrics are TimeSeries with expected data/size | ||||
|     """ | ||||
|     attributes = { | ||||
|         "service_name": "foo", | ||||
|         "bool_value": True, | ||||
|     } | ||||
| 
 | ||||
|     assert ( | ||||
|         len(metric.data.data_points) == 1 | ||||
|     ), "We can only support a single datapoint in tests" | ||||
|     series = prom_rw._parse_metric(metric, tuple(attributes.items())) | ||||
|     timestamp = metric.data.data_points[0].time_unix_nano // 1_000_000 | ||||
|     for single_series in series: | ||||
|         labels = str(single_series.labels) | ||||
|         # Its a bit easier to validate these stringified where we dont have to | ||||
|         # worry about ordering and protobuf TimeSeries object structure | ||||
|         # This doesn't guarantee the labels aren't mixed up, but our other | ||||
|         # test cases already do. | ||||
|         assert "__name__" in labels | ||||
|         assert prom_rw._sanitize_string(metric.name, "name") in labels | ||||
|         combined_attrs = list(attributes.items()) + list( | ||||
|             metric.data.data_points[0].attributes.items() | ||||
|         ) | ||||
|         for name, value in combined_attrs: | ||||
|             assert prom_rw._sanitize_string(name, "label") in labels | ||||
|             assert str(value) in labels | ||||
|         if isinstance(metric.data, Histogram): | ||||
|             values = [ | ||||
|                 metric.data.data_points[0].count, | ||||
|                 metric.data.data_points[0].sum, | ||||
|                 metric.data.data_points[0].bucket_counts[0], | ||||
|                 metric.data.data_points[0].bucket_counts[1], | ||||
|             ] | ||||
|         else: | ||||
|             values = [ | ||||
|                 metric.data.data_points[0].value, | ||||
|             ] | ||||
|         for sample in single_series.samples: | ||||
|             assert sample.timestamp == timestamp | ||||
|             assert sample.value in values | ||||
| 
 | ||||
| 
 | ||||
| class TestValidation(unittest.TestCase): | ||||
|     # Test cases to ensure exporter parameter validation works as intended | ||||
|     def test_valid_standard_param(self): | ||||
|         exporter = PrometheusRemoteWriteMetricsExporter( | ||||
|             endpoint="/prom/test_endpoint", | ||||
|         ) | ||||
|         self.assertEqual(exporter.endpoint, "/prom/test_endpoint") | ||||
| 
 | ||||
|     def test_valid_basic_auth_param(self): | ||||
|         exporter = PrometheusRemoteWriteMetricsExporter( | ||||
|             endpoint="/prom/test_endpoint", | ||||
|             basic_auth={ | ||||
|                 "username": "test_username", | ||||
|                 "password": "test_password", | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(exporter.basic_auth["username"], "test_username") | ||||
|         self.assertEqual(exporter.basic_auth["password"], "test_password") | ||||
| 
 | ||||
|     def test_invalid_no_endpoint_param(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter("") | ||||
| 
 | ||||
|     def test_invalid_no_username_param(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", | ||||
|                 basic_auth={"password": "test_password"}, | ||||
|             ) | ||||
| 
 | ||||
|     def test_invalid_no_password_param(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", | ||||
|                 basic_auth={"username": "test_username"}, | ||||
|             ) | ||||
| 
 | ||||
|     def test_invalid_conflicting_passwords_param(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", | ||||
|                 basic_auth={ | ||||
|                     "username": "test_username", | ||||
|                     "password": "test_password", | ||||
|                     "password_file": "test_file", | ||||
|                 }, | ||||
|             ) | ||||
| 
 | ||||
|     def test_invalid_timeout_param(self): | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", timeout=0 | ||||
|             ) | ||||
| 
 | ||||
|     def test_valid_tls_config_param(self): | ||||
|         tls_config = { | ||||
|             "ca_file": "test_ca_file", | ||||
|             "cert_file": "test_cert_file", | ||||
|             "key_file": "test_key_file", | ||||
|             "insecure_skip_verify": True, | ||||
|         } | ||||
|         exporter = PrometheusRemoteWriteMetricsExporter( | ||||
|             endpoint="/prom/test_endpoint", tls_config=tls_config | ||||
|         ) | ||||
|         self.assertEqual(exporter.tls_config["ca_file"], tls_config["ca_file"]) | ||||
|         self.assertEqual( | ||||
|             exporter.tls_config["cert_file"], tls_config["cert_file"] | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             exporter.tls_config["key_file"], tls_config["key_file"] | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             exporter.tls_config["insecure_skip_verify"], | ||||
|             tls_config["insecure_skip_verify"], | ||||
|         ) | ||||
| 
 | ||||
|     # if cert_file is provided, then key_file must also be provided | ||||
|     def test_invalid_tls_config_cert_only_param(self): | ||||
|         tls_config = {"cert_file": "value"} | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", tls_config=tls_config | ||||
|             ) | ||||
| 
 | ||||
|     # if cert_file is provided, then key_file must also be provided | ||||
|     def test_invalid_tls_config_key_only_param(self): | ||||
|         tls_config = {"cert_file": "value"} | ||||
|         with self.assertRaises(ValueError): | ||||
|             PrometheusRemoteWriteMetricsExporter( | ||||
|                 endpoint="/prom/test_endpoint", tls_config=tls_config | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| # Ensures export is successful with valid export_records and config | ||||
| @patch("requests.post") | ||||
| def test_valid_export(mock_post, prom_rw, metric): | ||||
|     mock_post.return_value.configure_mock(**{"status_code": 200}) | ||||
| 
 | ||||
|     # Assumed a "None" for Scope or Resource aren't valid, so build them here | ||||
|     scope = ScopeMetrics( | ||||
|         InstrumentationScope(name="prom-rw-test"), [metric], None | ||||
|     ) | ||||
|     resource = ResourceMetrics( | ||||
|         Resource({"service.name": "foo"}), [scope], None | ||||
|     ) | ||||
|     record = MetricsData([resource]) | ||||
| 
 | ||||
|     result = prom_rw.export(record) | ||||
|     assert result == MetricExportResult.SUCCESS | ||||
|     assert mock_post.call_count == 1 | ||||
| 
 | ||||
|     result = prom_rw.export([]) | ||||
|     assert result == MetricExportResult.SUCCESS | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_export(prom_rw): | ||||
|     record = MetricsData([]) | ||||
| 
 | ||||
|     result = prom_rw.export(record) | ||||
|     assert result == MetricExportResult.FAILURE | ||||
| 
 | ||||
| 
 | ||||
| @patch("requests.post") | ||||
| def test_valid_send_message(mock_post, prom_rw): | ||||
|     mock_post.return_value.configure_mock(**{"ok": True}) | ||||
|     result = prom_rw._send_message(bytes(), {}) | ||||
|     assert mock_post.call_count == 1 | ||||
|     assert result == MetricExportResult.SUCCESS | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_send_message(prom_rw): | ||||
|     result = prom_rw._send_message(bytes(), {}) | ||||
|     assert result == MetricExportResult.FAILURE | ||||
| 
 | ||||
| 
 | ||||
| # Verifies that build_message calls snappy.compress and returns SerializedString | ||||
| @patch("snappy.compress", return_value=bytes()) | ||||
| def test_build_message(mock_compress, prom_rw): | ||||
|     message = prom_rw._build_message([TimeSeries()]) | ||||
|     assert mock_compress.call_count == 1 | ||||
|     assert isinstance(message, bytes) | ||||
| 
 | ||||
| 
 | ||||
| # Ensure correct headers are added when valid config is provided | ||||
| def test_build_headers(prom_rw): | ||||
|     prom_rw.headers = {"Custom Header": "test_header"} | ||||
| 
 | ||||
|     headers = prom_rw._build_headers() | ||||
|     assert headers["Content-Encoding"] == "snappy" | ||||
|     assert headers["Content-Type"] == "application/x-protobuf" | ||||
|     assert headers["X-Prometheus-Remote-Write-Version"] == "0.1.0" | ||||
|     assert headers["Custom Header"] == "test_header" | ||||
							
								
								
									
										7
									
								
								tox.ini
								
								
								
								
							
							
						
						
									
										7
									
								
								tox.ini
								
								
								
								
							|  | @ -104,6 +104,9 @@ envlist = | |||
|     ; opentelemetry-exporter-richconsole | ||||
|     py3{7,8,9,10}-test-exporter-richconsole | ||||
| 
 | ||||
|     ; opentelemetry-exporter-prometheus-remote-write | ||||
|     py3{6,7,8,9,10}-test-exporter-prometheus-remote-write | ||||
| 
 | ||||
|     ; opentelemetry-instrumentation-mysql | ||||
|     py3{7,8,9,10}-test-instrumentation-mysql | ||||
|     pypy3-test-instrumentation-mysql | ||||
|  | @ -300,6 +303,7 @@ changedir = | |||
|   test-propagator-aws: propagator/opentelemetry-propagator-aws-xray/tests | ||||
|   test-propagator-ot-trace: propagator/opentelemetry-propagator-ot-trace/tests | ||||
|   test-exporter-richconsole: exporter/opentelemetry-exporter-richconsole/tests | ||||
|   test-exporter-prometheus-remote-write: exporter/opentelemetry-exporter-prometheus-remote-write/tests | ||||
| 
 | ||||
| commands_pre = | ||||
| ; Install without -e to test the actual installation | ||||
|  | @ -387,6 +391,8 @@ commands_pre = | |||
| 
 | ||||
|   richconsole: pip install flaky {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] | ||||
| 
 | ||||
|   prometheus: pip install {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write[test] | ||||
| 
 | ||||
|   sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] | ||||
| 
 | ||||
|   sqlalchemy{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] | ||||
|  | @ -498,6 +504,7 @@ commands_pre = | |||
|   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aws-lambda[test] | ||||
|   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[test] | ||||
|   python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] | ||||
|   python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write[test] | ||||
|   python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test] | ||||
|   python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-aws-xray[test] | ||||
|   python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue