From 39499c9f294baba7a46b21713d2a64109322c229 Mon Sep 17 00:00:00 2001 From: 0206pdh <0206pdh@naver.com> Date: Fri, 3 Jul 2026 12:29:58 +0900 Subject: [PATCH] fix(logging): read user_email from span attributes, not a ContextVar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전 PR(#94)의 ContextVar 방식이 실제로는 동작하지 않는 버그였다. get_current_user()는 동기 함수(def)라 FastAPI가 스레드풀에서 실행하는데, anyio.to_thread.run_sync는 contextvars.copy_context()로 복사본을 만들어 그 안에서 실행하므로 ContextVar.set()이 원래 요청 컨텍스트로 전파되지 않는다. 즉 이미지를 새로 배포해도 로그에 user_email이 절대 찍히지 않았다. span.set_attribute()는 이미 값이 채워진 span 객체 자체를 참조로 공유하기 때문에 스레드 경계와 무관하게 작동한다 (user.id/user.email 트레이스 검색이 이미 되던 이유). 그래서 별도 ContextVar 없이 포매터에서 현재 span의 attributes를 직접 읽도록 변경. --- app/api/dependencies.py | 2 -- app/core/logging.py | 17 ++++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/api/dependencies.py b/app/api/dependencies.py index d188e37..434401a 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -9,7 +9,6 @@ from app.core.db import get_db_session from app.core.enums import UserStatus from app.core.config import get_settings -from app.core.logging import set_request_user_email from app.core.security import decode_access_token from app.repositories.user import UserRepository from app.schemas.auth import TokenPayload, UserRead @@ -52,7 +51,6 @@ def get_current_user( span = trace.get_current_span() span.set_attribute("user.id", str(resolved.id)) span.set_attribute("user.email", resolved.email) - set_request_user_email(resolved.email) return resolved diff --git a/app/core/logging.py b/app/core/logging.py index 70d3b52..2109946 100644 --- a/app/core/logging.py +++ b/app/core/logging.py @@ -4,17 +4,9 @@ import json import logging -from contextvars import ContextVar from opentelemetry import trace -_user_email_var: ContextVar[str] = ContextVar('user_email', default='') - - -def set_request_user_email(email: str) -> None: - _user_email_var.set(email) - - _SKIP_ATTRS = frozenset(( "name", "msg", "args", "levelname", "levelno", "pathname", "filename", "module", "exc_info", @@ -41,7 +33,14 @@ def format(self, record: logging.LogRecord) -> str: "span_id": format(ctx.span_id, "016x") if ctx.is_valid else "", } - user_email = _user_email_var.get() + # Read off the span's attributes rather than a ContextVar: the + # dependency that resolves the user runs in FastAPI's threadpool + # (sync def), and contextvars.copy_context() there means any + # ContextVar.set() inside it never propagates back to the request's + # context. The span object itself is shared by reference across + # that boundary, so attribute mutations on it are visible everywhere. + attributes = getattr(span, "attributes", None) or {} + user_email = attributes.get("user.email") if user_email: entry["user_email"] = user_email