Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0195a21f30 | |||
| 4dca747386 | |||
| 7663f502fa | |||
| 104cd417d8 | |||
| 93254d5d3d | |||
| 9a3f121a9c | |||
| bef74eb1aa | |||
| 308d8e4851 | |||
| dc010dc3ae | |||
| 61d5d3b1ad | |||
| dd70790d40 | |||
| 2f8c04edc4 | |||
| 474cc328dd | |||
| 39ff159bf7 |
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
47
changelog.md
47
changelog.md
@@ -1,5 +1,52 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-26 - 11.10.5 - fix(build)
|
||||||
|
rename smart tooling config to .smartconfig.json and update package references
|
||||||
|
|
||||||
|
- Moves the shared tool configuration from npmextra.json to .smartconfig.json.
|
||||||
|
- Updates package.json published files and documentation to reference the new config file.
|
||||||
|
- Refreshes several development and runtime dependency versions alongside the config migration.
|
||||||
|
|
||||||
|
## 2026-03-24 - 11.10.4 - fix(monitoring)
|
||||||
|
handle multiple protocol cache entries per backend in metrics output
|
||||||
|
|
||||||
|
- Group detected protocol cache entries by backend host and port so multiple domain-specific records are preserved.
|
||||||
|
- Emit one backend metrics row per cached domain and avoid dropping unmatched protocol cache entries by tracking seen entries with a composite host:port:domain key.
|
||||||
|
- Use cached protocol values when available while keeping backend-only rows for metrics without protocol cache data.
|
||||||
|
|
||||||
|
## 2026-03-23 - 11.10.3 - fix(deps)
|
||||||
|
bump tstest, smartmetrics, and taskbuffer to latest patch releases
|
||||||
|
|
||||||
|
- update @git.zone/tstest from ^3.5.0 to ^3.5.1
|
||||||
|
- update @push.rocks/smartmetrics from ^3.0.2 to ^3.0.3
|
||||||
|
- update @push.rocks/taskbuffer from ^8.0.0 to ^8.0.2
|
||||||
|
|
||||||
|
## 2026-03-23 - 11.10.2 - fix(deps)
|
||||||
|
bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1
|
||||||
|
|
||||||
|
- Updates @api.global/typedserver from ^8.4.2 to ^8.4.6
|
||||||
|
- Updates @push.rocks/smartproxy from ^26.2.0 to ^26.2.1
|
||||||
|
|
||||||
|
## 2026-03-23 - 11.10.1 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^26.2.0
|
||||||
|
|
||||||
|
- Updates the @push.rocks/smartproxy dependency from ^26.1.0 to ^26.2.0 in package.json.
|
||||||
|
|
||||||
|
## 2026-03-23 - 11.10.0 - feat(monitoring)
|
||||||
|
add backend protocol metrics to network stats and ops dashboard
|
||||||
|
|
||||||
|
- Expose backend protocol, connection, error, and suppression metrics in stats responses.
|
||||||
|
- Add typed backend info interfaces and app state support for backend metrics.
|
||||||
|
- Render a new backend protocols table in the ops network view with detail modal and suppression badges.
|
||||||
|
- Update smartproxy and lik dependencies to support backend protocol metrics collection.
|
||||||
|
|
||||||
|
## 2026-03-21 - 11.9.1 - fix(lifecycle)
|
||||||
|
clean up service subscriptions, proxy retries, and stale runtime state on shutdown
|
||||||
|
|
||||||
|
- unsubscribe from ServiceManager event streams and use one-time signal handlers to avoid duplicate shutdown execution
|
||||||
|
- reset existing SmartProxy instances before retry setup and prune expired certificate backoff cache entries
|
||||||
|
- add periodic sweeping and shutdown cleanup for stale RADIUS accounting sessions
|
||||||
|
|
||||||
## 2026-03-20 - 11.9.0 - feat(dcrouter)
|
## 2026-03-20 - 11.9.0 - feat(dcrouter)
|
||||||
add service manager lifecycle orchestration and health-based ops status reporting
|
add service manager lifecycle orchestration and health-based ops status reporting
|
||||||
|
|
||||||
|
|||||||
34
package.json
34
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "11.9.0",
|
"version": "11.10.5",
|
||||||
"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": {
|
||||||
@@ -22,48 +22,48 @@
|
|||||||
"watch": "tswatch"
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.3.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.9.1",
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tstest": "^3.5.0",
|
"@git.zone/tstest": "^3.6.0",
|
||||||
"@git.zone/tswatch": "^3.3.0",
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
"@types/node": "^25.5.0"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.3.0",
|
"@api.global/typedrequest": "^3.3.0",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^8.4.2",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^4.1.2",
|
"@api.global/typedsocket": "^4.1.2",
|
||||||
"@apiclient.xyz/cloudflare": "^7.1.0",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@design.estate/dees-catalog": "^3.48.5",
|
"@design.estate/dees-catalog": "^3.49.0",
|
||||||
"@design.estate/dees-element": "^2.2.3",
|
"@design.estate/dees-element": "^2.2.3",
|
||||||
"@push.rocks/lik": "^6.3.1",
|
"@push.rocks/lik": "^6.4.0",
|
||||||
"@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.3.0",
|
"@push.rocks/smartacme": "^9.3.0",
|
||||||
"@push.rocks/smartdata": "^7.1.0",
|
"@push.rocks/smartdata": "^7.1.2",
|
||||||
"@push.rocks/smartdns": "^7.9.0",
|
"@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",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartmetrics": "^3.0.2",
|
"@push.rocks/smartmetrics": "^3.0.3",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartmta": "^5.3.1",
|
"@push.rocks/smartmta": "^5.3.1",
|
||||||
"@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.17.10",
|
"@push.rocks/smartproxy": "^26.2.4",
|
||||||
"@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",
|
||||||
"@push.rocks/smartstate": "^2.2.0",
|
"@push.rocks/smartstate": "^2.2.1",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "7.0.0",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.9.0",
|
"@serve.zone/catalog": "^2.9.0",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
"@serve.zone/remoteingress": "^4.14.0",
|
"@serve.zone/remoteingress": "^4.14.2",
|
||||||
"@tsclass/tsclass": "^9.4.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"lru-cache": "^11.2.7",
|
"lru-cache": "^11.2.7",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"dist_ts_apiclient/**/*",
|
"dist_ts_apiclient/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
1074
pnpm-lock.yaml
generated
1074
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -133,7 +133,7 @@ The project now uses tswatch for development:
|
|||||||
```bash
|
```bash
|
||||||
pnpm run watch
|
pnpm run watch
|
||||||
```
|
```
|
||||||
Configuration in `npmextra.json`:
|
Configuration in `.smartconfig.json`:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"@git.zone/tswatch": {
|
"@git.zone/tswatch": {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.9.0',
|
version: '11.10.5',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,14 +61,21 @@ export class CertProvisionScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a domain is currently in backoff
|
* Check if a domain is currently in backoff.
|
||||||
|
* Expired entries are pruned from the cache to prevent unbounded growth.
|
||||||
*/
|
*/
|
||||||
async isInBackoff(domain: string): Promise<boolean> {
|
async isInBackoff(domain: string): Promise<boolean> {
|
||||||
const entry = await this.loadBackoff(domain);
|
const entry = await this.loadBackoff(domain);
|
||||||
if (!entry) return false;
|
if (!entry) return false;
|
||||||
|
|
||||||
const retryAfter = new Date(entry.retryAfter);
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
return retryAfter.getTime() > Date.now();
|
if (retryAfter.getTime() > Date.now()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff has expired — prune the stale entry
|
||||||
|
this.backoffCache.delete(domain);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,9 +131,12 @@ export class CertProvisionScheduler {
|
|||||||
const entry = await this.loadBackoff(domain);
|
const entry = await this.loadBackoff(domain);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
|
|
||||||
// Only return if still in backoff
|
// Only return if still in backoff — prune expired entries
|
||||||
const retryAfter = new Date(entry.retryAfter);
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
if (retryAfter.getTime() <= Date.now()) return null;
|
if (retryAfter.getTime() <= Date.now()) {
|
||||||
|
this.backoffCache.delete(domain);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
failures: entry.failures,
|
failures: entry.failures,
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
// Service lifecycle management
|
// Service lifecycle management
|
||||||
public serviceManager: plugins.taskbuffer.ServiceManager;
|
public serviceManager: plugins.taskbuffer.ServiceManager;
|
||||||
|
private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
|
||||||
public smartAcmeReady = false;
|
public smartAcmeReady = false;
|
||||||
|
|
||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
@@ -516,7 +517,7 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wire up aggregated events for logging
|
// Wire up aggregated events for logging
|
||||||
this.serviceManager.serviceSubject.subscribe((event) => {
|
this.serviceSubjectSubscription = this.serviceManager.serviceSubject.subscribe((event) => {
|
||||||
const level = event.type === 'failed' ? 'error' : event.type === 'retrying' ? 'warn' : 'info';
|
const level = event.type === 'failed' ? 'error' : event.type === 'retrying' ? 'warn' : 'info';
|
||||||
logger.log(level as any, `Service '${event.serviceName}': ${event.type}`, {
|
logger.log(level as any, `Service '${event.serviceName}': ${event.type}`, {
|
||||||
state: event.state,
|
state: event.state,
|
||||||
@@ -639,6 +640,13 @@ export class DcRouter {
|
|||||||
*/
|
*/
|
||||||
private async setupSmartProxy(): Promise<void> {
|
private async setupSmartProxy(): Promise<void> {
|
||||||
logger.log('info', 'Setting up SmartProxy...');
|
logger.log('info', 'Setting up SmartProxy...');
|
||||||
|
|
||||||
|
// Clean up any existing SmartProxy instance (e.g. from a retry)
|
||||||
|
if (this.smartProxy) {
|
||||||
|
this.smartProxy.removeAllListeners();
|
||||||
|
this.smartProxy = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
||||||
|
|
||||||
@@ -1126,6 +1134,12 @@ export class DcRouter {
|
|||||||
public async stop() {
|
public async stop() {
|
||||||
logger.log('info', 'Stopping DcRouter services...');
|
logger.log('info', 'Stopping DcRouter services...');
|
||||||
|
|
||||||
|
// Unsubscribe from service events before stopping services
|
||||||
|
if (this.serviceSubjectSubscription) {
|
||||||
|
this.serviceSubjectSubscription.unsubscribe();
|
||||||
|
this.serviceSubjectSubscription = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceManager handles reverse-dependency-ordered shutdown
|
// ServiceManager handles reverse-dependency-ordered shutdown
|
||||||
await this.serviceManager.stop();
|
await this.serviceManager.stop();
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,6 @@ export const runCli = async () => {
|
|||||||
await dcRouter.stop();
|
await dcRouter.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
process.on('SIGINT', shutdown);
|
process.once('SIGINT', shutdown);
|
||||||
process.on('SIGTERM', shutdown);
|
process.once('SIGTERM', shutdown);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -558,6 +558,7 @@ export class MetricsManager {
|
|||||||
throughputByIP: new Map<string, { in: number; out: number }>(),
|
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
|
backends: [] as Array<any>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +591,110 @@ export class MetricsManager {
|
|||||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||||
const requestsTotal = proxyMetrics.requests.total();
|
const requestsTotal = proxyMetrics.requests.total();
|
||||||
|
|
||||||
|
// Collect backend protocol data
|
||||||
|
const backendMetrics = proxyMetrics.backends.byBackend();
|
||||||
|
const protocolCache = proxyMetrics.backends.detectedProtocols();
|
||||||
|
|
||||||
|
// Group protocol cache entries by host:port so we can match them to backend metrics.
|
||||||
|
// The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
|
||||||
|
// can have multiple entries for different domains.
|
||||||
|
const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
|
||||||
|
for (const entry of protocolCache) {
|
||||||
|
const backendKey = `${entry.host}:${entry.port}`;
|
||||||
|
let entries = cacheByBackend.get(backendKey);
|
||||||
|
if (!entries) {
|
||||||
|
entries = [];
|
||||||
|
cacheByBackend.set(backendKey, entries);
|
||||||
|
}
|
||||||
|
entries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backends: Array<any> = [];
|
||||||
|
const seenCacheKeys = new Set<string>();
|
||||||
|
|
||||||
|
for (const [key, bm] of backendMetrics) {
|
||||||
|
const cacheEntries = cacheByBackend.get(key);
|
||||||
|
if (!cacheEntries || cacheEntries.length === 0) {
|
||||||
|
// No protocol cache entry — emit one row with backend metrics only
|
||||||
|
backends.push({
|
||||||
|
backend: key,
|
||||||
|
domain: null,
|
||||||
|
protocol: bm.protocol,
|
||||||
|
activeConnections: bm.activeConnections,
|
||||||
|
totalConnections: bm.totalConnections,
|
||||||
|
connectErrors: bm.connectErrors,
|
||||||
|
handshakeErrors: bm.handshakeErrors,
|
||||||
|
requestErrors: bm.requestErrors,
|
||||||
|
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
||||||
|
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
||||||
|
h2Failures: bm.h2Failures,
|
||||||
|
h2Suppressed: false,
|
||||||
|
h3Suppressed: false,
|
||||||
|
h2CooldownRemainingSecs: null,
|
||||||
|
h3CooldownRemainingSecs: null,
|
||||||
|
h2ConsecutiveFailures: null,
|
||||||
|
h3ConsecutiveFailures: null,
|
||||||
|
h3Port: null,
|
||||||
|
cacheAgeSecs: null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// One row per domain, each enriched with the shared backend metrics
|
||||||
|
for (const cache of cacheEntries) {
|
||||||
|
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
|
||||||
|
seenCacheKeys.add(compositeKey);
|
||||||
|
backends.push({
|
||||||
|
backend: key,
|
||||||
|
domain: cache.domain ?? null,
|
||||||
|
protocol: cache.protocol ?? bm.protocol,
|
||||||
|
activeConnections: bm.activeConnections,
|
||||||
|
totalConnections: bm.totalConnections,
|
||||||
|
connectErrors: bm.connectErrors,
|
||||||
|
handshakeErrors: bm.handshakeErrors,
|
||||||
|
requestErrors: bm.requestErrors,
|
||||||
|
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
||||||
|
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
||||||
|
h2Failures: bm.h2Failures,
|
||||||
|
h2Suppressed: cache.h2Suppressed,
|
||||||
|
h3Suppressed: cache.h3Suppressed,
|
||||||
|
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
|
||||||
|
h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
|
||||||
|
h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
|
||||||
|
h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
|
||||||
|
h3Port: cache.h3Port,
|
||||||
|
cacheAgeSecs: cache.ageSecs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include protocol cache entries with no matching backend metric
|
||||||
|
for (const entry of protocolCache) {
|
||||||
|
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
|
||||||
|
if (!seenCacheKeys.has(compositeKey)) {
|
||||||
|
backends.push({
|
||||||
|
backend: `${entry.host}:${entry.port}`,
|
||||||
|
domain: entry.domain,
|
||||||
|
protocol: entry.protocol,
|
||||||
|
activeConnections: 0,
|
||||||
|
totalConnections: 0,
|
||||||
|
connectErrors: 0,
|
||||||
|
handshakeErrors: 0,
|
||||||
|
requestErrors: 0,
|
||||||
|
avgConnectTimeMs: 0,
|
||||||
|
poolHitRate: 0,
|
||||||
|
h2Failures: 0,
|
||||||
|
h2Suppressed: entry.h2Suppressed,
|
||||||
|
h3Suppressed: entry.h3Suppressed,
|
||||||
|
h2CooldownRemainingSecs: entry.h2CooldownRemainingSecs,
|
||||||
|
h3CooldownRemainingSecs: entry.h3CooldownRemainingSecs,
|
||||||
|
h2ConsecutiveFailures: entry.h2ConsecutiveFailures,
|
||||||
|
h3ConsecutiveFailures: entry.h3ConsecutiveFailures,
|
||||||
|
h3Port: entry.h3Port,
|
||||||
|
cacheAgeSecs: entry.ageSecs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate,
|
throughputRate,
|
||||||
@@ -599,6 +704,7 @@ export class MetricsManager {
|
|||||||
throughputByIP,
|
throughputByIP,
|
||||||
requestsPerSecond,
|
requestsPerSecond,
|
||||||
requestsTotal,
|
requestsTotal,
|
||||||
|
backends,
|
||||||
};
|
};
|
||||||
}, 1000); // 1s cache — matches typical dashboard poll interval
|
}, 1000); // 1s cache — matches typical dashboard poll interval
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export class SecurityHandler {
|
|||||||
throughputByIP,
|
throughputByIP,
|
||||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||||
requestsTotal: networkStats.requestsTotal || 0,
|
requestsTotal: networkStats.requestsTotal || 0,
|
||||||
|
backends: networkStats.backends || [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ export class SecurityHandler {
|
|||||||
throughputByIP: [],
|
throughputByIP: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
|
backends: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -309,6 +309,7 @@ export class StatsHandler {
|
|||||||
throughputHistory: stats.throughputHistory || [],
|
throughputHistory: stats.throughputHistory || [],
|
||||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||||
requestsTotal: stats.requestsTotal || 0,
|
requestsTotal: stats.requestsTotal || 0,
|
||||||
|
backends: stats.backends || [],
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export interface IAccountingManagerConfig {
|
|||||||
detailedLogging?: boolean;
|
detailedLogging?: boolean;
|
||||||
/** Maximum active sessions to track in memory */
|
/** Maximum active sessions to track in memory */
|
||||||
maxActiveSessions?: number;
|
maxActiveSessions?: number;
|
||||||
|
/** Stale session timeout in hours — sessions with no update for this long are evicted (default: 24) */
|
||||||
|
staleSessionTimeoutHours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +107,7 @@ export class AccountingManager {
|
|||||||
private activeSessions: Map<string, IAccountingSession> = new Map();
|
private activeSessions: Map<string, IAccountingSession> = new Map();
|
||||||
private config: Required<IAccountingManagerConfig>;
|
private config: Required<IAccountingManagerConfig>;
|
||||||
private storageManager?: StorageManager;
|
private storageManager?: StorageManager;
|
||||||
|
private staleSessionSweepTimer?: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
// Counters for statistics
|
// Counters for statistics
|
||||||
private stats = {
|
private stats = {
|
||||||
@@ -121,6 +124,7 @@ export class AccountingManager {
|
|||||||
retentionDays: config?.retentionDays ?? 30,
|
retentionDays: config?.retentionDays ?? 30,
|
||||||
detailedLogging: config?.detailedLogging ?? false,
|
detailedLogging: config?.detailedLogging ?? false,
|
||||||
maxActiveSessions: config?.maxActiveSessions ?? 10000,
|
maxActiveSessions: config?.maxActiveSessions ?? 10000,
|
||||||
|
staleSessionTimeoutHours: config?.staleSessionTimeoutHours ?? 24,
|
||||||
};
|
};
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
}
|
}
|
||||||
@@ -132,9 +136,60 @@ export class AccountingManager {
|
|||||||
if (this.storageManager) {
|
if (this.storageManager) {
|
||||||
await this.loadActiveSessions();
|
await this.loadActiveSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start periodic sweep to evict stale sessions (every 15 minutes)
|
||||||
|
this.staleSessionSweepTimer = setInterval(() => {
|
||||||
|
this.sweepStaleSessions();
|
||||||
|
}, 15 * 60 * 1000);
|
||||||
|
// Allow the process to exit even if the timer is pending
|
||||||
|
if (this.staleSessionSweepTimer.unref) {
|
||||||
|
this.staleSessionSweepTimer.unref();
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
|
logger.log('info', `AccountingManager initialized with ${this.activeSessions.size} active sessions`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the accounting manager and clean up timers
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (this.staleSessionSweepTimer) {
|
||||||
|
clearInterval(this.staleSessionSweepTimer);
|
||||||
|
this.staleSessionSweepTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sweep stale active sessions that have not received any update
|
||||||
|
* within the configured timeout. These are orphaned sessions where
|
||||||
|
* the Stop packet was never received.
|
||||||
|
*/
|
||||||
|
private sweepStaleSessions(): void {
|
||||||
|
const timeoutMs = this.config.staleSessionTimeoutHours * 60 * 60 * 1000;
|
||||||
|
const cutoff = Date.now() - timeoutMs;
|
||||||
|
let swept = 0;
|
||||||
|
|
||||||
|
for (const [sessionId, session] of this.activeSessions) {
|
||||||
|
if (session.lastUpdateTime < cutoff) {
|
||||||
|
session.status = 'terminated';
|
||||||
|
session.terminateCause = 'StaleSessionTimeout';
|
||||||
|
session.endTime = Date.now();
|
||||||
|
session.sessionTime = Math.floor((session.endTime - session.startTime) / 1000);
|
||||||
|
|
||||||
|
if (this.storageManager) {
|
||||||
|
this.archiveSession(session).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeSessions.delete(sessionId);
|
||||||
|
swept++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swept > 0) {
|
||||||
|
logger.log('info', `Swept ${swept} stale RADIUS sessions (no update for ${this.config.staleSessionTimeoutHours}h)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle accounting start request
|
* Handle accounting start request
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ export class RadiusServer {
|
|||||||
this.radiusServer = undefined;
|
this.radiusServer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.accountingManager.stop();
|
||||||
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
logger.log('info', 'RADIUS server stopped');
|
logger.log('info', 'RADIUS server stopped');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export interface INetworkMetrics {
|
|||||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond?: number;
|
requestsPerSecond?: number;
|
||||||
requestsTotal?: number;
|
requestsTotal?: number;
|
||||||
|
backends?: IBackendInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConnectionDetails {
|
export interface IConnectionDetails {
|
||||||
@@ -174,4 +175,26 @@ export interface IConnectionDetails {
|
|||||||
startTime: number;
|
startTime: number;
|
||||||
bytesIn: number;
|
bytesIn: number;
|
||||||
bytesOut: number;
|
bytesOut: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBackendInfo {
|
||||||
|
backend: string;
|
||||||
|
domain: string | null;
|
||||||
|
protocol: string;
|
||||||
|
activeConnections: number;
|
||||||
|
totalConnections: number;
|
||||||
|
connectErrors: number;
|
||||||
|
handshakeErrors: number;
|
||||||
|
requestErrors: number;
|
||||||
|
avgConnectTimeMs: number;
|
||||||
|
poolHitRate: number;
|
||||||
|
h2Failures: number;
|
||||||
|
h2Suppressed: boolean;
|
||||||
|
h3Suppressed: boolean;
|
||||||
|
h2CooldownRemainingSecs: number | null;
|
||||||
|
h3CooldownRemainingSecs: number | null;
|
||||||
|
h2ConsecutiveFailures: number | null;
|
||||||
|
h3ConsecutiveFailures: number | null;
|
||||||
|
h3Port: number | null;
|
||||||
|
cacheAgeSecs: number | null;
|
||||||
}
|
}
|
||||||
@@ -179,5 +179,6 @@ export interface IReq_GetNetworkStats extends plugins.typedrequestInterfaces.imp
|
|||||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
requestsPerSecond: number;
|
requestsPerSecond: number;
|
||||||
requestsTotal: number;
|
requestsTotal: number;
|
||||||
|
backends?: statsInterfaces.IBackendInfo[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '11.9.0',
|
version: '11.10.5',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface INetworkState {
|
|||||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond: number;
|
requestsPerSecond: number;
|
||||||
requestsTotal: number;
|
requestsTotal: number;
|
||||||
|
backends: interfaces.data.IBackendInfo[];
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -148,6 +149,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
throughputHistory: [],
|
throughputHistory: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
|
backends: [],
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -503,6 +505,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||||
|
backends: networkStatsResponse.backends || [],
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
import * as appstate from '../appstate.js';
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from './shared/css.js';
|
import { viewHostCss } from './shared/css.js';
|
||||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
@@ -198,6 +199,38 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
|
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.protocolBadge.h1 {
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')};
|
||||||
|
color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.h2 {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.h3 {
|
||||||
|
background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')};
|
||||||
|
color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.unknown {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||||
|
color: ${cssManager.bdTheme('#757575', '#999999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.suppressionBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.statusBadge {
|
.statusBadge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -265,6 +298,9 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
<!-- Top IPs Section -->
|
<!-- Top IPs Section -->
|
||||||
${this.renderTopIPs()}
|
${this.renderTopIPs()}
|
||||||
|
|
||||||
|
<!-- Backend Protocols Section -->
|
||||||
|
${this.renderBackendProtocols()}
|
||||||
|
|
||||||
<!-- Requests Table -->
|
<!-- Requests Table -->
|
||||||
<dees-table
|
<dees-table
|
||||||
.data=${this.networkRequests}
|
.data=${this.networkRequests}
|
||||||
@@ -519,6 +555,106 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderBackendProtocols(): TemplateResult {
|
||||||
|
const backends = this.networkState.backends;
|
||||||
|
if (!backends || backends.length === 0) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.data=${backends}
|
||||||
|
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
|
||||||
|
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
|
||||||
|
const protocolClass = item.protocol.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
'Backend': item.backend,
|
||||||
|
'Domain': item.domain || '-',
|
||||||
|
'Protocol': html`
|
||||||
|
<span class="protocolBadge ${protocolClass}">${item.protocol.toUpperCase()}</span>
|
||||||
|
${item.h2Suppressed ? html`<span class="suppressionBadge" title="H2 suppressed: ${item.h2ConsecutiveFailures ?? 0} failures, cooldown ${item.h2CooldownRemainingSecs ?? 0}s">H2 suppressed</span>` : ''}
|
||||||
|
${item.h3Suppressed ? html`<span class="suppressionBadge" title="H3 suppressed: ${item.h3ConsecutiveFailures ?? 0} failures, cooldown ${item.h3CooldownRemainingSecs ?? 0}s">H3 suppressed</span>` : ''}
|
||||||
|
`,
|
||||||
|
'Active': item.activeConnections,
|
||||||
|
'Total': this.formatNumber(item.totalConnections),
|
||||||
|
'Avg Connect': item.avgConnectTimeMs > 0 ? `${item.avgConnectTimeMs.toFixed(1)}ms` : '-',
|
||||||
|
'Pool Hit Rate': item.poolHitRate > 0 ? `${(item.poolHitRate * 100).toFixed(1)}%` : '-',
|
||||||
|
'Errors': totalErrors > 0
|
||||||
|
? html`<span class="statusBadge error">${totalErrors}</span>`
|
||||||
|
: html`<span class="statusBadge success">0</span>`,
|
||||||
|
'Cache Age': item.cacheAgeSecs != null ? `${Math.round(item.cacheAgeSecs)}s` : '-',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'View Details',
|
||||||
|
iconName: 'lucide:info',
|
||||||
|
type: ['inRow', 'doubleClick', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
await this.showBackendDetails(actionData.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
heading1="Backend Protocols"
|
||||||
|
heading2="Auto-detected backend protocols and connection pool health"
|
||||||
|
searchable
|
||||||
|
.pagination=${false}
|
||||||
|
dataName="backend"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showBackendDetails(backend: interfaces.data.IBackendInfo) {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `Backend: ${backend.backend}`,
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<dees-dataview-codebox
|
||||||
|
.heading=${'Backend Details'}
|
||||||
|
progLang="json"
|
||||||
|
.codeToDisplay=${JSON.stringify({
|
||||||
|
backend: backend.backend,
|
||||||
|
domain: backend.domain,
|
||||||
|
protocol: backend.protocol,
|
||||||
|
activeConnections: backend.activeConnections,
|
||||||
|
totalConnections: backend.totalConnections,
|
||||||
|
avgConnectTimeMs: backend.avgConnectTimeMs,
|
||||||
|
poolHitRate: backend.poolHitRate,
|
||||||
|
errors: {
|
||||||
|
connect: backend.connectErrors,
|
||||||
|
handshake: backend.handshakeErrors,
|
||||||
|
request: backend.requestErrors,
|
||||||
|
h2Failures: backend.h2Failures,
|
||||||
|
},
|
||||||
|
suppression: {
|
||||||
|
h2Suppressed: backend.h2Suppressed,
|
||||||
|
h3Suppressed: backend.h3Suppressed,
|
||||||
|
h2CooldownRemainingSecs: backend.h2CooldownRemainingSecs,
|
||||||
|
h3CooldownRemainingSecs: backend.h3CooldownRemainingSecs,
|
||||||
|
h2ConsecutiveFailures: backend.h2ConsecutiveFailures,
|
||||||
|
h3ConsecutiveFailures: backend.h3ConsecutiveFailures,
|
||||||
|
},
|
||||||
|
h3Port: backend.h3Port,
|
||||||
|
cacheAgeSecs: backend.cacheAgeSecs,
|
||||||
|
}, null, 2)}
|
||||||
|
></dees-dataview-codebox>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy Backend Key',
|
||||||
|
iconName: 'lucide:Copy',
|
||||||
|
action: async () => {
|
||||||
|
await navigator.clipboard.writeText(backend.backend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async updateNetworkData() {
|
private async updateNetworkData() {
|
||||||
// Track requests/sec history for the trend sparkline (moved out of render)
|
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user