Update prom rw exporter (#1359)
This commit is contained in:
		
							parent
							
								
									26d3343428
								
							
						
					
					
						commit
						0dc16a4118
					
				
							
								
								
									
										1
									
								
								.flake8
								
								
								
								
							
							
						
						
									
										1
									
								
								.flake8
								
								
								
								
							|  | @ -16,6 +16,7 @@ exclude = | ||||||
|   target |   target | ||||||
|   __pycache__ |   __pycache__ | ||||||
|   exporter/opentelemetry-exporter-jaeger/src/opentelemetry/exporter/jaeger/gen/ |   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/* |   exporter/opentelemetry-exporter-jaeger/build/* | ||||||
|   docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ |   docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/grpc/gen/ | ||||||
|   docs/examples/opentelemetry-example-app/build/* |   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)) |   ([#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. | - `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)) |   ([#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 | ### Fixed | ||||||
| 
 | 
 | ||||||
|  | @ -62,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||||
| - Add metric instrumentation in starlette | - Add metric instrumentation in starlette | ||||||
|   ([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327)) |   ([#1327](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1327)) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| ### Fixed | ### Fixed | ||||||
| 
 | 
 | ||||||
| - `opentelemetry-instrumentation-boto3sqs` Make propagation compatible with other SQS instrumentations, add 'messaging.url' span attribute, and fix missing package dependencies. | - `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 |     ; opentelemetry-exporter-richconsole | ||||||
|     py3{7,8,9,10}-test-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 |     ; opentelemetry-instrumentation-mysql | ||||||
|     py3{7,8,9,10}-test-instrumentation-mysql |     py3{7,8,9,10}-test-instrumentation-mysql | ||||||
|     pypy3-test-instrumentation-mysql |     pypy3-test-instrumentation-mysql | ||||||
|  | @ -300,6 +303,7 @@ changedir = | ||||||
|   test-propagator-aws: propagator/opentelemetry-propagator-aws-xray/tests |   test-propagator-aws: propagator/opentelemetry-propagator-aws-xray/tests | ||||||
|   test-propagator-ot-trace: propagator/opentelemetry-propagator-ot-trace/tests |   test-propagator-ot-trace: propagator/opentelemetry-propagator-ot-trace/tests | ||||||
|   test-exporter-richconsole: exporter/opentelemetry-exporter-richconsole/tests |   test-exporter-richconsole: exporter/opentelemetry-exporter-richconsole/tests | ||||||
|  |   test-exporter-prometheus-remote-write: exporter/opentelemetry-exporter-prometheus-remote-write/tests | ||||||
| 
 | 
 | ||||||
| commands_pre = | commands_pre = | ||||||
| ; Install without -e to test the actual installation | ; Install without -e to test the actual installation | ||||||
|  | @ -387,6 +391,8 @@ commands_pre = | ||||||
| 
 | 
 | ||||||
|   richconsole: pip install flaky {toxinidir}/exporter/opentelemetry-exporter-richconsole[test] |   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] |   sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] | ||||||
| 
 | 
 | ||||||
|   sqlalchemy{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[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-aws-lambda[test] | ||||||
|   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-system-metrics[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-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}/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-aws-xray[test] | ||||||
|   python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] |   python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue