3 min read

Monitor and Visualize PiHole stats using Python, InfluxDB and Grafana

Monitor and Visualize PiHole stats using Python, InfluxDB and Grafana
Photo by KOBU Agency / Unsplash

Visualizing PiHole stats allows you to view, graph and alert on various PiHole metrics.

In this example we are looking into how to run gather and visualize the PiHole data using a Python wrapper, InfluxDB 2.x and Grafana.

Prerequisites

I'm running all the software on Ubuntu 20.04 server and Python 3.10.2. Lesser versions should work also, though Python < 3 has not been tested (and should not be used anymore anyways).

  • PiHole up-and-running
  • InfluxDB 2.x installed (1.x will work also but the Python modules and syntax is different, so copy paste from this example does not work)
  • Grafana installed

Test the PiHole API via browser or curl

Verify the API output(s) via browser or curl. You can modify the scripts based on the outputs to gather the data you want. I'm using "jq" piped below to pretty print the output JSON.

# Get the data for 10 minute sets over last 24 hours
cli@automate:~/pihole-stats# curl http://pihole/admin/api.php?overTimeData10mins
{
  "domains_over_time": {
    "1644771900": 435,
    "1644772500": 415,
    "1644773100": 185,
    [...]
  },
  "ads_over_time": {
    "1644771900": 46,
    "1644772500": 55,
    "1644773100": 9,
    [...]
  }
}

# Get the basic API data
cli@automate:~/pihole-stats# curl http://pihole/admin/api.php
{
  "domains_being_blocked": 102634,
  "dns_queries_today": 36719,
  "ads_blocked_today": 3961,
  "ads_percentage_today": 10.787331,
  "unique_domains": 8011,
  "queries_forwarded": 18496,
  "queries_cached": 14212,
  "clients_ever_seen": 30,
  "unique_clients": 26,
  "dns_queries_all_types": 36711,
  "reply_NODATA": 5470,
  "reply_NXDOMAIN": 1734,
  "reply_CNAME": 9184,
  "reply_IP": 20022,
  "privacy_level": 0,
  "status": "enabled",
  "gravity_last_updated": {
    "file_exists": true,
    "absolute": 1644719283,
    "relative": {
      "days": 1,
      "hours": 13,
      "minutes": 54
    }
  }
}

Get PiHole stats using Python

This Python script will fetch the PiHole API output, parse the JSON-formatted result and save it to remote InfluxDB 2.x server.

Note: You can of course modify the script to save the data anywhere you like or for example perform data calculations and such before saving the data.
#! /usr/bin/python
import requests
import time
from influxdb_client import InfluxDBClient, Point, WritePrecision
from influxdb_client.client.write_api import SYNCHRONOUS

# Send to InfluxDB function
def sendToInfluxdb(dns_last, block_last, HOSTNAME, INFLUXDB_SERVER, INFLUXDB_ORG, INFLUXDB_TOKEN, INFLUXDB_BUCKET):
    json_body = [
        {
        "measurement": "piholestats." + HOSTNAME,
        "tags": {
            "host": HOSTNAME
        },
        "fields": {
            "dns_queries": int(dns_last),
            "ads_blocked": int(block_last)
        }
        }
    ]
    client = InfluxDBClient(url=INFLUXDB_SERVER, token=INFLUXDB_TOKEN, org=INFLUXDB_ORG)
    write_api = client.write_api(write_options=SYNCHRONOUS)
    write_api.write(bucket=INFLUXDB_BUCKET, org=INFLUXDB_ORG, record=json_body)

def main():
  HOSTNAME = "pihole" # PiHole hostname to report in InfluxDB
  PIHOLE_API = "http://pihole/admin/api.php?overTimeData10mins" # PiHole API url
  INFLUXDB_SERVER = "http://influxdb:8086" # InfluxDB server
  INFLUXDB_ORG = "ORG" # InfluxDB Organization
  INFLUXDB_TOKEN = "TOKEN" # InfluxDB Token
  INFLUXDB_BUCKET = "pihole"  # InfluxDB Bucket

  # Get the API output
  api = requests.get(PIHOLE_API)
  API_out = api.json()

  # Save outputs from API query into variables
  domains_last10min = (API_out['domains_over_time'])
  blocked_last10min = (API_out['ads_over_time'])
  dns_last = domains_last10min[list(domains_last10min)[-2]]
  block_last = blocked_last10min[list(blocked_last10min)[-2]]

  # Call the send to InfluxDB function
  sendToInfluxdb(dns_last, block_last, HOSTNAME, INFLUXDB_SERVER, INFLUXDB_ORG, INFLUXDB_TOKEN, INFLUXDB_BUCKET)

if __name__ == "__main__":
  main()

Automate the Python script in Cron

The Python script above can be automated to run via Cron for example every 10 minutes (in the output below).

*/10 * * * * python3 /automate/pihole_stats/influxv2_piholestats.py >/dev/null 2>&1

Verify data input in InfluxDB browser

Browse through the InfluxDB Data Explorer to verify that the data is coming to the proper Bucket and is getting written correctly.

InfluxDB Data Explorer
Verify data in InfluxDB browser

Visualize data in Grafana

Below are three example graphs on how to visualize the data.

PiHole Query Time-series Graph
First example
// PiHole - DNS Query statistics (Time series graph)
from(bucket: "pihole")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "piholestats.pihole")
  |> filter(fn: (r) => r["_field"] == "ads_blocked" or r["_field"] == "dns_queries")
  |> yield(name: "last")
PiHole Query Heatmap
Second example
// PiHole - DNS Query statistics (Hourly heatmap)
from(bucket: "pihole")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "piholestats.pihole")
  |> filter(fn: (r) => r["_field"] == "dns_queries")
  |> aggregateWindow(every: 10m, fn: last, createEmpty: false)
  |> yield(name: "last")
// PiHole - DNS Query statistics (Hourly blocked ads)
from(bucket: "pihole")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "piholestats.pihole")
  |> filter(fn: (r) => r["_field"] == "ads_blocked")
  |> aggregateWindow(every: 10m, fn: last, createEmpty: false)
  |> yield(name: "last")

Conclusion

This short guide should allow you to visualize PiHole data outputs easily. You can modify the output and input as you see fit, and of course gather more detailed data via the API.