Compare commits

..

8 Commits

Author SHA1 Message Date
447cf44d68 v7.4.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-21 18:56:44 +00:00
82ce17a941 fix(monitoring,remoteingress,web): Prune old metrics buckets periodically, clear metrics caches on shutdown, simplify edge disconnect handling, and optimize network view data updates 2026-02-21 18:56:44 +00:00
15da996e70 v7.4.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-21 18:13:10 +00:00
582e19e6a6 fix(dcrouter): replace console logging with structured logger, improve metrics logging, add terminal-ready wait in ops UI, bump dees-catalog patch 2026-02-21 18:13:10 +00:00
79765d6729 v7.4.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-21 14:02:48 +00:00
ffc93eb9d3 feat(opsserver): add real-time log push to ops dashboard and recent DNS query tracking 2026-02-21 14:02:48 +00:00
1337a4905a v7.3.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-20 15:37:15 +00:00
c7418d9e1a feat(dcrouter): Wire DNS server query events to MetricsManager for time-series tracking and bump @push.rocks/smartdns to ^7.9.0 2026-02-20 15:37:15 +00:00
18 changed files with 357 additions and 188 deletions

View File

@@ -1,5 +1,42 @@
# Changelog # Changelog
## 2026-02-21 - 7.4.2 - fix(monitoring,remoteingress,web)
Prune old metrics buckets periodically, clear metrics caches on shutdown, simplify edge disconnect handling, and optimize network view data updates
- Call pruneOldBuckets() each minute to proactively remove stale time-series buckets in MetricsManager
- Clear metricsCache, emailMinuteBuckets and dnsMinuteBuckets when MetricsManager stops to avoid stale state on shutdown
- On edgeDisconnected remove the edgeStatuses entry instead of mutating an existing record (more explicit cleanup)
- Remove unused traffic-timer variables and move requestsPerSec history updates from render() into updateNetworkData() to avoid unnecessary re-renders
- Optimize traffic data array updates by shifting in-place then reassigning arrays to preserve Lit reactivity and reduce intermediate allocations
## 2026-02-21 - 7.4.1 - fix(dcrouter)
replace console logging with structured logger, improve metrics logging, add terminal-ready wait in ops UI, bump dees-catalog patch
- Replace console.log/console.error calls in classes.dcrouter.ts with structured logger.log (info/debug/error) including contextual data and stringified errors
- MetricsManager: create a dedicated Smartlog instance (metricsLogger) for SmartMetrics and use shared logger for lifecycle events (start/stop)
- SmartProxy/ACME: convert startup/stop/cert events and error logging to structured logs; include generated route and cert metadata where relevant
- Shutdown/startup flows: unify service start/stop/error messages through logger to provide consistent, structured output
- UI change: ops-view-logs now waits for xterm terminalReady before pushing initial logs to avoid race conditions
- Bump dependency @design.estate/dees-catalog from 3.43.0 to 3.43.1
## 2026-02-21 - 7.4.0 - feat(opsserver)
add real-time log push to ops dashboard and recent DNS query tracking
- Export baseLogger and add a log destination that pushes log entries to connected ops_dashboard TypedSocket clients (ts/opsserver/handlers/logs.handler.ts, ts/logger.ts).
- Introduce a new TypedRequest (pushLogEntry) interface for server→client log pushes (ts_interfaces/requests/logs.ts) and wire client handling in the web UI (ts_web/appstate.ts, ts_web/plugins.ts).
- Add TypedSocket client connection lifecycle to the web app, stream pushed log entries into app state and update log views incrementally (ts_web/appstate.ts, ts_web/elements/ops-view-logs.ts).
- MetricsManager now records recent DNS queries (timestamp, domain, type, answered, responseTimeMs) and exposes them via stats endpoints for display in the UI (ts/monitoring/classes.metricsmanager.ts, ts/opsserver/handlers/stats.handler.ts, ts_interfaces/data/stats.ts).
- UI overview now displays DNS query entries and uses answered flag to set log level (ts_web/elements/ops-view-overview.ts).
- Add import/export for typedsocket in web plugins to enable real-time push (ts_web/plugins.ts).
- Bump dependency @push.rocks/smartproxy patch version ^25.7.8 → ^25.7.9 (package.json).
## 2026-02-20 - 7.3.0 - feat(dcrouter)
Wire DNS server 'query' events to MetricsManager for time-series tracking and bump @push.rocks/smartdns to ^7.9.0
- Add dnsServer 'query' event listener that iterates event.questions and calls metricsManager.trackDnsQuery(question.type, question.name, false, event.responseTimeMs).
- Listener is guarded by a metricsManager existence check to avoid runtime errors when metrics are not configured.
- Bump dependency @push.rocks/smartdns from ^7.8.1 to ^7.9.0 in package.json.
## 2026-02-20 - 7.2.0 - feat(logs) ## 2026-02-20 - 7.2.0 - feat(logs)
replace custom logs list with dees-chart-log component and push logs to chart, add log mapping and lifecycle sync, and bump smartlog dependency replace custom logs list with dees-chart-log component and push logs to chart, add log mapping and lifecycle sync, and bump smartlog dependency

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "7.2.0", "version": "7.4.2",
"description": "A multifaceted routing service handling mail and SMS delivery functions.", "description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -32,13 +32,13 @@
"@api.global/typedserver": "^8.3.0", "@api.global/typedserver": "^8.3.0",
"@api.global/typedsocket": "^4.1.0", "@api.global/typedsocket": "^4.1.0",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.43.0", "@design.estate/dees-catalog": "^3.43.1",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.1.3", "@push.rocks/smartacme": "^9.1.3",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartdns": "^7.8.1", "@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
@@ -49,7 +49,7 @@
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^25.7.8", "@push.rocks/smartproxy": "^25.7.9",
"@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",

50
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^3.43.0 specifier: ^3.43.1
version: 3.43.0(@tiptap/pm@2.27.2) version: 3.43.1(@tiptap/pm@2.27.2)
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.1.6 specifier: ^2.1.6
version: 2.1.6 version: 2.1.6
@@ -42,8 +42,8 @@ importers:
specifier: ^7.0.15 specifier: ^7.0.15
version: 7.0.15(socks@2.8.7) version: 7.0.15(socks@2.8.7)
'@push.rocks/smartdns': '@push.rocks/smartdns':
specifier: ^7.8.1 specifier: ^7.9.0
version: 7.8.1 version: 7.9.0
'@push.rocks/smartfile': '@push.rocks/smartfile':
specifier: ^13.1.2 specifier: ^13.1.2
version: 13.1.2 version: 13.1.2
@@ -75,8 +75,8 @@ importers:
specifier: ^4.2.3 specifier: ^4.2.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^25.7.8 specifier: ^25.7.9
version: 25.7.8 version: 25.7.9
'@push.rocks/smartradius': '@push.rocks/smartradius':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
@@ -351,8 +351,8 @@ packages:
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.43.0': '@design.estate/dees-catalog@3.43.1':
resolution: {integrity: sha512-UFW8oThP9Mc4L0wVVgmuGux868Ct/TwZ1WP8hZCe4e/+5gmxDc+4EArnt5hePHENboe1Soobh9mmrMN6kQZ3xQ==} resolution: {integrity: sha512-WAWOV8dIgdKfAbS4Ciek8oDVIWC0OSPODhpQdLlsGBXERcFaBPaYxcpywmrjXB/TFeoAQPxBxhS7jb9/p2Rprg==}
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -894,8 +894,8 @@ packages:
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartdns@7.8.1': '@push.rocks/smartdns@7.9.0':
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==} resolution: {integrity: sha512-1nDUfyXQo6j9HTUfcjE+BLeAv9QZ7WtAsM1V28zIoFdUpjNg/5g382L024H73PHsxh6lSYNhYYmFvWqzFQhXKg==}
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
@@ -1030,8 +1030,8 @@ packages:
'@push.rocks/smartpromise@4.2.3': '@push.rocks/smartpromise@4.2.3':
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
'@push.rocks/smartproxy@25.7.8': '@push.rocks/smartproxy@25.7.9':
resolution: {integrity: sha512-rKuC/5DgCBQmk1iCY2mZd+ZdH2mBOfcP1hWMARTP4Je4KqnNTJ2STM1tJmc9FmKVXxtEQCxWJnEnq1wNqwQFRA==} resolution: {integrity: sha512-5esFvD72TEyveaEQbDYRgD7C5hDfWMSBvurNx3KPi02CBKG1gnhx/WWT7RHDS3KRF5fEQh9YxvI9aMkOwjc7sQ==}
'@push.rocks/smartpuppeteer@2.0.5': '@push.rocks/smartpuppeteer@2.0.5':
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
@@ -3221,10 +3221,6 @@ packages:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
minimatch@10.2.0:
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
engines: {node: 20 || >=22}
minimatch@10.2.1: minimatch@10.2.1:
resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -4222,13 +4218,11 @@ packages:
xterm-addon-fit@0.8.0: xterm-addon-fit@0.8.0:
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==} resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
peerDependencies: peerDependencies:
xterm: ^5.0.0 xterm: ^5.0.0
xterm@5.3.0: xterm@5.3.0:
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
@@ -4340,7 +4334,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260210.0
'@design.estate/dees-catalog': 3.43.0(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.43.1(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4938,7 +4932,7 @@ snapshots:
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.43.0(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.43.1(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.8 '@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
@@ -5745,7 +5739,7 @@ snapshots:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdata': 7.0.15(socks@2.8.7) '@push.rocks/smartdata': 7.0.15(socks@2.8.7)
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
'@push.rocks/smartnetwork': 4.4.0 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -5939,7 +5933,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartdns@7.8.1': '@push.rocks/smartdns@7.9.0':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 6.0.0 '@push.rocks/smartenv': 6.0.0
@@ -5947,7 +5941,7 @@ snapshots:
'@push.rocks/smartrust': 1.2.1 '@push.rocks/smartrust': 1.2.1
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
acme-client: 5.4.0 acme-client: 5.4.0
minimatch: 10.2.0 minimatch: 10.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6117,7 +6111,7 @@ snapshots:
'@push.rocks/smartmail@2.2.0': '@push.rocks/smartmail@2.2.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartfile': 13.1.2 '@push.rocks/smartfile': 13.1.2
'@push.rocks/smartmustache': 3.0.2 '@push.rocks/smartmustache': 3.0.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6235,7 +6229,7 @@ snapshots:
'@push.rocks/smartnetwork@4.4.0': '@push.rocks/smartnetwork@4.4.0':
dependencies: dependencies:
'@push.rocks/smartdns': 7.8.1 '@push.rocks/smartdns': 7.9.0
'@push.rocks/smartping': 1.0.8 '@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0 '@push.rocks/smartstring': 4.1.0
@@ -6321,7 +6315,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.7.8': '@push.rocks/smartproxy@25.7.9':
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.2.1 '@push.rocks/smartlog': 3.2.1
@@ -9120,10 +9114,6 @@ snapshots:
dependencies: dependencies:
'@isaacs/brace-expansion': 5.0.1 '@isaacs/brace-expansion': 5.0.1
minimatch@10.2.0:
dependencies:
brace-expansion: 5.0.2
minimatch@10.2.1: minimatch@10.2.1:
dependencies: dependencies:
brace-expansion: 5.0.2 brace-expansion: 5.0.2

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '7.2.0', version: '7.4.2',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -252,9 +252,7 @@ export class DcRouter {
} }
public async start() { public async start() {
console.log('╔═══════════════════════════════════════════════════════════════════╗'); logger.log('info', 'Starting DcRouter Services');
console.log('║ Starting DcRouter Services ║');
console.log('╚═══════════════════════════════════════════════════════════════════╝');
this.opsServer = new OpsServer(this); this.opsServer = new OpsServer(this);
@@ -296,7 +294,7 @@ export class DcRouter {
this.logStartupSummary(); this.logStartupSummary();
} catch (error) { } catch (error) {
console.error('❌ Error starting DcRouter:', error); logger.log('error', 'Error starting DcRouter', { error: String(error) });
// Try to clean up any services that may have started // Try to clean up any services that may have started
await this.stop(); await this.stop();
throw error; throw error;
@@ -307,104 +305,60 @@ export class DcRouter {
* Log comprehensive startup summary * Log comprehensive startup summary
*/ */
private logStartupSummary(): void { private logStartupSummary(): void {
console.log('\n╔═══════════════════════════════════════════════════════════════════╗'); logger.log('info', 'DcRouter Started Successfully');
console.log('║ DcRouter Started Successfully ║');
console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
// Metrics summary // Metrics summary
if (this.metricsManager) { if (this.metricsManager) {
console.log('📊 Metrics Service:'); logger.log('info', 'Metrics Service: SmartMetrics active, SmartProxy stats active, real-time tracking enabled');
console.log(' ├─ SmartMetrics: Active');
console.log(' ├─ SmartProxy Stats: Active');
console.log(' └─ Real-time tracking: Enabled');
} }
// SmartProxy summary // SmartProxy summary
if (this.smartProxy) { if (this.smartProxy) {
console.log('🌐 SmartProxy Service:');
const routeCount = this.options.smartProxyConfig?.routes?.length || 0; const routeCount = this.options.smartProxyConfig?.routes?.length || 0;
console.log(` ├─ Routes configured: ${routeCount}`); const acmeEnabled = this.options.smartProxyConfig?.acme?.enabled || false;
console.log(` ├─ ACME enabled: ${this.options.smartProxyConfig?.acme?.enabled || false}`); const acmeMode = acmeEnabled
if (this.options.smartProxyConfig?.acme?.enabled) { ? `email=${this.options.smartProxyConfig!.acme!.email || 'not set'}, mode=${this.options.smartProxyConfig!.acme!.useProduction ? 'production' : 'staging'}`
console.log(` ├─ ACME email: ${this.options.smartProxyConfig.acme.email || 'not set'}`); : 'disabled';
console.log(` └─ ACME mode: ${this.options.smartProxyConfig.acme.useProduction ? 'production' : 'staging'}`); logger.log('info', `SmartProxy Service: ${routeCount} routes, ACME: ${acmeMode}`);
} else {
console.log(' └─ ACME: disabled');
}
} }
// Email service summary // Email service summary
if (this.emailServer && this.options.emailConfig) { if (this.emailServer && this.options.emailConfig) {
console.log('\n📧 Email Service:');
const ports = this.options.emailConfig.ports || []; const ports = this.options.emailConfig.ports || [];
console.log(` ├─ Ports: ${ports.join(', ')}`); const domainCount = this.options.emailConfig.domains?.length || 0;
console.log(` ├─ Hostname: ${this.options.emailConfig.hostname || 'localhost'}`); const domainNames = this.options.emailConfig.domains?.map(d => `${d.domain} (${d.dnsMode || 'default'})`).join(', ') || 'none';
console.log(` ├─ Domains configured: ${this.options.emailConfig.domains?.length || 0}`); logger.log('info', `Email Service: ports=[${ports.join(', ')}], hostname=${this.options.emailConfig.hostname || 'localhost'}, domains=${domainCount} [${domainNames}], DKIM initialized`);
if (this.options.emailConfig.domains && this.options.emailConfig.domains.length > 0) {
this.options.emailConfig.domains.forEach((domain, index) => {
const isLast = index === this.options.emailConfig!.domains!.length - 1;
console.log(` ${isLast ? '└─' : '├─'} ${domain.domain} (${domain.dnsMode || 'default'})`);
});
}
console.log(` └─ DKIM: Initialized for all domains`);
} }
// DNS service summary // DNS service summary
if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) { if (this.dnsServer && this.options.dnsNsDomains && this.options.dnsScopes) {
console.log('\n🌍 DNS Service:'); logger.log('info', `DNS Service: nameservers=[${this.options.dnsNsDomains.join(', ')}], authoritative for ${this.options.dnsScopes.length} domains [${this.options.dnsScopes.join(', ')}], UDP:53, DoH enabled`);
console.log(` ├─ Nameservers: ${this.options.dnsNsDomains.join(', ')}`);
console.log(` ├─ Primary NS: ${this.options.dnsNsDomains[0]}`);
console.log(` ├─ Authoritative for: ${this.options.dnsScopes.length} domains`);
console.log(` ├─ UDP Port: 53`);
console.log(` ├─ DNS-over-HTTPS: Enabled via socket handler`);
console.log(` └─ DNSSEC: ${this.options.dnsNsDomains[0] ? 'Enabled' : 'Disabled'}`);
// Show authoritative domains
if (this.options.dnsScopes.length > 0) {
console.log('\n Authoritative Domains:');
this.options.dnsScopes.forEach((domain, index) => {
const isLast = index === this.options.dnsScopes!.length - 1;
console.log(` ${isLast ? '└─' : '├─'} ${domain}`);
});
}
} }
// RADIUS service summary // RADIUS service summary
if (this.radiusServer && this.options.radiusConfig) { if (this.radiusServer && this.options.radiusConfig) {
console.log('\n🔐 RADIUS Service:');
console.log(` ├─ Auth Port: ${this.options.radiusConfig.authPort || 1812}`);
console.log(` ├─ Acct Port: ${this.options.radiusConfig.acctPort || 1813}`);
console.log(` ├─ Clients configured: ${this.options.radiusConfig.clients?.length || 0}`);
const vlanStats = this.radiusServer.getVlanManager().getStats(); const vlanStats = this.radiusServer.getVlanManager().getStats();
console.log(` ├─ VLAN mappings: ${vlanStats.totalMappings}`); logger.log('info', `RADIUS Service: auth=${this.options.radiusConfig.authPort || 1812}, acct=${this.options.radiusConfig.acctPort || 1813}, clients=${this.options.radiusConfig.clients?.length || 0}, VLANs=${vlanStats.totalMappings}, accounting=${this.options.radiusConfig.accounting?.enabled ? 'enabled' : 'disabled'}`);
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
} }
// Remote Ingress summary // Remote Ingress summary
if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) { if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
console.log('\n🌐 Remote Ingress:');
console.log(` ├─ Tunnel Port: ${this.options.remoteIngressConfig.tunnelPort || 8443}`);
const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0; const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
const connectedCount = this.tunnelManager.getConnectedCount(); const connectedCount = this.tunnelManager.getConnectedCount();
console.log(` ├─ Registered Edges: ${edgeCount}`); logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
console.log(` └─ Connected Edges: ${connectedCount}`);
} }
// Storage summary // Storage summary
if (this.storageManager && this.options.storage) { if (this.storageManager && this.options.storage) {
console.log('\n💾 Storage:'); logger.log('info', `Storage: path=${this.options.storage.fsPath || 'default'}`);
console.log(` └─ Path: ${this.options.storage.fsPath || 'default'}`);
} }
// Cache database summary // Cache database summary
if (this.cacheDb) { if (this.cacheDb) {
console.log('\n🗄 Cache Database (smartdata + LocalTsmDb):'); logger.log('info', `Cache Database: storage=${this.cacheDb.getStoragePath()}, db=${this.cacheDb.getDbName()}, cleaner=${this.cacheCleaner?.isActive() ? 'active' : 'inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
console.log(` ├─ Storage: ${this.cacheDb.getStoragePath()}`);
console.log(` ├─ Database: ${this.cacheDb.getDbName()}`);
console.log(` └─ Cleaner: ${this.cacheCleaner?.isActive() ? 'Active' : 'Inactive'} (${(this.options.cacheConfig?.cleanupIntervalHours || 1)}h interval)`);
} }
console.log('\n✅ All services are running\n'); logger.log('info', 'All services are running');
} }
/** /**
@@ -439,7 +393,7 @@ export class DcRouter {
* Set up SmartProxy with direct configuration and automatic email routes * Set up SmartProxy with direct configuration and automatic email routes
*/ */
private async setupSmartProxy(): Promise<void> { private async setupSmartProxy(): Promise<void> {
console.log('[DcRouter] Setting up SmartProxy...'); logger.log('info', 'Setting up SmartProxy...');
let routes: plugins.smartproxy.IRouteConfig[] = []; let routes: plugins.smartproxy.IRouteConfig[] = [];
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined; let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
@@ -447,22 +401,20 @@ export class DcRouter {
if (this.options.smartProxyConfig) { if (this.options.smartProxyConfig) {
routes = this.options.smartProxyConfig.routes || []; routes = this.options.smartProxyConfig.routes || [];
acmeConfig = this.options.smartProxyConfig.acme; acmeConfig = this.options.smartProxyConfig.acme;
console.log(`[DcRouter] Found ${routes.length} routes in config`); logger.log('info', `Found ${routes.length} routes in config, ACME config present: ${!!acmeConfig}`);
console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`);
} }
// If email config exists, automatically add email routes // If email config exists, automatically add email routes
if (this.options.emailConfig) { if (this.options.emailConfig) {
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig); const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
console.log(`Email Routes are:`) logger.log('debug', 'Email routes generated', { routes: JSON.stringify(emailRoutes) });
console.log(emailRoutes)
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
} }
// If DNS is configured, add DNS routes // If DNS is configured, add DNS routes
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) { if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0) {
const dnsRoutes = this.generateDnsRoutes(); const dnsRoutes = this.generateDnsRoutes();
console.log(`DNS Routes for nameservers ${this.options.dnsNsDomains.join(', ')}:`, dnsRoutes); logger.log('debug', `DNS routes for nameservers ${this.options.dnsNsDomains.join(', ')}`, { routes: JSON.stringify(dnsRoutes) });
routes = [...routes, ...dnsRoutes]; routes = [...routes, ...dnsRoutes];
} }
@@ -480,7 +432,7 @@ export class DcRouter {
// Configure DNS challenge if available // Configure DNS challenge if available
let challengeHandlers: any[] = []; let challengeHandlers: any[] = [];
if (this.options.dnsChallenge?.cloudflareApiKey) { if (this.options.dnsChallenge?.cloudflareApiKey) {
console.log('Configuring Cloudflare DNS challenge for ACME'); logger.log('info', 'Configuring Cloudflare DNS challenge for ACME');
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey); const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount); const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
challengeHandlers.push(dns01Handler); challengeHandlers.push(dns01Handler);
@@ -488,7 +440,7 @@ export class DcRouter {
// If we have routes or need a basic SmartProxy instance, create it // If we have routes or need a basic SmartProxy instance, create it
if (routes.length > 0 || this.options.smartProxyConfig) { if (routes.length > 0 || this.options.smartProxyConfig) {
console.log('Setting up SmartProxy with combined configuration'); logger.log('info', 'Setting up SmartProxy with combined configuration');
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start // Track cert entries loaded from cert store so we can populate certificateStatusMap after start
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = []; const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
@@ -537,7 +489,7 @@ export class DcRouter {
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig) // Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
if (this.smartAcme) { if (this.smartAcme) {
await this.smartAcme.stop().catch(err => await this.smartAcme.stop().catch(err =>
console.error('[DcRouter] Error stopping old SmartAcme:', err) logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
); );
} }
this.smartAcme = new plugins.smartacme.SmartAcme({ this.smartAcme = new plugins.smartacme.SmartAcme({
@@ -600,25 +552,19 @@ export class DcRouter {
} }
// Create SmartProxy instance // Create SmartProxy instance
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({ logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
routeCount: smartProxyConfig.routes?.length,
acmeEnabled: smartProxyConfig.acme?.enabled,
acmeEmail: smartProxyConfig.acme?.email,
certProvisionFunction: !!smartProxyConfig.certProvisionFunction
}, null, 2));
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig); this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
// Set up event listeners // Set up event listeners
this.smartProxy.on('error', (err) => { this.smartProxy.on('error', (err) => {
console.error('[DcRouter] SmartProxy error:', err); logger.log('error', `SmartProxy error: ${err.message}`, { stack: err.stack });
console.error('[DcRouter] Error stack:', err.stack);
}); });
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
// Events are keyed by domain for domain-centric certificate tracking // Events are keyed by domain for domain-centric certificate tracking
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => { this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); logger.log('info', `Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeNames = this.findRouteNamesForDomain(event.domain); const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, { this.certificateStatusMap.set(event.domain, {
status: 'valid', routeNames, status: 'valid', routeNames,
@@ -628,7 +574,7 @@ export class DcRouter {
}); });
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => { this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`); logger.log('info', `Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
const routeNames = this.findRouteNamesForDomain(event.domain); const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, { this.certificateStatusMap.set(event.domain, {
status: 'valid', routeNames, status: 'valid', routeNames,
@@ -638,7 +584,7 @@ export class DcRouter {
}); });
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => { this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error); logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
const routeNames = this.findRouteNamesForDomain(event.domain); const routeNames = this.findRouteNamesForDomain(event.domain);
this.certificateStatusMap.set(event.domain, { this.certificateStatusMap.set(event.domain, {
status: 'failed', routeNames, error: event.error, status: 'failed', routeNames, error: event.error,
@@ -647,9 +593,9 @@ export class DcRouter {
}); });
// Start SmartProxy // Start SmartProxy
console.log('[DcRouter] Starting SmartProxy...'); logger.log('info', 'Starting SmartProxy...');
await this.smartProxy.start(); await this.smartProxy.start();
console.log('[DcRouter] SmartProxy started successfully'); logger.log('info', 'SmartProxy started successfully');
// Populate certificateStatusMap for certs loaded from store at startup // Populate certificateStatusMap for certs loaded from store at startup
for (const entry of loadedCertEntries) { for (const entry of loadedCertEntries) {
@@ -701,10 +647,10 @@ export class DcRouter {
} }
} }
if (loadedCertEntries.length > 0) { if (loadedCertEntries.length > 0) {
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`); logger.log('info', `Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
} }
console.log(`SmartProxy started with ${routes.length} routes`); logger.log('info', `SmartProxy started with ${routes.length} routes`);
} }
} }
@@ -907,7 +853,7 @@ export class DcRouter {
} }
public async stop() { public async stop() {
console.log('Stopping DcRouter services...'); logger.log('info', 'Stopping DcRouter services...');
await this.opsServer.stop(); await this.opsServer.stop();
@@ -918,36 +864,36 @@ export class DcRouter {
this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(), this.cacheCleaner ? Promise.resolve(this.cacheCleaner.stop()) : Promise.resolve(),
// Stop metrics manager if running // Stop metrics manager if running
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(), this.metricsManager ? this.metricsManager.stop().catch(err => logger.log('error', 'Error stopping MetricsManager', { error: String(err) })) : Promise.resolve(),
// Stop unified email server if running // Stop unified email server if running
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(), this.emailServer ? this.emailServer.stop().catch(err => logger.log('error', 'Error stopping email server', { error: String(err) })) : Promise.resolve(),
// Stop SmartAcme if running // Stop SmartAcme if running
this.smartAcme ? this.smartAcme.stop().catch(err => console.error('Error stopping SmartAcme:', err)) : Promise.resolve(), this.smartAcme ? this.smartAcme.stop().catch(err => logger.log('error', 'Error stopping SmartAcme', { error: String(err) })) : Promise.resolve(),
// Stop HTTP SmartProxy if running // Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(), this.smartProxy ? this.smartProxy.stop().catch(err => logger.log('error', 'Error stopping SmartProxy', { error: String(err) })) : Promise.resolve(),
// Stop DNS server if running // Stop DNS server if running
this.dnsServer ? this.dnsServer ?
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) : this.dnsServer.stop().catch(err => logger.log('error', 'Error stopping DNS server', { error: String(err) })) :
Promise.resolve(), Promise.resolve(),
// Stop RADIUS server if running // Stop RADIUS server if running
this.radiusServer ? this.radiusServer ?
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) : this.radiusServer.stop().catch(err => logger.log('error', 'Error stopping RADIUS server', { error: String(err) })) :
Promise.resolve(), Promise.resolve(),
// Stop Remote Ingress tunnel manager if running // Stop Remote Ingress tunnel manager if running
this.tunnelManager ? this.tunnelManager ?
this.tunnelManager.stop().catch(err => console.error('Error stopping TunnelManager:', err)) : this.tunnelManager.stop().catch(err => logger.log('error', 'Error stopping TunnelManager', { error: String(err) })) :
Promise.resolve() Promise.resolve()
]); ]);
// Stop cache database after other services (they may need it during shutdown) // Stop cache database after other services (they may need it during shutdown)
if (this.cacheDb) { if (this.cacheDb) {
await this.cacheDb.stop().catch(err => console.error('Error stopping CacheDb:', err)); await this.cacheDb.stop().catch(err => logger.log('error', 'Error stopping CacheDb', { error: String(err) }));
} }
// Clear backoff cache in cert scheduler // Clear backoff cache in cert scheduler
@@ -969,9 +915,9 @@ export class DcRouter {
this.remoteIngressManager = undefined; this.remoteIngressManager = undefined;
this.certificateStatusMap.clear(); this.certificateStatusMap.clear();
console.log('All DcRouter services stopped'); logger.log('info', 'All DcRouter services stopped');
} catch (error) { } catch (error) {
console.error('Error during DcRouter shutdown:', error); logger.log('error', 'Error during DcRouter shutdown', { error: String(error) });
throw error; throw error;
} }
} }
@@ -998,7 +944,7 @@ export class DcRouter {
// Start new SmartProxy with updated configuration (will include email routes if configured) // Start new SmartProxy with updated configuration (will include email routes if configured)
await this.setupSmartProxy(); await this.setupSmartProxy();
console.log('SmartProxy configuration updated'); logger.log('info', 'SmartProxy configuration updated');
} }
@@ -1091,7 +1037,7 @@ export class DcRouter {
// Start email handling with new configuration // Start email handling with new configuration
await this.setupUnifiedEmailHandling(); await this.setupUnifiedEmailHandling();
console.log('Unified email configuration updated'); logger.log('info', 'Unified email configuration updated');
} }
/** /**
@@ -1131,7 +1077,7 @@ export class DcRouter {
this.emailServer.updateEmailRoutes(routes); this.emailServer.updateEmailRoutes(routes);
} }
console.log(`Email routes updated with ${routes.length} routes`); logger.log('info', `Email routes updated with ${routes.length} routes`);
} }
/** /**
@@ -1256,6 +1202,21 @@ export class DcRouter {
// Start the DNS server (UDP only) // Start the DNS server (UDP only)
await this.dnsServer.start(); await this.dnsServer.start();
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`); logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
// Wire DNS query events to MetricsManager for time-series tracking
if (this.metricsManager && this.dnsServer) {
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
for (const question of event.questions) {
this.metricsManager.trackDnsQuery(
question.type,
question.name,
false,
event.responseTimeMs,
event.answered,
);
}
});
}
// Validate DNS configuration // Validate DNS configuration
await this.validateDnsConfiguration(); await this.validateDnsConfiguration();

View File

@@ -14,8 +14,8 @@ const envMap: Record<string, 'local' | 'test' | 'staging' | 'production'> = {
// In-memory log buffer for the OpsServer UI // In-memory log buffer for the OpsServer UI
export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 }); export const logBuffer = new SmartlogDestinationBuffer({ maxEntries: 2000 });
// Default Smartlog instance // Default Smartlog instance (exported so OpsServer can add push destinations)
const baseLogger = new plugins.smartlog.Smartlog({ export const baseLogger = new plugins.smartlog.Smartlog({
logContext: { logContext: {
environment: envMap[nodeEnv] || 'production', environment: envMap[nodeEnv] || 'production',
runtime: 'node', runtime: 'node',

View File

@@ -2,9 +2,10 @@ import * as plugins from '../plugins.js';
import { DcRouter } from '../classes.dcrouter.js'; import { DcRouter } from '../classes.dcrouter.js';
import { MetricsCache } from './classes.metricscache.js'; import { MetricsCache } from './classes.metricscache.js';
import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js'; import { SecurityLogger, SecurityEventType } from '../security/classes.securitylogger.js';
import { logger } from '../logger.js';
export class MetricsManager { export class MetricsManager {
private logger: plugins.smartlog.Smartlog; private metricsLogger: plugins.smartlog.Smartlog;
private smartMetrics: plugins.smartmetrics.SmartMetrics; private smartMetrics: plugins.smartmetrics.SmartMetrics;
private dcRouter: DcRouter; private dcRouter: DcRouter;
private resetInterval?: NodeJS.Timeout; private resetInterval?: NodeJS.Timeout;
@@ -36,6 +37,7 @@ export class MetricsManager {
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
queryTimestamps: [] as number[], // Track query timestamps for rate calculation queryTimestamps: [] as number[], // Track query timestamps for rate calculation
responseTimes: [] as number[], // Track response times in ms responseTimes: [] as number[], // Track response times in ms
recentQueries: [] as Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>,
}; };
// Per-minute time-series buckets for charts // Per-minute time-series buckets for charts
@@ -55,15 +57,15 @@ export class MetricsManager {
constructor(dcRouter: DcRouter) { constructor(dcRouter: DcRouter) {
this.dcRouter = dcRouter; this.dcRouter = dcRouter;
// Create a new Smartlog instance for metrics // Create a Smartlog instance for SmartMetrics (requires its own instance)
this.logger = new plugins.smartlog.Smartlog({ this.metricsLogger = new plugins.smartlog.Smartlog({
logContext: { logContext: {
environment: 'production', environment: 'production',
runtime: 'node', runtime: 'node',
zone: 'dcrouter-metrics', zone: 'dcrouter-metrics',
} }
}); });
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter'); this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.metricsLogger, 'dcrouter');
// Initialize metrics cache with 500ms TTL // Initialize metrics cache with 500ms TTL
this.metricsCache = new MetricsCache(500); this.metricsCache = new MetricsCache(500);
} }
@@ -95,6 +97,7 @@ export class MetricsManager {
this.dnsMetrics.topDomains.clear(); this.dnsMetrics.topDomains.clear();
this.dnsMetrics.queryTimestamps = []; this.dnsMetrics.queryTimestamps = [];
this.dnsMetrics.responseTimes = []; this.dnsMetrics.responseTimes = [];
this.dnsMetrics.recentQueries = [];
this.dnsMetrics.lastResetDate = currentDate; this.dnsMetrics.lastResetDate = currentDate;
} }
@@ -107,20 +110,29 @@ export class MetricsManager {
this.securityMetrics.incidents = []; this.securityMetrics.incidents = [];
this.securityMetrics.lastResetDate = currentDate; this.securityMetrics.lastResetDate = currentDate;
} }
// Prune old time-series buckets every minute (don't wait for lazy query)
this.pruneOldBuckets();
}, 60000); // Check every minute }, 60000); // Check every minute
this.logger.log('info', 'MetricsManager started'); logger.log('info', 'MetricsManager started');
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
// Clear the reset interval // Clear the reset interval
if (this.resetInterval) { if (this.resetInterval) {
clearInterval(this.resetInterval); clearInterval(this.resetInterval);
this.resetInterval = undefined; this.resetInterval = undefined;
} }
this.smartMetrics.stop(); this.smartMetrics.stop();
this.logger.log('info', 'MetricsManager stopped');
// Clear caches and time-series buckets on shutdown
this.metricsCache.clear();
this.emailMinuteBuckets.clear();
this.dnsMinuteBuckets.clear();
logger.log('info', 'MetricsManager stopped');
} }
// Get server metrics from SmartMetrics and SmartProxy // Get server metrics from SmartMetrics and SmartProxy
@@ -228,6 +240,7 @@ export class MetricsManager {
queryTypes: this.dnsMetrics.queryTypes, queryTypes: this.dnsMetrics.queryTypes,
averageResponseTime: Math.round(avgResponseTime), averageResponseTime: Math.round(avgResponseTime),
activeDomains: this.dnsMetrics.topDomains.size, activeDomains: this.dnsMetrics.topDomains.size,
recentQueries: this.dnsMetrics.recentQueries.slice(),
}; };
}); });
} }
@@ -392,9 +405,21 @@ export class MetricsManager {
} }
// DNS event tracking methods // DNS event tracking methods
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void { public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number, answered?: boolean): void {
this.dnsMetrics.totalQueries++; this.dnsMetrics.totalQueries++;
this.incrementDnsBucket(); this.incrementDnsBucket();
// Store recent query entry
this.dnsMetrics.recentQueries.push({
timestamp: Date.now(),
domain,
type: queryType,
answered: answered ?? true,
responseTimeMs: responseTimeMs ?? 0,
});
if (this.dnsMetrics.recentQueries.length > 100) {
this.dnsMetrics.recentQueries.shift();
}
if (cacheHit) { if (cacheHit) {
this.dnsMetrics.cacheHits++; this.dnsMetrics.cacheHits++;

View File

@@ -1,15 +1,16 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js'; import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js'; import * as interfaces from '../../../ts_interfaces/index.js';
import { logBuffer } from '../../logger.js'; import { logBuffer, baseLogger } from '../../logger.js';
export class LogsHandler { export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent // Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
this.setupLogPushDestination();
} }
private registerHandlers(): void { private registerHandlers(): void {
@@ -165,6 +166,50 @@ export class LogsHandler {
return mapped; return mapped;
} }
/**
* Add a log destination to the base logger that pushes entries
* to all connected ops_dashboard TypedSocket clients.
*/
private setupLogPushDestination(): void {
const opsServerRef = this.opsServerRef;
baseLogger.addLogDestination({
async handleLog(logPackage: any) {
// Access the TypedSocket server instance from OpsServer
const typedsocket = opsServerRef.server?.typedserver?.typedsocket;
if (!typedsocket) return;
let connections: any[];
try {
connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
} catch {
return;
}
if (connections.length === 0) return;
const entry: interfaces.data.ILogEntry = {
timestamp: logPackage.timestamp || Date.now(),
level: LogsHandler.mapLogLevel(logPackage.level),
category: LogsHandler.deriveCategory(logPackage.context?.zone, logPackage.message),
message: logPackage.message,
metadata: logPackage.data,
};
for (const conn of connections) {
try {
const push = typedsocket.createTypedRequest<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
conn,
);
push.fire({ entry }).catch(() => {}); // fire-and-forget
} catch {
// connection may have closed
}
}
},
});
}
private setupLogStream( private setupLogStream(
virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>, virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>,
levelFilter?: string[], levelFilter?: string[],

View File

@@ -241,6 +241,7 @@ export class StatsHandler {
averageResponseTime: 0, averageResponseTime: 0,
queryTypes: stats.queryTypes, queryTypes: stats.queryTypes,
timeSeries, timeSeries,
recentQueries: stats.recentQueries,
}; };
}) })
); );
@@ -422,6 +423,7 @@ export class StatsHandler {
count: number; count: number;
}>; }>;
queryTypes: { [key: string]: number }; queryTypes: { [key: string]: number };
recentQueries?: Array<{ timestamp: number; domain: string; type: string; answered: boolean; responseTimeMs: number }>;
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats }; domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
}> { }> {
// Get metrics from MetricsManager if available // Get metrics from MetricsManager if available
@@ -435,9 +437,10 @@ export class StatsHandler {
cacheHitRate: dnsStats.cacheHitRate, cacheHitRate: dnsStats.cacheHitRate,
topDomains: dnsStats.topDomains, topDomains: dnsStats.topDomains,
queryTypes: dnsStats.queryTypes, queryTypes: dnsStats.queryTypes,
recentQueries: dnsStats.recentQueries,
}; };
} }
// Fallback if MetricsManager not available // Fallback if MetricsManager not available
return { return {
queriesPerSecond: 0, queriesPerSecond: 0,

View File

@@ -35,11 +35,7 @@ export class TunnelManager {
}); });
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => { this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
const existing = this.edgeStatuses.get(data.edgeId); this.edgeStatuses.delete(data.edgeId);
if (existing) {
existing.connected = false;
existing.activeTunnels = 0;
}
}); });
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => { this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {

View File

@@ -60,6 +60,13 @@ export interface IDnsStats {
timeSeries?: { timeSeries?: {
queries: ITimeSeriesPoint[]; queries: ITimeSeriesPoint[];
}; };
recentQueries?: Array<{
timestamp: number;
domain: string;
type: string;
answered: boolean;
responseTimeMs: number;
}>;
} }
export interface IRateLimitInfo { export interface IRateLimitInfo {

View File

@@ -41,4 +41,16 @@ export interface IReq_GetLogStream extends plugins.typedrequestInterfaces.implem
response: { response: {
logStream: plugins.typedrequestInterfaces.IVirtualStream; logStream: plugins.typedrequestInterfaces.IVirtualStream;
}; };
}
// Push Log Entry (server → client via TypedSocket)
export interface IReq_PushLogEntry extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushLogEntry
> {
method: 'pushLogEntry';
request: {
entry: statsInterfaces.ILogEntry;
};
response: {};
} }

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '7.2.0', version: '7.4.2',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -1075,6 +1075,55 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
} }
}); });
// ============================================================================
// TypedSocket Client for Real-time Log Streaming
// ============================================================================
let socketClient: plugins.typedsocket.TypedSocket | null = null;
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
// Register handler for pushed log entries from the server
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
async (dataArg) => {
const current = logStatePart.getState();
const updated = [...current.recentLogs, dataArg.entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
logStatePart.setState({ ...current, recentLogs: updated });
return {};
}
)
);
async function connectSocket() {
if (socketClient) return;
try {
socketClient = await plugins.typedsocket.TypedSocket.createClient(
socketRouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
);
await socketClient.setTag('role', 'ops_dashboard');
} catch (err) {
console.error('TypedSocket connection failed:', err);
socketClient = null;
}
}
async function disconnectSocket() {
if (socketClient) {
try {
await socketClient.stop();
} catch {
// ignore disconnect errors
}
socketClient = null;
}
}
// Combined refresh action for efficient polling // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();
@@ -1237,9 +1286,21 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
if (state.isLoggedIn !== previousIsLoggedIn) { if (state.isLoggedIn !== previousIsLoggedIn) {
previousIsLoggedIn = state.isLoggedIn; previousIsLoggedIn = state.isLoggedIn;
startAutoRefresh(); startAutoRefresh();
// Connect/disconnect TypedSocket based on login state
if (state.isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
} }
}); });
// Initial start // Initial start
startAutoRefresh(); startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState().isLoggedIn) {
connectSocket();
}
})(); })();

View File

@@ -29,6 +29,8 @@ export class OpsViewLogs extends DeesElement {
@state() @state()
accessor filterLimit: number = 100; accessor filterLimit: number = 100;
private lastPushedCount = 0;
constructor() { constructor() {
super(); super();
const subscription = appstate.logStatePart const subscription = appstate.logStatePart
@@ -110,7 +112,11 @@ export class OpsViewLogs extends DeesElement {
async connectedCallback() { async connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.fetchLogs(); this.lastPushedCount = 0;
// Only fetch if state is empty (streaming will handle new entries)
if (this.logState.recentLogs.length === 0) {
this.fetchLogs();
}
} }
async updated(changedProperties: Map<string, any>) { async updated(changedProperties: Map<string, any>) {
@@ -127,10 +133,27 @@ export class OpsViewLogs extends DeesElement {
// Ensure the chart element has finished its own initialization // Ensure the chart element has finished its own initialization
await chartLog.updateComplete; await chartLog.updateComplete;
chartLog.clearLogs(); // Wait for xterm terminal to finish initializing (CDN load)
const entries = this.getMappedLogEntries(); if (!chartLog.terminalReady) {
if (entries.length > 0) { await new Promise<void>((resolve) => {
chartLog.updateLog(entries); const check = () => {
if (chartLog.terminalReady) { resolve(); return; }
setTimeout(check, 50);
};
check();
});
}
const allEntries = this.getMappedLogEntries();
if (this.lastPushedCount === 0 && allEntries.length > 0) {
// Initial load: push all entries
chartLog.updateLog(allEntries);
this.lastPushedCount = allEntries.length;
} else if (allEntries.length > this.lastPushedCount) {
// Incremental: only push new entries
const newEntries = allEntries.slice(this.lastPushedCount);
chartLog.updateLog(newEntries);
this.lastPushedCount = allEntries.length;
} }
} }

View File

@@ -47,9 +47,6 @@ export class OpsViewNetwork extends DeesElement {
private lastChartUpdate = 0; private lastChartUpdate = 0;
private chartUpdateThreshold = 1000; // Minimum ms between chart updates private chartUpdateThreshold = 1000; // Minimum ms between chart updates
private lastTrafficUpdateTime = 0;
private trafficUpdateInterval = 1000; // Update every 1 second
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
private trafficUpdateTimer: any = null; private trafficUpdateTimer: any = null;
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
private historyLoaded = false; // Whether server-side throughput history has been loaded private historyLoaded = false; // Whether server-side throughput history has been loaded
@@ -106,8 +103,6 @@ export class OpsViewNetwork extends DeesElement {
this.trafficDataIn = [...emptyData]; this.trafficDataIn = [...emptyData];
this.trafficDataOut = emptyData.map(point => ({ ...point })); this.trafficDataOut = emptyData.map(point => ({ ...point }));
this.lastTrafficUpdateTime = now;
} }
/** /**
@@ -413,11 +408,7 @@ export class OpsViewNetwork extends DeesElement {
const throughput = this.calculateThroughput(); const throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0; const activeConnections = this.statsState.serverStats?.activeConnections || 0;
// Track requests/sec history for the trend sparkline // Build trend data from pre-computed history (mutated in updateNetworkData, not here)
this.requestsPerSecHistory.push(reqPerSec);
if (this.requestsPerSecHistory.length > 20) {
this.requestsPerSecHistory.shift();
}
const trendData = [...this.requestsPerSecHistory]; const trendData = [...this.requestsPerSecHistory];
while (trendData.length < 20) { while (trendData.length < 20) {
trendData.unshift(0); trendData.unshift(0);
@@ -529,6 +520,13 @@ export class OpsViewNetwork extends DeesElement {
} }
private async updateNetworkData() { private async updateNetworkData() {
// Track requests/sec history for the trend sparkline (moved out of render)
const reqPerSec = this.networkState.requestsPerSecond || 0;
this.requestsPerSecHistory.push(reqPerSec);
if (this.requestsPerSecHistory.length > 20) {
this.requestsPerSecHistory.shift();
}
// Only update if connections changed significantly // Only update if connections changed significantly
const newConnectionCount = this.networkState.connections.length; const newConnectionCount = this.networkState.connections.length;
const oldConnectionCount = this.networkRequests.length; const oldConnectionCount = this.networkRequests.length;
@@ -602,16 +600,13 @@ export class OpsViewNetwork extends DeesElement {
y: Math.round(throughputOutMbps * 10) / 10 y: Math.round(throughputOutMbps * 10) / 10
}; };
// Efficient array updates - modify in place when possible // In-place mutation then reassign for Lit reactivity (avoids 4 intermediate arrays)
if (this.trafficDataIn.length >= 60) { if (this.trafficDataIn.length >= 60) {
// Remove oldest and add newest this.trafficDataIn.shift();
this.trafficDataIn = [...this.trafficDataIn.slice(1), newDataPointIn]; this.trafficDataOut.shift();
this.trafficDataOut = [...this.trafficDataOut.slice(1), newDataPointOut];
} else {
// Still filling up the initial data
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
} }
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
this.lastChartUpdate = now; this.lastChartUpdate = now;
} }

View File

@@ -133,8 +133,8 @@ export class OpsViewOverview extends DeesElement {
.logEntries=${this.getRecentEventEntries()} .logEntries=${this.getRecentEventEntries()}
></dees-chart-log> ></dees-chart-log>
<dees-chart-log <dees-chart-log
.label=${'Security Alerts'} .label=${'DNS Queries'}
.logEntries=${this.getSecurityAlertEntries()} .logEntries=${this.getDnsQueryEntries()}
></dees-chart-log> ></dees-chart-log>
</div> </div>
`} `}
@@ -395,6 +395,16 @@ export class OpsViewOverview extends DeesElement {
})); }));
} }
private getDnsQueryEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
const queries: any[] = (this.statsState.dnsStats as any)?.recentQueries || [];
return queries.map((q: any) => ({
timestamp: new Date(q.timestamp).toISOString(),
level: q.answered ? 'info' as const : 'warn' as const,
message: `${q.type} ${q.domain} (${q.responseTimeMs}ms)`,
source: 'dns',
}));
}
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> { private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
const ts = this.statsState.emailStats?.timeSeries; const ts = this.statsState.emailStats?.timeSeries;
if (!ts) return []; if (!ts) return [];

View File

@@ -2,9 +2,13 @@
import * as deesElement from '@design.estate/dees-element'; import * as deesElement from '@design.estate/dees-element';
import * as deesCatalog from '@design.estate/dees-catalog'; import * as deesCatalog from '@design.estate/dees-catalog';
// TypedSocket for real-time push communication
import * as typedsocket from '@api.global/typedsocket';
export { export {
deesElement, deesElement,
deesCatalog deesCatalog,
typedsocket,
} }
// domtools gives us TypedRequest and other utilities // domtools gives us TypedRequest and other utilities