mtk-logger 0.3.0__tar.gz
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.
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mtk_logger
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Logging utilities for Python applications
|
|
5
|
+
Author: Patrick Prasquier
|
|
6
|
+
Author-email: pprasquier@odm-tech.com
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Dist: fastapi[all] (>=0.115.0,<0.116.0)
|
|
12
|
+
Requires-Dist: prefy (>=1.4.1,<2.0.0)
|
|
13
|
+
Project-URL: Repository, https://github.com/pprasquier/mtk_logger
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# MTK Logger
|
|
17
|
+
|
|
18
|
+
Logging utilities for MarkTrack applications.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
To install from GitHub Packages:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Configure pip to use GitHub Packages
|
|
26
|
+
pip config set global.extra-index-url https://YOUR_GITHUB_TOKEN@github.com/pprasquier/mtk_logger/raw/main/dist/
|
|
27
|
+
|
|
28
|
+
# Install the package
|
|
29
|
+
pip install mtk_logger
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with Poetry:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Add GitHub Packages as a source
|
|
36
|
+
poetry source add --priority=supplemental github https://github.com/pprasquier/mtk_logger/raw/main/dist/
|
|
37
|
+
|
|
38
|
+
# Install the package
|
|
39
|
+
poetry add mtk_logger
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from mtk_logger.logger import get_logger
|
|
46
|
+
|
|
47
|
+
logger = get_logger(__name__)
|
|
48
|
+
logger.info("This is a log message")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For FastAPI middleware:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from mtk_logger.fastapi_middleware import RequestLoggingMiddleware
|
|
55
|
+
|
|
56
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
57
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# MTK Logger
|
|
2
|
+
|
|
3
|
+
Logging utilities for MarkTrack applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To install from GitHub Packages:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Configure pip to use GitHub Packages
|
|
11
|
+
pip config set global.extra-index-url https://YOUR_GITHUB_TOKEN@github.com/pprasquier/mtk_logger/raw/main/dist/
|
|
12
|
+
|
|
13
|
+
# Install the package
|
|
14
|
+
pip install mtk_logger
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or with Poetry:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Add GitHub Packages as a source
|
|
21
|
+
poetry source add --priority=supplemental github https://github.com/pprasquier/mtk_logger/raw/main/dist/
|
|
22
|
+
|
|
23
|
+
# Install the package
|
|
24
|
+
poetry add mtk_logger
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from mtk_logger.logger import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
logger.info("This is a log message")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For FastAPI middleware:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from mtk_logger.fastapi_middleware import RequestLoggingMiddleware
|
|
40
|
+
|
|
41
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
42
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "mtk_logger"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Logging utilities for Python applications"
|
|
5
|
+
authors = ["Patrick Prasquier <pprasquier@odm-tech.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "mtk_logger", from = "src"}]
|
|
8
|
+
repository = "https://github.com/pprasquier/mtk_logger"
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.12"
|
|
12
|
+
fastapi = {extras = ["all"], version = "^0.115.0"}
|
|
13
|
+
prefy = "^1.4.1"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["poetry-core"]
|
|
17
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
from typing import Callable
|
|
6
|
+
from starlette.responses import StreamingResponse
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
import time
|
|
9
|
+
from .logger import get_logger
|
|
10
|
+
|
|
11
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
12
|
+
async def dispatch(self, request: Request, call_next: Callable):
|
|
13
|
+
# Get request details
|
|
14
|
+
request_id = str(uuid.uuid4())
|
|
15
|
+
start_time = time.time()
|
|
16
|
+
logger = get_logger(os.path.basename(__file__))
|
|
17
|
+
|
|
18
|
+
# Extract path and method
|
|
19
|
+
path = request.url.path
|
|
20
|
+
method = request.method
|
|
21
|
+
logger.debug(f'("Request: {request_id} - {method} {path}")')
|
|
22
|
+
|
|
23
|
+
# Try to extract query parameters
|
|
24
|
+
# Convert query_params to dict, preserving lists for repeated keys
|
|
25
|
+
query_params = {}
|
|
26
|
+
for key, value in request.query_params.multi_items():
|
|
27
|
+
if key in query_params:
|
|
28
|
+
logger.debug(f"Duplicate key '{key}' found in query parameters")
|
|
29
|
+
# If the key exists but its value is not a list yet, convert it to a list
|
|
30
|
+
if not isinstance(query_params[key], list):
|
|
31
|
+
query_params[key] = [query_params[key]]
|
|
32
|
+
# Now we can safely append the new value
|
|
33
|
+
query_params[key].append(value)
|
|
34
|
+
else:
|
|
35
|
+
query_params[key] = value
|
|
36
|
+
logger.debug(f"Added key '{key}' to query parameters")
|
|
37
|
+
|
|
38
|
+
# Try to extract request body (this is tricky because we can only read it once)
|
|
39
|
+
body = None
|
|
40
|
+
if method in ["POST", "PUT", "PATCH"]:
|
|
41
|
+
try:
|
|
42
|
+
# We need to read the body and then restore it for the actual endpoint
|
|
43
|
+
body_bytes = await request.body()
|
|
44
|
+
# Restore the request body
|
|
45
|
+
request._body = body_bytes
|
|
46
|
+
|
|
47
|
+
# Try to parse as JSON, but handle non-JSON bodies gracefully
|
|
48
|
+
try:
|
|
49
|
+
body = json.loads(body_bytes.decode())
|
|
50
|
+
# For large payloads, consider truncating or summarizing
|
|
51
|
+
if isinstance(body, dict):
|
|
52
|
+
# For each list in the body, replace with count and sample
|
|
53
|
+
for key, value in body.items():
|
|
54
|
+
if isinstance(value, list) and len(value) > 5:
|
|
55
|
+
sample = value[:3] # Take first 3 items as sample
|
|
56
|
+
body[key] = {
|
|
57
|
+
"count": len(value),
|
|
58
|
+
"sample": sample
|
|
59
|
+
}
|
|
60
|
+
except:
|
|
61
|
+
body = {"raw_size": len(body_bytes), "note": "Non-JSON body"}
|
|
62
|
+
except:
|
|
63
|
+
body = {"note": "Could not read request body"}
|
|
64
|
+
|
|
65
|
+
# Log the request
|
|
66
|
+
log_data = {
|
|
67
|
+
"request_id": request_id,
|
|
68
|
+
"method": method,
|
|
69
|
+
"path": path,
|
|
70
|
+
"query_params": query_params,
|
|
71
|
+
"body": body
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.info(f"API Request: {json.dumps(log_data)}")
|
|
75
|
+
|
|
76
|
+
# Process the request
|
|
77
|
+
response = await call_next(request)
|
|
78
|
+
|
|
79
|
+
# Read and log the response body
|
|
80
|
+
response_body = b""
|
|
81
|
+
async for chunk in response.body_iterator:
|
|
82
|
+
response_body += chunk
|
|
83
|
+
|
|
84
|
+
# Decode the response body
|
|
85
|
+
responses = response_body.decode()
|
|
86
|
+
|
|
87
|
+
# Try to parse the response body as JSON and count the records
|
|
88
|
+
try:
|
|
89
|
+
response_data = json.loads(responses)
|
|
90
|
+
if isinstance(response_data, list):
|
|
91
|
+
record_count = len(response_data)
|
|
92
|
+
else:
|
|
93
|
+
record_count = 1 # If it's not a list, assume it's a single record
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
record_count = 0 # If the response is not JSON, set record count to 0
|
|
96
|
+
|
|
97
|
+
# Log response details
|
|
98
|
+
process_time = time.time() - start_time
|
|
99
|
+
logger.info(f"Request {request_id} completed in {process_time:.4f}s with status {response.status_code} and {record_count} records")
|
|
100
|
+
logger.debug(f'Request {request_id} response= {responses}')
|
|
101
|
+
|
|
102
|
+
# Return a new StreamingResponse with the original response body
|
|
103
|
+
return StreamingResponse(iter([response_body]), status_code=response.status_code, headers=dict(response.headers))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
5
|
+
from prefy import Preferences
|
|
6
|
+
|
|
7
|
+
### BE SURE TO ADD log_file_name and app_name settings. Cf. settings_example.json
|
|
8
|
+
|
|
9
|
+
class MTKLogger(logging.Logger):
|
|
10
|
+
def __init__(self, script_name=None, backup_count=7):
|
|
11
|
+
settings = Preferences()
|
|
12
|
+
|
|
13
|
+
log_file_name=settings.log_file_name
|
|
14
|
+
app_name=settings.app_name
|
|
15
|
+
|
|
16
|
+
# Set up base logger
|
|
17
|
+
super().__init__(app_name)
|
|
18
|
+
preferences_level=settings.log_level
|
|
19
|
+
level = getattr(logging, preferences_level, logging.DEBUG)
|
|
20
|
+
self.setLevel(level)
|
|
21
|
+
|
|
22
|
+
# Common log file for the day
|
|
23
|
+
log_filename = os.path.join(settings.log_dir, f'{log_file_name}.log')
|
|
24
|
+
|
|
25
|
+
# Script name for contextual logging
|
|
26
|
+
script_id = script_name or os.path.basename(sys.argv[0])
|
|
27
|
+
|
|
28
|
+
# Custom formatter including script name
|
|
29
|
+
formatter = logging.Formatter(
|
|
30
|
+
f'%(asctime)s - %(name)s - %(levelname)s - {script_id} - [Line %(lineno)d] - %(message)s'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Timed rotating file handler (daily, retain N days)
|
|
34
|
+
file_handler = TimedRotatingFileHandler(
|
|
35
|
+
filename=log_filename,
|
|
36
|
+
when='midnight',
|
|
37
|
+
interval=1,
|
|
38
|
+
backupCount=backup_count,
|
|
39
|
+
encoding='utf-8',
|
|
40
|
+
utc=False # Use True if you want UTC timestamps
|
|
41
|
+
)
|
|
42
|
+
file_handler.setLevel(logging.DEBUG)
|
|
43
|
+
file_handler.setFormatter(formatter)
|
|
44
|
+
self.addHandler(file_handler)
|
|
45
|
+
|
|
46
|
+
# Console handler
|
|
47
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
48
|
+
console_handler.setLevel(logging.DEBUG)
|
|
49
|
+
console_handler.setFormatter(formatter)
|
|
50
|
+
self.addHandler(console_handler)
|
|
51
|
+
|
|
52
|
+
self.debug("Logging initialized for script: %s, daily rotation enabled, logs in: %s", script_id, log_filename)
|
|
53
|
+
|
|
54
|
+
def get_logger(script_name=None, backup_count=7):
|
|
55
|
+
return MTKLogger(script_name, backup_count)
|