wsbuilder 0.2.7__tar.gz → 0.3.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.
- {wsbuilder-0.2.7/src/wsbuilder.egg-info → wsbuilder-0.3.0}/PKG-INFO +49 -1
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/README.md +48 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/pyproject.toml +1 -1
- wsbuilder-0.3.0/src/wsbuilder/__init__.py +65 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/app.py +16 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/framework.py +3 -1
- wsbuilder-0.3.0/src/wsbuilder/metrics.py +264 -0
- wsbuilder-0.3.0/src/wsbuilder/orm.py +849 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/server.py +93 -11
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/ws_demo.py +248 -66
- {wsbuilder-0.2.7 → wsbuilder-0.3.0/src/wsbuilder.egg-info}/PKG-INFO +49 -1
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder.egg-info/SOURCES.txt +5 -1
- wsbuilder-0.3.0/tests/test_metrics.py +87 -0
- wsbuilder-0.3.0/tests/test_orm.py +102 -0
- wsbuilder-0.2.7/src/wsbuilder/__init__.py +0 -26
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/LICENSE +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/setup.cfg +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/__main__.py +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/constants.py +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/http.py +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder/ws.py +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder.egg-info/dependency_links.txt +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder.egg-info/entry_points.txt +0 -0
- {wsbuilder-0.2.7 → wsbuilder-0.3.0}/src/wsbuilder.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wsbuilder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Framework ligero HTTP + WebSocket.
|
|
5
5
|
Author: wsbuilder Authors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,7 @@ Lightweight Python HTTP + WebSocket framework for building real-time APIs and cu
|
|
|
25
25
|
## Incluye
|
|
26
26
|
|
|
27
27
|
- `wsbuilder.framework`: router, request/response, servidor HTTP, handshake WS y utilidades de frames.
|
|
28
|
+
- `wsbuilder.orm`: ORM flexible para SQLite3 (modelos, QuerySet, filtros, transacciones anidadas).
|
|
28
29
|
- `wsbuilder.ws_demo`: demo completo HTTP + WebSocket + REST + SQLite.
|
|
29
30
|
|
|
30
31
|
## Instalacion local
|
|
@@ -73,6 +74,53 @@ Para CORS sin variables de entorno:
|
|
|
73
74
|
app = App(cors_allow_origin="*")
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
## ORM SQLite3 rapido
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from wsbuilder import Database, IntegerField, Model, TextField
|
|
81
|
+
|
|
82
|
+
class User(Model):
|
|
83
|
+
id = IntegerField(primary_key=True, auto_increment=True)
|
|
84
|
+
username = TextField(unique=True, index=True, null=False)
|
|
85
|
+
email = TextField(null=False)
|
|
86
|
+
|
|
87
|
+
db = Database("app.db")
|
|
88
|
+
User.create_table(db)
|
|
89
|
+
|
|
90
|
+
u = User(username="alice", email="alice@example.com")
|
|
91
|
+
u.save(db)
|
|
92
|
+
|
|
93
|
+
admins = User.objects(db).filter(username__contains="ali").order_by("-id").all()
|
|
94
|
+
print([x.to_dict() for x in admins])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Metrics (JSON stream)
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from wsbuilder import App
|
|
101
|
+
|
|
102
|
+
app = App()
|
|
103
|
+
app.enable_metrics() # crea /api/metrics y /api/metrics/stream
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Probar snapshot:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
curl http://127.0.0.1:8765/api/metrics
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Probar stream NDJSON:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -N "http://127.0.0.1:8765/api/metrics/stream?interval=1&limit=5"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Modo continuo (no termina solo):
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
curl -N "http://127.0.0.1:8765/api/metrics/stream?interval=1&follow=1"
|
|
122
|
+
```
|
|
123
|
+
|
|
76
124
|
## HTTP streaming (chunked)
|
|
77
125
|
|
|
78
126
|
```python
|
|
@@ -9,6 +9,7 @@ Lightweight Python HTTP + WebSocket framework for building real-time APIs and cu
|
|
|
9
9
|
## Incluye
|
|
10
10
|
|
|
11
11
|
- `wsbuilder.framework`: router, request/response, servidor HTTP, handshake WS y utilidades de frames.
|
|
12
|
+
- `wsbuilder.orm`: ORM flexible para SQLite3 (modelos, QuerySet, filtros, transacciones anidadas).
|
|
12
13
|
- `wsbuilder.ws_demo`: demo completo HTTP + WebSocket + REST + SQLite.
|
|
13
14
|
|
|
14
15
|
## Instalacion local
|
|
@@ -57,6 +58,53 @@ Para CORS sin variables de entorno:
|
|
|
57
58
|
app = App(cors_allow_origin="*")
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
## ORM SQLite3 rapido
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from wsbuilder import Database, IntegerField, Model, TextField
|
|
65
|
+
|
|
66
|
+
class User(Model):
|
|
67
|
+
id = IntegerField(primary_key=True, auto_increment=True)
|
|
68
|
+
username = TextField(unique=True, index=True, null=False)
|
|
69
|
+
email = TextField(null=False)
|
|
70
|
+
|
|
71
|
+
db = Database("app.db")
|
|
72
|
+
User.create_table(db)
|
|
73
|
+
|
|
74
|
+
u = User(username="alice", email="alice@example.com")
|
|
75
|
+
u.save(db)
|
|
76
|
+
|
|
77
|
+
admins = User.objects(db).filter(username__contains="ali").order_by("-id").all()
|
|
78
|
+
print([x.to_dict() for x in admins])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Metrics (JSON stream)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from wsbuilder import App
|
|
85
|
+
|
|
86
|
+
app = App()
|
|
87
|
+
app.enable_metrics() # crea /api/metrics y /api/metrics/stream
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Probar snapshot:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl http://127.0.0.1:8765/api/metrics
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Probar stream NDJSON:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -N "http://127.0.0.1:8765/api/metrics/stream?interval=1&limit=5"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Modo continuo (no termina solo):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
curl -N "http://127.0.0.1:8765/api/metrics/stream?interval=1&follow=1"
|
|
106
|
+
```
|
|
107
|
+
|
|
60
108
|
## HTTP streaming (chunked)
|
|
61
109
|
|
|
62
110
|
```python
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""wsbuilder: mini framework HTTP + WebSocket."""
|
|
2
|
+
|
|
3
|
+
from .framework import (
|
|
4
|
+
App,
|
|
5
|
+
HTTPServer,
|
|
6
|
+
Request,
|
|
7
|
+
Response,
|
|
8
|
+
Route,
|
|
9
|
+
Router,
|
|
10
|
+
WebSocket,
|
|
11
|
+
parse_close_payload,
|
|
12
|
+
parse_query_string,
|
|
13
|
+
)
|
|
14
|
+
from .metrics import AppMetrics, install_metrics
|
|
15
|
+
from .orm import (
|
|
16
|
+
BlobField,
|
|
17
|
+
BooleanField,
|
|
18
|
+
Database,
|
|
19
|
+
DateTimeField,
|
|
20
|
+
Field,
|
|
21
|
+
IntegerField,
|
|
22
|
+
JSONField,
|
|
23
|
+
Model,
|
|
24
|
+
QuerySet,
|
|
25
|
+
RealField,
|
|
26
|
+
SQL,
|
|
27
|
+
TextField,
|
|
28
|
+
Transaction,
|
|
29
|
+
create_tables,
|
|
30
|
+
drop_tables,
|
|
31
|
+
quote_identifier,
|
|
32
|
+
validate_identifier,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__version__ = "0.3.0"
|
|
36
|
+
__all__ = [
|
|
37
|
+
"App",
|
|
38
|
+
"HTTPServer",
|
|
39
|
+
"Request",
|
|
40
|
+
"Response",
|
|
41
|
+
"Route",
|
|
42
|
+
"Router",
|
|
43
|
+
"WebSocket",
|
|
44
|
+
"AppMetrics",
|
|
45
|
+
"install_metrics",
|
|
46
|
+
"Database",
|
|
47
|
+
"Model",
|
|
48
|
+
"QuerySet",
|
|
49
|
+
"Transaction",
|
|
50
|
+
"Field",
|
|
51
|
+
"IntegerField",
|
|
52
|
+
"TextField",
|
|
53
|
+
"RealField",
|
|
54
|
+
"BlobField",
|
|
55
|
+
"BooleanField",
|
|
56
|
+
"DateTimeField",
|
|
57
|
+
"JSONField",
|
|
58
|
+
"SQL",
|
|
59
|
+
"create_tables",
|
|
60
|
+
"drop_tables",
|
|
61
|
+
"quote_identifier",
|
|
62
|
+
"validate_identifier",
|
|
63
|
+
"parse_close_payload",
|
|
64
|
+
"parse_query_string",
|
|
65
|
+
]
|
|
@@ -32,6 +32,7 @@ class App:
|
|
|
32
32
|
self.ws_routes = {}
|
|
33
33
|
self.startup_hooks = []
|
|
34
34
|
self.cors_allow_origin = (cors_allow_origin or "").strip()
|
|
35
|
+
self.metrics = None
|
|
35
36
|
|
|
36
37
|
def route(self, path, methods=("GET",), kind="plain"):
|
|
37
38
|
def decorator(func):
|
|
@@ -56,6 +57,21 @@ class App:
|
|
|
56
57
|
def add_startup(self, func):
|
|
57
58
|
self.startup_hooks.append(func)
|
|
58
59
|
|
|
60
|
+
def enable_metrics(
|
|
61
|
+
self,
|
|
62
|
+
path="/api/metrics",
|
|
63
|
+
stream_path="/api/metrics/stream",
|
|
64
|
+
app_name=None,
|
|
65
|
+
):
|
|
66
|
+
from .metrics import install_metrics
|
|
67
|
+
|
|
68
|
+
return install_metrics(
|
|
69
|
+
self,
|
|
70
|
+
path=path,
|
|
71
|
+
stream_path=stream_path,
|
|
72
|
+
app_name=app_name,
|
|
73
|
+
)
|
|
74
|
+
|
|
59
75
|
def dispatch(self, request):
|
|
60
76
|
cors_allow_origin = self.cors_allow_origin
|
|
61
77
|
if request.method == "OPTIONS":
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .app import App, Route, Router
|
|
4
4
|
from .constants import DEFAULT_CORS_ALLOW_ORIGIN, MAGIC_WS, STATUS_MESSAGES
|
|
5
5
|
from .http import Request, Response, parse_http_request, parse_query_string, send_http_response
|
|
6
|
+
from .metrics import AppMetrics, install_metrics
|
|
6
7
|
from .server import HTTPServer
|
|
7
8
|
from .ws import (
|
|
8
9
|
B64_ALPHABET,
|
|
@@ -22,6 +23,8 @@ __all__ = [
|
|
|
22
23
|
"HTTPServer",
|
|
23
24
|
"Request",
|
|
24
25
|
"Response",
|
|
26
|
+
"AppMetrics",
|
|
27
|
+
"install_metrics",
|
|
25
28
|
"Route",
|
|
26
29
|
"Router",
|
|
27
30
|
"WebSocket",
|
|
@@ -41,4 +44,3 @@ __all__ = [
|
|
|
41
44
|
"send_http_response",
|
|
42
45
|
"sha1",
|
|
43
46
|
]
|
|
44
|
-
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Application metrics collector and JSON streaming helpers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
from .http import Response
|
|
9
|
+
|
|
10
|
+
DEFAULT_STREAM_POINTS = 5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _iso_utc(ts):
|
|
14
|
+
return datetime.fromtimestamp(ts, UTC).isoformat()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _safe_int(value, default):
|
|
18
|
+
try:
|
|
19
|
+
return int(value)
|
|
20
|
+
except Exception:
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _safe_float(value, default):
|
|
25
|
+
try:
|
|
26
|
+
return float(value)
|
|
27
|
+
except Exception:
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_truthy(value):
|
|
32
|
+
if value is None:
|
|
33
|
+
return False
|
|
34
|
+
text = str(value).strip().lower()
|
|
35
|
+
return text in {"1", "true", "yes", "on"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AppMetrics:
|
|
39
|
+
def __init__(self, app_name="wsbuilder-app"):
|
|
40
|
+
self.app_name = app_name
|
|
41
|
+
self.started_at = time.time()
|
|
42
|
+
self._lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
self.active_tcp_connections = 0
|
|
45
|
+
self.total_tcp_connections = 0
|
|
46
|
+
|
|
47
|
+
self.active_http_requests = 0
|
|
48
|
+
self.total_http_requests = 0
|
|
49
|
+
self.total_http_responses = 0
|
|
50
|
+
self.http_methods = {}
|
|
51
|
+
self.http_paths = {}
|
|
52
|
+
self.http_status = {}
|
|
53
|
+
self.http_total_duration_ms = 0.0
|
|
54
|
+
self.http_max_duration_ms = 0.0
|
|
55
|
+
|
|
56
|
+
self.active_ws_connections = 0
|
|
57
|
+
self.total_ws_upgrades = 0
|
|
58
|
+
self.ws_paths = {}
|
|
59
|
+
self.ws_messages_in = 0
|
|
60
|
+
self.ws_messages_out = 0
|
|
61
|
+
self.ws_bytes_in = 0
|
|
62
|
+
self.ws_bytes_out = 0
|
|
63
|
+
|
|
64
|
+
self.bytes_in = 0
|
|
65
|
+
self.bytes_out = 0
|
|
66
|
+
|
|
67
|
+
self.total_errors = 0
|
|
68
|
+
self.last_error = ""
|
|
69
|
+
|
|
70
|
+
def _inc_map(self, data, key, step=1):
|
|
71
|
+
data[key] = data.get(key, 0) + step
|
|
72
|
+
|
|
73
|
+
def tcp_connection_open(self):
|
|
74
|
+
with self._lock:
|
|
75
|
+
self.active_tcp_connections += 1
|
|
76
|
+
self.total_tcp_connections += 1
|
|
77
|
+
|
|
78
|
+
def tcp_connection_close(self):
|
|
79
|
+
with self._lock:
|
|
80
|
+
if self.active_tcp_connections > 0:
|
|
81
|
+
self.active_tcp_connections -= 1
|
|
82
|
+
|
|
83
|
+
def http_request_started(self, method, path, body_size=0):
|
|
84
|
+
method = (method or "").upper() or "UNKNOWN"
|
|
85
|
+
path = path or "/"
|
|
86
|
+
body_size = max(0, int(body_size or 0))
|
|
87
|
+
with self._lock:
|
|
88
|
+
self.active_http_requests += 1
|
|
89
|
+
self.total_http_requests += 1
|
|
90
|
+
self.bytes_in += body_size
|
|
91
|
+
self._inc_map(self.http_methods, method)
|
|
92
|
+
self._inc_map(self.http_paths, path)
|
|
93
|
+
|
|
94
|
+
def http_response_sent(self, method, path, status, body_size=0, duration_ms=0.0):
|
|
95
|
+
status_key = str(int(status or 0))
|
|
96
|
+
body_size = max(0, int(body_size or 0))
|
|
97
|
+
duration_ms = max(0.0, float(duration_ms or 0.0))
|
|
98
|
+
with self._lock:
|
|
99
|
+
if self.active_http_requests > 0:
|
|
100
|
+
self.active_http_requests -= 1
|
|
101
|
+
self.total_http_responses += 1
|
|
102
|
+
self.bytes_out += body_size
|
|
103
|
+
self._inc_map(self.http_status, status_key)
|
|
104
|
+
self.http_total_duration_ms += duration_ms
|
|
105
|
+
if duration_ms > self.http_max_duration_ms:
|
|
106
|
+
self.http_max_duration_ms = duration_ms
|
|
107
|
+
|
|
108
|
+
def ws_opened(self, path):
|
|
109
|
+
path = path or "/ws/"
|
|
110
|
+
with self._lock:
|
|
111
|
+
self.active_ws_connections += 1
|
|
112
|
+
self.total_ws_upgrades += 1
|
|
113
|
+
self._inc_map(self.ws_paths, path)
|
|
114
|
+
|
|
115
|
+
def ws_closed(self, path):
|
|
116
|
+
with self._lock:
|
|
117
|
+
if self.active_ws_connections > 0:
|
|
118
|
+
self.active_ws_connections -= 1
|
|
119
|
+
|
|
120
|
+
def ws_message_in(self, payload_size=0):
|
|
121
|
+
payload_size = max(0, int(payload_size or 0))
|
|
122
|
+
with self._lock:
|
|
123
|
+
self.ws_messages_in += 1
|
|
124
|
+
self.ws_bytes_in += payload_size
|
|
125
|
+
self.bytes_in += payload_size
|
|
126
|
+
|
|
127
|
+
def ws_message_out(self, payload_size=0):
|
|
128
|
+
payload_size = max(0, int(payload_size or 0))
|
|
129
|
+
with self._lock:
|
|
130
|
+
self.ws_messages_out += 1
|
|
131
|
+
self.ws_bytes_out += payload_size
|
|
132
|
+
self.bytes_out += payload_size
|
|
133
|
+
|
|
134
|
+
def error(self, where, exc):
|
|
135
|
+
msg = f"{where}: {exc}"
|
|
136
|
+
with self._lock:
|
|
137
|
+
self.total_errors += 1
|
|
138
|
+
self.last_error = msg
|
|
139
|
+
|
|
140
|
+
def snapshot(self):
|
|
141
|
+
now = time.time()
|
|
142
|
+
with self._lock:
|
|
143
|
+
methods = dict(self.http_methods)
|
|
144
|
+
paths = dict(self.http_paths)
|
|
145
|
+
status = dict(self.http_status)
|
|
146
|
+
ws_paths = dict(self.ws_paths)
|
|
147
|
+
total_responses = self.total_http_responses
|
|
148
|
+
avg_ms = 0.0
|
|
149
|
+
if total_responses > 0:
|
|
150
|
+
avg_ms = self.http_total_duration_ms / float(total_responses)
|
|
151
|
+
|
|
152
|
+
data = {
|
|
153
|
+
"app_name": self.app_name,
|
|
154
|
+
"timestamp_unix": now,
|
|
155
|
+
"timestamp_utc": _iso_utc(now),
|
|
156
|
+
"started_at_unix": self.started_at,
|
|
157
|
+
"started_at_utc": _iso_utc(self.started_at),
|
|
158
|
+
"uptime_seconds": round(now - self.started_at, 3),
|
|
159
|
+
"connections": {
|
|
160
|
+
"tcp_active": self.active_tcp_connections,
|
|
161
|
+
"tcp_total": self.total_tcp_connections,
|
|
162
|
+
"http_inflight": self.active_http_requests,
|
|
163
|
+
"ws_active": self.active_ws_connections,
|
|
164
|
+
},
|
|
165
|
+
"http": {
|
|
166
|
+
"requests_total": self.total_http_requests,
|
|
167
|
+
"responses_total": self.total_http_responses,
|
|
168
|
+
"methods": methods,
|
|
169
|
+
"paths": paths,
|
|
170
|
+
"status": status,
|
|
171
|
+
"duration_ms_avg": round(avg_ms, 3),
|
|
172
|
+
"duration_ms_max": round(self.http_max_duration_ms, 3),
|
|
173
|
+
},
|
|
174
|
+
"websocket": {
|
|
175
|
+
"upgrades_total": self.total_ws_upgrades,
|
|
176
|
+
"paths": ws_paths,
|
|
177
|
+
"messages_in_total": self.ws_messages_in,
|
|
178
|
+
"messages_out_total": self.ws_messages_out,
|
|
179
|
+
"bytes_in_total": self.ws_bytes_in,
|
|
180
|
+
"bytes_out_total": self.ws_bytes_out,
|
|
181
|
+
},
|
|
182
|
+
"traffic": {
|
|
183
|
+
"bytes_in_total": self.bytes_in,
|
|
184
|
+
"bytes_out_total": self.bytes_out,
|
|
185
|
+
},
|
|
186
|
+
"errors": {
|
|
187
|
+
"total": self.total_errors,
|
|
188
|
+
"last": self.last_error,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
return data
|
|
192
|
+
|
|
193
|
+
def stream_chunks(self, interval_seconds=1.0, max_points=None):
|
|
194
|
+
interval_seconds = _safe_float(interval_seconds, 1.0)
|
|
195
|
+
if interval_seconds < 0.1:
|
|
196
|
+
interval_seconds = 0.1
|
|
197
|
+
if interval_seconds > 60.0:
|
|
198
|
+
interval_seconds = 60.0
|
|
199
|
+
if max_points is not None:
|
|
200
|
+
max_points = _safe_int(max_points, None)
|
|
201
|
+
if max_points is not None and max_points <= 0:
|
|
202
|
+
max_points = None
|
|
203
|
+
|
|
204
|
+
sent = 0
|
|
205
|
+
while True:
|
|
206
|
+
payload = json.dumps(
|
|
207
|
+
self.snapshot(),
|
|
208
|
+
ensure_ascii=False,
|
|
209
|
+
separators=(",", ":"),
|
|
210
|
+
).encode("utf-8") + b"\n"
|
|
211
|
+
yield payload
|
|
212
|
+
sent += 1
|
|
213
|
+
if max_points is not None and sent >= max_points:
|
|
214
|
+
break
|
|
215
|
+
time.sleep(interval_seconds)
|
|
216
|
+
|
|
217
|
+
def response_snapshot(self):
|
|
218
|
+
return Response.json(self.snapshot())
|
|
219
|
+
|
|
220
|
+
def response_stream(self, interval_seconds=1.0, max_points=None):
|
|
221
|
+
headers = {
|
|
222
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
223
|
+
"Pragma": "no-cache",
|
|
224
|
+
"Expires": "0",
|
|
225
|
+
"X-Accel-Buffering": "no",
|
|
226
|
+
}
|
|
227
|
+
return Response.stream(
|
|
228
|
+
self.stream_chunks(interval_seconds=interval_seconds, max_points=max_points),
|
|
229
|
+
content_type="application/x-ndjson; charset=utf-8",
|
|
230
|
+
headers=headers,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def install_metrics(app, path="/api/metrics", stream_path="/api/metrics/stream", app_name=None):
|
|
235
|
+
name = app_name or app.__class__.__name__
|
|
236
|
+
metrics = AppMetrics(app_name=name)
|
|
237
|
+
app.metrics = metrics
|
|
238
|
+
|
|
239
|
+
@app.api(path, methods=("GET",))
|
|
240
|
+
def _metrics_snapshot(request):
|
|
241
|
+
return metrics.response_snapshot()
|
|
242
|
+
|
|
243
|
+
@app.api(stream_path, methods=("GET",))
|
|
244
|
+
def _metrics_stream(request):
|
|
245
|
+
interval = request.query.get("interval", "1.0")
|
|
246
|
+
limit_text = request.query.get("limit", "")
|
|
247
|
+
follow = request.query.get("follow", "")
|
|
248
|
+
if limit_text:
|
|
249
|
+
parsed = _safe_int(limit_text, DEFAULT_STREAM_POINTS)
|
|
250
|
+
if parsed <= 0:
|
|
251
|
+
max_points = None
|
|
252
|
+
else:
|
|
253
|
+
max_points = parsed
|
|
254
|
+
else:
|
|
255
|
+
if _is_truthy(follow):
|
|
256
|
+
max_points = None
|
|
257
|
+
else:
|
|
258
|
+
max_points = DEFAULT_STREAM_POINTS
|
|
259
|
+
return metrics.response_stream(interval_seconds=interval, max_points=max_points)
|
|
260
|
+
|
|
261
|
+
return metrics
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
__all__ = ["AppMetrics", "install_metrics"]
|