Source code for logeverything.correlation
"""
Correlation Context for LogEverything.
Provides async-safe, thread-safe correlation ID propagation using contextvars.
This is the foundation for request tracing across middleware, transports, and dashboards.
Key Features:
- ContextVar-based correlation IDs (async-safe, same pattern as indent_manager.py)
- Request context propagation (method, path, client)
- CorrelationFilter for automatic LogRecord injection
- Thread propagation decorator for executor-based workloads
"""
import functools
import logging
import os
from contextvars import ContextVar, copy_context
from typing import Any, Callable, Dict, Optional
# --- Context Variables ---
_correlation_id: ContextVar[str] = ContextVar("correlation_id", default="")
_request_context: ContextVar[Dict[str, Any]] = ContextVar("request_context", default={})
# --- Public API ---
[docs]
def set_correlation_id(cid: Optional[str] = None) -> str:
"""
Set or auto-generate a correlation ID for the current context.
Args:
cid: An explicit correlation ID. If None, a 16-char hex ID is generated.
Returns:
The correlation ID that was set.
"""
if cid is None:
cid = os.urandom(8).hex()
_correlation_id.set(cid)
return cid
[docs]
def get_correlation_id() -> str:
"""
Get the current correlation ID.
Returns:
The correlation ID, or empty string if unset.
"""
return _correlation_id.get()
[docs]
def set_request_context(ctx: Dict[str, Any]) -> None:
"""
Set request context metadata for the current context.
Typical keys: ``method``, ``path``, ``client_ip``.
Args:
ctx: Dictionary of request metadata.
"""
_request_context.set(ctx)
[docs]
def get_request_context() -> Dict[str, Any]:
"""
Get the current request context metadata.
Returns:
Dictionary of request metadata, or empty dict if unset.
"""
return _request_context.get()
[docs]
def clear_correlation() -> None:
"""Reset all correlation state for the current context."""
_correlation_id.set("")
_request_context.set({})
# --- logging.Filter ---
[docs]
class CorrelationFilter(logging.Filter):
"""
A logging filter that auto-injects correlation context into every LogRecord.
Adds the following attributes to each record:
- ``correlation_id``
- ``request_method``
- ``request_path``
Usage::
import logging
from logeverything.correlation import CorrelationFilter
handler = logging.StreamHandler()
handler.addFilter(CorrelationFilter())
logger = logging.getLogger("myapp")
logger.addHandler(handler)
"""
[docs]
def filter(self, record: logging.LogRecord) -> bool:
"""Inject correlation fields into the log record."""
record.correlation_id = _correlation_id.get()
ctx = _request_context.get()
record.request_method = ctx.get("method", "")
record.request_path = ctx.get("path", "")
return True
# --- Thread Propagation ---
[docs]
def propagate_context(func: Callable) -> Callable:
"""
Decorator that copies the current contextvars into a new thread.
Use this when dispatching work to ``concurrent.futures.ThreadPoolExecutor``
or ``threading.Thread`` to ensure correlation IDs follow the call.
Example::
from logeverything.correlation import propagate_context
@propagate_context
def background_task():
# get_correlation_id() returns the caller's ID here
...
executor.submit(background_task)
"""
ctx = copy_context()
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
return ctx.run(func, *args, **kwargs)
return wrapper