Update prom rw exporter (#1359)

This commit is contained in:
Youssef Gamal
2022-10-31 16:00:32 -04:00
committed by GitHub
parent 26d3343428
commit 0dc16a4118
22 changed files with 1684 additions and 0 deletions

View File

@ -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/*

View File

@ -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.

View File

@ -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/>`_

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
psutil
protobuf>=3.13.0
requests>=2.25.0
python-snappy>=0.5.4
opentelemetry-api
opentelemetry-sdk
opentelemetry-proto

View File

@ -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

View File

@ -0,0 +1 @@
opentelemetry

View File

@ -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

View File

@ -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/..

View File

@ -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"]

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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.

View File

@ -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,
)

View File

@ -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"

View File

@ -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]