Skip to content

Commit b54f97b

Browse files
committed
feat: enable sql commentor
Also, minor fixes w.r.t to profile tags
1 parent 8550f19 commit b54f97b

8 files changed

Lines changed: 85 additions & 28 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ packages = ["src/django_o11y"]
77

88
[project]
99
name = "django-o11y"
10-
version = "0.6.0"
10+
version = "0.7.0"
1111
description = "Comprehensive OpenTelemetry observability for Django with traces, logs, metrics, and profiling"
1212
readme = "README.md"
1313
requires-python = ">=3.12"

src/django_o11y/config/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def get_config() -> dict[str, Any]:
7171
"SAMPLE_RATE": default_sample_rate,
7272
"CONSOLE_EXPORTER": False,
7373
"AWS_ENABLED": False,
74+
"SQL_COMMENTER": True,
7475
},
7576
"LOGGING": {
7677
"FORMAT": "json" if not settings.DEBUG else "console",
@@ -155,6 +156,7 @@ def _apply_env_overrides(config: dict[str, Any], default_sample_rate: float) ->
155156
_set_float(t, "SAMPLE_RATE", "OTEL_TRACES_SAMPLER_ARG", default_sample_rate)
156157
_set_bool(t, "CONSOLE_EXPORTER", "DJANGO_O11Y_TRACING_CONSOLE_EXPORTER")
157158
_set_bool(t, "AWS_ENABLED", "DJANGO_O11Y_TRACING_AWS_ENABLED")
159+
_set_bool(t, "SQL_COMMENTER", "DJANGO_O11Y_TRACING_SQL_COMMENTER", True)
158160

159161
_set_str(lg, "FORMAT", "DJANGO_O11Y_LOGGING_FORMAT")
160162
_set_str(lg, "LEVEL", "DJANGO_O11Y_LOGGING_LEVEL")

src/django_o11y/profiling/setup.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,25 @@ def setup_profiling(config: dict[str, Any]) -> None:
5353
# RESOURCE_ATTRIBUTES (including OTEL_RESOURCE_ATTRIBUTES) are the base;
5454
# automatic attributes override them so runtime values are always accurate.
5555
resource_attrs = dict(config.get("RESOURCE_ATTRIBUTES", {}))
56-
tags = resource_attrs
5756

58-
tags.update(
59-
{
60-
"service_version": config["SERVICE_VERSION"],
61-
"host": socket.gethostname(),
62-
"process_id": str(os.getpid()),
63-
}
64-
)
65-
66-
if deployment_environment := resource_attrs.get("deployment.environment"):
67-
tags["environment"] = deployment_environment
68-
if service_namespace := resource_attrs.get("service.namespace"):
69-
tags["service_namespace"] = service_namespace
57+
# Pyroscope tag keys cannot contain dots (must match [a-zA-Z_][a-zA-Z0-9_]*).
58+
# Well-known OTel attributes are mapped to their canonical Pyroscope equivalents;
59+
# all other dotted keys fall back to dot→underscore replacement.
60+
_OTEL_TAG_MAP = {
61+
"deployment.environment": "environment",
62+
"service.namespace": "service_namespace",
63+
"service.version": "service_version",
64+
"service.instance.id": "service_instance_id",
65+
}
66+
tags: dict[str, str] = {
67+
"service_version": config["SERVICE_VERSION"],
68+
"host": socket.gethostname(),
69+
"process_id": str(os.getpid()),
70+
}
71+
for key, value in resource_attrs.items():
72+
if value:
73+
tag_key = _OTEL_TAG_MAP.get(key) or key.replace(".", "_")
74+
tags[tag_key] = value
7075

7176
try:
7277
pyroscope.configure(

src/django_o11y/tracing/instrumentation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ def setup_instrumentation(config: dict[str, Any]) -> None:
77
"""Set up automatic tracing instrumentation for Django and dependencies."""
88
from opentelemetry.instrumentation.django import DjangoInstrumentor
99

10-
DjangoInstrumentor().instrument()
10+
DjangoInstrumentor().instrument(
11+
is_sql_commentor_enabled=config.get("TRACING", {}).get("SQL_COMMENTER", True)
12+
)
1113
_instrument_database()
1214
_instrument_cache()
1315
_instrument_celery(config)

src/django_o11y/tracing/middleware.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,20 @@ async def __acall__(self, request: HttpRequest) -> HttpResponse:
4646
# In ASGI mode we own the server span so it stays active in the async
4747
# context for the entire request — including sync_to_async calls made by
4848
# downstream middleware (e.g. django-structlog's handle_response).
49-
carrier = {
50-
k.decode("latin-1"): v.decode("latin-1")
51-
for k, v in request.scope.get("headers", []) # type: ignore[union-attr]
52-
}
53-
parent_context = extract(carrier)
49+
parent_context = extract(request.headers) # type: ignore[arg-type]
5450

5551
method = request.method or "GET"
56-
span_name = f"{method} {request.path}"
5752
with self.tracer.start_as_current_span(
58-
span_name,
53+
method,
5954
context=parent_context,
6055
kind=SpanKind.SERVER,
6156
attributes={"url.path": request.path, "http.request.method": method},
6257
) as span:
6358
self._annotate_user(request, span)
6459
response = await self.get_response(request) # type: ignore[misc]
6560
if span.is_recording():
61+
if request.resolver_match:
62+
span.update_name(f"{method} {request.resolver_match.route}")
6663
span.set_attribute("http.response.status_code", response.status_code)
6764
return response
6865

tests/asgi.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
application = ProtocolTypeRouter(
2020
{
2121
"http": django_http_handler,
22-
# ChannelsLoggingMiddleware sits outside AuthMiddlewareStack so that
23-
# scope["user"] is already resolved when connection events are logged.
24-
"websocket": ChannelsLoggingMiddleware(
25-
AuthMiddlewareStack(URLRouter(tests.ws_urls.websocket_urlpatterns))
22+
# Middleware order (outermost → innermost):
23+
# 1. AllowedHostsOriginValidator — rejects bad origins before any work
24+
# 2. AuthMiddlewareStack — resolves scope["user"]
25+
# 3. ChannelsLoggingMiddleware — logs with user context already set
26+
# 4. URLRouter — routes to the consumer
27+
"websocket": AuthMiddlewareStack(
28+
ChannelsLoggingMiddleware(URLRouter(tests.ws_urls.websocket_urlpatterns))
2629
),
2730
}
2831
)

tests/tracing/test_instrumentation.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,55 @@ def test_setup_instrumentation_instruments_django():
2525
):
2626
setup_instrumentation(config)
2727

28-
mock_inst.instrument.assert_called_once()
28+
mock_inst.instrument.assert_called_once_with(is_sql_commentor_enabled=True)
29+
30+
31+
def test_setup_instrumentation_sql_commenter_enabled_by_default():
32+
"""SQL commenter is on by default so queries carry trace context."""
33+
from django_o11y.tracing.instrumentation import setup_instrumentation
34+
35+
config = {"SERVICE_NAME": "test", "TRACING": {}}
36+
37+
mock_inst = MagicMock()
38+
mock_django_module = MagicMock()
39+
mock_django_module.DjangoInstrumentor.return_value = mock_inst
40+
41+
with (
42+
patch.dict(
43+
"sys.modules", {"opentelemetry.instrumentation.django": mock_django_module}
44+
),
45+
patch("django_o11y.tracing.instrumentation._instrument_database"),
46+
patch("django_o11y.tracing.instrumentation._instrument_cache"),
47+
patch("django_o11y.tracing.instrumentation._instrument_celery"),
48+
patch("django_o11y.tracing.instrumentation._instrument_http_clients"),
49+
):
50+
setup_instrumentation(config)
51+
52+
mock_inst.instrument.assert_called_once_with(is_sql_commentor_enabled=True)
53+
54+
55+
def test_setup_instrumentation_sql_commenter_can_be_disabled():
56+
"""SQL commenter can be turned off via TRACING.SQL_COMMENTER=False."""
57+
from django_o11y.tracing.instrumentation import setup_instrumentation
58+
59+
config = {"SERVICE_NAME": "test", "TRACING": {"SQL_COMMENTER": False}}
60+
61+
mock_inst = MagicMock()
62+
mock_django_module = MagicMock()
63+
mock_django_module.DjangoInstrumentor.return_value = mock_inst
64+
65+
with (
66+
patch.dict(
67+
"sys.modules", {"opentelemetry.instrumentation.django": mock_django_module}
68+
),
69+
patch("django_o11y.tracing.instrumentation._instrument_database"),
70+
patch("django_o11y.tracing.instrumentation._instrument_cache"),
71+
patch("django_o11y.tracing.instrumentation._instrument_celery"),
72+
patch("django_o11y.tracing.instrumentation._instrument_http_clients"),
73+
):
74+
setup_instrumentation(config)
75+
76+
mock_inst.instrument.assert_called_once_with(is_sql_commentor_enabled=False)
2977

3078

3179
def test_instrument_cache_redis():

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)