tiphys.logging
Structured logging configuration for Tiphys.
Uses structlog for structured JSON logging with request-scoped context propagation.
Features:
- Request-scoped logging context (correlation IDs, channel, peer info)
- Context propagation through async operations
- Structured JSON output for production
- Colored console output for development
1""" 2Structured logging configuration for Tiphys. 3 4Uses structlog for structured JSON logging with request-scoped context propagation. 5 6Features: 7- Request-scoped logging context (correlation IDs, channel, peer info) 8- Context propagation through async operations 9- Structured JSON output for production 10- Colored console output for development 11""" 12 13from __future__ import annotations 14 15import logging 16import sys 17from collections.abc import MutableMapping 18from contextvars import ContextVar 19from dataclasses import dataclass, field 20from typing import Any 21from uuid import uuid4 22 23import structlog 24from structlog.types import Processor 25 26from tiphys.config import LogLevel 27 28# Context variable for request-scoped logging 29_log_context: ContextVar[dict[str, Any] | None] = ContextVar("log_context", default=None) 30 31 32@dataclass 33class LogContext: 34 """ 35 Request-scoped logging context. 36 37 Provides structured context that propagates through the request lifecycle. 38 All fields are automatically included in log output. 39 40 Example: 41 from tiphys.logging import LogContext, set_log_context 42 43 # At request start 44 ctx = LogContext( 45 channel_name="webchat", 46 peer_id="user123", 47 session_key="webchat:user123", 48 ) 49 set_log_context(ctx) 50 51 # All subsequent logs will include the context 52 logger.info("Processing message") # Includes correlation_id, channel, etc. 53 """ 54 55 correlation_id: str = field(default_factory=lambda: str(uuid4())) 56 channel_name: str | None = None 57 peer_id: str | None = None 58 session_key: str | None = None 59 agent_id: str | None = None 60 message_id: str | None = None 61 extra: dict[str, Any] = field(default_factory=dict) 62 63 def as_dict(self) -> dict[str, Any]: 64 """Convert to dict for structlog binding.""" 65 result: dict[str, Any] = {"correlation_id": self.correlation_id} 66 if self.channel_name: 67 result["channel"] = self.channel_name 68 if self.peer_id: 69 result["peer_id"] = self.peer_id 70 if self.session_key: 71 result["session_key"] = self.session_key 72 if self.agent_id: 73 result["agent_id"] = self.agent_id 74 if self.message_id: 75 result["message_id"] = self.message_id 76 result.update(self.extra) 77 return result 78 79 def with_extra(self, **kwargs: Any) -> LogContext: 80 """Return a new context with additional fields.""" 81 new_extra = {**self.extra, **kwargs} 82 return LogContext( 83 correlation_id=self.correlation_id, 84 channel_name=self.channel_name, 85 peer_id=self.peer_id, 86 session_key=self.session_key, 87 agent_id=self.agent_id, 88 message_id=self.message_id, 89 extra=new_extra, 90 ) 91 92 93def set_log_context(ctx: LogContext) -> None: 94 """ 95 Set the current logging context. 96 97 Args: 98 ctx: The LogContext to set as current. 99 """ 100 _log_context.set(ctx.as_dict()) 101 102 103def get_log_context() -> dict[str, Any]: 104 """ 105 Get the current logging context. 106 107 Returns: 108 Dict of context values, or empty dict if not set. 109 """ 110 return _log_context.get() or {} 111 112 113def add_log_context(**kwargs: Any) -> None: 114 """ 115 Add fields to the current logging context. 116 117 Args: 118 **kwargs: Fields to add to the context. 119 """ 120 ctx = (_log_context.get() or {}).copy() 121 ctx.update(kwargs) 122 _log_context.set(ctx) 123 124 125def clear_log_context() -> None: 126 """Clear the current logging context.""" 127 _log_context.set({}) 128 129 130def inject_context( 131 logger: Any, 132 method_name: str, 133 event_dict: MutableMapping[str, Any], 134) -> MutableMapping[str, Any]: 135 """ 136 Structlog processor that injects request-scoped context. 137 138 Context values are injected but can be overridden by explicit log arguments. 139 140 Args: 141 logger: The bound logger. 142 method_name: The logging method name. 143 event_dict: The event dictionary. 144 145 Returns: 146 Event dict with context values merged. 147 """ 148 ctx = get_log_context() 149 # Context values are defaults - explicit arguments override them 150 return {**ctx, **event_dict} 151 152 153def setup_logging( 154 level: LogLevel = LogLevel.INFO, 155 json_output: bool = False, 156) -> None: 157 """ 158 Configure structured logging for Tiphys. 159 160 Args: 161 level: Minimum log level to output. 162 json_output: If True, output logs as JSON. Otherwise, use colored console output. 163 """ 164 # Convert LogLevel enum to logging level 165 log_level = getattr(logging, level.value) 166 167 # Shared processors for all configurations 168 shared_processors: list[Processor] = [ 169 structlog.contextvars.merge_contextvars, 170 inject_context, # Inject request-scoped context (correlation_id, channel, etc.) 171 structlog.processors.add_log_level, 172 structlog.processors.TimeStamper(fmt="iso"), 173 structlog.stdlib.PositionalArgumentsFormatter(), 174 structlog.processors.StackInfoRenderer(), 175 structlog.processors.UnicodeDecoder(), 176 ] 177 178 if json_output: 179 # JSON output for production 180 processors: list[Processor] = [ 181 *shared_processors, 182 structlog.processors.format_exc_info, 183 structlog.processors.JSONRenderer(), 184 ] 185 else: 186 # Colored console output for development 187 processors = [ 188 *shared_processors, 189 structlog.dev.ConsoleRenderer(colors=True), 190 ] 191 192 # Configure structlog 193 structlog.configure( 194 processors=processors, 195 wrapper_class=structlog.make_filtering_bound_logger(log_level), 196 context_class=dict, 197 logger_factory=structlog.PrintLoggerFactory(), 198 cache_logger_on_first_use=True, 199 ) 200 201 # Also configure stdlib logging for third-party libraries 202 logging.basicConfig( 203 format="%(message)s", 204 stream=sys.stdout, 205 level=log_level, 206 ) 207 208 # Reduce noise from third-party libraries 209 logging.getLogger("uvicorn").setLevel(logging.WARNING) 210 logging.getLogger("uvicorn.access").setLevel(logging.WARNING) 211 logging.getLogger("httpx").setLevel(logging.WARNING) 212 logging.getLogger("httpcore").setLevel(logging.WARNING) 213 214 215def get_logger(name: str) -> structlog.typing.FilteringBoundLogger: 216 """ 217 Get a logger for a specific module. 218 219 Args: 220 name: Module name (typically __name__). 221 222 Returns: 223 A bound logger instance. 224 """ 225 return structlog.get_logger(name) # type: ignore[no-any-return]
33@dataclass 34class LogContext: 35 """ 36 Request-scoped logging context. 37 38 Provides structured context that propagates through the request lifecycle. 39 All fields are automatically included in log output. 40 41 Example: 42 from tiphys.logging import LogContext, set_log_context 43 44 # At request start 45 ctx = LogContext( 46 channel_name="webchat", 47 peer_id="user123", 48 session_key="webchat:user123", 49 ) 50 set_log_context(ctx) 51 52 # All subsequent logs will include the context 53 logger.info("Processing message") # Includes correlation_id, channel, etc. 54 """ 55 56 correlation_id: str = field(default_factory=lambda: str(uuid4())) 57 channel_name: str | None = None 58 peer_id: str | None = None 59 session_key: str | None = None 60 agent_id: str | None = None 61 message_id: str | None = None 62 extra: dict[str, Any] = field(default_factory=dict) 63 64 def as_dict(self) -> dict[str, Any]: 65 """Convert to dict for structlog binding.""" 66 result: dict[str, Any] = {"correlation_id": self.correlation_id} 67 if self.channel_name: 68 result["channel"] = self.channel_name 69 if self.peer_id: 70 result["peer_id"] = self.peer_id 71 if self.session_key: 72 result["session_key"] = self.session_key 73 if self.agent_id: 74 result["agent_id"] = self.agent_id 75 if self.message_id: 76 result["message_id"] = self.message_id 77 result.update(self.extra) 78 return result 79 80 def with_extra(self, **kwargs: Any) -> LogContext: 81 """Return a new context with additional fields.""" 82 new_extra = {**self.extra, **kwargs} 83 return LogContext( 84 correlation_id=self.correlation_id, 85 channel_name=self.channel_name, 86 peer_id=self.peer_id, 87 session_key=self.session_key, 88 agent_id=self.agent_id, 89 message_id=self.message_id, 90 extra=new_extra, 91 )
Request-scoped logging context.
Provides structured context that propagates through the request lifecycle. All fields are automatically included in log output.
Example: from tiphys.logging import LogContext, set_log_context
# At request start
ctx = LogContext(
channel_name="webchat",
peer_id="user123",
session_key="webchat:user123",
)
set_log_context(ctx)
# All subsequent logs will include the context
logger.info("Processing message") # Includes correlation_id, channel, etc.
64 def as_dict(self) -> dict[str, Any]: 65 """Convert to dict for structlog binding.""" 66 result: dict[str, Any] = {"correlation_id": self.correlation_id} 67 if self.channel_name: 68 result["channel"] = self.channel_name 69 if self.peer_id: 70 result["peer_id"] = self.peer_id 71 if self.session_key: 72 result["session_key"] = self.session_key 73 if self.agent_id: 74 result["agent_id"] = self.agent_id 75 if self.message_id: 76 result["message_id"] = self.message_id 77 result.update(self.extra) 78 return result
Convert to dict for structlog binding.
80 def with_extra(self, **kwargs: Any) -> LogContext: 81 """Return a new context with additional fields.""" 82 new_extra = {**self.extra, **kwargs} 83 return LogContext( 84 correlation_id=self.correlation_id, 85 channel_name=self.channel_name, 86 peer_id=self.peer_id, 87 session_key=self.session_key, 88 agent_id=self.agent_id, 89 message_id=self.message_id, 90 extra=new_extra, 91 )
Return a new context with additional fields.
94def set_log_context(ctx: LogContext) -> None: 95 """ 96 Set the current logging context. 97 98 Args: 99 ctx: The LogContext to set as current. 100 """ 101 _log_context.set(ctx.as_dict())
Set the current logging context.
Args: ctx: The LogContext to set as current.
104def get_log_context() -> dict[str, Any]: 105 """ 106 Get the current logging context. 107 108 Returns: 109 Dict of context values, or empty dict if not set. 110 """ 111 return _log_context.get() or {}
Get the current logging context.
Returns: Dict of context values, or empty dict if not set.
114def add_log_context(**kwargs: Any) -> None: 115 """ 116 Add fields to the current logging context. 117 118 Args: 119 **kwargs: Fields to add to the context. 120 """ 121 ctx = (_log_context.get() or {}).copy() 122 ctx.update(kwargs) 123 _log_context.set(ctx)
Add fields to the current logging context.
Args: **kwargs: Fields to add to the context.
126def clear_log_context() -> None: 127 """Clear the current logging context.""" 128 _log_context.set({})
Clear the current logging context.
131def inject_context( 132 logger: Any, 133 method_name: str, 134 event_dict: MutableMapping[str, Any], 135) -> MutableMapping[str, Any]: 136 """ 137 Structlog processor that injects request-scoped context. 138 139 Context values are injected but can be overridden by explicit log arguments. 140 141 Args: 142 logger: The bound logger. 143 method_name: The logging method name. 144 event_dict: The event dictionary. 145 146 Returns: 147 Event dict with context values merged. 148 """ 149 ctx = get_log_context() 150 # Context values are defaults - explicit arguments override them 151 return {**ctx, **event_dict}
Structlog processor that injects request-scoped context.
Context values are injected but can be overridden by explicit log arguments.
Args: logger: The bound logger. method_name: The logging method name. event_dict: The event dictionary.
Returns: Event dict with context values merged.
154def setup_logging( 155 level: LogLevel = LogLevel.INFO, 156 json_output: bool = False, 157) -> None: 158 """ 159 Configure structured logging for Tiphys. 160 161 Args: 162 level: Minimum log level to output. 163 json_output: If True, output logs as JSON. Otherwise, use colored console output. 164 """ 165 # Convert LogLevel enum to logging level 166 log_level = getattr(logging, level.value) 167 168 # Shared processors for all configurations 169 shared_processors: list[Processor] = [ 170 structlog.contextvars.merge_contextvars, 171 inject_context, # Inject request-scoped context (correlation_id, channel, etc.) 172 structlog.processors.add_log_level, 173 structlog.processors.TimeStamper(fmt="iso"), 174 structlog.stdlib.PositionalArgumentsFormatter(), 175 structlog.processors.StackInfoRenderer(), 176 structlog.processors.UnicodeDecoder(), 177 ] 178 179 if json_output: 180 # JSON output for production 181 processors: list[Processor] = [ 182 *shared_processors, 183 structlog.processors.format_exc_info, 184 structlog.processors.JSONRenderer(), 185 ] 186 else: 187 # Colored console output for development 188 processors = [ 189 *shared_processors, 190 structlog.dev.ConsoleRenderer(colors=True), 191 ] 192 193 # Configure structlog 194 structlog.configure( 195 processors=processors, 196 wrapper_class=structlog.make_filtering_bound_logger(log_level), 197 context_class=dict, 198 logger_factory=structlog.PrintLoggerFactory(), 199 cache_logger_on_first_use=True, 200 ) 201 202 # Also configure stdlib logging for third-party libraries 203 logging.basicConfig( 204 format="%(message)s", 205 stream=sys.stdout, 206 level=log_level, 207 ) 208 209 # Reduce noise from third-party libraries 210 logging.getLogger("uvicorn").setLevel(logging.WARNING) 211 logging.getLogger("uvicorn.access").setLevel(logging.WARNING) 212 logging.getLogger("httpx").setLevel(logging.WARNING) 213 logging.getLogger("httpcore").setLevel(logging.WARNING)
Configure structured logging for Tiphys.
Args: level: Minimum log level to output. json_output: If True, output logs as JSON. Otherwise, use colored console output.
216def get_logger(name: str) -> structlog.typing.FilteringBoundLogger: 217 """ 218 Get a logger for a specific module. 219 220 Args: 221 name: Module name (typically __name__). 222 223 Returns: 224 A bound logger instance. 225 """ 226 return structlog.get_logger(name) # type: ignore[no-any-return]
Get a logger for a specific module.
Args: name: Module name (typically __name__).
Returns: A bound logger instance.