devinspector 2.0.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.0.0/PKG-INFO +15 -0
- devinspector-2.0.0/README.md +0 -0
- devinspector-2.0.0/devinspector/__init__.py +3 -0
- devinspector-2.0.0/devinspector/core.py +208 -0
- devinspector-2.0.0/devinspector/fastapi.py +35 -0
- devinspector-2.0.0/devinspector/flask.py +51 -0
- devinspector-2.0.0/devinspector.egg-info/PKG-INFO +15 -0
- devinspector-2.0.0/devinspector.egg-info/SOURCES.txt +11 -0
- devinspector-2.0.0/devinspector.egg-info/dependency_links.txt +1 -0
- devinspector-2.0.0/devinspector.egg-info/requires.txt +7 -0
- devinspector-2.0.0/devinspector.egg-info/top_level.txt +1 -0
- devinspector-2.0.0/pyproject.toml +23 -0
- devinspector-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devinspector
|
|
3
|
+
Version: 2.0.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,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"
|
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
"method": method,
|
|
56
|
+
"url": url,
|
|
57
|
+
"statusCode": status_code,
|
|
58
|
+
"durationMs": round(duration_ms, 2),
|
|
59
|
+
"browser": user_agent,
|
|
60
|
+
"metadata": {
|
|
61
|
+
"environment": self.environment,
|
|
62
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
self._enqueue(payload)
|
|
66
|
+
|
|
67
|
+
def capture_error(
|
|
68
|
+
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
69
|
+
) -> None:
|
|
70
|
+
raw_message = str(error) or "Erro Desconhecido"
|
|
71
|
+
raw_stack = "".join(traceback.format_tb(error.__traceback__))
|
|
72
|
+
|
|
73
|
+
meta = metadata.copy() if metadata else {}
|
|
74
|
+
meta.update(
|
|
75
|
+
{
|
|
76
|
+
"environment": self.environment,
|
|
77
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
payload = {
|
|
82
|
+
"type": "error",
|
|
83
|
+
"message": self._truncate(raw_message, 500),
|
|
84
|
+
"stackTrace": self._truncate(raw_stack, 10000),
|
|
85
|
+
"url": "",
|
|
86
|
+
"browser": f"Python/{sys.version.split()[0]}",
|
|
87
|
+
"metadata": self._truncate(meta, 5000),
|
|
88
|
+
}
|
|
89
|
+
self._enqueue(payload)
|
|
90
|
+
|
|
91
|
+
def capture_exception(
|
|
92
|
+
self, error: Exception, metadata: Optional[Dict[str, Any]] = None
|
|
93
|
+
) -> None:
|
|
94
|
+
self.capture_error(error, metadata)
|
|
95
|
+
|
|
96
|
+
def capture_message(
|
|
97
|
+
self, message: str, metadata: Optional[Dict[str, Any]] = None
|
|
98
|
+
) -> None:
|
|
99
|
+
meta = metadata.copy() if metadata else {}
|
|
100
|
+
meta.update(
|
|
101
|
+
{
|
|
102
|
+
"level": "info",
|
|
103
|
+
"environment": self.environment,
|
|
104
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
payload = {
|
|
109
|
+
"type": "message",
|
|
110
|
+
"message": self._truncate(message, 500),
|
|
111
|
+
"stackTrace": "",
|
|
112
|
+
"url": "",
|
|
113
|
+
"browser": f"Python/{sys.version.split()[0]}",
|
|
114
|
+
"metadata": self._truncate(meta, 5000),
|
|
115
|
+
}
|
|
116
|
+
self._enqueue(payload)
|
|
117
|
+
|
|
118
|
+
def _listen_global_errors(self) -> None:
|
|
119
|
+
if self._listeners_attached:
|
|
120
|
+
return
|
|
121
|
+
self._listeners_attached = True
|
|
122
|
+
|
|
123
|
+
original_excepthook = sys.excepthook
|
|
124
|
+
|
|
125
|
+
def custom_excepthook(exc_type, exc_value, exc_traceback):
|
|
126
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
127
|
+
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
self.capture_error(
|
|
131
|
+
exc_value,
|
|
132
|
+
metadata={"type": "uncaught_exception", "exc_type": exc_type.__name__},
|
|
133
|
+
)
|
|
134
|
+
original_excepthook(exc_type, exc_value, exc_traceback)
|
|
135
|
+
|
|
136
|
+
sys.excepthook = custom_excepthook
|
|
137
|
+
|
|
138
|
+
# Captura exceções em Threads secundárias (Python 3.8+)
|
|
139
|
+
if hasattr(threading, "excepthook"):
|
|
140
|
+
|
|
141
|
+
def custom_thread_excepthook(args):
|
|
142
|
+
self.capture_error(
|
|
143
|
+
args.exc_value,
|
|
144
|
+
metadata={
|
|
145
|
+
"type": "uncaught_thread_exception",
|
|
146
|
+
"thread": args.thread.name,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
threading.excepthook = custom_thread_excepthook
|
|
151
|
+
|
|
152
|
+
def _truncate(self, value: Any, max_length: int = 5000) -> Any:
|
|
153
|
+
if isinstance(value, str):
|
|
154
|
+
return (
|
|
155
|
+
value[:max_length] + "... [truncated]"
|
|
156
|
+
if len(value) > max_length
|
|
157
|
+
else value
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if isinstance(value, dict):
|
|
161
|
+
return {
|
|
162
|
+
str(k): self._truncate(v, max_length) for k, v in value.items()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
def _enqueue(self, payload: dict) -> None:
|
|
168
|
+
try:
|
|
169
|
+
self._queue.put_nowait(payload)
|
|
170
|
+
except queue.Full:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def _worker(self) -> None:
|
|
174
|
+
while not self._shutdown_event.is_set():
|
|
175
|
+
try:
|
|
176
|
+
payload = self._queue.get(timeout=0.5)
|
|
177
|
+
self._send(payload)
|
|
178
|
+
self._queue.task_done()
|
|
179
|
+
except queue.Empty:
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
def _send(self, payload: dict) -> None:
|
|
183
|
+
active_key = (
|
|
184
|
+
self.api_key
|
|
185
|
+
if self.api_key and self.api_key != "PENDING_API_KEY"
|
|
186
|
+
else "dev-fallback-key"
|
|
187
|
+
)
|
|
188
|
+
data_bytes = json.dumps(payload).encode("utf-8")
|
|
189
|
+
|
|
190
|
+
req = urllib.request.Request(
|
|
191
|
+
self.endpoint,
|
|
192
|
+
data=data_bytes,
|
|
193
|
+
headers={
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"x-api-key": active_key,
|
|
196
|
+
},
|
|
197
|
+
method="POST",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
with urllib.request.urlopen(req, timeout=5):
|
|
202
|
+
pass
|
|
203
|
+
except Exception as err:
|
|
204
|
+
print(f"[DevInspector] Falha ao enviar requisição para o painel: {err}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Singleton Global
|
|
208
|
+
audit = AuditCore()
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
audit.capture_error(
|
|
18
|
+
exc,
|
|
19
|
+
metadata={
|
|
20
|
+
"path": request.url.path,
|
|
21
|
+
"method": request.method,
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
raise exc
|
|
25
|
+
finally:
|
|
26
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
27
|
+
user_agent = request.headers.get("user-agent", "")
|
|
28
|
+
|
|
29
|
+
audit.capture_request(
|
|
30
|
+
method=request.method,
|
|
31
|
+
url=str(request.url),
|
|
32
|
+
status_code=status_code,
|
|
33
|
+
duration_ms=duration_ms,
|
|
34
|
+
user_agent=user_agent,
|
|
35
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
audit.capture_error(
|
|
26
|
+
exception,
|
|
27
|
+
metadata={
|
|
28
|
+
"path": request.path,
|
|
29
|
+
"method": request.method,
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
# Garante que métrica de erro (500) seja registrada caso falhe antes do after_request
|
|
33
|
+
if not getattr(g, "_devinspector_metric_recorded", False):
|
|
34
|
+
self._record_metric(500)
|
|
35
|
+
|
|
36
|
+
def _record_metric(self, status_code: int):
|
|
37
|
+
start_time = getattr(g, "_devinspector_start_time", None)
|
|
38
|
+
if start_time is None:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
42
|
+
user_agent = request.headers.get("User-Agent", "")
|
|
43
|
+
|
|
44
|
+
audit.capture_request(
|
|
45
|
+
method=request.method,
|
|
46
|
+
url=request.url,
|
|
47
|
+
status_code=status_code,
|
|
48
|
+
duration_ms=duration_ms,
|
|
49
|
+
user_agent=user_agent,
|
|
50
|
+
)
|
|
51
|
+
g._devinspector_metric_recorded = True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devinspector
|
|
3
|
+
Version: 2.0.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,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"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
devinspector/__init__.py
|
|
4
|
+
devinspector/core.py
|
|
5
|
+
devinspector/fastapi.py
|
|
6
|
+
devinspector/flask.py
|
|
7
|
+
devinspector.egg-info/PKG-INFO
|
|
8
|
+
devinspector.egg-info/SOURCES.txt
|
|
9
|
+
devinspector.egg-info/dependency_links.txt
|
|
10
|
+
devinspector.egg-info/requires.txt
|
|
11
|
+
devinspector.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devinspector
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "devinspector"
|
|
7
|
+
version = "2.0.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", "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"
|