Skip to content

Bootstrap dormant OpenTelemetry wiring in the Django backend - #1583

Open
Nazehs wants to merge 1 commit into
mainfrom
pro-647-bootstrap-opentelemetry-in-the-django-backend
Open

Bootstrap dormant OpenTelemetry wiring in the Django backend#1583
Nazehs wants to merge 1 commit into
mainfrom
pro-647-bootstrap-opentelemetry-in-the-django-backend

Conversation

@Nazehs

@Nazehs Nazehs commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Context

Wires the dormant OTel seam into the Django request path: instruments Django and re-applies the structlog trace-context processor after the logger is built, so the existing request log line picks up trace/span ids. There's no parallel request logger, so nothing gets logged twice.

Changes proposed in this pull request

  • New backend/otel_common.py: shared otel_requested() gating, so the Django and worker bootstraps can't drift. otel_django.py and otel_bootstrap.py both use it.
  • New backend/otel_django.py: when enabled, configures the tracer provider and re-applies the structlog processors in AppConfig.ready() (non-worker only, the worker has its own bootstrap). Warn-and-continue if the [otel] extra is missing or setup fails, so telemetry never blocks startup.
  • Aligned otel_bootstrap.py to the same broad warn-and-continue.
  • Integration test drives the enabled path against an in-memory exporter, asserting the active span is exported and the existing logs carry matching trace_id/span_id. Unit tests cover the gating and warn-and-continue.

@linear-code

linear-code Bot commented Aug 28, 2026

Copy link
Copy Markdown

PRO-647

@Nazehs
Nazehs force-pushed the pro-647-bootstrap-opentelemetry-in-the-django-backend branch 3 times, most recently from 52a4fed to 00d6af5 Compare August 28, 2026 11:35
@Nazehs
Nazehs marked this pull request as ready for review August 28, 2026 11:49
@Nazehs
Nazehs requested a review from a team as a code owner August 28, 2026 11:49
@Nazehs
Nazehs requested review from 252afh and removed request for a team August 28, 2026 11:49
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Nazehs's task in 41s —— View job


PR Review: Bootstrap dormant OpenTelemetry wiring in the Django backend

Clean, well-tested bootstrap. The dormant-by-default gating is solid. A couple of minor points:


backend/otel_django.py:22install_middleware mutates the list in-place via .insert() and also returns it. Callers get silent aliasing; a copy would be safer.

⚪️ NIT: return middleware → work on a copy: mw = list(middleware); mw.insert(...); return mw


backend/otel_django.py:55except RuntimeError may not cover all exporter init failures (e.g. OSError, ConnectionRefusedError at startup).

🔵 CONSIDER: broadening to except Exception or checking what the library actually raises, to keep warn-and-continue robust.


backend/tests/unit/test_otel_django.py — no test for the case where AuthenticationMiddleware is absent (falls back to appending).

⚪️ NIT: Add a case to confirm the fallback path.

@Nazehs
Nazehs force-pushed the pro-647-bootstrap-opentelemetry-in-the-django-backend branch from 00d6af5 to 0c47603 Compare August 28, 2026 12:07
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Nazehs's task in 36s —— View job


PR Review: Bootstrap dormant OpenTelemetry wiring in the Django backend

Clean, dormant-by-default OTel bootstrap. Consistent with otel_bootstrap.py patterns. One structural note:


backend/otel_django.py & backend/otel_bootstrap.pyotel_requested(), OTEL_ENDPOINT_ENV, and OTEL_ENABLED_ENV are duplicated across both modules with identical logic.

⚪️ NIT: Extract to a shared otel_utils.py or otel_common.py to avoid drift. Fix this →


backend/otel_bootstrap.py:50 — now inconsistent: otel_django.py (this PR) correctly uses except Exception but otel_bootstrap.py still narrows to except RuntimeError, missing OSError/ConnectionRefusedError.

🔵 CONSIDER: Aligning otel_bootstrap.py to use except Exception for the same warn-and-continue robustness. Fix this →


Otherwise solid — gating, warn-and-continue, test coverage all look good. ✅

@Nazehs
Nazehs force-pushed the pro-647-bootstrap-opentelemetry-in-the-django-backend branch from 0c47603 to 41bb41a Compare August 28, 2026 12:23
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Nazehs's task in 41s —— View job


PR Review: Bootstrap dormant OpenTelemetry wiring in the Django backend

Clean, well-structured OTel bootstrap. The dormant-by-default gating is solid. Previous review concerns (shared module, except Exception) have been addressed. One structural note:


backend/consultations/apps.py:4 vs :15import otel_django is at module level, but from otel_bootstrap import bootstrap_otel is inside the if block. The top-level import means any load-time error in otel_django.py itself (not the guarded inner imports) would crash Django startup before ready() can warn-and-continue.

⚪️ NIT: Move import otel_django inside the else branch to be consistent with the worker path and keep the same startup-safety guarantee.


backend/tests/unit/test_otel_django.py:25-55 — tests call otel_django.otel_requested() (a re-exported symbol), effectively retesting otel_common.otel_requested() which is already covered identically in test_otel_bootstrap.py.

⚪️ NIT: Test the gating logic once in test_otel_common.py and trim these duplicate cases, keeping only the Django-specific configure_django_otel tests here.


Otherwise solid. ✅

Add an OTel request-path seam that stays a no-op unless OTEL_ENABLED and a
collector endpoint are set (dev only via Terraform), so prod keeps running on
the existing StructuredLogger with no behaviour change.

- otel_django.py gates on env, inserts StructuredLoggingMiddlewareOTel after
  auth, and configures the tracer provider + structlog processors in ready()
- settings expose I_DOT_AI_LOGGER and install the middleware conditionally
- warn-and-continue if the [otel] extra or exporter is unavailable
@Nazehs
Nazehs force-pushed the pro-647-bootstrap-opentelemetry-in-the-django-backend branch from 41bb41a to 9bbcc67 Compare August 28, 2026 14:45
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Nazehs's task in 31s —— View job


PR Review: Bootstrap dormant OpenTelemetry wiring in the Django backend

  • Gather context
  • Read changed files
  • Post review

Clean implementation. Previous review concerns (shared module, except Exception, lazy imports) are all resolved. otel_common.py is appropriately minimal, warn-and-continue is consistent across both bootstraps, and the integration test asserting trace/span correlation on existing logs is the right level of coverage.

No issues to raise. ✅

@@ -14,6 +15,8 @@ def ready(self):
from otel_bootstrap import bootstrap_otel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate note: can we avoid inline imports, they should be top-level only, unless ABSOLUTELY necessary

Comment thread backend/otel_common.py

def otel_requested() -> bool:
"""Telemetry is requested only when the flag is on and an endpoint is set."""
enabled = os.environ.get(OTEL_ENABLED_ENV, "").strip().lower() == "true"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to use the django settings here instead to access the var. Also, don't name env vars with env on the end, it's assumed already

Comment thread backend/otel_common.py

import os

OTEL_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove string assignments at the top of files that are only used once within the file

Comment thread backend/otel_bootstrap.py
"""Bootstrap only when the flag is on and a collector endpoint is configured."""
enabled = os.environ.get(OTEL_ENABLED_ENV, "").strip().lower() == "true"
return enabled and bool(os.environ.get(OTEL_ENDPOINT_ENV))
_TRACER_NAME = "consult.worker"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This string isn't used anywhere else, just set it on line 41 instead

Comment thread backend/otel_django.py

from otel_common import otel_requested

_SERVICE_NAME = "consult-backend"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These service names could come from the django env and set in terraform, to save them being defined in code instead

Comment thread backend/otel_django.py
if not otel_requested():
return

try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we need this check?

Comment thread backend/otel_django.py
@@ -0,0 +1,41 @@
"""OpenTelemetry bootstrap for the Django request path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything in this file that stops it being done as part of the django settings setup instead, similar to how the sentry init is done?

def _clear_otel_env(monkeypatch):
monkeypatch.delenv(otel_bootstrap.OTEL_ENDPOINT_ENV, raising=False)
monkeypatch.delenv(otel_bootstrap.OTEL_ENABLED_ENV, raising=False)
monkeypatch.delenv(otel_common.OTEL_ENDPOINT_ENV, raising=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files are currently the only place in the backend where we have monkeypatch. Could we make use of env vars and patch instead to align with other locations, e.g.:

def _clear_otel_env():
    with patch.dict(os.environ, {otel_bootstrap.OTEL_ENDPOINT_ENV: "", otel_bootstrap.OTEL_ENABLED_ENV: ""}, clear=False):


import otel_common

ENDPOINT = "http://collector:4317"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL for the collector should be an env var

ENDPOINT = "http://collector:4317"


@pytest.fixture(autouse=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixtures belong in the conftest.py file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants