adonisjs-server-stats 1.10.0 → 1.10.3
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 +23 -14
- package/dist/core/config-utils.d.ts +8 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/dashboard-data-controller.d.ts +16 -0
- package/dist/core/dashboard-data-helpers.d.ts +12 -0
- package/dist/core/debug-data-controller.d.ts +4 -0
- package/dist/core/define-config-helpers.d.ts +25 -0
- package/dist/core/feature-detect-helpers.d.ts +36 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/index.js +594 -509
- package/dist/core/log-utils-helpers.d.ts +13 -0
- package/dist/core/metrics.d.ts +3 -28
- package/dist/core/pagination.d.ts +0 -9
- package/dist/core/server-stats-controller.d.ts +6 -0
- package/dist/core/transmit-helpers.d.ts +7 -0
- package/dist/core/types-dashboard.d.ts +178 -0
- package/dist/core/types-diagnostics.d.ts +85 -0
- package/dist/core/types.d.ts +10 -442
- package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
- package/dist/react/CacheTab-2cw_rMzj.js +117 -0
- package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
- package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
- package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
- package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
- package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
- package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
- package/dist/react/EventsTab-CfVr7AiM.js +57 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
- package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
- package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
- package/dist/react/LogsTab-D8unMV5P.js +108 -0
- package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
- package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
- package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
- package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
- package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
- package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
- package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
- package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
- package/dist/react/index-CsImORX6.js +1121 -0
- package/dist/react/index.js +1 -1
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/app_collector.d.ts +0 -8
- package/dist/src/collectors/app_collector.js +45 -52
- package/dist/src/collectors/auto_detect.d.ts +0 -23
- package/dist/src/collectors/auto_detect.js +33 -55
- package/dist/src/collectors/db_pool_collector.d.ts +14 -16
- package/dist/src/collectors/db_pool_collector.js +72 -57
- package/dist/src/collectors/log_collector.d.ts +0 -47
- package/dist/src/collectors/log_collector.js +36 -65
- package/dist/src/collectors/queue_collector.d.ts +0 -20
- package/dist/src/collectors/queue_collector.js +60 -76
- package/dist/src/collectors/redis_collector.d.ts +10 -10
- package/dist/src/collectors/redis_collector.js +69 -66
- package/dist/src/config/deprecation_migration.d.ts +7 -0
- package/dist/src/config/deprecation_migration.js +201 -0
- package/dist/src/controller/debug_controller.d.ts +1 -1
- package/dist/src/controller/debug_controller.js +87 -81
- package/dist/src/dashboard/cache_handlers.d.ts +14 -0
- package/dist/src/dashboard/cache_handlers.js +52 -0
- package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
- package/dist/src/dashboard/chart_aggregator.js +68 -50
- package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
- package/dist/src/dashboard/coalesce_cache.js +47 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
- package/dist/src/dashboard/dashboard_controller.js +51 -544
- package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
- package/dist/src/dashboard/dashboard_page_assets.js +51 -0
- package/dist/src/dashboard/dashboard_store.d.ts +19 -218
- package/dist/src/dashboard/dashboard_store.js +115 -1116
- package/dist/src/dashboard/dashboard_types.d.ts +83 -0
- package/dist/src/dashboard/dashboard_types.js +4 -0
- package/dist/src/dashboard/detail_queries.d.ts +19 -0
- package/dist/src/dashboard/detail_queries.js +98 -0
- package/dist/src/dashboard/email_event_builder.d.ts +8 -0
- package/dist/src/dashboard/email_event_builder.js +65 -0
- package/dist/src/dashboard/explain_query.d.ts +8 -0
- package/dist/src/dashboard/explain_query.js +22 -0
- package/dist/src/dashboard/filter_handlers.d.ts +23 -0
- package/dist/src/dashboard/filter_handlers.js +56 -0
- package/dist/src/dashboard/filtered_queries.d.ts +15 -0
- package/dist/src/dashboard/filtered_queries.js +155 -0
- package/dist/src/dashboard/flush_manager.d.ts +25 -0
- package/dist/src/dashboard/flush_manager.js +107 -0
- package/dist/src/dashboard/format_helpers.d.ts +126 -0
- package/dist/src/dashboard/format_helpers.js +140 -0
- package/dist/src/dashboard/inspector_manager.d.ts +36 -0
- package/dist/src/dashboard/inspector_manager.js +102 -0
- package/dist/src/dashboard/integrations/config_inspector.js +11 -13
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
- package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
- package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
- package/dist/src/dashboard/jobs_handlers.js +61 -0
- package/dist/src/dashboard/knex_factory.d.ts +18 -0
- package/dist/src/dashboard/knex_factory.js +91 -0
- package/dist/src/dashboard/migrator.js +30 -159
- package/dist/src/dashboard/migrator_tables.d.ts +19 -0
- package/dist/src/dashboard/migrator_tables.js +153 -0
- package/dist/src/dashboard/overview_queries.d.ts +66 -0
- package/dist/src/dashboard/overview_queries.js +155 -0
- package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
- package/dist/src/dashboard/overview_query_runners.js +84 -0
- package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
- package/dist/src/dashboard/overview_store_queries.js +69 -0
- package/dist/src/dashboard/paginate_helper.d.ts +12 -0
- package/dist/src/dashboard/paginate_helper.js +33 -0
- package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
- package/dist/src/dashboard/query_explain_handler.js +80 -0
- package/dist/src/dashboard/read_queries.d.ts +32 -0
- package/dist/src/dashboard/read_queries.js +107 -0
- package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
- package/dist/src/dashboard/saved_filter_queries.js +24 -0
- package/dist/src/dashboard/storage_stats.d.ts +41 -0
- package/dist/src/dashboard/storage_stats.js +81 -0
- package/dist/src/dashboard/write_queue.d.ts +106 -0
- package/dist/src/dashboard/write_queue.js +225 -0
- package/dist/src/data/data_access.d.ts +2 -39
- package/dist/src/data/data_access.js +17 -193
- package/dist/src/data/data_access_helpers.d.ts +130 -0
- package/dist/src/data/data_access_helpers.js +212 -0
- package/dist/src/debug/debug_store.js +37 -32
- package/dist/src/debug/email_collector.d.ts +1 -10
- package/dist/src/debug/email_collector.js +78 -81
- package/dist/src/debug/event_collector.d.ts +0 -9
- package/dist/src/debug/event_collector.js +79 -62
- package/dist/src/debug/query_collector.js +23 -19
- package/dist/src/debug/route_inspector.d.ts +1 -5
- package/dist/src/debug/route_inspector.js +50 -51
- package/dist/src/debug/trace_collector.d.ts +9 -1
- package/dist/src/debug/trace_collector.js +21 -15
- package/dist/src/debug/types.d.ts +1 -1
- package/dist/src/define_config.d.ts +0 -65
- package/dist/src/define_config.js +93 -333
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/edge/plugin.d.ts +0 -16
- package/dist/src/edge/plugin.js +57 -64
- package/dist/src/engine/request_metrics.d.ts +1 -0
- package/dist/src/engine/request_metrics.js +32 -42
- package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -93
- package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
- package/dist/src/provider/auth_middleware_detector.js +97 -0
- package/dist/src/provider/boot_helpers.d.ts +20 -0
- package/dist/src/provider/boot_helpers.js +91 -0
- package/dist/src/provider/boot_initializer.d.ts +28 -0
- package/dist/src/provider/boot_initializer.js +35 -0
- package/dist/src/provider/dashboard_init.d.ts +30 -0
- package/dist/src/provider/dashboard_init.js +138 -0
- package/dist/src/provider/dashboard_setup.d.ts +25 -0
- package/dist/src/provider/dashboard_setup.js +78 -0
- package/dist/src/provider/diagnostics.d.ts +134 -0
- package/dist/src/provider/diagnostics.js +127 -0
- package/dist/src/provider/email_bridge.d.ts +43 -0
- package/dist/src/provider/email_bridge.js +80 -0
- package/dist/src/provider/email_helpers.d.ts +13 -0
- package/dist/src/provider/email_helpers.js +68 -0
- package/dist/src/provider/pino_hook.d.ts +17 -0
- package/dist/src/provider/pino_hook.js +35 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
- package/dist/src/provider/provider_helpers_extra.js +177 -0
- package/dist/src/provider/server_stats_provider.d.ts +39 -85
- package/dist/src/provider/server_stats_provider.js +132 -951
- package/dist/src/provider/shutdown_helpers.d.ts +43 -0
- package/dist/src/provider/shutdown_helpers.js +70 -0
- package/dist/src/provider/toolbar_setup.d.ts +57 -0
- package/dist/src/provider/toolbar_setup.js +141 -0
- package/dist/src/routes/dashboard_routes.d.ts +14 -0
- package/dist/src/routes/dashboard_routes.js +197 -0
- package/dist/src/routes/debug_routes.d.ts +14 -0
- package/dist/src/routes/debug_routes.js +101 -0
- package/dist/src/routes/register_routes.d.ts +0 -78
- package/dist/src/routes/register_routes.js +22 -352
- package/dist/src/routes/stats_routes.d.ts +5 -0
- package/dist/src/routes/stats_routes.js +14 -0
- package/dist/src/styles/components.css +96 -0
- package/dist/src/styles/dashboard.css +8 -90
- package/dist/src/styles/debug-panel.css +1 -31
- package/dist/src/types.d.ts +305 -14
- package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
- package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
- package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
- package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
- package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
- package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
- package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
- package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
- package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-C8MxnS7Q.js +1232 -0
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/CacheTab-CA8LB1J5.js +0 -123
- package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
- package/dist/react/EmailsTab-BDhEiomM.js +0 -153
- package/dist/react/EventsTab-CMfY98Rl.js +0 -63
- package/dist/react/LogsTab-CicucmVk.js +0 -103
- package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
- package/dist/react/index-Cflz9Ebj.js +0 -1069
- package/dist/vue/index-Dtgysd26.js +0 -1229
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure data-mapping helpers for dashboard overview and chart queries.
|
|
3
|
+
*
|
|
4
|
+
* These functions transform raw database rows into the shapes
|
|
5
|
+
* expected by the dashboard API. No I/O — just mapping and math.
|
|
6
|
+
*/
|
|
7
|
+
import { round } from '../utils/math_helpers.js';
|
|
8
|
+
import { rangeToMinutes, roundBucket } from '../utils/time_helpers.js';
|
|
9
|
+
const EMPTY_OVERVIEW = {
|
|
10
|
+
avgResponseTime: 0,
|
|
11
|
+
p95ResponseTime: 0,
|
|
12
|
+
requestsPerMinute: 0,
|
|
13
|
+
errorRate: 0,
|
|
14
|
+
totalRequests: 0,
|
|
15
|
+
slowestEndpoints: [],
|
|
16
|
+
queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
|
|
17
|
+
recentErrors: [],
|
|
18
|
+
};
|
|
19
|
+
function mapQueryStats(queryStats, total) {
|
|
20
|
+
const qTotal = queryStats?.total ?? 0;
|
|
21
|
+
return {
|
|
22
|
+
total: qTotal,
|
|
23
|
+
avgDuration: queryStats?.avg_duration ?? 0,
|
|
24
|
+
perRequest: total > 0 ? round(qTotal / total) : 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function buildOverviewResult(input) {
|
|
28
|
+
const { total, stats, range, slowestEndpoints, queryStats, recentErrors } = input;
|
|
29
|
+
if (total === 0)
|
|
30
|
+
return EMPTY_OVERVIEW;
|
|
31
|
+
const errorCount = Number(stats?.error_count ?? 0);
|
|
32
|
+
return {
|
|
33
|
+
avgResponseTime: round(stats?.avg_duration ?? 0),
|
|
34
|
+
p95ResponseTime: 0,
|
|
35
|
+
requestsPerMinute: round(total / rangeToMinutes(range)),
|
|
36
|
+
errorRate: round((errorCount / total) * 100),
|
|
37
|
+
totalRequests: total,
|
|
38
|
+
slowestEndpoints: slowestEndpoints.map((s) => ({
|
|
39
|
+
url: s.url,
|
|
40
|
+
count: s.count,
|
|
41
|
+
avgDuration: s.avg_duration,
|
|
42
|
+
})),
|
|
43
|
+
queryStats: mapQueryStats(queryStats, total),
|
|
44
|
+
recentErrors: recentErrors.map((e) => ({
|
|
45
|
+
id: e.id,
|
|
46
|
+
message: e.message,
|
|
47
|
+
createdAt: e.created_at,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Widget data mappers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Map raw top events rows to the widget shape.
|
|
56
|
+
*/
|
|
57
|
+
export function mapTopEvents(raw) {
|
|
58
|
+
return (raw || []).map((r) => ({
|
|
59
|
+
eventName: r.event_name,
|
|
60
|
+
count: r.count,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Aggregate email status rows into sent/queued/failed counts.
|
|
65
|
+
*/
|
|
66
|
+
export function mapEmailActivity(raw) {
|
|
67
|
+
const activity = { sent: 0, queued: 0, failed: 0 };
|
|
68
|
+
for (const row of raw || []) {
|
|
69
|
+
const status = row.status;
|
|
70
|
+
const count = row.count;
|
|
71
|
+
if (status === 'sent' || status === 'sending')
|
|
72
|
+
activity.sent += count;
|
|
73
|
+
else if (status === 'queued' || status === 'queueing')
|
|
74
|
+
activity.queued += count;
|
|
75
|
+
else if (status === 'failed')
|
|
76
|
+
activity.failed = count;
|
|
77
|
+
}
|
|
78
|
+
return activity;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Map log level rows to the breakdown shape.
|
|
82
|
+
*/
|
|
83
|
+
export function mapLogLevelBreakdown(raw) {
|
|
84
|
+
const breakdown = { error: 0, warn: 0, info: 0, debug: 0 };
|
|
85
|
+
for (const row of raw || []) {
|
|
86
|
+
const level = row.level;
|
|
87
|
+
if (level in breakdown) {
|
|
88
|
+
breakdown[level] = row.count;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return breakdown;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Map status distribution row to the widget shape.
|
|
95
|
+
*/
|
|
96
|
+
export function mapStatusDistribution(row) {
|
|
97
|
+
return {
|
|
98
|
+
'2xx': row?.s2xx ?? 0,
|
|
99
|
+
'3xx': row?.s3xx ?? 0,
|
|
100
|
+
'4xx': row?.s4xx ?? 0,
|
|
101
|
+
'5xx': row?.s5xx ?? 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Map slowest query rows to the widget shape.
|
|
106
|
+
*/
|
|
107
|
+
export function mapSlowestQueries(raw) {
|
|
108
|
+
return (raw || []).map((r) => ({
|
|
109
|
+
sqlNormalized: r.sql_normalized,
|
|
110
|
+
avgDuration: r.avg_duration,
|
|
111
|
+
count: r.count,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Aggregate per-minute metrics into larger buckets for 24h/7d ranges.
|
|
116
|
+
* For 1h/6h, rows are returned as-is.
|
|
117
|
+
*/
|
|
118
|
+
export function aggregateChartBuckets(rows, range) {
|
|
119
|
+
if (range === '1h' || range === '6h')
|
|
120
|
+
return rows;
|
|
121
|
+
const bucketMinutes = range === '7d' ? 60 : 15;
|
|
122
|
+
const grouped = new Map();
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
const bucketKey = roundBucket(row.bucket, bucketMinutes);
|
|
125
|
+
if (!grouped.has(bucketKey)) {
|
|
126
|
+
grouped.set(bucketKey, {
|
|
127
|
+
bucket: bucketKey,
|
|
128
|
+
request_count: 0,
|
|
129
|
+
avg_duration: 0,
|
|
130
|
+
p95_duration: 0,
|
|
131
|
+
error_count: 0,
|
|
132
|
+
query_count: 0,
|
|
133
|
+
avg_query_duration: 0,
|
|
134
|
+
_count: 0,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const g = grouped.get(bucketKey);
|
|
138
|
+
g.request_count += row.request_count;
|
|
139
|
+
g.error_count += row.error_count;
|
|
140
|
+
g.query_count += row.query_count;
|
|
141
|
+
g.avg_duration += row.avg_duration;
|
|
142
|
+
g.p95_duration = Math.max(g.p95_duration, row.p95_duration);
|
|
143
|
+
g.avg_query_duration += row.avg_query_duration;
|
|
144
|
+
g._count++;
|
|
145
|
+
}
|
|
146
|
+
return Array.from(grouped.values()).map((g) => ({
|
|
147
|
+
bucket: g.bucket,
|
|
148
|
+
request_count: g.request_count,
|
|
149
|
+
avg_duration: g._count > 0 ? round(g.avg_duration / g._count) : 0,
|
|
150
|
+
p95_duration: round(g.p95_duration),
|
|
151
|
+
error_count: g.error_count,
|
|
152
|
+
query_count: g.query_count,
|
|
153
|
+
avg_query_duration: g._count > 0 ? round(g.avg_query_duration / g._count) : 0,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query runners for overview metrics and widget data.
|
|
3
|
+
*
|
|
4
|
+
* These functions execute the actual Knex queries within transactions.
|
|
5
|
+
* Extracted from DashboardStore to reduce file length.
|
|
6
|
+
*/
|
|
7
|
+
import type { Knex } from 'knex';
|
|
8
|
+
/**
|
|
9
|
+
* Query aggregated overview metrics within a transaction.
|
|
10
|
+
*/
|
|
11
|
+
export declare function queryOverviewMetrics(trx: Knex.Transaction, cutoff: string, range: string): Promise<Record<string, unknown>>;
|
|
12
|
+
/**
|
|
13
|
+
* Raw widget data shape returned by queryWidgetData.
|
|
14
|
+
*/
|
|
15
|
+
export interface WidgetDataRaw {
|
|
16
|
+
topEventsRaw: Record<string, unknown>[];
|
|
17
|
+
emailStatusRaw: Record<string, unknown>[];
|
|
18
|
+
logLevelsRaw: Record<string, unknown>[];
|
|
19
|
+
statusRaw: Record<string, unknown> | undefined;
|
|
20
|
+
slowQueriesRaw: Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Query all widget data in a single transaction.
|
|
24
|
+
*/
|
|
25
|
+
export declare function queryWidgetData(db: Knex, cutoff: string): Promise<WidgetDataRaw>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query runners for overview metrics and widget data.
|
|
3
|
+
*
|
|
4
|
+
* These functions execute the actual Knex queries within transactions.
|
|
5
|
+
* Extracted from DashboardStore to reduce file length.
|
|
6
|
+
*/
|
|
7
|
+
import { round } from '../utils/math_helpers.js';
|
|
8
|
+
import { buildOverviewResult } from './overview_queries.js';
|
|
9
|
+
/**
|
|
10
|
+
* Query aggregated overview metrics within a transaction.
|
|
11
|
+
*/
|
|
12
|
+
export async function queryOverviewMetrics(trx, cutoff, range) {
|
|
13
|
+
const stats = await trx('server_stats_requests')
|
|
14
|
+
.where('created_at', '>=', cutoff)
|
|
15
|
+
.select(trx.raw('COUNT(*) as total'), trx.raw('ROUND(AVG(duration), 2) as avg_duration'), trx.raw('SUM(CASE WHEN status_code >= 400 THEN 1 ELSE 0 END) as error_count'))
|
|
16
|
+
.first();
|
|
17
|
+
const total = Number(stats?.total ?? 0);
|
|
18
|
+
const slowestEndpoints = await trx('server_stats_requests')
|
|
19
|
+
.where('created_at', '>=', cutoff)
|
|
20
|
+
.select('url', trx.raw('COUNT(*) as count'), trx.raw('ROUND(AVG(duration), 2) as avg_duration'))
|
|
21
|
+
.groupBy('url')
|
|
22
|
+
.orderBy('avg_duration', 'desc')
|
|
23
|
+
.limit(5);
|
|
24
|
+
const queryStats = await trx('server_stats_queries')
|
|
25
|
+
.where('created_at', '>=', cutoff)
|
|
26
|
+
.select(trx.raw('COUNT(*) as total'), trx.raw('ROUND(AVG(duration), 2) as avg_duration'))
|
|
27
|
+
.first();
|
|
28
|
+
const recentErrors = await trx('server_stats_logs')
|
|
29
|
+
.where('created_at', '>=', cutoff)
|
|
30
|
+
.whereIn('level', ['error', 'fatal'])
|
|
31
|
+
.orderBy('created_at', 'desc')
|
|
32
|
+
.limit(5);
|
|
33
|
+
const result = buildOverviewResult({
|
|
34
|
+
total,
|
|
35
|
+
stats,
|
|
36
|
+
range,
|
|
37
|
+
slowestEndpoints,
|
|
38
|
+
queryStats,
|
|
39
|
+
recentErrors,
|
|
40
|
+
});
|
|
41
|
+
if (total > 0) {
|
|
42
|
+
const p95Offset = Math.floor(total * 0.95);
|
|
43
|
+
const p95Row = await trx('server_stats_requests')
|
|
44
|
+
.where('created_at', '>=', cutoff)
|
|
45
|
+
.orderBy('duration', 'asc')
|
|
46
|
+
.offset(Math.min(p95Offset, total - 1))
|
|
47
|
+
.limit(1)
|
|
48
|
+
.select('duration')
|
|
49
|
+
.first();
|
|
50
|
+
result.p95ResponseTime = round(p95Row?.duration ?? 0);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Query all widget data in a single transaction.
|
|
56
|
+
*/
|
|
57
|
+
export async function queryWidgetData(db, cutoff) {
|
|
58
|
+
return db.transaction(async (trx) => ({
|
|
59
|
+
topEventsRaw: await trx('server_stats_events')
|
|
60
|
+
.select('event_name', trx.raw('COUNT(*) as count'))
|
|
61
|
+
.where('created_at', '>=', cutoff)
|
|
62
|
+
.groupBy('event_name')
|
|
63
|
+
.orderBy('count', 'desc')
|
|
64
|
+
.limit(5),
|
|
65
|
+
emailStatusRaw: await trx('server_stats_emails')
|
|
66
|
+
.select('status', trx.raw('COUNT(*) as count'))
|
|
67
|
+
.where('created_at', '>=', cutoff)
|
|
68
|
+
.groupBy('status'),
|
|
69
|
+
logLevelsRaw: await trx('server_stats_logs')
|
|
70
|
+
.select('level', trx.raw('COUNT(*) as count'))
|
|
71
|
+
.where('created_at', '>=', cutoff)
|
|
72
|
+
.groupBy('level'),
|
|
73
|
+
statusRaw: await trx('server_stats_requests')
|
|
74
|
+
.select(trx.raw(`SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) as "s2xx"`), trx.raw(`SUM(CASE WHEN status_code >= 300 AND status_code < 400 THEN 1 ELSE 0 END) as "s3xx"`), trx.raw(`SUM(CASE WHEN status_code >= 400 AND status_code < 500 THEN 1 ELSE 0 END) as "s4xx"`), trx.raw(`SUM(CASE WHEN status_code >= 500 AND status_code < 600 THEN 1 ELSE 0 END) as "s5xx"`))
|
|
75
|
+
.where('created_at', '>=', cutoff)
|
|
76
|
+
.first(),
|
|
77
|
+
slowQueriesRaw: await trx('server_stats_queries')
|
|
78
|
+
.select('sql_normalized', trx.raw('ROUND(AVG(duration), 2) as avg_duration'), trx.raw('COUNT(*) as count'))
|
|
79
|
+
.where('created_at', '>=', cutoff)
|
|
80
|
+
.groupBy('sql_normalized')
|
|
81
|
+
.orderBy('avg_duration', 'desc')
|
|
82
|
+
.limit(5),
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview, chart, widget, and sparkline queries for the DashboardStore.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DashboardStore to reduce file length.
|
|
5
|
+
* All functions take a Knex instance and CoalesceCache as parameters.
|
|
6
|
+
*/
|
|
7
|
+
import { CoalesceCache } from './coalesce_cache.js';
|
|
8
|
+
import type { Knex } from 'knex';
|
|
9
|
+
export declare function fetchOverviewMetrics(db: Knex, cache: CoalesceCache, range: string): Promise<Record<string, unknown> | null>;
|
|
10
|
+
export declare function fetchChartData(db: Knex, cache: CoalesceCache, range: string): Promise<Record<string, unknown>[]>;
|
|
11
|
+
export interface OverviewWidgets {
|
|
12
|
+
topEvents: {
|
|
13
|
+
eventName: string;
|
|
14
|
+
count: number;
|
|
15
|
+
}[];
|
|
16
|
+
emailActivity: {
|
|
17
|
+
sent: number;
|
|
18
|
+
queued: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
};
|
|
21
|
+
logLevelBreakdown: {
|
|
22
|
+
error: number;
|
|
23
|
+
warn: number;
|
|
24
|
+
info: number;
|
|
25
|
+
debug: number;
|
|
26
|
+
};
|
|
27
|
+
statusDistribution: {
|
|
28
|
+
'2xx': number;
|
|
29
|
+
'3xx': number;
|
|
30
|
+
'4xx': number;
|
|
31
|
+
'5xx': number;
|
|
32
|
+
};
|
|
33
|
+
slowestQueries: {
|
|
34
|
+
sqlNormalized: string;
|
|
35
|
+
avgDuration: number;
|
|
36
|
+
count: number;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export declare function fetchOverviewWidgets(db: Knex, cache: CoalesceCache, range: string): Promise<OverviewWidgets>;
|
|
40
|
+
export declare function fetchSparklineData(db: Knex, cache: CoalesceCache, range: string): Promise<Record<string, unknown>[]>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overview, chart, widget, and sparkline queries for the DashboardStore.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DashboardStore to reduce file length.
|
|
5
|
+
* All functions take a Knex instance and CoalesceCache as parameters.
|
|
6
|
+
*/
|
|
7
|
+
import { log } from '../utils/logger.js';
|
|
8
|
+
import { rangeToCutoff } from '../utils/time_helpers.js';
|
|
9
|
+
import { mapTopEvents, mapEmailActivity, mapLogLevelBreakdown, mapStatusDistribution, mapSlowestQueries, aggregateChartBuckets, } from './overview_queries.js';
|
|
10
|
+
import { queryOverviewMetrics, queryWidgetData } from './overview_query_runners.js';
|
|
11
|
+
const WIDGETS_TTL = 2_000;
|
|
12
|
+
const SPARKLINE_TTL = 5_000;
|
|
13
|
+
const CHART_TTL = 5_000;
|
|
14
|
+
let overviewWidgetWarned = false;
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Overview metrics
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export function fetchOverviewMetrics(db, cache, range) {
|
|
19
|
+
return cache.cached('overviewMetrics:' + range, 2_000, () => db.transaction((trx) => queryOverviewMetrics(trx, rangeToCutoff(range), range)));
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Chart data
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export function fetchChartData(db, cache, range) {
|
|
25
|
+
return cache.cached('chartData:' + range, CHART_TTL, async () => {
|
|
26
|
+
const rows = await db('server_stats_metrics')
|
|
27
|
+
.where('bucket', '>=', rangeToCutoff(range))
|
|
28
|
+
.orderBy('bucket', 'asc');
|
|
29
|
+
return aggregateChartBuckets(rows, range);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const EMPTY_WIDGETS = {
|
|
33
|
+
topEvents: [],
|
|
34
|
+
emailActivity: { sent: 0, queued: 0, failed: 0 },
|
|
35
|
+
logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
|
|
36
|
+
statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
|
|
37
|
+
slowestQueries: [],
|
|
38
|
+
};
|
|
39
|
+
export function fetchOverviewWidgets(db, cache, range) {
|
|
40
|
+
return cache.cached('overviewWidgets:' + range, WIDGETS_TTL, async () => {
|
|
41
|
+
try {
|
|
42
|
+
const raw = await queryWidgetData(db, rangeToCutoff(range));
|
|
43
|
+
return {
|
|
44
|
+
topEvents: mapTopEvents(raw.topEventsRaw),
|
|
45
|
+
emailActivity: mapEmailActivity(raw.emailStatusRaw),
|
|
46
|
+
logLevelBreakdown: mapLogLevelBreakdown(raw.logLevelsRaw),
|
|
47
|
+
statusDistribution: mapStatusDistribution(raw.statusRaw),
|
|
48
|
+
slowestQueries: mapSlowestQueries(raw.slowQueriesRaw),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (!overviewWidgetWarned) {
|
|
53
|
+
overviewWidgetWarned = true;
|
|
54
|
+
log.warn('dashboard: getOverviewWidgets query failed — ' + err?.message);
|
|
55
|
+
}
|
|
56
|
+
return EMPTY_WIDGETS;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Sparkline data
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
export function fetchSparklineData(db, cache, range) {
|
|
64
|
+
return cache.cached('sparkline:' + range, SPARKLINE_TTL, async () => {
|
|
65
|
+
return (await db('server_stats_metrics')
|
|
66
|
+
.where('bucket', '>=', rangeToCutoff(range))
|
|
67
|
+
.orderBy('bucket', 'asc')).slice(-15);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic paginated query helper.
|
|
3
|
+
*
|
|
4
|
+
* Wraps COUNT + SELECT in a single transaction so the pool connection
|
|
5
|
+
* is acquired only once instead of two separate acquire/release cycles.
|
|
6
|
+
*/
|
|
7
|
+
import type { PaginatedResult, PaginateOptions } from './dashboard_types.js';
|
|
8
|
+
import type { Knex } from 'knex';
|
|
9
|
+
/**
|
|
10
|
+
* Execute a paginated query within a transaction.
|
|
11
|
+
*/
|
|
12
|
+
export declare function executePaginate(db: Knex, opts: PaginateOptions): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic paginated query helper.
|
|
3
|
+
*
|
|
4
|
+
* Wraps COUNT + SELECT in a single transaction so the pool connection
|
|
5
|
+
* is acquired only once instead of two separate acquire/release cycles.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Execute a paginated query within a transaction.
|
|
9
|
+
*/
|
|
10
|
+
export async function executePaginate(db, opts) {
|
|
11
|
+
return db.transaction(async (trx) => {
|
|
12
|
+
const countQuery = trx(opts.table);
|
|
13
|
+
if (opts.applyFilters)
|
|
14
|
+
opts.applyFilters(countQuery);
|
|
15
|
+
const [{ count: totalRaw }] = await countQuery.count('* as count');
|
|
16
|
+
const total = Number(totalRaw);
|
|
17
|
+
const offset = (opts.page - 1) * opts.perPage;
|
|
18
|
+
const dataQuery = trx(opts.table)
|
|
19
|
+
.orderBy('created_at', 'desc')
|
|
20
|
+
.limit(opts.perPage)
|
|
21
|
+
.offset(offset);
|
|
22
|
+
if (opts.applyFilters)
|
|
23
|
+
opts.applyFilters(dataQuery);
|
|
24
|
+
const data = await dataQuery;
|
|
25
|
+
return {
|
|
26
|
+
data,
|
|
27
|
+
total,
|
|
28
|
+
page: opts.page,
|
|
29
|
+
perPage: opts.perPage,
|
|
30
|
+
lastPage: Math.ceil(total / opts.perPage),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DashboardStore } from './dashboard_store.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
4
|
+
/**
|
|
5
|
+
* Handle the EXPLAIN endpoint for a stored query.
|
|
6
|
+
*
|
|
7
|
+
* Extracted from DashboardController.queryExplain to reduce
|
|
8
|
+
* complexity and function length.
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleQueryExplain(dashboardStore: DashboardStore, app: ApplicationService, ctx: HttpContext): Promise<unknown>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch and validate the query record for EXPLAIN.
|
|
3
|
+
* Returns null and sends an error response if invalid.
|
|
4
|
+
*/
|
|
5
|
+
async function fetchQueryRecord(dashboardStore, id) {
|
|
6
|
+
const db = dashboardStore.getDb();
|
|
7
|
+
if (!db)
|
|
8
|
+
return null;
|
|
9
|
+
return db('server_stats_queries').where('id', id).first() ?? null;
|
|
10
|
+
}
|
|
11
|
+
/** Check if the SQL is a SELECT statement (required for EXPLAIN). */
|
|
12
|
+
function isSelectQuery(sqlText) {
|
|
13
|
+
return sqlText.trim().toUpperCase().startsWith('SELECT');
|
|
14
|
+
}
|
|
15
|
+
/** Parse bindings from a stored query. */
|
|
16
|
+
function parseBindings(raw) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return [];
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Get the Lucid write client for running EXPLAIN. */
|
|
27
|
+
async function getAppDbClient(app) {
|
|
28
|
+
try {
|
|
29
|
+
const lucid = await app.container.make('lucid.db');
|
|
30
|
+
return lucid
|
|
31
|
+
.connection()
|
|
32
|
+
.getWriteClient();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Extract the plan from raw EXPLAIN result. */
|
|
39
|
+
function extractPlan(explainResult) {
|
|
40
|
+
const rawRows = explainResult?.rows ??
|
|
41
|
+
(Array.isArray(explainResult) ? explainResult : []);
|
|
42
|
+
if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
|
|
43
|
+
return rawRows[0]['QUERY PLAN'];
|
|
44
|
+
}
|
|
45
|
+
return rawRows;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Handle the EXPLAIN endpoint for a stored query.
|
|
49
|
+
*
|
|
50
|
+
* Extracted from DashboardController.queryExplain to reduce
|
|
51
|
+
* complexity and function length.
|
|
52
|
+
*/
|
|
53
|
+
export async function handleQueryExplain(dashboardStore, app, ctx) {
|
|
54
|
+
const { params, response } = ctx;
|
|
55
|
+
if (!dashboardStore.isReady()) {
|
|
56
|
+
return response.notFound({ error: 'Not found' });
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const query = await fetchQueryRecord(dashboardStore, Number(params.id));
|
|
60
|
+
if (!query)
|
|
61
|
+
return response.notFound({ error: 'Query not found' });
|
|
62
|
+
if (!isSelectQuery(query.sql_text)) {
|
|
63
|
+
return response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
|
|
64
|
+
}
|
|
65
|
+
const appDb = await getAppDbClient(app);
|
|
66
|
+
if (!appDb) {
|
|
67
|
+
return response.serviceUnavailable({ error: 'App database connection not available' });
|
|
68
|
+
}
|
|
69
|
+
const bindings = parseBindings(query.bindings);
|
|
70
|
+
const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
|
|
71
|
+
const plan = extractPlan(explainResult);
|
|
72
|
+
return response.json({ queryId: Number(params.id), sql: query.sql_text, plan });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return response.internalServerError({
|
|
76
|
+
error: 'EXPLAIN failed',
|
|
77
|
+
message: error?.message ?? 'Unknown error',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated and detail read queries for the DashboardStore.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DashboardStore to reduce file length.
|
|
5
|
+
*/
|
|
6
|
+
import type { CoalesceCache } from './coalesce_cache.js';
|
|
7
|
+
import type { RequestFilters, QueryFilters, EventFilters, EmailFilters, LogFilters, TraceFilters, PaginatedResult } from './dashboard_types.js';
|
|
8
|
+
import type { Knex } from 'knex';
|
|
9
|
+
interface ReadCtx {
|
|
10
|
+
db: Knex;
|
|
11
|
+
cache: CoalesceCache;
|
|
12
|
+
}
|
|
13
|
+
export declare function queryRequests(ctx: ReadCtx, page: number, perPage: number, filters?: RequestFilters): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
14
|
+
export declare function queryQueries(ctx: ReadCtx, page: number, perPage: number, filters?: QueryFilters): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
15
|
+
export declare function queryEvents(ctx: ReadCtx, page: number, perPage: number, filters?: EventFilters): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
16
|
+
export declare function queryEmails(ctx: ReadCtx, opts: {
|
|
17
|
+
page: number;
|
|
18
|
+
perPage: number;
|
|
19
|
+
filters?: EmailFilters;
|
|
20
|
+
excludeBody?: boolean;
|
|
21
|
+
}): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
22
|
+
export declare function queryLogs(ctx: ReadCtx, page: number, perPage: number, filters?: LogFilters): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
23
|
+
export declare function queryTraces(ctx: ReadCtx, page: number, perPage: number, filters?: TraceFilters): Promise<PaginatedResult<Record<string, unknown>>>;
|
|
24
|
+
export declare function queryQueriesGrouped(ctx: ReadCtx, opts: {
|
|
25
|
+
limit: number;
|
|
26
|
+
sort: string;
|
|
27
|
+
search?: string;
|
|
28
|
+
}): Promise<Record<string, unknown>[]>;
|
|
29
|
+
export declare function queryEmailHtml(ctx: ReadCtx, id: number): Promise<string | null>;
|
|
30
|
+
export declare function queryTraceDetail(ctx: ReadCtx, id: number): Promise<Record<string, unknown> | null>;
|
|
31
|
+
export declare function queryRequestDetail(ctx: ReadCtx, id: number): Promise<Record<string, unknown> | null>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated and detail read queries for the DashboardStore.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DashboardStore to reduce file length.
|
|
5
|
+
*/
|
|
6
|
+
import { rangeToCutoff } from '../utils/time_helpers.js';
|
|
7
|
+
import { fetchTraceDetail, fetchRequestDetail } from './detail_queries.js';
|
|
8
|
+
import { applyRequestFilters, applyQueryFilters, applyEventFilters, applyEmailFilters, applyLogFilters, applyTraceFilters, } from './filtered_queries.js';
|
|
9
|
+
import { executePaginate } from './paginate_helper.js';
|
|
10
|
+
const GROUPED_TTL = 3_000;
|
|
11
|
+
const PAGINATE_TTL = 1_000;
|
|
12
|
+
export function queryRequests(ctx, page, perPage, filters) {
|
|
13
|
+
return paginateCached(ctx, {
|
|
14
|
+
table: 'server_stats_requests',
|
|
15
|
+
page,
|
|
16
|
+
perPage,
|
|
17
|
+
applyFilters: (q) => applyRequestFilters(q, filters),
|
|
18
|
+
filterKey: filters ? JSON.stringify(filters) : '',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function queryQueries(ctx, page, perPage, filters) {
|
|
22
|
+
return paginateCached(ctx, {
|
|
23
|
+
table: 'server_stats_queries',
|
|
24
|
+
page,
|
|
25
|
+
perPage,
|
|
26
|
+
applyFilters: (q) => applyQueryFilters(q, filters),
|
|
27
|
+
filterKey: filters ? JSON.stringify(filters) : '',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function queryEvents(ctx, page, perPage, filters) {
|
|
31
|
+
return paginateCached(ctx, {
|
|
32
|
+
table: 'server_stats_events',
|
|
33
|
+
page,
|
|
34
|
+
perPage,
|
|
35
|
+
applyFilters: (q) => applyEventFilters(q, filters),
|
|
36
|
+
filterKey: filters ? JSON.stringify(filters) : '',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function queryEmails(ctx, opts) {
|
|
40
|
+
const { page, perPage, filters, excludeBody = false } = opts;
|
|
41
|
+
return paginateCached(ctx, {
|
|
42
|
+
table: 'server_stats_emails',
|
|
43
|
+
page,
|
|
44
|
+
perPage,
|
|
45
|
+
applyFilters: (q) => applyEmailFilters(q, filters, excludeBody),
|
|
46
|
+
filterKey: (filters ? JSON.stringify(filters) : '') + (excludeBody ? ':noBody' : ''),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function queryLogs(ctx, page, perPage, filters) {
|
|
50
|
+
return paginateCached(ctx, {
|
|
51
|
+
table: 'server_stats_logs',
|
|
52
|
+
page,
|
|
53
|
+
perPage,
|
|
54
|
+
applyFilters: (q) => applyLogFilters(q, filters),
|
|
55
|
+
filterKey: filters ? JSON.stringify(filters) : '',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function queryTraces(ctx, page, perPage, filters) {
|
|
59
|
+
return paginateCached(ctx, {
|
|
60
|
+
table: 'server_stats_traces',
|
|
61
|
+
page,
|
|
62
|
+
perPage,
|
|
63
|
+
applyFilters: (q) => applyTraceFilters(q, filters),
|
|
64
|
+
filterKey: filters ? JSON.stringify(filters) : '',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function queryQueriesGrouped(ctx, opts) {
|
|
68
|
+
const { db, cache } = ctx;
|
|
69
|
+
const { limit, sort, search } = opts;
|
|
70
|
+
return cache.cached('queriesGrouped:' + limit + ':' + sort + ':' + (search || ''), GROUPED_TTL, async () => {
|
|
71
|
+
const sorts = {
|
|
72
|
+
count: 'count',
|
|
73
|
+
avg_duration: 'avg_duration',
|
|
74
|
+
total_duration: 'total_duration',
|
|
75
|
+
};
|
|
76
|
+
const cutoff = rangeToCutoff('7d');
|
|
77
|
+
const q = db('server_stats_queries')
|
|
78
|
+
.select('sql_normalized', db.raw('COUNT(*) as count'), db.raw('ROUND(AVG(duration), 2) as avg_duration'), db.raw('ROUND(MIN(duration), 2) as min_duration'), db.raw('ROUND(MAX(duration), 2) as max_duration'), db.raw('ROUND(SUM(duration), 2) as total_duration'))
|
|
79
|
+
.where('created_at', '>=', cutoff)
|
|
80
|
+
.groupBy('sql_normalized')
|
|
81
|
+
.orderBy(sorts[sort] || 'total_duration', 'desc')
|
|
82
|
+
.limit(limit);
|
|
83
|
+
if (search)
|
|
84
|
+
q.where('sql_normalized', 'like', `%${search}%`);
|
|
85
|
+
return q;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function queryEmailHtml(ctx, id) {
|
|
89
|
+
return ctx.cache.coalesce('emailHtml:' + id, async () => {
|
|
90
|
+
const row = await ctx
|
|
91
|
+
.db('server_stats_emails')
|
|
92
|
+
.where('id', id)
|
|
93
|
+
.select('html', 'text_body')
|
|
94
|
+
.first();
|
|
95
|
+
return row ? row.html || row.text_body || null : null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export function queryTraceDetail(ctx, id) {
|
|
99
|
+
return ctx.cache.coalesce('traceDetail:' + id, () => fetchTraceDetail(ctx.db, id));
|
|
100
|
+
}
|
|
101
|
+
export function queryRequestDetail(ctx, id) {
|
|
102
|
+
return ctx.cache.coalesce('requestDetail:' + id, () => fetchRequestDetail(ctx.db, id));
|
|
103
|
+
}
|
|
104
|
+
function paginateCached(ctx, opts) {
|
|
105
|
+
const key = 'paginate:' + opts.table + ':' + opts.page + ':' + opts.perPage + ':' + (opts.filterKey || '');
|
|
106
|
+
return ctx.cache.cached(key, PAGINATE_TTL, () => executePaginate(ctx.db, opts));
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saved filter CRUD operations for the DashboardStore.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from DashboardStore to reduce file length.
|
|
5
|
+
*/
|
|
6
|
+
import { CoalesceCache } from './coalesce_cache.js';
|
|
7
|
+
import type { Knex } from 'knex';
|
|
8
|
+
export declare function fetchSavedFilters(db: Knex, cache: CoalesceCache, section?: string): Promise<Record<string, unknown>[]>;
|
|
9
|
+
export declare function insertSavedFilter(db: Knex, name: string, section: string, filterConfig: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
10
|
+
export declare function removeSavedFilter(db: Knex, id: number): Promise<boolean>;
|