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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mod_wsgi-telemetry = mod_wsgi.telemetry.cli:main