road24-artifacthub 0.1.1__tar.gz → 0.1.3__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.
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/PKG-INFO +1 -11
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/README.md +0 -10
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/pyproject.toml +1 -1
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/fastapi.py +23 -4
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/httpx.py +37 -3
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_fastapi.py +23 -3
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.claude/CLAUDE.md +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.claude/agents/engineer.md +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.claude/agents/tester.md +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.gitignore +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.hooks/check-commit-msg.sh +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/.pre-commit-config.yaml +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/Makefile +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_logs_db.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_logs_exceptions.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_logs_http_input.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_logs_http_output.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_logs_redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/commands/test_metrics_http.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/database/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/database/config.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/database/mixins.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/http.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/road24.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/settings.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/core/utils.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/src/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/examples/src/main.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/main.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/_formatter.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/_sanitizer.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/_schemas.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/_types.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/_base.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/integrations/sqlalchemy.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/metrics/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/metrics/db.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/metrics/http.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/road24_sdk/metrics/redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/conftest.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_data_classes.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_formatter.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_init.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_base.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_httpx.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_sqlalchemy.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_metrics/__init__.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_metrics/test_db.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_metrics/test_http.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_metrics/test_redis.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_sanitizer.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_schemas.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_types.py +0 -0
- {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: road24-artifacthub
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Shared logging and metrics library for Road24 FastAPI microservices
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: build>=1.4.0
|
|
@@ -88,16 +88,6 @@ FastApiLoggingIntegration().setup(app)
|
|
|
88
88
|
| `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
|
|
89
89
|
| `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
|
|
90
90
|
|
|
91
|
-
Pass integrations to `init()` for automatic class-level patching. All new client instances are auto-instrumented.
|
|
92
|
-
|
|
93
|
-
The `.setup(client)` method is still available for per-instance patching (backwards compatible):
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
# Per-instance patching (still works)
|
|
97
|
-
client = AsyncClient(timeout=Timeout(25.0))
|
|
98
|
-
HttpxLoggingIntegration().setup(client)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
91
|
## Manual Testing
|
|
102
92
|
|
|
103
93
|
Standalone scripts to inspect structured JSON logs and metrics (no server needed):
|
|
@@ -52,16 +52,6 @@ FastApiLoggingIntegration().setup(app)
|
|
|
52
52
|
| `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
|
|
53
53
|
| `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
|
|
54
54
|
|
|
55
|
-
Pass integrations to `init()` for automatic class-level patching. All new client instances are auto-instrumented.
|
|
56
|
-
|
|
57
|
-
The `.setup(client)` method is still available for per-instance patching (backwards compatible):
|
|
58
|
-
|
|
59
|
-
```python
|
|
60
|
-
# Per-instance patching (still works)
|
|
61
|
-
client = AsyncClient(timeout=Timeout(25.0))
|
|
62
|
-
HttpxLoggingIntegration().setup(client)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
55
|
## Manual Testing
|
|
66
56
|
|
|
67
57
|
Standalone scripts to inspect structured JSON logs and metrics (no server needed):
|
|
@@ -21,6 +21,17 @@ _request_exception: ContextVar[Exception | None] = ContextVar("request_exception
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class HttpInputLogger:
|
|
24
|
+
_BINARY_CONTENT_TYPES = (
|
|
25
|
+
"multipart/form-data",
|
|
26
|
+
"application/octet-stream",
|
|
27
|
+
"image/",
|
|
28
|
+
"audio/",
|
|
29
|
+
"video/",
|
|
30
|
+
"application/pdf",
|
|
31
|
+
"application/zip",
|
|
32
|
+
"application/gzip",
|
|
33
|
+
)
|
|
34
|
+
|
|
24
35
|
def log(
|
|
25
36
|
self,
|
|
26
37
|
scope: "Scope",
|
|
@@ -37,6 +48,7 @@ class HttpInputLogger:
|
|
|
37
48
|
headers = dict(scope.get("headers", []))
|
|
38
49
|
host = headers.get(b"host", b"").decode() or ""
|
|
39
50
|
url = f"{scheme}://{host}{path}" if host else path
|
|
51
|
+
content_type = headers.get(b"content-type", b"").decode() or ""
|
|
40
52
|
|
|
41
53
|
if record_metrics:
|
|
42
54
|
from road24_sdk.metrics.http import record_http_request
|
|
@@ -56,8 +68,8 @@ class HttpInputLogger:
|
|
|
56
68
|
target=path,
|
|
57
69
|
status_code=status_code,
|
|
58
70
|
duration_seconds=duration_seconds,
|
|
59
|
-
request_body=self._decode_body(request_body),
|
|
60
|
-
response_body=self._decode_body(response_body),
|
|
71
|
+
request_body=self._decode_body(request_body, content_type),
|
|
72
|
+
response_body=self._decode_body(response_body, None),
|
|
61
73
|
error_class=type(exc).__name__ if exc else "",
|
|
62
74
|
error_message=str(exc) if exc else "",
|
|
63
75
|
)
|
|
@@ -71,14 +83,21 @@ class HttpInputLogger:
|
|
|
71
83
|
headers = dict(scope.get("headers", []))
|
|
72
84
|
return headers.get(b"x-trace-id", b"").decode() or secrets.token_hex(16)
|
|
73
85
|
|
|
74
|
-
def _decode_body(self, body: bytes) -> str:
|
|
86
|
+
def _decode_body(self, body: bytes, content_type: str | None) -> str:
|
|
75
87
|
if not body:
|
|
76
88
|
return ""
|
|
89
|
+
if self._is_binary_content(content_type):
|
|
90
|
+
return f"<binary data: {len(body)} bytes>"
|
|
77
91
|
try:
|
|
78
92
|
decoded = body.decode("utf-8")
|
|
79
93
|
return sanitize_body(decoded)
|
|
80
94
|
except UnicodeDecodeError:
|
|
81
|
-
return "<binary data>"
|
|
95
|
+
return f"<binary data: {len(body)} bytes>"
|
|
96
|
+
|
|
97
|
+
def _is_binary_content(self, content_type: str | None) -> bool:
|
|
98
|
+
if not content_type or not isinstance(content_type, str):
|
|
99
|
+
return False
|
|
100
|
+
return any(bt in content_type.lower() for bt in self._BINARY_CONTENT_TYPES)
|
|
82
101
|
|
|
83
102
|
def _get_log_level(self, status_code: int) -> str:
|
|
84
103
|
if status_code < HTTPStatus.BAD_REQUEST:
|
|
@@ -10,7 +10,7 @@ from road24_sdk._types import HttpDirection, LogType
|
|
|
10
10
|
from road24_sdk.integrations._base import Integration
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from httpx import AsyncClient, Request, Response
|
|
13
|
+
from httpx import AsyncClient, Request, RequestNotRead, Response
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -46,8 +46,8 @@ class HttpOutputLogger:
|
|
|
46
46
|
direction=HttpDirection.OUTPUT,
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
-
request_body =
|
|
50
|
-
response_body = response.
|
|
49
|
+
request_body = self._get_request_body(request)
|
|
50
|
+
response_body = self._decode_body(response.content, response.headers.get("content-type"))
|
|
51
51
|
|
|
52
52
|
http_attrs = HttpAttributes(
|
|
53
53
|
direction=HttpDirection.OUTPUT,
|
|
@@ -63,6 +63,40 @@ class HttpOutputLogger:
|
|
|
63
63
|
log_level = self._get_log_level(response.status_code)
|
|
64
64
|
getattr(logger, log_level)(LogType.HTTP_REQUEST, extra=http_attrs.as_dict())
|
|
65
65
|
|
|
66
|
+
def _get_request_body(self, request: "Request") -> str:
|
|
67
|
+
from httpx import RequestNotRead
|
|
68
|
+
|
|
69
|
+
content_type = request.headers.get("content-type")
|
|
70
|
+
try:
|
|
71
|
+
return self._decode_body(request.content, content_type)
|
|
72
|
+
except RequestNotRead:
|
|
73
|
+
return "<streaming request>"
|
|
74
|
+
|
|
75
|
+
def _decode_body(self, content: bytes, content_type: str | None) -> str:
|
|
76
|
+
if not content:
|
|
77
|
+
return ""
|
|
78
|
+
if self._is_binary_content(content_type):
|
|
79
|
+
return f"<binary data: {len(content)} bytes>"
|
|
80
|
+
try:
|
|
81
|
+
return content.decode("utf-8")
|
|
82
|
+
except UnicodeDecodeError:
|
|
83
|
+
return f"<binary data: {len(content)} bytes>"
|
|
84
|
+
|
|
85
|
+
def _is_binary_content(self, content_type: str | None) -> bool:
|
|
86
|
+
if not content_type or not isinstance(content_type, str):
|
|
87
|
+
return False
|
|
88
|
+
binary_types = (
|
|
89
|
+
"multipart/form-data",
|
|
90
|
+
"application/octet-stream",
|
|
91
|
+
"image/",
|
|
92
|
+
"audio/",
|
|
93
|
+
"video/",
|
|
94
|
+
"application/pdf",
|
|
95
|
+
"application/zip",
|
|
96
|
+
"application/gzip",
|
|
97
|
+
)
|
|
98
|
+
return any(bt in content_type.lower() for bt in binary_types)
|
|
99
|
+
|
|
66
100
|
def _get_log_level(self, status_code: int) -> str:
|
|
67
101
|
if status_code < HTTPStatus.BAD_REQUEST:
|
|
68
102
|
return "info"
|
{road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_fastapi.py
RENAMED
|
@@ -282,7 +282,7 @@ class TestHttpInputLogger:
|
|
|
282
282
|
input_logger = HttpInputLogger()
|
|
283
283
|
|
|
284
284
|
# Act
|
|
285
|
-
result = input_logger._decode_body(b"")
|
|
285
|
+
result = input_logger._decode_body(b"", None)
|
|
286
286
|
|
|
287
287
|
# Assert
|
|
288
288
|
assert result == "", "Empty bytes should return empty string"
|
|
@@ -292,10 +292,30 @@ class TestHttpInputLogger:
|
|
|
292
292
|
input_logger = HttpInputLogger()
|
|
293
293
|
|
|
294
294
|
# Act
|
|
295
|
-
result = input_logger._decode_body(b"\x80\x81\x82")
|
|
295
|
+
result = input_logger._decode_body(b"\x80\x81\x82", None)
|
|
296
296
|
|
|
297
297
|
# Assert
|
|
298
|
-
assert
|
|
298
|
+
assert "<binary data:" in result, "Non-UTF8 data should return binary data marker"
|
|
299
|
+
|
|
300
|
+
async def test_decode_body_multipart_form_data(self) -> None:
|
|
301
|
+
# Arrange
|
|
302
|
+
input_logger = HttpInputLogger()
|
|
303
|
+
|
|
304
|
+
# Act
|
|
305
|
+
result = input_logger._decode_body(b"file content", "multipart/form-data; boundary=xxx")
|
|
306
|
+
|
|
307
|
+
# Assert
|
|
308
|
+
assert "<binary data:" in result, "Multipart form data should return binary marker"
|
|
309
|
+
|
|
310
|
+
async def test_decode_body_image_content(self) -> None:
|
|
311
|
+
# Arrange
|
|
312
|
+
input_logger = HttpInputLogger()
|
|
313
|
+
|
|
314
|
+
# Act
|
|
315
|
+
result = input_logger._decode_body(b"\x89PNG\r\n", "image/png")
|
|
316
|
+
|
|
317
|
+
# Assert
|
|
318
|
+
assert "<binary data:" in result, "Image content should return binary marker"
|
|
299
319
|
|
|
300
320
|
async def test_generate_trace_id_from_header(
|
|
301
321
|
self, mock_scope: dict[str, Any]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{road24_artifacthub-0.1.1 → road24_artifacthub-0.1.3}/tests/test_integrations/test_sqlalchemy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|