@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.
Files changed (79) hide show
  1. package/README.md +6 -0
  2. package/dist/browser/index.d.cts +6 -5
  3. package/dist/browser/index.d.ts +6 -5
  4. package/dist/{browser_telemetry_client-BSAMZ8Qb.d.cts → browser_telemetry_client-ClZl3awO.d.cts} +2 -2
  5. package/dist/{browser_telemetry_client-DPBU6NGw.d.ts → browser_telemetry_client-t4dPp8Ku.d.ts} +2 -2
  6. package/dist/cli/telemetry_bin.cjs +393 -35
  7. package/dist/cli/telemetry_bin.cjs.map +1 -1
  8. package/dist/cli/telemetry_bin.js +378 -20
  9. package/dist/cli/telemetry_bin.js.map +1 -1
  10. package/dist/cli/telemetry_cli.cjs +393 -35
  11. package/dist/cli/telemetry_cli.cjs.map +1 -1
  12. package/dist/cli/telemetry_cli.d.cts +35 -5
  13. package/dist/cli/telemetry_cli.d.ts +35 -5
  14. package/dist/cli/telemetry_cli.js +378 -20
  15. package/dist/cli/telemetry_cli.js.map +1 -1
  16. package/dist/{client_defaults-ShVYQYmn.d.ts → client_defaults-CZ8PysZX.d.ts} +2 -2
  17. package/dist/{client_defaults-Dk0MMzvx.d.cts → client_defaults-lDbWYWWK.d.cts} +2 -2
  18. package/dist/core/index.cjs +5 -1
  19. package/dist/core/index.cjs.map +1 -1
  20. package/dist/core/index.d.cts +15 -12
  21. package/dist/core/index.d.ts +15 -12
  22. package/dist/core/index.js +5 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/electron/index.d.cts +5 -4
  25. package/dist/electron/index.d.ts +5 -4
  26. package/dist/{envelope.types-rOLpWXRC.d.cts → envelope.types-DIvZD3xH.d.ts} +2 -6
  27. package/dist/{envelope.types-rOLpWXRC.d.ts → envelope.types-DyrlRG49.d.cts} +2 -6
  28. package/dist/{in_memory_batch_transport-DKW9EZnU.d.cts → in_memory_batch_transport-3T6gElXP.d.cts} +1 -1
  29. package/dist/{in_memory_batch_transport-oOwNxXnA.d.ts → in_memory_batch_transport-DpQU6JqM.d.ts} +1 -1
  30. package/dist/index.cjs +5 -1
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +14 -12
  33. package/dist/index.d.ts +14 -12
  34. package/dist/index.js +5 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/json.types-BqS77fcN.d.cts +7 -0
  37. package/dist/json.types-BqS77fcN.d.ts +7 -0
  38. package/dist/{maintenance_coordinator.types-C4ULi4A4.d.cts → maintenance_coordinator.types-Crhx9i9k.d.cts} +3 -3
  39. package/dist/{maintenance_coordinator.types-ikUrRXmO.d.ts → maintenance_coordinator.types-DprmgpVn.d.ts} +3 -3
  40. package/dist/metric.types-D8aOAnUm.d.ts +67 -0
  41. package/dist/metric.types-Dg4eZq6u.d.cts +67 -0
  42. package/dist/{metric_definition_contract-BYtnODhu.d.ts → metric_definition_contract-C6MERhJs.d.ts} +1 -1
  43. package/dist/{metric_definition_contract-DfhhmTZ1.d.cts → metric_definition_contract-DoAk7NOw.d.cts} +1 -1
  44. package/dist/node/index.cjs +5 -1
  45. package/dist/node/index.cjs.map +1 -1
  46. package/dist/node/index.d.cts +11 -9
  47. package/dist/node/index.d.ts +11 -9
  48. package/dist/node/index.js +5 -1
  49. package/dist/node/index.js.map +1 -1
  50. package/dist/node/producer/index.d.cts +5 -4
  51. package/dist/node/producer/index.d.ts +5 -4
  52. package/dist/projector/index.cjs +1370 -0
  53. package/dist/projector/index.cjs.map +1 -0
  54. package/dist/projector/index.d.cts +246 -0
  55. package/dist/projector/index.d.ts +246 -0
  56. package/dist/projector/index.js +1328 -0
  57. package/dist/projector/index.js.map +1 -0
  58. package/dist/{query.types-DU5v7vM0.d.ts → query.types-CRqAsGTX.d.cts} +4 -66
  59. package/dist/{query.types-DuYWbFFe.d.cts → query.types-DnOMYXpC.d.ts} +4 -66
  60. package/dist/sqlite/index.cjs +5 -1
  61. package/dist/sqlite/index.cjs.map +1 -1
  62. package/dist/sqlite/index.d.cts +7 -5
  63. package/dist/sqlite/index.d.ts +7 -5
  64. package/dist/sqlite/index.js +5 -1
  65. package/dist/sqlite/index.js.map +1 -1
  66. package/dist/{storage.types-k7oElEB3.d.ts → storage.types-BKWoIOHp.d.ts} +3 -2
  67. package/dist/{storage.types-Bji_pWqr.d.cts → storage.types-DHbAm3j_.d.cts} +3 -2
  68. package/dist/{telemetry_client-BO5Pjp41.d.cts → telemetry_client-Bg5tbj5E.d.cts} +3 -2
  69. package/dist/{telemetry_client-D8uZ-mTL.d.ts → telemetry_client-DWKgufmc.d.ts} +3 -2
  70. package/dist/{telemetry_configuration_parser-BZ8Nvnle.d.ts → telemetry_configuration_parser-6BR3_dD_.d.ts} +2 -2
  71. package/dist/{telemetry_configuration_parser-CgSN7R6A.d.cts → telemetry_configuration_parser-ybeweUPE.d.cts} +2 -2
  72. package/dist/testing/index.d.cts +7 -5
  73. package/dist/testing/index.d.ts +7 -5
  74. package/dist/trace-adapter/index.d.cts +4 -3
  75. package/dist/trace-adapter/index.d.ts +4 -3
  76. package/dist/{transport.types-BAl_TRtU.d.cts → transport.types-C2dP-eed.d.cts} +1 -1
  77. package/dist/{transport.types-DDnZoKWi.d.ts → transport.types-SA8KpLiK.d.ts} +1 -1
  78. package/native/noego_telemetry_native.node +0 -0
  79. package/package.json +11 -4
@@ -25,7 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  ));
26
26
 
27
27
  // src/cli/telemetry_cli.ts
28
- var import_node_fs3 = require("fs");
28
+ var import_node_fs5 = require("fs");
29
29
 
30
30
  // src/platform/node/telemetry_configuration_parser.ts
31
31
  var import_node_path = require("path");
@@ -90,11 +90,15 @@ var MetricDefinitionContract = class {
90
90
  if (!eventSelector || !bracket) {
91
91
  return { ok: false, error: `Invalid metric selector ${selector}` };
92
92
  }
93
+ const groupBy = bracket === "*" ? [] : bracket.split(",").map((field) => field.trim());
94
+ if (groupBy.some((field) => field.length === 0)) {
95
+ return { ok: false, error: `Invalid metric selector ${selector}` };
96
+ }
93
97
  return {
94
98
  ok: true,
95
99
  value: {
96
100
  selector: eventSelector,
97
- groupBy: bracket === "*" ? [] : [bracket],
101
+ groupBy,
98
102
  wildcardUngrouped: bracket === "*"
99
103
  }
100
104
  };
@@ -1810,24 +1814,200 @@ var MetricGraphRenderer = class {
1810
1814
  }
1811
1815
  };
1812
1816
 
1817
+ // src/cli/history_metric_reader.ts
1818
+ var import_node_fs2 = require("fs");
1819
+ var import_node_path2 = require("path");
1820
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1821
+ var HISTORY_DATABASE_NAME = "history.db";
1822
+ var AGGREGATES = /* @__PURE__ */ new Set(["count", "sum", "min", "max", "last", "avg"]);
1823
+ function historyDatabasePath(target) {
1824
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)(target.path), HISTORY_DATABASE_NAME);
1825
+ }
1826
+ var HistoryMetricReader = class _HistoryMetricReader {
1827
+ constructor(db, definitions) {
1828
+ this.db = db;
1829
+ this.definitions = definitions;
1830
+ }
1831
+ db;
1832
+ definitions;
1833
+ static open(databasePath, definitions) {
1834
+ const db = new import_better_sqlite3.default(databasePath, { readonly: true, fileMustExist: true });
1835
+ db.pragma("busy_timeout = 1000");
1836
+ db.pragma("query_only = ON");
1837
+ return new _HistoryMetricReader(db, definitions);
1838
+ }
1839
+ static exists(databasePath) {
1840
+ return (0, import_node_fs2.existsSync)(databasePath);
1841
+ }
1842
+ metrics(query, targetId) {
1843
+ if (query.toMs <= query.fromMs) {
1844
+ return { ok: false, error: { kind: "invalid-query", message: "--to must be after --from" } };
1845
+ }
1846
+ const invalidMeasurement = query.measurements.find((measurement) => !isMeasurement(measurement));
1847
+ if (invalidMeasurement !== void 0) {
1848
+ return {
1849
+ ok: false,
1850
+ error: {
1851
+ kind: "invalid-query",
1852
+ message: `Unknown measurement ${invalidMeasurement}; use count or <field>.<sum|min|max|last|count|avg>`
1853
+ }
1854
+ };
1855
+ }
1856
+ const versions = this.registeredVersions(query.metric);
1857
+ if (versions.length === 0) {
1858
+ return {
1859
+ ok: false,
1860
+ error: { kind: "invalid-query", message: `Metric ${query.metric} has no projection in history.db` }
1861
+ };
1862
+ }
1863
+ const resolutionMs = this.resolveResolution(query, versions);
1864
+ if (resolutionMs === null) {
1865
+ return {
1866
+ ok: false,
1867
+ error: { kind: "invalid-query", message: `Metric ${query.metric} is not projected at the requested resolution` }
1868
+ };
1869
+ }
1870
+ for (let index = versions.length - 1; index >= 0; index -= 1) {
1871
+ const version = versions[index];
1872
+ const buckets = this.readBuckets(query, version, resolutionMs);
1873
+ if (buckets.length === 0) continue;
1874
+ const olderVersions = versions.slice(0, index);
1875
+ return {
1876
+ ok: true,
1877
+ value: Object.freeze({
1878
+ targetId,
1879
+ metric: query.metric,
1880
+ segments: [Object.freeze({
1881
+ fromMs: query.fromMs,
1882
+ toMs: query.toMs,
1883
+ resolutionMs,
1884
+ isAggregated: true,
1885
+ definitionVersion: version,
1886
+ buckets
1887
+ })],
1888
+ gaps: [],
1889
+ nextCursor: null,
1890
+ partialFailures: olderVersions.map((older) => Object.freeze({
1891
+ targetId,
1892
+ reason: `version ${older} buckets omitted; only version ${version} returned`
1893
+ }))
1894
+ })
1895
+ };
1896
+ }
1897
+ return {
1898
+ ok: true,
1899
+ value: Object.freeze({
1900
+ targetId,
1901
+ metric: query.metric,
1902
+ segments: [],
1903
+ gaps: [Object.freeze({ fromMs: query.fromMs, toMs: query.toMs, reason: "not-materialized" })],
1904
+ nextCursor: null,
1905
+ partialFailures: []
1906
+ })
1907
+ };
1908
+ }
1909
+ close() {
1910
+ if (this.db.open) this.db.close();
1911
+ }
1912
+ registeredVersions(metric) {
1913
+ const rows = this.db.prepare(
1914
+ "SELECT projection_version FROM trace_projection_definitions WHERE projection_name = ? ORDER BY projection_version"
1915
+ ).all(metric);
1916
+ return rows.map((row) => row.projection_version);
1917
+ }
1918
+ resolveResolution(query, versions) {
1919
+ const available = this.definitions.filter((definition) => definition.name === query.metric && versions.includes(definition.version)).flatMap((definition) => [...definition.resolutionsMs]);
1920
+ const resolutions = [...new Set(available.length > 0 ? available : this.storedResolutions(query.metric))].sort((a, b) => a - b);
1921
+ if (resolutions.length === 0) return null;
1922
+ if (query.resolution !== "auto") {
1923
+ return resolutions.includes(query.resolution) ? query.resolution : null;
1924
+ }
1925
+ const span = query.toMs - query.fromMs;
1926
+ return resolutions.find((resolution) => Math.ceil(span / resolution) <= query.limit) ?? resolutions[resolutions.length - 1];
1927
+ }
1928
+ storedResolutions(metric) {
1929
+ const rows = this.db.prepare(
1930
+ "SELECT DISTINCT resolution_ms FROM trace_metric_buckets WHERE projection_name = ?"
1931
+ ).all(metric);
1932
+ return rows.map((row) => row.resolution_ms);
1933
+ }
1934
+ readBuckets(query, version, resolutionMs) {
1935
+ const rows = this.db.prepare(`
1936
+ SELECT projection_version, bucket_start_ms, dimensions_json, count, values_json
1937
+ FROM trace_metric_buckets
1938
+ WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
1939
+ AND bucket_start_ms >= ? AND bucket_start_ms < ?
1940
+ ORDER BY bucket_start_ms ${query.order === "desc" ? "DESC" : "ASC"}, dimension_key
1941
+ `).all(query.metric, version, resolutionMs, query.fromMs, query.toMs);
1942
+ const buckets = [];
1943
+ for (const row of rows) {
1944
+ const dimensions = JSON.parse(row.dimensions_json);
1945
+ if (!matchesWhere(dimensions, query.where)) continue;
1946
+ const values = JSON.parse(row.values_json);
1947
+ buckets.push(Object.freeze({
1948
+ startMs: row.bucket_start_ms,
1949
+ endMs: row.bucket_start_ms + resolutionMs,
1950
+ dimensions,
1951
+ values: selectMeasurements(query.measurements, row.count, values)
1952
+ }));
1953
+ if (buckets.length >= query.limit) break;
1954
+ }
1955
+ return buckets;
1956
+ }
1957
+ };
1958
+ function isMeasurement(measurement) {
1959
+ if (measurement === "count") return true;
1960
+ const dot = measurement.lastIndexOf(".");
1961
+ return dot > 0 && AGGREGATES.has(measurement.slice(dot + 1));
1962
+ }
1963
+ function selectMeasurements(measurements, bucketCount, values) {
1964
+ const selected = {};
1965
+ for (const measurement of measurements) {
1966
+ if (measurement === "count") {
1967
+ selected.count = bucketCount;
1968
+ continue;
1969
+ }
1970
+ const dot = measurement.lastIndexOf(".");
1971
+ const field = measurement.slice(0, dot);
1972
+ const aggregate = measurement.slice(dot + 1);
1973
+ const state = values[field];
1974
+ if (state === void 0) continue;
1975
+ const value = aggregate === "avg" ? state.count > 0 ? state.sum / state.count : 0 : state[aggregate];
1976
+ if (!Number.isFinite(value)) continue;
1977
+ const existing = selected[field];
1978
+ selected[field] = { ...typeof existing === "object" ? existing : {}, [aggregate]: value };
1979
+ }
1980
+ return Object.freeze(selected);
1981
+ }
1982
+ function matchesWhere(dimensions, where) {
1983
+ for (const [key, filter] of Object.entries(where)) {
1984
+ const actual = dimensions[key];
1985
+ if (actual === void 0) return false;
1986
+ const accepted = Array.isArray(filter) ? filter : [filter];
1987
+ if (!accepted.some((candidate) => String(candidate) === actual)) return false;
1988
+ }
1989
+ return true;
1990
+ }
1991
+
1813
1992
  // src/cli/offline_sqlite_session.ts
1814
1993
  var import_node_crypto2 = require("crypto");
1815
- var import_node_path3 = require("path");
1994
+ var import_node_fs4 = require("fs");
1995
+ var import_node_path4 = require("path");
1816
1996
  var import_node_url = require("url");
1817
1997
  var import_proper = require("@noego/proper");
1818
- var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1998
+ var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
1819
1999
  var import_adapters = require("sqlstack/adapters");
1820
2000
 
1821
2001
  // src/storage/sqlite_ownership_lease.ts
1822
- var import_node_fs2 = require("fs");
1823
- var import_node_path2 = require("path");
2002
+ var import_node_fs3 = require("fs");
2003
+ var import_node_path3 = require("path");
1824
2004
  var SqliteOwnershipLease = class {
1825
2005
  constructor(options) {
1826
2006
  this.options = options;
1827
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(options.databasePath), { recursive: true });
1828
- const canonicalDirectory = (0, import_node_fs2.realpathSync)((0, import_node_path2.dirname)((0, import_node_path2.resolve)(options.databasePath)));
1829
- const databaseName = (0, import_node_path2.resolve)(options.databasePath).slice((0, import_node_path2.dirname)((0, import_node_path2.resolve)(options.databasePath)).length + 1);
1830
- this.canonicalDatabasePath = (0, import_node_path2.resolve)(canonicalDirectory, databaseName);
2007
+ (0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(options.databasePath), { recursive: true });
2008
+ const canonicalDirectory = (0, import_node_fs3.realpathSync)((0, import_node_path3.dirname)((0, import_node_path3.resolve)(options.databasePath)));
2009
+ const databaseName = (0, import_node_path3.resolve)(options.databasePath).slice((0, import_node_path3.dirname)((0, import_node_path3.resolve)(options.databasePath)).length + 1);
2010
+ this.canonicalDatabasePath = (0, import_node_path3.resolve)(canonicalDirectory, databaseName);
1831
2011
  this.leasePath = `${this.canonicalDatabasePath}.telemetry-owner.json`;
1832
2012
  }
1833
2013
  options;
@@ -1870,14 +2050,14 @@ var SqliteOwnershipLease = class {
1870
2050
  });
1871
2051
  }
1872
2052
  release() {
1873
- if (!this.held || !(0, import_node_fs2.existsSync)(this.leasePath)) {
2053
+ if (!this.held || !(0, import_node_fs3.existsSync)(this.leasePath)) {
1874
2054
  this.held = false;
1875
2055
  return;
1876
2056
  }
1877
2057
  const current = this.readRecord();
1878
2058
  if (current.ok && current.value.processToken === this.options.processToken) {
1879
2059
  try {
1880
- (0, import_node_fs2.rmSync)(this.leasePath, { force: true });
2060
+ (0, import_node_fs3.rmSync)(this.leasePath, { force: true });
1881
2061
  } catch {
1882
2062
  }
1883
2063
  }
@@ -1906,14 +2086,14 @@ var SqliteOwnershipLease = class {
1906
2086
  writeExclusive(record) {
1907
2087
  let descriptor = null;
1908
2088
  try {
1909
- descriptor = (0, import_node_fs2.openSync)(this.leasePath, "wx", 384);
1910
- (0, import_node_fs2.writeFileSync)(descriptor, JSON.stringify(record), { encoding: "utf8" });
1911
- (0, import_node_fs2.closeSync)(descriptor);
2089
+ descriptor = (0, import_node_fs3.openSync)(this.leasePath, "wx", 384);
2090
+ (0, import_node_fs3.writeFileSync)(descriptor, JSON.stringify(record), { encoding: "utf8" });
2091
+ (0, import_node_fs3.closeSync)(descriptor);
1912
2092
  return { ok: true, value: void 0 };
1913
2093
  } catch (cause) {
1914
2094
  if (descriptor !== null) {
1915
2095
  try {
1916
- (0, import_node_fs2.closeSync)(descriptor);
2096
+ (0, import_node_fs3.closeSync)(descriptor);
1917
2097
  } catch {
1918
2098
  }
1919
2099
  }
@@ -1923,12 +2103,12 @@ var SqliteOwnershipLease = class {
1923
2103
  writeReplace(record) {
1924
2104
  const temporaryPath = `${this.leasePath}.${this.options.processToken}.tmp`;
1925
2105
  try {
1926
- (0, import_node_fs2.writeFileSync)(temporaryPath, JSON.stringify(record), { encoding: "utf8", mode: 384 });
1927
- (0, import_node_fs2.renameSync)(temporaryPath, this.leasePath);
2106
+ (0, import_node_fs3.writeFileSync)(temporaryPath, JSON.stringify(record), { encoding: "utf8", mode: 384 });
2107
+ (0, import_node_fs3.renameSync)(temporaryPath, this.leasePath);
1928
2108
  return { ok: true, value: void 0 };
1929
2109
  } catch (cause) {
1930
2110
  try {
1931
- (0, import_node_fs2.rmSync)(temporaryPath, { force: true });
2111
+ (0, import_node_fs3.rmSync)(temporaryPath, { force: true });
1932
2112
  } catch {
1933
2113
  }
1934
2114
  return { ok: false, error: this.errorMessage(cause) };
@@ -1936,7 +2116,7 @@ var SqliteOwnershipLease = class {
1936
2116
  }
1937
2117
  readRecord() {
1938
2118
  try {
1939
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(this.leasePath, "utf8"));
2119
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(this.leasePath, "utf8"));
1940
2120
  if (!this.isLeaseRecord(parsed)) {
1941
2121
  return { ok: false, error: `Invalid telemetry ownership lease at ${this.leasePath}` };
1942
2122
  }
@@ -1960,8 +2140,8 @@ var SqliteOwnershipLease = class {
1960
2140
  }
1961
2141
  try {
1962
2142
  const stalePath = `${this.leasePath}.stale.${current.value.processToken}`;
1963
- (0, import_node_fs2.renameSync)(this.leasePath, stalePath);
1964
- (0, import_node_fs2.rmSync)(stalePath, { force: true });
2143
+ (0, import_node_fs3.renameSync)(this.leasePath, stalePath);
2144
+ (0, import_node_fs3.rmSync)(stalePath, { force: true });
1965
2145
  return { ok: true, value: void 0 };
1966
2146
  } catch (cause) {
1967
2147
  return { ok: false, error: this.errorMessage(cause) };
@@ -4356,7 +4536,7 @@ var ReadOnlySqlGuard = class {
4356
4536
 
4357
4537
  // src/cli/offline_sqlite_session.ts
4358
4538
  var import_meta = {};
4359
- var MIGRATIONS_DIRECTORY = typeof __dirname === "string" ? (0, import_node_path3.resolve)(__dirname, "../../migrations") : (0, import_node_url.fileURLToPath)(new URL("../../migrations/", import_meta.url));
4539
+ var MIGRATIONS_DIRECTORY = typeof __dirname === "string" ? (0, import_node_path4.resolve)(__dirname, "../../migrations") : (0, import_node_url.fileURLToPath)(new URL("../../migrations/", import_meta.url));
4360
4540
  var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4361
4541
  constructor(target, mode, definitions, now, sqlGuard) {
4362
4542
  this.target = target;
@@ -4498,6 +4678,87 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4498
4678
  this.verifySchema();
4499
4679
  return this.migrationStatus();
4500
4680
  }
4681
+ async compact() {
4682
+ this.requireMaintenanceMode();
4683
+ const connection = this.requireConnection();
4684
+ const maintenanceTable = this.requireTableMap().physical("maintenanceRuns");
4685
+ const runId = (0, import_node_crypto2.randomUUID)();
4686
+ const startedAtMs = this.now();
4687
+ connection.prepare(
4688
+ `INSERT INTO "${maintenanceTable}" (
4689
+ target_id, run_id, state, stage, started_at_ms,
4690
+ completed_at_ms, affected_rows, affected_pages, detail
4691
+ ) VALUES (?, ?, 'started', 'compaction', ?, NULL, 0, 0, ?)`
4692
+ ).run(this.target.targetId, runId, startedAtMs, "offline-cli; outcome=started");
4693
+ try {
4694
+ const before = this.compactionSnapshot();
4695
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4696
+ const checkpointed = this.compactionSnapshot();
4697
+ this.assertCompactionSpace(checkpointed);
4698
+ connection.pragma("auto_vacuum = INCREMENTAL");
4699
+ connection.exec("VACUUM");
4700
+ connection.pragma("wal_checkpoint(TRUNCATE)");
4701
+ const after = this.compactionSnapshot();
4702
+ if (after.autoVacuum !== 2) {
4703
+ throw new Error("Telemetry compaction did not enable auto_vacuum=INCREMENTAL");
4704
+ }
4705
+ const completedAtMs = this.now();
4706
+ const result = Object.freeze({
4707
+ targetId: this.target.targetId,
4708
+ autoVacuum: "incremental",
4709
+ pageSize: checkpointed.pageSize,
4710
+ pagesBefore: checkpointed.pageCount,
4711
+ pagesAfter: after.pageCount,
4712
+ freePagesBefore: checkpointed.freePages,
4713
+ freePagesAfter: after.freePages,
4714
+ reclaimedPages: Math.max(0, checkpointed.pageCount - after.pageCount),
4715
+ databaseBytesBefore: before.databaseBytes,
4716
+ databaseBytesAfter: after.databaseBytes,
4717
+ walBytesBefore: before.walBytes,
4718
+ walBytesAfter: after.walBytes,
4719
+ reclaimedBytes: Math.max(
4720
+ 0,
4721
+ before.databaseBytes + before.walBytes - after.databaseBytes - after.walBytes
4722
+ ),
4723
+ elapsedMs: completedAtMs - startedAtMs
4724
+ });
4725
+ connection.prepare(
4726
+ `UPDATE "${maintenanceTable}"
4727
+ SET state = 'completed', completed_at_ms = ?, affected_pages = ?, detail = ?
4728
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4729
+ ).run(
4730
+ completedAtMs,
4731
+ result.reclaimedPages,
4732
+ JSON.stringify({
4733
+ mode: "offline-cli",
4734
+ outcome: "completed",
4735
+ reclaimedBytes: result.reclaimedBytes,
4736
+ walBytesBefore: result.walBytesBefore,
4737
+ walBytesAfter: result.walBytesAfter,
4738
+ autoVacuum: result.autoVacuum
4739
+ }),
4740
+ this.target.targetId,
4741
+ runId
4742
+ );
4743
+ return result;
4744
+ } catch (cause) {
4745
+ const message = cause instanceof Error ? cause.message : String(cause);
4746
+ try {
4747
+ connection.prepare(
4748
+ `UPDATE "${maintenanceTable}"
4749
+ SET state = 'failed', completed_at_ms = ?, detail = ?
4750
+ WHERE target_id = ? AND run_id = ? AND stage = 'compaction'`
4751
+ ).run(
4752
+ this.now(),
4753
+ JSON.stringify({ mode: "offline-cli", outcome: "failed", error: message }),
4754
+ this.target.targetId,
4755
+ runId
4756
+ );
4757
+ } catch {
4758
+ }
4759
+ throw cause;
4760
+ }
4761
+ }
4501
4762
  async close() {
4502
4763
  try {
4503
4764
  if (this.connection?.open) {
@@ -4531,7 +4792,7 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4531
4792
  throw new Error(acquired.error);
4532
4793
  }
4533
4794
  }
4534
- const connection = new import_better_sqlite3.default(this.target.path, {
4795
+ const connection = new import_better_sqlite32.default(this.target.path, {
4535
4796
  readonly: this.mode === "query",
4536
4797
  fileMustExist: true
4537
4798
  });
@@ -4558,6 +4819,32 @@ var OfflineSqliteTelemetrySession = class _OfflineSqliteTelemetrySession {
4558
4819
  }
4559
4820
  }
4560
4821
  }
4822
+ compactionSnapshot() {
4823
+ const connection = this.requireConnection();
4824
+ return Object.freeze({
4825
+ pageSize: Number(connection.pragma("page_size", { simple: true })),
4826
+ pageCount: Number(connection.pragma("page_count", { simple: true })),
4827
+ freePages: Number(connection.pragma("freelist_count", { simple: true })),
4828
+ autoVacuum: Number(connection.pragma("auto_vacuum", { simple: true })),
4829
+ databaseBytes: this.fileBytes(this.target.path),
4830
+ walBytes: this.fileBytes(`${this.target.path}-wal`)
4831
+ });
4832
+ }
4833
+ assertCompactionSpace(snapshot) {
4834
+ const liveBytes = Math.max(0, snapshot.pageCount - snapshot.freePages) * snapshot.pageSize;
4835
+ const reserveBytes = Math.max(64 * 1024 * 1024, Math.ceil(liveBytes * 0.05));
4836
+ const filesystem = (0, import_node_fs4.statfsSync)((0, import_node_path4.dirname)(this.target.path));
4837
+ const availableBytes = Number(filesystem.bavail) * Number(filesystem.bsize);
4838
+ const requiredBytes = liveBytes + reserveBytes;
4839
+ if (availableBytes < requiredBytes) {
4840
+ throw new Error(
4841
+ `Telemetry compaction requires at least ${requiredBytes} free bytes; only ${availableBytes} are available`
4842
+ );
4843
+ }
4844
+ }
4845
+ fileBytes(path) {
4846
+ return (0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.statSync)(path).size : 0;
4847
+ }
4561
4848
  verifyManifest() {
4562
4849
  const rows = this.requireConnection().prepare(
4563
4850
  `SELECT target_id, format_version, mapping_json, mapping_checksum,
@@ -4742,7 +5029,8 @@ var ALLOWED_OPTIONS = Object.freeze({
4742
5029
  "metric",
4743
5030
  "where",
4744
5031
  "measurement",
4745
- "resolution"
5032
+ "resolution",
5033
+ "source"
4746
5034
  ]),
4747
5035
  "metrics-graph": /* @__PURE__ */ new Set([
4748
5036
  ...COMMON_QUERY_OPTIONS,
@@ -4750,6 +5038,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4750
5038
  "where",
4751
5039
  "measurement",
4752
5040
  "resolution",
5041
+ "source",
4753
5042
  "type",
4754
5043
  "output",
4755
5044
  "width",
@@ -4767,6 +5056,7 @@ var ALLOWED_OPTIONS = Object.freeze({
4767
5056
  "limit",
4768
5057
  "max-duration"
4769
5058
  ]),
5059
+ "sqlite-compact": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4770
5060
  "sqlite-migration-status": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"]),
4771
5061
  "sqlite-migration-up": /* @__PURE__ */ new Set(["config", "target", "storage-base", "format"])
4772
5062
  });
@@ -4776,6 +5066,7 @@ var COMMANDS = Object.freeze([
4776
5066
  { tokens: ["metrics", "query"], kind: "metrics-query" },
4777
5067
  { tokens: ["metrics", "graph"], kind: "metrics-graph" },
4778
5068
  { tokens: ["sqlite", "query"], kind: "sqlite-query" },
5069
+ { tokens: ["sqlite", "compact"], kind: "sqlite-compact" },
4779
5070
  { tokens: ["sqlite", "migration", "status"], kind: "sqlite-migration-status" },
4780
5071
  { tokens: ["sqlite", "migration", "up"], kind: "sqlite-migration-up" }
4781
5072
  ]);
@@ -5054,6 +5345,22 @@ var TelemetryCliFormatter = class {
5054
5345
  return `${lines.join("\n")}
5055
5346
  `;
5056
5347
  }
5348
+ compaction(result, format) {
5349
+ if (format === "json") {
5350
+ return this.json(result);
5351
+ }
5352
+ return [
5353
+ `Target: ${result.targetId}`,
5354
+ `Auto-vacuum: ${result.autoVacuum}`,
5355
+ `Pages: ${result.pagesBefore} -> ${result.pagesAfter} (${result.reclaimedPages} reclaimed)`,
5356
+ `Freelist: ${result.freePagesBefore} -> ${result.freePagesAfter}`,
5357
+ `Database bytes: ${result.databaseBytesBefore} -> ${result.databaseBytesAfter}`,
5358
+ `WAL bytes: ${result.walBytesBefore} -> ${result.walBytesAfter}`,
5359
+ `Filesystem bytes reclaimed: ${result.reclaimedBytes}`,
5360
+ `Elapsed ms: ${result.elapsedMs}`,
5361
+ ""
5362
+ ].join("\n");
5363
+ }
5057
5364
  migrations(result, format) {
5058
5365
  if (format === "json") {
5059
5366
  return this.json(result);
@@ -5130,7 +5437,7 @@ var TelemetryCli = class {
5130
5437
  this.configurationParser = options.configurationParser ?? new NodeTelemetryConfigurationParser();
5131
5438
  this.stdout = options.stdout ?? ((text) => process.stdout.write(text));
5132
5439
  this.stderr = options.stderr ?? ((text) => process.stderr.write(text));
5133
- this.writeOutputFile = options.writeOutputFile ?? ((path, content) => (0, import_node_fs3.writeFileSync)(path, content, "utf8"));
5440
+ this.writeOutputFile = options.writeOutputFile ?? ((path, content) => (0, import_node_fs5.writeFileSync)(path, content, "utf8"));
5134
5441
  this.now = options.now ?? (() => Date.now());
5135
5442
  this.createSession = options.createSession ?? ((configuration, target, mode) => OfflineSqliteTelemetrySession.create(target, mode, configuration.metrics, this.now));
5136
5443
  }
@@ -5161,6 +5468,10 @@ var TelemetryCli = class {
5161
5468
  storageBaseDirectory === null ? {} : { storageBaseDirectory }
5162
5469
  );
5163
5470
  const target = this.resolveTarget(configuration, this.optionalOption(command, "target"));
5471
+ if (this.metricSource(command, target) === "history") {
5472
+ await this.executeHistoryMetrics(command, target, configuration);
5473
+ return 0;
5474
+ }
5164
5475
  const mode = this.sessionMode(command);
5165
5476
  const session = await this.createSession(configuration, target, mode);
5166
5477
  this.activeSession = session;
@@ -5174,23 +5485,61 @@ var TelemetryCli = class {
5174
5485
  }
5175
5486
  }
5176
5487
  }
5488
+ /**
5489
+ * Which store answers a metric command. `history` is the projector's
5490
+ * pre-aggregated buckets (history.db, current whenever the app runs with
5491
+ * ingest-time extraction off); `legacy` is the collector's own rollups in
5492
+ * telemetry.db. `auto` prefers history.db when it exists beside the target —
5493
+ * since ingestion moved to the projector the legacy rollups stopped
5494
+ * advancing, so answering from them silently reports stale gaps.
5495
+ */
5496
+ metricSource(command, target) {
5497
+ if (command.kind !== "metrics-query" && command.kind !== "metrics-graph") return "legacy";
5498
+ const requested = this.optionalOption(command, "source") ?? "auto";
5499
+ if (requested === "history" || requested === "legacy") return requested;
5500
+ if (requested !== "auto") {
5501
+ throw new TelemetryCliUsageError("--source must be auto, history, or legacy");
5502
+ }
5503
+ return HistoryMetricReader.exists(historyDatabasePath(target)) ? "history" : "legacy";
5504
+ }
5505
+ async executeHistoryMetrics(command, target, configuration) {
5506
+ const databasePath = historyDatabasePath(target);
5507
+ if (!HistoryMetricReader.exists(databasePath)) {
5508
+ throw new TelemetryCliUsageError(`No projector history store at ${databasePath}`);
5509
+ }
5510
+ const reader = HistoryMetricReader.open(databasePath, configuration.metrics);
5511
+ try {
5512
+ const run = async (query) => reader.metrics(query, target.targetId);
5513
+ if (command.kind === "metrics-graph") {
5514
+ await this.metricGraph(command, target, run);
5515
+ } else {
5516
+ await this.metricQuery(command, target, run);
5517
+ }
5518
+ } finally {
5519
+ reader.close();
5520
+ }
5521
+ }
5177
5522
  async executeCommand(command, target, session) {
5178
5523
  if (command.kind === "timeline-query") {
5179
5524
  await this.timelineQuery(command, target, session);
5180
5525
  return;
5181
5526
  }
5182
5527
  if (command.kind === "metrics-query") {
5183
- await this.metricQuery(command, target, session);
5528
+ await this.metricQuery(command, target, (query) => session.metrics(query));
5184
5529
  return;
5185
5530
  }
5186
5531
  if (command.kind === "metrics-graph") {
5187
- await this.metricGraph(command, target, session);
5532
+ await this.metricGraph(command, target, (query) => session.metrics(query));
5188
5533
  return;
5189
5534
  }
5190
5535
  if (command.kind === "sqlite-query") {
5191
5536
  await this.sqliteQuery(command, target, session);
5192
5537
  return;
5193
5538
  }
5539
+ if (command.kind === "sqlite-compact") {
5540
+ await this.sqliteCompact(command, target, session);
5541
+ return;
5542
+ }
5194
5543
  if (command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up") {
5195
5544
  await this.sqliteMigration(command, target, session);
5196
5545
  return;
@@ -5220,8 +5569,8 @@ var TelemetryCli = class {
5220
5569
  const output = this.formatter.timeline(result.value, format, this.timezoneOption(command));
5221
5570
  this.writeBounded(output, target.query.maxResponseBytes);
5222
5571
  }
5223
- async metricQuery(command, target, session) {
5224
- const result = await session.metrics(this.metricQueryInput(command, target));
5572
+ async metricQuery(command, target, runMetrics) {
5573
+ const result = await runMetrics(this.metricQueryInput(command, target));
5225
5574
  if (!result.ok) {
5226
5575
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5227
5576
  }
@@ -5232,12 +5581,12 @@ var TelemetryCli = class {
5232
5581
  );
5233
5582
  this.writeBounded(output, target.query.maxResponseBytes);
5234
5583
  }
5235
- async metricGraph(command, target, session) {
5584
+ async metricGraph(command, target, runMetrics) {
5236
5585
  const measurements = this.options(command, "measurement");
5237
5586
  if (measurements.length !== 1) {
5238
5587
  throw new TelemetryCliUsageError("metrics graph requires exactly one --measurement");
5239
5588
  }
5240
- const result = await session.metrics(this.metricQueryInput(command, target));
5589
+ const result = await runMetrics(this.metricQueryInput(command, target));
5241
5590
  if (!result.ok) {
5242
5591
  throw new Error(`${result.error.kind}: ${result.error.message}`);
5243
5592
  }
@@ -5288,6 +5637,14 @@ var TelemetryCli = class {
5288
5637
  }
5289
5638
  this.writeBounded(this.formatter.migrations(result, format), target.query.maxResponseBytes);
5290
5639
  }
5640
+ async sqliteCompact(command, target, session) {
5641
+ const format = this.optionalOption(command, "format") ?? "table";
5642
+ if (format !== "table" && format !== "json") {
5643
+ throw new TelemetryCliUsageError("Compaction format must be table or json");
5644
+ }
5645
+ const result = await session.compact();
5646
+ this.writeBounded(this.formatter.compaction(result, format), target.query.maxResponseBytes);
5647
+ }
5291
5648
  metricQueryInput(command, target) {
5292
5649
  const measurements = this.options(command, "measurement");
5293
5650
  if (measurements.length === 0) {
@@ -5322,7 +5679,7 @@ var TelemetryCli = class {
5322
5679
  return target;
5323
5680
  }
5324
5681
  sessionMode(command) {
5325
- return command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5682
+ return command.kind === "sqlite-compact" || command.kind === "sqlite-migration-status" || command.kind === "sqlite-migration-up" ? "maintenance" : "query";
5326
5683
  }
5327
5684
  structuredFormat(command) {
5328
5685
  const value = this.optionalOption(command, "format") ?? "table";
@@ -5489,9 +5846,10 @@ var TelemetryCli = class {
5489
5846
  help() {
5490
5847
  return `Usage:
5491
5848
  telemetry timeline query --config FILE --from TIME --to TIME [filters] [--format table|json|jsonl|csv]
5492
- telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE]
5493
- 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]
5849
+ telemetry metrics query --config FILE --metric NAME --measurement NAME --resolution DURATION|auto --from TIME --to TIME [--where KEY=VALUE] [--source auto|history|legacy]
5850
+ 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]
5494
5851
  telemetry sqlite query --config FILE --sql SQL [--param NAME=VALUE] [--format table|json|jsonl|csv]
5852
+ telemetry sqlite compact --config FILE [--format table|json]
5495
5853
  telemetry sqlite migration status --config FILE [--format table|json]
5496
5854
  telemetry sqlite migration up --config FILE [--format table|json]
5497
5855