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.
- errivanta-0.2.0/PKG-INFO +17 -0
- errivanta-0.2.0/README.md +48 -0
- errivanta-0.2.0/errivanta/__init__.py +69 -0
- errivanta-0.2.0/errivanta/client.py +80 -0
- errivanta-0.2.0/errivanta/middleware.py +79 -0
- errivanta-0.2.0/errivanta/models.py +17 -0
- errivanta-0.2.0/errivanta.egg-info/PKG-INFO +17 -0
- errivanta-0.2.0/errivanta.egg-info/SOURCES.txt +12 -0
- errivanta-0.2.0/errivanta.egg-info/dependency_links.txt +1 -0
- errivanta-0.2.0/errivanta.egg-info/requires.txt +3 -0
- errivanta-0.2.0/errivanta.egg-info/top_level.txt +1 -0
- errivanta-0.2.0/setup.cfg +4 -0
- errivanta-0.2.0/setup.py +17 -0
- errivanta-0.2.0/tests/test_sdk.py +80 -0
errivanta-0.2.0/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
errivanta
|
errivanta-0.2.0/setup.py
ADDED
|
@@ -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
|