errivanta 0.2.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,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,48 @@
1
+ # Errivanta Python SDK (`errivanta`)
2
+
3
+ Official Python SDK and non-blocking FastAPI middleware for the **Errivanta Observability & Monitoring Platform**.
4
+
5
+ ---
6
+
7
+ ## 📦 Installation
8
+
9
+ ```bash
10
+ pip install errivanta
11
+ ```
12
+
13
+ ---
14
+
15
+ ## ⚡ Quickstart
16
+
17
+ Integrate Errivanta into any FastAPI or Starlette application in under 30 seconds:
18
+
19
+ ```python
20
+ from fastapi import FastAPI
21
+ from errivanta import Errivanta
22
+
23
+ app = FastAPI(title="My Microservice")
24
+
25
+ # Initialize Errivanta monitoring
26
+ monitor = Errivanta(
27
+ service_name="payment-service",
28
+ api_key="your_organization_api_key",
29
+ monitoring_url="https://your-errivanta-api.onrender.com"
30
+ )
31
+ monitor.init_app(app)
32
+
33
+ @app.get("/health")
34
+ def health():
35
+ return {"status": "ok"}
36
+
37
+ @app.post("/payments/process")
38
+ def process_payment():
39
+ return {"status": "success", "amount": 100}
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 🛡️ Non-Blocking & Fault Tolerant
45
+
46
+ - **Zero-Latency Overhead**: Telemetry events are dispatched in asynchronous fire-and-forget background tasks.
47
+ - **Fail-Safe**: If the monitoring server is unreachable or times out, exceptions are safely swallowed so your application never crashes or slows down.
48
+ - **Selective Route Filtering**: Automatically skips `/health`, `/docs`, and `/openapi.json` to keep metric logs clean.
@@ -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
@@ -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
@@ -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,12 @@
1
+ README.md
2
+ setup.py
3
+ errivanta/__init__.py
4
+ errivanta/client.py
5
+ errivanta/middleware.py
6
+ errivanta/models.py
7
+ errivanta.egg-info/PKG-INFO
8
+ errivanta.egg-info/SOURCES.txt
9
+ errivanta.egg-info/dependency_links.txt
10
+ errivanta.egg-info/requires.txt
11
+ errivanta.egg-info/top_level.txt
12
+ tests/test_sdk.py
@@ -0,0 +1,3 @@
1
+ httpx>=0.27.0
2
+ pydantic>=2.0.0
3
+ starlette>=0.36.0
@@ -0,0 +1 @@
1
+ errivanta
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="errivanta",
5
+ version="0.2.0",
6
+ description="Official Python SDK & FastAPI Middleware for Errivanta APM & Observability Platform",
7
+ author="Errivanta Team",
8
+ author_email="errivanta@gmail.com",
9
+ url="https://github.com/codesbyaditya/errivanta",
10
+ packages=find_packages(),
11
+ install_requires=[
12
+ "httpx>=0.27.0",
13
+ "pydantic>=2.0.0",
14
+ "starlette>=0.36.0",
15
+ ],
16
+ python_requires=">=3.9",
17
+ )
@@ -0,0 +1,80 @@
1
+ import pytest
2
+ from fastapi import FastAPI
3
+ from fastapi.testclient import TestClient
4
+ from errivanta import Errivanta, ServiceWatch
5
+ from errivanta.client import ErrivantaClient
6
+ from errivanta.models import TelemetryEvent
7
+
8
+
9
+ def test_telemetry_event_schema():
10
+ event = TelemetryEvent(
11
+ service_name="test-service",
12
+ endpoint="/api/v1/test",
13
+ http_method="GET",
14
+ status_code=200,
15
+ latency_ms=12.5,
16
+ )
17
+ assert event.service_name == "test-service"
18
+ assert event.status_code == 200
19
+ assert event.latency_ms == 12.5
20
+ assert event.error_message is None
21
+
22
+
23
+ def test_errivanta_initialization():
24
+ monitor = Errivanta(
25
+ service_name="payment-service",
26
+ api_key="test_key_123",
27
+ monitoring_url="http://localhost:8001",
28
+ timeout=1.5,
29
+ )
30
+ assert monitor.service_name == "payment-service"
31
+ assert monitor.api_key == "test_key_123"
32
+ assert monitor.monitoring_url == "http://localhost:8001"
33
+ assert isinstance(monitor.client, ErrivantaClient)
34
+
35
+
36
+ def test_backward_compatibility_alias():
37
+ monitor = ServiceWatch(
38
+ service_name="order-service",
39
+ api_key="test_key_456",
40
+ monitoring_url="http://localhost:8001",
41
+ )
42
+ assert monitor.service_name == "order-service"
43
+ assert isinstance(monitor, Errivanta)
44
+
45
+
46
+ def test_sdk_middleware_interception():
47
+ app = FastAPI()
48
+ monitor = Errivanta(
49
+ service_name="test-service",
50
+ api_key="test_key",
51
+ monitoring_url="http://localhost:8001",
52
+ )
53
+ monitor.init_app(app)
54
+
55
+ @app.get("/items")
56
+ def get_items():
57
+ return {"items": [1, 2, 3]}
58
+
59
+ client = TestClient(app)
60
+ response = client.get("/items")
61
+ assert response.status_code == 200
62
+ assert response.json() == {"items": [1, 2, 3]}
63
+
64
+
65
+ def test_sdk_error_resilience():
66
+ # Verify that failed telemetry delivery swallows exceptions and returns False
67
+ client = ErrivantaClient(
68
+ api_key="test_key",
69
+ monitoring_url="http://invalid-host-unreachable.example.com",
70
+ timeout_seconds=0.1,
71
+ )
72
+ event = TelemetryEvent(
73
+ service_name="test-service",
74
+ endpoint="/error",
75
+ http_method="GET",
76
+ status_code=500,
77
+ latency_ms=50.0,
78
+ )
79
+ result = client.send_event_sync(event)
80
+ assert result is False