Skip to content

Commit 8464cff

Browse files
feat(app-b): add otlp sdk configuration
1 parent 3a2374b commit 8464cff

12 files changed

+884
-211
lines changed

b/.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
!letter_b
44
!scripts
55
!manage.py
6+
!otlp.py
67
!poetry*
78
!pyproject.toml
89
!gunicorn_config.py

b/.env

+14-1
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ DB_USE_REPLICA=False
2424
####################
2525
#### Logging config
2626
ROOT_LOG_LEVEL=INFO
27-
DEFAULT_LOG_FORMATTER=development
27+
DEFAULT_LOG_FORMATTER=otlp
2828
STOMP_LOG_LEVEL=WARNING
2929
DJANGO_DB_BACKENDS_LOG_LEVEL=INFO
3030
DJANGO_REQUEST_LOG_LEVEL=INFO
31+
PROJECT_LOG_LEVEL=INFO
3132

3233
##################
3334
#### DEBUG LIB CONFIGURATION
@@ -44,3 +45,15 @@ STOMP_SERVER_USER=guest
4445
STOMP_SERVER_PASSWORD=guest
4546
STOMP_USE_SSL=False
4647
CREATE_AUDIT_ACTION_DESTINATION=/queue/create-audit-action
48+
49+
##################
50+
#### OTL
51+
# https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
52+
# https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
53+
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
54+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://otel-collector:4317
55+
OTEL_EXPORTER_OTLP_LOGS_INSECURE=true
56+
OTEL_RESOURCE_ATTRIBUTES=service.name=letter-b,service.version=unknown
57+
OTEL_LOG_LEVEL=debug
58+
OTEL_PYTHON_LOG_LEVEL=DEBUG
59+
OTEL_PYTHON_LOG_CORRELATION=true

b/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Execute `docker compose up app-production` to start API server in production mod
88
- http://localhost:8080/api/v1/
99
- http://localhost:8080/api/v1/users/attributes
1010

11+
Look at traces, logs, and metrics at `http://localhost:8090`.
12+
1113
## Recurring procedures
1214

1315
### Updating the lock file

b/docker-compose.yml

+27
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ services:
4141
condition: service_healthy
4242
rabbitmq:
4343
condition: service_healthy
44+
otel-collector:
45+
condition: service_started
4446
command:
4547
[
4648
"./scripts/start-development.sh",
@@ -85,6 +87,8 @@ services:
8587
condition: service_healthy
8688
rabbitmq:
8789
condition: service_healthy
90+
otel-collector:
91+
condition: service_started
8892
command:
8993
[
9094
"./scripts/start-production.sh",
@@ -95,6 +99,29 @@ services:
9599
timeout: 5s
96100
retries: 5
97101

102+
otel-collector:
103+
image: otel/opentelemetry-collector-contrib:0.113.0
104+
command: [ "--config=/etc/otel-collector-config.yml" ]
105+
volumes:
106+
- ./otel-collector-config.yml:/etc/otel-collector-config.yml
107+
depends_on:
108+
hyperdx:
109+
condition: service_healthy
110+
111+
hyperdx:
112+
image: hyperdx/hyperdx-local:1.10.0
113+
environment:
114+
HYPERDX_APP_PORT: 8090 # HyperDX UI (Next.js)
115+
HYPERDX_API_PORT: 8091 # Private HyperDX API for the UI
116+
ports:
117+
- "8090:8080"
118+
- "8091:8000"
119+
- "4317:4317"
120+
healthcheck:
121+
test: curl --fail http://localhost:8000/health || exit 1
122+
interval: 10s
123+
timeout: 3s
124+
98125
integration-tests:
99126
build: *dockerfile-dev-build
100127
env_file: .env

b/gunicorn_config.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import psycogreen.gevent
1+
from gevent import monkey
22

3-
# https://github.com/psycopg/psycogreen/blob/39a258cb4040b88b60a7600f6942e651a28db9a7/README.rst#module-psycogreengevent
4-
psycogreen.gevent.patch_psycopg()
3+
# If you don't do this, you'll get the following error for example:
4+
# DatabaseWrapper objects created in a thread can only be used in that same thread. The object with alias 'default' was created in thread id 140208374647872 and this is thread id 140208369922144
5+
monkey.patch_all()
56

67
import json
78
import os
89

910
from pythonjsonlogger.jsonlogger import JsonFormatter
1011

12+
13+
def post_fork(server, worker):
14+
"""
15+
https://opentelemetry-python.readthedocs.io/en/stable/examples/fork-process-model/README.html#gunicorn-post-fork-hook
16+
"""
17+
server.log.info("Worker spawned (pid: %s)", worker.pid)
18+
from otlp import configure_opentelemetry
19+
20+
configure_opentelemetry()
21+
22+
1123
bind = os.environ.get("DJANGO_BIND_ADDRESS", "0.0.0.0") + ":" + os.environ.get("DJANGO_BIND_PORT", "8000")
1224
backlog = int(os.getenv("GUNICORN_BACKLOG", "2048"))
1325
workers = int(os.getenv("GUNICORN_WORKERS", "3"))

b/letter_b/settings.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@
158158

159159

160160
# Logging
161-
# https://docs.djangoproject.com/en/4.0/topics/logging/
161+
# https://docs.djangoproject.com/en/5.1/topics/logging/
162162

163163
LOGGING = {
164164
"version": 1,
@@ -175,6 +175,10 @@
175175
"()": JsonFormatter,
176176
"format": "%(levelname)-8s [%(asctime)s] [%(request_id)s] %(name)s: %(message)s",
177177
},
178+
"otlp": {
179+
"()": Formatter,
180+
"format": "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s trace_sampled=%(otelTraceSampled)s] - %(message)s",
181+
},
178182
"development": {
179183
"()": Formatter,
180184
"format": "%(levelname)-8s [%(asctime)s] [%(request_id)s] %(name)s: %(message)s",
@@ -185,10 +189,11 @@
185189
"class": "logging.StreamHandler",
186190
"filters": ["request_id", "redact_filter"],
187191
"formatter": os.getenv("DEFAULT_LOG_FORMATTER", "standard"),
188-
}
192+
},
193+
"otlp": {"class": "otlp.CustomLoggingHandler"},
189194
},
190195
"loggers": {
191-
"": {"level": os.getenv("ROOT_LOG_LEVEL", "INFO"), "handlers": ["console"]},
196+
"": {"level": os.getenv("ROOT_LOG_LEVEL", "INFO"), "handlers": ["console", "otlp"]},
192197
"letter_b": {
193198
"level": os.getenv("PROJECT_LOG_LEVEL", "INFO"),
194199
"handlers": ["console"],

b/letter_b/wsgi.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
from django.conf import settings
44
from django.contrib.staticfiles.handlers import StaticFilesHandler
55
from django.core.wsgi import get_wsgi_application
6+
from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
67

78
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "letter_b.settings")
89

10+
wsgi_application = OpenTelemetryMiddleware(get_wsgi_application())
11+
912
if settings.USE_STATIC_FILE_HANDLER_FROM_WSGI:
10-
application = StaticFilesHandler(get_wsgi_application())
13+
application = StaticFilesHandler(wsgi_application)
1114
else:
12-
application = get_wsgi_application()
15+
application = wsgi_application

b/manage.py

+4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import os
44
import sys
55

6+
from otlp import configure_opentelemetry
7+
68

79
def main():
810
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "letter_b.settings")
911
try:
1012
from django.core.management import execute_from_command_line
13+
14+
configure_opentelemetry()
1115
except ImportError as exc:
1216
raise ImportError(
1317
"Couldn't import Django. Are you sure it's installed and "

b/otel-collector-config.yml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# https://opentelemetry.io/docs/collector/configuration/
2+
3+
# Receivers collect telemetry from one or more sources.
4+
# They can be pull or push based, and may support one or more data sources.
5+
receivers:
6+
otlp:
7+
protocols:
8+
grpc:
9+
endpoint: 0.0.0.0:4317
10+
http:
11+
endpoint: 0.0.0.0:4318
12+
13+
# Processors take the data collected by receivers and modify or transform it before sending it to the exporters.
14+
processors:
15+
batch: # https://github.com/open-telemetry/opentelemetry-collector/blob/e1a688fc4ef45306b2782b68423b21334b5ebd18/processor/batchprocessor/README.md
16+
timeout: 5s
17+
send_batch_size: 10
18+
send_batch_max_size: 100
19+
20+
# Exporters send data to one or more backends or destinations.
21+
exporters:
22+
debug:
23+
verbosity: detailed
24+
otlp/hyperdx:
25+
endpoint: "hyperdx:4317"
26+
tls:
27+
insecure: true
28+
29+
extensions:
30+
health_check:
31+
pprof:
32+
zpages:
33+
34+
service:
35+
extensions: [ health_check, pprof, zpages ]
36+
telemetry:
37+
logs:
38+
level: "debug"
39+
pipelines:
40+
traces:
41+
receivers: [ otlp ]
42+
processors: [ batch ]
43+
exporters: [ otlp/hyperdx ]
44+
metrics:
45+
receivers: [ otlp ]
46+
processors: [ batch ]
47+
exporters: [ otlp/hyperdx ]
48+
logs:
49+
receivers: [ otlp ]
50+
processors: [ batch ]
51+
exporters: [ otlp/hyperdx ]

b/otlp.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import logging
2+
3+
from opentelemetry import trace
4+
from opentelemetry._logs import set_logger_provider
5+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
6+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
7+
from opentelemetry.instrumentation.django import DjangoInstrumentor
8+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
9+
from opentelemetry.instrumentation.psycopg import PsycopgInstrumentor
10+
from opentelemetry.sdk._logs import LoggerProvider
11+
from opentelemetry.sdk._logs import LoggingHandler
12+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
13+
from opentelemetry.sdk.trace import TracerProvider
14+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
15+
16+
17+
# This is experimental and not recommended for production use.
18+
# https://github.com/open-telemetry/opentelemetry-python/discussions/4294
19+
class CustomLoggingHandler(LoggingHandler):
20+
def __init__(self, level=logging.NOTSET, logger_provider=None) -> None:
21+
logger_provider = LoggerProvider()
22+
set_logger_provider(logger_provider)
23+
logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
24+
super().__init__(level, logger_provider)
25+
26+
27+
def configure_opentelemetry():
28+
# https://opentelemetry-python.readthedocs.io/en/stable/exporter/otlp/otlp.html
29+
# Don't customize the provider, exporter, and so on programmatically. Use environment variables instead.
30+
tracer_provider = TracerProvider()
31+
trace.set_tracer_provider(tracer_provider)
32+
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
33+
# TSHOOT PURPOSES ONLY: tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
34+
# https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/django/django.html#usage
35+
LoggingInstrumentor().instrument(tracer_provider=tracer_provider)
36+
DjangoInstrumentor().instrument(tracer_provider=tracer_provider)
37+
# https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/psycopg/psycopg.html#module-opentelemetry.instrumentation.psycopg
38+
PsycopgInstrumentor().instrument(tracer_provider=tracer_provider, enable_commenter=True)

0 commit comments

Comments
 (0)