Skip to content

Use structed json logging#2036

Draft
yubiuser wants to merge 4 commits into
developmentfrom
log_json
Draft

Use structed json logging#2036
yubiuser wants to merge 4 commits into
developmentfrom
log_json

Conversation

@yubiuser

Copy link
Copy Markdown
Member

State of the art logging for docker is structured json logging. This allows aggregation, analysis and filtering. Currently, we use plain shell output that is optimized for human consumption. This PR changes the output to structured json which can be easily parsed and consumed by other tools.

This kind of output allows separation of the logging of the processes running on the container simultaneously: start.sh, FTL, crond,...

This is a draft. Before I go on and parse the FTL output (or maybe adjust this in FTL repo), I'd like to know the opinion of the other team members. Example output

pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Setting up user & group for the pihole user"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"PIHOLE_UID not set in environment, using default (1000)"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"PIHOLE_GID not set in environment, using default (1000)"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Starting FTL configuration"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Assigning password defined by Environment Variable"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Starting crond for scheduled scripts. Randomizing times for gravity and update checker"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Ensuring logrotate script exists in /etc/pihole"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Gravity migration checks"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Existing gravity database found - schema will be upgraded if necessary"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Gravity DB migration output: "}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"pihole-FTL pre-start checks"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Setting capabilities on pihole-FTL where possible"}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":{"applied_capabilities":["CAP_CHOWN","CAP_NET_BIND_SERVICE","CAP_NET_RAW","CAP_SYS_TIME"]}}
pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Starting pihole-FTL as user pihole"}
pihole  | {"timestamp":"20260516T155606","log_level":"INFO","service":"pihole-docker","message":{"core":{"version":"9648b90","branch":"development","hash":"9648b901"},"web":{"version":"7e0949c","branch":"development","hash":"7e0949c2"},"ftl":{"version":"vDev-230eb78","branch":"development","hash":"230eb78e","date":"2026-05-16 12:31:13 +0200"}}}
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: ########## FTL started on 0b7a96562b8f! ##########
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL branch: development
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL version: vDev-230eb78
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL commit: 230eb78e
[...]
pihole  | {"timestamp":"20260516T160218","log_level":"INFO","service":"pihole-docker","message":"Container stop requested..."}
pihole  | {"timestamp":"20260516T160218","log_level":"INFO","service":"pihole-docker","message":"pihole-FTL is running - Attempting to shut it down cleanly"}
pihole  | 
pihole  | 2026-05-16 16:02:18.433 UTC [125M] INFO: Shutting down (exit code 0, jmpret 0)
pihole  | 2026-05-16 16:02:18.443 UTC [125/T127] INFO: Terminating database thread
pihole  | 2026-05-16 16:02:18.479 UTC [125/T130] INFO: Terminating timer thread
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Finished final database update
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Asked to terminate by "N/A" (PID 343, user root UID 0)
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Waiting for threads to join
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Thread housekeeper (1) is idle, terminating it.
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Thread dns-client (2) is idle, terminating it.
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Thread ntp-client (4) is idle, terminating it.
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: Thread webserver (7) is idle, terminating it.
pihole  | 2026-05-16 16:02:18.684 UTC [125M] INFO: All threads joined
pihole  | 2026-05-16 16:02:18.685 UTC [125M] INFO: PID file emptied
pihole  | 2026-05-16 16:02:18.685 UTC [125M] INFO: Stored 0 API sessions in the database
pihole  | 2026-05-16 16:02:19.480 UTC [125M] INFO: Terminated by "N/A" (PID 343, user root UID 0)
pihole  | 2026-05-16 16:02:19.480 UTC [125M] INFO: ########## FTL terminated after 6m 15s  (code 0)! ##########
pihole  | {"timestamp":"20260516T160221","log_level":"INFO","service":"pihole-docker","message":"pihole-FTL exited with status 0"}
pihole  | {"timestamp":"20260516T160221","log_level":"INFO","service":"pihole-docker","message":"Container will now stop or restart depending on your restart policy - https://docs.docker.com/engine/containers/start-containers-automatically/#use-a-restart-policy"}

@darkexplosiveqwx

Copy link
Copy Markdown

Having structured logging sounds very interesting.
Seeing this reminds me of a feature request on improving journald logging.
I think the JSON logs for docker and journald improvements belong in FTL and may be controlled via a startup flag or pihole.toml, I'd be leaning on a flag, this way the systemd.unit and docker can enable it automatically.
The "service": field could allow the writing of all three FTL.log, pihole.log and webserver.log to the docker log without it getting messy. In journald, this would be achived with Special journal fields. Note that for fields logging to stdout would not suffice, and either sd_journal_send from libsystemd or sending to to /run/systemd/journal/socket would be needed.

@rdwebdesign

Copy link
Copy Markdown
Member

Personally I don't like mixing JSON and plain text output on the same file.
It makes very hard to parse the messages:

pihole  | {"timestamp":"20260516T155604","log_level":"INFO","service":"pihole-docker","message":"Starting pihole-FTL as user pihole"}
pihole  | {"timestamp":"20260516T155606","log_level":"INFO","service":"pihole-docker","message":{"core":{"version":"9648b90","branch":"development","hash":"9648b901"},"web":{"version":"7e0949c","branch":"development","hash":"7e0949c2"},"ftl":{"version":"vDev-230eb78","branch":"development","hash":"230eb78e","date":"2026-05-16 12:31:13 +0200"}}}
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: ########## FTL started on 0b7a96562b8f! ##########
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL branch: development
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL version: vDev-230eb78
pihole  | 2026-05-16 15:56:04.368 UTC [125M] INFO: FTL commit: 230eb78e
[...]
pihole  | {"timestamp":"20260516T160218","log_level":"INFO","service":"pihole-docker","message":"Container stop requested..."}
pihole  | {"timestamp":"20260516T160218","log_level":"INFO","service":"pihole-docker","message":"pihole-FTL is running - Attempting to shut it down cleanly"}
pihole  | 
pihole  | 2026-05-16 16:02:18.433 UTC [125M] INFO: Shutting down (exit code 0, jmpret 0)

Maybe we need a FTL option to choose between JSON and plain text logs, so users can choose the output format for all log files.

@rdwebdesign

rdwebdesign commented May 17, 2026

Copy link
Copy Markdown
Member

My previous comment was a suggestion to add an option to FTL, so users could choose between plain text or JSON log formats.

Since this PR is only about docker logging, did you try to play with the the json-file driver?

Examples
docker run --log-driver json-file ...

or

services:
  pihole:
    (...)
    logging:
      driver: "json-file"

Actually, docker captures stdout and stderr, but it stores the messages internally using JSON format by default:

As a default, Docker uses the json-file logging driver, which caches container logs as JSON internally.

I'm not sure if we should change the output here.

Just try:

cat $(docker inspect --format='{{.LogPath}}' <container_name_or_ID>) | jq

@yubiuser

Copy link
Copy Markdown
Member Author

Personally I don't like mixing JSON and plain text output on the same file.
It makes very hard to parse the messages:

Me neither, this is not the goal of this PR. I'd also like to change the FTL output, but before I start working on this I wanted to get some feedback on the whole idea. Having a flag or option in the toml sounds like a good idea and also include pihole.log and webserver.log.

@yubiuser

Copy link
Copy Markdown
Member Author

I'm not sure if we should change the output here.
Just try:
cat $(docker inspect --format='{{.LogPath}}' <container_name_or_ID>) | jq

{
  "log": "2026-05-17 05:48:59.047 UTC [124/T126] INFO: NTP round-trip delay: 2.333260e+01 ms (excluded 1 outliers)\n",
  "stream": "stdout",
  "time": "2026-05-17T05:48:59.778056279Z"
}
{
  "log": "2026-05-17 05:48:59.048 UTC [124/T225] INFO: NTP server listening on 0.0.0.0:123 (IPv4)\n",
  "stream": "stdout",
  "time": "2026-05-17T05:48:59.778059745Z"
}
{
  "log": "2026-05-17 05:48:59.048 UTC [124/T226] INFO: NTP server listening on :::123 (IPv6)\n",
  "stream": "stdout",
  "time": "2026-05-17T05:48:59.778062832Z"
}

This is json, but not as structured as I think it could be. The timestamp is duplicated, there is no distinction between each 'service' (only 'stream') and no severity level.

My main reason to bring this up is that the current logging is flooded with the ntp info, but there is no easy way to filter this for errors.

2026-05-17_08-43

@rdwebdesign

Copy link
Copy Markdown
Member

My main reason to bring this up is that the current logging is flooded with the ntp info, but there is no easy way to filter this for errors.

OK. In this case, I don't think we should change this on the docker repository.

The best approach to achieve this goal would be to change how FTL prints the log messages, including an option to generate logs using JSON output.

@yubiuser

Copy link
Copy Markdown
Member Author

We need both. Otherwise the log of our docker commands incl. crond would still be plain bash output

@darkexplosiveqwx

Copy link
Copy Markdown

I have hacked together a quick prototype of JSON logging in FTL.
Some example logs (debug.all is enabled):

$ sudo -u pihole pihole-FTL -f --log-json
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"########## FTL started on frodo-blue! ##########"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"FTL branch: log-json"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"FTL version: v6.6.2-210-gc92e52c8-dirty"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"FTL commit: c92e52c8-dirty"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"FTL date: 2026-05-15 20:36:39 +0200"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"FTL user: pihole"}
{"timestamp":"2026-05-20T22:52:37.240+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"Compiled for x86_64 (compiled locally) using gcc (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)"}
{"timestamp":"2026-05-20T22:52:37.241+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"Reading default TOML config file"}
{"timestamp":"2026-05-20T22:52:37.242+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"dns.revServer does not exist - nothing to migrate"}
{"timestamp":"2026-05-20T22:52:37.242+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"dns.domain does not exist - nothing to migrate"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Calling nlgetmsg(type = 26)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Calling parse_nl_msg (len = 116)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Parsing Netlink message (type 24, len 116/116, flags 0x2, sqe 0)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Parsing IPv6 route: table 255 is ff00:: via direct"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Returning next nl_msg_type: 0<unknown>, remaining len: 0/0"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Calling nlgetmsg(type = 26)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Calling parse_nl_msg (len = 20)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"Returning next nl_msg_type: 3 (NLMSG_DONE), remaining len: 20/20"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_NETLINK","service":"pihole-FTL","component":"FTL","message":"netlink done: Success (0)"}
{"timestamp":"2026-05-20T22:52:37.247+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"Testing /etc/pihole/dnsmasq.conf.temp"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"Files /etc/pihole/dnsmasq.conf.temp and /etc/pihole/dnsmasq.conf are identical (skipped the first 24 lines)"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"dnsmasq.conf unchanged"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"Opening /etc/pihole/custom.list.tmp for writing"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"Files /etc/pihole/custom.list.tmp and /etc/pihole/hosts/custom.list are identical (skipped the first 24 lines)"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"DEBUG_CONFIG","service":"pihole-FTL","component":"FTL","message":"custom.list unchanged"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"Parsed config file /etc/pihole/pihole.toml successfully"}
{"timestamp":"2026-05-20T22:52:37.252+02:00","log_level":"WARNING","service":"pihole-FTL","component":"FTL","message":"Unable to read PID from file: No such file or directory"}
{"timestamp":"2026-05-20T22:52:37.253+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"PID file does not exist or not readable"}
{"timestamp":"2026-05-20T22:52:37.253+02:00","log_level":"INFO","service":"pihole-FTL","component":"FTL","message":"No other running FTL process found."}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"* CAP_MKNOD                (27) = --- *"}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"* CAP_LEASE                (28) = --- *"}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"* CAP_AUDIT_WRITE          (29) = --- *"}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"* CAP_AUDIT_CONTROL        (30) = --- *"}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"* CAP_SETFCAP              (31) = --- *"}
{"timestamp":"2026-05-20T22:52:37.266+02:00","log_level":"DEBUG_CAPS","service":"pihole-FTL","component":"FTL","message":"***************************************"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"INFO","service":"pihole-FTL","component":"webserver","message":"Initializing HTTP server on ports \"+8086\""}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[document_root] -> [/usr/share/pihole/www] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[error_pages] -> [/usr/share/pihole/www/admin/] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[listening_ports] -> [+8086] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[decode_url] -> [yes] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[enable_directory_listing] -> [no] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[num_threads] -> [50] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[authentication_domain] -> [pi.hole] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[additional_header] -> [X-DNS-Prefetch-Control: off\r\nContent-Security-Policy: default-src 'none'; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; img-src 'self'; manifest-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; for"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[index_files] -> [index.html,index.htm,index.lp] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[enable_keep_alive] -> [yes] (src/webserver/civetweb/civetweb.c:21420)"}
{"timestamp":"2026-05-20T22:52:37.324+02:00","log_level":"DEBUG","service":"pihole-FTL","component":"webserver","message":"[keep_alive_timeout_ms] -> [5000] (src/webserver/civetweb/civetweb.c:21420)"}

Escaping is done automatically, as I made use of the already embedded cJSON.
The regular logfiles are not affected, this is only for stdout.

This includes currently only the FTL and webserver logs, the dnsmasq log isn't exposed that nicely and I didn't go that deeply just yet.
I went with the long-form ISO 8601 timestamp, while yubiuser chose a short-form timestamp, this should be the same on later on.
The FTL and webserver logs also log the PID, that might be useful to add as well

Should I open a draft PR on the FTL repo where we can discuss further details and look at my bad code?

@yubiuser

Copy link
Copy Markdown
Member Author

This is great! Thanks for contributing. Yes, please open a draft PR at FTL repo.

I went with the long-form ISO 8601 timestamp, while yubiuser chose a short-form timestamp, this should be the same on later on.

This can be changes, I just took the short form as an example.

@rdwebdesign

Copy link
Copy Markdown
Member

The FTL and webserver logs also log the PID, that might be useful to add as well

Yes.
We shouldn't lose this information when using a different log format.

The current log shows the ID string as [124/T226]. We could show it as "pid":"124/T226".

@darkexplosiveqwx

darkexplosiveqwx commented May 21, 2026

Copy link
Copy Markdown

I have submitted pi-hole/FTL#2897

The current log shows the ID string as [124/T226]. We could show it as "pid":"124/T226".

done.

I went with the long-form ISO 8601 timestamp, while yubiuser chose a short-form timestamp, this should be the same on later on.

This can be changes, I just took the short form as an example.

I just noted that it currently mismatches and we need a single format later on, but thats a minor issue.

Some problems regarding this:
I want to keep the logfile format as-is and only log JSON to stdout (there is a dnsmasq issue here, see the PR), but docker doesnt capture stdout, but instead tails FTL.log, how should that be worked out?

@rdwebdesign

Copy link
Copy Markdown
Member

docker doesnt capture stdout, but instead tails FTL.log,

As far as I now docker always captures the container stdout and stderr.

The issue here is that FTL only prints the messages to the file (nothing is sent to stdout or stderr). Then, the container tails FTL.log and this the output that docker actually sees.

@darkexplosiveqwx

Copy link
Copy Markdown

docker doesnt capture stdout, but instead tails FTL.log,

As far as I now docker always captures the container stdout and stderr.

The issue here is that FTL only prints the messages to the file (nothing is sent to stdout or stderr). Then, the container tails FTL.log and this the output that docker actually sees.

Yes, what I meant is that the stdout/stderr of pihole-FTL isn't captured, but that FTL.log is tailed instead to show FTL's logs.

@github-actions

Copy link
Copy Markdown

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Signed-off-by: yubiuser <github@yubiuser.dev>
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Conflicts have been resolved.

yubiuser added 2 commits June 2, 2026 20:51
Signed-off-by: yubiuser <github@yubiuser.dev>
Signed-off-by: yubiuser <github@yubiuser.dev>
@darkexplosiveqwx

Copy link
Copy Markdown

For FTL's JSON logs we need to read its stdout directly, as I opted not to change FTL.log to JSON.

A quick idea of mine:

diff --git a/src/Dockerfile b/src/Dockerfile
index 3797d90..b8b0a86 100644
--- a/src/Dockerfile
+++ b/src/Dockerfile
@@ -18,7 +18,7 @@ ARG PIHOLE_UID=1000
 ARG PIHOLE_GID=1000
 
 ENV DNSMASQ_USER=pihole
-ENV FTL_CMD=no-daemon
+ENV FTL_CMD="no-daemon --log-json"
 
 RUN apk add --no-cache \
     bash \
diff --git a/src/start.sh b/src/start.sh
index 46e698b..b47d03b 100644
--- a/src/start.sh
+++ b/src/start.sh
@@ -66,11 +66,10 @@ start() {
 
     __log "INFO" "pihole-docker" "Starting pihole-FTL as user ${DNSMASQ_USER}"
 
-    capsh --user="${DNSMASQ_USER}" --keep=1 -- -c "/usr/bin/pihole-FTL $FTL_CMD >/dev/null" &
+    capsh --user="${DNSMASQ_USER}" --keep=1 -- -c "/usr/bin/pihole-FTL $FTL_CMD" &
     # Notes on above:
     # - DNSMASQ_USER default of pihole is in Dockerfile & can be overwritten by runtime container env
-    # - /var/log/pihole/pihole*.log has FTL's output that no-daemon would normally print in FG too
-    #   prevent duplicating it in docker logs by sending to dev null
+    # - "--log-json" already writes full structured JSON, we can capture it directly
 
     # We need the PID of the capsh process so that we can wait for it to finish
     CAPSH_PID=$!
@@ -92,13 +91,6 @@ start() {
     }')
     __log "INFO" "pihole-docker" "$versionJson"
 
-    if [ "${TAIL_FTL_LOG:-1}" -eq 1 ]; then
-        # Start tailing the FTL log file from the EOF position we recorded on container start�����������������������������������������������������������������������������������������������������-        tail -F -c +$((startFrom + 1)) -- "${FTLlogFile}" &
-    else
-        __log "INFO" "pihole-docker" "FTL log output is disabled. Remove the Environment variable TAIL_FTL_LOG, or set it to 1 to enable FTL log output."
-    fi��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-
     # Wait for the capsh process (which spawned FTL) to finish
     wait $CAPSH_PID
     FTL_EXIT_CODE=$?

This however would remove the support for TAIL_FTL_LOG (add a conditional >/dev/null?), but we are changing the log output in a breaking manner anyway…

I am not that familiar with docker development (only run a few containers), so I don’t even know whether this approach work.
I also couldn’t do any testing yet, as the docker container doesn’t want my glibc binary. STATIC=true ./build.sh fails and I didn’t quite understand how to use ftl-build.

@yubiuser

yubiuser commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Oh, look at that

pihole  | {"timestamp":"2026-06-03 12:00:46.443 UTC","log_level":"INFO","service":"pihole-docker","message":"Existing gravity database found - schema will be upgraded if necessary"}
pihole  | {"timestamp":"2026-06-03 12:00:46.456 UTC","log_level":"INFO","service":"pihole-docker","message":"Gravity DB migration output: "}
pihole  | {"timestamp":"2026-06-03 12:00:46.464 UTC","log_level":"INFO","service":"pihole-docker","message":"pihole-FTL pre-start checks"}
pihole  | {"timestamp":"2026-06-03 12:00:46.474 UTC","log_level":"INFO","service":"pihole-docker","message":"Setting capabilities on pihole-FTL where possible"}
pihole  | {"timestamp":"2026-06-03 12:00:46.491 UTC","log_level":"INFO","service":"pihole-docker","message":{"applied_capabilities":["CAP_CHOWN","CAP_NET_BIND_SERVICE","CAP_NET_RAW","CAP_SYS_TIME"]}}
pihole  | stat: cannot statx '/var/log/pihole/FTL.log': No such file or directory
pihole  | {"timestamp":"2026-06-03 12:00:46.535 UTC","log_level":"INFO","service":"pihole-docker","message":"Starting pihole-FTL as user pihole"}
pihole  | 2026-06-03 12:00:46.541 UTC [127M] INFO: ########## FTL started on f6ca1bc8a53f! ##########
pihole  | {"timestamp":"2026-06-03T12:00:46.541Z","log_level":"INFO","service":"pihole-FTL","component":"FTL","pid":"127M","message":"########## FTL started on f6ca1bc8a53f! ##########"}
pihole  | 2026-06-03 12:00:46.541 UTC [127M] INFO: FTL branch: log-json
pihole  | {"timestamp":"2026-06-03T12:00:46.541Z","log_level":"INFO","service":"pihole-FTL","component":"FTL","pid":"127M","message":"FTL branch: log-json"}
pihole  | 2026-06-03 12:00:46.541 UTC [127M] INFO: FTL version: v6.6.2-228-g0c6ea364-dirty
pihole  | {"timestamp":"2026-06-03T12:00:46.541Z","log_level":"INFO","service":"pihole-FTL","component":"FTL","pid":"127M","message":"FTL version: v6.6.2-228-g0c6ea364-dirty"}
pihole  | 2026-06-03 12:00:46.541 UTC [127M] INFO: FTL commit: 0c6ea364-dirty
pihole  | {"timestamp":"2026-06-03T12:00:46.541Z","log_level":"INFO","service":"pihole-FTL","component":"FTL","pid":"127M","message":"FTL commit: 0c6ea364-dirty"}

I compiled FTL in the dev container locally and copied the binary into /src of the docker repo and build the image with ./build.sh -l -t pihole:log_json which will use this local FTL binary.

@yubiuser

yubiuser commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

This however would remove the support for TAIL_FTL_LOG (add a conditional >/dev/null?), but we are changing the log output in a breaking manner anyway…

I think it would be OK to remove this option. I'm not sure how many users disabled the option - and why would I don't want any log output of the container's main service?

Signed-off-by: yubiuser <github@yubiuser.dev>
@casperklein

Copy link
Copy Markdown
Contributor

Will there be an option to turn structured logging off or will this completely replace the current more human readable output?

I understand the reason for that change, but I prefer the current state for my personal usage.

@rdwebdesign

Copy link
Copy Markdown
Member

I agree with casperklein.

I don't think JSON output will be the best format for every use case.
We should have an option to choose between the 2 log formats, even in docker containers.

@PromoFaux

Copy link
Copy Markdown
Member

I'm not against this as a concept, but it should be opt-in via an env var (which also sets the FTL logging mode to be JSON too, if that's the direction), and not the default.

JSON is great for consumption by machine, but it's not so easy for a human to quickly parse

@darkexplosiveqwx

Copy link
Copy Markdown

Even with a var like JSON_LOGS=true/false, it can still make sense to drop TAIL_FTL_LOG, or at least fold it into something like LOG_MODE=JSON/PLAIN/NONE.

I went with the long-form ISO 8601 timestamp, while yubiuser chose a short-form timestamp, this should be the same on later on.

This can be changes, I just took the short form as an example.

Especially for machine consumption, we should go with ISO 8601, or more specifically RFC 3339, ("YYYY-MM-DDTHH:MM:SS.mmm"+"Z"/"±hh:mm") the question that remains is if we want to normalize the timestamp to UTC (and use "Z") or if we want to keep the information of the local timezone (and use "±hh:mm").

@rdwebdesign

rdwebdesign commented Jun 3, 2026

Copy link
Copy Markdown
Member

Note:

Currently, TAIL_FTL_LOG only changes if FTL log lines are added (or not) to the docker logs.
The other lines generated at the container startup (using echo "[i] ...") are always logged. Your suggestion above would change this behavior (not an issue, but we must be aware of this change).

We can use a variable (LOG_MODE or a different name) to set the log format or no logs at all, but I just think the plain text format must be the default. JSON log or disabled log should be optional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants