road24-artifacthub 0.1.1__tar.gz → 0.1.2__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.
Files changed (64) hide show
  1. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/PKG-INFO +1 -11
  2. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/README.md +0 -10
  3. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/pyproject.toml +1 -1
  4. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/fastapi.py +23 -4
  5. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/httpx.py +27 -2
  6. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/test_fastapi.py +23 -3
  7. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.claude/CLAUDE.md +0 -0
  8. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.claude/agents/engineer.md +0 -0
  9. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.claude/agents/tester.md +0 -0
  10. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.gitignore +0 -0
  11. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.hooks/check-commit-msg.sh +0 -0
  12. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/.pre-commit-config.yaml +0 -0
  13. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/Makefile +0 -0
  14. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/__init__.py +0 -0
  15. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_logs_db.py +0 -0
  16. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_logs_exceptions.py +0 -0
  17. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_logs_http_input.py +0 -0
  18. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_logs_http_output.py +0 -0
  19. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_logs_redis.py +0 -0
  20. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/commands/test_metrics_http.py +0 -0
  21. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/__init__.py +0 -0
  22. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/__init__.py +0 -0
  23. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/database/__init__.py +0 -0
  24. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/database/config.py +0 -0
  25. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/database/mixins.py +0 -0
  26. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/http.py +0 -0
  27. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/redis.py +0 -0
  28. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/road24.py +0 -0
  29. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/settings.py +0 -0
  30. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/core/utils.py +0 -0
  31. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/src/__init__.py +0 -0
  32. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/examples/src/main.py +0 -0
  33. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/main.py +0 -0
  34. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/__init__.py +0 -0
  35. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/_formatter.py +0 -0
  36. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/_sanitizer.py +0 -0
  37. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/_schemas.py +0 -0
  38. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/_types.py +0 -0
  39. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/__init__.py +0 -0
  40. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/_base.py +0 -0
  41. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/redis.py +0 -0
  42. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/integrations/sqlalchemy.py +0 -0
  43. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/metrics/__init__.py +0 -0
  44. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/metrics/db.py +0 -0
  45. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/metrics/http.py +0 -0
  46. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/road24_sdk/metrics/redis.py +0 -0
  47. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/__init__.py +0 -0
  48. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/conftest.py +0 -0
  49. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_data_classes.py +0 -0
  50. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_formatter.py +0 -0
  51. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_init.py +0 -0
  52. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/__init__.py +0 -0
  53. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/test_base.py +0 -0
  54. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/test_httpx.py +0 -0
  55. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/test_redis.py +0 -0
  56. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_integrations/test_sqlalchemy.py +0 -0
  57. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_metrics/__init__.py +0 -0
  58. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_metrics/test_db.py +0 -0
  59. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_metrics/test_http.py +0 -0
  60. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_metrics/test_redis.py +0 -0
  61. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_sanitizer.py +0 -0
  62. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_schemas.py +0 -0
  63. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/tests/test_types.py +0 -0
  64. {road24_artifacthub-0.1.1 → road24_artifacthub-0.1.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: road24-artifacthub
3
- Version: 0.1.1
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
@@ -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):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "road24-artifacthub"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Shared logging and metrics library for Road24 FastAPI microservices"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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"
@@ -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 result == "<binary data>", "Non-UTF8 data should return binary data marker"
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]