pingoni 0.1.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.
pingoni/__init__.py ADDED
@@ -0,0 +1,250 @@
1
+ """
2
+ Pingoni — Drop-in API monitoring for Flask and FastAPI.
3
+
4
+ Usage with Flask:
5
+ from flask import Flask
6
+ from pingoni import init_flask
7
+
8
+ app = Flask(__name__)
9
+ init_flask(app, api_key="your_api_key")
10
+
11
+ Usage with FastAPI:
12
+ from fastapi import FastAPI
13
+ from pingoni import init_fastapi
14
+
15
+ app = FastAPI()
16
+ init_fastapi(app, api_key="your_api_key")
17
+ """
18
+
19
+ import json
20
+ import time
21
+ import threading
22
+ import traceback
23
+ from urllib.request import Request, urlopen
24
+ from urllib.error import URLError
25
+
26
+ __version__ = "0.1.0"
27
+
28
+ PINGONI_INGEST_URL = "https://apiwatch-production-3f19.up.railway.app/ingest"
29
+ PINGONI_LLM_URL = "https://apiwatch-production-3f19.up.railway.app/api/llm-events"
30
+
31
+ SENSITIVE_HEADERS = {
32
+ "authorization", "cookie", "set-cookie", "x-api-key",
33
+ "x-auth-token", "proxy-authorization"
34
+ }
35
+
36
+
37
+ def _build_safe_headers(headers):
38
+ """Strip sensitive headers, truncate long values."""
39
+ safe = {}
40
+ try:
41
+ for key, value in dict(headers or {}).items():
42
+ lower = str(key).lower()
43
+ if lower in SENSITIVE_HEADERS:
44
+ safe[key] = "[REDACTED]"
45
+ else:
46
+ v = str(value)
47
+ safe[key] = v[:500] + "..." if len(v) > 500 else v
48
+ except Exception:
49
+ pass
50
+ return safe
51
+
52
+
53
+ def _send_log(api_key, payload):
54
+ """Send a log to Pingoni in a background thread. Never blocks or raises."""
55
+ def _do_send():
56
+ try:
57
+ clean = {k: v for k, v in payload.items() if v is not None}
58
+ data = json.dumps(clean).encode("utf-8")
59
+ req = Request(
60
+ PINGONI_INGEST_URL,
61
+ data=data,
62
+ headers={
63
+ "Content-Type": "application/json",
64
+ "x-api-key": api_key,
65
+ },
66
+ method="POST",
67
+ )
68
+ urlopen(req, timeout=5).read()
69
+ except Exception:
70
+ pass # never crash the user's app
71
+
72
+ t = threading.Thread(target=_do_send, daemon=False)
73
+ t.start()
74
+
75
+
76
+ def init_flask(app, api_key):
77
+ """
78
+ Add Pingoni monitoring to a Flask app.
79
+
80
+ Args:
81
+ app: The Flask application instance
82
+ api_key: Your Pingoni project API key
83
+ """
84
+ if not api_key:
85
+ print("[Pingoni] No API key provided. Monitoring disabled.")
86
+ return
87
+
88
+ from flask import request, g
89
+
90
+ @app.before_request
91
+ def _pingoni_before():
92
+ g._pingoni_start = time.time()
93
+
94
+ @app.after_request
95
+ def _pingoni_after(response):
96
+ try:
97
+ if getattr(g, "_pingoni_sent", False):
98
+ return response
99
+ duration = int((time.time() - g._pingoni_start) * 1000)
100
+ error_message = None
101
+ if response.status_code >= 500:
102
+ error_message = f"Server error ({response.status_code})"
103
+ elif response.status_code >= 400:
104
+ error_message = f"Client error ({response.status_code})"
105
+
106
+ _send_log(api_key, {
107
+ "method": request.method,
108
+ "endpoint": request.path,
109
+ "status_code": response.status_code,
110
+ "response_time": duration,
111
+ "error_message": error_message,
112
+ "headers": _build_safe_headers(request.headers),
113
+ "stack_trace": None,
114
+ })
115
+ except Exception:
116
+ pass
117
+ return response
118
+
119
+ @app.errorhandler(Exception)
120
+ def _pingoni_error(err):
121
+ try:
122
+ g._pingoni_sent = True
123
+ duration = int((time.time() - getattr(g, '_pingoni_start', time.time())) * 1000)
124
+ status = getattr(err, 'code', 500) or 500
125
+
126
+ _send_log(api_key, {
127
+ "method": request.method,
128
+ "endpoint": request.path,
129
+ "status_code": status,
130
+ "response_time": duration,
131
+ "error_message": str(err) or "Internal Server Error",
132
+ "headers": _build_safe_headers(request.headers),
133
+ "stack_trace": traceback.format_exc(),
134
+ })
135
+ except Exception:
136
+ pass
137
+
138
+ from flask import jsonify
139
+ return jsonify({"error": str(err) or "Internal Server Error"}), getattr(err, 'code', 500) or 500
140
+
141
+
142
+ def init_fastapi(app, api_key):
143
+ """
144
+ Add Pingoni monitoring to a FastAPI app.
145
+
146
+ Args:
147
+ app: The FastAPI application instance
148
+ api_key: Your Pingoni project API key
149
+ """
150
+ if not api_key:
151
+ print("[Pingoni] No API key provided. Monitoring disabled.")
152
+ return
153
+
154
+ from starlette.middleware.base import BaseHTTPMiddleware
155
+ from starlette.responses import JSONResponse
156
+
157
+ class PingoniMiddleware(BaseHTTPMiddleware):
158
+ async def dispatch(self, request, call_next):
159
+ start = time.time()
160
+ try:
161
+ response = await call_next(request)
162
+ duration = int((time.time() - start) * 1000)
163
+ error_message = None
164
+ if response.status_code >= 500:
165
+ error_message = f"Server error ({response.status_code})"
166
+ elif response.status_code >= 400:
167
+ error_message = f"Client error ({response.status_code})"
168
+
169
+ _send_log(api_key, {
170
+ "method": request.method,
171
+ "endpoint": str(request.url.path),
172
+ "status_code": response.status_code,
173
+ "response_time": duration,
174
+ "error_message": error_message,
175
+ "headers": _build_safe_headers(request.headers),
176
+ "stack_trace": None,
177
+ })
178
+ return response
179
+ except Exception as err:
180
+ duration = int((time.time() - start) * 1000)
181
+ _send_log(api_key, {
182
+ "method": request.method,
183
+ "endpoint": str(request.url.path),
184
+ "status_code": 500,
185
+ "response_time": duration,
186
+ "error_message": str(err) or "Internal Server Error",
187
+ "headers": _build_safe_headers(request.headers),
188
+ "stack_trace": traceback.format_exc(),
189
+ })
190
+ return JSONResponse(
191
+ status_code=500,
192
+ content={"error": str(err) or "Internal Server Error"}
193
+ )
194
+
195
+ app.add_middleware(PingoniMiddleware)
196
+
197
+
198
+ def track_llm(provider, model, usage, user_id=None, feature=None, metadata=None, api_key=None):
199
+ """
200
+ Track an LLM API call for cost monitoring.
201
+
202
+ Args:
203
+ provider: LLM provider name (e.g., "openai", "anthropic")
204
+ model: Model name (e.g., "gpt-4o-mini")
205
+ usage: Usage dict with prompt_tokens, completion_tokens, total_tokens
206
+ user_id: Optional user identifier for per-user cost attribution
207
+ feature: Optional feature name (e.g., "summarization")
208
+ metadata: Optional dict of additional metadata
209
+ api_key: Pingoni API key (or set PINGONI_API_KEY env var)
210
+ """
211
+ import os
212
+ if api_key is None:
213
+ api_key = os.environ.get("PINGONI_API_KEY")
214
+
215
+ if not api_key:
216
+ print("[Pingoni] No API key for track_llm. Pass api_key= or set PINGONI_API_KEY env var.")
217
+ return
218
+
219
+ if not provider or not model or not usage:
220
+ print("[Pingoni] track_llm requires provider, model, and usage.")
221
+ return
222
+
223
+ def _do_send():
224
+ try:
225
+ payload = json.dumps({
226
+ "provider": provider,
227
+ "model": model,
228
+ "usage": usage,
229
+ "user_id": user_id,
230
+ "feature": feature,
231
+ "metadata": metadata,
232
+ }).encode("utf-8")
233
+ req = Request(
234
+ PINGONI_LLM_URL,
235
+ data=payload,
236
+ headers={
237
+ "Content-Type": "application/json",
238
+ "x-api-key": api_key,
239
+ },
240
+ method="POST",
241
+ )
242
+ urlopen(req, timeout=5).read()
243
+ except Exception:
244
+ pass
245
+
246
+ threading.Thread(target=_do_send, daemon=True).start()
247
+
248
+
249
+ # Aliases for clarity
250
+ trackLLM = track_llm
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: pingoni
3
+ Version: 0.1.0
4
+ Summary: Drop-in API monitoring for Flask and FastAPI
5
+ Author-email: Ayomide <afolaluayomide5@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pingoni.com
8
+ Project-URL: Repository, https://github.com/Ayomide780/pingoni-python
9
+ Keywords: monitoring,flask,fastapi,api,observability
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Framework :: Flask
19
+ Classifier: Framework :: FastAPI
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.25.0
@@ -0,0 +1,5 @@
1
+ pingoni/__init__.py,sha256=bR91_7pasCP6A8oWb618nHrU6ESw2KGegOXqLXWso_I,8529
2
+ pingoni-0.1.0.dist-info/METADATA,sha256=qNYhx70QFz9b95QhxXoQOZ0Tim8a_u2rNmF4EGwEnus,914
3
+ pingoni-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
4
+ pingoni-0.1.0.dist-info/top_level.txt,sha256=lOlWbyjmjPX3TgqWbpvfmW9iOKSjHcAYB9jAiMd1gZQ,8
5
+ pingoni-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pingoni