Observability and Debugging
These executors record queries, export metrics, or support debugging and tests. Evaluate hot-path cost and data retention.
learn_domain
learn_domain was first introduced in v1.2.0 and is currently in an experimental stage. Filtering rules, write trigger timing, and the integration interface with dynamic_domain_set may change in future releases. Before deploying in production, please evaluate the impact of learning rates and disk writes on the persistence layer.
Purpose
Observes qnames in the DNS pipeline and writes matching request names into a target dynamic_domain_set. It is a side-effect executor and never modifies the DNS request or response.
Example Configuration
- tag: learned_allow
type: dynamic_domain_set
args:
path: "/etc/oxidns/learned-allow.txt"
- tag: learn_allow
type: learn_domain
args:
provider: learned_allow
phase: after
questions: first
qtypes: ["A", "AAAA"]
success_only: true
answer_required: true
rule_kind: full
async: true
error_mode: continue
timeout: 1s
Configuration Details
provider
- Type:
string; Required: yes - Purpose: Target
dynamic_domain_setprovider tag. - Constraint: must reference
dynamic_domain_set, not a regulardomain_set.
phase
- Type:
string; Required: no; Default:after - Values:
before,after - Purpose:
before: learn from the request questions before downstream executors run.after: run downstream first, then decide whether to learn based on the response.
questions
- Type:
string; Required: no; Default:first - Values:
first,all - Purpose: Learns only the first question or all questions.
qtypes
- Type:
array; Required: no; Default:["A", "AAAA"] - Purpose: DNS query types eligible for learning.
success_only
- Type:
bool; Required: no; Default:true - Purpose: Only applies to
phase: after; requiresNOERROR.
answer_required
- Type:
bool; Required: no; Default:true - Purpose: Only applies to
phase: after; requires at least one answer record.
rule_kind
- Type:
string; Required: no; Default:full - Values:
full,domain - Purpose: Rule kind written into the target
dynamic_domain_set.
async
- Type:
bool; Required: no; Default:true - Purpose: Enqueues learned rules and continues immediately. When
false, waits for provider persistence.
error_mode
- Type:
string; Required: no; Default:continue - Values:
continue,stop,fail - Purpose: Controls execution behavior if learning fails.
timeout
- Type:
duration; Required: no; Default:1s - Purpose: Limits provider write waiting time when
async: false.
Behavior
- Learned domains are normalized to lowercase with trailing dots removed.
- The default output is
full:example.comto avoid broad accidental matches. - With the default
phase: after, only successfulA/AAAAresponses with answers are learned. phase: beforedoes not inspect response conditions.- v1 only learns the request question qname and does not learn CNAME targets.
Typical Uses
- Add successfully resolved domains to a dynamic allow list.
- Persist domains seen on a policy branch into a local file for later
qname $learned_allowor parentdomain_setmatching.
query_summary
Purpose
Records concise query summaries.
Example Configuration
- tag: summary_main
type: query_summary
args:
# Extra title so multiple summary points are easy to distinguish
msg: "main pipeline"
Configuration Details
msg
- Type:
string; Required: no - Purpose: Extra summary label.
quick setup
- exec: "query_summary main"
Behavior
- Emits compact logs or summaries for operator visibility.
Typical Uses
- Light observability on the main path
- Distinguish different branches
query_recorder
Purpose
Persists the entry request, the post-next response, and sequence execution-path events into a recorder-owned SQLite database, then exposes history, aggregate stats, and an SSE stream.
Example Configuration
- tag: query_recorder_main
type: query_recorder
args:
# SQLite path for this recorder. Multiple recorders may share one file.
path: "./data/query-recorder-main.sqlite"
# Hot-path enqueue buffer size
queue_size: 8192
# Batch size per SQLite flush
batch_size: 256
# Background flush interval in milliseconds
flush_interval_ms: 200
# Number of recent records kept in memory for SSE tail replay
memory_tail: 1024
# Retention window in days; minimum 1
retention_days: 7
# Cleanup interval in hours; minimum 1
cleanup_interval_hours: 1
# Maximum concurrent SQLite readers; minimum 1
reader_concurrency: 2
Configuration Details
path
- Type:
string; Required: yes - Purpose: SQLite path for this recorder.
queue_size
- Type:
integer; Required: no; Default:8192 - Purpose: Bounded queue size between the request path and the writer thread.
batch_size
- Type:
integer; Required: no; Default:256 - Purpose: Number of records flushed per SQLite batch.
flush_interval_ms
- Type:
integer; Required: no; Default:200 - Purpose: Maximum batch flush interval in milliseconds.
memory_tail
- Type:
integer; Required: no; Default:1024 - Purpose: Size of the in-memory tail used by
stream?tail=n.
retention_days
- Type:
integer; Required: no; Default:7; Minimum:1 - Purpose: Record retention window. Expired rows are deleted by the cleanup task.
cleanup_interval_hours
- Type:
integer; Required: no; Default:1; Minimum:1 - Purpose: Cleanup task cadence.
reader_concurrency
- Type:
integer; Required: no; Default:2; Minimum:1 - Purpose: Limits concurrent SQLite readers used by WebUI / API history and stats queries, so read bursts on large recorder databases do not occupy too many blocking threads or too much memory.
Behavior
- This is a pure executor observer and does not change server finalization logic.
- It captures a structured snapshot of the entry request, enables
DnsContext.execution_path, runsnext, and commits immediately afternextreturns. - Successful runs store the current response. Failed runs store
errorand an empty response shape. - Request and response payloads are not stored as wire blobs. Question, RR, and EDNS fields are extracted into JSON text columns.
- The
client_ipfield comes from the transport source address seen by OxiDNS; if a local forwarding chain such as systemd-resolved, dnsmasq, AdGuardHome, dae, or clash sits in front, rows may show only127.0.0.1. - Each recorder uses versioned tables under the same prefix:
qr_<safe_tag>_<fnv64hex>_v1_recordsqr_<safe_tag>_<fnv64hex>_v1_stepsqr_<safe_tag>_<fnv64hex>_v1_questionsqr_<safe_tag>_<fnv64hex>_v1_meta
recordscontains only the fixed schema fields for structured snapshots.stepsstoressequencepath events for path analysis and hit-rate reporting.questionsis a derived index table fromquestions_jsonfor faster qname/qtype queries.metarecords completed derived-table migrations.- Every recorder owns its own bounded queue, SQLite connection, writer thread, tail buffer, and SSE broadcaster.
- Recorders that share the same
pathcoordinate readers, writers, and maintenance by normalized database path; tables and in-memory state remain isolated by tag. - Periodic retention cleanup deletes expired rows, reclaims every completely free page, and truncates the WAL last. The database file shrinks only when deletion produces complete free pages; small deletions may instead leave reusable space inside live pages.
- If an existing database still uses
auto_vacuum=NONE, its first periodic or manual cleanup performs one fullVACUUMto migrate it toINCREMENTAL. This may temporarily pause recorder database reads and writes and require additional temporary disk space, but it does not block DNS request processing. - A maintenance failure does not stop the writer. Periodic cleanup retries on a later interval and structured logs report page counts, file sizes, and the failure stage.
Data Shape
questions_jsonis always a question array, for example:
[
{ "name": "www.example.com.", "qtype": "A", "qclass": "IN" }
]
answers_json,authorities_json,additionals_json, andsignature_jsonare RR arrays, for example:
[
{
"name": "www.example.com.",
"class": "IN",
"ttl": 300,
"rr_type": "A",
"payload_kind": "A",
"payload_text": "192.0.2.1",
"payload": { "ip": "192.0.2.1" }
}
]
req_edns_jsonandresp_edns_jsonare EDNS objects orNULL.- The
v1suffix in the table name is the schema version. Future upgrades add new versioned tables rather than altering the existing ones in place.
API
GET /plugins/<tag>/records- Returns record rows ordered by
created_at_msdescending. - Query parameters:
cursor=<created_at_ms>:<id>limit=<n>, default100, max500since_ms=<unix_ms>until_ms=<unix_ms>qname=<text>, substring match against request question namesclient_ip=<text>, substring match against the client IP stringqtype=<type>/rcode=<rcode>/status=all|error|has_response|no_response
- Returns record rows ordered by
GET /plugins/<tag>/records/<id>- Returns one full record plus
steps.
- Returns one full record plus
DELETE /plugins/<tag>/records- Clears all history rows and
stepsfor the current recorder, and clears the in-memory tail. - The operation first flushes the background writer queue, then deletes records in small auto-committed batches. It truncates the WAL after every deletion and page-reclamation batch, avoiding one transaction spanning the full clear operation. It returns
cleared_recordsfor the number of deleted main-table rows. - If history deletion succeeds but disk reclamation fails, the endpoint returns
query_recorder_clear_failedwith a message that history was already cleared; later maintenance can retry space reclamation.
- Clears all history rows and
GET /plugins/<tag>/stats/plugins- Returns hit stats grouped by
matcher / executor / builtin. - Supports
since_ms,until_ms,kind=matcher|executor|builtin|all, and the record filters.
- Returns hit stats grouped by
GET /plugins/<tag>/stats/top_clients/stats/top_qnames- Returns client IP or QNAME rankings.
- Supports
limit=<n>, default20; the backend no longer enforces a200cap. - Supports the same time range and filter parameters as records.
GET /plugins/<tag>/stats/qtype/stats/rcode- Returns QTYPE or RCODE distribution.
- Supports the same time range and filter parameters as records.
GET /plugins/<tag>/stats/latency- Returns latency summary values, histogram buckets, and slow-query rankings.
- Supports
slow_limit=<n>orlimit=<n>, default20; the backend no longer enforces a200cap.
GET /plugins/<tag>/stats/timeseries- Returns query trends aggregated by minute or hour.
- Supports
bucket=minute|hourandbuckets=<n>(default60, maximum720).
GET /plugins/<tag>/stream- Streams newly written records over SSE.
- Supports
tail=<n>to replay the in-memory tail. - Clients should send
Accept: text/event-streamand tolerate heartbeat frames, error events, empty payloads, and brief disconnects.
Typical Uses
- Persistent audit and troubleshooting trails
sequencepath analysis and plugin hit-rate reporting- Real-time query log feeds for dashboards or control planes
- Place the recorder close to the entry point when the full main-path trace is required.
- If an earlier branch short-circuits before the recorder, that request will not be recorded.
- If
nextfails and the server later emits a fallback response, the database still reflects the plugin's point of view:errorplus an empty response. - If the management API is disabled, the recorder still writes SQLite data but does not expose query or SSE routes.
- To preserve real client IPs, point clients directly at OxiDNS or configure a trusted
src_ip_headerfor HTTP/DoH reverse-proxy deployments.
metrics_collector
Purpose
Collects Prometheus metrics for query handling.
Example Configuration
- tag: metrics_main
type: metrics_collector
args:
# Collector label exported through /api/metrics
name: "main"
Configuration Details
name
- Type:
string; Required: no - Purpose: Metrics label namespace.
quick setup
- exec: "metrics_collector main"
Behavior
- Exposes query counters, inflight counts, and latency metrics through the management API.
API
GET /api/metrics- Prometheus text format. This is the single global endpoint, and built-in metrics from other plugins are exported through the same route.
Typical Uses
- Prometheus integration
- Observe multiple policy entry points separately
debug_print
Purpose
Prints a debug message.
Example Configuration
- tag: debug_main
type: debug_print
args:
# Log title; defaults to "debug print" when omitted
msg: "before forward"
Configuration Details
msg
- Type:
string; Required: yes - Purpose: Message content.
quick setup
- exec: "debug_print cache branch"
Typical Uses
- Temporary debugging
- Reading sequence branches during development
sleep
Purpose
Sleeps for a bounded duration inside the chain.
Example Configuration
- tag: sleep_100ms
type: sleep
args:
# Add 100 ms of async delay
duration: 100
Configuration Details
duration
- Type:
duration; Required: yes - Purpose: Sleep duration.
quick setup
- exec: "sleep 100"
Typical Uses
- Testing
- Timing experiments