road24-artifacthub 0.1.1__py3-none-any.whl → 0.1.3__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.
- {road24_artifacthub-0.1.1.dist-info → road24_artifacthub-0.1.3.dist-info}/METADATA +1 -11
- {road24_artifacthub-0.1.1.dist-info → road24_artifacthub-0.1.3.dist-info}/RECORD +5 -5
- road24_sdk/integrations/fastapi.py +23 -4
- road24_sdk/integrations/httpx.py +37 -3
- {road24_artifacthub-0.1.1.dist-info → road24_artifacthub-0.1.3.dist-info}/WHEEL +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):
|
|
@@ -5,14 +5,14 @@ road24_sdk/_schemas.py,sha256=HOlBsIZSN2sm3s2MMbp-JoGGju_jjgZPmm8uNc4BCeI,3026
|
|
|
5
5
|
road24_sdk/_types.py,sha256=OfRdYJhIAeMOtjPPCx-W9vXXAxYvuKjq37NzCubWIg8,1071
|
|
6
6
|
road24_sdk/integrations/__init__.py,sha256=uCtjD-5z2lQYA1j4jN9pFw1azS095L7BVtoxzz-Rl3c,497
|
|
7
7
|
road24_sdk/integrations/_base.py,sha256=IePXuVsvKmDDf3Aq411KzurXkkq5opelz9nqeXeCr2o,489
|
|
8
|
-
road24_sdk/integrations/fastapi.py,sha256=
|
|
9
|
-
road24_sdk/integrations/httpx.py,sha256=
|
|
8
|
+
road24_sdk/integrations/fastapi.py,sha256=OZCb2iFULLDmID9MgyeQ6uGC0La8O8xvUPobI0CmQfA,9404
|
|
9
|
+
road24_sdk/integrations/httpx.py,sha256=ojCBoAuB8oUTnDfNG1i6Kn5lerbWAYh-OcxLHThp9OY,5443
|
|
10
10
|
road24_sdk/integrations/redis.py,sha256=KvLilhaZywGSrUJ4hL7T5BqfKGZWxSYQ7BADRVS5Fq0,7401
|
|
11
11
|
road24_sdk/integrations/sqlalchemy.py,sha256=WOhFpUt7RfknhJL253MxNif16OKK8GJlXQ87sDsN9WQ,5055
|
|
12
12
|
road24_sdk/metrics/__init__.py,sha256=vvXfTFdHYnYHkN9REgqvqiGsTkXnszVKXnqHkb5C1FQ,257
|
|
13
13
|
road24_sdk/metrics/db.py,sha256=Lqqy7k1qF8D9sdxEl4qDP5pwqVHXgo1ucDHgUYD4exs,780
|
|
14
14
|
road24_sdk/metrics/http.py,sha256=Ynbf-IvJwkzU6G2eRNWf99CH8ef-iR7CxfEvHR66OvQ,949
|
|
15
15
|
road24_sdk/metrics/redis.py,sha256=qdDuG-2px7_Hp1eiFRc4x8F0MG7KbcuiDiFxw8QsavA,805
|
|
16
|
-
road24_artifacthub-0.1.
|
|
17
|
-
road24_artifacthub-0.1.
|
|
18
|
-
road24_artifacthub-0.1.
|
|
16
|
+
road24_artifacthub-0.1.3.dist-info/METADATA,sha256=H65x4vBDOVaQfYx-ApKsbu_4hDDO472wM4pvnZbZ7XM,3722
|
|
17
|
+
road24_artifacthub-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
road24_artifacthub-0.1.3.dist-info/RECORD,,
|
|
@@ -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:
|
road24_sdk/integrations/httpx.py
CHANGED
|
@@ -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"
|
|
File without changes
|