errivanta 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- errivanta/__init__.py +69 -0
- errivanta/client.py +80 -0
- errivanta/middleware.py +79 -0
- errivanta/models.py +17 -0
- errivanta-0.2.0.dist-info/METADATA +17 -0
- errivanta-0.2.0.dist-info/RECORD +8 -0
- errivanta-0.2.0.dist-info/WHEEL +5 -0
- errivanta-0.2.0.dist-info/top_level.txt +1 -0
errivanta/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from errivanta.client import ErrivantaClient, ServiceWatchClient
|
|
3
|
+
from errivanta.middleware import ErrivantaMiddleware, ServiceWatchMiddleware
|
|
4
|
+
from errivanta.models import TelemetryEvent
|
|
5
|
+
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Errivanta",
|
|
9
|
+
"ErrivantaClient",
|
|
10
|
+
"ErrivantaMiddleware",
|
|
11
|
+
"TelemetryEvent",
|
|
12
|
+
"ServiceWatch",
|
|
13
|
+
"ServiceWatchClient",
|
|
14
|
+
"ServiceWatchMiddleware",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Errivanta:
|
|
19
|
+
"""
|
|
20
|
+
Main entry point for integrating Errivanta into a Python/FastAPI service.
|
|
21
|
+
|
|
22
|
+
Example usage:
|
|
23
|
+
```python
|
|
24
|
+
from fastapi import FastAPI
|
|
25
|
+
from errivanta import Errivanta
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
|
|
29
|
+
monitor = Errivanta(
|
|
30
|
+
service_name="payment-service",
|
|
31
|
+
api_key="sw_live_YOUR_KEY",
|
|
32
|
+
monitoring_url="http://localhost:8001"
|
|
33
|
+
)
|
|
34
|
+
monitor.init_app(app)
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
service_name: str,
|
|
41
|
+
api_key: str,
|
|
42
|
+
monitoring_url: str = "http://localhost:8001",
|
|
43
|
+
timeout: float = 2.0,
|
|
44
|
+
skip_paths: Optional[List[str]] = None,
|
|
45
|
+
):
|
|
46
|
+
self.service_name = service_name
|
|
47
|
+
self.api_key = api_key
|
|
48
|
+
self.monitoring_url = monitoring_url.rstrip("/")
|
|
49
|
+
self.skip_paths = skip_paths or []
|
|
50
|
+
self.client = ErrivantaClient(
|
|
51
|
+
api_key=self.api_key,
|
|
52
|
+
monitoring_url=self.monitoring_url,
|
|
53
|
+
timeout_seconds=timeout,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def init_app(self, app) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Attaches the Errivanta monitoring middleware to the provided FastAPI / Starlette app.
|
|
59
|
+
"""
|
|
60
|
+
app.add_middleware(
|
|
61
|
+
ErrivantaMiddleware,
|
|
62
|
+
service_name=self.service_name,
|
|
63
|
+
client=self.client,
|
|
64
|
+
skip_paths=self.skip_paths,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Backward compatibility alias
|
|
69
|
+
ServiceWatch = Errivanta
|
errivanta/client.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import httpx
|
|
4
|
+
from errivanta.models import TelemetryEvent
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger("errivanta")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ErrivantaClient:
|
|
10
|
+
"""
|
|
11
|
+
HTTP client responsible for dispatching telemetry events to the Errivanta Monitoring API.
|
|
12
|
+
Designed with strict timeouts and error-swallowing to ensure customer applications never crash.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_key: str,
|
|
18
|
+
monitoring_url: str = "http://localhost:8001",
|
|
19
|
+
timeout_seconds: float = 2.0,
|
|
20
|
+
):
|
|
21
|
+
self.api_key = api_key
|
|
22
|
+
self.monitoring_url = monitoring_url.rstrip("/")
|
|
23
|
+
self.events_endpoint = f"{self.monitoring_url}/api/v1/events"
|
|
24
|
+
self.timeout = timeout_seconds
|
|
25
|
+
|
|
26
|
+
async def send_event_async(self, event: TelemetryEvent) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Asynchronously sends a telemetry event to the Errivanta API.
|
|
29
|
+
Returns True if successful, False if failed. Never raises exceptions.
|
|
30
|
+
"""
|
|
31
|
+
headers = {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"X-API-Key": self.api_key,
|
|
34
|
+
}
|
|
35
|
+
try:
|
|
36
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
37
|
+
response = await client.post(
|
|
38
|
+
self.events_endpoint,
|
|
39
|
+
json=event.model_dump(),
|
|
40
|
+
headers=headers,
|
|
41
|
+
)
|
|
42
|
+
if response.status_code not in (200, 201):
|
|
43
|
+
logger.warning(
|
|
44
|
+
f"[Errivanta] Failed to deliver telemetry: HTTP {response.status_code} - {response.text}"
|
|
45
|
+
)
|
|
46
|
+
return False
|
|
47
|
+
return True
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
# Graceful degradation: Log a warning and swallow exception so customer app is unaffected
|
|
50
|
+
logger.warning(f"[Errivanta] Telemetry delivery error (gracefully ignored): {exc}")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def send_event_sync(self, event: TelemetryEvent) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Synchronous fallback for sending telemetry events.
|
|
56
|
+
"""
|
|
57
|
+
headers = {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"X-API-Key": self.api_key,
|
|
60
|
+
}
|
|
61
|
+
try:
|
|
62
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
63
|
+
response = client.post(
|
|
64
|
+
self.events_endpoint,
|
|
65
|
+
json=event.model_dump(),
|
|
66
|
+
headers=headers,
|
|
67
|
+
)
|
|
68
|
+
if response.status_code not in (200, 201):
|
|
69
|
+
logger.warning(
|
|
70
|
+
f"[Errivanta] Failed to deliver telemetry: HTTP {response.status_code} - {response.text}"
|
|
71
|
+
)
|
|
72
|
+
return False
|
|
73
|
+
return True
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.warning(f"[Errivanta] Telemetry delivery error (gracefully ignored): {exc}")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Backward compatibility alias
|
|
80
|
+
ServiceWatchClient = ErrivantaClient
|
errivanta/middleware.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
7
|
+
from starlette.requests import Request
|
|
8
|
+
from starlette.responses import Response
|
|
9
|
+
|
|
10
|
+
from errivanta.client import ErrivantaClient
|
|
11
|
+
from errivanta.models import TelemetryEvent
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("errivanta")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ErrivantaMiddleware(BaseHTTPMiddleware):
|
|
17
|
+
"""
|
|
18
|
+
FastAPI / Starlette middleware that intercepts every HTTP request,
|
|
19
|
+
calculates execution latency, extracts response codes/errors,
|
|
20
|
+
and non-blockingly dispatches a TelemetryEvent to the Errivanta Monitoring Platform.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
app,
|
|
26
|
+
service_name: str,
|
|
27
|
+
client: ErrivantaClient,
|
|
28
|
+
skip_paths: Optional[List[str]] = None,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(app)
|
|
31
|
+
self.service_name = service_name
|
|
32
|
+
self.client = client
|
|
33
|
+
self.skip_paths = skip_paths or ["/docs", "/openapi.json", "/health", "/favicon.ico"]
|
|
34
|
+
|
|
35
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
36
|
+
# Check if route is in skip_paths
|
|
37
|
+
path = request.url.path
|
|
38
|
+
if any(path.startswith(skipped) for skipped in self.skip_paths):
|
|
39
|
+
return await call_next(request)
|
|
40
|
+
|
|
41
|
+
start_time = time.perf_counter()
|
|
42
|
+
status_code = 500
|
|
43
|
+
error_message = None
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
response = await call_next(request)
|
|
47
|
+
status_code = response.status_code
|
|
48
|
+
return response
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
error_message = str(exc)
|
|
51
|
+
raise exc
|
|
52
|
+
finally:
|
|
53
|
+
latency_ms = (time.perf_counter() - start_time) * 1000.0
|
|
54
|
+
|
|
55
|
+
event = TelemetryEvent(
|
|
56
|
+
service_name=self.service_name,
|
|
57
|
+
endpoint=path,
|
|
58
|
+
http_method=request.method,
|
|
59
|
+
status_code=status_code,
|
|
60
|
+
latency_ms=round(latency_ms, 2),
|
|
61
|
+
timestamp=datetime.now(timezone.utc),
|
|
62
|
+
error_message=error_message,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Fire-and-forget asynchronous dispatch
|
|
66
|
+
asyncio.create_task(self._safe_dispatch(event))
|
|
67
|
+
|
|
68
|
+
async def _safe_dispatch(self, event: TelemetryEvent) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Background dispatch task with error catching to ensure total isolation from customer requests.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
await self.client.send_event_async(event)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.debug(f"[Errivanta] Non-blocking telemetry background dispatch error: {exc}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Backward compatibility alias
|
|
79
|
+
ServiceWatchMiddleware = ErrivantaMiddleware
|
errivanta/models.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Dict, Optional, Any
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelemetryEvent(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Schema for telemetry event payload dispatched by Errivanta SDK.
|
|
9
|
+
"""
|
|
10
|
+
service_name: str
|
|
11
|
+
endpoint: str
|
|
12
|
+
http_method: str
|
|
13
|
+
status_code: int
|
|
14
|
+
latency_ms: float
|
|
15
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
16
|
+
error_message: Optional[str] = None
|
|
17
|
+
extra_metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: errivanta
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK & FastAPI Middleware for Errivanta APM & Observability Platform
|
|
5
|
+
Home-page: https://github.com/codesbyaditya/errivanta
|
|
6
|
+
Author: Errivanta Team
|
|
7
|
+
Author-email: errivanta@gmail.com
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0.0
|
|
11
|
+
Requires-Dist: starlette>=0.36.0
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: home-page
|
|
15
|
+
Dynamic: requires-dist
|
|
16
|
+
Dynamic: requires-python
|
|
17
|
+
Dynamic: summary
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
errivanta/__init__.py,sha256=CE1HKYLQM4jrcKEtjVqOXBuu3rg6poadmtX8iEO0wKI,1810
|
|
2
|
+
errivanta/client.py,sha256=YktL7ADqDZQgwOC6BVwk_VvGWAhNMyN63cVllH5kM4M,2946
|
|
3
|
+
errivanta/middleware.py,sha256=msFXwqbbN0ItO156SBNMRfXEURCNWnjiRk1WKf6zNHE,2651
|
|
4
|
+
errivanta/models.py,sha256=acVwI6llJdLuwXWMNzU52TqKbWFHmBKYXl8uU8JNL8Y,529
|
|
5
|
+
errivanta-0.2.0.dist-info/METADATA,sha256=JK02fSnRMGA-PRkiKy-nJKo7rfCc78RQlbvlNFfubH0,512
|
|
6
|
+
errivanta-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
errivanta-0.2.0.dist-info/top_level.txt,sha256=kPOqd27S_X26o0X2y7ajb0NKUYITgicazctKVCcWvY4,10
|
|
8
|
+
errivanta-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
errivanta
|