Trying to centralize logs from services that do not log to syslog is an interesting journey.
In this post we will cover some basics of rsyslog and how to setup forwarding of pihole logs to a rsyslog server. Note that this covers pihole logs as of pihole 6.x. If you are using pihole 5.x these instructions most likely will not work for you, but hopefully can be adapted with tweaks.
Our goal is to forward the logs from pihole to rsyslog server and retain the timestamps of the original log files. We also want the syslog server to automatically pick up the new logs when pihole rotates its log files. Turns out these two conspire to be the trickiest parts of the problem.
Pihole logging
Pihole 6.x by default saves its logs to /var/log/pihole – specifically it saves them in /var/log/pihole/pihole.log which is for dnsmasq where it logs queries, results etc. and /var/log/pihole/FTL.log where it logs service logs for pihole. Note that you do get symlinks to the latest log files in /var/log/pihole.log and /var/log/pihole-FTL.log. However, if you use the symlinks, I found that rsyslog will not notice or correctly pick up log rotation even with the right flags set. So you will need to point rsyslog to the underlying files and not the symlinks.
rsyslog Basics
rsyslog is both a log sender and receiver. Here, we will cover the sender part as the receiver part is generally straight-forward. In our case, let’s say we want to configure rsyslog to send log files from a service (say pihole) which writes its logs to a log file.
You can configure specific filesets or services that rsyslog will handle through config files in /etc/rsyslog.d – so we create 10-pihole.conf for forwarding pihole logs to our rsyslog server.
rsyslog has some basic concepts that we need to understand in order to be able to make this work:
rsyslog modules – rsyslog uses modules for adding/enabling functionality. Some of the relevant modules for forwarding pihole logs are imfile which is input file module, mmnormalize which is a normalization module we will use for formatting and extracting fields.
rsyslog inputs – inputs specify the inputs that will be processed by rsyslog. In our case, since pihole logs are regular files in the system, these would be imfile inputs.
rsyslog rulesets – rsyslog can use rulesets to decide how to process inputs. When we setup a ruleset for an input, the input only gets processed by that ruleset and won’t do any of the additional processing configured for default inputs.
rsyslog templates – rsyslog templates are used to format the output sent from rsyslog.
rsyslog mmnormalize rulebase – mmnormalize is a module that you can use to parse out fields from the input. You can specify a rulebase for mmnormalize that parses the input lines into fields based on the rule and these parsed fields can be used in the output template for building the output line.
Testing mmnormalize Rules
One of the annoying things about setting this up is ensuring that your input parsing rules are right – if they aren’t you simply won’t see the output and be left figuring out whether the issue was in some other part of the configuration. There’s a simple technique for validating your mmnormalize rule – install the libnorm-utils package which will provide you with lognormalizer tool. Now you can copy an input log line, then run this command:
echo <your input line> | lognormalizer -r <your rulebase file>
If your rule is configured properly, you will see a fully parsed json output where each field is listed as you expect. If there’s any issue you will see an unparsed data field in the output.
Pihole DNS Logs mmnormalize Rules
Here’s an example log line in /var/log/pihole/pihole.log look like for Pihole 6+ as of the day of writing:
Sep 26 19:41:05 dnsmasq[2759]: cached <hostname> is <ip address>
We create the following mmnormalize rule in /etc/rsyslog.d/pihole-dns.rulebase to parse out fields:
rule=:%date:date-rfc3164% %program:char-to:[%[%pid:char-to:]%]: %msg:rest%
The rule is straightforward once you understand the arcane syntax – first field to parse is a date in rfc3164 format, and it is placed in the date field. Then we parse out dnsmasq as program field by parsing to the [ character. Then we have the [ itself, followed by the Process ID of dnsmasq parsed into the pid field by parsing up to ], the ]: itself and then the rest of the message. Note that any literal white spaces are expected to be present in the input.
Pihole FTL Logs mmnormalize Rules
Here’s an example of log line in /var/log/pihole/FTL.log for Pihole 6+ as of the day of writing:
2025-09-26 20:35:58.284 PDT [41680/T41684] INFO: Ending rate-limitation of <ip address>
2025-09-26 20:35:02.345 PDT [41680M] INFO: Rate-limiting <ip address> for at least 56 seconds
Note that these two lines have slightly different formats – the first one has a PID/TID and the second one has PIDM (likely indicating main thread) format. These are not the only ones by the way. But these seem to be the predominant ones.
Here’s the mmnormalize rulebase in /etc/rsyslog.d/pihole-ftl.rulebase to parse out fields:
rule=:%date:date-iso% %time:time-24hr%.%ms:number% %tz:word% [%pid:number%/T%tid:number%] %level:char-to::%: %msg:rest%
rule=:%date:date-iso% %time:time-24hr%.%ms:number% %tz:word% [%pid:number%M] %level:char-to::%: %msg:rest%
This handles both types of log lines above.
You may want to hold off on sending FTL logs based on the above templates. In my vanilla rsyslog receiver setup with the above templates, I noticed that the labels for hostname, process etc get messed up. For now, I have disabled FTL logs since they’re less interesting than the dns logs.
rsyslog configuration for forwarding pihole logs
Finally, we put it all together in a rsyslog configuration in /etc/rsyslog.d/10-pihole.conf file that sets up forwarding of pihole logs to the rsyslog server:
module(load="imfile")
module(load="mmnormalize")
# --- Pi-hole DNS log ---
input(type="imfile"
File="/var/log/pihole/pihole.log"
Tag="pihole-dns:"
Severity="info"
PersistStateInterval="100"
reopenOnTruncate="on"
readMode="2"
ruleset="rs_pihole_dns")
template(name="T_PiHoleDNS" type="string"
string="%$!date% %hostname% pihole-dns[%$!pid%]: %$!msg%\n")
ruleset(name="rs_pihole_dns") {
action(type="mmnormalize"
ruleBase="/etc/rsyslog.d/pihole-dns.rulebase")
# action(type="omfile" file="/tmp/pihole-debug.log" template="T_PiHoleDNS")
action(type="omfwd"
target="<rsyslog server ip address>"
port="<rsyslog server tcp port>"
protocol="tcp"
template="T_PiHoleDNS")
}
# --- Pi-hole FTL log ---
input(type="imfile"
File="/var/log/pihole/FTL.log"
Tag="pihole-ftl:"
Severity="info"
PersistStateInterval="100"
reopenOnTruncate="on"
readMode="2"
ruleset="rs_pihole_ftl")
template(name="T_PiHoleFTL" type="string"
string="%$!date%T%$!time%.%$!ms% %$!tz% %hostname% pihole-ftl[%$!pid%]: %$!level%: %$!msg%\n")
ruleset(name="rs_pihole_ftl") {
action(type="mmnormalize"
ruleBase="/etc/rsyslog.d/pihole-ftl.rulebase")
# action(type="omfile" file="/tmp/pihole-ftl-debug.log" template="T_PiHoleFTL")
action(type="omfwd"
target="<rsyslog server ip address>"
port="<rsyslog server tcp port>"
protocol="tcp"
template="T_PiHoleFTL")
}
The rs_pihole_dns ruleset parses the pihole.log log lines through mmnormalize using the rulebase mentioned above and outputs them using the T_PiHoleDNS template which pulls in the date, pid and msg fields from the mmlognormalize parsed input, hostname which is a parameter available for rsyslog templates and replaces programname with pihole-dns (so service/appname will be pihole-dns for these log lines).
Similarly the rs_pihole_ftl ruleset does something similar for FTL.log lines using the T_PiHoleFTL template.
The commented out omfile actions allow you to see the output lines in the /tmp folder for debugging if necessary.
The only other two things of note are, instead of the symlink for pihole.log and pihole-FTL.log, we refer to the actual underlying files. Without this, log traffic tends to stop when log files are rotated. Similarly, we need the reopenOnTruncate=”on” for the same reason for both log files – as the logrotate for pihole does CopyTruncate, we need this flag in rsyslog handling.