Logging is deceptively tricky. When I started building my app, I assumed logging would be simple β just add logger.info()
or logger.error()
where needed. But as the app grew, so did the problems:
- Inconsistent logs from different layers
- Inability to trace what failed and why
- Unstructured logs that Fluent Bit couldnβt reliably parse
- Missing context (like response time, request ID) in error logs
I had questions. A lot of them. Here's how I answered them β and built a logging system thatβs clean, structured, and future-ready.
π€― 1. My First Realization: Logging Is Closely Tied to Exceptions
Doubt: βLogging needs all details at the end (status code, time taken, etc.), but most of those are only known at the response stage. Should I log in request and response both?β
Answer:
β Logging once β at the response/finalization stage β makes more sense. You get the complete picture: time taken, status code, route, etc.
β Logging both request and response separately introduces duplication and inconsistency.
π 2. Then I Asked: How Do I Get Debug Details from Inside the Library?
Doubt: βIf I log inside the library, I miss outer context like time taken. But if I don't log there, I lose debug info.β
Insight:
I realized exceptions are the bridge β I can raise exceptions from deep inside the code, carrying debug metadata with them. Then, at the top-level (e.g., middleware), I catch and log the whole thing.
ποΈ 3. My First Design Attempt: Logging + Exception Carrying Details
class AppException(Exception):
def __init__(self, message, level="error", details=None):
super().__init__(message)
self.level = level
self.details = details or {}
π‘ Now I could raise exceptions with structured metadata. I made sure every exception had a
level
anddetails
dict β which the logger could later use.
π€ 4. I Wanted JSON Logs for Fluent Bit
Doubt: βHow do I send structured logs to Fluent Bit, and will all logs have the same JSON format?β
Answer:
I used Python's
logging.config.dictConfig()
with a JSON formatter (e.g.,json-log-formatter
orstructlog
). Now every log has fields likelevel
,message
,request_id
,time_taken
, etc.
π§ͺ 5. Then I Faced: Should Exceptions Decide Log Level?
Doubt: βWait, does the exception decide if this is info
, debug
, or error
? Or does the logger?β
Answer:
β I let exceptions carry the log level as an enum (
DEBUG
,INFO
,ERROR
, etc.).
Then my logger dynamically callslogger.debug()
,logger.info()
, etc. based on that.
π§΅ 6. Then the Real Design Emerged: Middleware-Level Logging
I moved all logging to the final response handler:
- If thereβs no exception, log
INFO
with full context (status
,duration
, etc.) - If there's a custom exception, log using its
level
anddetails
- If it's unhandled, log as
ERROR
with traceback
Example final logger:
def log_exception(exc, request_context):
if isinstance(exc, AppException):
getattr(logger, exc.level)(exc.args[0], extra={**exc.details, **request_context})
else:
logger.error("Unhandled exception", exc_info=exc, extra=request_context)
π 7. I Also Asked: What About DEBUG
Logs?
Doubt: βDo debug logs bubble up to middleware too?β
Answer:
β If debug details are important, I attach them as
details
in exceptions.
π« I donβt emit low-level debug logs directly β instead, I raise them with exceptions if they matter for observability.
π§ 8. I Realized: Logs and Exceptions Are Loosely Coupled, But Work Together
Insight:
Exceptions carry structured data, but they don't log themselves. The logger does that, at a single point, using info from the exception.
I even thought about creating a single field like log_level
and one details
dict β now the logger only looks at two fields, making it future-proof and decoupled.
π₯ 9. Final Problem: FastAPIβs HTTPException
Doesnβt Work for This
Doubt: βI'm using FastAPI. Should I subclass HTTPException
or replace it?β
Answer:
β I wrapped
HTTPException
into a customLoggedException
, addinglevel
anddetails
β Middleware logs and re-raises it as usual
π« I never log inside business logic β I only raise with metadata
π¦ Final Architecture
lib code
β
βββ raises AppException("user not found", level="info", details={...})
β
controller
β
βββ catches exception
βββ logs once with log level and details (JSON)
βββ returns response
π Tools I Considered or Used
Tool | Use |
---|---|
logging + dictConfig
|
Core logger setup |
json-log-formatter |
JSON formatter for structured logs |
structlog |
More advanced context-based structured logging |
contextvars |
Track request-specific info like trace_id |
Fluent Bit |
Ship logs to log storage |
Elasticsearch , Loki , Sentry
|
Log & error analysis |
π§΅ Key Takeaways
- Raise structured exceptions with log metadata
- Log once, centrally, with all context (request ID, time taken, etc.)
- Use JSON format for compatibility with log collectors
- Let exceptions carry log level, but log only at the final point
- Avoid scattered logs β treat logs as events, not prints
π Written on 2025-06-01
Top comments (0)