fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

ContextVar modified in async deps are not available in middleware (with structlog)

Open charlax opened this issue 2 years ago • 2 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the FastAPI documentation, with the integrated search.
  • [X] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to FastAPI but to Pydantic.
  • [X] I already checked if it is not related to FastAPI but to Swagger UI.
  • [X] I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

import logging
import uuid
from typing import Callable, Awaitable

import structlog
from fastapi import Depends, FastAPI, Request, Response

app = FastAPI()
Middleware = Callable[[Request], Awaitable[Response]]

logger = structlog.get_logger(__name__)


@app.middleware("http")
async def request_middleware(request: Request, call_next: Middleware) -> Response:
    structlog.contextvars.clear_contextvars()

    request_id = str(uuid.uuid4())
    structlog.contextvars.bind_contextvars(request_id=request_id)

    logger.info(
        "request received (in middlware)",
        method=request.method,
        path=request.url.path,
        client=request.client and request.client.host,
        ua=request.headers.get("User-Agent"),
    )


    response = await call_next(request)

    # THIS LINE WON'T INCLUDE in_dep="true"
    logger.info("request finished (in middleware)")

    response.headers["Request-ID"] = request_id
    return response


def setup_logging() -> None:
    """Configure logging."""
    logging.basicConfig(format="%(message)s", level="INFO")

    processors = [
        structlog.contextvars.merge_contextvars,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M.%S"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.dev.ConsoleRenderer(),
    ]

    structlog.configure(
        processors=processors,  # type: ignore
        wrapper_class=structlog.stdlib.BoundLogger,
        logger_factory=structlog.stdlib.LoggerFactory(),
        cache_logger_on_first_use=True,
    )


async def dep():
    logger.info("dep start")
    structlog.contextvars.bind_contextvars(in_dep="true")
    logger.info("dep end")
    return "foo"


@app.get("/")
async def home(value: str = Depends(dep)):
    logger.info("hello")
    return value


setup_logging()

Description

curl http://127.0.0.1:8001

The included code lets you reproduce the issue. This is what's logged:

2022-03-17 15:22.38 [info     ] request received (in middlware) [app] client=127.0.0.1 method=GET path=/ request_id=3cd6772f-4cb4-4cd4-9ace-44c2a3f19710 ua=curl/7.77.0
2022-03-17 15:22.38 [info     ] dep start                      [app] request_id=3cd6772f-4cb4-4cd4-9ace-44c2a3f19710
2022-03-17 15:22.38 [info     ] dep end                        [app] in_dep=true request_id=3cd6772f-4cb4-4cd4-9ace-44c2a3f19710
2022-03-17 15:22.38 [info     ] hello                          [app] in_dep=true request_id=3cd6772f-4cb4-4cd4-9ace-44c2a3f19710
2022-03-17 15:22.38 [info     ] request finished (in middleware) [app] request_id=3cd6772f-4cb4-4cd4-9ace-44c2a3f19710

In the last "request finished" line, I was expected in_dep=true to be kept, but it's not there.

It might be coming from structlog, but I doubt it since it's probably just using contextvars under the hood.

Operating System

macOS

Operating System Details

No response

FastAPI Version

0.75.0

Python Version

3.9.10

Additional Context

Tickets that seem relevant to this issue:

  • https://github.com/tiangolo/fastapi/issues/953 (but that's only about deps, not middleware)
  • https://github.com/tiangolo/fastapi/issues/397
  • https://github.com/encode/starlette/issues/1081

I have read the following pages:

  • https://fastapi.tiangolo.com/async/

In the meantime I might be using https://github.com/snok/asgi-correlation-id

charlax avatar Mar 17 '22 15:03 charlax

I have a similar case and for me it's not even related to Depends, but also happens if you simply use bind_contextvars() in the route handler.

timofurrer avatar Jul 15 '22 09:07 timofurrer

Experiencing the same thing here on 0.78.0. Just to add it seems like the context returned from contextvars.copy_context in structlog does not contain the updated/added vars. The global variable _CONTEXT_VARS in structlog does contain the added variables so it seems to not be a structlog issue.

jarviliam avatar Jul 23 '22 08:07 jarviliam

same here, cannot seem to come up with a fix that would allow me to use structlog with fastapi. Because of concurrency, cannot use threadlocal, but contextvars don't seem to work either

ramilmsh avatar Aug 19 '22 11:08 ramilmsh