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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: road24-artifacthub
3
- Version: 0.1.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=7DN5jwR4SWMNKSDWu4yYecCP8w0pgktKqeuLMliBHB8,8677
9
- road24_sdk/integrations/httpx.py,sha256=TZySabu04UnUk0Q7LUlszQzTqpj_lZOvqPHN4QOdCT8,4223
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.1.dist-info/METADATA,sha256=yrSPWG9udBlgOSzQv4EF9nkkOy8M7oGb5dhBcNNS4Pw,4072
17
- road24_artifacthub-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
- road24_artifacthub-0.1.1.dist-info/RECORD,,
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:
@@ -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 = request.content.decode("utf-8") if request.content else ""
50
- response_body = response.text if response.text else ""
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"