Request ID middleware
Under concurrency, two users can hit your agent at the same millisecond. Their log lines interleave in stdout. Without a request ID, you cannot tell which SQL query belongs to which user, and you cannot join logs to traces. A request ID is the cheapest, highest-impact observability primitive you can add.
import contextvars
import uuid
request_id_var: contextvars.ContextVar[str] = contextvars.ContextVar(
"request_id", default=""
)
def new_request_id() -> str:
value = uuid.uuid4().hex[:12]
request_id_var.set(value)
return value
def current_request_id() -> str:
return request_id_var.get()A Python ContextVar travels with the current task, including across asyncio await points. One set call at the top, every caller downstream reads the same value.
import structlog
from observability.context import current_request_id, new_request_id
def add_request_id(_, __, event_dict):
rid = current_request_id()
if rid:
event_dict["request_id"] = rid
return event_dict
structlog.configure(
processors=[
add_request_id,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
)
log = structlog.get_logger("agent")A structlog processor injects the current request ID into every log line. No call site has to remember to pass it.
from observability.context import new_request_id
from observability.logger import log
class EcommerceSession:
def ask(self, question: str) -> str:
rid = new_request_id()
log.info("turn.start", question=question, turn=self.turn_number + 1)
answer = self.graph.invoke({"user_message": question, "conversation_history": self.conversation_history})
log.info("turn.done", request_id=rid)
return answer["final_answer"]One new_request_id call at the top of every turn. Every log line in the router, tools, and synthesiser picks up the same id automatically.
Ordering exercise: Order the request id flow
Loading practice…
Yes. asyncio.create_task copies the current Context by default, so the child task sees the same request_id. If you spawn a thread with run_in_executor, copy the context explicitly with contextvars.copy_context so the id follows the work across the thread boundary.