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.
@@ -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,3 @@
1
+ from .core import AuditCore, audit, audit_operation
2
+
3
+ __all__ = ["audit", "AuditCore", "audit_operation"]
@@ -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
@@ -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,3 +0,0 @@
1
- from .core import AuditCore, audit
2
-
3
- __all__ = ["audit", "AuditCore"]
@@ -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