pingoni 0.1.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.
- pingoni-0.1.0/PKG-INFO +22 -0
- pingoni-0.1.0/pingoni/__init__.py +250 -0
- pingoni-0.1.0/pingoni.egg-info/PKG-INFO +22 -0
- pingoni-0.1.0/pingoni.egg-info/SOURCES.txt +7 -0
- pingoni-0.1.0/pingoni.egg-info/dependency_links.txt +1 -0
- pingoni-0.1.0/pingoni.egg-info/requires.txt +1 -0
- pingoni-0.1.0/pingoni.egg-info/top_level.txt +1 -0
- pingoni-0.1.0/pyproject.toml +38 -0
- pingoni-0.1.0/setup.cfg +4 -0
pingoni-0.1.0/PKG-INFO
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pingoni
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pingoni"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Drop-in API monitoring for Flask and FastAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Ayomide", email = "afolaluayomide5@gmail.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["monitoring", "flask", "fastapi", "api", "observability"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Framework :: Flask",
|
|
26
|
+
"Framework :: FastAPI",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"requests>=2.25.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://pingoni.com"
|
|
34
|
+
Repository = "https://github.com/Ayomide780/pingoni-python"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["."]
|
|
38
|
+
include = ["pingoni*"]
|
pingoni-0.1.0/setup.cfg
ADDED