File size: 6,805 Bytes
9024ad9
7ab470d
 
 
 
 
9024ad9
2ed2bd7
7ab470d
9024ad9
7ab470d
29ed661
2ed2bd7
7ab470d
 
9024ad9
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75fe59b
 
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9024ad9
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75fe59b
9024ad9
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
9024ad9
 
 
7ab470d
 
 
 
 
 
 
 
 
 
 
2ed2bd7
7ab470d
 
 
 
9024ad9
2ed2bd7
 
 
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
 
75fe59b
7ab470d
2ed2bd7
 
 
 
7ab470d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75fe59b
7ab470d
2ed2bd7
9024ad9
7ab470d
 
 
 
 
 
 
 
 
 
 
75fe59b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"""
Logging configuration for the text summarizer backend using Loguru.

This module provides structured logging with automatic request ID tracking,
environment-aware formatting (JSON for production, colored text for development),
and backward-compatible API with the previous stdlib logging implementation.
"""

import os
import sys
from contextvars import ContextVar
from typing import Any

from loguru import logger

from app.core.config import settings

# Context variable for request ID (automatic propagation across async contexts)
request_id_var: ContextVar[str | None] = ContextVar("request_id", default=None)


def _serialize_record(record: dict) -> str:
    """
    Custom serializer for JSON format that includes extra fields.

    Args:
        record: Loguru record dictionary

    Returns:
        JSON formatted log string
    """
    import json

    # Build structured log entry
    log_entry = {
        "timestamp": record["time"].isoformat(),
        "level": record["level"].name,
        "logger": record["name"],
        "message": record["message"],
        "function": record["function"],
        "line": record["line"],
    }

    # Add request ID from context if available
    request_id = request_id_var.get()
    if request_id:
        log_entry["request_id"] = request_id

    # Add any extra fields bound to the logger
    if record["extra"]:
        log_entry.update(record["extra"])

    # Add exception info if present
    if record["exception"]:
        log_entry["exception"] = {
            "type": record["exception"].type.__name__
            if record["exception"].type
            else None,
            "value": str(record["exception"].value),
        }

    return json.dumps(log_entry)


def _determine_log_format() -> str:
    """
    Determine log format based on environment.

    Returns:
        "json" for production (HF Spaces), "text" for development
    """
    # Check if LOG_FORMAT is explicitly set
    log_format = getattr(settings, "log_format", "auto")

    if log_format == "auto":
        # Auto-detect: JSON if running on HuggingFace Spaces, text otherwise
        is_hf_spaces = os.getenv("HF_SPACE_ROOT_PATH") is not None
        return "json" if is_hf_spaces else "text"

    return log_format


def setup_logging() -> None:
    """
    Set up Loguru logging configuration.

    Configures logging with environment-aware formatting:
    - Production (HF Spaces): Structured JSON output for log aggregation
    - Development: Colored, human-readable text output

    The logger automatically includes request IDs from context variables.
    """
    # Remove default handler
    logger.remove()

    # Determine format based on environment
    log_format_type = _determine_log_format()
    log_level = settings.log_level.upper()

    if log_format_type == "json":
        # Production: JSON structured logging
        logger.add(
            sys.stdout,
            format=_serialize_record,
            level=log_level,
            serialize=False,  # We handle serialization ourselves
            backtrace=True,
            diagnose=False,  # Don't show local variables in production
        )
    else:
        # Development: Colored text logging
        logger.add(
            sys.stdout,
            format=(
                "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
                "<level>{level: <8}</level> | "
                "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
                "<level>{message}</level>"
            ),
            level=log_level,
            colorize=True,
            backtrace=True,
            diagnose=True,  # Show local variables for debugging
        )

    # Log startup configuration
    logger.info(f"Logging initialized with format={log_format_type}, level={log_level}")


def get_logger(name: str) -> Any:
    """
    Get a logger instance (backward-compatible with stdlib logging).

    Args:
        name: Logger name (typically __name__ of the calling module)

    Returns:
        Loguru logger instance bound to the module name
    """
    # Bind the logger to the module name for context
    return logger.bind(module=name)


class RequestLogger:
    """
    Logger for request/response logging with automatic request ID tracking.

    This class provides a backward-compatible API with the previous
    stdlib logging implementation, but uses Loguru with automatic
    context variable propagation for request IDs.
    """

    def __init__(self, base_logger: Any = None):
        """
        Initialize request logger.

        Args:
            base_logger: Base logger (ignored, uses global Loguru logger)
        """
        # Always use the global logger with automatic request ID binding
        self.logger = logger

    def log_request(
        self, method: str, path: str, request_id: str, **kwargs: Any
    ) -> None:
        """
        Log incoming request with structured fields.

        Args:
            method: HTTP method (GET, POST, etc.)
            path: Request path
            request_id: Unique request identifier
            **kwargs: Additional fields to log
        """
        # Get request ID from context var (fallback to parameter)
        context_request_id = request_id_var.get() or request_id

        self.logger.bind(
            request_id=context_request_id, method=method, path=path, **kwargs
        ).info(f"Request {context_request_id}: {method} {path}")

    def log_response(
        self, request_id: str, status_code: int, duration_ms: float, **kwargs: Any
    ) -> None:
        """
        Log response with structured fields.

        Args:
            request_id: Unique request identifier
            status_code: HTTP status code
            duration_ms: Request duration in milliseconds
            **kwargs: Additional fields to log
        """
        # Get request ID from context var (fallback to parameter)
        context_request_id = request_id_var.get() or request_id

        self.logger.bind(
            request_id=context_request_id,
            status_code=status_code,
            duration_ms=duration_ms,
            **kwargs,
        ).info(f"Response {context_request_id}: {status_code} ({duration_ms:.2f}ms)")

    def log_error(self, request_id: str, error: str, **kwargs: Any) -> None:
        """
        Log error with structured fields.

        Args:
            request_id: Unique request identifier
            error: Error message
            **kwargs: Additional fields to log
        """
        # Get request ID from context var (fallback to parameter)
        context_request_id = request_id_var.get() or request_id

        self.logger.bind(request_id=context_request_id, error=error, **kwargs).error(
            f"Error {context_request_id}: {error}"
        )