devinspector 2.0.6__tar.gz → 2.3.0__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.
- devinspector-2.3.0/PKG-INFO +27 -0
- devinspector-2.3.0/devinspector/__init__.py +3 -0
- devinspector-2.3.0/devinspector/core.py +393 -0
- devinspector-2.3.0/devinspector/fastapi.py +73 -0
- devinspector-2.3.0/devinspector/flask.py +88 -0
- devinspector-2.3.0/devinspector.egg-info/PKG-INFO +27 -0
- devinspector-2.3.0/pyproject.toml +40 -0
- devinspector-2.0.6/PKG-INFO +0 -15
- devinspector-2.0.6/devinspector/__init__.py +0 -3
- devinspector-2.0.6/devinspector/core.py +0 -214
- devinspector-2.0.6/devinspector/fastapi.py +0 -39
- devinspector-2.0.6/devinspector/flask.py +0 -55
- devinspector-2.0.6/devinspector.egg-info/PKG-INFO +0 -15
- devinspector-2.0.6/pyproject.toml +0 -23
- {devinspector-2.0.6 → devinspector-2.3.0}/README.md +0 -0
- {devinspector-2.0.6 → devinspector-2.3.0}/devinspector.egg-info/SOURCES.txt +0 -0
- {devinspector-2.0.6 → devinspector-2.3.0}/devinspector.egg-info/dependency_links.txt +0 -0
- {devinspector-2.0.6 → devinspector-2.3.0}/devinspector.egg-info/requires.txt +0 -0
- {devinspector-2.0.6 → devinspector-2.3.0}/devinspector.egg-info/top_level.txt +0 -0
- {devinspector-2.0.6 → devinspector-2.3.0}/setup.cfg +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devinspector
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
|
|
5
|
+
Author-email: DevInspector <suporte@devinspector.com.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://devinspector.com.br
|
|
8
|
+
Keywords: monitoring,error-tracking,apm,audit,devinspector,fastapi,flask
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Provides-Extra: fastapi
|
|
24
|
+
Requires-Dist: fastapi; extra == "fastapi"
|
|
25
|
+
Requires-Dist: starlette; extra == "fastapi"
|
|
26
|
+
Provides-Extra: flask
|
|
27
|
+
Requires-Dist: flask; extra == "flask"
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import queue
|
|
5
|
+
import threading
|
|
6
|
+
import traceback
|
|
7
|
+
import urllib.request
|
|
8
|
+
import asyncio
|
|
9
|
+
import functools
|
|
10
|
+
import contextvars
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Dict, Optional, Union
|
|
13
|
+
|
|
14
|
+
# Variável de contexto para armazenar dados da requisição atual (APM)
|
|
15
|
+
request_context = contextvars.ContextVar("request_context", default=None)
|
|
16
|
+
|
|
17
|
+
class AuditCore:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.api_key: str = "PENDING_API_KEY"
|
|
20
|
+
self.endpoint: str = "https://api.devinspector.com.br/api/ingest/track"
|
|
21
|
+
self.environment: str = "production"
|
|
22
|
+
self.initialized: bool = False
|
|
23
|
+
self._listeners_attached: bool = False
|
|
24
|
+
|
|
25
|
+
# Fila e thread em background para envio assíncrono
|
|
26
|
+
self._queue: queue.Queue = queue.Queue(maxsize=10000)
|
|
27
|
+
self._shutdown_event = threading.Event()
|
|
28
|
+
self._worker_thread = threading.Thread(target=self._worker, daemon=True)
|
|
29
|
+
self._worker_thread.start()
|
|
30
|
+
|
|
31
|
+
def init(
|
|
32
|
+
self,
|
|
33
|
+
api_key: str,
|
|
34
|
+
endpoint: Optional[str] = None,
|
|
35
|
+
environment: Optional[str] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
if not api_key:
|
|
38
|
+
print("[AuditSDK] API Key não fornecida no init().")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
self.api_key = api_key
|
|
42
|
+
if endpoint:
|
|
43
|
+
self.endpoint = endpoint
|
|
44
|
+
if environment:
|
|
45
|
+
self.environment = environment
|
|
46
|
+
|
|
47
|
+
self.initialized = True
|
|
48
|
+
self._listen_global_errors()
|
|
49
|
+
|
|
50
|
+
def is_ingest_url(self, target_url: str) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Verifica dinamicamente se uma URL pertence ao servidor de ingestão do Dev Inspector.
|
|
53
|
+
Evita chamadas em loop em ambientes self-hosted (Coolify, VPS, etc).
|
|
54
|
+
"""
|
|
55
|
+
if not target_url:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
if self.endpoint and target_url in self.endpoint or target_url in self.endpoint:
|
|
59
|
+
if target_url in self.endpoint or self.endpoint in target_url:
|
|
60
|
+
return True
|
|
61
|
+
from urllib.parse import urlparse
|
|
62
|
+
endpoint_obj = urlparse(self.endpoint)
|
|
63
|
+
return (
|
|
64
|
+
endpoint_obj.netloc in target_url or
|
|
65
|
+
endpoint_obj.path in target_url
|
|
66
|
+
)
|
|
67
|
+
except Exception:
|
|
68
|
+
return (
|
|
69
|
+
"devinspector.com.br" in target_url or
|
|
70
|
+
"/ingest/track" in target_url
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def capture_request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
url: str,
|
|
77
|
+
status_code: int,
|
|
78
|
+
duration_ms: float,
|
|
79
|
+
user_agent: str = "",
|
|
80
|
+
route: str = "",
|
|
81
|
+
db_queries_count: int = 0,
|
|
82
|
+
slow_query_ms: float = 0,
|
|
83
|
+
) -> None:
|
|
84
|
+
payload = {
|
|
85
|
+
"type": "request_metric",
|
|
86
|
+
"message": f"{method} {url} - {status_code}",
|
|
87
|
+
"method": method,
|
|
88
|
+
"url": url,
|
|
89
|
+
"statusCode": status_code,
|
|
90
|
+
"durationMs": round(duration_ms, 2),
|
|
91
|
+
"browser": user_agent,
|
|
92
|
+
"metadata": {
|
|
93
|
+
"environment": self.environment,
|
|
94
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
95
|
+
"route": route or url,
|
|
96
|
+
"dbQueriesCount": db_queries_count,
|
|
97
|
+
"slowQueryMs": round(slow_query_ms, 2),
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
self._enqueue(payload)
|
|
101
|
+
|
|
102
|
+
def capture_error(
|
|
103
|
+
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
104
|
+
) -> None:
|
|
105
|
+
raw_message = str(error) or "Erro Desconhecido"
|
|
106
|
+
raw_stack = "".join(traceback.format_tb(error.__traceback__)) if error.__traceback__ else str(error)
|
|
107
|
+
|
|
108
|
+
meta = metadata.copy() if metadata else {}
|
|
109
|
+
meta.update(
|
|
110
|
+
{
|
|
111
|
+
"environment": self.environment,
|
|
112
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
payload = {
|
|
117
|
+
"type": "error",
|
|
118
|
+
"message": self._truncate(raw_message, 500),
|
|
119
|
+
"stackTrace": self._truncate(raw_stack, 10000),
|
|
120
|
+
"url": "",
|
|
121
|
+
"browser": f"Python/{sys.version.split()[0]}",
|
|
122
|
+
"metadata": self._truncate(meta, 5000),
|
|
123
|
+
}
|
|
124
|
+
self._enqueue(payload)
|
|
125
|
+
|
|
126
|
+
def capture_exception(
|
|
127
|
+
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
128
|
+
) -> None:
|
|
129
|
+
self.capture_error(error, metadata)
|
|
130
|
+
|
|
131
|
+
def capture_message(
|
|
132
|
+
self, message: str, metadata: Optional[Dict[str, Any]] = None
|
|
133
|
+
) -> None:
|
|
134
|
+
meta = metadata.copy() if metadata else {}
|
|
135
|
+
meta.update(
|
|
136
|
+
{
|
|
137
|
+
"level": "info",
|
|
138
|
+
"environment": self.environment,
|
|
139
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
payload = {
|
|
144
|
+
"type": "message",
|
|
145
|
+
"message": self._truncate(message, 500),
|
|
146
|
+
"stackTrace": "",
|
|
147
|
+
"url": "",
|
|
148
|
+
"browser": f"Python/{sys.version.split()[0]}",
|
|
149
|
+
"metadata": self._truncate(meta, 5000),
|
|
150
|
+
}
|
|
151
|
+
self._enqueue(payload)
|
|
152
|
+
|
|
153
|
+
def _listen_global_errors(self) -> None:
|
|
154
|
+
if self._listeners_attached:
|
|
155
|
+
return
|
|
156
|
+
self._listeners_attached = True
|
|
157
|
+
|
|
158
|
+
original_excepthook = sys.excepthook
|
|
159
|
+
|
|
160
|
+
def custom_excepthook(exc_type, exc_value, exc_traceback):
|
|
161
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
162
|
+
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
self.capture_error(
|
|
166
|
+
exc_value,
|
|
167
|
+
metadata={"type": "uncaught_exception", "exc_type": exc_type.__name__},
|
|
168
|
+
)
|
|
169
|
+
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
170
|
+
|
|
171
|
+
sys.excepthook = custom_excepthook
|
|
172
|
+
|
|
173
|
+
if hasattr(threading, "excepthook"):
|
|
174
|
+
def custom_thread_excepthook(args):
|
|
175
|
+
self.capture_error(
|
|
176
|
+
args.exc_value,
|
|
177
|
+
metadata={
|
|
178
|
+
"type": "uncaught_thread_exception",
|
|
179
|
+
"thread": args.thread.name,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
threading.excepthook = custom_thread_excepthook
|
|
184
|
+
|
|
185
|
+
def _truncate(self, value: Any, max_length: int = 5000, seen=None) -> Any:
|
|
186
|
+
if seen is None:
|
|
187
|
+
seen = set()
|
|
188
|
+
|
|
189
|
+
if isinstance(value, str):
|
|
190
|
+
return (
|
|
191
|
+
value[:max_length] + "... [truncated]"
|
|
192
|
+
if len(value) > max_length
|
|
193
|
+
else value
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if isinstance(value, dict):
|
|
197
|
+
obj_id = id(value)
|
|
198
|
+
if obj_id in seen:
|
|
199
|
+
return "[Circular Reference]"
|
|
200
|
+
seen.add(obj_id)
|
|
201
|
+
|
|
202
|
+
truncated_obj = {}
|
|
203
|
+
for k, v in value.items():
|
|
204
|
+
truncated_obj[str(k)] = self._truncate(v, max_length, seen)
|
|
205
|
+
seen.remove(obj_id)
|
|
206
|
+
return truncated_obj
|
|
207
|
+
|
|
208
|
+
if isinstance(value, (list, tuple, set)):
|
|
209
|
+
obj_id = id(value)
|
|
210
|
+
if obj_id in seen:
|
|
211
|
+
return "[Circular Reference]"
|
|
212
|
+
seen.add(obj_id)
|
|
213
|
+
|
|
214
|
+
truncated_list = [self._truncate(item, max_length, seen) for item in value]
|
|
215
|
+
seen.remove(obj_id)
|
|
216
|
+
return truncated_list
|
|
217
|
+
|
|
218
|
+
return value
|
|
219
|
+
|
|
220
|
+
def _enqueue(self, payload: dict) -> None:
|
|
221
|
+
try:
|
|
222
|
+
self._queue.put_nowait(payload)
|
|
223
|
+
except queue.Full:
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
def _worker(self) -> None:
|
|
227
|
+
while not self._shutdown_event.is_set():
|
|
228
|
+
try:
|
|
229
|
+
payload = self._queue.get(timeout=0.5)
|
|
230
|
+
self._send(payload)
|
|
231
|
+
self._queue.task_done()
|
|
232
|
+
except queue.Empty:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
def _send(self, payload: dict) -> None:
|
|
236
|
+
active_key = (
|
|
237
|
+
self.api_key
|
|
238
|
+
if self.api_key and self.api_key != "PENDING_API_KEY"
|
|
239
|
+
else "dev-fallback-key"
|
|
240
|
+
)
|
|
241
|
+
data_bytes = json.dumps(payload).encode("utf-8")
|
|
242
|
+
|
|
243
|
+
req = urllib.request.Request(
|
|
244
|
+
self.endpoint,
|
|
245
|
+
data=data_bytes,
|
|
246
|
+
headers={
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
"x-api-key": active_key,
|
|
249
|
+
},
|
|
250
|
+
method="POST",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with urllib.request.urlopen(req, timeout=5):
|
|
255
|
+
pass
|
|
256
|
+
except urllib.error.HTTPError as e:
|
|
257
|
+
error_body = e.read().decode('utf-8', errors='ignore')
|
|
258
|
+
print(f"[DevInspector] HTTP {e.code} do painel: {e.reason} - Resposta: {error_body}")
|
|
259
|
+
except Exception as err:
|
|
260
|
+
print(f"[DevInspector] Falha ao enviar requisição para o painel: {err}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Singleton Global
|
|
264
|
+
audit = AuditCore()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def audit_operation(name: str, threshold_ms: int = 300):
|
|
268
|
+
"""
|
|
269
|
+
Decorador para monitorar funções de banco de dados ou chamadas externas.
|
|
270
|
+
Alimenta o request_context atual com a contagem de queries e lentidão.
|
|
271
|
+
Suporta tanto funções síncronas (def) quanto assíncronas (async def).
|
|
272
|
+
"""
|
|
273
|
+
def decorator(func):
|
|
274
|
+
@functools.wraps(func)
|
|
275
|
+
async def async_wrapper(*args, **kwargs):
|
|
276
|
+
ctx = request_context.get()
|
|
277
|
+
if ctx is not None:
|
|
278
|
+
ctx["queriesCount"] = ctx.get("queriesCount", 0) + 1
|
|
279
|
+
|
|
280
|
+
start = time.perf_counter()
|
|
281
|
+
try:
|
|
282
|
+
result = await func(*args, **kwargs)
|
|
283
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
284
|
+
|
|
285
|
+
if duration_ms > threshold_ms and ctx is not None:
|
|
286
|
+
ctx["slowQueries"] = max(ctx.get("slowQueries", 0), duration_ms)
|
|
287
|
+
audit.capture_message(
|
|
288
|
+
f"Slow Query detectada em [{name}]",
|
|
289
|
+
{"durationMs": round(duration_ms), "operationName": name}
|
|
290
|
+
)
|
|
291
|
+
return result
|
|
292
|
+
except Exception as e:
|
|
293
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
294
|
+
audit.capture_exception(e, {"operationName": name, "durationMs": round(duration_ms)})
|
|
295
|
+
raise
|
|
296
|
+
|
|
297
|
+
@functools.wraps(func)
|
|
298
|
+
def sync_wrapper(*args, **kwargs):
|
|
299
|
+
ctx = request_context.get()
|
|
300
|
+
if ctx is not None:
|
|
301
|
+
ctx["queriesCount"] = ctx.get("queriesCount", 0) + 1
|
|
302
|
+
|
|
303
|
+
start = time.perf_counter()
|
|
304
|
+
try:
|
|
305
|
+
result = func(*args, **kwargs)
|
|
306
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
307
|
+
|
|
308
|
+
if duration_ms > threshold_ms and ctx is not None:
|
|
309
|
+
ctx["slowQueries"] = max(ctx.get("slowQueries", 0), duration_ms)
|
|
310
|
+
audit.capture_message(
|
|
311
|
+
f"Slow Query detectada em [{name}]",
|
|
312
|
+
{"durationMs": round(duration_ms), "operationName": name}
|
|
313
|
+
)
|
|
314
|
+
return result
|
|
315
|
+
except Exception as e:
|
|
316
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
317
|
+
audit.capture_exception(e, {"operationName": name, "durationMs": round(duration_ms)})
|
|
318
|
+
raise
|
|
319
|
+
|
|
320
|
+
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
|
321
|
+
return decorator
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
async def core_audit_fetch(
|
|
325
|
+
url: str,
|
|
326
|
+
method: str = "GET",
|
|
327
|
+
headers: Optional[Dict[str, str]] = None,
|
|
328
|
+
data: Optional[Any] = None,
|
|
329
|
+
timeout: float = 10.0,
|
|
330
|
+
error_type: str = "network_error",
|
|
331
|
+
default_user_agent: str = "",
|
|
332
|
+
) -> Any:
|
|
333
|
+
"""
|
|
334
|
+
Helper centralizado de Intercepção de Fetch/Requisições HTTP para APM em Python.
|
|
335
|
+
Equivalente ao coreAuditFetch do SDK TypeScript.
|
|
336
|
+
"""
|
|
337
|
+
if url and audit.is_ingest_url(url):
|
|
338
|
+
# Execução direta sem rastreamento para evitar loop
|
|
339
|
+
req = urllib.request.Request(url, data=data, headers=headers or {}, method=method)
|
|
340
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
341
|
+
return response
|
|
342
|
+
|
|
343
|
+
start = time.perf_counter()
|
|
344
|
+
try:
|
|
345
|
+
# Exemplo usando urllib síncrono ou adaptado para chamadas
|
|
346
|
+
# (Para puro asyncio, pode-se integrar com httpx caso prefira)
|
|
347
|
+
req_headers = headers or {}
|
|
348
|
+
if default_user_agent and "User-Agent" not in req_headers:
|
|
349
|
+
req_headers["User-Agent"] = default_user_agent
|
|
350
|
+
|
|
351
|
+
req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
|
|
352
|
+
|
|
353
|
+
# Executa em executor separado para não travar o loop async se chamado em contexto assíncrono
|
|
354
|
+
loop = asyncio.get_running_loop()
|
|
355
|
+
response = await loop.run_in_executor(
|
|
356
|
+
None, lambda: urllib.request.urlopen(req, timeout=timeout)
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
360
|
+
status_code = response.getcode()
|
|
361
|
+
|
|
362
|
+
if status_code >= 400 or duration_ms >= 300:
|
|
363
|
+
audit.capture_request(
|
|
364
|
+
method=method,
|
|
365
|
+
url=url,
|
|
366
|
+
status_code=status_code,
|
|
367
|
+
duration_ms=duration_ms,
|
|
368
|
+
route=url,
|
|
369
|
+
user_agent=req_headers.get("User-Agent", default_user_agent),
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return response
|
|
373
|
+
except Exception as error:
|
|
374
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
375
|
+
|
|
376
|
+
status_code = 0
|
|
377
|
+
if hasattr(error, "code"):
|
|
378
|
+
status_code = error.code
|
|
379
|
+
|
|
380
|
+
audit.capture_request(
|
|
381
|
+
method=method,
|
|
382
|
+
url=url,
|
|
383
|
+
status_code=status_code,
|
|
384
|
+
duration_ms=duration_ms,
|
|
385
|
+
route=url,
|
|
386
|
+
user_agent=default_user_agent,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
audit.capture_exception(
|
|
390
|
+
error if isinstance(error, Exception) else Exception(str(error)),
|
|
391
|
+
{"url": url, "method": method, "type": error_type},
|
|
392
|
+
)
|
|
393
|
+
raise error
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
from .core import audit, request_context
|
|
5
|
+
|
|
6
|
+
class DevInspectorMiddleware(BaseHTTPMiddleware):
|
|
7
|
+
def __init__(self, app, slow_threshold_ms: int = 300):
|
|
8
|
+
super().__init__(app)
|
|
9
|
+
self.slow_threshold_ms = slow_threshold_ms
|
|
10
|
+
|
|
11
|
+
async def dispatch(self, request: Request, call_next):
|
|
12
|
+
# Ignora rotas estáticas (adicione outras se necessário)
|
|
13
|
+
if request.url.path.startswith("/uploads"):
|
|
14
|
+
return await call_next(request)
|
|
15
|
+
|
|
16
|
+
start_time = time.perf_counter()
|
|
17
|
+
status_code = 500
|
|
18
|
+
|
|
19
|
+
# 1. Inicia o contexto da requisição para o APM isolar as contagens
|
|
20
|
+
token = request_context.set({
|
|
21
|
+
"queriesCount": 0,
|
|
22
|
+
"slowQueries": 0
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
response = await call_next(request)
|
|
27
|
+
status_code = response.status_code
|
|
28
|
+
return response
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
if hasattr(exc, "status_code"):
|
|
31
|
+
status_code = exc.status_code
|
|
32
|
+
|
|
33
|
+
audit.capture_error(
|
|
34
|
+
exc,
|
|
35
|
+
metadata={
|
|
36
|
+
"path": request.url.path,
|
|
37
|
+
"method": request.method,
|
|
38
|
+
"statusCode": status_code,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
raise exc
|
|
42
|
+
finally:
|
|
43
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
44
|
+
user_agent = request.headers.get("user-agent", "")
|
|
45
|
+
|
|
46
|
+
# 2. Resgata os dados do APM que foram injetados pelas operações do banco
|
|
47
|
+
ctx = request_context.get()
|
|
48
|
+
db_queries_count = ctx.get("queriesCount", 0) if ctx else 0
|
|
49
|
+
slow_query_ms = ctx.get("slowQueries", 0) if ctx else 0
|
|
50
|
+
|
|
51
|
+
is_error = status_code >= 400
|
|
52
|
+
is_slow = duration_ms >= self.slow_threshold_ms
|
|
53
|
+
has_slow_query = slow_query_ms > 0
|
|
54
|
+
|
|
55
|
+
# 3. 🎯 FILTRO INTELIGENTE: Só envia se for Erro, Lenta ou tiver Query Lenta
|
|
56
|
+
if is_error or is_slow or has_slow_query:
|
|
57
|
+
# Tenta extrair a rota mapeada (ex: /users/{id}) em vez da URL crua (/users/123)
|
|
58
|
+
route_path = request.scope.get("route")
|
|
59
|
+
route_name = route_path.path if hasattr(route_path, "path") else request.url.path
|
|
60
|
+
|
|
61
|
+
audit.capture_request(
|
|
62
|
+
method=request.method,
|
|
63
|
+
url=str(request.url),
|
|
64
|
+
status_code=status_code,
|
|
65
|
+
duration_ms=duration_ms,
|
|
66
|
+
user_agent=user_agent,
|
|
67
|
+
route=route_name,
|
|
68
|
+
db_queries_count=db_queries_count,
|
|
69
|
+
slow_query_ms=slow_query_ms,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 4. Limpa o contexto para evitar vazamento de memória em outras requisições
|
|
73
|
+
request_context.reset(token)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from flask import Flask, g, request
|
|
3
|
+
from .core import audit, request_context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DevInspectorFlask:
|
|
7
|
+
def __init__(self, app: Flask = None, slow_threshold_ms: int = 300):
|
|
8
|
+
self.slow_threshold_ms = slow_threshold_ms
|
|
9
|
+
if app is not None:
|
|
10
|
+
self.init_app(app)
|
|
11
|
+
|
|
12
|
+
def init_app(self, app: Flask):
|
|
13
|
+
app.before_request(self._before_request)
|
|
14
|
+
app.after_request(self._after_request)
|
|
15
|
+
app.teardown_request(self._teardown_request)
|
|
16
|
+
|
|
17
|
+
def _before_request(self):
|
|
18
|
+
# Ignora rotas estáticas (adicione outras se necessário)
|
|
19
|
+
if request.path.startswith("/uploads"):
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
g._devinspector_start_time = time.perf_counter()
|
|
23
|
+
|
|
24
|
+
# 1. Inicia o contexto da requisição para isolar as contagens do APM
|
|
25
|
+
g._devinspector_ctx_token = request_context.set({
|
|
26
|
+
"queriesCount": 0,
|
|
27
|
+
"slowQueries": 0
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
def _after_request(self, response):
|
|
31
|
+
self._record_metric(response.status_code)
|
|
32
|
+
return response
|
|
33
|
+
|
|
34
|
+
def _teardown_request(self, exception=None):
|
|
35
|
+
if exception:
|
|
36
|
+
status_code = 500
|
|
37
|
+
if hasattr(exception, "code"):
|
|
38
|
+
status_code = exception.code
|
|
39
|
+
|
|
40
|
+
audit.capture_error(
|
|
41
|
+
exception,
|
|
42
|
+
metadata={
|
|
43
|
+
"path": request.path,
|
|
44
|
+
"method": request.method,
|
|
45
|
+
"statusCode": status_code,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
if not getattr(g, "_devinspector_metric_recorded", False):
|
|
49
|
+
self._record_metric(status_code)
|
|
50
|
+
|
|
51
|
+
# 4. Limpa o contexto ao final da requisição para evitar vazamento
|
|
52
|
+
if hasattr(g, "_devinspector_ctx_token"):
|
|
53
|
+
request_context.reset(g._devinspector_ctx_token)
|
|
54
|
+
|
|
55
|
+
def _record_metric(self, status_code: int):
|
|
56
|
+
start_time = getattr(g, "_devinspector_start_time", None)
|
|
57
|
+
if start_time is None:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
61
|
+
user_agent = request.headers.get("User-Agent", "")
|
|
62
|
+
|
|
63
|
+
# 2. Resgata os dados do APM que foram injetados pelas operações do banco
|
|
64
|
+
ctx = request_context.get()
|
|
65
|
+
db_queries_count = ctx.get("queriesCount", 0) if ctx else 0
|
|
66
|
+
slow_query_ms = ctx.get("slowQueries", 0) if ctx else 0
|
|
67
|
+
|
|
68
|
+
is_error = status_code >= 400
|
|
69
|
+
is_slow = duration_ms >= self.slow_threshold_ms
|
|
70
|
+
has_slow_query = slow_query_ms > 0
|
|
71
|
+
|
|
72
|
+
# 3. 🎯 FILTRO INTELIGENTE: Só envia se for Erro, Lenta ou tiver Query Lenta
|
|
73
|
+
if is_error or is_slow or has_slow_query:
|
|
74
|
+
# Extrai a rota com parâmetros (ex: /users/<id>) em vez da URL crua
|
|
75
|
+
route_name = request.url_rule.rule if request.url_rule else request.path
|
|
76
|
+
|
|
77
|
+
audit.capture_request(
|
|
78
|
+
method=request.method,
|
|
79
|
+
url=request.url,
|
|
80
|
+
status_code=status_code,
|
|
81
|
+
duration_ms=duration_ms,
|
|
82
|
+
user_agent=user_agent,
|
|
83
|
+
route=route_name,
|
|
84
|
+
db_queries_count=db_queries_count,
|
|
85
|
+
slow_query_ms=slow_query_ms,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
g._devinspector_metric_recorded = True
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devinspector
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
|
|
5
|
+
Author-email: DevInspector <suporte@devinspector.com.br>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://devinspector.com.br
|
|
8
|
+
Keywords: monitoring,error-tracking,apm,audit,devinspector,fastapi,flask
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Provides-Extra: fastapi
|
|
24
|
+
Requires-Dist: fastapi; extra == "fastapi"
|
|
25
|
+
Requires-Dist: starlette; extra == "fastapi"
|
|
26
|
+
Provides-Extra: flask
|
|
27
|
+
Requires-Dist: flask; extra == "flask"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "devinspector"
|
|
7
|
+
version = "2.3.0"
|
|
8
|
+
description = "SDK Python de monitoramento e auditoria em tempo real | DevInspector"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "DevInspector", email = "suporte@devinspector.com.br"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["monitoring", "error-tracking", "apm", "audit", "devinspector", "fastapi", "flask"]
|
|
16
|
+
dependencies = []
|
|
17
|
+
|
|
18
|
+
# Classificadores ajudam os desenvolvedores a encontrarem seu pacote no PyPI
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 5 - Production/Stable",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.8",
|
|
26
|
+
"Programming Language :: Python :: 3.9",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
"Topic :: System :: Monitoring",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
fastapi = ["fastapi", "starlette"]
|
|
36
|
+
flask = ["flask"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://devinspector.com.br"
|
|
40
|
+
# Documentation = "https://docs.devinspector.com.br" # Descomente se tiver
|
devinspector-2.0.6/PKG-INFO
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: devinspector
|
|
3
|
-
Version: 2.0.6
|
|
4
|
-
Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
|
|
5
|
-
Author-email: DevInspector <suporte@devinspector.com.br>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://devinspector.com.br
|
|
8
|
-
Keywords: monitoring,error-tracking,audit,devinspector,fastapi,flask
|
|
9
|
-
Requires-Python: >=3.8
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Provides-Extra: fastapi
|
|
12
|
-
Requires-Dist: fastapi; extra == "fastapi"
|
|
13
|
-
Requires-Dist: starlette; extra == "fastapi"
|
|
14
|
-
Provides-Extra: flask
|
|
15
|
-
Requires-Dist: flask; extra == "flask"
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import time
|
|
4
|
-
import queue
|
|
5
|
-
import threading
|
|
6
|
-
import traceback
|
|
7
|
-
import urllib.request
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from typing import Any, Dict, Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class AuditCore:
|
|
13
|
-
def __init__(self):
|
|
14
|
-
self.api_key: str = "PENDING_API_KEY"
|
|
15
|
-
self.endpoint: str = "https://api.devinspector.com.br/api/ingest/track"
|
|
16
|
-
self.environment: str = "production"
|
|
17
|
-
self.initialized: bool = False
|
|
18
|
-
self._listeners_attached: bool = False
|
|
19
|
-
|
|
20
|
-
# Fila e thread em background para envio assíncrono
|
|
21
|
-
self._queue: queue.Queue = queue.Queue(maxsize=10000)
|
|
22
|
-
self._shutdown_event = threading.Event()
|
|
23
|
-
self._worker_thread = threading.Thread(target=self._worker, daemon=True)
|
|
24
|
-
self._worker_thread.start()
|
|
25
|
-
|
|
26
|
-
def init(
|
|
27
|
-
self,
|
|
28
|
-
api_key: str,
|
|
29
|
-
endpoint: Optional[str] = None,
|
|
30
|
-
environment: Optional[str] = None,
|
|
31
|
-
) -> None:
|
|
32
|
-
if not api_key:
|
|
33
|
-
print("[AuditSDK] API Key não fornecida no init().")
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
self.api_key = api_key
|
|
37
|
-
if endpoint:
|
|
38
|
-
self.endpoint = endpoint
|
|
39
|
-
if environment:
|
|
40
|
-
self.environment = environment
|
|
41
|
-
|
|
42
|
-
self.initialized = True
|
|
43
|
-
self._listen_global_errors()
|
|
44
|
-
|
|
45
|
-
def capture_request(
|
|
46
|
-
self,
|
|
47
|
-
method: str,
|
|
48
|
-
url: str,
|
|
49
|
-
status_code: int,
|
|
50
|
-
duration_ms: float,
|
|
51
|
-
user_agent: str = "",
|
|
52
|
-
) -> None:
|
|
53
|
-
payload = {
|
|
54
|
-
"type": "request_metric",
|
|
55
|
-
"message": f"{method} {url} - {status_code}",
|
|
56
|
-
"method": method,
|
|
57
|
-
"url": url,
|
|
58
|
-
"statusCode": status_code,
|
|
59
|
-
"durationMs": round(duration_ms, 2),
|
|
60
|
-
"browser": user_agent,
|
|
61
|
-
"metadata": {
|
|
62
|
-
"environment": self.environment,
|
|
63
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
self._enqueue(payload)
|
|
67
|
-
|
|
68
|
-
def capture_error(
|
|
69
|
-
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
70
|
-
) -> None:
|
|
71
|
-
raw_message = str(error) or "Erro Desconhecido"
|
|
72
|
-
raw_stack = "".join(traceback.format_tb(error.__traceback__))
|
|
73
|
-
|
|
74
|
-
meta = metadata.copy() if metadata else {}
|
|
75
|
-
meta.update(
|
|
76
|
-
{
|
|
77
|
-
"environment": self.environment,
|
|
78
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
payload = {
|
|
83
|
-
"type": "error",
|
|
84
|
-
"message": self._truncate(raw_message, 500),
|
|
85
|
-
"stackTrace": self._truncate(raw_stack, 10000),
|
|
86
|
-
"url": "",
|
|
87
|
-
"browser": f"Python/{sys.version.split()[0]}",
|
|
88
|
-
"metadata": self._truncate(meta, 5000),
|
|
89
|
-
}
|
|
90
|
-
self._enqueue(payload)
|
|
91
|
-
|
|
92
|
-
def capture_exception(
|
|
93
|
-
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
94
|
-
) -> None:
|
|
95
|
-
self.capture_error(error, metadata)
|
|
96
|
-
|
|
97
|
-
def capture_message(
|
|
98
|
-
self, message: str, metadata: Optional[Dict[str, Any]] = None
|
|
99
|
-
) -> None:
|
|
100
|
-
meta = metadata.copy() if metadata else {}
|
|
101
|
-
meta.update(
|
|
102
|
-
{
|
|
103
|
-
"level": "info",
|
|
104
|
-
"environment": self.environment,
|
|
105
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
payload = {
|
|
110
|
-
"type": "message",
|
|
111
|
-
"message": self._truncate(message, 500),
|
|
112
|
-
"stackTrace": "",
|
|
113
|
-
"url": "",
|
|
114
|
-
"browser": f"Python/{sys.version.split()[0]}",
|
|
115
|
-
"metadata": self._truncate(meta, 5000),
|
|
116
|
-
}
|
|
117
|
-
self._enqueue(payload)
|
|
118
|
-
|
|
119
|
-
def _listen_global_errors(self) -> None:
|
|
120
|
-
if self._listeners_attached:
|
|
121
|
-
return
|
|
122
|
-
self._listeners_attached = True
|
|
123
|
-
|
|
124
|
-
original_excepthook = sys.excepthook
|
|
125
|
-
|
|
126
|
-
def custom_excepthook(exc_type, exc_value, exc_traceback):
|
|
127
|
-
if issubclass(exc_type, KeyboardInterrupt):
|
|
128
|
-
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
self.capture_error(
|
|
132
|
-
exc_value,
|
|
133
|
-
metadata={"type": "uncaught_exception", "exc_type": exc_type.__name__},
|
|
134
|
-
)
|
|
135
|
-
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
136
|
-
|
|
137
|
-
sys.excepthook = custom_excepthook
|
|
138
|
-
|
|
139
|
-
# Captura exceções em Threads secundárias (Python 3.8+)
|
|
140
|
-
if hasattr(threading, "excepthook"):
|
|
141
|
-
|
|
142
|
-
def custom_thread_excepthook(args):
|
|
143
|
-
self.capture_error(
|
|
144
|
-
args.exc_value,
|
|
145
|
-
metadata={
|
|
146
|
-
"type": "uncaught_thread_exception",
|
|
147
|
-
"thread": args.thread.name,
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
threading.excepthook = custom_thread_excepthook
|
|
152
|
-
|
|
153
|
-
def _truncate(self, value: Any, max_length: int = 5000) -> Any:
|
|
154
|
-
if isinstance(value, str):
|
|
155
|
-
return (
|
|
156
|
-
value[:max_length] + "... [truncated]"
|
|
157
|
-
if len(value) > max_length
|
|
158
|
-
else value
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
if isinstance(value, dict):
|
|
162
|
-
return {
|
|
163
|
-
str(k): self._truncate(v, max_length) for k, v in value.items()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return value
|
|
167
|
-
|
|
168
|
-
def _enqueue(self, payload: dict) -> None:
|
|
169
|
-
try:
|
|
170
|
-
self._queue.put_nowait(payload)
|
|
171
|
-
except queue.Full:
|
|
172
|
-
pass
|
|
173
|
-
|
|
174
|
-
def _worker(self) -> None:
|
|
175
|
-
while not self._shutdown_event.is_set():
|
|
176
|
-
try:
|
|
177
|
-
payload = self._queue.get(timeout=0.5)
|
|
178
|
-
self._send(payload)
|
|
179
|
-
self._queue.task_done()
|
|
180
|
-
except queue.Empty:
|
|
181
|
-
continue
|
|
182
|
-
|
|
183
|
-
def _send(self, payload: dict) -> None:
|
|
184
|
-
active_key = (
|
|
185
|
-
self.api_key
|
|
186
|
-
if self.api_key and self.api_key != "PENDING_API_KEY"
|
|
187
|
-
else "dev-fallback-key"
|
|
188
|
-
)
|
|
189
|
-
data_bytes = json.dumps(payload).encode("utf-8")
|
|
190
|
-
|
|
191
|
-
req = urllib.request.Request(
|
|
192
|
-
self.endpoint,
|
|
193
|
-
data=data_bytes,
|
|
194
|
-
headers={
|
|
195
|
-
"Content-Type": "application/json",
|
|
196
|
-
"x-api-key": active_key,
|
|
197
|
-
},
|
|
198
|
-
method="POST",
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
with urllib.request.urlopen(req, timeout=5):
|
|
203
|
-
pass
|
|
204
|
-
except urllib.error.HTTPError as e:
|
|
205
|
-
# Mostra o erro detalhado que o painel devolveu
|
|
206
|
-
error_body = e.read().decode('utf-8', errors='ignore')
|
|
207
|
-
print(f"[DevInspector] HTTP {e.code} do painel: {e.reason} - Resposta: {error_body}")
|
|
208
|
-
print(f"[DevInspector] Payload enviado que gerou o erro: {payload}")
|
|
209
|
-
except Exception as err:
|
|
210
|
-
print(f"[DevInspector] Falha ao enviar requisição para o painel: {err}")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
# Singleton Global
|
|
214
|
-
audit = AuditCore()
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
|
3
|
-
from starlette.requests import Request
|
|
4
|
-
from .core import audit
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class DevInspectorMiddleware(BaseHTTPMiddleware):
|
|
8
|
-
async def dispatch(self, request: Request, call_next):
|
|
9
|
-
start_time = time.perf_counter()
|
|
10
|
-
status_code = 500
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
response = await call_next(request)
|
|
14
|
-
status_code = response.status_code
|
|
15
|
-
return response
|
|
16
|
-
except Exception as exc:
|
|
17
|
-
if hasattr(exc, "status_code"):
|
|
18
|
-
status_code = exc.status_code
|
|
19
|
-
|
|
20
|
-
audit.capture_error(
|
|
21
|
-
exc,
|
|
22
|
-
metadata={
|
|
23
|
-
"path": request.url.path,
|
|
24
|
-
"method": request.method,
|
|
25
|
-
"statusCode": status_code,
|
|
26
|
-
},
|
|
27
|
-
)
|
|
28
|
-
raise exc
|
|
29
|
-
finally:
|
|
30
|
-
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
31
|
-
user_agent = request.headers.get("user-agent", "")
|
|
32
|
-
|
|
33
|
-
audit.capture_request(
|
|
34
|
-
method=request.method,
|
|
35
|
-
url=str(request.url),
|
|
36
|
-
status_code=status_code,
|
|
37
|
-
duration_ms=duration_ms,
|
|
38
|
-
user_agent=user_agent,
|
|
39
|
-
)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from flask import Flask, g, request
|
|
3
|
-
from .core import audit
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class DevInspectorFlask:
|
|
7
|
-
def __init__(self, app: Flask = None):
|
|
8
|
-
if app is not None:
|
|
9
|
-
self.init_app(app)
|
|
10
|
-
|
|
11
|
-
def init_app(self, app: Flask):
|
|
12
|
-
app.before_request(self._before_request)
|
|
13
|
-
app.after_request(self._after_request)
|
|
14
|
-
app.teardown_request(self._teardown_request)
|
|
15
|
-
|
|
16
|
-
def _before_request(self):
|
|
17
|
-
g._devinspector_start_time = time.perf_counter()
|
|
18
|
-
|
|
19
|
-
def _after_request(self, response):
|
|
20
|
-
self._record_metric(response.status_code)
|
|
21
|
-
return response
|
|
22
|
-
|
|
23
|
-
def _teardown_request(self, exception=None):
|
|
24
|
-
if exception:
|
|
25
|
-
status_code = 500
|
|
26
|
-
if hasattr(exception, "code"):
|
|
27
|
-
status_code = exception.code
|
|
28
|
-
|
|
29
|
-
audit.capture_error(
|
|
30
|
-
exception,
|
|
31
|
-
metadata={
|
|
32
|
-
"path": request.path,
|
|
33
|
-
"method": request.method,
|
|
34
|
-
"statusCode": status_code,
|
|
35
|
-
},
|
|
36
|
-
)
|
|
37
|
-
if not getattr(g, "_devinspector_metric_recorded", False):
|
|
38
|
-
self._record_metric(status_code)
|
|
39
|
-
|
|
40
|
-
def _record_metric(self, status_code: int):
|
|
41
|
-
start_time = getattr(g, "_devinspector_start_time", None)
|
|
42
|
-
if start_time is None:
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
46
|
-
user_agent = request.headers.get("User-Agent", "")
|
|
47
|
-
|
|
48
|
-
audit.capture_request(
|
|
49
|
-
method=request.method,
|
|
50
|
-
url=request.url,
|
|
51
|
-
status_code=status_code,
|
|
52
|
-
duration_ms=duration_ms,
|
|
53
|
-
user_agent=user_agent,
|
|
54
|
-
)
|
|
55
|
-
g._devinspector_metric_recorded = True
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: devinspector
|
|
3
|
-
Version: 2.0.6
|
|
4
|
-
Summary: SDK Python de monitoramento e auditoria em tempo real | DevInspector
|
|
5
|
-
Author-email: DevInspector <suporte@devinspector.com.br>
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://devinspector.com.br
|
|
8
|
-
Keywords: monitoring,error-tracking,audit,devinspector,fastapi,flask
|
|
9
|
-
Requires-Python: >=3.8
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Provides-Extra: fastapi
|
|
12
|
-
Requires-Dist: fastapi; extra == "fastapi"
|
|
13
|
-
Requires-Dist: starlette; extra == "fastapi"
|
|
14
|
-
Provides-Extra: flask
|
|
15
|
-
Requires-Dist: flask; extra == "flask"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "devinspector"
|
|
7
|
-
version = "2.0.6"
|
|
8
|
-
description = "SDK Python de monitoramento e auditoria em tempo real | DevInspector"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.8"
|
|
11
|
-
license = {text = "MIT"}
|
|
12
|
-
authors = [
|
|
13
|
-
{name = "DevInspector", email = "suporte@devinspector.com.br"}
|
|
14
|
-
]
|
|
15
|
-
keywords = ["monitoring", "error-tracking", "audit", "devinspector", "fastapi", "flask"]
|
|
16
|
-
dependencies = []
|
|
17
|
-
|
|
18
|
-
[project.optional-dependencies]
|
|
19
|
-
fastapi = ["fastapi", "starlette"]
|
|
20
|
-
flask = ["flask"]
|
|
21
|
-
|
|
22
|
-
[project.urls]
|
|
23
|
-
Homepage = "https://devinspector.com.br"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|