Add opt-in JSON logging with structured DNS query fields (#8553)

* plugin/pkg/log: add opt-in JSON logging backend

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>

* plugin/log: emit typed query records in JSON mode

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>

* coremain: expose process-wide JSON logging

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>

---------

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
houyuwushang
2026-09-17 08:50:39 +08:00
committed by GitHub
parent 84a93a0b89
commit b93e449b2f
20 changed files with 1064 additions and 22 deletions

View File

@@ -88,12 +88,51 @@ The default Common Log Format is:
`{remote}:{port} - {>id} "{type} {class} {name} {proto} {size} {>do} {>bufsize}" {rcode} {>rflags} {rsize} {duration}`
~~~
Each of these logs will be outputted with `log.Infof`, so a typical example looks like this:
In the default text mode, each of these logs is output with `log.Info`, so a typical example looks like this:
~~~ txt
[INFO] [::1]:50759 - 29008 "A IN example.org. udp 41 false 4096" NOERROR qr,rd,ra,ad 68 0.037990251s
~~~
## JSON Output
Start CoreDNS with `-log-format=json` to select JSON output for the entire process.
This is a command-line flag, not a Corefile directive. `-log-format=text` is the default.
The `log` plugin's name and response-class filters work identically in both modes.
Each query produces one JSON record with common fields `time`, `level`, `msg`, and
`plugin` (always `log` for query records), plus these typed fields:
| Field | Type | Meaning |
| --- | --- | --- |
| `client_ip` | string | Client address, without brackets around IPv6 addresses |
| `client_port` | number | Client port |
| `qname` | string | Lowercase, fully qualified query name, in DNS presentation format |
| `qtype`, `qclass` | string | Query type and class, including numeric forms for unknown values |
| `protocol` | string | `udp` or `tcp`, as for `{proto}` |
| `id`, `opcode` | number | Query ID and opcode |
| `request_size` | number | Request size in bytes, as for `{size}` |
| `dnssec_ok` | boolean | Query's DNSSEC OK bit |
| `bufsize` | number | Effective response buffer size, as for `{>bufsize}` |
| `rcode` | string or null | Response RCODE, or null if no DNS response was recorded |
| `response_size` | number | Recorded response size in bytes, as for `{rsize}` |
| `duration_seconds` | number | Elapsed handling time in seconds |
As in text mode, response sizes describe recorded, uncompressed messages, not
necessarily the bytes delivered to the client. Deferred errors (such as SERVFAIL)
use the response CoreDNS will generate after the plugin chain returns. A dropped
request has `rcode: null`; it is not logged as a successful response. Raw `Write`
calls contribute to the size but do not provide a decoded response RCODE.
`FORMAT` still controls `msg`, including custom formats and metadata placeholders.
It does not replace the JSON schema or define new top-level fields. The DNS fields
come directly from the request and response, not from parsing `msg`. For example,
with `log . "{name} {rcode}"`:
```json
{"time":"2026-09-15T08:00:00Z","level":"INFO","msg":"example.org. NOERROR","plugin":"log","client_ip":"127.0.0.1","client_port":40212,"qname":"example.org.","qtype":"A","qclass":"IN","protocol":"udp","id":42,"opcode":0,"request_size":29,"dnssec_ok":false,"bufsize":512,"rcode":"NOERROR","response_size":29,"duration_seconds":0.001}
```
## Additional metadata
The log plugin adds the following metadata to allow for granular differentiation of NOERROR denial vs success messages. These are mapped from `plugin/pkg/response/classify.go` and `plugin/pkg/response/typify.go`.