← Back to list

Building Observability in My Solo Django Project (Trip-Nest) with Structlog, OpenTelemetry, and…

I’m building Trip-Nest as a hobby project (solo), a Django backend for hotel discovery/booking flows. Core features include:

Manjurul Hoque Rumi · 2026-04-30 16:22 · 1 claps · 2.3 min read
#django #opentelemetry #jaeger #distributed-tracing #observability
Open on Medium ↗
Wiki topics: 🌐 · Web Development ✈️ · Travel

Building Observability in My Solo Django Project (Trip-Nest) with Structlog, OpenTelemetry, and Jaeger

I’m building **Trip-Nest** as a hobby project (solo), a Django backend for hotel discovery/booking flows. Core features include:

  • hotel search with filters (city, rating, price, facilities)
  • host CRUD flows (create/update/delete)
  • hotel stats endpoints
  • Redis cache for search responses

As the project grew, plain logs weren’t enough. I wanted to answer:

  • Why is this endpoint slow?
  • Is Redis actually helping?
  • Which user hit this request?
  • Where exactly did a failure happen?

So I added observability in this order:

  1. Structlog (structured logs)
  2. OpenTelemetry (spans + context)
  3. Jaeger (trace visualization)

1) Structlog setup in my project

I use structlog as the logger in observability and views.

import structlog
from django.contrib.auth import get_user
from opentelemetry.instrumentation.django import DjangoInstrumentor

logger = structlog.get_logger(__name__)

def _instrument_django():
    def response_hook(span, request, response):
        user = getattr(request, "user", None)

        if user is None and hasattr(request, "session"):
            user = get_user(request)

        if user and user.is_authenticated:
            span.set_attribute("enduser.id", str(user.pk))

            email_field = getattr(user, "get_email_field_name", lambda: "email")()
            email = getattr(user, email_field, None)
            if email:
                span.set_attribute("enduser.email", email)

    DjangoInstrumentor().instrument(response_hook=response_hook)
    logger.info("OpenTelemetry Django initialized with user tracking")

I also keep business logs (example from hotel delete flow):

logger.info(
    "hotel_soft_deleted",
    hotel_id=hotel_id,
    owner_id=owner_id,
    actor_id=str(request.user.pk),
)

Structured logs became much easier to query and understand than plain text logs.

2) OpenTelemetry integration (real code)

I initialize instrumentation at WSGI boot:

import os

from django.core.wsgi import get_wsgi_application
from backend import observability

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")

observability.instrument()

application = get_wsgi_application()

Then I created a small tracing utility:

from collections.abc import Mapping
from typing import Any

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

tracer = trace.get_tracer(__name__)

def set_span_attributes(span, attributes: Mapping[str, Any]) -> None:
    if span is None or not span.is_recording():
        return
    for key, value in attributes.items():
        if value is not None:
            span.set_attribute(key, value)

def add_span_event(span, event_name: str, attributes: Mapping[str, Any] | None = None) -> None:
    if span is None or not span.is_recording():
        return
    span.add_event(event_name, attributes or {})

def record_span_exception(span, exc: Exception) -> None:
    if span is None or not span.is_recording():
        return
    span.record_exception(exc)
    span.set_status(Status(StatusCode.ERROR))

3) Manual spans for meaningful business traces

Django auto-instrumentation gives a root span, but I wanted business-level clarity. So I manually added nested spans in HotelViewSet.search.

with tracer.start_as_current_span("hotels.search") as span:
    set_span_attributes(
        span,
        {
            "enduser.id": str(request.user.pk) if request.user.is_authenticated else None,
            "hotel.action": "search",
        },
    )

    cache_key = self.build_search_cache_key(request)
    set_span_attributes(span, {"cache.key": cache_key})
    add_span_event(span, "search.started")

    with tracer.start_as_current_span("hotels.search.cache_get") as cache_get_span:
        cache_get_start = perf_counter()
        cached = cache.get(cache_key)
        cache_get_ms = (perf_counter() - cache_get_start) * 1000
        set_span_attributes(
            cache_get_span,
            {
                "cache.system": "redis",
                "cache.operation": "get",
                "cache.redis.get.ms": round(cache_get_ms, 2),
                "cache.hit": bool(cached),
            },
        )

    # filtering + pagination spans...

I also added exception recording in every instrumented endpoint:

except Exception as exc:
    record_span_exception(span, exc)
    raise

4) Jaeger: what improved immediately

Once exported to Jaeger, traces became actionable:

  • clear parent/child flow (hotels.search -> cache_get -> filtering -> pagination -> cache_set)
  • Redis timing visible per request
  • easy filtering by attributes like enduser.id or hotel.id
  • error spans visibly marked instead of hidden in logs

So now, when search is slow, I can quickly tell if it’s cache miss path, filtering, pagination, or cache write.

Final thoughts (solo-dev perspective)

Even for a hobby project, this was worth it.

If I had to repeat it, I’d do the same order:

  • Structlog first (clean logs)
  • OpenTelemetry second (trace semantics)
  • Jaeger third (visual diagnosis)

That combo gave me observability that actually helps while building features.

Github: https://github.com/manjurulhoque/trip-nest


메타데이터
post_id
ebcbb70fa57e
slug
building-observability-in-my-solo-django-project-trip-nest-with-structlog-opentelemetry-and-ebcbb70fa57e
url
https://medium.com/@manzurulhoque/building-observability-in-my-solo-django-project-trip-nest-with-structlog-opentelemetry-and-ebcbb70fa57e
canonical_url
https://medium.com/@manzurulhoque/building-observability-in-my-solo-django-project-trip-nest-with-structlog-opentelemetry-and-ebcbb70fa57e
author_url
https://medium.com/@manzurulhoque
status
ok
fetched_at
2026-07-10 22:17:52