diff --git a/site/dashboard.html b/site/dashboard.html
index 2d9cc60..77018fc 100644
--- a/site/dashboard.html
+++ b/site/dashboard.html
@@ -217,6 +217,7 @@ body {
min-width: 2px;
}
.path-bar-fill.forward { background: var(--amber); }
+.path-bar-fill.upstream { background: var(--amber-dim); }
.path-bar-fill.recursive { background: var(--cyan); }
.path-bar-fill.cached { background: var(--teal); }
.path-bar-fill.local { background: var(--violet); }
@@ -285,6 +286,7 @@ body {
font-weight: 500;
}
.path-tag.FORWARD { background: rgba(192, 98, 58, 0.12); color: var(--amber-dim); }
+.path-tag.UPSTREAM { background: rgba(160, 120, 72, 0.12); color: var(--amber-dim); }
.path-tag.RECURSIVE { background: rgba(74, 124, 138, 0.12); color: var(--cyan); }
.path-tag.CACHED { background: rgba(107, 124, 78, 0.12); color: var(--teal-dim); }
.path-tag.LOCAL { background: rgba(100, 116, 139, 0.12); color: var(--violet-dim); }
@@ -655,6 +657,7 @@ body {
+
@@ -936,10 +939,12 @@ function renderMemory(mem, stats) {
function renderBarChart(containerId, defs, data, total) {
total = total || 1;
- document.getElementById(containerId).innerHTML = defs.map(d => {
- const count = data[d.key] || 0;
- const pct = ((count / total) * 100).toFixed(1);
- return `
+ document.getElementById(containerId).innerHTML = defs
+ .filter(d => (data[d.key] || 0) > 0)
+ .map(d => {
+ const count = data[d.key] || 0;
+ const pct = ((count / total) * 100).toFixed(1);
+ return `
${d.label}
@@ -947,7 +952,7 @@ function renderBarChart(containerId, defs, data, total) {
${pct}%
`;
- }).join('');
+ }).join('');
}
function encryptionPct(transport) {
@@ -957,6 +962,7 @@ function encryptionPct(transport) {
const PATH_DEFS = [
{ key: 'forwarded', label: 'Forward', cls: 'forward' },
+ { key: 'upstream', label: 'Upstream', cls: 'upstream' },
{ key: 'recursive', label: 'Recursive', cls: 'recursive' },
{ key: 'cached', label: 'Cached', cls: 'cached' },
{ key: 'local', label: 'Local', cls: 'local' },
@@ -1209,7 +1215,7 @@ async function refresh() {
prevTime = now;
// Cache hit rate
- const answered = q.cached + q.forwarded + q.recursive + q.coalesced + q.local + q.overridden;
+ const answered = q.cached + q.forwarded + q.upstream + q.recursive + q.coalesced + q.local + q.overridden;
const hitRate = answered > 0 ? ((q.cached / answered) * 100).toFixed(1) : '0.0';
document.getElementById('cacheRate').textContent = hitRate + '%';
diff --git a/src/api.rs b/src/api.rs
index 6ec3e48..17c4614 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -201,6 +201,7 @@ struct LanStatsResponse {
struct QueriesStats {
total: u64,
forwarded: u64,
+ upstream: u64,
recursive: u64,
coalesced: u64,
cached: u64,
@@ -548,6 +549,7 @@ async fn stats(State(ctx): State>) -> Json {
queries: QueriesStats {
total: snap.total,
forwarded: snap.forwarded,
+ upstream: snap.upstream,
recursive: snap.recursive,
coalesced: snap.coalesced,
cached: snap.cached,
diff --git a/src/ctx.rs b/src/ctx.rs
index 222e407..eeca407 100644
--- a/src/ctx.rs
+++ b/src/ctx.rs
@@ -246,7 +246,7 @@ pub async fn resolve_query(
.await
{
Ok(resp_wire) => match cache_and_parse(ctx, &qname, qtype, &resp_wire) {
- Ok(resp) => (resp, QueryPath::Forwarded, DnssecStatus::Indeterminate),
+ Ok(resp) => (resp, QueryPath::Upstream, DnssecStatus::Indeterminate),
Err(e) => {
error!("{} | {:?} {} | PARSE ERROR | {}", src_addr, qtype, qname, e);
(
@@ -1253,4 +1253,29 @@ mod tests {
other => panic!("expected A record, got {:?}", other),
}
}
+
+ #[tokio::test]
+ async fn pipeline_default_pool_reports_upstream_path() {
+ let mut upstream_resp = DnsPacket::new();
+ upstream_resp.header.response = true;
+ upstream_resp.header.rescode = ResultCode::NOERROR;
+ upstream_resp.answers.push(DnsRecord::A {
+ domain: "example.com".to_string(),
+ addr: Ipv4Addr::new(93, 184, 216, 34),
+ ttl: 300,
+ });
+ let upstream_addr = crate::testutil::mock_upstream(upstream_resp).await;
+
+ let ctx = crate::testutil::test_ctx().await;
+ ctx.upstream_pool
+ .lock()
+ .unwrap()
+ .set_primary(vec![Upstream::Udp(upstream_addr)]);
+ let ctx = Arc::new(ctx);
+
+ let (resp, path) = resolve_in_test(&ctx, "example.com", QueryType::A).await;
+ assert_eq!(path, QueryPath::Upstream);
+ assert_eq!(resp.header.rescode, ResultCode::NOERROR);
+ assert_eq!(resp.answers.len(), 1);
+ }
}
diff --git a/src/stats.rs b/src/stats.rs
index feae945..df9127c 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -90,6 +90,7 @@ fn linux_rss() -> usize {
pub struct ServerStats {
queries_total: u64,
queries_forwarded: u64,
+ queries_upstream: u64,
queries_recursive: u64,
queries_coalesced: u64,
queries_cached: u64,
@@ -127,7 +128,10 @@ impl Transport {
pub enum QueryPath {
Local,
Cached,
+ /// Matched a `[[forwarding]]` suffix rule.
Forwarded,
+ /// Resolved via the default `[upstream]` pool (no suffix match).
+ Upstream,
Recursive,
Coalesced,
Blocked,
@@ -141,6 +145,7 @@ impl QueryPath {
QueryPath::Local => "LOCAL",
QueryPath::Cached => "CACHED",
QueryPath::Forwarded => "FORWARD",
+ QueryPath::Upstream => "UPSTREAM",
QueryPath::Recursive => "RECURSIVE",
QueryPath::Coalesced => "COALESCED",
QueryPath::Blocked => "BLOCKED",
@@ -156,6 +161,8 @@ impl QueryPath {
Some(QueryPath::Cached)
} else if s.eq_ignore_ascii_case("FORWARD") {
Some(QueryPath::Forwarded)
+ } else if s.eq_ignore_ascii_case("UPSTREAM") {
+ Some(QueryPath::Upstream)
} else if s.eq_ignore_ascii_case("RECURSIVE") {
Some(QueryPath::Recursive)
} else if s.eq_ignore_ascii_case("COALESCED") {
@@ -183,6 +190,7 @@ impl ServerStats {
ServerStats {
queries_total: 0,
queries_forwarded: 0,
+ queries_upstream: 0,
queries_recursive: 0,
queries_coalesced: 0,
queries_cached: 0,
@@ -204,6 +212,7 @@ impl ServerStats {
QueryPath::Local => self.queries_local += 1,
QueryPath::Cached => self.queries_cached += 1,
QueryPath::Forwarded => self.queries_forwarded += 1,
+ QueryPath::Upstream => self.queries_upstream += 1,
QueryPath::Recursive => self.queries_recursive += 1,
QueryPath::Coalesced => self.queries_coalesced += 1,
QueryPath::Blocked => self.queries_blocked += 1,
@@ -232,6 +241,7 @@ impl ServerStats {
uptime_secs: self.uptime_secs(),
total: self.queries_total,
forwarded: self.queries_forwarded,
+ upstream: self.queries_upstream,
recursive: self.queries_recursive,
coalesced: self.queries_coalesced,
cached: self.queries_cached,
@@ -253,10 +263,11 @@ impl ServerStats {
let secs = uptime.as_secs() % 60;
log::info!(
- "STATS | uptime {}h{}m{}s | total {} | fwd {} | recursive {} | coalesced {} | cached {} | local {} | override {} | blocked {} | errors {}",
+ "STATS | uptime {}h{}m{}s | total {} | fwd {} | upstream {} | recursive {} | coalesced {} | cached {} | local {} | override {} | blocked {} | errors {}",
hours, mins, secs,
self.queries_total,
self.queries_forwarded,
+ self.queries_upstream,
self.queries_recursive,
self.queries_coalesced,
self.queries_cached,
@@ -272,6 +283,7 @@ pub struct StatsSnapshot {
pub uptime_secs: u64,
pub total: u64,
pub forwarded: u64,
+ pub upstream: u64,
pub recursive: u64,
pub coalesced: u64,
pub cached: u64,