pi-codemcp 1.2.2 → 1.3.1

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/sidecar/stats.py CHANGED
@@ -1,30 +1,109 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import json
5
- import os
4
+ import sqlite3
6
5
  import time
7
6
  from contextlib import suppress
8
7
  from dataclasses import dataclass, field
9
- from pathlib import Path
10
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Literal
11
9
 
12
10
  from .json_types import JSON_OBJECT_ADAPTER, JSON_VALUE_ADAPTER, JsonObject
13
11
 
14
12
  if TYPE_CHECKING:
15
13
  from collections.abc import Mapping
14
+ from pathlib import Path
16
15
 
17
16
  HISTOGRAM_BOUNDS_MS = (1, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000)
18
17
  HISTOGRAM_BOUNDS_BYTES = (64, 256, 1_024, 4_096, 16_384, 65_536, 262_144, 1_048_576)
19
18
  RECENT_BUCKET_SECONDS = 60 * 60
20
19
  RECENT_BUCKET_COUNT = 24
20
+ RECENT_FAILURE_LIMIT = 200
21
21
  FLUSH_DELAY_SECONDS = 5.0
22
22
  MAX_OPERATIONS = 32
23
23
  MAX_PHASES = 16
24
24
  MAX_SERVERS = 64
25
25
  MAX_TOOLS = 384
26
26
  MAX_FAILURE_STAGES = 16
27
+ MAX_OUTCOMES = 16
27
28
  OTHER_DIMENSION = "<other>"
29
+ SQLITE_SCHEMA_VERSION = 1
30
+ SQLITE_BUSY_TIMEOUT_MS = 30_000
31
+ MAX_HISTOGRAM_BUCKETS = len(HISTOGRAM_BOUNDS_MS) + 1
32
+ TRACE_ID_LIMIT = 256
33
+ DIMENSION_VALUE_LIMIT = 256
34
+ PACKAGE_VERSION_LIMIT = 64
35
+ HISTOGRAM_BUCKET_NAMES = (
36
+ "bucket_0",
37
+ "bucket_1",
38
+ "bucket_2",
39
+ "bucket_3",
40
+ "bucket_4",
41
+ "bucket_5",
42
+ "bucket_6",
43
+ "bucket_7",
44
+ "bucket_8",
45
+ "bucket_9",
46
+ "bucket_10",
47
+ "bucket_11",
48
+ "bucket_12",
49
+ "bucket_13",
50
+ )
51
+ HISTOGRAM_UPSERT_SQL = """
52
+ INSERT INTO histograms(
53
+ dimension, name, metric, count, total, maximum,
54
+ bucket_0, bucket_1, bucket_2, bucket_3, bucket_4, bucket_5, bucket_6,
55
+ bucket_7, bucket_8, bucket_9, bucket_10, bucket_11, bucket_12, bucket_13
56
+ ) VALUES (
57
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
58
+ )
59
+ ON CONFLICT(dimension, name, metric) DO UPDATE SET
60
+ count = histograms.count + excluded.count,
61
+ total = histograms.total + excluded.total,
62
+ maximum = MAX(histograms.maximum, excluded.maximum),
63
+ bucket_0 = histograms.bucket_0 + excluded.bucket_0,
64
+ bucket_1 = histograms.bucket_1 + excluded.bucket_1,
65
+ bucket_2 = histograms.bucket_2 + excluded.bucket_2,
66
+ bucket_3 = histograms.bucket_3 + excluded.bucket_3,
67
+ bucket_4 = histograms.bucket_4 + excluded.bucket_4,
68
+ bucket_5 = histograms.bucket_5 + excluded.bucket_5,
69
+ bucket_6 = histograms.bucket_6 + excluded.bucket_6,
70
+ bucket_7 = histograms.bucket_7 + excluded.bucket_7,
71
+ bucket_8 = histograms.bucket_8 + excluded.bucket_8,
72
+ bucket_9 = histograms.bucket_9 + excluded.bucket_9,
73
+ bucket_10 = histograms.bucket_10 + excluded.bucket_10,
74
+ bucket_11 = histograms.bucket_11 + excluded.bucket_11,
75
+ bucket_12 = histograms.bucket_12 + excluded.bucket_12,
76
+ bucket_13 = histograms.bucket_13 + excluded.bucket_13
77
+ """
78
+ HISTOGRAM_READ_SQL = """
79
+ SELECT dimension, name, metric, count, total, maximum,
80
+ bucket_0, bucket_1, bucket_2, bucket_3, bucket_4, bucket_5, bucket_6,
81
+ bucket_7, bucket_8, bucket_9, bucket_10, bucket_11, bucket_12, bucket_13
82
+ FROM histograms
83
+ WHERE dimension != 'phase'
84
+ """
85
+ PHASE_READ_SQL = """
86
+ SELECT name, count, total, maximum,
87
+ bucket_0, bucket_1, bucket_2, bucket_3, bucket_4, bucket_5, bucket_6,
88
+ bucket_7, bucket_8, bucket_9, bucket_10, bucket_11, bucket_12, bucket_13
89
+ FROM histograms
90
+ WHERE dimension = 'phase' AND metric = 'duration_ms'
91
+ """
92
+ DISTINCT_NAME_QUERIES = {
93
+ "rollups": "SELECT DISTINCT name FROM rollups WHERE dimension = ?",
94
+ "histograms": "SELECT DISTINCT name FROM histograms WHERE dimension = ?",
95
+ "counters": "SELECT DISTINCT name FROM counters WHERE dimension = ?",
96
+ }
97
+
98
+ FailureOutcome = Literal[
99
+ "success",
100
+ "preflight_rejection",
101
+ "result_refinement",
102
+ "upstream_failure",
103
+ "transport_failure",
104
+ "cancellation",
105
+ "internal_error",
106
+ ]
28
107
 
29
108
 
30
109
  @dataclass
@@ -46,6 +125,15 @@ class Histogram:
46
125
  return
47
126
  self.counts[-1] += 1
48
127
 
128
+ def merge(self, other: Histogram) -> None:
129
+ if self.bounds != other.bounds:
130
+ raise ValueError("cannot merge histograms with different bounds")
131
+ self.count += other.count
132
+ self.total += other.total
133
+ self.maximum = max(self.maximum, other.maximum)
134
+ for index, value in enumerate(other.counts):
135
+ self.counts[index] += value
136
+
49
137
  def snapshot(self) -> JsonObject:
50
138
  buckets: list[JsonObject] = [
51
139
  {"le": bound, "count": count}
@@ -60,27 +148,6 @@ class Histogram:
60
148
  "buckets": JSON_VALUE_ADAPTER.validate_python(buckets),
61
149
  }
62
150
 
63
- @classmethod
64
- def from_snapshot(
65
- cls,
66
- value: object,
67
- *,
68
- bounds: tuple[int, ...] = HISTOGRAM_BOUNDS_MS,
69
- ) -> Histogram:
70
- histogram = cls(bounds=bounds, counts=[0] * (len(bounds) + 1))
71
- if not isinstance(value, dict):
72
- return histogram
73
- histogram.count = _integer(value.get("count"))
74
- histogram.total = _number(value.get("sum"))
75
- histogram.maximum = _number(value.get("max"))
76
- raw_buckets = value.get("buckets")
77
- if isinstance(raw_buckets, list) and len(raw_buckets) == len(histogram.counts):
78
- histogram.counts = [
79
- _integer(bucket.get("count")) if isinstance(bucket, dict) else 0
80
- for bucket in raw_buckets
81
- ]
82
- return histogram
83
-
84
151
 
85
152
  @dataclass
86
153
  class Rollup:
@@ -128,6 +195,18 @@ class Rollup:
128
195
  self.input_size_bytes.observe(float(max(0, input_bytes)))
129
196
  self.output_size_bytes.observe(float(max(0, output_bytes)))
130
197
 
198
+ def merge(self, other: Rollup) -> None:
199
+ self.count += other.count
200
+ self.success += other.success
201
+ self.failure += other.failure
202
+ self.input_bytes += other.input_bytes
203
+ self.output_bytes += other.output_bytes
204
+ self.calls += other.calls
205
+ self.chain_calls += other.chain_calls
206
+ self.duration_ms.merge(other.duration_ms)
207
+ self.input_size_bytes.merge(other.input_size_bytes)
208
+ self.output_size_bytes.merge(other.output_size_bytes)
209
+
131
210
  def snapshot(self, *, include_distributions: bool = True) -> JsonObject:
132
211
  values: JsonObject = {
133
212
  "count": self.count,
@@ -144,89 +223,157 @@ class Rollup:
144
223
  values["output_size_bytes"] = self.output_size_bytes.snapshot()
145
224
  return values
146
225
 
147
- @classmethod
148
- def from_snapshot(cls, value: object) -> Rollup:
149
- if not isinstance(value, dict):
150
- return cls()
151
- return cls(
152
- count=_integer(value.get("count")),
153
- success=_integer(value.get("success")),
154
- failure=_integer(value.get("failure")),
155
- input_bytes=_integer(value.get("input_bytes")),
156
- output_bytes=_integer(value.get("output_bytes")),
157
- calls=_integer(value.get("calls")),
158
- chain_calls=_integer(value.get("chain_calls")),
159
- duration_ms=Histogram.from_snapshot(value.get("duration_ms")),
160
- input_size_bytes=Histogram.from_snapshot(
161
- value.get("input_size_bytes"), bounds=HISTOGRAM_BOUNDS_BYTES
162
- ),
163
- output_size_bytes=Histogram.from_snapshot(
164
- value.get("output_size_bytes"), bounds=HISTOGRAM_BOUNDS_BYTES
165
- ),
166
- )
226
+
227
+ @dataclass(frozen=True)
228
+ class OperationFailure:
229
+ stage: str
230
+ trace_id: str
231
+ subtype: str | None = None
232
+ server: str | None = None
233
+ tool: str | None = None
234
+
235
+
236
+ @dataclass(frozen=True)
237
+ class OperationObservation:
238
+ duration_ms: float
239
+ success: bool
240
+ input_bytes: int = 0
241
+ output_bytes: int = 0
242
+ calls: int = 0
243
+ chain_calls: int = 0
244
+ failure: OperationFailure | None = None
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class FailureEvent:
249
+ timestamp: int
250
+ trace_id: str
251
+ operation: str
252
+ stage: str
253
+ subtype: str
254
+ calls: int
255
+ chain_calls: int
256
+ server: str | None
257
+ tool: str | None
258
+ package_version: str
259
+
260
+ def snapshot(self) -> JsonObject:
261
+ values: JsonObject = {
262
+ "timestamp": self.timestamp,
263
+ "operation": self.operation,
264
+ "stage": self.stage,
265
+ "subtype": self.subtype,
266
+ "calls": self.calls,
267
+ "chain_calls": self.chain_calls,
268
+ "package_version": self.package_version,
269
+ }
270
+ values["trace_id"] = self.trace_id
271
+ if self.server is not None:
272
+ values["server"] = self.server
273
+ if self.tool is not None:
274
+ values["tool"] = self.tool
275
+ return values
276
+
277
+
278
+ @dataclass
279
+ class StatsAccumulator:
280
+ lifetime: Rollup = field(default_factory=Rollup)
281
+ operations: dict[str, Rollup] = field(default_factory=dict)
282
+ phases: dict[str, Histogram] = field(default_factory=dict)
283
+ servers: dict[str, Rollup] = field(default_factory=dict)
284
+ tools: dict[str, Rollup] = field(default_factory=dict)
285
+ failures: dict[str, int] = field(default_factory=dict)
286
+ outcomes: dict[str, int] = field(default_factory=dict)
287
+ cache_hits: int = 0
288
+ cache_misses: int = 0
289
+ recent: dict[int, Rollup] = field(default_factory=dict)
290
+ failure_events: list[FailureEvent] = field(default_factory=list)
291
+ updated_at: int = 0
292
+
293
+ def merge(self, other: StatsAccumulator) -> None:
294
+ self.lifetime.merge(other.lifetime)
295
+ _merge_rollup_mapping(self.operations, other.operations, MAX_OPERATIONS)
296
+ _merge_histogram_mapping(self.phases, other.phases, MAX_PHASES)
297
+ _merge_rollup_mapping(self.servers, other.servers, MAX_SERVERS)
298
+ _merge_rollup_mapping(self.tools, other.tools, MAX_TOOLS)
299
+ _merge_counter_mapping(self.failures, other.failures, MAX_FAILURE_STAGES)
300
+ _merge_counter_mapping(self.outcomes, other.outcomes, MAX_OUTCOMES)
301
+ self.cache_hits += other.cache_hits
302
+ self.cache_misses += other.cache_misses
303
+ for timestamp, rollup in other.recent.items():
304
+ self.recent.setdefault(timestamp, Rollup()).merge(rollup)
305
+ while len(self.recent) > RECENT_BUCKET_COUNT:
306
+ del self.recent[min(self.recent)]
307
+ self.failure_events.extend(other.failure_events)
308
+ self.failure_events = self.failure_events[-RECENT_FAILURE_LIMIT:]
309
+ self.updated_at = max(self.updated_at, other.updated_at)
167
310
 
168
311
 
169
312
  class StatsStore:
170
- def __init__(self, path: Path) -> None:
313
+ def __init__(self, path: Path, *, package_version: str = "unknown") -> None:
171
314
  self.path = path
172
- self.lifetime = Rollup()
173
- self.operations: dict[str, Rollup] = {}
174
- self.phases: dict[str, Histogram] = {}
175
- self.servers: dict[str, Rollup] = {}
176
- self.tools: dict[str, Rollup] = {}
177
- self.failures: dict[str, int] = {}
178
- self.cache_hits = 0
179
- self.cache_misses = 0
180
- self.recent: dict[int, Rollup] = {}
181
- self.updated_at = 0
315
+ self.package_version = _bounded_text(package_version, PACKAGE_VERSION_LIMIT) or "unknown"
316
+ self._delta = StatsAccumulator()
182
317
  self._dirty = False
183
318
  self._closing = False
184
319
  self._flush_task: asyncio.Task[None] | None = None
185
- self._load()
320
+ self._flush_lock = asyncio.Lock()
186
321
 
187
- def record_operation(
188
- self,
189
- name: str,
190
- *,
191
- duration_ms: float,
192
- success: bool,
193
- failure_stage: str | None = None,
194
- input_bytes: int = 0,
195
- output_bytes: int = 0,
196
- calls: int = 0,
197
- chain_calls: int = 0,
198
- ) -> None:
199
- self.lifetime.observe(
200
- duration_ms=duration_ms,
201
- success=success,
202
- input_bytes=input_bytes,
203
- output_bytes=output_bytes,
204
- calls=calls,
205
- chain_calls=chain_calls,
322
+ def record_operation(self, name: str, observation: OperationObservation) -> None:
323
+ operation = _bounded_text(name, DIMENSION_VALUE_LIMIT) or OTHER_DIMENSION
324
+ self._delta.lifetime.observe(
325
+ duration_ms=observation.duration_ms,
326
+ success=observation.success,
327
+ input_bytes=observation.input_bytes,
328
+ output_bytes=observation.output_bytes,
329
+ calls=observation.calls,
330
+ chain_calls=observation.chain_calls,
206
331
  )
207
- self._rollup_dimension(self.operations, name, MAX_OPERATIONS).observe(
208
- duration_ms=duration_ms,
209
- success=success,
210
- input_bytes=input_bytes,
211
- output_bytes=output_bytes,
212
- calls=calls,
213
- chain_calls=chain_calls,
332
+ self._rollup_dimension(self._delta.operations, operation, MAX_OPERATIONS).observe(
333
+ duration_ms=observation.duration_ms,
334
+ success=observation.success,
335
+ input_bytes=observation.input_bytes,
336
+ output_bytes=observation.output_bytes,
337
+ calls=observation.calls,
338
+ chain_calls=observation.chain_calls,
214
339
  )
215
340
  self._recent_rollup().observe(
216
- duration_ms=duration_ms,
217
- success=success,
218
- input_bytes=input_bytes,
219
- output_bytes=output_bytes,
220
- calls=calls,
221
- chain_calls=chain_calls,
341
+ duration_ms=observation.duration_ms,
342
+ success=observation.success,
343
+ input_bytes=observation.input_bytes,
344
+ output_bytes=observation.output_bytes,
345
+ calls=observation.calls,
346
+ chain_calls=observation.chain_calls,
222
347
  )
223
- if failure_stage is not None:
224
- key = self._bounded_key(self.failures, failure_stage, MAX_FAILURE_STAGES)
225
- self.failures[key] = self.failures.get(key, 0) + 1
348
+ failure = observation.failure
349
+ outcome = _operation_outcome(observation.success, failure)
350
+ outcome_key = _bounded_key(self._delta.outcomes, outcome, MAX_OUTCOMES)
351
+ self._delta.outcomes[outcome_key] = self._delta.outcomes.get(outcome_key, 0) + 1
352
+ if failure is not None:
353
+ stage = _bounded_text(failure.stage, DIMENSION_VALUE_LIMIT) or "unknown"
354
+ key = _bounded_key(self._delta.failures, stage, MAX_FAILURE_STAGES)
355
+ self._delta.failures[key] = self._delta.failures.get(key, 0) + 1
356
+ subtype = _bounded_text(failure.subtype, DIMENSION_VALUE_LIMIT) or stage
357
+ self._delta.failure_events.append(
358
+ FailureEvent(
359
+ timestamp=int(time.time()),
360
+ trace_id=_required_bounded_text(failure.trace_id, TRACE_ID_LIMIT),
361
+ operation=operation,
362
+ stage=stage,
363
+ subtype=subtype,
364
+ calls=max(0, observation.calls),
365
+ chain_calls=max(0, observation.chain_calls),
366
+ server=_bounded_text(failure.server, DIMENSION_VALUE_LIMIT),
367
+ tool=_bounded_text(failure.tool, DIMENSION_VALUE_LIMIT),
368
+ package_version=self.package_version,
369
+ )
370
+ )
371
+ self._delta.failure_events = self._delta.failure_events[-RECENT_FAILURE_LIMIT:]
226
372
  self._changed()
227
373
 
228
374
  def record_phase(self, name: str, duration_ms: float) -> None:
229
- self._histogram_dimension(self.phases, name, MAX_PHASES).observe(duration_ms)
375
+ phase = _bounded_text(name, DIMENSION_VALUE_LIMIT) or OTHER_DIMENSION
376
+ self._histogram_dimension(self._delta.phases, phase, MAX_PHASES).observe(duration_ms)
230
377
  self._changed()
231
378
 
232
379
  def record_upstream(
@@ -239,9 +386,15 @@ class StatsStore:
239
386
  input_bytes: int,
240
387
  output_bytes: int,
241
388
  ) -> None:
389
+ server_name = _bounded_text(server, DIMENSION_VALUE_LIMIT) or OTHER_DIMENSION
390
+ tool_name = _bounded_text(tool, DIMENSION_VALUE_LIMIT) or OTHER_DIMENSION
242
391
  for rollup in (
243
- self._rollup_dimension(self.servers, server, MAX_SERVERS),
244
- self._rollup_dimension(self.tools, f"{server}.{tool}", MAX_TOOLS),
392
+ self._rollup_dimension(self._delta.servers, server_name, MAX_SERVERS),
393
+ self._rollup_dimension(
394
+ self._delta.tools,
395
+ f"{server_name}.{tool_name}",
396
+ MAX_TOOLS,
397
+ ),
245
398
  ):
246
399
  rollup.observe(
247
400
  duration_ms=duration_ms,
@@ -254,28 +407,14 @@ class StatsStore:
254
407
 
255
408
  def record_cache(self, *, hit: bool) -> None:
256
409
  if hit:
257
- self.cache_hits += 1
410
+ self._delta.cache_hits += 1
258
411
  else:
259
- self.cache_misses += 1
412
+ self._delta.cache_misses += 1
260
413
  self._changed()
261
414
 
262
- def snapshot(self) -> JsonObject:
263
- recent = [
264
- {"bucket_start": timestamp, **rollup.snapshot(include_distributions=False)}
265
- for timestamp, rollup in sorted(self.recent.items())
266
- ]
267
- return JSON_OBJECT_ADAPTER.validate_python({
268
- "version": 1,
269
- "updated_at": self.updated_at,
270
- "lifetime": self.lifetime.snapshot(),
271
- "recent": recent,
272
- "operations": _snapshot_mapping(self.operations),
273
- "phases": _snapshot_mapping(self.phases),
274
- "servers": _snapshot_mapping(self.servers),
275
- "tools": _snapshot_mapping(self.tools, include_distributions=False),
276
- "failures": dict(sorted(self.failures.items())),
277
- "cache": {"hits": self.cache_hits, "misses": self.cache_misses},
278
- })
415
+ async def snapshot(self) -> JsonObject:
416
+ await self.flush()
417
+ return await asyncio.to_thread(self._read_snapshot)
279
418
 
280
419
  def schedule_flush(self) -> None:
281
420
  if self._closing:
@@ -289,15 +428,18 @@ class StatsStore:
289
428
  self._flush_task = loop.create_task(self._delayed_flush())
290
429
 
291
430
  async def flush(self) -> None:
292
- if not self._dirty:
293
- return
294
- payload = self.snapshot()
295
- self._dirty = False
296
- try:
297
- await asyncio.to_thread(self._write, payload)
298
- except BaseException:
299
- self._dirty = True
300
- raise
431
+ async with self._flush_lock:
432
+ if not self._dirty:
433
+ return
434
+ delta = self._delta
435
+ self._delta = StatsAccumulator()
436
+ self._dirty = False
437
+ try:
438
+ await asyncio.to_thread(self._merge_delta, delta)
439
+ except BaseException:
440
+ self._delta.merge(delta)
441
+ self._dirty = True
442
+ raise
301
443
 
302
444
  async def close(self) -> None:
303
445
  self._closing = True
@@ -318,107 +460,572 @@ class StatsStore:
318
460
  self.schedule_flush()
319
461
 
320
462
  def _changed(self) -> None:
321
- self.updated_at = int(time.time())
463
+ self._delta.updated_at = int(time.time())
322
464
  self._dirty = True
323
465
  self.schedule_flush()
324
466
 
325
467
  def _recent_rollup(self) -> Rollup:
326
468
  timestamp = int(time.time() // RECENT_BUCKET_SECONDS) * RECENT_BUCKET_SECONDS
327
- rollup = self.recent.setdefault(timestamp, Rollup())
328
- while len(self.recent) > RECENT_BUCKET_COUNT:
329
- del self.recent[min(self.recent)]
469
+ rollup = self._delta.recent.setdefault(timestamp, Rollup())
470
+ while len(self._delta.recent) > RECENT_BUCKET_COUNT:
471
+ del self._delta.recent[min(self._delta.recent)]
330
472
  return rollup
331
473
 
332
474
  @staticmethod
333
475
  def _rollup_dimension(values: dict[str, Rollup], key: str, limit: int) -> Rollup:
334
- bounded = StatsStore._bounded_key(values, key, limit)
476
+ bounded = _bounded_key(values, key, limit)
335
477
  return values.setdefault(bounded, Rollup())
336
478
 
337
479
  @staticmethod
338
480
  def _histogram_dimension(values: dict[str, Histogram], key: str, limit: int) -> Histogram:
339
- bounded = StatsStore._bounded_key(values, key, limit)
481
+ bounded = _bounded_key(values, key, limit)
340
482
  return values.setdefault(bounded, Histogram())
341
483
 
342
- @staticmethod
343
- def _bounded_key(values: Mapping[str, object], key: str, limit: int) -> str:
344
- if key in values:
345
- return key
346
- if len(values) < max(1, limit - 1):
347
- return key
348
- return OTHER_DIMENSION
349
-
350
- def _write(self, payload: JsonObject) -> None:
484
+ def _connect(self) -> sqlite3.Connection:
351
485
  self.path.parent.mkdir(parents=True, exist_ok=True)
352
- temporary = self.path.with_name(f"{self.path.name}.{os.getpid()}.tmp")
353
- temporary.write_text(
354
- json.dumps(payload, separators=(",", ":"), sort_keys=True),
355
- encoding="utf-8",
486
+ connection = sqlite3.connect(
487
+ self.path,
488
+ timeout=SQLITE_BUSY_TIMEOUT_MS / 1_000,
356
489
  )
357
- Path(temporary).replace(self.path)
490
+ connection.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
491
+ connection.execute("PRAGMA journal_mode=WAL")
492
+ connection.execute("PRAGMA synchronous=NORMAL")
493
+ _initialize_schema(connection)
494
+ return connection
358
495
 
359
- def _load(self) -> None:
496
+ def _merge_delta(self, delta: StatsAccumulator) -> None:
497
+ connection = self._connect()
360
498
  try:
361
- raw = json.loads(self.path.read_text(encoding="utf-8"))
362
- except (FileNotFoundError, json.JSONDecodeError, OSError):
363
- return
364
- if not isinstance(raw, dict) or raw.get("version") != 1:
365
- return
366
- self.updated_at = _integer(raw.get("updated_at"))
367
- self.lifetime = Rollup.from_snapshot(raw.get("lifetime"))
368
- self.operations = _load_rollups(raw.get("operations"), MAX_OPERATIONS)
369
- self.phases = _load_histograms(raw.get("phases"), MAX_PHASES)
370
- self.servers = _load_rollups(raw.get("servers"), MAX_SERVERS)
371
- self.tools = _load_rollups(raw.get("tools"), MAX_TOOLS)
372
- raw_failures = raw.get("failures")
373
- if isinstance(raw_failures, dict):
374
- self.failures = {
375
- str(key): _integer(value)
376
- for key, value in list(raw_failures.items())[:MAX_FAILURE_STAGES]
377
- }
378
- raw_cache = raw.get("cache")
379
- if isinstance(raw_cache, dict):
380
- self.cache_hits = _integer(raw_cache.get("hits"))
381
- self.cache_misses = _integer(raw_cache.get("misses"))
382
- raw_recent = raw.get("recent")
383
- if isinstance(raw_recent, list):
384
- for item in raw_recent[-RECENT_BUCKET_COUNT:]:
385
- if not isinstance(item, dict):
386
- continue
387
- timestamp = _integer(item.get("bucket_start"))
388
- if timestamp > 0:
389
- self.recent[timestamp] = Rollup.from_snapshot(item)
390
-
391
-
392
- def _snapshot_mapping(
393
- values: Mapping[str, Rollup | Histogram],
499
+ connection.execute("BEGIN IMMEDIATE")
500
+ _apply_delta(connection, delta)
501
+ connection.commit()
502
+ except BaseException:
503
+ connection.rollback()
504
+ raise
505
+ finally:
506
+ connection.close()
507
+
508
+ def _read_snapshot(self) -> JsonObject:
509
+ connection = self._connect()
510
+ try:
511
+ rollups = _read_rollups(connection)
512
+ phases = _read_phases(connection)
513
+ counters = _read_counters(connection)
514
+ lifetime = rollups.get("lifetime", {}).get("", Rollup())
515
+ recent = [
516
+ {
517
+ "bucket_start": int(timestamp),
518
+ **rollup.snapshot(include_distributions=False),
519
+ }
520
+ for timestamp, rollup in sorted(
521
+ rollups.get("recent", {}).items(),
522
+ key=lambda item: int(item[0]),
523
+ )
524
+ ]
525
+ updated_row = connection.execute(
526
+ "SELECT value FROM metadata WHERE key = 'updated_at'"
527
+ ).fetchone()
528
+ recent_failures = [
529
+ FailureEvent(
530
+ timestamp=row[0],
531
+ trace_id=row[1],
532
+ operation=row[2],
533
+ stage=row[3],
534
+ subtype=row[4],
535
+ calls=row[5],
536
+ chain_calls=row[6],
537
+ server=row[7],
538
+ tool=row[8],
539
+ package_version=row[9],
540
+ ).snapshot()
541
+ for row in connection.execute(
542
+ """
543
+ SELECT timestamp, trace_id, operation, stage, subtype,
544
+ calls, chain_calls, server, tool, package_version
545
+ FROM failure_events
546
+ ORDER BY id DESC
547
+ LIMIT ?
548
+ """,
549
+ (RECENT_FAILURE_LIMIT,),
550
+ )
551
+ ]
552
+ return JSON_OBJECT_ADAPTER.validate_python({
553
+ "version": 2,
554
+ "updated_at": updated_row[0] if updated_row is not None else 0,
555
+ "lifetime": lifetime.snapshot(),
556
+ "recent": recent,
557
+ "operations": _snapshot_rollups(rollups.get("operation", {})),
558
+ "phases": _snapshot_histograms(phases),
559
+ "servers": _snapshot_rollups(rollups.get("server", {})),
560
+ "tools": _snapshot_rollups(
561
+ rollups.get("tool", {}),
562
+ include_distributions=False,
563
+ ),
564
+ "failures": counters.get("failure", {}),
565
+ "outcomes": counters.get("outcome", {}),
566
+ "recent_failures": recent_failures,
567
+ "cache": {
568
+ "hits": counters.get("cache", {}).get("hits", 0),
569
+ "misses": counters.get("cache", {}).get("misses", 0),
570
+ },
571
+ })
572
+ finally:
573
+ connection.close()
574
+
575
+
576
+ def _apply_delta(connection: sqlite3.Connection, delta: StatsAccumulator) -> None:
577
+ _merge_rollups(connection, "lifetime", {"": delta.lifetime}, 1)
578
+ _merge_rollups(connection, "operation", delta.operations, MAX_OPERATIONS)
579
+ _merge_histograms(connection, "phase", delta.phases, MAX_PHASES, "duration_ms")
580
+ _merge_rollups(connection, "server", delta.servers, MAX_SERVERS)
581
+ _merge_rollups(connection, "tool", delta.tools, MAX_TOOLS)
582
+ _merge_rollups(
583
+ connection,
584
+ "recent",
585
+ {str(timestamp): rollup for timestamp, rollup in delta.recent.items()},
586
+ RECENT_BUCKET_COUNT,
587
+ bound_names=False,
588
+ include_histograms=False,
589
+ )
590
+ _merge_counters(connection, "failure", delta.failures, MAX_FAILURE_STAGES)
591
+ _merge_counters(connection, "outcome", delta.outcomes, MAX_OUTCOMES)
592
+ _merge_counters(
593
+ connection,
594
+ "cache",
595
+ {"hits": delta.cache_hits, "misses": delta.cache_misses},
596
+ 3,
597
+ )
598
+ connection.execute(
599
+ """
600
+ INSERT INTO metadata(key, value) VALUES ('updated_at', ?)
601
+ ON CONFLICT(key) DO UPDATE SET value = MAX(metadata.value, excluded.value)
602
+ """,
603
+ (delta.updated_at,),
604
+ )
605
+ for event in delta.failure_events:
606
+ connection.execute(
607
+ """
608
+ INSERT INTO failure_events(
609
+ timestamp, trace_id, operation, stage, subtype,
610
+ calls, chain_calls, server, tool, package_version
611
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
612
+ """,
613
+ (
614
+ event.timestamp,
615
+ event.trace_id,
616
+ event.operation,
617
+ event.stage,
618
+ event.subtype,
619
+ event.calls,
620
+ event.chain_calls,
621
+ event.server,
622
+ event.tool,
623
+ event.package_version,
624
+ ),
625
+ )
626
+ connection.execute(
627
+ """
628
+ DELETE FROM failure_events
629
+ WHERE id NOT IN (
630
+ SELECT id FROM failure_events ORDER BY id DESC LIMIT ?
631
+ )
632
+ """,
633
+ (RECENT_FAILURE_LIMIT,),
634
+ )
635
+ connection.execute(
636
+ """
637
+ DELETE FROM rollups
638
+ WHERE dimension = 'recent'
639
+ AND name NOT IN (
640
+ SELECT name FROM rollups
641
+ WHERE dimension = 'recent'
642
+ ORDER BY CAST(name AS INTEGER) DESC
643
+ LIMIT ?
644
+ )
645
+ """,
646
+ (RECENT_BUCKET_COUNT,),
647
+ )
648
+
649
+
650
+ def _initialize_schema(connection: sqlite3.Connection) -> None:
651
+ bucket_columns = ",\n".join(
652
+ f"bucket_{index} INTEGER NOT NULL DEFAULT 0" for index in range(MAX_HISTOGRAM_BUCKETS)
653
+ )
654
+ connection.executescript(f"""
655
+ CREATE TABLE IF NOT EXISTS metadata(
656
+ key TEXT PRIMARY KEY,
657
+ value INTEGER NOT NULL
658
+ );
659
+ CREATE TABLE IF NOT EXISTS rollups(
660
+ dimension TEXT NOT NULL,
661
+ name TEXT NOT NULL,
662
+ count INTEGER NOT NULL DEFAULT 0,
663
+ success INTEGER NOT NULL DEFAULT 0,
664
+ failure INTEGER NOT NULL DEFAULT 0,
665
+ input_bytes INTEGER NOT NULL DEFAULT 0,
666
+ output_bytes INTEGER NOT NULL DEFAULT 0,
667
+ calls INTEGER NOT NULL DEFAULT 0,
668
+ chain_calls INTEGER NOT NULL DEFAULT 0,
669
+ PRIMARY KEY(dimension, name)
670
+ );
671
+ CREATE TABLE IF NOT EXISTS histograms(
672
+ dimension TEXT NOT NULL,
673
+ name TEXT NOT NULL,
674
+ metric TEXT NOT NULL,
675
+ count INTEGER NOT NULL DEFAULT 0,
676
+ total REAL NOT NULL DEFAULT 0,
677
+ maximum REAL NOT NULL DEFAULT 0,
678
+ {bucket_columns},
679
+ PRIMARY KEY(dimension, name, metric)
680
+ );
681
+ CREATE TABLE IF NOT EXISTS counters(
682
+ dimension TEXT NOT NULL,
683
+ name TEXT NOT NULL,
684
+ value INTEGER NOT NULL DEFAULT 0,
685
+ PRIMARY KEY(dimension, name)
686
+ );
687
+ CREATE TABLE IF NOT EXISTS failure_events(
688
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
689
+ timestamp INTEGER NOT NULL,
690
+ trace_id TEXT NOT NULL,
691
+ operation TEXT NOT NULL,
692
+ stage TEXT NOT NULL,
693
+ subtype TEXT NOT NULL,
694
+ calls INTEGER NOT NULL,
695
+ chain_calls INTEGER NOT NULL,
696
+ server TEXT,
697
+ tool TEXT,
698
+ package_version TEXT NOT NULL
699
+ );
700
+ CREATE INDEX IF NOT EXISTS failure_events_timestamp
701
+ ON failure_events(timestamp DESC);
702
+ PRAGMA user_version = {SQLITE_SCHEMA_VERSION};
703
+ """)
704
+
705
+
706
+ def _merge_rollups(
707
+ connection: sqlite3.Connection,
708
+ dimension: str,
709
+ values: Mapping[str, Rollup],
710
+ limit: int,
711
+ *,
712
+ bound_names: bool = True,
713
+ include_histograms: bool = True,
714
+ ) -> None:
715
+ name_mapping = (
716
+ _bounded_database_names(connection, "rollups", dimension, values, limit)
717
+ if bound_names
718
+ else {name: name for name in values}
719
+ )
720
+ merged: dict[str, Rollup] = {}
721
+ for original_name, rollup in values.items():
722
+ merged.setdefault(name_mapping[original_name], Rollup()).merge(rollup)
723
+ for name, rollup in merged.items():
724
+ connection.execute(
725
+ """
726
+ INSERT INTO rollups(
727
+ dimension, name, count, success, failure, input_bytes,
728
+ output_bytes, calls, chain_calls
729
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
730
+ ON CONFLICT(dimension, name) DO UPDATE SET
731
+ count = rollups.count + excluded.count,
732
+ success = rollups.success + excluded.success,
733
+ failure = rollups.failure + excluded.failure,
734
+ input_bytes = rollups.input_bytes + excluded.input_bytes,
735
+ output_bytes = rollups.output_bytes + excluded.output_bytes,
736
+ calls = rollups.calls + excluded.calls,
737
+ chain_calls = rollups.chain_calls + excluded.chain_calls
738
+ """,
739
+ (
740
+ dimension,
741
+ name,
742
+ rollup.count,
743
+ rollup.success,
744
+ rollup.failure,
745
+ rollup.input_bytes,
746
+ rollup.output_bytes,
747
+ rollup.calls,
748
+ rollup.chain_calls,
749
+ ),
750
+ )
751
+ if include_histograms:
752
+ _upsert_histogram(connection, dimension, name, "duration_ms", rollup.duration_ms)
753
+ _upsert_histogram(
754
+ connection,
755
+ dimension,
756
+ name,
757
+ "input_size_bytes",
758
+ rollup.input_size_bytes,
759
+ )
760
+ _upsert_histogram(
761
+ connection,
762
+ dimension,
763
+ name,
764
+ "output_size_bytes",
765
+ rollup.output_size_bytes,
766
+ )
767
+
768
+
769
+ def _merge_histograms(
770
+ connection: sqlite3.Connection,
771
+ dimension: str,
772
+ values: Mapping[str, Histogram],
773
+ limit: int,
774
+ metric: str,
775
+ ) -> None:
776
+ name_mapping = _bounded_database_names(connection, "histograms", dimension, values, limit)
777
+ merged: dict[str, Histogram] = {}
778
+ for original_name, histogram in values.items():
779
+ target = merged.setdefault(
780
+ name_mapping[original_name],
781
+ Histogram(bounds=histogram.bounds, counts=[0] * len(histogram.counts)),
782
+ )
783
+ target.merge(histogram)
784
+ for name, histogram in merged.items():
785
+ _upsert_histogram(connection, dimension, name, metric, histogram)
786
+
787
+
788
+ def _upsert_histogram(
789
+ connection: sqlite3.Connection,
790
+ dimension: str,
791
+ name: str,
792
+ metric: str,
793
+ histogram: Histogram,
794
+ ) -> None:
795
+ bucket_values = [
796
+ *histogram.counts,
797
+ *([0] * (MAX_HISTOGRAM_BUCKETS - len(histogram.counts))),
798
+ ]
799
+ connection.execute(
800
+ HISTOGRAM_UPSERT_SQL,
801
+ (
802
+ dimension,
803
+ name,
804
+ metric,
805
+ histogram.count,
806
+ histogram.total,
807
+ histogram.maximum,
808
+ *bucket_values,
809
+ ),
810
+ )
811
+
812
+
813
+ def _merge_counters(
814
+ connection: sqlite3.Connection,
815
+ dimension: str,
816
+ values: Mapping[str, int],
817
+ limit: int,
818
+ ) -> None:
819
+ name_mapping = _bounded_database_names(connection, "counters", dimension, values, limit)
820
+ merged: dict[str, int] = {}
821
+ for original_name, value in values.items():
822
+ target = name_mapping[original_name]
823
+ merged[target] = merged.get(target, 0) + value
824
+ for name, value in merged.items():
825
+ connection.execute(
826
+ """
827
+ INSERT INTO counters(dimension, name, value) VALUES (?, ?, ?)
828
+ ON CONFLICT(dimension, name) DO UPDATE SET
829
+ value = counters.value + excluded.value
830
+ """,
831
+ (dimension, name, value),
832
+ )
833
+
834
+
835
+ def _bounded_database_names(
836
+ connection: sqlite3.Connection,
837
+ table: Literal["rollups", "histograms", "counters"],
838
+ dimension: str,
839
+ values: Mapping[str, object],
840
+ limit: int,
841
+ ) -> dict[str, str]:
842
+ existing = {
843
+ str(row[0])
844
+ for row in connection.execute(
845
+ DISTINCT_NAME_QUERIES[table],
846
+ (dimension,),
847
+ )
848
+ }
849
+ mapping: dict[str, str] = {}
850
+ for name in sorted(values):
851
+ if name in existing:
852
+ mapping[name] = name
853
+ elif len(existing - {OTHER_DIMENSION}) < max(1, limit - 1):
854
+ existing.add(name)
855
+ mapping[name] = name
856
+ else:
857
+ existing.add(OTHER_DIMENSION)
858
+ mapping[name] = OTHER_DIMENSION
859
+ return mapping
860
+
861
+
862
+ def _read_rollups(connection: sqlite3.Connection) -> dict[str, dict[str, Rollup]]:
863
+ histograms = _read_rollup_histograms(connection)
864
+ result: dict[str, dict[str, Rollup]] = {}
865
+ for row in connection.execute(
866
+ """
867
+ SELECT dimension, name, count, success, failure, input_bytes,
868
+ output_bytes, calls, chain_calls
869
+ FROM rollups
870
+ """
871
+ ):
872
+ dimension, name = str(row[0]), str(row[1])
873
+ rollup = Rollup(
874
+ count=row[2],
875
+ success=row[3],
876
+ failure=row[4],
877
+ input_bytes=row[5],
878
+ output_bytes=row[6],
879
+ calls=row[7],
880
+ chain_calls=row[8],
881
+ )
882
+ rollup.duration_ms = histograms.get(
883
+ (dimension, name, "duration_ms"),
884
+ Histogram(),
885
+ )
886
+ rollup.input_size_bytes = histograms.get(
887
+ (dimension, name, "input_size_bytes"),
888
+ Histogram(
889
+ bounds=HISTOGRAM_BOUNDS_BYTES,
890
+ counts=[0] * (len(HISTOGRAM_BOUNDS_BYTES) + 1),
891
+ ),
892
+ )
893
+ rollup.output_size_bytes = histograms.get(
894
+ (dimension, name, "output_size_bytes"),
895
+ Histogram(
896
+ bounds=HISTOGRAM_BOUNDS_BYTES,
897
+ counts=[0] * (len(HISTOGRAM_BOUNDS_BYTES) + 1),
898
+ ),
899
+ )
900
+ result.setdefault(dimension, {})[name] = rollup
901
+ return result
902
+
903
+
904
+ def _read_rollup_histograms(
905
+ connection: sqlite3.Connection,
906
+ ) -> dict[tuple[str, str, str], Histogram]:
907
+ result: dict[tuple[str, str, str], Histogram] = {}
908
+ for row in connection.execute(HISTOGRAM_READ_SQL):
909
+ dimension, name, metric = str(row[0]), str(row[1]), str(row[2])
910
+ bounds = HISTOGRAM_BOUNDS_BYTES if metric.endswith("size_bytes") else HISTOGRAM_BOUNDS_MS
911
+ result[dimension, name, metric] = Histogram(
912
+ bounds=bounds,
913
+ counts=list(row[6 : 6 + len(bounds) + 1]),
914
+ count=row[3],
915
+ total=row[4],
916
+ maximum=row[5],
917
+ )
918
+ return result
919
+
920
+
921
+ def _read_phases(connection: sqlite3.Connection) -> dict[str, Histogram]:
922
+ return {
923
+ str(row[0]): Histogram(
924
+ counts=list(row[4 : 4 + len(HISTOGRAM_BOUNDS_MS) + 1]),
925
+ count=row[1],
926
+ total=row[2],
927
+ maximum=row[3],
928
+ )
929
+ for row in connection.execute(PHASE_READ_SQL)
930
+ }
931
+
932
+
933
+ def _read_counters(connection: sqlite3.Connection) -> dict[str, dict[str, int]]:
934
+ result: dict[str, dict[str, int]] = {}
935
+ for dimension, name, value in connection.execute("SELECT dimension, name, value FROM counters"):
936
+ result.setdefault(str(dimension), {})[str(name)] = int(value)
937
+ return result
938
+
939
+
940
+ def _snapshot_rollups(
941
+ values: Mapping[str, Rollup],
394
942
  *,
395
943
  include_distributions: bool = True,
396
944
  ) -> JsonObject:
397
945
  return {
398
- key: (
399
- value.snapshot(include_distributions=include_distributions)
400
- if isinstance(value, Rollup)
401
- else value.snapshot()
402
- )
946
+ key: value.snapshot(include_distributions=include_distributions)
403
947
  for key, value in sorted(values.items())
404
948
  }
405
949
 
406
950
 
407
- def _load_rollups(value: object, limit: int) -> dict[str, Rollup]:
408
- if not isinstance(value, dict):
409
- return {}
410
- return {str(key): Rollup.from_snapshot(item) for key, item in list(value.items())[:limit]}
951
+ def _snapshot_histograms(values: Mapping[str, Histogram]) -> JsonObject:
952
+ return {key: value.snapshot() for key, value in sorted(values.items())}
953
+
954
+
955
+ def _merge_rollup_mapping(
956
+ target: dict[str, Rollup],
957
+ source: Mapping[str, Rollup],
958
+ limit: int,
959
+ ) -> None:
960
+ for key, value in source.items():
961
+ bounded = _bounded_key(target, key, limit)
962
+ target.setdefault(bounded, Rollup()).merge(value)
963
+
964
+
965
+ def _merge_histogram_mapping(
966
+ target: dict[str, Histogram],
967
+ source: Mapping[str, Histogram],
968
+ limit: int,
969
+ ) -> None:
970
+ for key, value in source.items():
971
+ bounded = _bounded_key(target, key, limit)
972
+ histogram = target.setdefault(
973
+ bounded,
974
+ Histogram(bounds=value.bounds, counts=[0] * len(value.counts)),
975
+ )
976
+ histogram.merge(value)
977
+
978
+
979
+ def _merge_counter_mapping(
980
+ target: dict[str, int],
981
+ source: Mapping[str, int],
982
+ limit: int,
983
+ ) -> None:
984
+ for key, value in source.items():
985
+ bounded = _bounded_key(target, key, limit)
986
+ target[bounded] = target.get(bounded, 0) + value
987
+
988
+
989
+ def _operation_outcome(
990
+ success: bool,
991
+ failure: OperationFailure | None,
992
+ ) -> FailureOutcome:
993
+ if success:
994
+ return "success"
995
+ if failure is None:
996
+ return "internal_error"
997
+ if failure.stage == "preflight":
998
+ return "preflight_rejection"
999
+ if failure.stage == "result":
1000
+ return "result_refinement"
1001
+ if failure.stage == "cancelled":
1002
+ return "cancellation"
1003
+ if failure.subtype == "upstream_transport" or failure.stage == "timeout":
1004
+ return "transport_failure"
1005
+ if failure.stage in {"runtime", "discovery"}:
1006
+ return "upstream_failure"
1007
+ return "internal_error"
411
1008
 
412
1009
 
413
- def _load_histograms(value: object, limit: int) -> dict[str, Histogram]:
414
- if not isinstance(value, dict):
415
- return {}
416
- return {str(key): Histogram.from_snapshot(item) for key, item in list(value.items())[:limit]}
1010
+ def _bounded_key(values: Mapping[str, object], key: str, limit: int) -> str:
1011
+ if key in values:
1012
+ return key
1013
+ if len(values) < max(1, limit - 1):
1014
+ return key
1015
+ return OTHER_DIMENSION
417
1016
 
418
1017
 
419
- def _integer(value: object) -> int:
420
- return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else 0
1018
+ def _required_bounded_text(value: str, limit: int) -> str:
1019
+ bounded = _bounded_text(value, limit)
1020
+ if bounded is None:
1021
+ raise ValueError("required telemetry text must not be empty")
1022
+ return bounded
421
1023
 
422
1024
 
423
- def _number(value: object) -> float:
424
- return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else 0.0
1025
+ def _bounded_text(value: str | None, limit: int) -> str | None:
1026
+ if value is None:
1027
+ return None
1028
+ compact = value.strip()
1029
+ if not compact:
1030
+ return None
1031
+ return compact[:limit]