mod-wsgi-telemetry 1.0.0.dev2__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.
- mod_wsgi/__init__.py +2 -0
- mod_wsgi/telemetry/__init__.py +1 -0
- mod_wsgi/telemetry/cli.py +55 -0
- mod_wsgi/telemetry/contention.py +229 -0
- mod_wsgi/telemetry/dump.py +102 -0
- mod_wsgi/telemetry/ingest.py +773 -0
- mod_wsgi/telemetry/server.py +270 -0
- mod_wsgi/telemetry/simulate.py +649 -0
- mod_wsgi/telemetry/static/index.html +8368 -0
- mod_wsgi/telemetry/tui.py +1536 -0
- mod_wsgi/telemetry/wire.py +480 -0
- mod_wsgi_telemetry-1.0.0.dev2.dist-info/METADATA +79 -0
- mod_wsgi_telemetry-1.0.0.dev2.dist-info/RECORD +16 -0
- mod_wsgi_telemetry-1.0.0.dev2.dist-info/WHEEL +4 -0
- mod_wsgi_telemetry-1.0.0.dev2.dist-info/entry_points.txt +2 -0
- mod_wsgi_telemetry-1.0.0.dev2.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""TLV wire format decoder for mod_wsgi telemetry samples.
|
|
2
|
+
|
|
3
|
+
This file is the Python-side mirror of src/server/wsgi_telemetry.h on the
|
|
4
|
+
mod_wsgi C side. Field IDs and type tags must stay in sync. Once the C
|
|
5
|
+
header is committed, this table should be regenerated from it.
|
|
6
|
+
|
|
7
|
+
All time-valued payload fields are IEEE 754 double-precision floats in
|
|
8
|
+
seconds. Wall-clock instants are seconds since the Unix epoch; durations
|
|
9
|
+
are seconds. The fixed-header stamp follows the same convention. Counts
|
|
10
|
+
are integer types.
|
|
11
|
+
|
|
12
|
+
Wire layout:
|
|
13
|
+
|
|
14
|
+
fixed header (24 bytes, little-endian):
|
|
15
|
+
magic uint32 b'WSGI'
|
|
16
|
+
version uint8
|
|
17
|
+
kind uint8 1=process, 2=request, 3=server, 4=slow_request,
|
|
18
|
+
5=process_started, 6=process_stopping, 7=process_stopped
|
|
19
|
+
flags uint16
|
|
20
|
+
pid uint32
|
|
21
|
+
seq uint32 monotonic per process, drop detection
|
|
22
|
+
stamp float64 seconds since epoch
|
|
23
|
+
|
|
24
|
+
then repeated TLV records until the end of the datagram:
|
|
25
|
+
id uint16
|
|
26
|
+
type uint8 one of T_U64, T_F64, T_I64, T_BYTES, T_I32_ARRAY,
|
|
27
|
+
T_F64_ARRAY
|
|
28
|
+
[len uint16] only for BYTES / I32_ARRAY / F64_ARRAY
|
|
29
|
+
value per type
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import struct
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
MAGIC = b"WSGI"
|
|
39
|
+
|
|
40
|
+
# Kind codes
|
|
41
|
+
KIND_PROCESS = 1
|
|
42
|
+
KIND_REQUEST = 2
|
|
43
|
+
KIND_SERVER = 3
|
|
44
|
+
KIND_SLOW_REQUEST = 4
|
|
45
|
+
KIND_PROCESS_STARTED = 5
|
|
46
|
+
KIND_PROCESS_STOPPING = 6
|
|
47
|
+
KIND_PROCESS_STOPPED = 7
|
|
48
|
+
KIND_GC_SNAPSHOT = 8
|
|
49
|
+
KIND_GC_EVENT = 9
|
|
50
|
+
|
|
51
|
+
KIND_NAMES = {
|
|
52
|
+
KIND_PROCESS: "process_metrics",
|
|
53
|
+
KIND_REQUEST: "request_metrics",
|
|
54
|
+
KIND_SERVER: "server_metrics",
|
|
55
|
+
KIND_SLOW_REQUEST: "slow_request",
|
|
56
|
+
KIND_PROCESS_STARTED: "process_started",
|
|
57
|
+
KIND_PROCESS_STOPPING: "process_stopping",
|
|
58
|
+
KIND_PROCESS_STOPPED: "process_stopped",
|
|
59
|
+
KIND_GC_SNAPSHOT: "gc_snapshot",
|
|
60
|
+
KIND_GC_EVENT: "gc_event",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Type tags
|
|
64
|
+
T_U64 = 0x01
|
|
65
|
+
T_F64 = 0x02
|
|
66
|
+
T_I64 = 0x03
|
|
67
|
+
T_BYTES = 0x04
|
|
68
|
+
T_I32_ARRAY = 0x05
|
|
69
|
+
T_F64_ARRAY = 0x06
|
|
70
|
+
|
|
71
|
+
# Field ID table. Mirrors wsgi_telemetry.h. Grouped in blocks of 10 by
|
|
72
|
+
# concept; see the header for the authoritative groupings. IDs are kept
|
|
73
|
+
# stable while the wire format is in development; once a release is cut,
|
|
74
|
+
# they become append-only and renumbering is no longer permitted.
|
|
75
|
+
FIELDS = {
|
|
76
|
+
# 1-9: identity. Build/runtime versions come first so a consumer
|
|
77
|
+
# that wants to print a "who is this" banner can reach them without
|
|
78
|
+
# scanning the whole TLV record. All seven fields are static for
|
|
79
|
+
# the life of a process.
|
|
80
|
+
1: "mod_wsgi_version",
|
|
81
|
+
2: "python_version",
|
|
82
|
+
3: "apache_version",
|
|
83
|
+
4: "mpm_name",
|
|
84
|
+
5: "hostname",
|
|
85
|
+
6: "process_group",
|
|
86
|
+
7: "process_parent_pid",
|
|
87
|
+
# interpreter_name is the per-emission application-group label
|
|
88
|
+
# (empty for the main interpreter); identifies which sub-
|
|
89
|
+
# interpreter a datagram describes when a process hosts more
|
|
90
|
+
# than one. Omitted in the single-interpreter common case.
|
|
91
|
+
8: "interpreter_name",
|
|
92
|
+
|
|
93
|
+
# 10-19: sampling and reporter configuration. sample_period is the
|
|
94
|
+
# measured wall-clock interval between snapshot calls (drifts with
|
|
95
|
+
# scheduling jitter); telemetry_interval is the configured
|
|
96
|
+
# WSGITelemetry directive (constant). slow_requests_threshold is
|
|
97
|
+
# the configured WSGISlowRequests value in seconds (0 when the
|
|
98
|
+
# directive is not configured).
|
|
99
|
+
10: "sample_period",
|
|
100
|
+
11: "telemetry_interval",
|
|
101
|
+
12: "slow_requests_threshold",
|
|
102
|
+
13: "switch_interval",
|
|
103
|
+
|
|
104
|
+
# 20-29: request rates and capacity for the interval.
|
|
105
|
+
20: "request_count",
|
|
106
|
+
21: "request_throughput",
|
|
107
|
+
22: "capacity_utilization",
|
|
108
|
+
|
|
109
|
+
# 30-39: CPU. *_utilization are interval rates from
|
|
110
|
+
# wsgi_request_metrics; *_time are cumulative seconds reserved for
|
|
111
|
+
# wsgi_process_metrics (not yet emitted on the wire).
|
|
112
|
+
30: "cpu_user_utilization",
|
|
113
|
+
31: "cpu_system_utilization",
|
|
114
|
+
32: "cpu_utilization",
|
|
115
|
+
35: "cpu_user_time",
|
|
116
|
+
36: "cpu_system_time",
|
|
117
|
+
37: "cpu_time",
|
|
118
|
+
|
|
119
|
+
# 40-49: memory.
|
|
120
|
+
40: "memory_rss",
|
|
121
|
+
41: "memory_max_rss",
|
|
122
|
+
|
|
123
|
+
# 50-59: worker-thread counts.
|
|
124
|
+
50: "request_threads_maximum",
|
|
125
|
+
51: "request_threads_started",
|
|
126
|
+
52: "request_threads_active",
|
|
127
|
+
|
|
128
|
+
# 60-69: per-phase mean times for the interval (seconds).
|
|
129
|
+
# request_time is the per-request total
|
|
130
|
+
# (server + queue + daemon + application) — what the caller actually
|
|
131
|
+
# experienced. gil_wait_time, input_read_time and output_write_time
|
|
132
|
+
# are cross-cutting overlap indicators that accumulate *during*
|
|
133
|
+
# application_time and are *not* addends in the request_time
|
|
134
|
+
# invariant. gil_wait_time is partial — see UI help text for the
|
|
135
|
+
# full coverage caveat. output_write_time is adapter-handoff time
|
|
136
|
+
# (Apache may buffer / async-flush past the WSGI app's return), not
|
|
137
|
+
# client-receive latency.
|
|
138
|
+
60: "server_time",
|
|
139
|
+
61: "queue_time",
|
|
140
|
+
62: "daemon_time",
|
|
141
|
+
63: "application_time",
|
|
142
|
+
64: "request_time",
|
|
143
|
+
65: "gil_wait_time",
|
|
144
|
+
66: "input_read_time",
|
|
145
|
+
67: "output_write_time",
|
|
146
|
+
|
|
147
|
+
# 70-79: per-phase exact min times for the interval (float seconds).
|
|
148
|
+
# Only present on ticks where at least one request completed; the
|
|
149
|
+
# encoder skips the field on idle ticks. Aggregate cleanly across
|
|
150
|
+
# processes and across windows by min-of-mins: exact, no histogram
|
|
151
|
+
# approximation.
|
|
152
|
+
70: "server_time_min",
|
|
153
|
+
71: "queue_time_min",
|
|
154
|
+
72: "daemon_time_min",
|
|
155
|
+
73: "application_time_min",
|
|
156
|
+
74: "request_time_min",
|
|
157
|
+
75: "gil_wait_time_min",
|
|
158
|
+
76: "input_read_time_min",
|
|
159
|
+
77: "output_write_time_min",
|
|
160
|
+
|
|
161
|
+
# 80-89: per-phase exact max times for the interval (float seconds).
|
|
162
|
+
# Same emission rule and aggregation semantics as the min block:
|
|
163
|
+
# max-of-maxes is exact. Pairs with the histograms below to give a
|
|
164
|
+
# true worst-case alongside bucket-bounded percentiles.
|
|
165
|
+
80: "server_time_max",
|
|
166
|
+
81: "queue_time_max",
|
|
167
|
+
82: "daemon_time_max",
|
|
168
|
+
83: "application_time_max",
|
|
169
|
+
84: "request_time_max",
|
|
170
|
+
85: "gil_wait_time_max",
|
|
171
|
+
86: "input_read_time_max",
|
|
172
|
+
87: "output_write_time_max",
|
|
173
|
+
|
|
174
|
+
# 90-99: per-phase histograms. HDR-style: 16 octaves from 1 ms to
|
|
175
|
+
# 65.5 s, each octave linearly split into 4 sub-buckets, plus one
|
|
176
|
+
# overflow bucket for >65536 ms = 65 entries per phase. Max relative
|
|
177
|
+
# error inside any sub-bucket is <=25%.
|
|
178
|
+
90: "server_time_buckets",
|
|
179
|
+
91: "queue_time_buckets",
|
|
180
|
+
92: "daemon_time_buckets",
|
|
181
|
+
93: "application_time_buckets",
|
|
182
|
+
94: "request_time_buckets",
|
|
183
|
+
95: "gil_wait_time_buckets",
|
|
184
|
+
96: "input_read_time_buckets",
|
|
185
|
+
97: "output_write_time_buckets",
|
|
186
|
+
|
|
187
|
+
# 100-109: per-interval request I/O totals. Drained from the
|
|
188
|
+
# adapter's InputObject.bytes/reads and
|
|
189
|
+
# AdapterObject.output_length/output_writes at end-of-request;
|
|
190
|
+
# in-flight requests don't contribute until they finish.
|
|
191
|
+
100: "input_bytes_total",
|
|
192
|
+
101: "input_reads_total",
|
|
193
|
+
102: "output_bytes_total",
|
|
194
|
+
103: "output_writes_total",
|
|
195
|
+
|
|
196
|
+
# 110-119: per-worker-slot capacity signals. One entry per worker
|
|
197
|
+
# thread; array length matches the emitting process's live
|
|
198
|
+
# request_threads_maximum. Field names parallel the same-shape keys
|
|
199
|
+
# in the mod_wsgi.request_metrics() Python dict. The four
|
|
200
|
+
# time-valued lists are f64 arrays in seconds; the completed-count
|
|
201
|
+
# list stays an i32 array of counts.
|
|
202
|
+
110: "request_threads_completed",
|
|
203
|
+
111: "request_threads_busy_time",
|
|
204
|
+
112: "request_threads_cpu_time",
|
|
205
|
+
113: "request_threads_current_elapsed",
|
|
206
|
+
114: "request_threads_max_duration",
|
|
207
|
+
|
|
208
|
+
# 120-129: per-interval HTTP response class totals. Drained from
|
|
209
|
+
# the same accumulator that wsgi_record_request_times() updates at
|
|
210
|
+
# end-of-request, sharing drain-and-reset semantics with the
|
|
211
|
+
# 100-109 I/O totals block. status==0 (no start_response call) is
|
|
212
|
+
# folded into status_5xx_total. 1xx is included as a PEP-3333
|
|
213
|
+
# tripwire — a WSGI app should never return 1xx, so a non-zero
|
|
214
|
+
# count flags a protocol violation. Sum equals request_count for
|
|
215
|
+
# the same interval; consumers can use this as a sanity check.
|
|
216
|
+
120: "status_1xx_total",
|
|
217
|
+
121: "status_2xx_total",
|
|
218
|
+
122: "status_3xx_total",
|
|
219
|
+
123: "status_4xx_total",
|
|
220
|
+
124: "status_5xx_total",
|
|
221
|
+
|
|
222
|
+
# 130-139: lifecycle event payload. Only present in
|
|
223
|
+
# KIND_PROCESS_STARTED, KIND_PROCESS_STOPPING and
|
|
224
|
+
# KIND_PROCESS_STOPPED datagrams. Identity (hostname,
|
|
225
|
+
# process_group, parent_pid) is repeated on each lifecycle
|
|
226
|
+
# datagram and on every periodic KIND_PROCESS tick so a consumer
|
|
227
|
+
# that joins mid-stream can rehydrate the full identity without
|
|
228
|
+
# waiting for a restart cycle. The static identity strings
|
|
229
|
+
# (versions, MPM) are also emitted on every periodic tick;
|
|
230
|
+
# STOPPING and STOPPED still expect the consumer to have keyed
|
|
231
|
+
# them by pid because those frames omit the version strings.
|
|
232
|
+
130: "shutdown_reason",
|
|
233
|
+
131: "process_uptime",
|
|
234
|
+
132: "lifetime_request_count",
|
|
235
|
+
133: "active_requests_at_decision",
|
|
236
|
+
134: "active_requests_at_exit",
|
|
237
|
+
135: "graceful_drain", # 0 = reaper aborted, 1 = drain completed cleanly
|
|
238
|
+
|
|
239
|
+
# 160-199: reserved for future intermediate categories. Field IDs
|
|
240
|
+
# are uint16 and the address space is effectively unbounded; this
|
|
241
|
+
# gap exists so new conceptual blocks can be inserted before the
|
|
242
|
+
# slow-request region without disturbing already-allocated IDs.
|
|
243
|
+
|
|
244
|
+
# 200-299: slow-request fields (only present in KIND_SLOW_REQUEST
|
|
245
|
+
# datagrams). Identity (hostname, process_group) is keyed per pid
|
|
246
|
+
# from the accompanying KIND_REQUEST and KIND_PROCESS_STARTED
|
|
247
|
+
# streams, so it is not repeated here. The 200-block reserves 100
|
|
248
|
+
# IDs for the slow-record category so future additions land cleanly
|
|
249
|
+
# within this range.
|
|
250
|
+
#
|
|
251
|
+
# Block layout:
|
|
252
|
+
# 200-209: record metadata (state, start, duration, thread, log id)
|
|
253
|
+
# 210-219: HTTP request identity (method, URL components, protocol,
|
|
254
|
+
# peer IP, user agent)
|
|
255
|
+
# 220-229: per-phase timing breakdown (server / queue / daemon /
|
|
256
|
+
# application time, seconds)
|
|
257
|
+
# 230-239: per-request I/O counters
|
|
258
|
+
# 240-249: response outcome (HTTP status; future error.type)
|
|
259
|
+
# 250-259: per-request CPU and resource use
|
|
260
|
+
# 260-269: concurrency context (in-flight counts at boundaries)
|
|
261
|
+
# 270-289: reserved for future trace-context fields
|
|
262
|
+
# 290-299: reserved
|
|
263
|
+
#
|
|
264
|
+
# Active records carry zero for fields not yet observable.
|
|
265
|
+
200: "slow_record_state", # 0 = active, 1 = completed
|
|
266
|
+
201: "slow_start_stamp", # f64 seconds since epoch
|
|
267
|
+
202: "slow_duration", # f64 seconds
|
|
268
|
+
203: "slow_thread_id",
|
|
269
|
+
204: "slow_log_id",
|
|
270
|
+
205: "slow_server_pid", # Apache child worker pid that accepted the request
|
|
271
|
+
|
|
272
|
+
210: "slow_method",
|
|
273
|
+
211: "slow_scheme",
|
|
274
|
+
212: "slow_hostname",
|
|
275
|
+
213: "slow_script_name",
|
|
276
|
+
214: "slow_path_info",
|
|
277
|
+
215: "slow_protocol", # "HTTP/1.1", "HTTP/2.0"
|
|
278
|
+
216: "slow_peer_ip", # post-trusted-proxy resolution
|
|
279
|
+
217: "slow_user_agent", # only when WSGITelemetryOptions +CaptureUserAgent
|
|
280
|
+
|
|
281
|
+
220: "slow_server_time",
|
|
282
|
+
221: "slow_queue_time", # 0 in embedded mode
|
|
283
|
+
222: "slow_daemon_time", # 0 in embedded mode
|
|
284
|
+
223: "slow_application_time", # partial for active records still in flight
|
|
285
|
+
224: "slow_gil_wait_time", # GIL-wait pressure indicator; running total for active records
|
|
286
|
+
225: "slow_gil_wait_count", # number of re-acquire events observed
|
|
287
|
+
|
|
288
|
+
230: "slow_input_bytes",
|
|
289
|
+
231: "slow_input_reads",
|
|
290
|
+
232: "slow_output_bytes",
|
|
291
|
+
233: "slow_output_writes",
|
|
292
|
+
234: "slow_input_read_time", # per-request total time inside wsgi.input.read*
|
|
293
|
+
235: "slow_output_write_time", # per-request total time inside adapter output path
|
|
294
|
+
|
|
295
|
+
240: "slow_status", # 0 = not yet known, else final WSGI status
|
|
296
|
+
|
|
297
|
+
250: "slow_cpu_user_time",
|
|
298
|
+
251: "slow_cpu_system_time",
|
|
299
|
+
|
|
300
|
+
260: "slow_active_at_start", # in-flight count including this request
|
|
301
|
+
261: "slow_active_at_completion", # 0 for active records
|
|
302
|
+
|
|
303
|
+
# 300-317: per-interpreter GC tier-1 counters, present only on
|
|
304
|
+
# KIND_GC_SNAPSHOT datagrams (one per interpreter per tick when
|
|
305
|
+
# WSGITelemetryService is configured). Layout follows the gc
|
|
306
|
+
# module's grouping: get_count, get_threshold, get_stats per
|
|
307
|
+
# generation, isenabled, get_freeze_count. dropped_events is
|
|
308
|
+
# the cumulative count of tier-2 records lost to ring overflow.
|
|
309
|
+
300: "gc_count0",
|
|
310
|
+
301: "gc_count1",
|
|
311
|
+
302: "gc_count2",
|
|
312
|
+
303: "gc_threshold0",
|
|
313
|
+
304: "gc_threshold1",
|
|
314
|
+
305: "gc_threshold2",
|
|
315
|
+
306: "gc_collections0",
|
|
316
|
+
307: "gc_collections1",
|
|
317
|
+
308: "gc_collections2",
|
|
318
|
+
309: "gc_collected0",
|
|
319
|
+
310: "gc_collected1",
|
|
320
|
+
311: "gc_collected2",
|
|
321
|
+
312: "gc_uncollectable0",
|
|
322
|
+
313: "gc_uncollectable1",
|
|
323
|
+
314: "gc_uncollectable2",
|
|
324
|
+
315: "gc_is_enabled",
|
|
325
|
+
316: "gc_freeze_count",
|
|
326
|
+
317: "gc_dropped_events",
|
|
327
|
+
|
|
328
|
+
# 320-324: per-event GC pause record, present only on
|
|
329
|
+
# KIND_GC_EVENT datagrams (one per cyclic GC pass).
|
|
330
|
+
320: "gc_event_start_stamp", # f64 seconds since epoch
|
|
331
|
+
321: "gc_event_duration", # f64 seconds
|
|
332
|
+
322: "gc_event_generation", # 0 / 1 / 2
|
|
333
|
+
323: "gc_event_collected",
|
|
334
|
+
324: "gc_event_uncollectable",
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Reverse map for encoders / tests.
|
|
338
|
+
FIELD_IDS = {name: fid for fid, name in FIELDS.items()}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
_HDR = struct.Struct("<4sBBHIId")
|
|
342
|
+
_TLV_HDR = struct.Struct("<HB")
|
|
343
|
+
_U16 = struct.Struct("<H")
|
|
344
|
+
_U64 = struct.Struct("<Q")
|
|
345
|
+
_I64 = struct.Struct("<q")
|
|
346
|
+
_F64 = struct.Struct("<d")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@dataclass
|
|
350
|
+
class Sample:
|
|
351
|
+
version: int
|
|
352
|
+
kind: int
|
|
353
|
+
pid: int
|
|
354
|
+
seq: int
|
|
355
|
+
stamp: float
|
|
356
|
+
fields: dict[str, Any]
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def kind_name(self) -> str:
|
|
360
|
+
return KIND_NAMES.get(self.kind, f"kind{self.kind}")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class DecodeError(ValueError):
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def decode(buf: bytes | memoryview) -> Sample:
|
|
368
|
+
"""Decode one TLV datagram into a Sample.
|
|
369
|
+
|
|
370
|
+
Unknown field IDs are kept under a synthetic name like "id42" so
|
|
371
|
+
nothing is silently dropped during wire-format evolution.
|
|
372
|
+
"""
|
|
373
|
+
if len(buf) < _HDR.size:
|
|
374
|
+
raise DecodeError(f"datagram too short: {len(buf)} bytes")
|
|
375
|
+
|
|
376
|
+
magic, version, kind, _flags, pid, seq, stamp = _HDR.unpack_from(buf, 0)
|
|
377
|
+
if magic != MAGIC:
|
|
378
|
+
raise DecodeError(f"bad magic {magic!r}")
|
|
379
|
+
|
|
380
|
+
off = _HDR.size
|
|
381
|
+
fields: dict[str, Any] = {}
|
|
382
|
+
while off < len(buf):
|
|
383
|
+
if len(buf) - off < _TLV_HDR.size:
|
|
384
|
+
raise DecodeError(f"truncated TLV header at offset {off}")
|
|
385
|
+
fid, ftype = _TLV_HDR.unpack_from(buf, off)
|
|
386
|
+
off += _TLV_HDR.size
|
|
387
|
+
|
|
388
|
+
if ftype == T_U64:
|
|
389
|
+
(value,) = _U64.unpack_from(buf, off)
|
|
390
|
+
off += 8
|
|
391
|
+
elif ftype == T_F64:
|
|
392
|
+
(value,) = _F64.unpack_from(buf, off)
|
|
393
|
+
off += 8
|
|
394
|
+
elif ftype == T_I64:
|
|
395
|
+
(value,) = _I64.unpack_from(buf, off)
|
|
396
|
+
off += 8
|
|
397
|
+
elif ftype == T_BYTES:
|
|
398
|
+
(n,) = _U16.unpack_from(buf, off)
|
|
399
|
+
off += 2
|
|
400
|
+
value = bytes(buf[off:off + n])
|
|
401
|
+
off += n
|
|
402
|
+
elif ftype == T_I32_ARRAY:
|
|
403
|
+
(n,) = _U16.unpack_from(buf, off)
|
|
404
|
+
off += 2
|
|
405
|
+
value = list(struct.unpack_from(f"<{n}i", buf, off))
|
|
406
|
+
off += n * 4
|
|
407
|
+
elif ftype == T_F64_ARRAY:
|
|
408
|
+
(n,) = _U16.unpack_from(buf, off)
|
|
409
|
+
off += 2
|
|
410
|
+
value = list(struct.unpack_from(f"<{n}d", buf, off))
|
|
411
|
+
off += n * 8
|
|
412
|
+
else:
|
|
413
|
+
raise DecodeError(
|
|
414
|
+
f"unknown type tag 0x{ftype:02x} at offset {off - 1}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
name = FIELDS.get(fid, f"id{fid}")
|
|
418
|
+
fields[name] = value
|
|
419
|
+
|
|
420
|
+
return Sample(
|
|
421
|
+
version=version,
|
|
422
|
+
kind=kind,
|
|
423
|
+
pid=pid,
|
|
424
|
+
seq=seq,
|
|
425
|
+
stamp=stamp,
|
|
426
|
+
fields=fields,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def encode(sample: Sample) -> bytes:
|
|
431
|
+
"""Encode a Sample back to TLV bytes. Used by tests and simulate.py."""
|
|
432
|
+
out = bytearray(
|
|
433
|
+
_HDR.pack(
|
|
434
|
+
MAGIC,
|
|
435
|
+
sample.version,
|
|
436
|
+
sample.kind,
|
|
437
|
+
0,
|
|
438
|
+
sample.pid,
|
|
439
|
+
sample.seq,
|
|
440
|
+
sample.stamp,
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
for name, value in sample.fields.items():
|
|
444
|
+
fid = FIELD_IDS.get(name)
|
|
445
|
+
if fid is None:
|
|
446
|
+
if name.startswith("id"):
|
|
447
|
+
fid = int(name[2:])
|
|
448
|
+
else:
|
|
449
|
+
raise KeyError(f"no field id assigned for {name!r}")
|
|
450
|
+
|
|
451
|
+
if isinstance(value, float):
|
|
452
|
+
out += _TLV_HDR.pack(fid, T_F64) + _F64.pack(value)
|
|
453
|
+
elif isinstance(value, bool):
|
|
454
|
+
out += _TLV_HDR.pack(fid, T_U64) + _U64.pack(int(value))
|
|
455
|
+
elif isinstance(value, int):
|
|
456
|
+
if value < 0:
|
|
457
|
+
out += _TLV_HDR.pack(fid, T_I64) + _I64.pack(value)
|
|
458
|
+
else:
|
|
459
|
+
out += _TLV_HDR.pack(fid, T_U64) + _U64.pack(value)
|
|
460
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
461
|
+
out += _TLV_HDR.pack(fid, T_BYTES) + _U16.pack(len(value)) + bytes(value)
|
|
462
|
+
elif isinstance(value, str):
|
|
463
|
+
data = value.encode("utf-8")
|
|
464
|
+
out += _TLV_HDR.pack(fid, T_BYTES) + _U16.pack(len(data)) + data
|
|
465
|
+
elif isinstance(value, (list, tuple)):
|
|
466
|
+
if all(isinstance(x, int) and not isinstance(x, bool) for x in value):
|
|
467
|
+
out += _TLV_HDR.pack(fid, T_I32_ARRAY) + _U16.pack(len(value))
|
|
468
|
+
out += struct.pack(f"<{len(value)}i", *value)
|
|
469
|
+
elif all(isinstance(x, (int, float)) and not isinstance(x, bool)
|
|
470
|
+
for x in value):
|
|
471
|
+
out += _TLV_HDR.pack(fid, T_F64_ARRAY) + _U16.pack(len(value))
|
|
472
|
+
out += struct.pack(f"<{len(value)}d", *[float(x) for x in value])
|
|
473
|
+
else:
|
|
474
|
+
raise TypeError(
|
|
475
|
+
f"cannot encode field {name!r}: mixed list element types"
|
|
476
|
+
)
|
|
477
|
+
else:
|
|
478
|
+
raise TypeError(f"cannot encode field {name!r}: {type(value).__name__}")
|
|
479
|
+
|
|
480
|
+
return bytes(out)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mod_wsgi-telemetry
|
|
3
|
+
Version: 1.0.0.dev2
|
|
4
|
+
Summary: Ingestion service and live UI for mod_wsgi telemetry samples.
|
|
5
|
+
Project-URL: Homepage, https://www.modwsgi.org/
|
|
6
|
+
Project-URL: Documentation, https://modwsgi.readthedocs.io/en/latest/user-guides/external-telemetry-service.html
|
|
7
|
+
Project-URL: Source, https://github.com/GrahamDumpleton/mod_wsgi
|
|
8
|
+
Project-URL: Issues, https://github.com/GrahamDumpleton/mod_wsgi/issues
|
|
9
|
+
Author-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
10
|
+
Maintainer-email: Graham Dumpleton <Graham.Dumpleton@gmail.com>
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: apache,mod_wsgi,monitoring,telemetry,wsgi
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Operating System :: POSIX :: BSD
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
|
|
28
|
+
Classifier: Topic :: System :: Monitoring
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: aiohttp>=3.9
|
|
31
|
+
Description-Content-Type: text/x-rst
|
|
32
|
+
|
|
33
|
+
Overview
|
|
34
|
+
--------
|
|
35
|
+
|
|
36
|
+
The ``mod_wsgi-telemetry`` package provides an external telemetry
|
|
37
|
+
ingester and live UI for the mod_wsgi Apache module. With the
|
|
38
|
+
``WSGITelemetryService`` directive enabled in Apache, each mod_wsgi
|
|
39
|
+
process (daemon-mode worker or embedded-mode Apache child) emits
|
|
40
|
+
per-interval binary datagrams summarising its throughput, latency
|
|
41
|
+
distribution, capacity utilisation, CPU and memory consumption, and
|
|
42
|
+
any active slow requests. The ingester receives those datagrams over
|
|
43
|
+
a local UNIX socket, aggregates them across every reporting process,
|
|
44
|
+
and serves a browser-based live UI together with a curses terminal
|
|
45
|
+
monitor for hosts where opening a browser is impractical.
|
|
46
|
+
|
|
47
|
+
The package is distributed separately from ``mod_wsgi`` itself so
|
|
48
|
+
that an installation using the operating-system ``mod_wsgi`` package,
|
|
49
|
+
or any other manually-configured Apache, can use the telemetry
|
|
50
|
+
pipeline without adopting the PyPi ``mod_wsgi`` or
|
|
51
|
+
``mod_wsgi-express`` packages.
|
|
52
|
+
|
|
53
|
+
The ingester is intended to run co-located with the Apache instance
|
|
54
|
+
it observes: the transport is a local UNIX datagram socket, and the
|
|
55
|
+
UI binds to the loopback interface by default. For remote access,
|
|
56
|
+
either an SSH tunnel or an authenticated reverse proxy is
|
|
57
|
+
recommended.
|
|
58
|
+
|
|
59
|
+
Once installed, launch the ingester with::
|
|
60
|
+
|
|
61
|
+
mod_wsgi-telemetry serve
|
|
62
|
+
|
|
63
|
+
It binds ``unix:/tmp/mod_wsgi-telemetry.sock`` for incoming
|
|
64
|
+
datagrams and serves the browser UI on ``http://127.0.0.1:8888/`` by
|
|
65
|
+
default. A ``mod_wsgi-telemetry top`` subcommand provides a
|
|
66
|
+
curses-based terminal monitor for the same data.
|
|
67
|
+
|
|
68
|
+
For the full configuration reference, including the
|
|
69
|
+
``WSGITelemetryService``, ``WSGITelemetryOptions`` and
|
|
70
|
+
``WSGISlowRequests`` Apache directives, the matching
|
|
71
|
+
``mod_wsgi-express`` options, socket-permission handling for
|
|
72
|
+
multi-user deployments, and the remote-access patterns, see the
|
|
73
|
+
External Telemetry Service page in the mod_wsgi documentation site
|
|
74
|
+
at https://www.modwsgi.org.
|
|
75
|
+
|
|
76
|
+
This package is still being iterated on. The directive set, option
|
|
77
|
+
names, wire format and ingester CLI may change in a future release;
|
|
78
|
+
pair an ingester release with the matching mod_wsgi release until
|
|
79
|
+
the pipeline stabilises.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mod_wsgi/__init__.py,sha256=TAxMvzzecHUbYSemZ7gBbqi-3hYmsODLKrbmxrtbaUQ,66
|
|
2
|
+
mod_wsgi/telemetry/__init__.py,sha256=tGrdUB9X0rUAuN9VADL-SUrgTbv8v_nK-phsBbmCytM,26
|
|
3
|
+
mod_wsgi/telemetry/cli.py,sha256=Neo6xIGUAuRI8cVcLt1NCKLjn_6icgKibLjcHN34IXo,1792
|
|
4
|
+
mod_wsgi/telemetry/contention.py,sha256=05fDCrWnOYJPZIsdR3oi8nszlmNcyGNbRCX8h6Ot71U,7984
|
|
5
|
+
mod_wsgi/telemetry/dump.py,sha256=3HTLcwfWgDxLF0alCVtR2gX0JEbItOahuPZ6PS75x40,3264
|
|
6
|
+
mod_wsgi/telemetry/ingest.py,sha256=lzCtW7IIPn9wPYn5GfkjOvdQYeFzRyTC2YyxUxp9isA,31498
|
|
7
|
+
mod_wsgi/telemetry/server.py,sha256=vdz1DZ-KnjOSLio6lPMHUI3VT3SFINTmUbiFDmFWb-o,10074
|
|
8
|
+
mod_wsgi/telemetry/simulate.py,sha256=GY-OiQmrodehsmZ3it3-x36xyG8tsHKsD_c1tP1xgJc,26681
|
|
9
|
+
mod_wsgi/telemetry/tui.py,sha256=jJn0EhqbJAbE__etd8hhA8mbk0CaXXYjVAU5xVC3l4c,56112
|
|
10
|
+
mod_wsgi/telemetry/wire.py,sha256=xgHK51YKrq_H9y58UDljdBRTmtO_HvbJtzgVhHpqRig,17972
|
|
11
|
+
mod_wsgi/telemetry/static/index.html,sha256=Cj_VKtr5MH0HOOpL2nSBiBtKp-oWDSEZAY-a7DwwWhg,344478
|
|
12
|
+
mod_wsgi_telemetry-1.0.0.dev2.dist-info/METADATA,sha256=gCgTFz3xm37BG5SN_4z4OZmBjd70VPK2gpWTgd9n8K0,3653
|
|
13
|
+
mod_wsgi_telemetry-1.0.0.dev2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
mod_wsgi_telemetry-1.0.0.dev2.dist-info/entry_points.txt,sha256=5972xTQQUqlzCVhBgzTmUmvclPHBa1SD917CPtl9J5g,67
|
|
15
|
+
mod_wsgi_telemetry-1.0.0.dev2.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
16
|
+
mod_wsgi_telemetry-1.0.0.dev2.dist-info/RECORD,,
|