Skip to content

Commit 913b0b3

Browse files
committed
feat(access-logs): replace DuckDB/Parquet with external ClickHouse backend
- Panel is now a thin gateway: raw lines go to ClickHouse, parsing via versioned materialized view - Fix regex escaping in MV DDL, date normalization, UTC handling end-to-end - Batch retry dedup via insert_deduplication_token + dependent MV dedup - Remove all legacy DuckDB/Parquet/rollup code, dead JS parser and unused locale keys
1 parent f78ad9d commit 913b0b3

38 files changed

Lines changed: 1204 additions & 1993 deletions

‎.gitignore‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ cc-agent/cc-agent
4747
cc-agent/cc-agent-linux-amd64
4848
cc-agent/cc-agent-linux-arm64
4949

50-
# Access-logs pipeline data (Parquet store + ingest spool)
50+
# Access-logs ingest spool
5151
data/access-logs/
5252

5353
Rules.md

‎Dockerfile‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ FROM node:20-alpine
33

44
WORKDIR /app
55

6-
# System dependencies: mongodb-tools for backups; libstdc++/libgcc are required
7-
# by the DuckDB native binding used by the access-logs analytics pipeline.
8-
RUN apk add --no-cache mongodb-tools libstdc++ libgcc
6+
# System dependencies: mongodb-tools for backups. The access-logs pipeline now
7+
# talks to an external ClickHouse over HTTP (pure-JS client), so no native
8+
# runtime libraries are needed.
9+
RUN apk add --no-cache mongodb-tools
910

1011
# Копируем зависимости
1112
COPY package*.json ./

‎docker-compose.dokploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ services:
5252
volumes:
5353
- app_logs:/app/logs
5454
- app_backups:/app/backups
55-
# Access-logs pipeline data (Parquet store + ingest spool).
55+
# Access-logs ingest spool (batches not yet forwarded to ClickHouse).
5656
- app_data:/app/data
5757

5858
volumes:

‎docker-compose.hub.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ services:
5555
- ./logs:/app/logs
5656
- ./greenlock.d:/app/greenlock.d
5757
- ./backups:/app/backups
58-
# Access-logs pipeline data (Parquet store + ingest spool) must survive
58+
# Access-logs ingest spool (batches not yet forwarded to ClickHouse) must survive
5959
# container rebuilds.
6060
- ./data:/app/data
6161
env_file:

‎docker-compose.local.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ services:
5959
volumes:
6060
- ./logs:/app/logs
6161
- ./backups:/app/backups
62-
# Access-logs pipeline data (Parquet store + ingest spool).
62+
# Access-logs ingest spool (batches not yet forwarded to ClickHouse).
6363
- ./data:/app/data
6464
environment:
6565
MONGO_URI: mongodb://${MONGO_USER:-hysteria}:${MONGO_PASSWORD:-localdevpassword}@mongo:27017/hysteria?authSource=admin

‎docker-compose.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ services:
7272
volumes:
7373
- ./logs:/app/logs
7474
- ./backups:/app/backups
75-
# Access-logs pipeline data (Parquet store + ingest spool) must survive
76-
# container rebuilds.
75+
# Access-logs ingest spool must survive container rebuilds (batches not
76+
# yet forwarded to ClickHouse live here).
7777
- ./data:/app/data
7878
- caddy_data:/caddy_data:ro
7979
env_file:

‎index.js‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -714,14 +714,29 @@ async function startServer() {
714714
setupCronJobs();
715715

716716
// Access-logs spool processor. Always started: it is idle when the spool
717-
// is empty (feature off) and picks up batches immediately once an admin
718-
// enables collection, without needing a restart.
717+
// is empty (feature off / ClickHouse not configured) and picks up batches
718+
// immediately once collection is enabled, without needing a restart.
719719
try {
720720
require('./src/services/accessLogs/processService').start();
721721
} catch (e) {
722722
logger.warn(`[AccessLogs] processor start skipped: ${e.message}`);
723723
}
724724

725+
// Ensure the ClickHouse schema exists on boot when credentials are set,
726+
// so the pipeline can insert immediately. Idempotent (IF NOT EXISTS) and
727+
// best-effort: a missing/unreachable ClickHouse just logs and retries on
728+
// the next settings save.
729+
setTimeout(async () => {
730+
try {
731+
const clickhouse = require('./src/services/accessLogs/clickhouseService');
732+
if (await clickhouse.isConfigured()) {
733+
await clickhouse.ensureSchema();
734+
}
735+
} catch (e) {
736+
logger.warn(`[AccessLogs] ClickHouse schema ensure at boot skipped: ${e.message}`);
737+
}
738+
}, 15 * 1000);
739+
725740
// Crash recovery: if the panel died mid-reconcile the access-logs state
726741
// stays stuck in a transitional value. Re-run reconciliation once on
727742
// boot (delayed so nodes/DB settle first); it is a no-op fast path when
@@ -1031,19 +1046,8 @@ function setupCronJobs() {
10311046
}
10321047
});
10331048

1034-
// Access-logs retention & storage-cap enforcement (daily 03:20). Cheap no-op
1035-
// when the feature is off or the store is empty.
1036-
cron.schedule('20 3 * * *', async () => {
1037-
try {
1038-
const Settings = require('./src/models/settingsModel');
1039-
const settings = await Settings.get();
1040-
if (settings?.accessLogs?.enabled) {
1041-
await require('./src/services/accessLogs/retentionService').enforce();
1042-
}
1043-
} catch (error) {
1044-
logger.error(`[Cron] Access-logs retention failed: ${error.message}`);
1045-
}
1046-
});
1049+
// Access-logs retention is enforced natively by the ClickHouse TTL on the
1050+
// access_events table (set via settings), so the panel runs no retention job.
10471051

10481052
// Clean inactive HWID device rows (daily 03:30)
10491053
cron.schedule('30 3 * * *', async () => {

‎package-lock.json‎

Lines changed: 11 additions & 133 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333
"start": "node index.js",
3434
"dev": "nodemon index.js",
3535
"test": "node scripts/test-session-cookie-config.js && node scripts/test-i18n-fallback.js && node scripts/test-node-ui-meta.js && node scripts/test-node-active-api.js && node scripts/test-auth-subscription-boundaries.js && node scripts/test-setup-ejs-validation.js && node scripts/test-security-ejs-validation.js && node scripts/test-remote-cron-service.js && node scripts/test-node-cron-panel.js && node scripts/test-node-cron-ui.js && node scripts/test-multicast-cron-service.js && node scripts/test-broadcast-panel.js && node scripts/test-broadcast-ui.js && npm run test:access-logs",
36-
"test:access-logs": "node scripts/test-access-logs-contract.js && node scripts/test-access-logs-credentials.js && node scripts/test-access-logs-ingest.js && node scripts/test-access-logs-parquet.js && node scripts/test-access-logs-retention.js",
36+
"test:access-logs": "node scripts/test-access-logs-credentials.js && node scripts/test-access-logs-ingest.js && node scripts/test-access-logs-clickhouse.js",
3737
"build:assets": "npx --yes esbuild public/css/style.css public/css/network.css --minify --loader:.css=css --outdir=public/css --out-extension:.css=.min.css --allow-overwrite && npx --yes esbuild public/js/network.js --minify --outfile=public/js/network.min.js"
3838
},
3939
"dependencies": {
4040
"@aws-sdk/client-s3": "^3.500.0",
41-
"@duckdb/node-api": "^1.5.4-r.1",
41+
"@clickhouse/client": "^1.23.1",
4242
"@root/greenlock-express": "^4.0.3",
4343
"axios": "^1.6.2",
4444
"bcryptjs": "^2.4.3",

‎public/js/access-logs.js‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
// Access-logs dashboard: filters, analytics overview (charts + user/top tables),
22
// on-demand event search, node status, purge.
33
//
4-
// All DuckDB-backed data comes from a single /api/analytics call (the server
5-
// runs the whole overview in one worker spawn). The raw-event search is a
6-
// separate on-demand request. Requests are issued sequentially, never in
7-
// parallel, because the panel intentionally allows only one heavy DuckDB query
8-
// at a time (weak-hardware constraint) and parallel calls would be rejected.
4+
// The whole overview comes from a single /api/analytics call (the server fans
5+
// the aggregate queries out to ClickHouse). The raw-event search is a separate
6+
// on-demand request. Requests are issued sequentially so the slower analytics
7+
// response does not delay the search rows the user is usually after.
98
(function () {
109
'use strict';
1110

@@ -43,8 +42,8 @@
4342
return n.toFixed(i ? 1 : 0) + ' ' + u[i];
4443
}
4544

46-
// DuckDB returns naive UTC timestamps ("2026-07-10 10:05:00", no zone). Parse
47-
// them as UTC so the whole page displays in the viewer's LOCAL time —
45+
// The server returns naive UTC timestamps ("2026-07-10 10:05:00", no zone).
46+
// Parse them as UTC so the whole page displays in the viewer's LOCAL time —
4847
// consistent with the datetime-local filter inputs (which are local and get
4948
// converted to UTC for the query). Without this the shown times would be
5049
// silently shifted by the timezone offset.
@@ -359,9 +358,10 @@
359358
renderUsers(data.users);
360359
renderTops(data);
361360

362-
// In degraded mode the DuckDB-only widgets are empty; hint why.
363-
if (data.duckdbRequired) {
364-
const note = (span) => '<tr><td class="al-empty" colspan="' + span + '">' + esc(I18N.duckdbRequired) + '</td></tr>';
361+
// Degraded mode (ClickHouse not configured/unreachable): widgets are
362+
// empty; hint why.
363+
if (data.chRequired) {
364+
const note = (span) => '<tr><td class="al-empty" colspan="' + span + '">' + esc(I18N.chRequired) + '</td></tr>';
365365
if (!(data.users || []).length) { $('alUsersByIp').innerHTML = note(5); $('alUsersByFanout').innerHTML = note(4); }
366366
if (!(data.topPorts || []).length) $('alTopPorts').innerHTML = note(2);
367367
if (!(data.topBlocked || []).length) $('alTopBlocked').innerHTML = note(2);
@@ -417,8 +417,8 @@
417417
}
418418
}
419419

420-
// Sequential refresh: analytics -> search -> status (never parallel, to
421-
// respect the single-concurrent-DuckDB-query limit).
420+
// Sequential refresh: analytics -> search -> status. Keeps the page calm and
421+
// avoids piling concurrent aggregate queries onto ClickHouse from one tab.
422422
async function refreshAll() {
423423
await loadAnalytics();
424424
await loadSearch();

0 commit comments

Comments
 (0)