@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
@@ -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: bracket === "*" ? [] : [bracket],
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 { resolve as resolve3 } from "path";
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 BetterSqlite3 from "better-sqlite3";
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 dirname2, resolve as resolve2 } from "path";
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(dirname2(options.databasePath), { recursive: true });
1814
- const canonicalDirectory = realpathSync(dirname2(resolve2(options.databasePath)));
1815
- const databaseName = resolve2(options.databasePath).slice(dirname2(resolve2(options.databasePath)).length + 1);
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 || !existsSync(this.leasePath)) {
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 BetterSqlite3(this.target.path, {
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, session) {
5209
- const result = await session.metrics(this.metricQueryInput(command, target));
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, session) {
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 session.metrics(this.metricQueryInput(command, target));
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