log2fast-fastapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- log2fast_fastapi/__init__.py +27 -0
- log2fast_fastapi/__version__.py +5 -0
- log2fast_fastapi/base.py +342 -0
- log2fast_fastapi/formatters.py +148 -0
- log2fast_fastapi/middleware.py +106 -0
- log2fast_fastapi/settings.py +187 -0
- log2fast_fastapi-0.1.0.dist-info/METADATA +287 -0
- log2fast_fastapi-0.1.0.dist-info/RECORD +11 -0
- log2fast_fastapi-0.1.0.dist-info/WHEEL +5 -0
- log2fast_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
- log2fast_fastapi-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .__version__ import __version__
|
|
2
|
+
from .base import FastLogger, get_logger
|
|
3
|
+
from .middleware import RequestLoggingMiddleware, get_request_id
|
|
4
|
+
from .settings import (
|
|
5
|
+
LogEnvironment,
|
|
6
|
+
LogFileSettings,
|
|
7
|
+
LogFormat,
|
|
8
|
+
LogLevel,
|
|
9
|
+
LogSettings,
|
|
10
|
+
RotationStrategy,
|
|
11
|
+
settings,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__",
|
|
16
|
+
"FastLogger",
|
|
17
|
+
"get_logger",
|
|
18
|
+
"LogSettings",
|
|
19
|
+
"LogLevel",
|
|
20
|
+
"LogFormat",
|
|
21
|
+
"LogEnvironment",
|
|
22
|
+
"LogFileSettings",
|
|
23
|
+
"RotationStrategy",
|
|
24
|
+
"settings",
|
|
25
|
+
"RequestLoggingMiddleware",
|
|
26
|
+
"get_request_id",
|
|
27
|
+
]
|
log2fast_fastapi/base.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .formatters import (
|
|
7
|
+
ColoredFormatter,
|
|
8
|
+
JSONFormatter,
|
|
9
|
+
SimpleFormatter,
|
|
10
|
+
StructuredFormatter,
|
|
11
|
+
)
|
|
12
|
+
from .settings import LogFormat, LogSettings, RotationStrategy, settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FastLogger:
|
|
16
|
+
"""
|
|
17
|
+
Professional logger wrapper for FastAPI applications.
|
|
18
|
+
|
|
19
|
+
Provides environment-aware logging with support for:
|
|
20
|
+
- Multiple output formats (JSON, colored, structured, simple)
|
|
21
|
+
- File rotation
|
|
22
|
+
- Module-based loggers
|
|
23
|
+
- Context injection (request_id, user_id, etc.)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_instances: dict[str, logging.Logger] = {}
|
|
27
|
+
_configured: bool = False
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
name: str,
|
|
32
|
+
config: LogSettings | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize a FastLogger instance.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
name: Logger name (typically module name)
|
|
39
|
+
config: Optional custom configuration (uses global settings if not provided)
|
|
40
|
+
"""
|
|
41
|
+
self.name = name
|
|
42
|
+
self.config = config or settings
|
|
43
|
+
self.logger = self._get_or_create_logger(name)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def _get_or_create_logger(cls, name: str) -> logging.Logger:
|
|
47
|
+
"""Get or create a logger instance."""
|
|
48
|
+
if name not in cls._instances:
|
|
49
|
+
logger = logging.getLogger(name)
|
|
50
|
+
|
|
51
|
+
# Configure logger if not already configured
|
|
52
|
+
if not cls._configured:
|
|
53
|
+
cls._configure_root_logger()
|
|
54
|
+
|
|
55
|
+
cls._instances[name] = logger
|
|
56
|
+
|
|
57
|
+
return cls._instances[name]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def _configure_root_logger(cls) -> None:
|
|
61
|
+
"""Configure the root logger with handlers and formatters."""
|
|
62
|
+
if cls._configured:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
root_logger = logging.getLogger()
|
|
66
|
+
root_logger.setLevel(settings.get_effective_level())
|
|
67
|
+
|
|
68
|
+
# Remove existing handlers
|
|
69
|
+
root_logger.handlers.clear()
|
|
70
|
+
|
|
71
|
+
# Add console handler if enabled
|
|
72
|
+
if settings.console_enabled:
|
|
73
|
+
console_handler = logging.StreamHandler()
|
|
74
|
+
console_handler.setLevel(settings.get_effective_level())
|
|
75
|
+
console_handler.setFormatter(
|
|
76
|
+
cls._get_formatter(settings.get_effective_format())
|
|
77
|
+
)
|
|
78
|
+
root_logger.addHandler(console_handler)
|
|
79
|
+
|
|
80
|
+
# Add file handler if enabled
|
|
81
|
+
if settings.file_settings.enabled:
|
|
82
|
+
file_handler = cls._create_file_handler()
|
|
83
|
+
if file_handler:
|
|
84
|
+
file_handler.setLevel(settings.get_effective_level())
|
|
85
|
+
# Always use JSON format for file output in production
|
|
86
|
+
if settings.get_effective_format() == LogFormat.JSON.value:
|
|
87
|
+
file_handler.setFormatter(cls._get_formatter(LogFormat.JSON.value))
|
|
88
|
+
else:
|
|
89
|
+
file_handler.setFormatter(
|
|
90
|
+
cls._get_formatter(LogFormat.STRUCTURED.value)
|
|
91
|
+
)
|
|
92
|
+
root_logger.addHandler(file_handler)
|
|
93
|
+
|
|
94
|
+
cls._configured = True
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _get_formatter(cls, format_type: str) -> logging.Formatter:
|
|
98
|
+
"""Get the appropriate formatter based on format type."""
|
|
99
|
+
formatters = {
|
|
100
|
+
LogFormat.JSON.value: JSONFormatter(),
|
|
101
|
+
LogFormat.COLORED.value: ColoredFormatter(
|
|
102
|
+
fmt="[%(asctime)s] %(levelname)s | %(name)s | %(message)s"
|
|
103
|
+
),
|
|
104
|
+
LogFormat.STRUCTURED.value: StructuredFormatter(),
|
|
105
|
+
LogFormat.SIMPLE.value: SimpleFormatter(),
|
|
106
|
+
}
|
|
107
|
+
return formatters.get(format_type, SimpleFormatter())
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _create_file_handler(
|
|
111
|
+
cls, logger_name: str | None = None
|
|
112
|
+
) -> RotatingFileHandler | TimedRotatingFileHandler | None:
|
|
113
|
+
"""
|
|
114
|
+
Create a rotating file handler (time-based or size-based).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
logger_name: Optional logger name for per-module files
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
# Create logs directory if it doesn't exist
|
|
121
|
+
log_dir = settings.file_settings.directory
|
|
122
|
+
if not os.path.isabs(log_dir):
|
|
123
|
+
# Make it relative to current working directory (where the app runs)
|
|
124
|
+
log_dir = os.path.join(os.getcwd(), log_dir)
|
|
125
|
+
|
|
126
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
# Generate filename
|
|
129
|
+
filename_vars = {
|
|
130
|
+
"module": settings.module_name or "app",
|
|
131
|
+
"environment": settings.log_environment.value,
|
|
132
|
+
"logger": logger_name or "app",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Use logger name if per_module_files is enabled
|
|
136
|
+
if settings.file_settings.per_module_files and logger_name:
|
|
137
|
+
# Sanitize logger name for filename (replace dots with underscores)
|
|
138
|
+
safe_logger_name = logger_name.replace(".", "_")
|
|
139
|
+
filename_vars["logger"] = safe_logger_name
|
|
140
|
+
# Override pattern to include logger name
|
|
141
|
+
filename = f"{safe_logger_name}_{settings.log_environment.value}.log"
|
|
142
|
+
else:
|
|
143
|
+
filename = settings.file_settings.filename_pattern.format(
|
|
144
|
+
**filename_vars
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
filepath = os.path.join(log_dir, filename)
|
|
148
|
+
|
|
149
|
+
# Create handler based on rotation strategy
|
|
150
|
+
if settings.file_settings.rotation_strategy == RotationStrategy.TIME:
|
|
151
|
+
# Time-based rotation (default: daily at midnight, keep 31 days)
|
|
152
|
+
handler = TimedRotatingFileHandler(
|
|
153
|
+
filepath,
|
|
154
|
+
when=settings.file_settings.when,
|
|
155
|
+
interval=settings.file_settings.interval,
|
|
156
|
+
backupCount=settings.file_settings.backup_count,
|
|
157
|
+
encoding="utf-8",
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
# Size-based rotation
|
|
161
|
+
handler = RotatingFileHandler(
|
|
162
|
+
filepath,
|
|
163
|
+
maxBytes=settings.file_settings.max_bytes,
|
|
164
|
+
backupCount=settings.file_settings.backup_count,
|
|
165
|
+
encoding="utf-8",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return handler
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"⚠️ Failed to create file handler: {e}")
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def reconfigure(cls, new_settings: LogSettings) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Reconfigure all loggers with new settings.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
new_settings: New logging configuration
|
|
181
|
+
"""
|
|
182
|
+
global settings
|
|
183
|
+
settings = new_settings
|
|
184
|
+
cls._configured = False
|
|
185
|
+
cls._configure_root_logger()
|
|
186
|
+
|
|
187
|
+
def _log_with_context(
|
|
188
|
+
self,
|
|
189
|
+
level: int,
|
|
190
|
+
message: str,
|
|
191
|
+
extra_data: dict[str, Any] | None = None,
|
|
192
|
+
only_in: list[str] | None = None,
|
|
193
|
+
**kwargs: Any,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Internal method to log with context.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
level: Log level
|
|
200
|
+
message: Log message
|
|
201
|
+
extra_data: Extra context data
|
|
202
|
+
only_in: List of environments where this log should appear (e.g., ['development', 'debug'])
|
|
203
|
+
If None, logs in all environments
|
|
204
|
+
**kwargs: Additional arguments for logger
|
|
205
|
+
"""
|
|
206
|
+
# Check if we should log in current environment
|
|
207
|
+
if only_in is not None:
|
|
208
|
+
current_env = self.config.log_environment.value
|
|
209
|
+
if current_env not in only_in:
|
|
210
|
+
# Skip logging in this environment
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
extra = kwargs.get("extra", {})
|
|
214
|
+
|
|
215
|
+
if extra_data:
|
|
216
|
+
extra["extra_data"] = extra_data
|
|
217
|
+
|
|
218
|
+
kwargs["extra"] = extra
|
|
219
|
+
self.logger.log(level, message, **kwargs)
|
|
220
|
+
|
|
221
|
+
# Public logging methods
|
|
222
|
+
def debug(
|
|
223
|
+
self,
|
|
224
|
+
message: str,
|
|
225
|
+
extra_data: dict[str, Any] | None = None,
|
|
226
|
+
only_in: list[str] | None = None,
|
|
227
|
+
**kwargs: Any,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Log a debug message.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
message: Log message
|
|
234
|
+
extra_data: Extra context data
|
|
235
|
+
only_in: List of environments to log in (e.g., ['development', 'debug'])
|
|
236
|
+
"""
|
|
237
|
+
self._log_with_context(logging.DEBUG, message, extra_data, only_in, **kwargs)
|
|
238
|
+
|
|
239
|
+
def info(
|
|
240
|
+
self,
|
|
241
|
+
message: str,
|
|
242
|
+
extra_data: dict[str, Any] | None = None,
|
|
243
|
+
only_in: list[str] | None = None,
|
|
244
|
+
**kwargs: Any,
|
|
245
|
+
) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Log an info message.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
message: Log message
|
|
251
|
+
extra_data: Extra context data
|
|
252
|
+
only_in: List of environments to log in (e.g., ['development', 'production'])
|
|
253
|
+
"""
|
|
254
|
+
self._log_with_context(logging.INFO, message, extra_data, only_in, **kwargs)
|
|
255
|
+
|
|
256
|
+
def warning(
|
|
257
|
+
self,
|
|
258
|
+
message: str,
|
|
259
|
+
extra_data: dict[str, Any] | None = None,
|
|
260
|
+
only_in: list[str] | None = None,
|
|
261
|
+
**kwargs: Any,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Log a warning message.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
message: Log message
|
|
268
|
+
extra_data: Extra context data
|
|
269
|
+
only_in: List of environments to log in
|
|
270
|
+
"""
|
|
271
|
+
self._log_with_context(logging.WARNING, message, extra_data, only_in, **kwargs)
|
|
272
|
+
|
|
273
|
+
def error(
|
|
274
|
+
self,
|
|
275
|
+
message: str,
|
|
276
|
+
extra_data: dict[str, Any] | None = None,
|
|
277
|
+
only_in: list[str] | None = None,
|
|
278
|
+
**kwargs: Any,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Log an error message.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
message: Log message
|
|
285
|
+
extra_data: Extra context data
|
|
286
|
+
only_in: List of environments to log in
|
|
287
|
+
"""
|
|
288
|
+
self._log_with_context(logging.ERROR, message, extra_data, only_in, **kwargs)
|
|
289
|
+
|
|
290
|
+
def critical(
|
|
291
|
+
self,
|
|
292
|
+
message: str,
|
|
293
|
+
extra_data: dict[str, Any] | None = None,
|
|
294
|
+
only_in: list[str] | None = None,
|
|
295
|
+
**kwargs: Any,
|
|
296
|
+
) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Log a critical message.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
message: Log message
|
|
302
|
+
extra_data: Extra context data
|
|
303
|
+
only_in: List of environments to log in
|
|
304
|
+
"""
|
|
305
|
+
self._log_with_context(logging.CRITICAL, message, extra_data, only_in, **kwargs)
|
|
306
|
+
|
|
307
|
+
def exception(
|
|
308
|
+
self,
|
|
309
|
+
message: str,
|
|
310
|
+
extra_data: dict[str, Any] | None = None,
|
|
311
|
+
only_in: list[str] | None = None,
|
|
312
|
+
**kwargs: Any,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Log an exception with traceback.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
message: Log message
|
|
319
|
+
extra_data: Extra context data
|
|
320
|
+
only_in: List of environments to log in
|
|
321
|
+
"""
|
|
322
|
+
kwargs["exc_info"] = True
|
|
323
|
+
self._log_with_context(logging.ERROR, message, extra_data, only_in, **kwargs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_logger(name: str, config: LogSettings | None = None) -> FastLogger:
|
|
327
|
+
"""
|
|
328
|
+
Factory function to get a module-specific logger.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
name: Logger name (typically __name__ of the calling module)
|
|
332
|
+
config: Optional custom configuration
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
FastLogger instance
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> from log2fast_fastapi import get_logger
|
|
339
|
+
>>> logger = get_logger(__name__)
|
|
340
|
+
>>> logger.info("Application started")
|
|
341
|
+
"""
|
|
342
|
+
return FastLogger(name, config)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ColoredFormatter(logging.Formatter):
|
|
8
|
+
"""Colored formatter for console output in development."""
|
|
9
|
+
|
|
10
|
+
# ANSI color codes
|
|
11
|
+
COLORS = {
|
|
12
|
+
"DEBUG": "\033[36m", # Cyan
|
|
13
|
+
"INFO": "\033[32m", # Green
|
|
14
|
+
"WARNING": "\033[33m", # Yellow
|
|
15
|
+
"ERROR": "\033[31m", # Red
|
|
16
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
17
|
+
}
|
|
18
|
+
RESET = "\033[0m"
|
|
19
|
+
BOLD = "\033[1m"
|
|
20
|
+
|
|
21
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
22
|
+
"""Format log record with colors.
|
|
23
|
+
|
|
24
|
+
IMPORTANT: We create a shallow copy of the record to avoid modifying
|
|
25
|
+
the original, which would cause color codes to leak into file logs.
|
|
26
|
+
"""
|
|
27
|
+
# Create a shallow copy to avoid modifying the original record
|
|
28
|
+
record_copy = logging.makeLogRecord(record.__dict__)
|
|
29
|
+
|
|
30
|
+
# Add color to level name
|
|
31
|
+
levelname = record_copy.levelname
|
|
32
|
+
if levelname in self.COLORS:
|
|
33
|
+
record_copy.levelname = (
|
|
34
|
+
f"{self.COLORS[levelname]}{self.BOLD}{levelname}{self.RESET}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Add color to module name
|
|
38
|
+
record_copy.name = f"\033[94m{record_copy.name}{self.RESET}" # Blue
|
|
39
|
+
|
|
40
|
+
# Format timestamp
|
|
41
|
+
record_copy.asctime = self.formatTime(record_copy, "%Y-%m-%d %H:%M:%S")
|
|
42
|
+
|
|
43
|
+
# Format the message using the copy
|
|
44
|
+
formatted = super().format(record_copy)
|
|
45
|
+
|
|
46
|
+
return formatted
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class JSONFormatter(logging.Formatter):
|
|
50
|
+
"""JSON formatter for structured logging in production."""
|
|
51
|
+
|
|
52
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
53
|
+
"""Format log record as JSON."""
|
|
54
|
+
log_data: dict[str, Any] = {
|
|
55
|
+
"timestamp": datetime.fromtimestamp(record.created).isoformat(),
|
|
56
|
+
"level": record.levelname,
|
|
57
|
+
"logger": record.name,
|
|
58
|
+
"message": record.getMessage(),
|
|
59
|
+
"module": record.module,
|
|
60
|
+
"function": record.funcName,
|
|
61
|
+
"line": record.lineno,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Add exception info if present
|
|
65
|
+
if record.exc_info:
|
|
66
|
+
log_data["exception"] = self.formatException(record.exc_info)
|
|
67
|
+
|
|
68
|
+
# Add extra fields from record
|
|
69
|
+
if hasattr(record, "request_id"):
|
|
70
|
+
log_data["request_id"] = record.request_id
|
|
71
|
+
|
|
72
|
+
if hasattr(record, "user_id"):
|
|
73
|
+
log_data["user_id"] = record.user_id
|
|
74
|
+
|
|
75
|
+
if hasattr(record, "extra_data"):
|
|
76
|
+
log_data["extra"] = record.extra_data
|
|
77
|
+
|
|
78
|
+
# Add any custom attributes
|
|
79
|
+
for key, value in record.__dict__.items():
|
|
80
|
+
if key not in [
|
|
81
|
+
"name",
|
|
82
|
+
"msg",
|
|
83
|
+
"args",
|
|
84
|
+
"created",
|
|
85
|
+
"filename",
|
|
86
|
+
"funcName",
|
|
87
|
+
"levelname",
|
|
88
|
+
"levelno",
|
|
89
|
+
"lineno",
|
|
90
|
+
"module",
|
|
91
|
+
"msecs",
|
|
92
|
+
"message",
|
|
93
|
+
"pathname",
|
|
94
|
+
"process",
|
|
95
|
+
"processName",
|
|
96
|
+
"relativeCreated",
|
|
97
|
+
"thread",
|
|
98
|
+
"threadName",
|
|
99
|
+
"exc_info",
|
|
100
|
+
"exc_text",
|
|
101
|
+
"stack_info",
|
|
102
|
+
"request_id",
|
|
103
|
+
"user_id",
|
|
104
|
+
"extra_data",
|
|
105
|
+
]:
|
|
106
|
+
if not key.startswith("_"):
|
|
107
|
+
log_data[key] = value
|
|
108
|
+
|
|
109
|
+
return json.dumps(log_data, ensure_ascii=False)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class StructuredFormatter(logging.Formatter):
|
|
113
|
+
"""Structured formatter for human-readable logs with context."""
|
|
114
|
+
|
|
115
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
116
|
+
"""Format log record with structured information."""
|
|
117
|
+
# Base format
|
|
118
|
+
base = f"[{self.formatTime(record, '%Y-%m-%d %H:%M:%S')}] {record.levelname:8s} | {record.name:20s} | {record.getMessage()}"
|
|
119
|
+
|
|
120
|
+
# Add context if available
|
|
121
|
+
context_parts = []
|
|
122
|
+
|
|
123
|
+
if hasattr(record, "request_id"):
|
|
124
|
+
context_parts.append(f"request_id={record.request_id}")
|
|
125
|
+
|
|
126
|
+
if hasattr(record, "user_id"):
|
|
127
|
+
context_parts.append(f"user_id={record.user_id}")
|
|
128
|
+
|
|
129
|
+
if hasattr(record, "extra_data") and record.extra_data:
|
|
130
|
+
for key, value in record.extra_data.items():
|
|
131
|
+
context_parts.append(f"{key}={value}")
|
|
132
|
+
|
|
133
|
+
if context_parts:
|
|
134
|
+
base += f" [{', '.join(context_parts)}]"
|
|
135
|
+
|
|
136
|
+
# Add exception if present
|
|
137
|
+
if record.exc_info:
|
|
138
|
+
base += f"\n{self.formatException(record.exc_info)}"
|
|
139
|
+
|
|
140
|
+
return base
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class SimpleFormatter(logging.Formatter):
|
|
144
|
+
"""Simple formatter for testing and minimal output."""
|
|
145
|
+
|
|
146
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
147
|
+
"""Format log record simply."""
|
|
148
|
+
return f"[{record.levelname}] {record.name}: {record.getMessage()}"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import uuid
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
|
|
6
|
+
from fastapi import Request, Response
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
|
|
9
|
+
from .base import get_logger
|
|
10
|
+
|
|
11
|
+
# Context variable to store request ID across async calls
|
|
12
|
+
request_id_var: ContextVar[str] = ContextVar("request_id", default="")
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
18
|
+
"""
|
|
19
|
+
Middleware for logging HTTP requests and responses in FastAPI.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Automatic request ID generation
|
|
23
|
+
- Request/response timing
|
|
24
|
+
- Configurable body logging
|
|
25
|
+
- Context injection for request ID
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
async def dispatch(
|
|
29
|
+
self, request: Request, call_next: Callable[[Request], Response]
|
|
30
|
+
) -> Response:
|
|
31
|
+
"""Process the request and log information."""
|
|
32
|
+
# Generate unique request ID
|
|
33
|
+
request_id = str(uuid.uuid4())
|
|
34
|
+
request_id_var.set(request_id)
|
|
35
|
+
|
|
36
|
+
# Start timing
|
|
37
|
+
start_time = time.time()
|
|
38
|
+
|
|
39
|
+
# Log request
|
|
40
|
+
logger.info(
|
|
41
|
+
f"Request started: {request.method} {request.url.path}",
|
|
42
|
+
extra_data={
|
|
43
|
+
"method": request.method,
|
|
44
|
+
"path": request.url.path,
|
|
45
|
+
"query_params": str(request.query_params),
|
|
46
|
+
"client_host": request.client.host if request.client else None,
|
|
47
|
+
},
|
|
48
|
+
extra={"request_id": request_id},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Process request
|
|
52
|
+
try:
|
|
53
|
+
response = await call_next(request)
|
|
54
|
+
|
|
55
|
+
# Calculate duration
|
|
56
|
+
duration = time.time() - start_time
|
|
57
|
+
|
|
58
|
+
# Log response
|
|
59
|
+
logger.info(
|
|
60
|
+
f"Request completed: {request.method} {request.url.path} - {response.status_code}",
|
|
61
|
+
extra_data={
|
|
62
|
+
"method": request.method,
|
|
63
|
+
"path": request.url.path,
|
|
64
|
+
"status_code": response.status_code,
|
|
65
|
+
"duration_ms": round(duration * 1000, 2),
|
|
66
|
+
},
|
|
67
|
+
extra={"request_id": request_id},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Add request ID to response headers
|
|
71
|
+
response.headers["X-Request-ID"] = request_id
|
|
72
|
+
|
|
73
|
+
return response
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# Calculate duration
|
|
77
|
+
duration = time.time() - start_time
|
|
78
|
+
|
|
79
|
+
# Log error
|
|
80
|
+
logger.exception(
|
|
81
|
+
f"Request failed: {request.method} {request.url.path}",
|
|
82
|
+
extra_data={
|
|
83
|
+
"method": request.method,
|
|
84
|
+
"path": request.url.path,
|
|
85
|
+
"error": str(e),
|
|
86
|
+
"duration_ms": round(duration * 1000, 2),
|
|
87
|
+
},
|
|
88
|
+
extra={"request_id": request_id},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Re-raise the exception
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_request_id() -> str:
|
|
96
|
+
"""
|
|
97
|
+
Get the current request ID from context.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Current request ID or empty string if not in request context
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> from log2fast_fastapi.middleware import get_request_id
|
|
104
|
+
>>> request_id = get_request_id()
|
|
105
|
+
"""
|
|
106
|
+
return request_id_var.get()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
# Look for .env in the current working directory (where the app is running)
|
|
8
|
+
# This allows the package to work correctly when installed via pip
|
|
9
|
+
DOTENV_PATH = os.path.join(os.getcwd(), ".env")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogLevel(str, Enum):
|
|
13
|
+
"""Log levels supported by the logger."""
|
|
14
|
+
|
|
15
|
+
DEBUG = "DEBUG"
|
|
16
|
+
INFO = "INFO"
|
|
17
|
+
WARNING = "WARNING"
|
|
18
|
+
ERROR = "ERROR"
|
|
19
|
+
CRITICAL = "CRITICAL"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LogEnvironment(str, Enum):
|
|
23
|
+
"""Supported logging environments."""
|
|
24
|
+
|
|
25
|
+
DEVELOPMENT = "development"
|
|
26
|
+
PRODUCTION = "production"
|
|
27
|
+
TESTING = "testing"
|
|
28
|
+
DEBUG = "debug"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LogFormat(str, Enum):
|
|
32
|
+
"""Supported log formats."""
|
|
33
|
+
|
|
34
|
+
JSON = "json"
|
|
35
|
+
COLORED = "colored"
|
|
36
|
+
STRUCTURED = "structured"
|
|
37
|
+
SIMPLE = "simple"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RotationStrategy(str, Enum):
|
|
41
|
+
"""Log file rotation strategies."""
|
|
42
|
+
|
|
43
|
+
TIME = "time" # Rotate by time (daily, hourly, etc.)
|
|
44
|
+
SIZE = "size" # Rotate by file size
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LogFileSettings(BaseModel):
|
|
48
|
+
"""Configuration for file logging."""
|
|
49
|
+
|
|
50
|
+
enabled: bool = Field(default=True, description="Enable file logging")
|
|
51
|
+
directory: str = Field(default="logs", description="Directory for log files")
|
|
52
|
+
|
|
53
|
+
# Rotation strategy
|
|
54
|
+
rotation_strategy: RotationStrategy = Field(
|
|
55
|
+
default=RotationStrategy.TIME,
|
|
56
|
+
description="Rotation strategy: 'time' (daily) or 'size' (by file size)",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Time-based rotation settings (when rotation_strategy='time')
|
|
60
|
+
when: str = Field(
|
|
61
|
+
default="midnight",
|
|
62
|
+
description="When to rotate: 'midnight', 'H' (hourly), 'D' (daily), 'W0'-'W6' (weekday)",
|
|
63
|
+
)
|
|
64
|
+
interval: int = Field(
|
|
65
|
+
default=1,
|
|
66
|
+
description="Interval for rotation (e.g., 1 for daily, 2 for every 2 days)",
|
|
67
|
+
)
|
|
68
|
+
backup_count: int = Field(
|
|
69
|
+
default=31, description="Number of backup files to keep (default 31 days)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Size-based rotation settings (when rotation_strategy='size')
|
|
73
|
+
max_bytes: int = Field(
|
|
74
|
+
default=10 * 1024 * 1024,
|
|
75
|
+
description="Max size per log file in bytes (10MB default, only for size-based rotation)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Common settings
|
|
79
|
+
filename_pattern: str = Field(
|
|
80
|
+
default="{module}_{environment}.log",
|
|
81
|
+
description="Pattern for log filenames. Available: {module}, {environment}, {logger}",
|
|
82
|
+
)
|
|
83
|
+
per_module_files: bool = Field(
|
|
84
|
+
default=False,
|
|
85
|
+
description="Create separate log files per module (uses logger name in filename)",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LogSettings(BaseSettings):
|
|
90
|
+
"""Main logging configuration."""
|
|
91
|
+
|
|
92
|
+
# Environment configuration
|
|
93
|
+
log_environment: LogEnvironment = Field(
|
|
94
|
+
default=LogEnvironment.DEVELOPMENT,
|
|
95
|
+
description="Current logging environment",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Log level configuration (None = auto-configure based on environment)
|
|
99
|
+
log_level: LogLevel | None = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description="Minimum log level to capture (None = auto from environment)",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Format configuration (None = auto-configure based on environment)
|
|
105
|
+
log_format: LogFormat | None = Field(
|
|
106
|
+
default=None, description="Log output format (None = auto from environment)"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Console logging
|
|
110
|
+
console_enabled: bool = Field(default=True, description="Enable console logging")
|
|
111
|
+
|
|
112
|
+
# File logging
|
|
113
|
+
file_settings: LogFileSettings = Field(
|
|
114
|
+
default_factory=LogFileSettings, description="File logging configuration"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Request logging (for FastAPI middleware)
|
|
118
|
+
log_requests: bool = Field(
|
|
119
|
+
default=True, description="Enable request/response logging"
|
|
120
|
+
)
|
|
121
|
+
log_request_body: bool = Field(
|
|
122
|
+
default=False, description="Include request body in logs (be careful with PII)"
|
|
123
|
+
)
|
|
124
|
+
log_response_body: bool = Field(
|
|
125
|
+
default=False,
|
|
126
|
+
description="Include response body in logs (be careful with PII)",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Module-specific settings
|
|
130
|
+
module_name: str | None = Field(
|
|
131
|
+
default=None, description="Default module name for loggers"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
model_config = SettingsConfigDict(
|
|
135
|
+
env_file=DOTENV_PATH,
|
|
136
|
+
env_file_encoding="utf-8",
|
|
137
|
+
env_prefix="LOG_",
|
|
138
|
+
env_nested_delimiter="__", # Enable reading nested vars like LOG_FILE_SETTINGS__DIRECTORY
|
|
139
|
+
extra="ignore",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_effective_level(self) -> str:
|
|
143
|
+
"""Get the effective log level based on environment."""
|
|
144
|
+
# If explicitly set, use that value
|
|
145
|
+
if self.log_level is not None:
|
|
146
|
+
return self.log_level.value
|
|
147
|
+
|
|
148
|
+
# Auto-configure based on environment
|
|
149
|
+
environment_defaults = {
|
|
150
|
+
LogEnvironment.DEBUG: LogLevel.DEBUG,
|
|
151
|
+
LogEnvironment.DEVELOPMENT: LogLevel.INFO,
|
|
152
|
+
LogEnvironment.TESTING: LogLevel.INFO,
|
|
153
|
+
LogEnvironment.PRODUCTION: LogLevel.WARNING,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return environment_defaults.get(self.log_environment, LogLevel.INFO).value
|
|
157
|
+
|
|
158
|
+
def get_effective_format(self) -> str:
|
|
159
|
+
"""Get the effective log format based on environment."""
|
|
160
|
+
# If explicitly set, use that value
|
|
161
|
+
if self.log_format is not None:
|
|
162
|
+
return self.log_format.value
|
|
163
|
+
|
|
164
|
+
# Auto-configure based on environment
|
|
165
|
+
environment_defaults = {
|
|
166
|
+
LogEnvironment.DEBUG: LogFormat.COLORED,
|
|
167
|
+
LogEnvironment.DEVELOPMENT: LogFormat.COLORED,
|
|
168
|
+
LogEnvironment.TESTING: LogFormat.SIMPLE,
|
|
169
|
+
LogEnvironment.PRODUCTION: LogFormat.JSON,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return environment_defaults.get(self.log_environment, LogFormat.COLORED).value
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
settings = LogSettings()
|
|
177
|
+
except Exception as e:
|
|
178
|
+
import traceback
|
|
179
|
+
|
|
180
|
+
print("🚨 Error loading log configuration:")
|
|
181
|
+
print(e)
|
|
182
|
+
traceback.print_exc()
|
|
183
|
+
# Fallback to defaults (auto-configure based on environment)
|
|
184
|
+
settings = LogSettings(
|
|
185
|
+
log_environment=LogEnvironment.DEVELOPMENT,
|
|
186
|
+
)
|
|
187
|
+
print("⚠️ Using fallback log configuration (DEVELOPMENT mode)")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: log2fast-fastapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Advanced logging module for FastAPI - Designed for Solautyc Team internal use
|
|
5
|
+
Author-email: Angel Daniel Sanchez Castillo <angeldaniel.sanchezcastillo@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Angel Daniel Sanchez Castillo
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/AngelDanielSanchezCastillo/log2fast-fastapi
|
|
29
|
+
Project-URL: Documentation, https://github.com/AngelDanielSanchezCastillo/log2fast-fastapi/tree/main/docs
|
|
30
|
+
Project-URL: Repository, https://github.com/AngelDanielSanchezCastillo/log2fast-fastapi
|
|
31
|
+
Project-URL: Issues, https://github.com/AngelDanielSanchezCastillo/log2fast-fastapi/issues
|
|
32
|
+
Keywords: fastapi,logging,rotation,colored-logs,structured-logging,middleware
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Framework :: FastAPI
|
|
41
|
+
Classifier: Topic :: System :: Logging
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: fastapi>=0.100.0
|
|
47
|
+
Requires-Dist: pydantic>=2.0.0
|
|
48
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
51
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
52
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
53
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
54
|
+
Dynamic: license-file
|
|
55
|
+
|
|
56
|
+
# log2fast-fastapi
|
|
57
|
+
|
|
58
|
+
🚀 Advanced logging module for FastAPI with file rotation, colored output, and environment-based auto-configuration
|
|
59
|
+
|
|
60
|
+
> [!WARNING]
|
|
61
|
+
> **Internal Use Notice**
|
|
62
|
+
>
|
|
63
|
+
> This package is designed and maintained by the **Solautyc Team** for internal use. While it is publicly available, it may not work as expected in all environments or use cases outside of our specific infrastructure. We do not provide support or guarantees for external usage, and we are not responsible for any issues that may arise from using this package in other contexts.
|
|
64
|
+
>
|
|
65
|
+
> Use at your own risk. Contributions and feedback are welcome, but compatibility with external environments is not guaranteed.
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- 🎨 **Multiple Output Formats**: JSON (production), Colored (development), Structured (debugging), Simple (testing)
|
|
70
|
+
- 🌍 **Environment-Based Configuration**: Automatic setup for dev, test, prod, and debug
|
|
71
|
+
- 📦 **Module-Based Loggers**: Each module gets its own logger instance
|
|
72
|
+
- 🔄 **File Rotation**: Automatic log file rotation with configurable size
|
|
73
|
+
- 🔄 **Environment-Specific Logging**: Control which logs appear in which environments (prevent sensitive data leaks)
|
|
74
|
+
- 🚀 **FastAPI Integration**: Middleware for automatic request/response logging with unique request IDs
|
|
75
|
+
- 📊 **Structured Logging**: Add context data to any log message
|
|
76
|
+
- 📊 **Context Injection**: Support for request_id, user_id, and custom context data
|
|
77
|
+
- 🎯 **Zero Configuration**: Works out of the box with sensible defaults
|
|
78
|
+
|
|
79
|
+
## 📚 Documentation
|
|
80
|
+
|
|
81
|
+
- **[Usage Guide](docs/usage.md)** - Comprehensive usage guide with examples
|
|
82
|
+
- **[File Management](docs/file_management.md)** - Complete guide on log rotation and storage (English)
|
|
83
|
+
- **[Gestión de Archivos](docs/file_management_es.md)** - Guía completa de rotación y almacenamiento (Español)
|
|
84
|
+
- **[Logger Best Practices](docs/logger_best_practices.md)** - Best practices for creating and naming loggers
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Installation
|
|
88
|
+
|
|
89
|
+
### From PyPI (Recommended)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install log2fast-fastapi
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### From Source
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Clone the repository
|
|
99
|
+
git clone https://github.com/AngelDanielSanchezCastillo/log2fast-fastapi.git
|
|
100
|
+
cd log2fast-fastapi
|
|
101
|
+
|
|
102
|
+
# Install in development mode
|
|
103
|
+
pip install -e .
|
|
104
|
+
|
|
105
|
+
# Or install with dev dependencies
|
|
106
|
+
pip install -e ".[dev]"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
|
|
112
|
+
### Basic Usage
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from log2fast_fastapi import get_logger
|
|
116
|
+
|
|
117
|
+
logger = get_logger(__name__)
|
|
118
|
+
|
|
119
|
+
logger.info("Application started")
|
|
120
|
+
logger.warning("This is a warning")
|
|
121
|
+
logger.error("An error occurred")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### FastAPI Integration
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from fastapi import FastAPI
|
|
128
|
+
from log2fast_fastapi import RequestLoggingMiddleware, get_logger
|
|
129
|
+
|
|
130
|
+
app = FastAPI()
|
|
131
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
132
|
+
|
|
133
|
+
logger = get_logger(__name__)
|
|
134
|
+
|
|
135
|
+
@app.get("/")
|
|
136
|
+
async def root():
|
|
137
|
+
logger.info("Root endpoint accessed")
|
|
138
|
+
return {"message": "Hello World"}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Logging with Context
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from log2fast_fastapi import get_logger
|
|
145
|
+
|
|
146
|
+
logger = get_logger(__name__)
|
|
147
|
+
|
|
148
|
+
logger.info(
|
|
149
|
+
"User logged in",
|
|
150
|
+
extra_data={
|
|
151
|
+
"user_id": "12345",
|
|
152
|
+
"ip_address": "192.168.1.1"
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Environment-Specific Logging (Prevent Sensitive Data Leaks!)
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Logs ONLY in development/debug (NOT in production)
|
|
161
|
+
logger.debug(
|
|
162
|
+
"Sensitive debug info",
|
|
163
|
+
extra_data={"password_hash": "...", "token": "..."},
|
|
164
|
+
only_in=["development", "debug"]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Logs ONLY in production
|
|
168
|
+
logger.info(
|
|
169
|
+
"Performance metrics",
|
|
170
|
+
extra_data={"response_time": 120},
|
|
171
|
+
only_in=["production"]
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Configuration
|
|
176
|
+
|
|
177
|
+
**Simple: Just set the environment in `.env`**
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# That's it! Format and level auto-configure
|
|
181
|
+
LOG_ENVIRONMENT=production
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Auto-configuration by environment:
|
|
185
|
+
|
|
186
|
+
| Environment | Auto Level | Auto Format |
|
|
187
|
+
|------------|-----------|-------------|
|
|
188
|
+
| `development` | INFO | colored |
|
|
189
|
+
| `production` | WARNING | json |
|
|
190
|
+
| `testing` | INFO | simple |
|
|
191
|
+
| `debug` | DEBUG | colored |
|
|
192
|
+
|
|
193
|
+
**Optional: Override defaults**
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Optional: Override auto-configuration
|
|
198
|
+
LOG_LEVEL=DEBUG
|
|
199
|
+
LOG_FORMAT=json
|
|
200
|
+
LOG_FILE_SETTINGS__ENABLED=true
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Environment Presets
|
|
204
|
+
|
|
205
|
+
### Development
|
|
206
|
+
- Format: Colored console output
|
|
207
|
+
- Level: INFO
|
|
208
|
+
- Perfect for local development
|
|
209
|
+
|
|
210
|
+
### Production
|
|
211
|
+
- Format: JSON (structured)
|
|
212
|
+
- Level: WARNING
|
|
213
|
+
- Optimized for log aggregation tools
|
|
214
|
+
|
|
215
|
+
### Testing
|
|
216
|
+
- Format: Simple
|
|
217
|
+
- Level: INFO
|
|
218
|
+
- Minimal output for tests
|
|
219
|
+
|
|
220
|
+
### Debug
|
|
221
|
+
- Format: Colored
|
|
222
|
+
- Level: DEBUG
|
|
223
|
+
- Maximum verbosity
|
|
224
|
+
|
|
225
|
+
## Documentation
|
|
226
|
+
|
|
227
|
+
See [docs/usage.md](docs/usage.md) for complete documentation including:
|
|
228
|
+
- Advanced configuration
|
|
229
|
+
- Custom formatters
|
|
230
|
+
- Best practices
|
|
231
|
+
- Integration examples
|
|
232
|
+
|
|
233
|
+
## Example
|
|
234
|
+
|
|
235
|
+
Run the example application:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
python log2fast_fastapi/example.py
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Then visit:
|
|
242
|
+
- http://localhost:8000/ - Root endpoint
|
|
243
|
+
- http://localhost:8000/users/123 - User endpoint
|
|
244
|
+
- http://localhost:8000/docs - API documentation
|
|
245
|
+
|
|
246
|
+
## Testing
|
|
247
|
+
|
|
248
|
+
Run the test suite:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
python log2fast_fastapi/tests/test_logging.py
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Module Structure
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
log2fast-fastapi/
|
|
258
|
+
├── pyproject.toml # Package configuration
|
|
259
|
+
├── MANIFEST.in # Additional files to include
|
|
260
|
+
├── README.md # This file
|
|
261
|
+
├── LICENSE # License file
|
|
262
|
+
├── src/
|
|
263
|
+
│ └── log2fast_fastapi/
|
|
264
|
+
│ ├── __init__.py # Main exports
|
|
265
|
+
│ ├── __version__.py # Version information
|
|
266
|
+
│ ├── base.py # Core FastLogger class
|
|
267
|
+
│ ├── settings.py # Configuration with Pydantic
|
|
268
|
+
│ ├── formatters.py # Custom log formatters
|
|
269
|
+
│ └── middleware.py # FastAPI middleware
|
|
270
|
+
├── docs/
|
|
271
|
+
│ ├── usage.md # Complete documentation
|
|
272
|
+
│ ├── file_management.md # File rotation guide (EN)
|
|
273
|
+
│ ├── file_management_es.md # File rotation guide (ES)
|
|
274
|
+
│ ├── logger_best_practices.md # Best practices
|
|
275
|
+
│ └── publishing.md # PyPI publishing guide
|
|
276
|
+
├── examples/
|
|
277
|
+
│ ├── example.py # Basic example
|
|
278
|
+
│ ├── demo_features.py # Feature demonstrations
|
|
279
|
+
│ └── demo_rotation.py # Rotation examples
|
|
280
|
+
└── tests/
|
|
281
|
+
├── test_logging.py # Test suite
|
|
282
|
+
└── test_new_features.py # Feature tests
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
Same as parent project.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
log2fast_fastapi/__init__.py,sha256=MrLI4vM6FL8srk59V17kel__hiTWq-WIJ-MlS26RObM,553
|
|
2
|
+
log2fast_fastapi/__version__.py,sha256=kCz8xqPR71dMahmGZzxB8Rtha610gFPxiN-puxGgNSg,136
|
|
3
|
+
log2fast_fastapi/base.py,sha256=jd0dM3umwY-oGo161p-ianwZlspVBoien19bN8pIPVE,11113
|
|
4
|
+
log2fast_fastapi/formatters.py,sha256=OHjPiZp1aYF21_FK12ffdSkpZYBJVF8qAwybtAIVBcQ,4758
|
|
5
|
+
log2fast_fastapi/middleware.py,sha256=WslD7pQMWvsHYxRI1yLWZD6kffh1bTtmzJ7rZlInFKA,3162
|
|
6
|
+
log2fast_fastapi/settings.py,sha256=tRGoOKigjy1J_kmFjsSiyovjlXe3-ubZL8HUViRNkDs,5962
|
|
7
|
+
log2fast_fastapi-0.1.0.dist-info/licenses/LICENSE,sha256=CkISX1hNEwxxrPTOXet3IYMEH28Bn7SoUyKniRjg68I,1086
|
|
8
|
+
log2fast_fastapi-0.1.0.dist-info/METADATA,sha256=h2RXhjg5kl7C2PRkMeXprVVwJCotkn9QrjV_PguQRkA,9163
|
|
9
|
+
log2fast_fastapi-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
log2fast_fastapi-0.1.0.dist-info/top_level.txt,sha256=f7HIwpDpuUBk-2bhKzlkZ-WWqOS_eR_LO8fZ-XH3mqg,17
|
|
11
|
+
log2fast_fastapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Angel Daniel Sanchez Castillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
log2fast_fastapi
|