adonisjs-server-stats 1.17.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +11 -6
  2. package/dist/core/core/define-config-helpers.d.ts +1 -0
  3. package/dist/core/core/types-diagnostics.d.ts +2 -0
  4. package/dist/core/debug/types.d.ts +2 -0
  5. package/dist/core/types.d.ts +31 -3
  6. package/dist/react/{CacheSection-C-X2Tnca.js → CacheSection-B-lSIN8Q.js} +1 -1
  7. package/dist/react/{CacheTab-D55mfmeE.js → CacheTab-BN0QgPXz.js} +1 -1
  8. package/dist/react/{ConfigSection-C-HGnZfd.js → ConfigSection-u_TSfb9f.js} +1 -1
  9. package/dist/react/{ConfigTab-CNnsOXBh.js → ConfigTab-au26s-ON.js} +1 -1
  10. package/dist/react/{CustomPaneTab-D-267UVd.js → CustomPaneTab-qvMnh8ss.js} +1 -1
  11. package/dist/react/{EmailsSection-Bqw4DI4r.js → EmailsSection-HfEMeRex.js} +1 -1
  12. package/dist/react/{EmailsTab-CkHOhj2F.js → EmailsTab-BWwq-CxB.js} +1 -1
  13. package/dist/react/{EventsSection-CMeyjzVC.js → EventsSection-CA9b15dJ.js} +1 -1
  14. package/dist/react/{EventsTab-DgS2v1HX.js → EventsTab-CgSdHbKU.js} +1 -1
  15. package/dist/react/{InternalsContent-C9lIA92C.js → InternalsContent-CI7HvBtH.js} +51 -47
  16. package/dist/react/{InternalsSection-lOv_UcSV.js → InternalsSection-IEeWXJqP.js} +1 -1
  17. package/dist/react/{InternalsTab-BygoSorC.js → InternalsTab-CoVLHQbF.js} +1 -1
  18. package/dist/react/{JobsSection-oWH6pbrG.js → JobsSection-DgsS_VwU.js} +1 -1
  19. package/dist/react/{JobsTab-zdxX_as-.js → JobsTab-CzwLSRUC.js} +1 -1
  20. package/dist/react/{LogEntryRow-Cn8Ee_X0.js → LogEntryRow-VCCzwD1H.js} +1 -1
  21. package/dist/react/{LogsSection-XmiI2MMs.js → LogsSection-BujNn_cO.js} +2 -2
  22. package/dist/react/{LogsTab-ixusY4v7.js → LogsTab-BtD1h7DM.js} +2 -2
  23. package/dist/react/{OverviewSection-CvhObTLP.js → OverviewSection-B28em0gQ.js} +1 -1
  24. package/dist/react/{QueriesSection-BvledwBI.js → QueriesSection-B0tFn_Dc.js} +1 -1
  25. package/dist/react/{QueriesTab-smQQJvJa.js → QueriesTab-DJY7aavR.js} +1 -1
  26. package/dist/react/{RequestsSection-DKuYpViv.js → RequestsSection-DPmYM7tq.js} +2 -2
  27. package/dist/react/{RoutesSection-5RNVurQ6.js → RoutesSection-Ck-RcFVS.js} +1 -1
  28. package/dist/react/{RoutesTab-CpXNO-Rx.js → RoutesTab-Ch3PDLzo.js} +1 -1
  29. package/dist/react/{SplitPaneWrapper-DQvJnsuk.js → SplitPaneWrapper-BZ6VrBr-.js} +1 -1
  30. package/dist/react/{TimelineTab-LHQHHKkO.js → TimelineTab-_byxh2mJ.js} +2 -2
  31. package/dist/react/{index-LlFvooUN.js → index-De7nsWBN.js} +2 -2
  32. package/dist/react/index.js +1 -1
  33. package/dist/src/dashboard/dashboard_store.js +4 -12
  34. package/dist/src/dashboard/knex_factory.js +8 -0
  35. package/dist/src/dashboard/migrator.d.ts +7 -2
  36. package/dist/src/dashboard/migrator.js +140 -6
  37. package/dist/src/dashboard/migrator_tables.js +4 -0
  38. package/dist/src/dashboard/storage_stats.d.ts +5 -0
  39. package/dist/src/dashboard/storage_stats.js +15 -1
  40. package/dist/src/debug/types.d.ts +2 -0
  41. package/dist/src/define_config.js +2 -0
  42. package/dist/src/edge/client/dashboard.js +2 -2
  43. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  44. package/dist/src/edge/client-vue/dashboard.js +3 -3
  45. package/dist/src/edge/client-vue/debug-panel-deferred.js +2 -2
  46. package/dist/src/log_stream/log_stream_service.d.ts +5 -0
  47. package/dist/src/log_stream/log_stream_service.js +35 -10
  48. package/dist/src/provider/dashboard_setup.js +14 -0
  49. package/dist/src/provider/diagnostics.d.ts +2 -0
  50. package/dist/src/provider/diagnostics.js +1 -0
  51. package/dist/src/provider/server_stats_provider.d.ts +1 -0
  52. package/dist/src/stubs/config.stub +4 -0
  53. package/dist/src/types.d.ts +31 -3
  54. package/dist/vue/{CacheSection-Bm0goqX6.js → CacheSection-z2TQRez9.js} +1 -1
  55. package/dist/vue/{ConfigSection-BrYslIMN.js → ConfigSection-CFEaig69.js} +1 -1
  56. package/dist/vue/{EmailsSection-B8N5vaWA.js → EmailsSection-CYohBo62.js} +1 -1
  57. package/dist/vue/{EventsSection-D2Y_xt0m.js → EventsSection-B_TuVrGj.js} +1 -1
  58. package/dist/vue/{InternalsSection-jhTCuK3j.js → InternalsSection-B1P2kNT-.js} +92 -84
  59. package/dist/vue/{InternalsTab-Z3c82glB.js → InternalsTab-CRI-IFTo.js} +155 -147
  60. package/dist/vue/{JobsSection-cEKqELLC.js → JobsSection-AjkEzyiv.js} +1 -1
  61. package/dist/vue/{LogsSection-C3H2c0S3.js → LogsSection-DZD0CCDi.js} +1 -1
  62. package/dist/vue/{OverviewSection-I2swuHDa.js → OverviewSection-BEc4Gktf.js} +1 -1
  63. package/dist/vue/{QueriesSection-CF1IE3SN.js → QueriesSection-BMImGXTn.js} +1 -1
  64. package/dist/vue/{RequestsSection-CuwgXzmU.js → RequestsSection-DOI2MUFX.js} +1 -1
  65. package/dist/vue/{RoutesSection-BhJCF7Rn.js → RoutesSection-C1sxt-AW.js} +1 -1
  66. package/dist/vue/{index-DUsImuIN.js → index-BfPAAjL5.js} +2 -2
  67. package/dist/vue/index.js +1 -1
  68. package/package.json +1 -1
@@ -3,6 +3,7 @@ export declare function parseAndEnrich(line: string): Record<string, unknown> |
3
3
  export declare class LogStreamService {
4
4
  private recentEntries;
5
5
  private static readonly MAX_RECENT_ENTRIES;
6
+ private static readonly MAX_POLL_BYTES;
6
7
  private lastSize;
7
8
  private intervalId;
8
9
  private logPath;
@@ -16,8 +17,12 @@ export declare class LogStreamService {
16
17
  * in real-time without file polling.
17
18
  */
18
19
  ingest(entry: Record<string, unknown>): void;
20
+ /** Record a timestamp, capping the array to prevent unbounded growth under high log volume. */
21
+ private pushRecent;
19
22
  getLogStats(): LogStats;
20
23
  start(): Promise<void>;
21
24
  stop(): void;
25
+ /** Reset the once-per-streak failure flag after a fully successful poll. */
26
+ private markPollHealthy;
22
27
  private pollNewEntries;
23
28
  }
@@ -27,6 +27,7 @@ export function parseAndEnrich(line) {
27
27
  export class LogStreamService {
28
28
  recentEntries = [];
29
29
  static MAX_RECENT_ENTRIES = 10_000;
30
+ static MAX_POLL_BYTES = 4 * 1024 * 1024;
30
31
  lastSize = 0;
31
32
  intervalId = null;
32
33
  logPath;
@@ -44,12 +45,15 @@ export class LogStreamService {
44
45
  */
45
46
  ingest(entry) {
46
47
  const level = typeof entry.level === 'number' ? entry.level : 30;
47
- // Cap the array to prevent unbounded growth under high log volume
48
+ this.pushRecent(Date.now(), level);
49
+ this.onEntry?.(entry);
50
+ }
51
+ /** Record a timestamp, capping the array to prevent unbounded growth under high log volume. */
52
+ pushRecent(time, level) {
48
53
  if (this.recentEntries.length >= LogStreamService.MAX_RECENT_ENTRIES) {
49
54
  this.recentEntries.splice(0, Math.floor(LogStreamService.MAX_RECENT_ENTRIES / 4));
50
55
  }
51
- this.recentEntries.push({ time: Date.now(), level });
52
- this.onEntry?.(entry);
56
+ this.recentEntries.push({ time, level });
53
57
  }
54
58
  getLogStats() {
55
59
  const now = Date.now();
@@ -102,36 +106,57 @@ export class LogStreamService {
102
106
  this.intervalId = null;
103
107
  }
104
108
  }
109
+ /** Reset the once-per-streak failure flag after a fully successful poll. */
110
+ markPollHealthy() {
111
+ if (this.warnedPollFailure) {
112
+ this.warnedPollFailure = false;
113
+ log.info('log stream: log file is readable again — resuming');
114
+ }
115
+ }
105
116
  async pollNewEntries() {
106
117
  if (!this.logPath)
107
118
  return;
108
119
  try {
109
- this.warnedPollFailure = false;
110
120
  const stats = await stat(this.logPath);
111
121
  // File was truncated/rotated — reset
112
122
  if (stats.size < this.lastSize) {
113
123
  this.lastSize = 0;
114
124
  }
115
- if (stats.size <= this.lastSize)
125
+ if (stats.size <= this.lastSize) {
126
+ this.markPollHealthy();
116
127
  return;
117
- const newBytes = stats.size - this.lastSize;
128
+ }
129
+ // Cap each read so a rotation reset or burst can never allocate the
130
+ // whole backlog in one buffer; skip ahead and read only the tail.
131
+ // A partial first line after skipping fails JSON.parse and is dropped.
132
+ const readFrom = Math.max(this.lastSize, stats.size - LogStreamService.MAX_POLL_BYTES);
133
+ const newBytes = stats.size - readFrom;
118
134
  const buffer = Buffer.alloc(newBytes);
119
135
  const fd = await open(this.logPath, 'r');
120
- await fd.read(buffer, 0, newBytes, this.lastSize).finally(() => fd.close());
136
+ await fd.read(buffer, 0, newBytes, readFrom).finally(() => fd.close());
121
137
  this.lastSize = stats.size;
138
+ this.markPollHealthy();
122
139
  for (const line of buffer.toString('utf-8').trim().split('\n')) {
123
140
  const entry = parseAndEnrich(line);
124
141
  if (entry) {
125
142
  const level = typeof entry.level === 'number' ? entry.level : 30;
126
143
  const time = typeof entry.time === 'number' ? entry.time : Date.now();
127
- this.recentEntries.push({ time, level });
144
+ this.pushRecent(time, level);
128
145
  this.onEntry?.(entry);
129
146
  }
130
147
  }
131
148
  }
132
149
  catch (err) {
133
- if (!this.warnedPollFailure) {
134
- this.warnedPollFailure = true;
150
+ if (this.warnedPollFailure)
151
+ return;
152
+ this.warnedPollFailure = true;
153
+ // A missing file is a normal state for the fallback poller — the file
154
+ // appears once the app writes its first log line. Anything else
155
+ // (permissions, a directory in the way) deserves a real warning.
156
+ if (err?.code === 'ENOENT') {
157
+ log.info('log stream: log file not found (will keep watching) — ' + this.logPath);
158
+ }
159
+ else {
135
160
  log.warn('log stream: cannot read log file — ' + err?.message);
136
161
  }
137
162
  }
@@ -62,6 +62,7 @@ const TOOLBAR_DEFAULTS = {
62
62
  dashboard: false,
63
63
  dashboardPath: '/__stats',
64
64
  retentionDays: 7,
65
+ maxDbSizeMb: 500,
65
66
  dbPath: '.adonisjs/server-stats/dashboard.sqlite3',
66
67
  debugEndpoint: '/admin/api/debug',
67
68
  };
@@ -110,6 +111,18 @@ function resolveRetentionDays(ctx, explicit) {
110
111
  return explicit;
111
112
  return ctx?.inProduction ? PRODUCTION_RETENTION_DAYS : TOOLBAR_DEFAULTS.retentionDays;
112
113
  }
114
+ /**
115
+ * Resolve the database size cap. Order: `production.maxDbSizeMb` when in
116
+ * production, then whatever `dashboard` set, then the 500 MB default.
117
+ */
118
+ function resolveMaxDbSizeMb(ctx, explicit) {
119
+ const fromProduction = ctx?.inProduction ? ctx.production?.maxDbSizeMb : undefined;
120
+ if (fromProduction !== undefined)
121
+ return fromProduction;
122
+ if (explicit !== undefined)
123
+ return explicit;
124
+ return TOOLBAR_DEFAULTS.maxDbSizeMb;
125
+ }
113
126
  /**
114
127
  * Resolve a partial DevToolbarConfig by filling in all defaults.
115
128
  *
@@ -125,6 +138,7 @@ export function resolveToolbarConfig(partial, ctx) {
125
138
  return {
126
139
  ...merged,
127
140
  retentionDays: resolveRetentionDays(ctx, partial.retentionDays),
141
+ maxDbSizeMb: resolveMaxDbSizeMb(ctx, partial.maxDbSizeMb),
128
142
  capture: resolveCapture(ctx, merged.tracing),
129
143
  };
130
144
  }
@@ -47,6 +47,7 @@ interface DiagnosticsToolbar {
47
47
  dashboard?: boolean;
48
48
  dashboardPath?: string;
49
49
  retentionDays?: number;
50
+ maxDbSizeMb?: number;
50
51
  dbPath?: string;
51
52
  persistDebugData?: boolean | string;
52
53
  debugEndpoint?: string;
@@ -118,6 +119,7 @@ export declare function buildDiagnostics(input: DiagnosticsInput): {
118
119
  tracing: boolean;
119
120
  dashboard: boolean;
120
121
  retentionDays: number;
122
+ maxDbSizeMb: number;
121
123
  persistDebugData: string | boolean;
122
124
  renderer: string;
123
125
  excludeFromTracing: string[];
@@ -85,6 +85,7 @@ function pickToolbarFeatures(tb) {
85
85
  tracing: tb.tracing ?? true,
86
86
  dashboard: tb.dashboard ?? false,
87
87
  retentionDays: tb.retentionDays ?? 7,
88
+ maxDbSizeMb: tb.maxDbSizeMb ?? 500,
88
89
  persistDebugData: tb.persistDebugData ?? false,
89
90
  renderer: tb.renderer ?? 'preact',
90
91
  excludeFromTracing: tb.excludeFromTracing ?? [],
@@ -132,6 +132,7 @@ export default class ServerStatsProvider {
132
132
  tracing: boolean;
133
133
  dashboard: boolean;
134
134
  retentionDays: number;
135
+ maxDbSizeMb: number;
135
136
  persistDebugData: string | boolean;
136
137
  renderer: string;
137
138
  excludeFromTracing: string[];
@@ -17,6 +17,10 @@ export default defineConfig({
17
17
  // Enable the full-page dashboard at /__stats
18
18
  // dashboard: true,
19
19
 
20
+ // History window and disk budget for persisted stats. When the database
21
+ // outgrows the size cap the oldest records are pruned regardless of age.
22
+ // dashboard: { retentionDays: 7, maxDbSizeMb: 500 },
23
+
20
24
  // Log detailed initialization steps (SQLite, migrations, routes, etc.)
21
25
  // verbose: true,
22
26
 
@@ -263,6 +263,15 @@ export interface DevToolbarOptions {
263
263
  * @default 7
264
264
  */
265
265
  retentionDays?: number;
266
+ /**
267
+ * Size cap in MB for the SQLite database's live data. When exceeded,
268
+ * the hourly cleanup deletes the oldest records — regardless of age —
269
+ * until usage drops below the cap, effectively shortening the retention
270
+ * window when write volume outruns `retentionDays`. Set to `0` to
271
+ * disable the cap.
272
+ * @default 500
273
+ */
274
+ maxDbSizeMb?: number;
266
275
  /**
267
276
  * Path to the SQLite database file for historical persistence.
268
277
  * Relative to app root.
@@ -434,6 +443,17 @@ export interface DashboardConfig {
434
443
  * @default 7
435
444
  */
436
445
  retentionDays?: number;
446
+ /**
447
+ * Size cap in MB for the SQLite database's live data.
448
+ *
449
+ * When exceeded, the cleanup deletes the oldest records — regardless
450
+ * of age — until usage drops below the cap. This bounds disk usage
451
+ * when write volume (tracing, verbose query logging) outruns what
452
+ * `retentionDays` alone would keep small. Set to `0` to disable.
453
+ *
454
+ * @default 500
455
+ */
456
+ maxDbSizeMb?: number;
437
457
  }
438
458
  /**
439
459
  * Access-control callback signature.
@@ -515,13 +535,21 @@ export interface ProductionConfig {
515
535
  capture?: CaptureConfig;
516
536
  /**
517
537
  * How many days of history to keep in SQLite, overriding the usual default
518
- * of 7. Retention deletes rows hourly but never runs `VACUUM`, so the
519
- * database file reuses pages rather than shrinking — watch actual size via
520
- * the dashboard's storage panel.
538
+ * of 7. Retention deletes rows hourly and returns freed pages to the OS
539
+ * incrementally — watch actual size via the dashboard's storage panel.
521
540
  *
522
541
  * @default 3
523
542
  */
524
543
  retentionDays?: number;
544
+ /**
545
+ * Size cap in MB for the SQLite database's live data in production,
546
+ * overriding the usual default. When exceeded, the hourly cleanup deletes
547
+ * the oldest records — regardless of age — until usage drops below the
548
+ * cap. Set to `0` to disable.
549
+ *
550
+ * @default 500
551
+ */
552
+ maxDbSizeMb?: number;
525
553
  }
526
554
  /**
527
555
  * Advanced options that most users never need to touch.
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as L, inject as y, ref as h, onMounted as x, computed as w, onUnmounted as F, openBlock as l, createElementBlock as n, createElementVNode as e, toDisplayString as c, createCommentVNode as z, createVNode as N, unref as p, Fragment as U, renderList as B, withModifiers as M, createBlock as H } from "vue";
2
- import { u as I } from "./index-DUsImuIN.js";
2
+ import { u as I } from "./index-BfPAAjL5.js";
3
3
  import { u as j } from "./useResizableTable-BoivAevK.js";
4
4
  import { DashboardApi as q, formatCacheSize as P, formatTtl as G } from "adonisjs-server-stats/core";
5
5
  import { u as J } from "./useApiClient-BQQ9CF-q.js";
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as nt, inject as L, ref as h, computed as k, openBlock as r, createElementBlock as c, createElementVNode as n, normalizeClass as a, createCommentVNode as g, Fragment as x, toDisplayString as u, unref as d, renderList as E, normalizeStyle as j } from "vue";
2
- import { u as lt } from "./index-DUsImuIN.js";
2
+ import { u as lt } from "./index-BfPAAjL5.js";
3
3
  import { isRedactedValue as p, flattenConfig as O, countLeaves as ot, collectTopLevelObjectKeys as st, copyWithFeedback as at, formatFlatValue as rt } from "adonisjs-server-stats/core";
4
4
  const ct = { style: { position: "relative", flex: 1 } }, ut = ["value"], it = ["onClick"], pt = { key: 0 }, dt = ["onClick"], vt = { key: 0 }, ft = { style: { padding: "4px 16px", fontSize: "10px", color: "var(--ss-muted)" } }, yt = ["onClick"], gt = ["title"], bt = ["title"], ht = ["onClick"], $t = {
5
5
  key: 1,
@@ -1,6 +1,6 @@
1
1
  import { defineComponent as B, inject as f, ref as w, computed as g, onUnmounted as D, openBlock as s, createElementBlock as n, createElementVNode as t, Fragment as m, createTextVNode as d, toDisplayString as i, createCommentVNode as _, normalizeClass as k, createVNode as R, unref as r, renderList as U, createBlock as z } from "vue";
2
2
  import { formatTime as L, timeAgo as M } from "adonisjs-server-stats/core";
3
- import { u as q } from "./index-DUsImuIN.js";
3
+ import { u as q } from "./index-BfPAAjL5.js";
4
4
  import { u as H } from "./useResizableTable-BoivAevK.js";
5
5
  import { _ as I } from "./FilterBar.vue_vue_type_script_setup_true_lang-ClJ37hhT.js";
6
6
  import { _ as K } from "./PaginationControls.vue_vue_type_script_setup_true_lang-CuN7g_8Z.js";
@@ -1,6 +1,6 @@
1
1
  import { defineComponent as E, inject as i, ref as p, computed as P, openBlock as s, createElementBlock as l, createVNode as v, unref as a, Fragment as h, createElementVNode as e, renderList as T, toDisplayString as c, createBlock as C, createCommentVNode as D } from "vue";
2
2
  import { formatTime as S, timeAgo as V } from "adonisjs-server-stats/core";
3
- import { u as $ } from "./index-DUsImuIN.js";
3
+ import { u as $ } from "./index-BfPAAjL5.js";
4
4
  import { u as A } from "./useResizableTable-BoivAevK.js";
5
5
  import { _ as B } from "./FilterBar.vue_vue_type_script_setup_true_lang-ClJ37hhT.js";
6
6
  import { _ as R } from "./PaginationControls.vue_vue_type_script_setup_true_lang-CuN7g_8Z.js";