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/README.md +5 -2
- package/extensions/index.ts +12 -8
- package/package.json +1 -1
- package/sidecar/cli.py +27 -7
- package/sidecar/executor.py +207 -35
- package/sidecar/gateway.py +470 -115
- package/sidecar/refinement_cache.py +154 -0
- package/sidecar/sandbox_api.py +44 -0
- package/sidecar/stats.py +801 -194
- package/sidecar/tool_catalog.py +43 -26
- package/src/chains.ts +53 -13
- package/src/mcp-client.ts +1 -0
- package/src/modal.ts +11 -0
- package/src/prompts.ts +3 -3
- package/src/tools.ts +37 -14
package/sidecar/stats.py
CHANGED
|
@@ -1,30 +1,109 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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.
|
|
173
|
-
self.
|
|
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.
|
|
320
|
+
self._flush_lock = asyncio.Lock()
|
|
186
321
|
|
|
187
|
-
def record_operation(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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,
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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,
|
|
244
|
-
self._rollup_dimension(
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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 =
|
|
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 =
|
|
481
|
+
bounded = _bounded_key(values, key, limit)
|
|
340
482
|
return values.setdefault(bounded, Histogram())
|
|
341
483
|
|
|
342
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
encoding="utf-8",
|
|
486
|
+
connection = sqlite3.connect(
|
|
487
|
+
self.path,
|
|
488
|
+
timeout=SQLITE_BUSY_TIMEOUT_MS / 1_000,
|
|
356
489
|
)
|
|
357
|
-
|
|
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
|
|
496
|
+
def _merge_delta(self, delta: StatsAccumulator) -> None:
|
|
497
|
+
connection = self._connect()
|
|
360
498
|
try:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
414
|
-
if
|
|
415
|
-
return
|
|
416
|
-
|
|
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
|
|
420
|
-
|
|
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
|
|
424
|
-
|
|
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]
|