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