File size: 12,552 Bytes
6613cd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95687b7
fdc056d
6613cd9
 
 
 
 
 
95687b7
 
6613cd9
 
 
 
 
 
95687b7
 
6613cd9
 
 
 
 
 
 
95687b7
 
6613cd9
 
 
 
 
 
 
95687b7
 
6613cd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95687b7
6613cd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95687b7
 
6613cd9
 
 
 
 
95687b7
 
 
6613cd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
"""Structured logging with correlation IDs and context management."""

import logging
import json
import uuid
import time
from typing import Dict, Any, Optional, Union
from datetime import datetime
from contextvars import ContextVar
from dataclasses import dataclass, asdict
from enum import Enum

# Context variable for correlation ID
correlation_id_context: ContextVar[Optional[str]] = ContextVar('correlation_id', default=None)


class LogLevel(Enum):
    """Log levels."""
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"


@dataclass
class LogContext:
    """Structured log context."""
    correlation_id: str
    operation: Optional[str] = None
    component: Optional[str] = None
    user_id: Optional[str] = None
    session_id: Optional[str] = None
    request_id: Optional[str] = None
    metadata: Optional[Dict[str, Any]] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {k: v for k, v in asdict(self).items() if v is not None}


class StructuredLogger:
    """Structured logger with correlation ID support."""

    def __init__(self, name: str, enable_json_logging: bool = True):
        """
        Initialize structured logger.

        Args:
            name: Logger name
            enable_json_logging: Whether to use JSON format
        """
        self.logger = logging.getLogger(name)
        self.enable_json_logging = enable_json_logging
        self._setup_formatter()

    def _setup_formatter(self) -> None:
        """Setup log formatter."""
        if self.enable_json_logging:
            formatter = JsonFormatter()
        else:
            formatter = ContextFormatter()

        # Apply formatter to all handlers
        for handler in self.logger.handlers:
            handler.setFormatter(formatter)

    def _get_log_data(self, message: str, level: str,
                     context: Optional[LogContext] = None,
                     extra: Optional[Dict[str, Any]] = None,
                     exception: Optional[Exception] = None) -> Dict[str, Any]:
        """
        Prepare log data structure.

        Args:
            message: Log message
            level: Log level
            context: Log context
            extra: Extra data
            exception: Exception if any

        Returns:
            Dict[str, Any]: Structured log data
        """
        # Get correlation ID from context or generate new one
        correlation_id = correlation_id_context.get()
        if not correlation_id and context:
            correlation_id = context.correlation_id
        if not correlation_id:
            correlation_id = str(uuid.uuid4())

        log_data = {
            'timestamp': datetime.utcnow().isoformat() + 'Z',
            'level': level,
            'message': message,
            'correlation_id': correlation_id,
            'logger_name': self.logger.name
        }

        # Add context information
        if context:
            log_data.update(context.to_dict())

        # Add extra data
        if extra:
            log_data['extra'] = extra

        # Add exception information
        if exception:
            log_data['exception'] = {
                'type': type(exception).__name__,
                'message': str(exception),
                'module': getattr(exception, '__module__', None)
            }

            # Add stack trace for debugging
            import traceback
            log_data['exception']['traceback'] = traceback.format_exc()

        return log_data

    def debug(self, message: str, context: Optional[LogContext] = None,
             extra: Optional[Dict[str, Any]] = None) -> None:
        """Log debug message."""
        if self.logger.isEnabledFor(logging.DEBUG):
            log_data = self._get_log_data(message, LogLevel.DEBUG.value, context, extra)
            # Use 'structured_data' to avoid conflicts with LogRecord attributes
            self.logger.info(message, extra={'structured_data': log_data})

    def info(self, message: str, context: Optional[LogContext] = None,
            extra: Optional[Dict[str, Any]] = None) -> None:
        """Log info message."""
        if self.logger.isEnabledFor(logging.INFO):
            log_data = self._get_log_data(message, LogLevel.INFO.value, context, extra)
            # Use 'structured_data' to avoid conflicts with LogRecord attributes
            self.logger.info(message, extra={'structured_data': log_data})

    def warning(self, message: str, context: Optional[LogContext] = None,
               extra: Optional[Dict[str, Any]] = None) -> None:
        """Log warning message."""
        if self.logger.isEnabledFor(logging.WARNING):
            log_data = self._get_log_data(message, LogLevel.WARNING.value, context, extra)
            # Use 'structured_data' to avoid conflicts with LogRecord attributes
            self.logger.warning(message, extra={'structured_data': log_data})

    def error(self, message: str, context: Optional[LogContext] = None,
             extra: Optional[Dict[str, Any]] = None,
             exception: Optional[Exception] = None) -> None:
        """Log error message."""
        if self.logger.isEnabledFor(logging.ERROR):
            log_data = self._get_log_data(message, LogLevel.ERROR.value, context, extra, exception)
            # Use 'structured_data' to avoid conflicts with LogRecord attributes
            self.logger.error(message, extra={'structured_data': log_data})

    def critical(self, message: str, context: Optional[LogContext] = None,
                extra: Optional[Dict[str, Any]] = None,
                exception: Optional[Exception] = None) -> None:
        """Log critical message."""
        if self.logger.isEnabledFor(logging.CRITICAL):
            log_data = self._get_log_data(message, LogLevel.CRITICAL.value, context, extra, exception)
            # Use 'structured_data' to avoid conflicts with LogRecord attributes
            self.logger.critical(message, extra={'structured_data': log_data})

    def log_operation_start(self, operation: str, context: Optional[LogContext] = None,
                          extra: Optional[Dict[str, Any]] = None) -> str:
        """
        Log operation start and return correlation ID.

        Args:
            operation: Operation name
            context: Log context
            extra: Extra data

        Returns:
            str: Correlation ID for the operation
        """
        correlation_id = str(uuid.uuid4())

        if context:
            context.correlation_id = correlation_id
            context.operation = operation
        else:
            context = LogContext(correlation_id=correlation_id, operation=operation)

        # Set correlation ID in context
        correlation_id_context.set(correlation_id)

        self.info(f"Operation started: {operation}", context, extra)
        return correlation_id

    def log_operation_end(self, operation: str, correlation_id: str,
                         success: bool = True, duration: Optional[float] = None,
                         context: Optional[LogContext] = None,
                         extra: Optional[Dict[str, Any]] = None) -> None:
        """
        Log operation end.

        Args:
            operation: Operation name
            correlation_id: Correlation ID
            success: Whether operation succeeded
            duration: Operation duration in seconds
            context: Log context
            extra: Extra data
        """
        if context:
            context.correlation_id = correlation_id
            context.operation = operation
        else:
            context = LogContext(correlation_id=correlation_id, operation=operation)

        # Add performance data
        if extra is None:
            extra = {}
        extra['success'] = success
        if duration is not None:
            extra['duration_seconds'] = duration

        status = "completed successfully" if success else "failed"
        message = f"Operation {status}: {operation}"

        if success:
            self.info(message, context, extra)
        else:
            self.error(message, context, extra)

    def log_performance_metric(self, metric_name: str, value: Union[int, float],
                             unit: str = None, context: Optional[LogContext] = None,
                             extra: Optional[Dict[str, Any]] = None) -> None:
        """
        Log performance metric.

        Args:
            metric_name: Name of the metric
            value: Metric value
            unit: Unit of measurement
            context: Log context
            extra: Extra data
        """
        if extra is None:
            extra = {}

        extra['metric'] = {
            'name': metric_name,
            'value': value,
            'unit': unit,
            'timestamp': time.time()
        }

        message = f"Performance metric: {metric_name}={value}"
        if unit:
            message += f" {unit}"

        self.info(message, context, extra)


class JsonFormatter(logging.Formatter):
    """JSON log formatter."""

    def format(self, record: logging.LogRecord) -> str:
        """Format log record as JSON."""
        try:
            # Get structured data from extra
            log_data = getattr(record, 'structured_data', {})

            # Ensure basic fields are present
            if 'timestamp' not in log_data:
                log_data['timestamp'] = datetime.utcnow().isoformat() + 'Z'
            if 'level' not in log_data:
                log_data['level'] = record.levelname
            if 'message' not in log_data:
                log_data['message'] = record.getMessage()
            if 'logger_name' not in log_data:
                log_data['logger_name'] = record.name

            # Add standard log record fields
            log_data.update({
                'module': record.module,
                'function': record.funcName,
                'line': record.lineno,
                'thread': record.thread,
                'process': record.process
            })

            return json.dumps(log_data, default=str, ensure_ascii=False)

        except Exception as e:
            # Fallback to standard formatting
            return f"JSON formatting error: {e} | Original: {record.getMessage()}"


class ContextFormatter(logging.Formatter):
    """Context-aware log formatter."""

    def __init__(self):
        """Initialize formatter."""
        super().__init__(
            fmt='%(asctime)s - %(name)s - %(levelname)s - [%(correlation_id)s] - %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )

    def format(self, record: logging.LogRecord) -> str:
        """Format log record with context."""
        try:
            # Get correlation ID from context or record
            correlation_id = correlation_id_context.get()
            if not correlation_id:
                structured_data = getattr(record, 'structured_data', {})
                correlation_id = structured_data.get('correlation_id', 'unknown')

            # Add correlation ID to record
            record.correlation_id = correlation_id

            # Add context information if available
            structured_data = getattr(record, 'structured_data', {})
            if 'operation' in structured_data:
                record.message = f"[{structured_data['operation']}] {record.getMessage()}"

            return super().format(record)

        except Exception as e:
            # Fallback formatting
            return f"Formatting error: {e} | Original: {record.getMessage()}"


def get_structured_logger(name: str, enable_json_logging: bool = True) -> StructuredLogger:
    """
    Get a structured logger instance.

    Args:
        name: Logger name
        enable_json_logging: Whether to use JSON format

    Returns:
        StructuredLogger: Configured structured logger
    """
    return StructuredLogger(name, enable_json_logging)


def set_correlation_id(correlation_id: str) -> None:
    """
    Set correlation ID in context.

    Args:
        correlation_id: Correlation ID to set
    """
    correlation_id_context.set(correlation_id)


def get_correlation_id() -> Optional[str]:
    """
    Get current correlation ID from context.

    Returns:
        Optional[str]: Current correlation ID
    """
    return correlation_id_context.get()


def generate_correlation_id() -> str:
    """
    Generate a new correlation ID.

    Returns:
        str: New correlation ID
    """
    return str(uuid.uuid4())