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.
@@ -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.
@@ -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)