diff --git a/changelog.md b/changelog.md index 746140f..3b697eb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 93175ae..e12a1f9 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.10.3', + version: '11.10.4', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index fa029b5..85b5e5d 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -595,47 +595,84 @@ export class MetricsManager { const backendMetrics = proxyMetrics.backends.byBackend(); const protocolCache = proxyMetrics.backends.detectedProtocols(); - // Index protocol cache by "host:port" - const cacheByKey = new Map(); + // 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(); for (const entry of protocolCache) { - cacheByKey.set(`${entry.host}:${entry.port}`, entry); + const backendKey = `${entry.host}:${entry.port}`; + let entries = cacheByBackend.get(backendKey); + if (!entries) { + entries = []; + cacheByBackend.set(backendKey, entries); + } + entries.push(entry); } const backends: Array = []; - const seen = new Set(); + const seenCacheKeys = new Set(); for (const [key, bm] of backendMetrics) { - seen.add(key); - const cache = cacheByKey.get(key); - backends.push({ - backend: key, - domain: cache?.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: cache?.h2Suppressed ?? false, - h3Suppressed: cache?.h3Suppressed ?? false, - h2CooldownRemainingSecs: cache?.h2CooldownRemainingSecs ?? null, - h3CooldownRemainingSecs: cache?.h3CooldownRemainingSecs ?? null, - h2ConsecutiveFailures: cache?.h2ConsecutiveFailures ?? null, - h3ConsecutiveFailures: cache?.h3ConsecutiveFailures ?? null, - h3Port: cache?.h3Port ?? null, - cacheAgeSecs: cache?.ageSecs ?? null, - }); + 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 key = `${entry.host}:${entry.port}`; - if (!seen.has(key)) { + const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`; + if (!seenCacheKeys.has(compositeKey)) { backends.push({ - backend: key, + backend: `${entry.host}:${entry.port}`, domain: entry.domain, protocol: entry.protocol, activeConnections: 0, diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 93175ae..e12a1f9 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '11.10.3', + version: '11.10.4', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }