Skip to content

Commit 48aef5f

Browse files
authored
Add example of prometheus exemplars being scraped from a flask app (#245)
1 parent 4cf1498 commit 48aef5f

11 files changed

Lines changed: 465 additions & 5 deletions

File tree

docs/conf.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959
"sphinx.ext.githubpages",
6060
# Support external links to different versions in the Github repo
6161
"sphinx.ext.extlinks",
62+
# Rendered graphviz graphs
63+
"sphinx.ext.graphviz",
6264
]
6365

6466
intersphinx_mapping = {
@@ -101,9 +103,11 @@
101103
}
102104

103105
html_context = {
104-
"display_github": True, # Integrate GitHub
105-
"github_user": "GoogleCloudPlatform", # Username
106-
"github_repo": "opentelemetry-operations-python", # Repo name
107-
"github_version": "main", # Version
108-
"conf_py_path": "/docs/", # Path in the checkout to the docs root
106+
"display_github": True, # Integrate GitHub
107+
"github_user": "GoogleCloudPlatform", # Username
108+
"github_repo": "opentelemetry-operations-python", # Repo name
109+
"github_version": "main", # Version
110+
"conf_py_path": "/docs/", # Path in the checkout to the docs root
109111
}
112+
113+
graphviz_output_format = "svg"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
======================================================
2+
Prometheus Metric Exemplars with OpenTelemetry Tracing
3+
======================================================
4+
5+
The full code for this example is available `on Github
6+
<https://github.com/GoogleCloudPlatform/opentelemetry-operations-python/tree/main/docs/examples/prometheus_exemplars>`_.
7+
8+
This end-to-end example shows how to instrument a Flask app with with `Prometheus
9+
<https://prometheus.io/>`_ metrics linked to OpenTelemetry traces using exemplars. The example
10+
manually adds exemplars to a Prometheus Histogram which link the metric in Google Cloud managed
11+
service for Prometheus to Spans in Cloud Trace.
12+
13+
OpenTelemetry Python is configured to send traces to the `OpenTelemetry Collector
14+
<https://opentelemetry.io/docs/collector/>`_ and the Collector scrapes the python server's
15+
Prometheus endpoint. The Collector is configured to send metrics to `Google Cloud Managed
16+
Service for Prometheus <https://cloud.google.com/stackdriver/docs/managed-prometheus>`_ and
17+
traces to `Google Cloud Trace <https://cloud.google.com/trace/docs/overview>`_.
18+
19+
.. graphviz::
20+
21+
digraph {
22+
rankdir="LR"
23+
nodesep=1
24+
25+
subgraph cluster {
26+
server [label="Flask Application"]
27+
col [label="OpenTelemetry Collector"]
28+
}
29+
30+
gmp [label="Google Cloud Managed Service for Prometheus" shape="box"]
31+
gct [label="Google Cloud Trace" shape="box"]
32+
33+
server->col [label="OTLP traces"]
34+
col->server [label="Scrape Prometheus"]
35+
col->gmp [label="metrics"]
36+
col->gct [label="traces"]
37+
}
38+
39+
To run this example you first need to:
40+
* Create a Google Cloud project. You can `create one here <https://console.cloud.google.com/projectcreate>`_.
41+
* Enable Cloud Trace API (listed in the Cloud Console as Stackdriver Trace API) in the project `here <https://console.cloud.google.com/apis/library?q=cloud%20trace&filter=visibility:public>`_. If the page says "API Enabled" then you're done! No need to do anything.
42+
* Enable Default Application Credentials by creating setting `GOOGLE_APPLICATION_CREDENTIALS <https://cloud.google.com/docs/authentication/getting-started>`_ or by `installing gcloud sdk <https://cloud.google.com/sdk/install>`_ and calling ``gcloud auth application-default login``.
43+
* Have docker and docker compose installed on your machine
44+
45+
Attaching Prometheus Exemplars
46+
------------------------------
47+
48+
Prometheus exemplars can be linked to OpenTelemetry spans by setting the ``span_id`` and
49+
``trace_id`` attributes (`specification
50+
<https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/compatibility/prometheus_and_openmetrics.md#exemplars>`_).
51+
You can get the current span using :func:`opentelemetry.trace.get_current_span`, then format
52+
its span and trace IDs to hexadecimal strings using :func:`opentelemetry.trace.format_span_id`
53+
and :func:`opentelemetry.trace.format_trace_id`.
54+
55+
.. literalinclude:: server.py
56+
:language: python
57+
:dedent:
58+
:start-after: [START opentelemetry_prom_exemplars_attach]
59+
:end-before: [END opentelemetry_prom_exemplars_attach]
60+
61+
Then make an observation using a `Prometheus Histogram
62+
<https://prometheus.io/docs/concepts/metric_types/#histogram>`_ as shown below. Google Cloud
63+
Monitoring can only display exemplars attached to Histograms.
64+
65+
.. literalinclude:: server.py
66+
:language: python
67+
:dedent:
68+
:start-after: [START opentelemetry_prom_exemplars_observe]
69+
:end-before: [END opentelemetry_prom_exemplars_observe]
70+
71+
Run
72+
---
73+
74+
Checkout the example code if you don't already have the repository cloned:
75+
76+
.. code-block:: sh
77+
78+
git clone https://github.com/GoogleCloudPlatform/opentelemetry-operations-python.git
79+
cd docs/examples/prometheus_exemplars
80+
81+
First, set the environment variables needed to provide authentication to the Collector when it
82+
runs in docker.
83+
84+
.. code-block:: sh
85+
86+
export USERID=$(id -u)
87+
export PROJECT_ID=<your-gcp-project>
88+
export GOOGLE_APPLICATION_CREDENTIALS="${HOME}/.config/gcloud/application_default_credentials.json"
89+
90+
Build and start the example containers using ``docker-compose``:
91+
92+
.. code-block:: sh
93+
94+
docker-compose up --build --abort-on-container-exit
95+
96+
This starts three containers:
97+
98+
#. The Flask server written in ``server.py``. It receives requests and simulates some work by
99+
waiting for a random amount of time.
100+
#. The OpenTelemetry Collector which receives traces from the Flask server by OTLP and scrapes
101+
Prometheus metrics from the Flask server's ``/metrics`` endpoint.
102+
#. A load generator that sends constant requests to the Flask server.
103+
104+
Checking Output
105+
---------------
106+
107+
While running the example, you can go to `Cloud Monitoring Metrics Explorer page
108+
<https://console.cloud.google.com/monitoring/metrics-explorer>`_ to see the results. Click on
109+
the "Metric" dropdown, type ``my_prom_hist``, and select the metric from under "Prometheus
110+
Target". The full metric name is ``prometheus.googleapis.com/my_prom_hist_seconds/histogram``.
111+
112+
.. image:: select_metric.png
113+
:alt: Select the metric
114+
115+
After selecting the metric, you should see something like the image below, with a heatmap
116+
showing the distribution of request durations in the Python server.
117+
118+
.. image:: heatmap.png
119+
:alt: Metrics explorer heatmap
120+
121+
The circles on the heatmap are called "exemplars", which each link to an example span that fell
122+
within the given bucket on the heatmap. Notice that exemplars are plotted at the time when they
123+
occurred (x axis) and duration they took (y axis) in the heatmap. Clicking on an exemplar opens
124+
the Trace details flyout focused on the linked span.
125+
126+
.. image:: trace_details.png
127+
:alt: Trace details flyout
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
services:
16+
# OpenTelemetry collector. Make sure you set USERID and GOOGLE_APPLICATION_CREDENTIALS
17+
# environment variables for your container to authenticate correctly
18+
otel-collector:
19+
user: ${USERID?set USERID=$(id -u)}
20+
image: otel/opentelemetry-collector-contrib:0.77.0
21+
environment:
22+
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS?}
23+
- PROJECT_ID=${PROJECT_ID?}
24+
volumes:
25+
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
26+
- ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}
27+
ports:
28+
- 4317:4317 # OTLP gRPC receiver
29+
30+
# runs server.py
31+
python-server:
32+
build:
33+
context: .
34+
dockerfile: server.dockerfile
35+
init: true
36+
ports:
37+
- 6000:6000
38+
39+
# load generator that sends requests to the python server
40+
hey-loadgen:
41+
build:
42+
context: .
43+
dockerfile: hey.dockerfile
44+
command: -c 50 -n 100000000 http://python-server:6000
251 KB
Loading
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
FROM golang:1.20-alpine
15+
RUN go install github.com/rakyll/hey@latest
16+
ENTRYPOINT ["hey"]
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
receivers:
16+
otlp:
17+
protocols:
18+
grpc:
19+
20+
# Data sources: metrics
21+
prometheus:
22+
config:
23+
scrape_configs:
24+
- job_name: python-server
25+
scrape_interval: 5s
26+
static_configs:
27+
- targets:
28+
- python-server:6000
29+
30+
processors:
31+
batch:
32+
33+
memory_limiter:
34+
check_interval: 1s
35+
limit_percentage: 65
36+
spike_limit_percentage: 20
37+
38+
# Add required GMP attributes if they are missing
39+
# https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/googlemanagedprometheusexporter#manually-setting-location-cluster-or-namespace
40+
resource:
41+
attributes:
42+
- key: "cluster"
43+
value: "example-cluster"
44+
action: upsert
45+
- key: "namespace"
46+
value: "example-apps"
47+
action: upsert
48+
- key: "location"
49+
value: "us-east1"
50+
action: upsert
51+
52+
transform:
53+
# "location", "cluster", "namespace", "job", "instance", and "project_id" are reserved, and
54+
# metrics containing these labels will be rejected. Prefix them with exported_ to prevent this.
55+
metric_statements:
56+
- context: datapoint
57+
statements:
58+
- set(attributes["exported_location"], attributes["location"])
59+
- delete_key(attributes, "location")
60+
- set(attributes["exported_cluster"], attributes["cluster"])
61+
- delete_key(attributes, "cluster")
62+
- set(attributes["exported_namespace"], attributes["namespace"])
63+
- delete_key(attributes, "namespace")
64+
- set(attributes["exported_job"], attributes["job"])
65+
- delete_key(attributes, "job")
66+
- set(attributes["exported_instance"], attributes["instance"])
67+
- delete_key(attributes, "instance")
68+
- set(attributes["exported_project_id"], attributes["project_id"])
69+
- delete_key(attributes, "project_id")
70+
71+
exporters:
72+
googlecloud:
73+
project: ${PROJECT_ID}
74+
75+
googlemanagedprometheus:
76+
project: ${PROJECT_ID}
77+
78+
service:
79+
pipelines:
80+
metrics:
81+
receivers: [otlp, prometheus]
82+
processors: [memory_limiter, resource, transform, batch]
83+
exporters: [googlemanagedprometheus]
84+
85+
traces:
86+
receivers: [otlp]
87+
processors: [memory_limiter, resource, batch]
88+
exporters: [googlecloud]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
backoff==2.2.1
2+
cachetools==5.2.0
3+
certifi==2022.12.7
4+
charset-normalizer==2.1.1
5+
click==8.1.3
6+
Deprecated==1.2.13
7+
Flask==2.2.5
8+
google-api-core==2.10.2
9+
google-auth==2.14.1
10+
google-cloud-monitoring==2.11.3
11+
google-cloud-trace==1.7.3
12+
googleapis-common-protos==1.57.0
13+
grpcio==1.50.0
14+
grpcio-status==1.50.0
15+
idna==3.4
16+
importlib-metadata==6.0.1
17+
itsdangerous==2.1.2
18+
Jinja2==3.1.2
19+
MarkupSafe==2.1.1
20+
opentelemetry-api==1.17.0
21+
opentelemetry-exporter-otlp-proto-grpc==1.17.0
22+
opentelemetry-instrumentation==0.38b0
23+
opentelemetry-instrumentation-flask==0.38b0
24+
opentelemetry-instrumentation-requests==0.38b0
25+
opentelemetry-instrumentation-wsgi==0.38b0
26+
opentelemetry-proto==1.17.0
27+
opentelemetry-sdk==1.17.0
28+
opentelemetry-semantic-conventions==0.38b0
29+
opentelemetry-util-http==0.38b0
30+
prometheus-client==0.16.0
31+
proto-plus==1.22.1
32+
protobuf==4.21.9
33+
pyasn1==0.4.8
34+
pyasn1-modules==0.2.8
35+
requests==2.28.1
36+
rsa==4.9
37+
six==1.16.0
38+
typing_extensions==4.4.0
39+
urllib3==1.26.12
40+
Werkzeug==2.2.3
41+
wrapt==1.14.1
42+
zipp==3.15.0
196 KB
Loading
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2023 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
FROM python:3.10-alpine
16+
COPY requirements.txt .
17+
RUN pip install -r requirements.txt
18+
COPY server.py .
19+
ENV FLASK_APP=server.py
20+
ENTRYPOINT ["flask", "run", "-h", "0.0.0.0", "-p", "6000"]

0 commit comments

Comments
 (0)