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,175 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from road24_sdk._schemas import DbAttributes
|
|
7
|
+
from road24_sdk._types import DbOperation, LogType
|
|
8
|
+
from road24_sdk.integrations._base import Integration
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_START_TIME_KEY = "_query_start_time"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DbLogger:
|
|
16
|
+
MAX_STATEMENT_LENGTH = 500
|
|
17
|
+
|
|
18
|
+
TABLE_PATTERNS = [
|
|
19
|
+
r"FROM\s+([\"']?[\w\.]+[\"']?)",
|
|
20
|
+
r"INTO\s+([\"']?[\w\.]+[\"']?)",
|
|
21
|
+
r"UPDATE\s+([\"']?[\w\.]+[\"']?)",
|
|
22
|
+
r"DELETE\s+FROM\s+([\"']?[\w\.]+[\"']?)",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def log_query(
|
|
26
|
+
self,
|
|
27
|
+
operation: DbOperation,
|
|
28
|
+
table: str,
|
|
29
|
+
duration_seconds: float,
|
|
30
|
+
statement: str = "",
|
|
31
|
+
record_metrics: bool = True,
|
|
32
|
+
) -> None:
|
|
33
|
+
if record_metrics:
|
|
34
|
+
from road24_sdk.metrics.db import record_db_query
|
|
35
|
+
|
|
36
|
+
record_db_query(
|
|
37
|
+
operation=operation,
|
|
38
|
+
table=table,
|
|
39
|
+
duration_seconds=duration_seconds,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
db_attrs = DbAttributes(
|
|
43
|
+
operation=operation,
|
|
44
|
+
table=table,
|
|
45
|
+
duration_seconds=duration_seconds,
|
|
46
|
+
statement=statement[: self.MAX_STATEMENT_LENGTH],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logger.info(LogType.DB_QUERY, extra=db_attrs.as_dict())
|
|
50
|
+
|
|
51
|
+
def before_execute(
|
|
52
|
+
self,
|
|
53
|
+
conn: Any,
|
|
54
|
+
cursor: Any,
|
|
55
|
+
statement: str,
|
|
56
|
+
parameters: Any,
|
|
57
|
+
context: Any,
|
|
58
|
+
executemany: bool,
|
|
59
|
+
) -> None:
|
|
60
|
+
conn.info[_START_TIME_KEY] = time.perf_counter()
|
|
61
|
+
|
|
62
|
+
def after_execute(
|
|
63
|
+
self,
|
|
64
|
+
conn: Any,
|
|
65
|
+
cursor: Any,
|
|
66
|
+
statement: str,
|
|
67
|
+
parameters: Any,
|
|
68
|
+
context: Any,
|
|
69
|
+
executemany: bool,
|
|
70
|
+
record_metrics: bool = True,
|
|
71
|
+
) -> None:
|
|
72
|
+
start_time = conn.info.pop(_START_TIME_KEY, None)
|
|
73
|
+
duration_seconds = time.perf_counter() - start_time if start_time else 0.0
|
|
74
|
+
operation = self.extract_operation(statement)
|
|
75
|
+
table = self.extract_table(statement)
|
|
76
|
+
|
|
77
|
+
self.log_query(
|
|
78
|
+
operation=operation,
|
|
79
|
+
table=table,
|
|
80
|
+
duration_seconds=duration_seconds,
|
|
81
|
+
statement=statement,
|
|
82
|
+
record_metrics=record_metrics,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def extract_operation(self, statement: str) -> DbOperation:
|
|
86
|
+
statement_upper = statement.strip().upper()
|
|
87
|
+
|
|
88
|
+
if statement_upper.startswith("SELECT"):
|
|
89
|
+
return DbOperation.SELECT
|
|
90
|
+
if statement_upper.startswith("INSERT"):
|
|
91
|
+
return DbOperation.INSERT
|
|
92
|
+
if statement_upper.startswith("UPDATE"):
|
|
93
|
+
return DbOperation.UPDATE
|
|
94
|
+
if statement_upper.startswith("DELETE"):
|
|
95
|
+
return DbOperation.DELETE
|
|
96
|
+
|
|
97
|
+
return DbOperation.OTHER
|
|
98
|
+
|
|
99
|
+
def extract_table(self, statement: str) -> str:
|
|
100
|
+
statement_upper = statement.strip().upper()
|
|
101
|
+
|
|
102
|
+
for pattern in self.TABLE_PATTERNS:
|
|
103
|
+
if match := re.search(pattern, statement_upper):
|
|
104
|
+
table = match.group(1).strip("\"'").lower()
|
|
105
|
+
return table.split(".")[-1]
|
|
106
|
+
|
|
107
|
+
return "unknown"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SqlalchemyLoggingIntegration(Integration):
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
enable_logging: bool = True,
|
|
115
|
+
enable_metrics: bool = True,
|
|
116
|
+
) -> None:
|
|
117
|
+
self._enable_logging = enable_logging
|
|
118
|
+
self._enable_metrics = enable_metrics
|
|
119
|
+
self._db_logger = DbLogger()
|
|
120
|
+
|
|
121
|
+
def setup_once(self) -> None:
|
|
122
|
+
if self._mark_setup_once():
|
|
123
|
+
return
|
|
124
|
+
from sqlalchemy import event
|
|
125
|
+
from sqlalchemy.engine import Engine
|
|
126
|
+
|
|
127
|
+
event.listen(Engine, "before_cursor_execute", self._db_logger.before_execute)
|
|
128
|
+
|
|
129
|
+
if self._enable_logging:
|
|
130
|
+
enable_metrics = self._enable_metrics
|
|
131
|
+
db_logger = self._db_logger
|
|
132
|
+
|
|
133
|
+
event.listen(
|
|
134
|
+
Engine,
|
|
135
|
+
"after_cursor_execute",
|
|
136
|
+
lambda conn, cursor, stmt, params, ctx, many: (
|
|
137
|
+
db_logger.after_execute(
|
|
138
|
+
conn,
|
|
139
|
+
cursor,
|
|
140
|
+
stmt,
|
|
141
|
+
params,
|
|
142
|
+
ctx,
|
|
143
|
+
many,
|
|
144
|
+
record_metrics=enable_metrics,
|
|
145
|
+
)
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def setup(self, engine: Any) -> None:
|
|
150
|
+
from sqlalchemy import event
|
|
151
|
+
|
|
152
|
+
sync_engine = getattr(engine, "sync_engine", engine)
|
|
153
|
+
|
|
154
|
+
event.listen(
|
|
155
|
+
sync_engine,
|
|
156
|
+
"before_cursor_execute",
|
|
157
|
+
self._db_logger.before_execute,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if self._enable_logging:
|
|
161
|
+
event.listen(
|
|
162
|
+
sync_engine,
|
|
163
|
+
"after_cursor_execute",
|
|
164
|
+
lambda conn, cursor, stmt, params, ctx, many: (
|
|
165
|
+
self._db_logger.after_execute(
|
|
166
|
+
conn,
|
|
167
|
+
cursor,
|
|
168
|
+
stmt,
|
|
169
|
+
params,
|
|
170
|
+
ctx,
|
|
171
|
+
many,
|
|
172
|
+
record_metrics=self._enable_metrics,
|
|
173
|
+
)
|
|
174
|
+
),
|
|
175
|
+
)
|
road24_sdk/metrics/db.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from prometheus_client import Counter, Histogram
|
|
2
|
+
|
|
3
|
+
from road24_sdk._types import DbOperation
|
|
4
|
+
|
|
5
|
+
DB_QUERY_DURATION = Histogram(
|
|
6
|
+
name="db_query_duration_seconds",
|
|
7
|
+
documentation="Database query duration in seconds",
|
|
8
|
+
labelnames=["operation", "table"],
|
|
9
|
+
buckets=(0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
DB_QUERY_TOTAL = Counter(
|
|
13
|
+
name="db_queries_total",
|
|
14
|
+
documentation="Total number of database queries",
|
|
15
|
+
labelnames=["operation", "table"],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def record_db_query(
|
|
20
|
+
operation: DbOperation,
|
|
21
|
+
table: str,
|
|
22
|
+
duration_seconds: float,
|
|
23
|
+
) -> None:
|
|
24
|
+
labels = {
|
|
25
|
+
"operation": operation.value,
|
|
26
|
+
"table": table,
|
|
27
|
+
}
|
|
28
|
+
DB_QUERY_TOTAL.labels(**labels).inc()
|
|
29
|
+
DB_QUERY_DURATION.labels(**labels).observe(duration_seconds)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from prometheus_client import Counter, Histogram
|
|
2
|
+
|
|
3
|
+
from road24_sdk._types import HttpDirection
|
|
4
|
+
|
|
5
|
+
HTTP_REQUEST_DURATION = Histogram(
|
|
6
|
+
name="http_request_duration_seconds",
|
|
7
|
+
documentation="HTTP request duration in seconds",
|
|
8
|
+
labelnames=["method", "url", "status_code", "direction"],
|
|
9
|
+
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
HTTP_REQUEST_TOTAL = Counter(
|
|
13
|
+
name="http_requests_total",
|
|
14
|
+
documentation="Total number of HTTP requests",
|
|
15
|
+
labelnames=["method", "url", "status_code", "direction"],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def record_http_request(
|
|
20
|
+
method: str,
|
|
21
|
+
url: str,
|
|
22
|
+
status_code: int,
|
|
23
|
+
duration_seconds: float,
|
|
24
|
+
direction: HttpDirection,
|
|
25
|
+
) -> None:
|
|
26
|
+
labels = {
|
|
27
|
+
"method": method,
|
|
28
|
+
"url": url,
|
|
29
|
+
"status_code": str(status_code),
|
|
30
|
+
"direction": direction.value,
|
|
31
|
+
}
|
|
32
|
+
HTTP_REQUEST_TOTAL.labels(**labels).inc()
|
|
33
|
+
HTTP_REQUEST_DURATION.labels(**labels).observe(duration_seconds)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from prometheus_client import Counter, Histogram
|
|
2
|
+
|
|
3
|
+
from road24_sdk._types import RedisOperation
|
|
4
|
+
|
|
5
|
+
REDIS_COMMAND_DURATION = Histogram(
|
|
6
|
+
name="redis_command_duration_seconds",
|
|
7
|
+
documentation="Redis command duration in seconds",
|
|
8
|
+
labelnames=["operation", "key"],
|
|
9
|
+
buckets=(0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5),
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
REDIS_COMMAND_TOTAL = Counter(
|
|
13
|
+
name="redis_commands_total",
|
|
14
|
+
documentation="Total number of Redis commands",
|
|
15
|
+
labelnames=["operation", "key"],
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def record_redis_command(
|
|
20
|
+
operation: RedisOperation,
|
|
21
|
+
key: str,
|
|
22
|
+
duration_seconds: float,
|
|
23
|
+
) -> None:
|
|
24
|
+
labels = {
|
|
25
|
+
"operation": operation.value,
|
|
26
|
+
"key": key,
|
|
27
|
+
}
|
|
28
|
+
REDIS_COMMAND_TOTAL.labels(**labels).inc()
|
|
29
|
+
REDIS_COMMAND_DURATION.labels(**labels).observe(duration_seconds)
|