@noego/telemetry 0.0.5 → 0.1.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.md +6 -0
- package/dist/browser/index.d.cts +6 -5
- package/dist/browser/index.d.ts +6 -5
- package/dist/{browser_telemetry_client-BSAMZ8Qb.d.cts → browser_telemetry_client-ClZl3awO.d.cts} +2 -2
- package/dist/{browser_telemetry_client-DPBU6NGw.d.ts → browser_telemetry_client-t4dPp8Ku.d.ts} +2 -2
- package/dist/cli/telemetry_bin.cjs +393 -35
- package/dist/cli/telemetry_bin.cjs.map +1 -1
- package/dist/cli/telemetry_bin.js +378 -20
- package/dist/cli/telemetry_bin.js.map +1 -1
- package/dist/cli/telemetry_cli.cjs +393 -35
- package/dist/cli/telemetry_cli.cjs.map +1 -1
- package/dist/cli/telemetry_cli.d.cts +35 -5
- package/dist/cli/telemetry_cli.d.ts +35 -5
- package/dist/cli/telemetry_cli.js +378 -20
- package/dist/cli/telemetry_cli.js.map +1 -1
- package/dist/{client_defaults-ShVYQYmn.d.ts → client_defaults-CZ8PysZX.d.ts} +2 -2
- package/dist/{client_defaults-Dk0MMzvx.d.cts → client_defaults-lDbWYWWK.d.cts} +2 -2
- package/dist/core/index.cjs +5 -1
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +15 -12
- package/dist/core/index.d.ts +15 -12
- package/dist/core/index.js +5 -1
- package/dist/core/index.js.map +1 -1
- package/dist/electron/index.d.cts +5 -4
- package/dist/electron/index.d.ts +5 -4
- package/dist/{envelope.types-rOLpWXRC.d.cts → envelope.types-DIvZD3xH.d.ts} +2 -6
- package/dist/{envelope.types-rOLpWXRC.d.ts → envelope.types-DyrlRG49.d.cts} +2 -6
- package/dist/{in_memory_batch_transport-DKW9EZnU.d.cts → in_memory_batch_transport-3T6gElXP.d.cts} +1 -1
- package/dist/{in_memory_batch_transport-oOwNxXnA.d.ts → in_memory_batch_transport-DpQU6JqM.d.ts} +1 -1
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/json.types-BqS77fcN.d.cts +7 -0
- package/dist/json.types-BqS77fcN.d.ts +7 -0
- package/dist/{maintenance_coordinator.types-C4ULi4A4.d.cts → maintenance_coordinator.types-Crhx9i9k.d.cts} +3 -3
- package/dist/{maintenance_coordinator.types-ikUrRXmO.d.ts → maintenance_coordinator.types-DprmgpVn.d.ts} +3 -3
- package/dist/metric.types-D8aOAnUm.d.ts +67 -0
- package/dist/metric.types-Dg4eZq6u.d.cts +67 -0
- package/dist/{metric_definition_contract-BYtnODhu.d.ts → metric_definition_contract-C6MERhJs.d.ts} +1 -1
- package/dist/{metric_definition_contract-DfhhmTZ1.d.cts → metric_definition_contract-DoAk7NOw.d.cts} +1 -1
- package/dist/node/index.cjs +5 -1
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.d.cts +11 -9
- package/dist/node/index.d.ts +11 -9
- package/dist/node/index.js +5 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/producer/index.d.cts +5 -4
- package/dist/node/producer/index.d.ts +5 -4
- package/dist/projector/index.cjs +1370 -0
- package/dist/projector/index.cjs.map +1 -0
- package/dist/projector/index.d.cts +246 -0
- package/dist/projector/index.d.ts +246 -0
- package/dist/projector/index.js +1328 -0
- package/dist/projector/index.js.map +1 -0
- package/dist/{query.types-DU5v7vM0.d.ts → query.types-CRqAsGTX.d.cts} +4 -66
- package/dist/{query.types-DuYWbFFe.d.cts → query.types-DnOMYXpC.d.ts} +4 -66
- package/dist/sqlite/index.cjs +5 -1
- package/dist/sqlite/index.cjs.map +1 -1
- package/dist/sqlite/index.d.cts +7 -5
- package/dist/sqlite/index.d.ts +7 -5
- package/dist/sqlite/index.js +5 -1
- package/dist/sqlite/index.js.map +1 -1
- package/dist/{storage.types-k7oElEB3.d.ts → storage.types-BKWoIOHp.d.ts} +3 -2
- package/dist/{storage.types-Bji_pWqr.d.cts → storage.types-DHbAm3j_.d.cts} +3 -2
- package/dist/{telemetry_client-BO5Pjp41.d.cts → telemetry_client-Bg5tbj5E.d.cts} +3 -2
- package/dist/{telemetry_client-D8uZ-mTL.d.ts → telemetry_client-DWKgufmc.d.ts} +3 -2
- package/dist/{telemetry_configuration_parser-BZ8Nvnle.d.ts → telemetry_configuration_parser-6BR3_dD_.d.ts} +2 -2
- package/dist/{telemetry_configuration_parser-CgSN7R6A.d.cts → telemetry_configuration_parser-ybeweUPE.d.cts} +2 -2
- package/dist/testing/index.d.cts +7 -5
- package/dist/testing/index.d.ts +7 -5
- package/dist/trace-adapter/index.d.cts +4 -3
- package/dist/trace-adapter/index.d.ts +4 -3
- package/dist/{transport.types-BAl_TRtU.d.cts → transport.types-C2dP-eed.d.cts} +1 -1
- package/dist/{transport.types-DDnZoKWi.d.ts → transport.types-SA8KpLiK.d.ts} +1 -1
- package/native/noego_telemetry_native.node +0 -0
- package/package.json +11 -4
|
@@ -67,11 +67,15 @@ var MetricDefinitionContract = class {
|
|
|
67
67
|
if (!eventSelector || !bracket) {
|
|
68
68
|
return { ok: false, error: `Invalid metric selector ${selector}` };
|
|
69
69
|
}
|
|
70
|
+
const groupBy = bracket === "*" ? [] : bracket.split(",").map((field) => field.trim());
|
|
71
|
+
if (groupBy.some((field) => field.length === 0)) {
|
|
72
|
+
return { ok: false, error: `Invalid metric selector ${selector}` };
|
|
73
|
+
}
|
|
70
74
|
return {
|
|
71
75
|
ok: true,
|
|
72
76
|
value: {
|
|
73
77
|
selector: eventSelector,
|
|
74
|
-
groupBy
|
|
78
|
+
groupBy,
|
|
75
79
|
wildcardUngrouped: bracket === "*"
|
|
76
80
|
}
|
|
77
81
|
};
|
|
@@ -1787,18 +1791,194 @@ var MetricGraphRenderer = class {
|
|
|
1787
1791
|
}
|
|
1788
1792
|
};
|
|
1789
1793
|
|
|
1794
|
+
// src/cli/history_metric_reader.ts
|
|
1795
|
+
import { existsSync } from "fs";
|
|
1796
|
+
import { dirname as dirname2, join } from "path";
|
|
1797
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
1798
|
+
var HISTORY_DATABASE_NAME = "history.db";
|
|
1799
|
+
var AGGREGATES = /* @__PURE__ */ new Set(["count", "sum", "min", "max", "last", "avg"]);
|
|
1800
|
+
function historyDatabasePath(target) {
|
|
1801
|
+
return join(dirname2(target.path), HISTORY_DATABASE_NAME);
|
|
1802
|
+
}
|
|
1803
|
+
var HistoryMetricReader = class _HistoryMetricReader {
|
|
1804
|
+
constructor(db, definitions) {
|
|
1805
|
+
this.db = db;
|
|
1806
|
+
this.definitions = definitions;
|
|
1807
|
+
}
|
|
1808
|
+
db;
|
|
1809
|
+
definitions;
|
|
1810
|
+
static open(databasePath, definitions) {
|
|
1811
|
+
const db = new BetterSqlite3(databasePath, { readonly: true, fileMustExist: true });
|
|
1812
|
+
db.pragma("busy_timeout = 1000");
|
|
1813
|
+
db.pragma("query_only = ON");
|
|
1814
|
+
return new _HistoryMetricReader(db, definitions);
|
|
1815
|
+
}
|
|
1816
|
+
static exists(databasePath) {
|
|
1817
|
+
return existsSync(databasePath);
|
|
1818
|
+
}
|
|
1819
|
+
metrics(query, targetId) {
|
|
1820
|
+
if (query.toMs <= query.fromMs) {
|
|
1821
|
+
return { ok: false, error: { kind: "invalid-query", message: "--to must be after --from" } };
|
|
1822
|
+
}
|
|
1823
|
+
const invalidMeasurement = query.measurements.find((measurement) => !isMeasurement(measurement));
|
|
1824
|
+
if (invalidMeasurement !== void 0) {
|
|
1825
|
+
return {
|
|
1826
|
+
ok: false,
|
|
1827
|
+
error: {
|
|
1828
|
+
kind: "invalid-query",
|
|
1829
|
+
message: `Unknown measurement ${invalidMeasurement}; use count or <field>.<sum|min|max|last|count|avg>`
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
const versions = this.registeredVersions(query.metric);
|
|
1834
|
+
if (versions.length === 0) {
|
|
1835
|
+
return {
|
|
1836
|
+
ok: false,
|
|
1837
|
+
error: { kind: "invalid-query", message: `Metric ${query.metric} has no projection in history.db` }
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
const resolutionMs = this.resolveResolution(query, versions);
|
|
1841
|
+
if (resolutionMs === null) {
|
|
1842
|
+
return {
|
|
1843
|
+
ok: false,
|
|
1844
|
+
error: { kind: "invalid-query", message: `Metric ${query.metric} is not projected at the requested resolution` }
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
for (let index = versions.length - 1; index >= 0; index -= 1) {
|
|
1848
|
+
const version = versions[index];
|
|
1849
|
+
const buckets = this.readBuckets(query, version, resolutionMs);
|
|
1850
|
+
if (buckets.length === 0) continue;
|
|
1851
|
+
const olderVersions = versions.slice(0, index);
|
|
1852
|
+
return {
|
|
1853
|
+
ok: true,
|
|
1854
|
+
value: Object.freeze({
|
|
1855
|
+
targetId,
|
|
1856
|
+
metric: query.metric,
|
|
1857
|
+
segments: [Object.freeze({
|
|
1858
|
+
fromMs: query.fromMs,
|
|
1859
|
+
toMs: query.toMs,
|
|
1860
|
+
resolutionMs,
|
|
1861
|
+
isAggregated: true,
|
|
1862
|
+
definitionVersion: version,
|
|
1863
|
+
buckets
|
|
1864
|
+
})],
|
|
1865
|
+
gaps: [],
|
|
1866
|
+
nextCursor: null,
|
|
1867
|
+
partialFailures: olderVersions.map((older) => Object.freeze({
|
|
1868
|
+
targetId,
|
|
1869
|
+
reason: `version ${older} buckets omitted; only version ${version} returned`
|
|
1870
|
+
}))
|
|
1871
|
+
})
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
return {
|
|
1875
|
+
ok: true,
|
|
1876
|
+
value: Object.freeze({
|
|
1877
|
+
targetId,
|
|
1878
|
+
metric: query.metric,
|
|
1879
|
+
segments: [],
|
|
1880
|
+
gaps: [Object.freeze({ fromMs: query.fromMs, toMs: query.toMs, reason: "not-materialized" })],
|
|
1881
|
+
nextCursor: null,
|
|
1882
|
+
partialFailures: []
|
|
1883
|
+
})
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
close() {
|
|
1887
|
+
if (this.db.open) this.db.close();
|
|
1888
|
+
}
|
|
1889
|
+
registeredVersions(metric) {
|
|
1890
|
+
const rows = this.db.prepare(
|
|
1891
|
+
"SELECT projection_version FROM trace_projection_definitions WHERE projection_name = ? ORDER BY projection_version"
|
|
1892
|
+
).all(metric);
|
|
1893
|
+
return rows.map((row) => row.projection_version);
|
|
1894
|
+
}
|
|
1895
|
+
resolveResolution(query, versions) {
|
|
1896
|
+
const available = this.definitions.filter((definition) => definition.name === query.metric && versions.includes(definition.version)).flatMap((definition) => [...definition.resolutionsMs]);
|
|
1897
|
+
const resolutions = [...new Set(available.length > 0 ? available : this.storedResolutions(query.metric))].sort((a, b) => a - b);
|
|
1898
|
+
if (resolutions.length === 0) return null;
|
|
1899
|
+
if (query.resolution !== "auto") {
|
|
1900
|
+
return resolutions.includes(query.resolution) ? query.resolution : null;
|
|
1901
|
+
}
|
|
1902
|
+
const span = query.toMs - query.fromMs;
|
|
1903
|
+
return resolutions.find((resolution) => Math.ceil(span / resolution) <= query.limit) ?? resolutions[resolutions.length - 1];
|
|
1904
|
+
}
|
|
1905
|
+
storedResolutions(metric) {
|
|
1906
|
+
const rows = this.db.prepare(
|
|
1907
|
+
"SELECT DISTINCT resolution_ms FROM trace_metric_buckets WHERE projection_name = ?"
|
|
1908
|
+
).all(metric);
|
|
1909
|
+
return rows.map((row) => row.resolution_ms);
|
|
1910
|
+
}
|
|
1911
|
+
readBuckets(query, version, resolutionMs) {
|
|
1912
|
+
const rows = this.db.prepare(`
|
|
1913
|
+
SELECT projection_version, bucket_start_ms, dimensions_json, count, values_json
|
|
1914
|
+
FROM trace_metric_buckets
|
|
1915
|
+
WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
|
|
1916
|
+
AND bucket_start_ms >= ? AND bucket_start_ms < ?
|
|
1917
|
+
ORDER BY bucket_start_ms ${query.order === "desc" ? "DESC" : "ASC"}, dimension_key
|
|
1918
|
+
`).all(query.metric, version, resolutionMs, query.fromMs, query.toMs);
|
|
1919
|
+
const buckets = [];
|
|
1920
|
+
for (const row of rows) {
|
|
1921
|
+
const dimensions = JSON.parse(row.dimensions_json);
|
|
1922
|
+
if (!matchesWhere(dimensions, query.where)) continue;
|
|
1923
|
+
const values = JSON.parse(row.values_json);
|
|
1924
|
+
buckets.push(Object.freeze({
|
|
1925
|
+
startMs: row.bucket_start_ms,
|
|
1926
|
+
endMs: row.bucket_start_ms + resolutionMs,
|
|
1927
|
+
dimensions,
|
|
1928
|
+
values: selectMeasurements(query.measurements, row.count, values)
|
|
1929
|
+
}));
|
|
1930
|
+
if (buckets.length >= query.limit) break;
|
|
1931
|
+
}
|
|
1932
|
+
return buckets;
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
function isMeasurement(measurement) {
|
|
1936
|
+
if (measurement === "count") return true;
|
|
1937
|
+
const dot = measurement.lastIndexOf(".");
|
|
1938
|
+
return dot > 0 && AGGREGATES.has(measurement.slice(dot + 1));
|
|
1939
|
+
}
|
|
1940
|
+
function selectMeasurements(measurements, bucketCount, values) {
|
|
1941
|
+
const selected = {};
|
|
1942
|
+
for (const measurement of measurements) {
|
|
1943
|
+
if (measurement === "count") {
|
|
1944
|
+
selected.count = bucketCount;
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
const dot = measurement.lastIndexOf(".");
|
|
1948
|
+
const field = measurement.slice(0, dot);
|
|
1949
|
+
const aggregate = measurement.slice(dot + 1);
|
|
1950
|
+
const state = values[field];
|
|
1951
|
+
if (state === void 0) continue;
|
|
1952
|
+
const value = aggregate === "avg" ? state.count > 0 ? state.sum / state.count : 0 : state[aggregate];
|
|
1953
|
+
if (!Number.isFinite(value)) continue;
|
|
1954
|
+
const existing = selected[field];
|
|
1955
|
+
selected[field] = { ...typeof existing === "object" ? existing : {}, [aggregate]: value };
|
|
1956
|
+
}
|
|
1957
|
+
return Object.freeze(selected);
|
|
1958
|
+
}
|
|
1959
|
+
function matchesWhere(dimensions, where) {
|
|
1960
|
+
for (const [key, filter] of Object.entries(where)) {
|
|
1961
|
+
const actual = dimensions[key];
|
|
1962
|
+
if (actual === void 0) return false;
|
|
1963
|
+
const accepted = Array.isArray(filter) ? filter : [filter];
|
|
1964
|
+
if (!accepted.some((candidate) => String(candidate) === actual)) return false;
|
|
1965
|
+
}
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1790
1969
|
// src/cli/offline_sqlite_session.ts
|
|
1791
1970
|
import { randomUUID } from "crypto";
|
|
1792
|
-
import {
|
|
1971
|
+
import { existsSync as existsSync3, statfsSync, statSync } from "fs";
|
|
1972
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
1793
1973
|
import { fileURLToPath } from "url";
|
|
1794
1974
|
import { MigrationRunnerFactory } from "@noego/proper";
|
|
1795
|
-
import
|
|
1975
|
+
import BetterSqlite32 from "better-sqlite3";
|
|
1796
1976
|
import { createSqliteDb } from "sqlstack/adapters";
|
|
1797
1977
|
|
|
1798
1978
|
// src/storage/sqlite_ownership_lease.ts
|
|
1799
1979
|
import {
|
|
1800
1980
|
closeSync,
|
|
1801
|
-
existsSync,
|
|
1981
|
+
existsSync as existsSync2,
|
|
1802
1982
|
mkdirSync,
|
|
1803
1983
|
openSync,
|
|
1804
1984
|
readFileSync as readFileSync2,
|
|
@@ -1807,13 +1987,13 @@ import {
|
|
|
1807
1987
|
rmSync,
|
|
1808
1988
|
writeFileSync
|
|
1809
1989
|
} from "fs";
|
|
1810
|
-
import { dirname as
|
|
1990
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
1811
1991
|
var SqliteOwnershipLease = class {
|
|
1812
1992
|
constructor(options) {
|
|
1813
1993
|
this.options = options;
|
|
1814
|
-
mkdirSync(
|
|
1815
|
-
const canonicalDirectory = realpathSync(
|
|
1816
|
-
const databaseName = resolve2(options.databasePath).slice(
|
|
1994
|
+
mkdirSync(dirname3(options.databasePath), { recursive: true });
|
|
1995
|
+
const canonicalDirectory = realpathSync(dirname3(resolve2(options.databasePath)));
|
|
1996
|
+
const databaseName = resolve2(options.databasePath).slice(dirname3(resolve2(options.databasePath)).length + 1);
|
|
1817
1997
|
this.canonicalDatabasePath = resolve2(canonicalDirectory, databaseName);
|
|
1818
1998
|
this.leasePath = `${this.canonicalDatabasePath}.telemetry-owner.json`;
|
|
1819
1999
|
}
|
|
@@ -1857,7 +2037,7 @@ var SqliteOwnershipLease = class {
|
|
|
1857
2037
|
});
|
|
1858
2038
|
}
|
|
1859
2039
|
release() {
|
|
1860
|
-
if (!this.held || !
|
|
2040
|
+
if (!this.held || !existsSync2(this.leasePath)) {
|
|
1861
2041
|
this.held = false;
|
|
1862
2042
|
return;
|
|
1863
2043
|
}
|
|
@@ -4484,6 +4664,87 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
|
|
|
4484
4664
|
this.verifySchema();
|
|
4485
4665
|
return this.migrationStatus();
|
|
4486
4666
|
}
|
|
4667
|
+
async compact() {
|
|
4668
|
+
this.requireMaintenanceMode();
|
|
4669
|
+
const connection = this.requireConnection();
|
|
4670
|
+
const maintenanceTable = this.requireTableMap().physical("maintenanceRuns");
|
|
4671
|
+
const runId = randomUUID();
|
|
4672
|
+
const startedAtMs = this.now();
|
|
4673
|
+
connection.prepare(
|
|
4674
|
+
`INSERT INTO "${maintenanceTable}" (
|
|
4675
|
+
target_id, run_id, state, stage, started_at_ms,
|
|
4676
|
+
completed_at_ms, affected_rows, affected_pages, detail
|
|
4677
|
+
) VALUES (?, ?, 'started', 'compaction', ?, NULL, 0, 0, ?)`
|
|
4678
|
+
).run(this.target.targetId, runId, startedAtMs, "offline-cli; outcome=started");
|
|
4679
|
+
try {
|
|
4680
|
+
const before = this.compactionSnapshot();
|
|
4681
|
+
connection.pragma("wal_checkpoint(TRUNCATE)");
|
|
4682
|
+
const checkpointed = this.compactionSnapshot();
|
|
4683
|
+
this.assertCompactionSpace(checkpointed);
|
|
4684
|
+
connection.pragma("auto_vacuum = INCREMENTAL");
|
|
4685
|
+
connection.exec("VACUUM");
|
|
4686
|
+
connection.pragma("wal_checkpoint(TRUNCATE)");
|
|
4687
|
+
const after = this.compactionSnapshot();
|
|
4688
|
+
if (after.autoVacuum !== 2) {
|
|
4689
|
+
throw new Error("Telemetry compaction did not enable auto_vacuum=INCREMENTAL");
|
|
4690
|
+
}
|
|
4691
|
+
const completedAtMs = this.now();
|
|
4692
|
+
const result = Object.freeze({
|
|
4693
|
+
targetId: this.target.targetId,
|
|
4694
|
+
autoVacuum: "incremental",
|
|
4695
|
+
pageSize: checkpointed.pageSize,
|
|
4696
|
+
pagesBefore: checkpointed.pageCount,
|
|
4697
|
+
pagesAfter: after.pageCount,
|
|
4698
|
+
freePagesBefore: checkpointed.freePages,
|
|
4699
|
+
freePagesAfter: after.freePages,
|
|
4700
|
+
reclaimedPages: Math.max(0, checkpointed.pageCount - after.pageCount),
|
|
4701
|
+
databaseBytesBefore: before.databaseBytes,
|
|
4702
|
+
databaseBytesAfter: after.databaseBytes,
|
|
4703
|
+
walBytesBefore: before.walBytes,
|
|
4704
|
+
walBytesAfter: after.walBytes,
|
|
4705
|
+
reclaimedBytes: Math.max(
|
|
4706
|
+
0,
|
|
4707
|
+
before.databaseBytes + before.walBytes - after.databaseBytes - after.walBytes
|
|
4708
|
+
),
|
|
4709
|
+
elapsedMs: completedAtMs - startedAtMs
|
|
4710
|
+
});
|
|
4711
|
+
connection.prepare(
|
|
4712
|
+
`UPDATE "${maintenanceTable}"
|
|
4713
|
+
SET state = 'completed', completed_at_ms = ?, affected_pages = ?, detail = ?
|
|
4714
|
+
WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
|
|
4715
|
+
).run(
|
|
4716
|
+
completedAtMs,
|
|
4717
|
+
result.reclaimedPages,
|
|
4718
|
+
JSON.stringify({
|
|
4719
|
+
mode: "offline-cli",
|
|
4720
|
+
outcome: "completed",
|
|
4721
|
+
reclaimedBytes: result.reclaimedBytes,
|
|
4722
|
+
walBytesBefore: result.walBytesBefore,
|
|
4723
|
+
walBytesAfter: result.walBytesAfter,
|
|
4724
|
+
autoVacuum: result.autoVacuum
|
|
4725
|
+
}),
|
|
4726
|
+
this.target.targetId,
|
|
4727
|
+
runId
|
|
4728
|
+
);
|
|
4729
|
+
return result;
|
|
4730
|
+
} catch (cause) {
|
|
4731
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
4732
|
+
try {
|
|
4733
|
+
connection.prepare(
|
|
4734
|
+
`UPDATE "${maintenanceTable}"
|
|
4735
|
+
SET state = 'failed', completed_at_ms = ?, detail = ?
|
|
4736
|
+
WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
|
|
4737
|
+
).run(
|
|
4738
|
+
this.now(),
|
|
4739
|
+
JSON.stringify({ mode: "offline-cli", outcome: "failed", error: message }),
|
|
4740
|
+
this.target.targetId,
|
|
4741
|
+
runId
|
|
4742
|
+
);
|
|
4743
|
+
} catch {
|
|
4744
|
+
}
|
|
4745
|
+
throw cause;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4487
4748
|
async close() {
|
|
4488
4749
|
try {
|
|
4489
4750
|
if (this.connection?.open) {
|
|
@@ -4517,7 +4778,7 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
|
|
|
4517
4778
|
throw new Error(acquired.error);
|
|
4518
4779
|
}
|
|
4519
4780
|
}
|
|
4520
|
-
const connection = new
|
|
4781
|
+
const connection = new BetterSqlite32(this.target.path, {
|
|
4521
4782
|
readonly: this.mode === "query",
|
|
4522
4783
|
fileMustExist: true
|
|
4523
4784
|
});
|
|
@@ -4544,6 +4805,32 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
|
|
|
4544
4805
|
}
|
|
4545
4806
|
}
|
|
4546
4807
|
}
|
|
4808
|
+
compactionSnapshot() {
|
|
4809
|
+
const connection = this.requireConnection();
|
|
4810
|
+
return Object.freeze({
|
|
4811
|
+
pageSize: Number(connection.pragma("page_size", { simple: true })),
|
|
4812
|
+
pageCount: Number(connection.pragma("page_count", { simple: true })),
|
|
4813
|
+
freePages: Number(connection.pragma("freelist_count", { simple: true })),
|
|
4814
|
+
autoVacuum: Number(connection.pragma("auto_vacuum", { simple: true })),
|
|
4815
|
+
databaseBytes: this.fileBytes(this.target.path),
|
|
4816
|
+
walBytes: this.fileBytes(`${this.target.path}-wal`)
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4819
|
+
assertCompactionSpace(snapshot) {
|
|
4820
|
+
const liveBytes = Math.max(0, snapshot.pageCount - snapshot.freePages) * snapshot.pageSize;
|
|
4821
|
+
const reserveBytes = Math.max(64 * 1024 * 1024, Math.ceil(liveBytes * 0.05));
|
|
4822
|
+
const filesystem = statfsSync(dirname4(this.target.path));
|
|
4823
|
+
const availableBytes = Number(filesystem.bavail) * Number(filesystem.bsize);
|
|
4824
|
+
const requiredBytes = liveBytes + reserveBytes;
|
|
4825
|
+
if (availableBytes < requiredBytes) {
|
|
4826
|
+
throw new Error(
|
|
4827
|
+
`Telemetry compaction requires at least ${requiredBytes} free bytes; only ${availableBytes} are available`
|
|
4828
|
+
);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
fileBytes(path) {
|
|
4832
|
+
return existsSync3(path) ? statSync(path).size : 0;
|
|
4833
|
+
}
|
|
4547
4834
|
verifyManifest() {
|
|
4548
4835
|
const rows = this.requireConnection().prepare(
|
|
4549
4836
|
`SELECT target_id, format_version, mapping_json, mapping_checksum,
|
|
@@ -4728,7 +5015,8 @@ var ALLOWED_OPTIONS = Object.freeze({
|
|
|
4728
5015
|
"metric",
|
|
4729
5016
|
"where",
|
|
4730
5017
|
"measurement",
|
|
4731
|
-
"resolution"
|
|
5018
|
+
"resolution",
|
|
5019
|
+
"source"
|
|
4732
5020
|
]),
|
|
4733
5021
|
"metrics-graph": /* @__PURE__ */ new Set([
|
|
4734
5022
|
...COMMON_QUERY_OPTIONS,
|
|
@@ -4736,6 +5024,7 @@ var ALLOWED_OPTIONS = Object.freeze({
|
|
|
4736
5024
|
"where",
|
|
4737
5025
|
"measurement",
|
|
4738
5026
|
"resolution",
|
|
5027
|
+
"source",
|
|
4739
5028
|
"type",
|
|
4740
5029
|
"output",
|
|
4741
5030
|
"width",
|
|
@@ -4753,6 +5042,7 @@ var ALLOWED_OPTIONS = Object.freeze({
|
|
|
4753
5042
|
"limit",
|
|
4754
5043
|
"max-duration"
|
|
4755
5044
|
]),
|
|
5045
|
+
"sqlite-compact": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
|
|
4756
5046
|
"sqlite-migration-status": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
|
|
4757
5047
|
"sqlite-migration-up": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"])
|
|
4758
5048
|
});
|
|
@@ -4762,6 +5052,7 @@ var COMMANDS = Object.freeze([
|
|
|
4762
5052
|
{ tokens: ["metrics", "query"], kind: "metrics-query" },
|
|
4763
5053
|
{ tokens: ["metrics", "graph"], kind: "metrics-graph" },
|
|
4764
5054
|
{ tokens: ["sqlite", "query"], kind: "sqlite-query" },
|
|
5055
|
+
{ tokens: ["sqlite", "compact"], kind: "sqlite-compact" },
|
|
4765
5056
|
{ tokens: ["sqlite", "migration", "status"], kind: "sqlite-migration-status" },
|
|
4766
5057
|
{ tokens: ["sqlite", "migration", "up"], kind: "sqlite-migration-up" }
|
|
4767
5058
|
]);
|
|
@@ -5040,6 +5331,22 @@ var TelemetryCliFormatter = class {
|
|
|
5040
5331
|
return `${lines.join("\n")}
|
|
5041
5332
|
`;
|
|
5042
5333
|
}
|
|
5334
|
+
compaction(result, format) {
|
|
5335
|
+
if (format === "json") {
|
|
5336
|
+
return this.json(result);
|
|
5337
|
+
}
|
|
5338
|
+
return [
|
|
5339
|
+
`Target: ${result.targetId}`,
|
|
5340
|
+
`Auto-vacuum: ${result.autoVacuum}`,
|
|
5341
|
+
`Pages: ${result.pagesBefore} -> ${result.pagesAfter} (${result.reclaimedPages} reclaimed)`,
|
|
5342
|
+
`Freelist: ${result.freePagesBefore} -> ${result.freePagesAfter}`,
|
|
5343
|
+
`Database bytes: ${result.databaseBytesBefore} -> ${result.databaseBytesAfter}`,
|
|
5344
|
+
`WAL bytes: ${result.walBytesBefore} -> ${result.walBytesAfter}`,
|
|
5345
|
+
`Filesystem bytes reclaimed: ${result.reclaimedBytes}`,
|
|
5346
|
+
`Elapsed ms: ${result.elapsedMs}`,
|
|
5347
|
+
""
|
|
5348
|
+
].join("\n");
|
|
5349
|
+
}
|
|
5043
5350
|
migrations(result, format) {
|
|
5044
5351
|
if (format === "json") {
|
|
5045
5352
|
return this.json(result);
|
|
@@ -5147,6 +5454,10 @@ var TelemetryCli = class {
|
|
|
5147
5454
|
storageBaseDirectory === null ? {} : { storageBaseDirectory }
|
|
5148
5455
|
);
|
|
5149
5456
|
const target = this.resolveTarget(configuration, this.optionalOption(command, "target"));
|
|
5457
|
+
if (this.metricSource(command, target) === "history") {
|
|
5458
|
+
await this.executeHistoryMetrics(command, target, configuration);
|
|
5459
|
+
return 0;
|
|
5460
|
+
}
|
|
5150
5461
|
const mode = this.sessionMode(command);
|
|
5151
5462
|
const session = await this.createSession(configuration, target, mode);
|
|
5152
5463
|
this.activeSession = session;
|
|
@@ -5160,23 +5471,61 @@ var TelemetryCli = class {
|
|
|
5160
5471
|
}
|
|
5161
5472
|
}
|
|
5162
5473
|
}
|
|
5474
|
+
/**
|
|
5475
|
+
* Which store answers a metric command. `history` is the projector's
|
|
5476
|
+
* pre-aggregated buckets (history.db, current whenever the app runs with
|
|
5477
|
+
* ingest-time extraction off); `legacy` is the collector's own rollups in
|
|
5478
|
+
* telemetry.db. `auto` prefers history.db when it exists beside the target —
|
|
5479
|
+
* since ingestion moved to the projector the legacy rollups stopped
|
|
5480
|
+
* advancing, so answering from them silently reports stale gaps.
|
|
5481
|
+
*/
|
|
5482
|
+
metricSource(command, target) {
|
|
5483
|
+
if (command.kind !== "metrics-query" && command.kind !== "metrics-graph") return "legacy";
|
|
5484
|
+
const requested = this.optionalOption(command, "source") ?? "auto";
|
|
5485
|
+
if (requested === "history" || requested === "legacy") return requested;
|
|
5486
|
+
if (requested !== "auto") {
|
|
5487
|
+
throw new TelemetryCliUsageError("--source must be auto, history, or legacy");
|
|
5488
|
+
}
|
|
5489
|
+
return HistoryMetricReader.exists(historyDatabasePath(target)) ? "history" : "legacy";
|
|
5490
|
+
}
|
|
5491
|
+
async executeHistoryMetrics(command, target, configuration) {
|
|
5492
|
+
const databasePath = historyDatabasePath(target);
|
|
5493
|
+
if (!HistoryMetricReader.exists(databasePath)) {
|
|
5494
|
+
throw new TelemetryCliUsageError(`No projector history store at ${databasePath}`);
|
|
5495
|
+
}
|
|
5496
|
+
const reader = HistoryMetricReader.open(databasePath, configuration.metrics);
|
|
5497
|
+
try {
|
|
5498
|
+
const run = async (query) => reader.metrics(query, target.targetId);
|
|
5499
|
+
if (command.kind === "metrics-graph") {
|
|
5500
|
+
await this.metricGraph(command, target, run);
|
|
5501
|
+
} else {
|
|
5502
|
+
await this.metricQuery(command, target, run);
|
|
5503
|
+
}
|
|
5504
|
+
} finally {
|
|
5505
|
+
reader.close();
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5163
5508
|
async executeCommand(command, target, session) {
|
|
5164
5509
|
if (command.kind === "timeline-query") {
|
|
5165
5510
|
await this.timelineQuery(command, target, session);
|
|
5166
5511
|
return;
|
|
5167
5512
|
}
|
|
5168
5513
|
if (command.kind === "metrics-query") {
|
|
5169
|
-
await this.metricQuery(command, target, session);
|
|
5514
|
+
await this.metricQuery(command, target, (query) => session.metrics(query));
|
|
5170
5515
|
return;
|
|
5171
5516
|
}
|
|
5172
5517
|
if (command.kind === "metrics-graph") {
|
|
5173
|
-
await this.metricGraph(command, target, session);
|
|
5518
|
+
await this.metricGraph(command, target, (query) => session.metrics(query));
|
|
5174
5519
|
return;
|
|
5175
5520
|
}
|
|
5176
5521
|
if (command.kind === "sqlite-query") {
|
|
5177
5522
|
await this.sqliteQuery(command, target, session);
|
|
5178
5523
|
return;
|
|
5179
5524
|
}
|
|
5525
|
+
if (command.kind === "sqlite-compact") {
|
|
5526
|
+
await this.sqliteCompact(command, target, session);
|
|
5527
|
+
return;
|
|
5528
|
+
}
|
|
5180
5529
|
if (command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up") {
|
|
5181
5530
|
await this.sqliteMigration(command, target, session);
|
|
5182
5531
|
return;
|
|
@@ -5206,8 +5555,8 @@ var TelemetryCli = class {
|
|
|
5206
5555
|
const output = this.formatter.timeline(result.value, format, this.timezoneOption(command));
|
|
5207
5556
|
this.writeBounded(output, target.query.maxResponseBytes);
|
|
5208
5557
|
}
|
|
5209
|
-
async metricQuery(command, target,
|
|
5210
|
-
const result = await
|
|
5558
|
+
async metricQuery(command, target, runMetrics) {
|
|
5559
|
+
const result = await runMetrics(this.metricQueryInput(command, target));
|
|
5211
5560
|
if (!result.ok) {
|
|
5212
5561
|
throw new Error(`${result.error.kind}: ${result.error.message}`);
|
|
5213
5562
|
}
|
|
@@ -5218,12 +5567,12 @@ var TelemetryCli = class {
|
|
|
5218
5567
|
);
|
|
5219
5568
|
this.writeBounded(output, target.query.maxResponseBytes);
|
|
5220
5569
|
}
|
|
5221
|
-
async metricGraph(command, target,
|
|
5570
|
+
async metricGraph(command, target, runMetrics) {
|
|
5222
5571
|
const measurements = this.options(command, "measurement");
|
|
5223
5572
|
if (measurements.length !== 1) {
|
|
5224
5573
|
throw new TelemetryCliUsageError("metrics graph requires exactly one --measurement");
|
|
5225
5574
|
}
|
|
5226
|
-
const result = await
|
|
5575
|
+
const result = await runMetrics(this.metricQueryInput(command, target));
|
|
5227
5576
|
if (!result.ok) {
|
|
5228
5577
|
throw new Error(`${result.error.kind}: ${result.error.message}`);
|
|
5229
5578
|
}
|
|
@@ -5274,6 +5623,14 @@ var TelemetryCli = class {
|
|
|
5274
5623
|
}
|
|
5275
5624
|
this.writeBounded(this.formatter.migrations(result, format), target.query.maxResponseBytes);
|
|
5276
5625
|
}
|
|
5626
|
+
async sqliteCompact(command, target, session) {
|
|
5627
|
+
const format = this.optionalOption(command, "format") ?? "table";
|
|
5628
|
+
if (format !== "table" && format !== "json") {
|
|
5629
|
+
throw new TelemetryCliUsageError("Compaction format must be table or json");
|
|
5630
|
+
}
|
|
5631
|
+
const result = await session.compact();
|
|
5632
|
+
this.writeBounded(this.formatter.compaction(result, format), target.query.maxResponseBytes);
|
|
5633
|
+
}
|
|
5277
5634
|
metricQueryInput(command, target) {
|
|
5278
5635
|
const measurements = this.options(command, "measurement");
|
|
5279
5636
|
if (measurements.length === 0) {
|
|
@@ -5308,7 +5665,7 @@ var TelemetryCli = class {
|
|
|
5308
5665
|
return target;
|
|
5309
5666
|
}
|
|
5310
5667
|
sessionMode(command) {
|
|
5311
|
-
return command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
|
|
5668
|
+
return command.kind === "sqlite-compact" || command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
|
|
5312
5669
|
}
|
|
5313
5670
|
structuredFormat(command) {
|
|
5314
5671
|
const value = this.optionalOption(command, "format") ?? "table";
|
|
@@ -5475,9 +5832,10 @@ var TelemetryCli = class {
|
|
|
5475
5832
|
help() {
|
|
5476
5833
|
return `Usage:
|
|
5477
5834
|
telemetry timeline query --config FILE --from TIME --to TIME [filters] [--format table|json|jsonl|csv]
|
|
5478
|
-
telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE]
|
|
5479
|
-
telemetry metrics graph --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--type line|bar|stacked-bar] [--format terminal|svg|html]
|
|
5835
|
+
telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE] [--source auto|history|legacy]
|
|
5836
|
+
telemetry metrics graph --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--type line|bar|stacked-bar] [--format terminal|svg|html] [--source auto|history|legacy]
|
|
5480
5837
|
telemetry sqlite query --config FILE --sql SQL [--param NAME=VALUE] [--format table|json|jsonl|csv]
|
|
5838
|
+
telemetry sqlite compact --config FILE [--format table|json]
|
|
5481
5839
|
telemetry sqlite migration status --config FILE [--format table|json]
|
|
5482
5840
|
telemetry sqlite migration up --config FILE [--format table|json]
|
|
5483
5841
|
|