Skip to content

Commit 84b0c04

Browse files
Add documentation about OpenTelemetry 🔭 (#21)
* Add documentation for OpenTelemetry * Update main.yml
1 parent f365c1c commit 84b0c04

11 files changed

Lines changed: 290 additions & 31 deletions

‎.github/workflows/main.yml‎

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ on:
2121
paths-ignore:
2222
- README.md
2323

24+
permissions:
25+
contents: read
26+
pages: write
27+
id-token: write
28+
29+
concurrency:
30+
group: "pages"
31+
cancel-in-progress: false
32+
2433
jobs:
2534
build:
2635
strategy:
@@ -56,39 +65,19 @@ jobs:
5665
name: site
5766
path: site.zip
5867

68+
- name: Upload artifact
69+
uses: actions/upload-pages-artifact@v3
70+
with:
71+
path: ./site
72+
5973
publish:
6074
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_artifacts == 'Y')
6175
needs: build
6276
runs-on: ubuntu-latest
63-
77+
environment:
78+
name: github-pages
79+
url: ${{ steps.deployment.outputs.page_url }}
6480
steps:
65-
- name: Checkout repository
66-
uses: actions/checkout@v4
67-
with:
68-
ref: gh-pages
69-
path: neoteroi
70-
71-
- name: Download artifact
72-
uses: actions/download-artifact@v4
73-
with:
74-
name: site
75-
path: site
76-
77-
- name: Unzip artifact
78-
run: |
79-
unzip site/site.zip -d site
80-
81-
- name: Deploy to gh-pages branch
82-
run: |
83-
find neoteroi -mindepth 1 ! -name '.git' ! -name 'CNAME' ! -name 'README.md' ! -path 'neoteroi/.git/*' -exec rm -rf {} +
84-
cp -r site/site/* neoteroi/
85-
86-
cd neoteroi
87-
git config user.name "${GITHUB_ACTOR}"
88-
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
89-
90-
git add .
91-
git commit -m "Deploy documentation on $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
92-
93-
git push origin gh-pages
94-
echo "Published to gh-pages"
81+
- name: Deploy to GitHub Pages
82+
id: deployment
83+
uses: actions/deploy-pages@v4
71.5 KB
Loading
132 KB
Loading
59.5 KB
Loading
128 KB
Loading
177 KB
Loading
69.2 KB
Loading
95.3 KB
Loading
172 KB
Loading
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Introduction to OpenTelemetry
2+
3+
[OpenTelemetry (OTEL)](https://opentelemetry.io/) is an open-source
4+
observability framework for cloud-native software. It provides a standard for
5+
collecting, processing, and exporting telemetry data (such as traces, metrics,
6+
and logs) from applications. By using OpenTelemetry, you can gain deep insights
7+
into the performance and behavior of your distributed systems, making it easier
8+
to monitor, troubleshoot, and optimize your applications.
9+
10+
OpenTelemetry is vendor-agnostic and supports integration with many popular
11+
observability backends, including [Grafana](https://grafana.com/),
12+
[Jaeger](https://www.jaegertracing.io/), [Zipkin](https://zipkin.io/),
13+
[Prometheus](https://prometheus.io/), and others. It is a unified standard that
14+
simplifies the process of instrumenting code and collecting telemetry data,
15+
enabling to build robust monitoring and tracing solutions with small effort.
16+
17+
In the context of web frameworks like BlackSheep, OpenTelemetry can be used to
18+
automatically trace incoming requests, outgoing HTTP calls, and other
19+
operations, providing end-to-end visibility into your application's execution
20+
flow.
21+
22+
## Enabling OpenTelemetry in BlackSheep
23+
24+
BlackSheep offers built-in support for OpenTelemetry since version `2.3.2`, but
25+
it is anyway simple to use OpenTelemetry in previous versions of the web
26+
framework.
27+
28+
/// tab | Since v2.3.2
29+
30+
To enable OpenTelemetry integration starting with version `2.3.2`, you can
31+
import common functions from the `blacksheep.server.otel` namespace.
32+
33+
```python
34+
# To use exporters of your choice:
35+
from blacksheep.server.otel import use_open_telemetry
36+
37+
# To use a service that supports the OpenTelemetry Protocol:
38+
from blacksheep.server.otel.otlp import use_open_telemetry_otlp
39+
40+
```
41+
42+
More information is provided below.
43+
44+
---
45+
46+
///
47+
48+
/// tab | Before v2.3.2
49+
50+
To enable OpenTelemetry integration before version `2.3.2`, follow the example
51+
included in [BlackSheep-Examples/otel](https://github.com/Neoteroi/BlackSheep-Examples/tree/main/otel).
52+
53+
- Copy the code in the [`otel/__init__.py`](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/otel/otel/__init__.py)
54+
and [`otel/otlp.py`](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/otel/otel/otlp.py) folder
55+
into your own system.
56+
- Follow the instructions in the repository and the information below.
57+
58+
---
59+
60+
///
61+
62+
The common code consists of a middleware that enables tracing of all web requests
63+
and exceptions (`OTELMiddleware`), logging instrumentation, and application
64+
callbacks to ensure logs are collected and flushed properly.
65+
66+
The common code is _vendor-agnostic_ because it is intentionally abstracted
67+
from specific observability services. Depending on the service you want to use,
68+
you need to configure specific _exporters_, which are classes responsible to
69+
send collected logs and traces towards a certain OTEL service. If the service
70+
you intend to use supports the OpenTelemetry Protocol (OTLP), you can use the
71+
`use_open_telemetry_otlp` method imported from `otlp.py`. This method uses
72+
`OTLP` exporters, which rely on standard environment variables. The
73+
documentation below provides an example on how to use Grafana in such scenario.
74+
75+
## Requirements
76+
77+
```bash
78+
pip install opentelemetry-distro
79+
80+
opentelemetry-bootstrap --action=install
81+
```
82+
83+
To work using the OTLP protocol (for instance, with [Grafana](https://grafana.com/)),
84+
install the `opentelemetry-exporter-otlp` package:
85+
86+
```bash
87+
pip install opentelemetry-exporter-otlp
88+
```
89+
90+
Install other dependencies depending on the backend service you intend to use.
91+
For instance, for *Azure Application Insights*, install `azure-monitor-opentelemetry-exporter`:
92+
93+
```bash
94+
pip install azure-monitor-opentelemetry-exporter
95+
```
96+
97+
---
98+
99+
## Example: Grafana
100+
101+
There are several ways to integrate with Grafana. This documentation describes
102+
only manual setup using environment variables for the OTLP protocol. This
103+
approach is flexible because it only requires outgoing HTTP connections, and
104+
works well in scenarios where installing agents like `Grafana Alloy` is not
105+
easy or not possible (e.g. cloud `PaaS` services).
106+
107+
- Install the required dependencies like described above.
108+
- Obtain environment variables for the OTLP protocol, using the _OpenTelemetry
109+
getting started guide_ and the option _Send OpenTelemetry data directly to the Grafana Cloud OTLP endpoint._
110+
111+
![Grafana Direct](./img/grafana-otel-direct.png)
112+
113+
Obtain the environment variables with the following names:
114+
115+
```
116+
OTEL_RESOURCE_ATTRIBUTES="..."
117+
OTEL_EXPORTER_OTLP_ENDPOINT="..."
118+
OTEL_EXPORTER_OTLP_HEADERS="..."
119+
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
120+
```
121+
122+
![Grafana Manual Instrumentation](./img/grafana-instrument-manual.png)
123+
124+
Configure env variables for your application.
125+
126+
/// admonition | Python dot-env.
127+
type: tip
128+
129+
One way to handle env variables for local development is using the
130+
`python-dotenv` library and storing the env variables in a `.env` file, then
131+
load variables using the `load_dotenv` function imported from `dotenv`.
132+
In such case, ensure that `.env` is included in `.gitignore`.
133+
134+
```python
135+
from dotenv import load_dotenv
136+
137+
load_dotenv()
138+
```
139+
140+
///
141+
142+
Enable data collection:
143+
144+
```python
145+
from blacksheep import Application
146+
from blacksheep.server.otel.otlp import use_open_telemetry_otlp
147+
148+
app = Application()
149+
150+
use_open_telemetry_otlp(app)
151+
```
152+
153+
A trace is produced for each web request and for all handled and unhandled
154+
exceptions. For unhandled exceptions, OpenTelemetry includes the full
155+
stacktrace of the exception.
156+
157+
![Grafana traces](./img/grafana-traces.png)
158+
159+
![Grafana errors](./img/grafana-errors.png)
160+
161+
## Example: Azure Application Insights
162+
163+
This documentation describes how to integrate with an Azure Application Insights
164+
service.
165+
166+
- Install the required dependencies like described above.
167+
- Obtain the connection string of an [Azure Application Insights service](https://learn.microsoft.com/en-us/azure/azure-monitor/app/connection-strings).
168+
- Configure tracing like in the following example:
169+
170+
```python
171+
from azure.monitor.opentelemetry.exporter import (
172+
AzureMonitorLogExporter,
173+
AzureMonitorTraceExporter,
174+
)
175+
from blacksheep import Application
176+
from blacksheep.server.otel import use_open_telemetry
177+
178+
179+
def use_application_insights(
180+
app: Application,
181+
connection_string: str,
182+
):
183+
"""
184+
Configures OpenTelemetry for a BlackSheep application using Azure Application Insights.
185+
186+
Sets up logging and tracing exporters for Azure Monitor using the provided connection string.
187+
188+
Args:
189+
app: The BlackSheep Application instance.
190+
connection_string: Azure Application Insights connection string.
191+
"""
192+
use_open_telemetry(
193+
app,
194+
AzureMonitorLogExporter(connection_string=connection_string),
195+
AzureMonitorTraceExporter(connection_string=connection_string),
196+
)
197+
198+
app = Application()
199+
use_application_insights(app, "YOUR_CONN_STRING")
200+
```
201+
202+
Observe how web requests and errors are displayed:
203+
204+
![Azure Application Insights Requests](./img/azure-app-insights-requests.png)
205+
206+
## Logging dependencies
207+
208+
When using OpenTelemetry you can handle your own tracing explicitly.
209+
The BlackSheep code includes an asynchronous context manager and example code
210+
for granular control of tracing.
211+
212+
```python
213+
from blacksheep.server.otel import logcall
214+
215+
216+
@logcall("Example")
217+
async def dependency_example():
218+
await asyncio.sleep(0.1)
219+
220+
221+
@app.router.get("/")
222+
async def home(request) -> Response:
223+
await dependency_example()
224+
return text("Hello, traced BlackSheep!")
225+
```
226+
227+
---
228+
229+
The following screenshots illustrate how dependencies are displayed in Grafana
230+
and Azure Application Insights:
231+
232+
[![Dependency in Grafana](./img/grafana-dependency.png)](./img/grafana-dependency.png)
233+
234+
[![Dependency in Azure Application Insights](./img/azure-app-insights-dependency.png)](./img/azure-app-insights-dependency.png)
235+
236+
/// admonition | Dependencies with Azure Application Insights.
237+
type: example
238+
239+
For Azure, use a decorator that sets the `az.namespace` information like in
240+
the example provided in [_BlackSheep-Examples_](https://github.com/Neoteroi/BlackSheep-Examples/blob/main/otel/otel/azure.py).
241+
242+
///
243+
244+
## Working with spans
245+
246+
The provided `OTELMiddleware` ensures that a tracing context, called "span", is
247+
created for each request and response cycle. To include additional information
248+
to a web request cycle, obtain the current span like in the following example:
249+
250+
```python
251+
from opentelemetry import trace
252+
253+
254+
@app.router.get("/")
255+
async def home(request) -> Response:
256+
span = trace.get_current_span()
257+
span.set_attribute("custom.info", "This is extra info for the request")
258+
return text("Hello, traced BlackSheep!")
259+
```
260+
261+
`trace.get_current_span()` works by using context variables (such as Python's
262+
[`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html))
263+
under the hood. Context variables allow each asynchronous task or coroutine to
264+
have its own independent context, so the current span is correctly tracked even
265+
when code is running concurrently. This ensures that in async code, each task
266+
sees its own current span, avoiding conflicts or leaks between concurrent
267+
executions.

0 commit comments

Comments
 (0)