@push.rocks/smartproxy 27.21.0 → 27.22.0

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.
package/readme.metrics.md DELETED
@@ -1,490 +0,0 @@
1
- # SmartProxy Metrics System
2
-
3
- ## Architecture
4
-
5
- Two-tier design separating the data plane from the observation plane:
6
-
7
- **Byte recording (per-chunk):** Byte recording uses `AtomicU64` counters. `CountingBody` batches flushes every 64KB to reduce DashMap shard contention. Per-domain HTTP request recording uses a short tracker mutex; admitting a new domain also takes an admission mutex.
8
-
9
- **Cold path (1Hz sampling):** A background tokio task drains pending atomics into `ThroughputTracker` circular buffers (Mutex-guarded), producing per-second throughput history. Same task prunes orphaned entries and cleans up rate limiter state.
10
-
11
- **Read path (on-demand):** `snapshot()` reads all atomics and locks ThroughputTrackers to build a serializable `Metrics` struct. TypeScript polls this at 1s intervals via IPC.
12
-
13
- Metrics polling allows one outstanding snapshot RPC. Slow polls coalesce; stopping drains the outstanding poll and clears the snapshot/history caches. Native shutdown awaits confirmed child and stdio closure through SmartRust.
14
-
15
- Global HTTP domain rates retain at most 4,096 canonical domain keys, each at most 253 bytes. Idle trackers expire after their 60 completed one-second samples become empty. `requests.byDomain()` contains individually tracked domains. `requests.domainOverflow?.()` returns `{ perSecond, lastMinute }` for requests that could not receive an individual bucket. Overflow is separate from domain attribution; global request totals remain unchanged. Overflow requests are not retroactively assigned when a bucket later becomes available.
16
-
17
- Active connection pages retain the smallest `limit + 1` matching IDs in a bounded heap, then clone metadata for those IDs. Work remains a scan of active connections, with O(page size) temporary storage rather than cloning/sorting the full registry. Connections closing during a scan may leave a short page; cursor order and filtering remain best effort under concurrent changes.
18
-
19
- ```
20
- Data Plane (lock-free) Background (1Hz) Read Path
21
- ───────────────────── ────────────────── ─────────
22
- record_bytes() ──> AtomicU64 ──┐
23
- record_http_request() ──> AtomicU64 ──┤
24
- connection_opened/closed() ──> AtomicU64 ──┤ sample_all() snapshot()
25
- backend_*() ──> DashMap<AtomicU64> ──┤────> drain atomics ──────> Metrics struct
26
- protocol_*() ──> AtomicU64 ──┤ feed ThroughputTrackers ──> JSON
27
- datagram_*() ──> AtomicU64 ──┘ prune orphans ──> IPC stdout
28
- ──> TS cache
29
- ──> IMetrics API
30
- ```
31
-
32
- ### Key Types
33
-
34
- | Type | Crate | Purpose |
35
- |---|---|---|
36
- | `MetricsCollector` | `rustproxy-metrics` | Central store. All DashMaps, atomics, and throughput trackers |
37
- | `ThroughputTracker` | `rustproxy-metrics` | Circular buffer of 1Hz samples. Default 3600 capacity (1 hour) |
38
- | `ForwardMetricsCtx` | `rustproxy-passthrough` | Carries `Arc<MetricsCollector>` + route_id + source_ip through TCP forwarding |
39
- | `CountingBody` | `rustproxy-http` | Wraps HTTP bodies, batches byte recording per 64KB, flushes on drop |
40
- | `ProtocolGuard` | `rustproxy-http` | RAII guard for frontend/backend protocol active/total counters |
41
- | `ConnectionGuard` | `rustproxy-passthrough` | RAII guard calling `connection_closed()` on drop |
42
- | `RustMetricsAdapter` | TypeScript | Polls Rust via IPC, implements `IMetrics` interface over cached JSON |
43
-
44
- ---
45
-
46
- ## What's Collected
47
-
48
- ### Global Counters
49
-
50
- | Metric | Type | Updated by |
51
- |---|---|---|
52
- | Active connections | AtomicU64 | `connection_opened/closed` |
53
- | Total connections (lifetime) | AtomicU64 | `connection_opened` |
54
- | Total bytes in | AtomicU64 | `record_bytes` |
55
- | Total bytes out | AtomicU64 | `record_bytes` |
56
- | Total HTTP requests | AtomicU64 | `record_http_request` |
57
- | Active UDP sessions | AtomicU64 | `udp_session_opened/closed` |
58
- | Total UDP sessions | AtomicU64 | `udp_session_opened` |
59
- | Total datagrams in | AtomicU64 | `record_datagram_in` |
60
- | Total datagrams out | AtomicU64 | `record_datagram_out` |
61
-
62
- ### Per-Route Metrics (keyed by route ID string)
63
-
64
- | Metric | Storage |
65
- |---|---|
66
- | Active connections | `DashMap<String, AtomicU64>` |
67
- | Total connections | `DashMap<String, AtomicU64>` |
68
- | Bytes in / out | `DashMap<String, AtomicU64>` |
69
- | Pending throughput (in, out) | `DashMap<String, (AtomicU64, AtomicU64)>` |
70
- | Throughput history | `DashMap<String, Mutex<ThroughputTracker>>` |
71
-
72
- Entries are pruned via `retain_routes()` when routes are removed.
73
-
74
- ### Per-IP Metrics (keyed by IP string)
75
-
76
- | Metric | Storage |
77
- |---|---|
78
- | Active connections | `DashMap<String, AtomicU64>` |
79
- | Total connections | `DashMap<String, AtomicU64>` |
80
- | Bytes in / out | `DashMap<String, AtomicU64>` |
81
- | Pending throughput (in, out) | `DashMap<String, (AtomicU64, AtomicU64)>` |
82
- | Throughput history | `DashMap<String, Mutex<ThroughputTracker>>` |
83
- | Domain requests | `DashMap<String, DashMap<String, AtomicU64>>` (IP → domain → count) |
84
-
85
- All seven maps for an IP are evicted when its active connection count drops to 0. Safety-net pruning in `sample_all()` catches entries orphaned by races. Snapshots cap at 100 IPs, sorted by active connections descending.
86
-
87
- **Domain request tracking:** Records which domains each frontend IP has requested. Populated from HTTP Host headers (for HTTP/1.1, HTTP/2, HTTP/3 requests) and from SNI (for TLS passthrough connections). Domain keys are normalized to lowercase with any trailing dot stripped so the same hostname does not fragment across multiple counters. Capped at 256 domains per IP (`MAX_DOMAINS_PER_IP`) to prevent subdomain-spray abuse. Inner DashMap uses 2 shards to minimise base memory per IP (~200 bytes). Common case (IP + domain both known) is two DashMap reads + one atomic increment with zero allocation.
88
-
89
- ### Per-Backend Metrics (keyed by "host:port")
90
-
91
- | Metric | Storage |
92
- |---|---|
93
- | Active connections | `DashMap<String, AtomicU64>` |
94
- | Total connections | `DashMap<String, AtomicU64>` |
95
- | Detected protocol (h1/h2/h3) | `DashMap<String, String>` |
96
- | Connect errors | `DashMap<String, AtomicU64>` |
97
- | Handshake errors | `DashMap<String, AtomicU64>` |
98
- | Request errors | `DashMap<String, AtomicU64>` |
99
- | Total connect time (microseconds) | `DashMap<String, AtomicU64>` |
100
- | Connect count | `DashMap<String, AtomicU64>` |
101
- | Pool hits | `DashMap<String, AtomicU64>` |
102
- | Pool misses | `DashMap<String, AtomicU64>` |
103
- | H2 failures (fallback to H1) | `DashMap<String, AtomicU64>` |
104
-
105
- All per-backend maps are evicted when active count reaches 0. Pruned via `retain_backends()`.
106
-
107
- ### Frontend Protocol Distribution
108
-
109
- Tracked via `ProtocolGuard` RAII guards and `FrontendProtocolTracker`. Five protocol categories, each with active + total counters (AtomicU64):
110
-
111
- | Protocol | Where detected |
112
- |---|---|
113
- | h1 | `FrontendProtocolTracker` on first HTTP/1.x request |
114
- | h2 | `FrontendProtocolTracker` on first HTTP/2 request |
115
- | h3 | `ProtocolGuard::frontend("h3")` in H3ProxyService |
116
- | ws | `ProtocolGuard::frontend("ws")` on WebSocket upgrade |
117
- | other | Fallback (TCP passthrough without HTTP) |
118
-
119
- Uses `fetch_update` for saturating decrements to prevent underflow races. The same saturating-close pattern is also used for connection and UDP active gauges.
120
-
121
- ### Backend Protocol Distribution
122
-
123
- Same five categories (h1/h2/h3/ws/other), tracked via `ProtocolGuard::backend()` at connection establishment. Backend h2 failures (fallback to h1) are separately counted.
124
-
125
- ### Throughput History
126
-
127
- `ThroughputTracker` is a circular buffer storing `ThroughputSample { timestamp_ms, bytes_in, bytes_out }` at 1Hz.
128
-
129
- - Global tracker: 1 instance, default 3600 capacity
130
- - Per-route trackers: 1 per active route
131
- - Per-IP trackers: 1 per connected IP (evicted with the IP)
132
- - HTTP request tracker: reuses ThroughputTracker with bytes_in = request count, bytes_out = 0
133
-
134
- Query methods:
135
- - `instant()` — last 1 second average
136
- - `recent()` — last 10 seconds average
137
- - `throughput(N)` — last N seconds average
138
- - `history(N)` — last N raw samples in chronological order
139
-
140
- Snapshots return 60 samples of global throughput history.
141
-
142
- ### Protocol Detection Cache
143
-
144
- Not part of MetricsCollector. Maintained by `HttpProxyService`'s protocol detection system. Injected into the metrics snapshot at read time by `get_metrics()`.
145
-
146
- Each entry records: host, port, domain, detected protocol (h1/h2/h3), H3 port, age, last accessed, last probed, suppression flags, cooldown timers, consecutive failure counts.
147
-
148
- ---
149
-
150
- ## Instrumentation Points
151
-
152
- ### TCP Passthrough (`rustproxy-passthrough`)
153
-
154
- **Connection lifecycle** — `tcp_listener.rs`:
155
- - Accept: `conn_tracker.connection_opened(&ip)` (rate limiter) + `ConnectionTrackerGuard` RAII
156
- - Route match: `metrics.connection_opened(route_id, source_ip)` + `ConnectionGuard` RAII
157
- - Close: Both guards call their respective `_closed()` methods on drop
158
-
159
- **Byte recording** — `forwarder.rs` (`forward_bidirectional_with_timeouts`):
160
- - Initial peeked data recorded immediately
161
- - Per-chunk in both directions: `record_bytes(n, 0, ...)` / `record_bytes(0, n, ...)`
162
- - Same pattern in `forward_bidirectional_split_with_timeouts` (tcp_listener.rs) for TLS-terminated paths
163
-
164
- ### HTTP Proxy (`rustproxy-http`)
165
-
166
- **Request counting** — `proxy_service.rs`:
167
- - `record_http_request()` called once per request after route matching succeeds
168
-
169
- **Body byte counting** — `counting_body.rs` wrapping:
170
- - Request bodies: `CountingBody::new(body, ..., Direction::In)` — counts client-to-upstream bytes
171
- - Response bodies: `CountingBody::new(body, ..., Direction::Out)` — counts upstream-to-client bytes
172
- - Batched flush every 64KB (`BYTE_FLUSH_THRESHOLD = 65_536`), remainder flushed on drop
173
- - Also updates `connection_activity` atomic (idle watchdog) and `active_requests` counter (streaming detection)
174
-
175
- **Backend metrics** — `proxy_service.rs`:
176
- - `backend_connection_opened(key, connect_time)` — after TCP/TLS connect succeeds
177
- - `backend_connection_closed(key)` — on teardown
178
- - `backend_connect_error(key)` — TCP/TLS connect failure or timeout
179
- - `backend_handshake_error(key)` — H1/H2 protocol handshake failure
180
- - `backend_request_error(key)` — send_request failure
181
- - `backend_h2_failure(key)` — H2 attempted, fell back to H1
182
- - `backend_pool_hit(key)` / `backend_pool_miss(key)` — connection pool reuse
183
- - `set_backend_protocol(key, proto)` — records detected protocol
184
-
185
- **WebSocket** — `proxy_service.rs`:
186
- - Does NOT use CountingBody; records bytes directly per-chunk in both directions of the bidirectional copy loop
187
-
188
- ### QUIC (`rustproxy-passthrough`)
189
-
190
- **Connection level** — `quic_handler.rs`:
191
- - `connection_opened` / `connection_closed` via `QuicConnGuard` RAII
192
- - `conn_tracker.connection_opened/closed` for rate limiting
193
-
194
- **Stream level**:
195
- - For QUIC-to-TCP stream forwarding: `record_bytes(bytes_in, bytes_out, ...)` called once per stream at completion (post-hoc, not per-chunk)
196
- - For HTTP/3: delegates to `HttpProxyService.handle_request()`, so all HTTP proxy metrics apply
197
-
198
- **H3 specifics** — `h3_service.rs`:
199
- - `ProtocolGuard::frontend("h3")` tracks the H3 connection
200
- - H3 request bodies: `record_bytes(data.len(), 0, ...)` called directly (not CountingBody) since H3 uses `stream.send_data()`
201
- - H3 response bodies: wrapped in CountingBody like HTTP/1 and HTTP/2
202
-
203
- ### UDP (`rustproxy-passthrough`)
204
-
205
- **Session lifecycle** — `udp_listener.rs` / `udp_session.rs`:
206
- - `udp_session_opened()` + `connection_opened(route_id, source_ip)` on new session
207
- - `udp_session_closed()` + `connection_closed(route_id, source_ip)` on idle reap or port drain
208
-
209
- **Datagram counting** — `udp_listener.rs`:
210
- - Inbound: `record_bytes(len, 0, ...)` + `record_datagram_in()`
211
- - Outbound (backend reply): `record_bytes(0, len, ...)` + `record_datagram_out()`
212
-
213
- ---
214
-
215
- ## Sampling Loop
216
-
217
- `lib.rs` spawns a tokio task at configurable interval (default 1000ms):
218
-
219
- ```rust
220
- loop {
221
- tokio::select! {
222
- _ = cancel => break,
223
- _ = interval.tick() => {
224
- metrics.sample_all();
225
- conn_tracker.cleanup_stale_timestamps();
226
- http_proxy.cleanup_all_rate_limiters();
227
- }
228
- }
229
- }
230
- ```
231
-
232
- `sample_all()` performs in one pass:
233
- 1. Drains `global_pending_tp_in/out` into global ThroughputTracker, samples
234
- 2. Drains per-route pending counters into per-route trackers, samples each
235
- 3. Samples idle route trackers (no new data) to advance their window
236
- 4. Drains per-IP pending counters into per-IP trackers, samples each
237
- 5. Drains `pending_http_requests` into HTTP request throughput tracker
238
- 6. Prunes orphaned per-IP entries (bytes/throughput maps with no matching ip_connections key)
239
- 7. Prunes orphaned per-backend entries (error/stats maps with no matching active/total key)
240
-
241
- ---
242
-
243
- ## Data Flow: Rust to TypeScript
244
-
245
- ```
246
- MetricsCollector.snapshot()
247
- ├── reads all AtomicU64 counters
248
- ├── iterates DashMaps (routes, IPs, backends)
249
- ├── locks ThroughputTrackers for instant/recent rates + history
250
- └── produces Metrics struct
251
-
252
- RustProxy::get_metrics()
253
- ├── calls snapshot()
254
- ├── enriches with detectedProtocols from HTTP proxy protocol cache
255
- └── returns Metrics
256
-
257
- management.rs "getMetrics" IPC command
258
- ├── calls get_metrics()
259
- ├── serde_json::to_value (camelCase)
260
- └── writes JSON to stdout
261
-
262
- RustProxyBridge (TypeScript)
263
- ├── reads JSON from Rust process stdout
264
- └── returns parsed object
265
-
266
- RustMetricsAdapter
267
- ├── setInterval polls bridge.getMetrics() every 1s
268
- ├── stores raw JSON in this.cache
269
- └── IMetrics methods read synchronously from cache
270
-
271
- SmartProxy.getMetrics()
272
- └── returns the RustMetricsAdapter instance
273
- ```
274
-
275
- ### IPC JSON Shape (Metrics)
276
-
277
- ```json
278
- {
279
- "activeConnections": 42,
280
- "totalConnections": 1000,
281
- "bytesIn": 123456789,
282
- "bytesOut": 987654321,
283
- "throughputInBytesPerSec": 50000,
284
- "throughputOutBytesPerSec": 80000,
285
- "throughputRecentInBytesPerSec": 45000,
286
- "throughputRecentOutBytesPerSec": 75000,
287
- "routes": {
288
- "<route-id>": {
289
- "activeConnections": 5,
290
- "totalConnections": 100,
291
- "bytesIn": 0, "bytesOut": 0,
292
- "throughputInBytesPerSec": 0, "throughputOutBytesPerSec": 0,
293
- "throughputRecentInBytesPerSec": 0, "throughputRecentOutBytesPerSec": 0
294
- }
295
- },
296
- "ips": {
297
- "<ip>": {
298
- "activeConnections": 2, "totalConnections": 10,
299
- "bytesIn": 0, "bytesOut": 0,
300
- "throughputInBytesPerSec": 0, "throughputOutBytesPerSec": 0,
301
- "domainRequests": {
302
- "example.com": 4821,
303
- "api.example.com": 312
304
- }
305
- }
306
- },
307
- "backends": {
308
- "<host:port>": {
309
- "activeConnections": 3, "totalConnections": 50,
310
- "protocol": "h2",
311
- "connectErrors": 0, "handshakeErrors": 0, "requestErrors": 0,
312
- "totalConnectTimeUs": 150000, "connectCount": 50,
313
- "poolHits": 40, "poolMisses": 10, "h2Failures": 1
314
- }
315
- },
316
- "throughputHistory": [
317
- { "timestampMs": 1713000000000, "bytesIn": 50000, "bytesOut": 80000 }
318
- ],
319
- "totalHttpRequests": 5000,
320
- "httpRequestsPerSec": 100,
321
- "httpRequestsPerSecRecent": 95,
322
- "activeUdpSessions": 0, "totalUdpSessions": 5,
323
- "totalDatagramsIn": 1000, "totalDatagramsOut": 1000,
324
- "frontendProtocols": {
325
- "h1Active": 10, "h1Total": 500,
326
- "h2Active": 5, "h2Total": 200,
327
- "h3Active": 1, "h3Total": 50,
328
- "wsActive": 2, "wsTotal": 30,
329
- "otherActive": 0, "otherTotal": 0
330
- },
331
- "backendProtocols": { "...same shape..." },
332
- "detectedProtocols": [
333
- {
334
- "host": "backend", "port": 443, "domain": "example.com",
335
- "protocol": "h2", "h3Port": 443,
336
- "ageSecs": 120, "lastAccessedSecs": 5, "lastProbedSecs": 120,
337
- "h2Suppressed": false, "h3Suppressed": false,
338
- "h2CooldownRemainingSecs": null, "h3CooldownRemainingSecs": null,
339
- "h2ConsecutiveFailures": null, "h3ConsecutiveFailures": null
340
- }
341
- ]
342
- }
343
- ```
344
-
345
- ### IPC JSON Shape (Statistics)
346
-
347
- Lightweight administrative summary, fetched on-demand (not polled):
348
-
349
- ```json
350
- {
351
- "activeConnections": 42,
352
- "totalConnections": 1000,
353
- "routesCount": 5,
354
- "listeningPorts": [80, 443, 8443],
355
- "uptimeSeconds": 86400
356
- }
357
- ```
358
-
359
- ---
360
-
361
- ## TypeScript Consumer API
362
-
363
- `SmartProxy.getMetrics()` returns an `IMetrics` object. All members are synchronous methods reading from the polled cache.
364
-
365
- ### connections
366
-
367
- | Method | Return | Source |
368
- |---|---|---|
369
- | `active()` | `number` | `cache.activeConnections` |
370
- | `total()` | `number` | `cache.totalConnections` |
371
- | `byRoute()` | `Map<string, number>` | `cache.routes[name].activeConnections` |
372
- | `byIP()` | `Map<string, number>` | `cache.ips[ip].activeConnections` |
373
- | `topIPs(limit?)` | `Array<{ip, count}>` | `cache.ips` sorted by active desc, default 10 |
374
- | `domainRequestsByIP()` | `Map<string, Map<string, number>>` | `cache.ips[ip].domainRequests` |
375
- | `topDomainRequests(limit?)` | `Array<{ip, domain, count}>` | Flattened from all IPs, sorted by count desc, default 20 |
376
- | `frontendProtocols()` | `IProtocolDistribution` | `cache.frontendProtocols.*` |
377
- | `backendProtocols()` | `IProtocolDistribution` | `cache.backendProtocols.*` |
378
-
379
- ### throughput
380
-
381
- | Method | Return | Source |
382
- |---|---|---|
383
- | `instant()` | `{in, out}` | `cache.throughputInBytesPerSec/Out` |
384
- | `recent()` | `{in, out}` | `cache.throughputRecentInBytesPerSec/Out` |
385
- | `average()` | `{in, out}` | Falls back to `instant()` (not wired to windowed average) |
386
- | `custom(seconds)` | `{in, out}` | Falls back to `instant()` (not wired) |
387
- | `history(seconds)` | `IThroughputHistoryPoint[]` | `cache.throughputHistory` sliced to last N entries |
388
- | `byRoute(windowSeconds?)` | `Map<string, {in, out}>` | `cache.routes[name].throughputIn/OutBytesPerSec` |
389
- | `byIP(windowSeconds?)` | `Map<string, {in, out}>` | `cache.ips[ip].throughputIn/OutBytesPerSec` |
390
-
391
- ### requests
392
-
393
- | Method | Return | Source |
394
- |---|---|---|
395
- | `perSecond()` | `number` | `cache.httpRequestsPerSec` |
396
- | `perMinute()` | `number` | `cache.httpRequestsPerSecRecent * 60` |
397
- | `total()` | `number` | `cache.totalHttpRequests` (fallback: totalConnections) |
398
-
399
- ### totals
400
-
401
- | Method | Return | Source |
402
- |---|---|---|
403
- | `bytesIn()` | `number` | `cache.bytesIn` |
404
- | `bytesOut()` | `number` | `cache.bytesOut` |
405
- | `connections()` | `number` | `cache.totalConnections` |
406
-
407
- ### backends
408
-
409
- | Method | Return | Source |
410
- |---|---|---|
411
- | `byBackend()` | `Map<string, IBackendMetrics>` | `cache.backends[key].*` with computed `avgConnectTimeMs` and `poolHitRate` |
412
- | `protocols()` | `Map<string, string>` | `cache.backends[key].protocol` |
413
- | `topByErrors(limit?)` | `IBackendMetrics[]` | Sorted by total errors desc |
414
- | `detectedProtocols()` | `IProtocolCacheEntry[]` | `cache.detectedProtocols` passthrough |
415
-
416
- `IBackendMetrics`: `{ protocol, activeConnections, totalConnections, connectErrors, handshakeErrors, requestErrors, avgConnectTimeMs, poolHitRate, h2Failures }`
417
-
418
- ### udp
419
-
420
- | Method | Return | Source |
421
- |---|---|---|
422
- | `activeSessions()` | `number` | `cache.activeUdpSessions` |
423
- | `totalSessions()` | `number` | `cache.totalUdpSessions` |
424
- | `datagramsIn()` | `number` | `cache.totalDatagramsIn` |
425
- | `datagramsOut()` | `number` | `cache.totalDatagramsOut` |
426
-
427
- ### percentiles (stub)
428
-
429
- `connectionDuration()` and `bytesTransferred()` always return zeros. Not implemented.
430
-
431
- ---
432
-
433
- ## Configuration
434
-
435
- ```typescript
436
- interface IMetricsConfig {
437
- enabled: boolean; // default true
438
- sampleIntervalMs: number; // default 1000 (1Hz sampling + TS polling)
439
- retentionSeconds: number; // default 3600 (ThroughputTracker capacity)
440
- enableDetailedTracking: boolean;
441
- enablePercentiles: boolean;
442
- cacheResultsMs: number;
443
- prometheusEnabled: boolean; // not wired
444
- prometheusPath: string; // not wired
445
- prometheusPrefix: string; // not wired
446
- }
447
- ```
448
-
449
- Rust-side config (`MetricsConfig` in `rustproxy-config`):
450
-
451
- ```rust
452
- pub struct MetricsConfig {
453
- pub enabled: Option<bool>,
454
- pub sample_interval_ms: Option<u64>, // default 1000
455
- pub retention_seconds: Option<u64>, // default 3600
456
- }
457
- ```
458
-
459
- ---
460
-
461
- ## Design Decisions
462
-
463
- **Lock-free hot path.** `record_bytes()` is the most frequently called method (per-chunk in TCP, per-64KB in HTTP). It only touches `AtomicU64` with `Relaxed` ordering and short-circuits zero-byte directions to skip DashMap lookups entirely.
464
-
465
- **CountingBody batching.** HTTP body frames are typically 16KB. Flushing to MetricsCollector every 64KB reduces DashMap shard contention by ~4x compared to per-frame recording.
466
-
467
- **RAII guards everywhere.** `ConnectionGuard`, `ConnectionTrackerGuard`, `QuicConnGuard`, `ProtocolGuard`, `FrontendProtocolTracker` all use Drop to guarantee counter cleanup on all exit paths including panics.
468
-
469
- **Saturating decrements.** Protocol counters use `fetch_update` loops instead of `fetch_sub` to prevent underflow to `u64::MAX` from concurrent close races.
470
-
471
- **Bounded memory.** Per-IP entries evicted on last connection close. Per-backend entries evicted on last connection close. Snapshot caps IPs and backends at 100 each. `sample_all()` prunes orphaned entries every second.
472
-
473
- **Two-phase throughput.** Pending bytes accumulate in lock-free atomics. The 1Hz cold path drains them into Mutex-guarded ThroughputTrackers. This means the hot path never contends on a Mutex, while the cold path does minimal work (one drain + one sample per tracker).
474
-
475
- ---
476
-
477
- ## Known Gaps
478
-
479
- | Gap | Status |
480
- |---|---|
481
- | `throughput.average()` / `throughput.custom(seconds)` | Fall back to `instant()`. Not wired to Rust windowed queries. |
482
- | `percentiles.connectionDuration()` / `percentiles.bytesTransferred()` | Stub returning zeros. No histogram in Rust. |
483
- | Prometheus export | Config fields exist but not wired to any exporter. |
484
- | `LogDeduplicator` | Implemented in `rustproxy-metrics` but not connected to any call site. |
485
- | Rate limit hit counters | Rate-limited requests return 429 but no counter is recorded in MetricsCollector. |
486
- | QUIC stream byte counting | Post-hoc (per-stream totals after close), not per-chunk like TCP. |
487
- | Throughput history in snapshot | Capped at 60 samples. TS `history(seconds)` cannot return more than 60 points regardless of `retentionSeconds`. |
488
- | Per-route total connections / bytes | Available in Rust JSON but `IMetrics.connections.byRoute()` only exposes active connections. |
489
- | Per-IP total connections / bytes | Available in Rust JSON but `IMetrics.connections.byIP()` only exposes active connections. |
490
- | IPC response typing | `RustProxyBridge` declares `result: any` for both metrics commands. No type-safe response. |
package/readme.plan.md DELETED
Binary file