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 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
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ errivanta