Skip to content

Commit eee875d

Browse files
authored
OpenTelemetry improvements (#257)
* Use FastAPI instrumentor * Calculate RED metrics from spans * Default to local opentelemetry export * Switch to manual instrumentation
1 parent 59d16e6 commit eee875d

File tree

19 files changed

+508
-376
lines changed

19 files changed

+508
-376
lines changed

.idea/runConfigurations/Otel_Stack.xml

Lines changed: 0 additions & 19 deletions
This file was deleted.

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,19 @@ FROM base_app AS http
8080
COPY --from=http_builder /venv /venv
8181
COPY --chown=nonroot:nonroot src/http_app ./http_app
8282
# Run CMD using array syntax, so it uses `exec` and runs as PID1
83-
CMD ["opentelemetry-instrument", "python", "-m", "http_app"]
83+
CMD ["python", "-m", "http_app"]
8484

8585
# Copy the socketio python package and requirements from relevant builder
8686
FROM base_app AS socketio
8787
COPY --from=socketio_builder /venv /venv
8888
COPY --chown=nonroot:nonroot src/socketio_app ./socketio_app
8989
# Run CMD using array syntax, so it uses `exec` and runs as PID1
90-
CMD ["opentelemetry-instrument", "python", "-m", "socketio_app"]
90+
CMD ["python", "-m", "socketio_app"]
9191

9292
# Copy the dramatiq python package and requirements from relevant builder
9393
FROM base_app AS dramatiq
9494
COPY --from=dramatiq_builder /venv /venv
9595
COPY --chown=nonroot:nonroot src/dramatiq_worker ./dramatiq_worker
9696
# Run CMD using array syntax, so it uses `exec` and runs as PID1
9797
# TODO: Review processes/threads
98-
CMD ["opentelemetry-instrument", "dramatiq", "-p", "1", "-t", "1", "dramatiq_worker"]
98+
CMD ["dramatiq", "-p", "1", "-t", "1", "dramatiq_worker"]

Makefile

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
.PHONY: docs docs-build adr
22

33
containers:
4-
# Use local UID to avoid files permission issues when mounting directories
5-
# We could do this at runtime, by specifying the user, but it's easier doing it
6-
# at build time, so no special commands will be necessary at runtime
7-
docker compose build --build-arg UID=`id -u` dev
8-
# To build shared container layers only once we build a single container before the other ones
94
docker compose build --build-arg UID=`id -u`
105

116
dev-http:
@@ -14,9 +9,6 @@ dev-http:
149
dev-socketio:
1510
uv run ./src/socketio_app/dev_server.py
1611

17-
otel:
18-
OTEL_SERVICE_NAME=bootstrap-fastapi OTEL_TRACES_EXPORTER=none OTEL_METRICS_EXPORTER=none OTEL_LOGS_EXPORTER=none uv run opentelemetry-instrument uvicorn http_app:create_app --host 0.0.0.0 --port 8000 --factory
19-
2012
run:
2113
uv run uvicorn http_app:create_app --host 0.0.0.0 --port 8000 --factory
2214

config.alloy

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,85 @@
1+
otelcol.receiver.otlp "default" {
2+
grpc { }
3+
4+
output {
5+
metrics = [
6+
otelcol.processor.transform.add_resource_attributes_as_metric_attributes.input,
7+
]
8+
traces = [
9+
// This transforms the traces in metrics, we still have to send traces out
10+
otelcol.connector.spanmetrics.asgi_apm.input,
11+
// This also transforms the traces in metrics, we still have to send traces out
12+
otelcol.connector.host_info.default.input,
13+
// This sends the traces out
14+
otelcol.processor.batch.default.input,
15+
]
16+
logs = [
17+
otelcol.processor.batch.default.input,
18+
]
19+
}
20+
}
21+
22+
otelcol.connector.host_info "default" {
23+
// https://grafana.com/docs/alloy/latest/reference/components/otelcol.connector.host_info/
24+
host_identifiers = ["host.name"]
25+
26+
output {
27+
metrics = [otelcol.processor.batch.default.input]
28+
}
29+
}
30+
31+
otelcol.connector.spanmetrics "asgi_apm" {
32+
dimension {
33+
name = "http.status_code"
34+
}
35+
36+
dimension {
37+
name = "http.method"
38+
}
39+
40+
dimension {
41+
name = "http.route"
42+
}
43+
44+
histogram {
45+
explicit {
46+
buckets = ["2ms", "4ms", "6ms", "8ms", "10ms", "50ms", "100ms", "200ms", "400ms", "800ms", "1s", "1400ms", "2s", "5s", "10s", "15s"]
47+
}
48+
}
49+
50+
output {
51+
metrics = [otelcol.processor.transform.add_resource_attributes_as_metric_attributes.input]
52+
}
53+
}
54+
55+
otelcol.processor.transform "add_resource_attributes_as_metric_attributes" {
56+
error_mode = "ignore"
57+
58+
metric_statements {
59+
context = "datapoint"
60+
statements = [
61+
"set(attributes[\"deployment.environment\"], resource.attributes[\"deployment.environment\"])",
62+
"set(attributes[\"service.version\"], resource.attributes[\"service.version\"])",
63+
]
64+
}
65+
66+
output {
67+
metrics = [otelcol.processor.batch.default.input]
68+
}
69+
}
70+
71+
otelcol.processor.batch "default" {
72+
output {
73+
// metrics = [otelcol.exporter.otlphttp.grafanacloud.input]
74+
// logs = [otelcol.exporter.otlphttp.grafanacloud.input]
75+
// traces = [otelcol.exporter.otlphttp.grafanacloud.input]
76+
77+
metrics = [otelcol.exporter.debug.console.input]
78+
logs = [otelcol.exporter.debug.console.input]
79+
traces = [otelcol.exporter.otlp.jaeger.input]
80+
}
81+
}
82+
183
otelcol.exporter.otlp "jaeger" {
284
client {
385
endpoint = "jaeger:4317"
@@ -12,18 +94,15 @@ otelcol.exporter.debug "console" {
1294
verbosity = "Detailed"
1395
}
1496

15-
otelcol.processor.batch "default" {
16-
output {
17-
traces = [otelcol.exporter.otlp.jaeger.input]
18-
}
19-
}
2097

21-
otelcol.receiver.otlp "default" {
22-
grpc { }
98+
otelcol.auth.basic "grafanacloud" {
99+
username = sys.env("GC_USERNAME")
100+
password = sys.env("GC_PASSWORD")
101+
}
23102

24-
output {
25-
metrics = [otelcol.exporter.debug.console.input]
26-
logs = [otelcol.exporter.debug.console.input]
27-
traces = [otelcol.processor.batch.default.input]
28-
}
103+
otelcol.exporter.otlphttp "grafanacloud" {
104+
client {
105+
endpoint = sys.env("GC_ENDPOINT")
106+
auth = otelcol.auth.basic.grafanacloud.handler
107+
}
29108
}

docker-compose.yaml

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ services:
55
context: .
66
target: dev
77
env_file: local.env
8+
environment:
9+
APP_NAME: "bootstrap-fastapi"
810
ports:
911
- '8000:8000'
1012
working_dir: "/app/src"
@@ -18,34 +20,14 @@ services:
1820
- ./http_app/dev_server.py
1921

2022
dev-socketio:
21-
<<: *dev
22-
ports:
23-
- '8001:8001'
24-
command:
25-
- python
26-
- ./socketio_app/dev_server.py
27-
28-
otel-http:
2923
<<: *dev
3024
environment:
31-
OTEL_SERVICE_NAME: "bootstrap-fastapi-dev"
32-
command:
33-
- opentelemetry-instrument
34-
- python
35-
- -m
36-
- http_app
37-
38-
otel-socketio:
39-
<<: *dev
40-
environment:
41-
OTEL_SERVICE_NAME: "bootstrap-socketio-dev"
25+
APP_NAME: "bootstrap-socketio"
4226
ports:
4327
- '8001:8001'
4428
command:
45-
- opentelemetry-instrument
4629
- python
47-
- -m
48-
- socketio_app
30+
- ./socketio_app/dev_server.py
4931

5032
#########################
5133
#### Helper services ####
@@ -79,10 +61,9 @@ services:
7961
dramatiq-worker:
8062
<<: *dev
8163
environment:
82-
OTEL_SERVICE_NAME: "bootstrap-fastapi-dramatiq-worker"
64+
APP_NAME: "bootstrap-dramatiq-worker"
8365
ports: []
8466
command:
85-
- opentelemetry-instrument
8667
- dramatiq
8768
- --watch
8869
- .

local.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ ENVIRONMENT: "local"
22
AUTH__JWKS_URL: "http://oathkeeper:4456/.well-known/jwks.json"
33
#DRAMATIQ__REDIS_URL: "redis://redis:6379/0"
44
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"
5-
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED: "true"
5+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: ".*"
6+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: ".*"

pyproject.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ dependencies = [
1616
"dramatiq[redis,watch]<2.0.0,>=1.17.1",
1717
"hiredis<4.0.0,>=3.1.0", # Recommended by dramatiq
1818
"httpx>=0.23.0",
19-
"opentelemetry-distro[otlp]",
20-
"opentelemetry-instrumentation",
2119
"opentelemetry-instrumentation-httpx",
2220
"opentelemetry-instrumentation-sqlalchemy",
2321
"opentelemetry-instrumentor-dramatiq",
22+
"opentelemetry-exporter-otlp",
23+
"opentelemetry-sdk",
2424
"orjson<4.0.0,>=3.10.12",
2525
"pydantic<3.0.0,>=2.2.1",
2626
"pydantic-asyncapi>=0.2.1",
@@ -36,11 +36,12 @@ http = [
3636
"cryptography>=44.0.0",
3737
"fastapi>=0.99.0",
3838
"jinja2<4.0.0,>=3.1.2",
39-
# We use the generic ASGI instrumentation, so that if we decide to change
40-
# Framework it will still work consistently.
41-
"opentelemetry-instrumentation-asgi",
39+
# FastAPI instrumentation is based on the generic ASGI instrumentation,
40+
# but automatically creates span when routes are invoked.
41+
# If we decide to change framework, the generic ASGI instrumentation
42+
# will still name metrics with a generic naming.
43+
"opentelemetry-instrumentation-fastapi",
4244
"pyjwt>=2.10.1",
43-
"starlette-prometheus<1.0.0,>=0.10.0",
4445
"strawberry-graphql[debug-server]>=0.204.0",
4546
"uvicorn[standard]<1.0.0,>=0.34.0",
4647
]

src/common/bootstrap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .dramatiq import init_dramatiq
1111
from .logs import init_logger
1212
from .storage import init_storage
13+
from .telemetry import instrument_opentelemetry
1314

1415

1516
class InitReference(BaseModel):
@@ -29,6 +30,7 @@ def application_init(app_config: AppConfig) -> InitReference:
2930
init_storage()
3031
init_dramatiq(app_config)
3132
init_asyncapi_info(app_config.APP_NAME)
33+
instrument_opentelemetry(app_config)
3234

3335
return InitReference(
3436
di_container=container,

src/common/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ class AppConfig(BaseSettings):
3838
async_engine=True,
3939
),
4040
)
41+
OTEL_EXPORTER_OTLP_ENDPOINT: Optional[str] = None
42+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: Optional[str] = None
43+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Optional[str] = None
44+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: Optional[str] = None

src/common/dramatiq.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from dramatiq.brokers.stub import StubBroker
88
from dramatiq.encoder import DecodeError, Encoder, MessageData
99
from dramatiq.middleware import AsyncIO
10-
from opentelemetry_instrumentor_dramatiq import DramatiqInstrumentor
1110

1211
from .config import AppConfig
1312

@@ -28,10 +27,6 @@ def decode(self, data: bytes) -> MessageData:
2827
def init_dramatiq(config: AppConfig):
2928
broker: Broker
3029

31-
dramatiq_instrumentor = DramatiqInstrumentor()
32-
if not dramatiq_instrumentor.is_instrumented_by_opentelemetry:
33-
dramatiq_instrumentor.instrument()
34-
3530
if config.DRAMATIQ.REDIS_URL is not None:
3631
broker = RedisBroker(url=config.DRAMATIQ.REDIS_URL)
3732
else:

0 commit comments

Comments
 (0)