tracelite 0.1.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.
- tracelite-0.1.0/LICENSE +21 -0
- tracelite-0.1.0/PKG-INFO +98 -0
- tracelite-0.1.0/README.md +73 -0
- tracelite-0.1.0/pyproject.toml +34 -0
- tracelite-0.1.0/src/tracelite/__init__.py +0 -0
- tracelite-0.1.0/src/tracelite/cli/main.py +58 -0
- tracelite-0.1.0/src/tracelite/core/config.py +23 -0
- tracelite-0.1.0/src/tracelite/core/filters.py +10 -0
- tracelite-0.1.0/src/tracelite/core/models.py +18 -0
- tracelite-0.1.0/src/tracelite/core/storage/base.py +19 -0
- tracelite-0.1.0/src/tracelite/core/storage/sqlite.py +68 -0
- tracelite-0.1.0/src/tracelite/middleware/flask.py +40 -0
tracelite-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yeongseon Choe
|
|
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.
|
tracelite-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tracelite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight request & response tracing for your Flask, Django, or FastAPI dev server
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Yeongseon Choe
|
|
7
|
+
Author-email: yeongseon.choe@gmail.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Provides-Extra: django
|
|
17
|
+
Provides-Extra: fastapi
|
|
18
|
+
Requires-Dist: pydantic (>=2.6,<3.0)
|
|
19
|
+
Requires-Dist: rich (>=13.7,<14.0)
|
|
20
|
+
Requires-Dist: sqlalchemy (>=2.0,<3.0)
|
|
21
|
+
Requires-Dist: tomli (>=2.0,<3.0)
|
|
22
|
+
Requires-Dist: typer (>=0.15,<0.16)
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# README.md
|
|
26
|
+
# Tracelite
|
|
27
|
+
|
|
28
|
+
**Lightweight request & response tracing for your Flask, Django, or FastAPI dev server**
|
|
29
|
+
|
|
30
|
+
Tracelite logs incoming HTTP requests and outgoing responses in a structured format. It's ideal for local development and debugging.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
- 🔍 Logs method, path, status, duration, client IP, headers, body
|
|
34
|
+
- ⚙️ Configurable masking and path exclusion (via `tracelite.toml`)
|
|
35
|
+
- 📦 SQLite-based local storage
|
|
36
|
+
- 📊 Pretty CLI output using `rich`
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
```bash
|
|
40
|
+
pip install tracelite[flask]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
### CLI
|
|
45
|
+
```bash
|
|
46
|
+
tracelite view
|
|
47
|
+
tracelite export --format json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Flask Integration
|
|
51
|
+
```python
|
|
52
|
+
from flask import Flask
|
|
53
|
+
from tracelite.middleware.flask import TraceliteMiddleware
|
|
54
|
+
from tracelite.core.storage.sqlite import SQLiteStorage
|
|
55
|
+
from tracelite.core.config import load_config
|
|
56
|
+
|
|
57
|
+
app = Flask(__name__)
|
|
58
|
+
config = load_config()
|
|
59
|
+
storage = SQLiteStorage(db_path=config.db_path)
|
|
60
|
+
|
|
61
|
+
app.wsgi_app = TraceliteMiddleware(app.wsgi_app, storage, config)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration (tracelite.toml)
|
|
65
|
+
```toml
|
|
66
|
+
[storage]
|
|
67
|
+
type = "sqlite"
|
|
68
|
+
path = "tracelite.db"
|
|
69
|
+
|
|
70
|
+
[filter]
|
|
71
|
+
exclude_paths = ["/static", "/favicon.ico"]
|
|
72
|
+
mask_keys = ["password", "token"]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Testing
|
|
76
|
+
```bash
|
|
77
|
+
poetry install --with dev
|
|
78
|
+
pytest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Coverage Report
|
|
82
|
+
```bash
|
|
83
|
+
open htmlcov/index.html
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Requirementsgit add .
|
|
87
|
+
git commit -m "release: v0.1.0"
|
|
88
|
+
git push origin main
|
|
89
|
+
git tag v0.1.0
|
|
90
|
+
git push origin v0.1.0
|
|
91
|
+
|
|
92
|
+
- Python 3.9+
|
|
93
|
+
- Flask 3.x
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
MIT © Yeongseon Choe
|
|
98
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# README.md
|
|
2
|
+
# Tracelite
|
|
3
|
+
|
|
4
|
+
**Lightweight request & response tracing for your Flask, Django, or FastAPI dev server**
|
|
5
|
+
|
|
6
|
+
Tracelite logs incoming HTTP requests and outgoing responses in a structured format. It's ideal for local development and debugging.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
- 🔍 Logs method, path, status, duration, client IP, headers, body
|
|
10
|
+
- ⚙️ Configurable masking and path exclusion (via `tracelite.toml`)
|
|
11
|
+
- 📦 SQLite-based local storage
|
|
12
|
+
- 📊 Pretty CLI output using `rich`
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
```bash
|
|
16
|
+
pip install tracelite[flask]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
### CLI
|
|
21
|
+
```bash
|
|
22
|
+
tracelite view
|
|
23
|
+
tracelite export --format json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Flask Integration
|
|
27
|
+
```python
|
|
28
|
+
from flask import Flask
|
|
29
|
+
from tracelite.middleware.flask import TraceliteMiddleware
|
|
30
|
+
from tracelite.core.storage.sqlite import SQLiteStorage
|
|
31
|
+
from tracelite.core.config import load_config
|
|
32
|
+
|
|
33
|
+
app = Flask(__name__)
|
|
34
|
+
config = load_config()
|
|
35
|
+
storage = SQLiteStorage(db_path=config.db_path)
|
|
36
|
+
|
|
37
|
+
app.wsgi_app = TraceliteMiddleware(app.wsgi_app, storage, config)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration (tracelite.toml)
|
|
41
|
+
```toml
|
|
42
|
+
[storage]
|
|
43
|
+
type = "sqlite"
|
|
44
|
+
path = "tracelite.db"
|
|
45
|
+
|
|
46
|
+
[filter]
|
|
47
|
+
exclude_paths = ["/static", "/favicon.ico"]
|
|
48
|
+
mask_keys = ["password", "token"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
```bash
|
|
53
|
+
poetry install --with dev
|
|
54
|
+
pytest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Coverage Report
|
|
58
|
+
```bash
|
|
59
|
+
open htmlcov/index.html
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Requirementsgit add .
|
|
63
|
+
git commit -m "release: v0.1.0"
|
|
64
|
+
git push origin main
|
|
65
|
+
git tag v0.1.0
|
|
66
|
+
git push origin v0.1.0
|
|
67
|
+
|
|
68
|
+
- Python 3.9+
|
|
69
|
+
- Flask 3.x
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
MIT © Yeongseon Choe
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "tracelite"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight request & response tracing for your Flask, Django, or FastAPI dev server"
|
|
5
|
+
authors = ["Yeongseon Choe <yeongseon.choe@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "tracelite", from = "src" }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = ">=3.9,<4.0"
|
|
14
|
+
pydantic = "^2.6"
|
|
15
|
+
typer = "^0.15"
|
|
16
|
+
rich = "^13.7"
|
|
17
|
+
sqlalchemy = "^2.0"
|
|
18
|
+
tomli = "^2.0"
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
pytest = "^8.3.5"
|
|
22
|
+
pytest-cov = "^5.0.0"
|
|
23
|
+
flask = "^3.0"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.extras]
|
|
26
|
+
django = ["django"]
|
|
27
|
+
fastapi = ["fastapi", "starlette"]
|
|
28
|
+
|
|
29
|
+
[tool.poetry.scripts]
|
|
30
|
+
tracelite = "tracelite.cli.main:app"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["poetry-core"]
|
|
34
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import logging
|
|
3
|
+
from tracelite.core.config import load_config
|
|
4
|
+
from tracelite.core.storage.sqlite import SQLiteStorage
|
|
5
|
+
|
|
6
|
+
app = typer.Typer()
|
|
7
|
+
|
|
8
|
+
# Load configuration from file
|
|
9
|
+
config = load_config()
|
|
10
|
+
|
|
11
|
+
# Initialize storage based on config
|
|
12
|
+
db_path = config.db_path
|
|
13
|
+
storage = SQLiteStorage(db_path=db_path)
|
|
14
|
+
|
|
15
|
+
# Set up basic logging
|
|
16
|
+
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def view(since: int = 3600):
|
|
20
|
+
"""View logs from the last N seconds."""
|
|
21
|
+
logs = storage.fetch_recent(since_seconds=since)
|
|
22
|
+
if not logs:
|
|
23
|
+
logging.info("No logs found.")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
import rich
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.console import Console
|
|
29
|
+
|
|
30
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
31
|
+
table.add_column("Timestamp")
|
|
32
|
+
table.add_column("Method")
|
|
33
|
+
table.add_column("Path")
|
|
34
|
+
table.add_column("Status")
|
|
35
|
+
table.add_column("Duration (ms)", justify="right")
|
|
36
|
+
|
|
37
|
+
for row in logs:
|
|
38
|
+
timestamp, method, path, status_code, *_ , duration_ms = row
|
|
39
|
+
table.add_row(timestamp, method, path, str(status_code), f"{duration_ms:.2f}")
|
|
40
|
+
|
|
41
|
+
console = Console()
|
|
42
|
+
console.print(table)
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def export(format: str = "json"):
|
|
46
|
+
"""Export logs to given format."""
|
|
47
|
+
try:
|
|
48
|
+
output = storage.export(format)
|
|
49
|
+
from rich import print_json
|
|
50
|
+
if format == "json":
|
|
51
|
+
print_json(output)
|
|
52
|
+
else:
|
|
53
|
+
print(output)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logging.error(f"Export failed: {e}")
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
app()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import tomli
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
class AppConfig:
|
|
6
|
+
def __init__(self, db_path: str, exclude_paths: List[str], mask_keys: List[str]):
|
|
7
|
+
self.db_path = db_path
|
|
8
|
+
self.exclude_paths = exclude_paths
|
|
9
|
+
self.mask_keys = mask_keys
|
|
10
|
+
|
|
11
|
+
def load_config(file_path: str = "tracelite.toml") -> AppConfig:
|
|
12
|
+
config_path = Path(file_path)
|
|
13
|
+
if not config_path.exists():
|
|
14
|
+
raise FileNotFoundError(f"Configuration file not found: {file_path}")
|
|
15
|
+
|
|
16
|
+
with config_path.open("rb") as f:
|
|
17
|
+
data = tomli.load(f)
|
|
18
|
+
|
|
19
|
+
db_path = data.get("storage", {}).get("path", "tracelite.db")
|
|
20
|
+
exclude_paths = data.get("filter", {}).get("exclude_paths", [])
|
|
21
|
+
mask_keys = data.get("filter", {}).get("mask_keys", [])
|
|
22
|
+
|
|
23
|
+
return AppConfig(db_path=db_path, exclude_paths=exclude_paths, mask_keys=mask_keys)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
def should_exclude(path: str, exclude_paths: list) -> bool:
|
|
2
|
+
"""Return True if the path should be excluded from logging."""
|
|
3
|
+
return any(path.startswith(p) for p in exclude_paths)
|
|
4
|
+
|
|
5
|
+
def mask_sensitive(data: dict, keys_to_mask: list, mask: str = "***") -> dict:
|
|
6
|
+
"""Return a copy of the data with sensitive keys masked."""
|
|
7
|
+
return {
|
|
8
|
+
k: (mask if k.lower() in [m.lower() for m in keys_to_mask] else v)
|
|
9
|
+
for k, v in data.items()
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# models.py
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class RequestLog:
|
|
8
|
+
timestamp: datetime
|
|
9
|
+
method: str
|
|
10
|
+
path: str
|
|
11
|
+
status_code: int
|
|
12
|
+
client_ip: str
|
|
13
|
+
user_agent: Optional[str] = None
|
|
14
|
+
request_headers: Dict[str, str] = field(default_factory=dict)
|
|
15
|
+
request_body: Optional[str] = None
|
|
16
|
+
response_headers: Dict[str, str] = field(default_factory=dict)
|
|
17
|
+
response_body: Optional[str] = None
|
|
18
|
+
duration_ms: Optional[float] = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# base.py
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from tracelite.core.models import RequestLog
|
|
4
|
+
|
|
5
|
+
class ILoggerStorage(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def store(self, log: RequestLog) -> None:
|
|
8
|
+
"""Store a single request log entry."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def fetch_recent(self, since_seconds: int = 3600) -> list:
|
|
13
|
+
"""Fetch logs from the last N seconds."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def export(self, format: str = "json") -> str:
|
|
18
|
+
"""Export all logs to JSON, CSV, etc."""
|
|
19
|
+
pass
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from tracelite.core.models import RequestLog
|
|
3
|
+
from tracelite.core.storage.base import ILoggerStorage
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
class SQLiteStorage(ILoggerStorage):
|
|
8
|
+
def __init__(self, db_path: str = "tracelite.db"):
|
|
9
|
+
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
10
|
+
self._create_table()
|
|
11
|
+
|
|
12
|
+
def _create_table(self):
|
|
13
|
+
with self.conn:
|
|
14
|
+
self.conn.execute("""
|
|
15
|
+
CREATE TABLE IF NOT EXISTS request_logs (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
timestamp TEXT,
|
|
18
|
+
method TEXT,
|
|
19
|
+
path TEXT,
|
|
20
|
+
status_code INTEGER,
|
|
21
|
+
client_ip TEXT,
|
|
22
|
+
user_agent TEXT,
|
|
23
|
+
request_headers TEXT,
|
|
24
|
+
request_body TEXT,
|
|
25
|
+
response_headers TEXT,
|
|
26
|
+
response_body TEXT,
|
|
27
|
+
duration_ms REAL
|
|
28
|
+
)
|
|
29
|
+
""")
|
|
30
|
+
|
|
31
|
+
def store(self, log: RequestLog) -> None:
|
|
32
|
+
with self.conn:
|
|
33
|
+
self.conn.execute("""
|
|
34
|
+
INSERT INTO request_logs (
|
|
35
|
+
timestamp, method, path, status_code, client_ip, user_agent,
|
|
36
|
+
request_headers, request_body, response_headers, response_body, duration_ms
|
|
37
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
38
|
+
""", (
|
|
39
|
+
log.timestamp.isoformat(),
|
|
40
|
+
log.method,
|
|
41
|
+
log.path,
|
|
42
|
+
log.status_code,
|
|
43
|
+
log.client_ip,
|
|
44
|
+
log.user_agent,
|
|
45
|
+
json.dumps(log.request_headers),
|
|
46
|
+
log.request_body,
|
|
47
|
+
json.dumps(log.response_headers),
|
|
48
|
+
log.response_body,
|
|
49
|
+
log.duration_ms,
|
|
50
|
+
))
|
|
51
|
+
|
|
52
|
+
def fetch_recent(self, since_seconds: int = 3600) -> list:
|
|
53
|
+
cutoff = datetime.utcnow().timestamp() - since_seconds
|
|
54
|
+
with self.conn:
|
|
55
|
+
rows = self.conn.execute("""
|
|
56
|
+
SELECT * FROM request_logs WHERE strftime('%s', timestamp) >= ?
|
|
57
|
+
ORDER BY timestamp DESC
|
|
58
|
+
""", (int(cutoff),)).fetchall()
|
|
59
|
+
return rows
|
|
60
|
+
|
|
61
|
+
def export(self, format: str = "json") -> str:
|
|
62
|
+
with self.conn:
|
|
63
|
+
rows = self.conn.execute("SELECT * FROM request_logs").fetchall()
|
|
64
|
+
if format == "json":
|
|
65
|
+
columns = [col[0] for col in self.conn.execute("PRAGMA table_info(request_logs)")]
|
|
66
|
+
return json.dumps([dict(zip(columns, row)) for row in rows], indent=2)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError("Unsupported export format")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from flask import request
|
|
2
|
+
from tracelite.core.models import RequestLog
|
|
3
|
+
from tracelite.core.filters import should_exclude, mask_sensitive
|
|
4
|
+
from tracelite.core.config import AppConfig
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
class TraceliteMiddleware:
|
|
9
|
+
def __init__(self, app, storage, config: AppConfig):
|
|
10
|
+
self.app = app
|
|
11
|
+
self.storage = storage
|
|
12
|
+
self.config = config
|
|
13
|
+
|
|
14
|
+
def __call__(self, environ, start_response):
|
|
15
|
+
start_time = time.time()
|
|
16
|
+
req = request
|
|
17
|
+
|
|
18
|
+
if should_exclude(req.path, self.config.exclude_paths):
|
|
19
|
+
return self.app(environ, start_response)
|
|
20
|
+
|
|
21
|
+
def custom_start_response(status, headers, exc_info=None):
|
|
22
|
+
response_status = int(status.split(" ")[0])
|
|
23
|
+
duration = (time.time() - start_time) * 1000
|
|
24
|
+
log = RequestLog(
|
|
25
|
+
timestamp=datetime.utcnow(),
|
|
26
|
+
method=req.method,
|
|
27
|
+
path=req.path,
|
|
28
|
+
status_code=response_status,
|
|
29
|
+
client_ip=req.remote_addr,
|
|
30
|
+
user_agent=req.headers.get("User-Agent"),
|
|
31
|
+
request_headers=mask_sensitive(dict(req.headers), self.config.mask_keys),
|
|
32
|
+
request_body=req.get_data(as_text=True),
|
|
33
|
+
response_headers=dict(headers),
|
|
34
|
+
response_body=None, # capturing response body is omitted due to complexity
|
|
35
|
+
duration_ms=duration,
|
|
36
|
+
)
|
|
37
|
+
self.storage.store(log)
|
|
38
|
+
return start_response(status, headers, exc_info)
|
|
39
|
+
|
|
40
|
+
return self.app(environ, custom_start_response)
|