road24-artifacthub 0.1.0__py3-none-any.whl → 0.1.2__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.0
3
+ Version: 0.1.2
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
@@ -56,28 +56,27 @@ pip install road24-artifacthub[dev]
56
56
  ```python
57
57
  import road24_sdk
58
58
  from fastapi import FastAPI
59
+ from httpx import AsyncClient, Timeout
59
60
  from road24_sdk.integrations.fastapi import FastApiLoggingIntegration
60
61
  from road24_sdk.integrations.httpx import HttpxLoggingIntegration
61
62
  from road24_sdk.integrations.redis import RedisLoggingIntegration
62
63
  from road24_sdk.integrations.sqlalchemy import SqlalchemyLoggingIntegration
63
64
 
64
- road24_sdk.init(
65
- service_name="my-service",
66
- log_level="INFO",
67
- integrations=[
68
- FastApiLoggingIntegration(),
69
- HttpxLoggingIntegration(),
70
- SqlalchemyLoggingIntegration(),
71
- RedisLoggingIntegration(),
72
- ],
73
- )
74
-
75
- # FastAPI still requires .setup(app) since it needs the app instance
65
+ def setup_road24_sdk():
66
+ road24_sdk.init(
67
+ service_name="my-service",
68
+ log_level="INFO",
69
+ integrations=[
70
+ HttpxLoggingIntegration(),
71
+ SqlalchemyLoggingIntegration(),
72
+ RedisLoggingIntegration(),
73
+ ],
74
+ )
75
+
76
+ # FastAPI requires .setup(app) since it needs the app instance
76
77
  app = FastAPI()
78
+ setup_road24_sdk()
77
79
  FastApiLoggingIntegration().setup(app)
78
-
79
- # All other clients are auto-instrumented — no .setup() needed
80
- client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
81
80
  ```
82
81
 
83
82
  ## Available Integrations
@@ -89,16 +88,6 @@ client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
89
88
  | `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
90
89
  | `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
91
90
 
92
- Pass integrations to `init()` for automatic class-level patching. All new client instances are auto-instrumented.
93
-
94
- The `.setup(client)` method is still available for per-instance patching (backwards compatible):
95
-
96
- ```python
97
- # Per-instance patching (still works)
98
- client = AsyncClient(timeout=Timeout(25.0))
99
- HttpxLoggingIntegration().setup(client)
100
- ```
101
-
102
91
  ## Manual Testing
103
92
 
104
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=G5TgHJoA9Qkwb9BUhVzANo4HWARIRvGLe_-PG3MX3Ew,5152
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.0.dist-info/METADATA,sha256=DnB13VHVn2yG1vY2WoRwYoP9QJR_SbTledydFRCBa50,4131
17
- road24_artifacthub-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
- road24_artifacthub-0.1.0.dist-info/RECORD,,
16
+ road24_artifacthub-0.1.2.dist-info/METADATA,sha256=h52ypVoTSooqJ_JiC7VMbkCBh0De_8tmtmP6Ck_yzl0,3722
17
+ road24_artifacthub-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
+ road24_artifacthub-0.1.2.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:
@@ -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._decode_body(request.content, request.headers.get("content-type"))
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,31 @@ 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 _decode_body(self, content: bytes, content_type: str | None) -> str:
67
+ if not content:
68
+ return ""
69
+ if self._is_binary_content(content_type):
70
+ return f"<binary data: {len(content)} bytes>"
71
+ try:
72
+ return content.decode("utf-8")
73
+ except UnicodeDecodeError:
74
+ return f"<binary data: {len(content)} bytes>"
75
+
76
+ def _is_binary_content(self, content_type: str | None) -> bool:
77
+ if not content_type or not isinstance(content_type, str):
78
+ return False
79
+ binary_types = (
80
+ "multipart/form-data",
81
+ "application/octet-stream",
82
+ "image/",
83
+ "audio/",
84
+ "video/",
85
+ "application/pdf",
86
+ "application/zip",
87
+ "application/gzip",
88
+ )
89
+ return any(bt in content_type.lower() for bt in binary_types)
90
+
66
91
  def _get_log_level(self, status_code: int) -> str:
67
92
  if status_code < HTTPStatus.BAD_REQUEST:
68
93
  return "info"