devinspector 2.0.6__tar.gz → 2.1.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.1.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"]
@@ -5,9 +5,15 @@ import queue
5
5
  import threading
6
6
  import traceback
7
7
  import urllib.request
8
+ import asyncio
9
+ import functools
10
+ import contextvars
8
11
  from datetime import datetime, timezone
9
12
  from typing import Any, Dict, Optional
10
13
 
14
+ # Variável de contexto para armazenar dados da requisição atual (APM)
15
+ # Substitui o papel do AsyncLocalStorage do Node.js
16
+ request_context = contextvars.ContextVar("request_context", default=None)
11
17
 
12
18
  class AuditCore:
13
19
  def __init__(self):
@@ -49,6 +55,9 @@ class AuditCore:
49
55
  status_code: int,
50
56
  duration_ms: float,
51
57
  user_agent: str = "",
58
+ route: str = "",
59
+ db_queries_count: int = 0,
60
+ slow_query_ms: float = 0,
52
61
  ) -> None:
53
62
  payload = {
54
63
  "type": "request_metric",
@@ -61,6 +70,9 @@ class AuditCore:
61
70
  "metadata": {
62
71
  "environment": self.environment,
63
72
  "timestamp": datetime.now(timezone.utc).isoformat(),
73
+ "route": route or url,
74
+ "dbQueriesCount": db_queries_count,
75
+ "slowQueryMs": round(slow_query_ms, 2),
64
76
  },
65
77
  }
66
78
  self._enqueue(payload)
@@ -211,4 +223,62 @@ class AuditCore:
211
223
 
212
224
 
213
225
  # Singleton Global
214
- audit = AuditCore()
226
+ audit = AuditCore()
227
+
228
+
229
+ def audit_operation(name: str, threshold_ms: int = 300):
230
+ """
231
+ Decorador para monitorar funções de banco de dados ou chamadas externas.
232
+ Alimenta o request_context atual com a contagem de queries e lentidão.
233
+ Suporta tanto funções síncronas (def) quanto assíncronas (async def).
234
+ """
235
+ def decorator(func):
236
+ @functools.wraps(func)
237
+ async def async_wrapper(*args, **kwargs):
238
+ ctx = request_context.get()
239
+ if ctx is not None:
240
+ ctx["queriesCount"] = ctx.get("queriesCount", 0) + 1
241
+
242
+ start = time.perf_counter()
243
+ try:
244
+ result = await func(*args, **kwargs)
245
+ duration_ms = (time.perf_counter() - start) * 1000
246
+
247
+ if duration_ms > threshold_ms and ctx is not None:
248
+ ctx["slowQueries"] = max(ctx.get("slowQueries", 0), duration_ms)
249
+ audit.capture_message(
250
+ f"Slow Query detectada em [{name}]",
251
+ {"durationMs": round(duration_ms), "operationName": name}
252
+ )
253
+ return result
254
+ except Exception as e:
255
+ duration_ms = (time.perf_counter() - start) * 1000
256
+ audit.capture_exception(e, {"operationName": name, "durationMs": round(duration_ms)})
257
+ raise
258
+
259
+ @functools.wraps(func)
260
+ def sync_wrapper(*args, **kwargs):
261
+ ctx = request_context.get()
262
+ if ctx is not None:
263
+ ctx["queriesCount"] = ctx.get("queriesCount", 0) + 1
264
+
265
+ start = time.perf_counter()
266
+ try:
267
+ result = func(*args, **kwargs)
268
+ duration_ms = (time.perf_counter() - start) * 1000
269
+
270
+ if duration_ms > threshold_ms and ctx is not None:
271
+ ctx["slowQueries"] = max(ctx.get("slowQueries", 0), duration_ms)
272
+ audit.capture_message(
273
+ f"Slow Query detectada em [{name}]",
274
+ {"durationMs": round(duration_ms), "operationName": name}
275
+ )
276
+ return result
277
+ except Exception as e:
278
+ duration_ms = (time.perf_counter() - start) * 1000
279
+ audit.capture_exception(e, {"operationName": name, "durationMs": round(duration_ms)})
280
+ raise
281
+
282
+ # Retorna o wrapper correto dependendo se a função original é async ou não
283
+ return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
284
+ return decorator
@@ -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.1.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.1.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,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