road24-artifacthub 0.1.0__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.0.dist-info/METADATA +122 -0
- road24_artifacthub-0.1.0.dist-info/RECORD +18 -0
- road24_artifacthub-0.1.0.dist-info/WHEEL +4 -0
- road24_sdk/__init__.py +65 -0
- road24_sdk/_formatter.py +213 -0
- road24_sdk/_sanitizer.py +61 -0
- road24_sdk/_schemas.py +117 -0
- road24_sdk/_types.py +54 -0
- road24_sdk/integrations/__init__.py +13 -0
- road24_sdk/integrations/_base.py +19 -0
- road24_sdk/integrations/fastapi.py +258 -0
- road24_sdk/integrations/httpx.py +120 -0
- road24_sdk/integrations/redis.py +233 -0
- road24_sdk/integrations/sqlalchemy.py +175 -0
- road24_sdk/metrics/__init__.py +9 -0
- road24_sdk/metrics/db.py +29 -0
- road24_sdk/metrics/http.py +33 -0
- road24_sdk/metrics/redis.py +29 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: road24-artifacthub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared logging and metrics library for Road24 FastAPI microservices
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: build>=1.4.0
|
|
7
|
+
Requires-Dist: prometheus-client>=0.20.0
|
|
8
|
+
Requires-Dist: twine>=6.2.0
|
|
9
|
+
Provides-Extra: all
|
|
10
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'all'
|
|
11
|
+
Requires-Dist: httpx>=0.27.0; extra == 'all'
|
|
12
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
13
|
+
Requires-Dist: sqlalchemy>=2.0.45; extra == 'all'
|
|
14
|
+
Requires-Dist: starlette>=0.27.0; extra == 'all'
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: faker>=25.0.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: httpx>=0.27.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: redis>=5.0.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.45; extra == 'dev'
|
|
25
|
+
Requires-Dist: starlette>=0.27.0; extra == 'dev'
|
|
26
|
+
Provides-Extra: fastapi
|
|
27
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
28
|
+
Requires-Dist: starlette>=0.27.0; extra == 'fastapi'
|
|
29
|
+
Provides-Extra: httpx
|
|
30
|
+
Requires-Dist: httpx>=0.27.0; extra == 'httpx'
|
|
31
|
+
Provides-Extra: redis
|
|
32
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
33
|
+
Provides-Extra: sqlalchemy
|
|
34
|
+
Requires-Dist: sqlalchemy>=2.0.45; extra == 'sqlalchemy'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# road24-artifacthub
|
|
38
|
+
|
|
39
|
+
Shared logging and metrics SDK for Road24 microservices. Provides structured JSON logging, Prometheus metrics, and framework-specific integrations following a Sentry SDK-like pattern.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Core library
|
|
45
|
+
pip install road24-artifacthub
|
|
46
|
+
|
|
47
|
+
# With all integrations
|
|
48
|
+
pip install road24-artifacthub[all]
|
|
49
|
+
|
|
50
|
+
# Development (includes test tools)
|
|
51
|
+
pip install road24-artifacthub[dev]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import road24_sdk
|
|
58
|
+
from fastapi import FastAPI
|
|
59
|
+
from road24_sdk.integrations.fastapi import FastApiLoggingIntegration
|
|
60
|
+
from road24_sdk.integrations.httpx import HttpxLoggingIntegration
|
|
61
|
+
from road24_sdk.integrations.redis import RedisLoggingIntegration
|
|
62
|
+
from road24_sdk.integrations.sqlalchemy import SqlalchemyLoggingIntegration
|
|
63
|
+
|
|
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
|
|
76
|
+
app = FastAPI()
|
|
77
|
+
FastApiLoggingIntegration().setup(app)
|
|
78
|
+
|
|
79
|
+
# All other clients are auto-instrumented — no .setup() needed
|
|
80
|
+
client = AsyncClient(timeout=Timeout(25.0)) # already instrumented
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Available Integrations
|
|
84
|
+
|
|
85
|
+
| Integration | Extra | Description |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `FastApiLoggingIntegration` | `fastapi` | Inbound HTTP request/response logging and metrics |
|
|
88
|
+
| `HttpxLoggingIntegration` | `httpx` | Outbound HTTP request/response logging and metrics |
|
|
89
|
+
| `SqlalchemyLoggingIntegration` | `sqlalchemy` | Database query logging and metrics |
|
|
90
|
+
| `RedisLoggingIntegration` | `redis` | Redis command logging and metrics |
|
|
91
|
+
|
|
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
|
+
## Manual Testing
|
|
103
|
+
|
|
104
|
+
Standalone scripts to inspect structured JSON logs and metrics (no server needed):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
make test-logs-http-input # Inbound HTTP request log
|
|
108
|
+
make test-logs-http-output # Outbound HTTPX request log
|
|
109
|
+
make test-metrics-http # Prometheus metrics after HTTP calls
|
|
110
|
+
make test-logs-redis # Redis command logs (SET, GET, HSET, etc.)
|
|
111
|
+
make test-logs-db # DB query logs (SELECT, INSERT, UPDATE, DELETE)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
make install # Install all dependencies (requires uv)
|
|
118
|
+
make test # Run tests with coverage
|
|
119
|
+
make lint # Lint with ruff
|
|
120
|
+
make format # Format with ruff
|
|
121
|
+
make check # Lint + test
|
|
122
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
road24_sdk/__init__.py,sha256=BAqBQRTI5hwt4sewrvnFzpyl9E_FTRiKYbgqZsj3bKg,1444
|
|
2
|
+
road24_sdk/_formatter.py,sha256=OgiVmVidAWzP3VPQuW_WxJynyU7LFBobecXV026CKrA,5495
|
|
3
|
+
road24_sdk/_sanitizer.py,sha256=RxllcSa0QZC4e500hhMDotMZau9MnmAJUr7YbcZlmr0,1957
|
|
4
|
+
road24_sdk/_schemas.py,sha256=HOlBsIZSN2sm3s2MMbp-JoGGju_jjgZPmm8uNc4BCeI,3026
|
|
5
|
+
road24_sdk/_types.py,sha256=OfRdYJhIAeMOtjPPCx-W9vXXAxYvuKjq37NzCubWIg8,1071
|
|
6
|
+
road24_sdk/integrations/__init__.py,sha256=uCtjD-5z2lQYA1j4jN9pFw1azS095L7BVtoxzz-Rl3c,497
|
|
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
|
|
10
|
+
road24_sdk/integrations/redis.py,sha256=KvLilhaZywGSrUJ4hL7T5BqfKGZWxSYQ7BADRVS5Fq0,7401
|
|
11
|
+
road24_sdk/integrations/sqlalchemy.py,sha256=WOhFpUt7RfknhJL253MxNif16OKK8GJlXQ87sDsN9WQ,5055
|
|
12
|
+
road24_sdk/metrics/__init__.py,sha256=vvXfTFdHYnYHkN9REgqvqiGsTkXnszVKXnqHkb5C1FQ,257
|
|
13
|
+
road24_sdk/metrics/db.py,sha256=Lqqy7k1qF8D9sdxEl4qDP5pwqVHXgo1ucDHgUYD4exs,780
|
|
14
|
+
road24_sdk/metrics/http.py,sha256=Ynbf-IvJwkzU6G2eRNWf99CH8ef-iR7CxfEvHR66OvQ,949
|
|
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,,
|
road24_sdk/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from road24_sdk._formatter import setup_logging
|
|
4
|
+
from road24_sdk._types import (
|
|
5
|
+
ArtifactHubConfig,
|
|
6
|
+
DbOperation,
|
|
7
|
+
ExceptionType,
|
|
8
|
+
HttpDirection,
|
|
9
|
+
LogType,
|
|
10
|
+
RedisOperation,
|
|
11
|
+
)
|
|
12
|
+
from road24_sdk.integrations._base import Integration
|
|
13
|
+
|
|
14
|
+
_config: ArtifactHubConfig | None = None
|
|
15
|
+
_logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def init(
|
|
19
|
+
service_name: str = "unknown",
|
|
20
|
+
log_level: str = "INFO",
|
|
21
|
+
debug: bool = False,
|
|
22
|
+
log_format: str = "json",
|
|
23
|
+
integrations: list[Integration] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
global _config
|
|
26
|
+
resolved = integrations or []
|
|
27
|
+
_config = ArtifactHubConfig(
|
|
28
|
+
service_name=service_name,
|
|
29
|
+
log_level=log_level,
|
|
30
|
+
debug=debug,
|
|
31
|
+
log_format=log_format,
|
|
32
|
+
integrations=resolved,
|
|
33
|
+
)
|
|
34
|
+
setup_logging(
|
|
35
|
+
level=log_level,
|
|
36
|
+
service_name=service_name,
|
|
37
|
+
debug=debug,
|
|
38
|
+
)
|
|
39
|
+
for integration in resolved:
|
|
40
|
+
try:
|
|
41
|
+
integration.setup_once()
|
|
42
|
+
except Exception:
|
|
43
|
+
_logger.warning(
|
|
44
|
+
"Failed to setup integration %s",
|
|
45
|
+
type(integration).__name__,
|
|
46
|
+
exc_info=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def configure() -> ArtifactHubConfig:
|
|
51
|
+
if _config is None:
|
|
52
|
+
return ArtifactHubConfig()
|
|
53
|
+
return _config
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"ArtifactHubConfig",
|
|
58
|
+
"DbOperation",
|
|
59
|
+
"ExceptionType",
|
|
60
|
+
"HttpDirection",
|
|
61
|
+
"LogType",
|
|
62
|
+
"RedisOperation",
|
|
63
|
+
"configure",
|
|
64
|
+
"init",
|
|
65
|
+
]
|
road24_sdk/_formatter.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from contextvars import ContextVar, Token
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from road24_sdk._schemas import ExceptionAttributes, LogRecord
|
|
9
|
+
from road24_sdk._types import ExceptionType, LogType
|
|
10
|
+
|
|
11
|
+
STANDARD_RECORD_ATTRS = {
|
|
12
|
+
"name",
|
|
13
|
+
"msg",
|
|
14
|
+
"args",
|
|
15
|
+
"created",
|
|
16
|
+
"filename",
|
|
17
|
+
"funcName",
|
|
18
|
+
"levelname",
|
|
19
|
+
"levelno",
|
|
20
|
+
"lineno",
|
|
21
|
+
"module",
|
|
22
|
+
"msecs",
|
|
23
|
+
"pathname",
|
|
24
|
+
"process",
|
|
25
|
+
"processName",
|
|
26
|
+
"relativeCreated",
|
|
27
|
+
"stack_info",
|
|
28
|
+
"exc_info",
|
|
29
|
+
"exc_text",
|
|
30
|
+
"thread",
|
|
31
|
+
"threadName",
|
|
32
|
+
"taskName",
|
|
33
|
+
"message",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ZERO_TRACE_ID = "0" * 32
|
|
37
|
+
|
|
38
|
+
_trace_id: ContextVar[str] = ContextVar("trace_id", default="")
|
|
39
|
+
|
|
40
|
+
_service_name: str = "unknown"
|
|
41
|
+
|
|
42
|
+
SILENT_LOGGERS_DEBUG = [
|
|
43
|
+
"uvicorn.access",
|
|
44
|
+
"asyncio",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
SILENT_LOGGERS_PROD = [
|
|
48
|
+
"uvicorn",
|
|
49
|
+
"uvicorn.access",
|
|
50
|
+
"uvicorn.error",
|
|
51
|
+
"uvicorn.asgi",
|
|
52
|
+
"sqlalchemy",
|
|
53
|
+
"sqlalchemy.engine",
|
|
54
|
+
"sqlalchemy.pool",
|
|
55
|
+
"httpx",
|
|
56
|
+
"httpcore",
|
|
57
|
+
"asyncio",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LogFormatter(logging.Formatter):
|
|
62
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
63
|
+
log_record = LogRecord(
|
|
64
|
+
timestamp=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
|
65
|
+
trace_id=_trace_id.get() or ZERO_TRACE_ID,
|
|
66
|
+
log_level=record.levelname,
|
|
67
|
+
type=record.getMessage(),
|
|
68
|
+
service_name=_service_name,
|
|
69
|
+
attributes=self._build_attributes(record),
|
|
70
|
+
)
|
|
71
|
+
return json.dumps(log_record.as_dict(), ensure_ascii=False, default=str)
|
|
72
|
+
|
|
73
|
+
def _build_attributes(self, record: logging.LogRecord) -> dict[str, Any]:
|
|
74
|
+
attrs: dict[str, Any] = {
|
|
75
|
+
"logger_name": record.name,
|
|
76
|
+
"code_module": record.module,
|
|
77
|
+
"code_function": record.funcName,
|
|
78
|
+
"code_lineno": record.lineno,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
attrs.update(
|
|
82
|
+
{
|
|
83
|
+
k: v
|
|
84
|
+
for k, v in record.__dict__.items()
|
|
85
|
+
if k not in STANDARD_RECORD_ATTRS and not k.startswith("_")
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if record.exc_info and (exc_type := record.exc_info[0]):
|
|
90
|
+
attrs.update(
|
|
91
|
+
{
|
|
92
|
+
"exception_type": exc_type.__name__,
|
|
93
|
+
"exception_message": str(record.exc_info[1]),
|
|
94
|
+
"exception_stacktrace": self.formatException(record.exc_info),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return attrs
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def set_trace_context(trace_id: str) -> Token[str]:
|
|
102
|
+
return _trace_id.set(trace_id)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def clear_trace_context(token: Token[str] | None = None) -> None:
|
|
106
|
+
if token is not None:
|
|
107
|
+
_trace_id.reset(token)
|
|
108
|
+
else:
|
|
109
|
+
_trace_id.set("")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_trace_id() -> str:
|
|
113
|
+
return _trace_id.get() or ZERO_TRACE_ID
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def setup_logging(
|
|
117
|
+
level: str = "INFO",
|
|
118
|
+
service_name: str = "unknown",
|
|
119
|
+
debug: bool = False,
|
|
120
|
+
) -> None:
|
|
121
|
+
global _service_name
|
|
122
|
+
_service_name = service_name
|
|
123
|
+
|
|
124
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
125
|
+
|
|
126
|
+
root_logger = logging.getLogger()
|
|
127
|
+
root_logger.setLevel(log_level)
|
|
128
|
+
root_logger.handlers.clear()
|
|
129
|
+
|
|
130
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
131
|
+
handler.setFormatter(LogFormatter())
|
|
132
|
+
handler.setLevel(log_level)
|
|
133
|
+
root_logger.addHandler(handler)
|
|
134
|
+
|
|
135
|
+
_silence_libraries(debug, level)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _silence_libraries(debug: bool, level: str) -> None:
|
|
139
|
+
is_debug = debug or level.upper() == "DEBUG"
|
|
140
|
+
|
|
141
|
+
if is_debug:
|
|
142
|
+
loggers = SILENT_LOGGERS_DEBUG
|
|
143
|
+
silence_level = logging.WARNING
|
|
144
|
+
else:
|
|
145
|
+
loggers = SILENT_LOGGERS_PROD
|
|
146
|
+
silence_level = logging.CRITICAL
|
|
147
|
+
|
|
148
|
+
for name in loggers:
|
|
149
|
+
lib_logger = logging.getLogger(name)
|
|
150
|
+
lib_logger.setLevel(silence_level)
|
|
151
|
+
lib_logger.propagate = False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ExceptionLogger:
|
|
155
|
+
def log_http_error(
|
|
156
|
+
self,
|
|
157
|
+
method: str,
|
|
158
|
+
url: str,
|
|
159
|
+
path: str,
|
|
160
|
+
status_code: int,
|
|
161
|
+
detail: str,
|
|
162
|
+
exc: Exception,
|
|
163
|
+
) -> None:
|
|
164
|
+
attrs = ExceptionAttributes(
|
|
165
|
+
exception_type=ExceptionType.HTTP,
|
|
166
|
+
error_class=type(exc).__name__,
|
|
167
|
+
error_message=detail,
|
|
168
|
+
status_code=status_code,
|
|
169
|
+
method=method,
|
|
170
|
+
url=url,
|
|
171
|
+
path=path,
|
|
172
|
+
)
|
|
173
|
+
logger = logging.getLogger(__name__)
|
|
174
|
+
logger.error(LogType.EXCEPTION, exc_info=exc, extra=attrs.as_dict())
|
|
175
|
+
|
|
176
|
+
def log_validation_error(
|
|
177
|
+
self,
|
|
178
|
+
method: str,
|
|
179
|
+
url: str,
|
|
180
|
+
path: str,
|
|
181
|
+
errors: list[Any],
|
|
182
|
+
) -> None:
|
|
183
|
+
attrs = ExceptionAttributes(
|
|
184
|
+
exception_type=ExceptionType.VALIDATION,
|
|
185
|
+
error_class="RequestValidationError",
|
|
186
|
+
error_message=f"Validation error: {len(errors)} error(s)",
|
|
187
|
+
status_code=422,
|
|
188
|
+
method=method,
|
|
189
|
+
url=url,
|
|
190
|
+
path=path,
|
|
191
|
+
details=errors,
|
|
192
|
+
)
|
|
193
|
+
logger = logging.getLogger(__name__)
|
|
194
|
+
logger.warning(LogType.EXCEPTION, extra=attrs.as_dict())
|
|
195
|
+
|
|
196
|
+
def log_general_error(
|
|
197
|
+
self,
|
|
198
|
+
method: str,
|
|
199
|
+
url: str,
|
|
200
|
+
path: str,
|
|
201
|
+
exc: Exception,
|
|
202
|
+
) -> None:
|
|
203
|
+
attrs = ExceptionAttributes(
|
|
204
|
+
exception_type=ExceptionType.GENERAL,
|
|
205
|
+
error_class=type(exc).__name__,
|
|
206
|
+
error_message=str(exc),
|
|
207
|
+
status_code=500,
|
|
208
|
+
method=method,
|
|
209
|
+
url=url,
|
|
210
|
+
path=path,
|
|
211
|
+
)
|
|
212
|
+
logger = logging.getLogger(__name__)
|
|
213
|
+
logger.error(LogType.EXCEPTION, exc_info=exc, extra=attrs.as_dict())
|
road24_sdk/_sanitizer.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
SENSITIVE_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
6
|
+
re.compile(r"password", re.IGNORECASE),
|
|
7
|
+
re.compile(r"token", re.IGNORECASE),
|
|
8
|
+
re.compile(r"secret", re.IGNORECASE),
|
|
9
|
+
re.compile(r"api[_-]?key", re.IGNORECASE),
|
|
10
|
+
re.compile(r"auth", re.IGNORECASE),
|
|
11
|
+
re.compile(r"credential", re.IGNORECASE),
|
|
12
|
+
re.compile(r"private[_-]?key", re.IGNORECASE),
|
|
13
|
+
re.compile(r"access[_-]?key", re.IGNORECASE),
|
|
14
|
+
re.compile(r"session[_-]?id", re.IGNORECASE),
|
|
15
|
+
re.compile(r"cookie", re.IGNORECASE),
|
|
16
|
+
re.compile(r"bearer", re.IGNORECASE),
|
|
17
|
+
re.compile(r"pin", re.IGNORECASE),
|
|
18
|
+
re.compile(r"cvv", re.IGNORECASE),
|
|
19
|
+
re.compile(r"card[_-]?number", re.IGNORECASE),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
MASK = "***REDACTED***"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_sensitive_key(key: str) -> bool:
|
|
26
|
+
return any(pattern.search(key) for pattern in SENSITIVE_PATTERNS)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sanitize_dict(data: dict[str, Any]) -> dict[str, Any]:
|
|
30
|
+
result = {}
|
|
31
|
+
for key, value in data.items():
|
|
32
|
+
if is_sensitive_key(key):
|
|
33
|
+
result[key] = MASK
|
|
34
|
+
elif isinstance(value, dict):
|
|
35
|
+
result[key] = sanitize_dict(value)
|
|
36
|
+
elif isinstance(value, list):
|
|
37
|
+
result[key] = [
|
|
38
|
+
sanitize_dict(item) if isinstance(item, dict) else item for item in value
|
|
39
|
+
]
|
|
40
|
+
else:
|
|
41
|
+
result[key] = value
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sanitize_body(body: str) -> str:
|
|
46
|
+
if not body or not body.strip():
|
|
47
|
+
return body
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(body)
|
|
51
|
+
if isinstance(data, dict):
|
|
52
|
+
sanitized = sanitize_dict(data)
|
|
53
|
+
return json.dumps(sanitized, ensure_ascii=False)
|
|
54
|
+
if isinstance(data, list):
|
|
55
|
+
sanitized_list = [
|
|
56
|
+
sanitize_dict(item) if isinstance(item, dict) else item for item in data
|
|
57
|
+
]
|
|
58
|
+
return json.dumps(sanitized_list, ensure_ascii=False)
|
|
59
|
+
return body
|
|
60
|
+
except (json.JSONDecodeError, TypeError):
|
|
61
|
+
return body
|
road24_sdk/_schemas.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from road24_sdk._types import (
|
|
5
|
+
DbOperation,
|
|
6
|
+
ExceptionType,
|
|
7
|
+
HttpDirection,
|
|
8
|
+
RedisOperation,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class LogRecord:
|
|
14
|
+
timestamp: str
|
|
15
|
+
trace_id: str
|
|
16
|
+
log_level: str
|
|
17
|
+
type: str
|
|
18
|
+
service_name: str
|
|
19
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
def as_dict(self) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"timestamp": self.timestamp,
|
|
24
|
+
"trace_id": self.trace_id,
|
|
25
|
+
"log_level": self.log_level,
|
|
26
|
+
"type": self.type,
|
|
27
|
+
"service_name": self.service_name,
|
|
28
|
+
"attributes": self.attributes,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class HttpAttributes:
|
|
34
|
+
direction: HttpDirection
|
|
35
|
+
method: str
|
|
36
|
+
url: str
|
|
37
|
+
target: str
|
|
38
|
+
status_code: int
|
|
39
|
+
duration_seconds: float
|
|
40
|
+
request_body: str = ""
|
|
41
|
+
response_body: str = ""
|
|
42
|
+
error_class: str = ""
|
|
43
|
+
error_message: str = ""
|
|
44
|
+
|
|
45
|
+
def as_dict(self) -> dict[str, Any]:
|
|
46
|
+
result: dict[str, Any] = {
|
|
47
|
+
"direction": self.direction.value,
|
|
48
|
+
"method": self.method,
|
|
49
|
+
"url": self.url,
|
|
50
|
+
"target": self.target,
|
|
51
|
+
"status_code": self.status_code,
|
|
52
|
+
"duration_seconds": self.duration_seconds,
|
|
53
|
+
"request_body": self.request_body,
|
|
54
|
+
"response_body": self.response_body,
|
|
55
|
+
}
|
|
56
|
+
if self.error_class:
|
|
57
|
+
result["error_class"] = self.error_class
|
|
58
|
+
result["error_message"] = self.error_message
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class DbAttributes:
|
|
64
|
+
operation: DbOperation
|
|
65
|
+
table: str
|
|
66
|
+
duration_seconds: float
|
|
67
|
+
statement: str = ""
|
|
68
|
+
|
|
69
|
+
def as_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"operation": self.operation.value,
|
|
72
|
+
"table": self.table,
|
|
73
|
+
"duration_seconds": self.duration_seconds,
|
|
74
|
+
"statement": self.statement,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(slots=True)
|
|
79
|
+
class RedisAttributes:
|
|
80
|
+
operation: RedisOperation
|
|
81
|
+
key: str
|
|
82
|
+
duration_seconds: float
|
|
83
|
+
command_args: str = ""
|
|
84
|
+
|
|
85
|
+
def as_dict(self) -> dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"operation": self.operation.value,
|
|
88
|
+
"key": self.key,
|
|
89
|
+
"duration_seconds": self.duration_seconds,
|
|
90
|
+
"command_args": self.command_args,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(slots=True)
|
|
95
|
+
class ExceptionAttributes:
|
|
96
|
+
exception_type: ExceptionType
|
|
97
|
+
error_class: str
|
|
98
|
+
error_message: str
|
|
99
|
+
status_code: int
|
|
100
|
+
method: str
|
|
101
|
+
url: str
|
|
102
|
+
path: str
|
|
103
|
+
details: Any = None
|
|
104
|
+
|
|
105
|
+
def as_dict(self) -> dict[str, Any]:
|
|
106
|
+
result: dict[str, Any] = {
|
|
107
|
+
"exception_type": self.exception_type.value,
|
|
108
|
+
"error_class": self.error_class,
|
|
109
|
+
"error_message": self.error_message,
|
|
110
|
+
"status_code": self.status_code,
|
|
111
|
+
"method": self.method,
|
|
112
|
+
"url": self.url,
|
|
113
|
+
"path": self.path,
|
|
114
|
+
}
|
|
115
|
+
if self.details is not None:
|
|
116
|
+
result["details"] = self.details
|
|
117
|
+
return result
|
road24_sdk/_types.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class ArtifactHubConfig:
|
|
8
|
+
service_name: str = "unknown"
|
|
9
|
+
log_level: str = "INFO"
|
|
10
|
+
debug: bool = False
|
|
11
|
+
log_format: str = "json"
|
|
12
|
+
integrations: list[Any] = field(default_factory=list)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LogType(StrEnum):
|
|
16
|
+
HTTP_REQUEST = "http_request"
|
|
17
|
+
DB_QUERY = "db_query"
|
|
18
|
+
REDIS_COMMAND = "redis_command"
|
|
19
|
+
EXCEPTION = "exception"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExceptionType(StrEnum):
|
|
23
|
+
HTTP = "http"
|
|
24
|
+
VALIDATION = "validation"
|
|
25
|
+
GENERAL = "general"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HttpDirection(StrEnum):
|
|
29
|
+
INPUT = "input"
|
|
30
|
+
OUTPUT = "output"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DbOperation(StrEnum):
|
|
34
|
+
SELECT = "select"
|
|
35
|
+
INSERT = "insert"
|
|
36
|
+
UPDATE = "update"
|
|
37
|
+
DELETE = "delete"
|
|
38
|
+
OTHER = "other"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RedisOperation(StrEnum):
|
|
42
|
+
GET = "get"
|
|
43
|
+
SET = "set"
|
|
44
|
+
DELETE = "delete"
|
|
45
|
+
EXPIRE = "expire"
|
|
46
|
+
HGET = "hget"
|
|
47
|
+
HSET = "hset"
|
|
48
|
+
LPUSH = "lpush"
|
|
49
|
+
RPUSH = "rpush"
|
|
50
|
+
LPOP = "lpop"
|
|
51
|
+
RPOP = "rpop"
|
|
52
|
+
PUBLISH = "publish"
|
|
53
|
+
SUBSCRIBE = "subscribe"
|
|
54
|
+
OTHER = "other"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from road24_sdk.integrations._base import Integration
|
|
2
|
+
from road24_sdk.integrations.fastapi import FastApiLoggingIntegration
|
|
3
|
+
from road24_sdk.integrations.httpx import HttpxLoggingIntegration
|
|
4
|
+
from road24_sdk.integrations.redis import RedisLoggingIntegration
|
|
5
|
+
from road24_sdk.integrations.sqlalchemy import SqlalchemyLoggingIntegration
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"FastApiLoggingIntegration",
|
|
9
|
+
"HttpxLoggingIntegration",
|
|
10
|
+
"Integration",
|
|
11
|
+
"RedisLoggingIntegration",
|
|
12
|
+
"SqlalchemyLoggingIntegration",
|
|
13
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Integration(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def setup_once(self) -> None: ...
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def _mark_setup_once(cls) -> bool:
|
|
10
|
+
flag = f"_setup_once_called_{cls.__name__}"
|
|
11
|
+
if getattr(cls, flag, False):
|
|
12
|
+
return True
|
|
13
|
+
setattr(cls, flag, True)
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _reset_setup_once(cls) -> None:
|
|
18
|
+
flag = f"_setup_once_called_{cls.__name__}"
|
|
19
|
+
setattr(cls, flag, False)
|