If you are using Pi-Hole you may find this script useful
Currently this is designed to run on the server that is running Pi-Hole but as I have more than one I want to monitor I will soon have an update for additional.
First make sure you update your /var/www/html/admin/scripts/pi-hole/php/data.php
Look at getSummaryData function and include Uptime function
<?php
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
$log = array();
$setupVars = parse_ini_file("/etc/pihole/setupVars.conf");
$hosts = file_exists("/etc/hosts") ? file("/etc/hosts") : array();
// Check if pihole.log exists and is readable
$logListName = checkfile("/var/log/pihole.log");
$log = new \SplFileObject($logListName);
// Check if preEventHorizon exists and is readable
$gravityListName = checkfile("/etc/pihole/list.preEventHorizon");
$gravity = new \SplFileObject($gravityListName);
// whitelist.txt is optional and might not be there
$whiteListFile = checkfile("/etc/pihole/whitelist.txt");
$whitelist = new \SplFileObject($whiteListFile);
// blacklist.txt is optional and might not be there
$blackListFile = checkfile("/etc/pihole/blacklist.txt");
$blacklist = new \SplFileObject($blackListFile);
if(isset($setupVars["API_PRIVACY_MODE"]))
{
$privacyMode = $setupVars["API_PRIVACY_MODE"];
}
else
{
$privacyMode = false;
}
// Check if time zone is set
// https://github.com/pi-hole/AdminLTE/pull/394
if (!date_default_timezone_get("date.timezone")) {
date_default_timezone_set("UTC");
}
/******* Public Members ********/
function getSummaryData() {
$server_up_time = Uptime();
$domains_being_blocked = gravityCount();
$dns_queries_today = countDnsQueries();
$ads_blocked_today = countBlockedQueries();
$ads_percentage_today = $dns_queries_today > 0 ? ($ads_blocked_today / $dns_queries_today * 100) : 0;
return array(
'domains_being_blocked' => $domains_being_blocked,
'dns_queries_today' => $dns_queries_today,
'ads_blocked_today' => $ads_blocked_today,
'ads_percentage_today' => $ads_percentage_today,
);
}
function Uptime() {
$str = @file_get_contents('/proc/uptime');
$num = floatval($str);
$secs = $num % 60;
$num = (int)($num / 60);
$mins = $num % 60;
$num = (int)($num / 60);
$hours = $num % 24;
$num = (int)($num / 24);
$days = $num;
return $days . " days, " . $hours . " hours, " . $mins . " minutes, " . $secs . " seconds";
}
function getOverTimeData() {
global $log;
// Get log lines
$dns_queries = getDnsQueries($log);
// Get list of ad domains
$gravity_domains = getGravity();
// Bin log entries separated into Domains and Ads in 1 hour intervals
list($domains_over_time, $ads_over_time) = overTime($dns_queries, $gravity_domains);
// Align arrays
alignTimeArrays($ads_over_time, $domains_over_time);
// Provide a minimal valid array if there have are no blocked
// queries at all. Otherwise the output of the API is inconsistent.
if(count($ads_over_time) == 0)
{
$ads_over_time = [1 => 0];
}
return Array(
'domains_over_time' => $domains_over_time,
'ads_over_time' => $ads_over_time,
);
}
function getOverTimeData10mins() {
global $log;
// Get log lines
$dns_queries = getDnsQueries($log);
// Get list of ad domains
$gravity_domains = getGravity();
// Bin log entries separated into Domains and Ads in 10 minute intervals
list($domains_over_time, $ads_over_time) = overTime10mins($dns_queries, $gravity_domains);
// Align arrays (in case there have been hours without ad queries)
alignTimeArrays($ads_over_time, $domains_over_time);
// Provide a minimal valid array if there have are no blocked
// queries at all. Otherwise the output of the API is inconsistent.
if(count($ads_over_time) == 0)
{
$ads_over_time = [1 => 0];
}
return Array(
'domains_over_time' => $domains_over_time,
'ads_over_time' => $ads_over_time,
);
}
// Test if variable exists and is positive
function ispositive(&$arg)
{
if(isset($arg))
{
if($arg > 0)
{
return true;
}
return false;
}
return false;
}
function getTopItems($argument) {
global $log,$setupVars,$privacyMode;
// Process log file
$dns_domains = getDnsQueryDomains($log);
// Get list of ad domains
$gravity_domains = getGravity();
// Exclude domains the user doesn't want to see
if(isset($setupVars["API_EXCLUDE_DOMAINS"]))
{
excludeFromList($dns_domains, "API_EXCLUDE_DOMAINS");
}
// Sort array in descending order
arsort($dns_domains);
// Prepare arrays and counters for Top Items
$topDomains = []; $domaincounter = 0;
$topAds = []; $adcounter = 0;
// Default number of Top Items to show is 10
$qty = 10;
// If argument is numeric, the user may want to
// see a different number of entries
if(is_numeric($argument))
{
$qty = intval($argument);
}
// Process sorted domain names
foreach ($dns_domains as $key => $value) {
if(ispositive($gravity_domains[$key]) && $adcounter < $qty)
{
// New entry for Top Ads
$topAds[$key] = $value;
$adcounter++;
}
else if($domaincounter < $qty && !$privacyMode)
{
// New entry for Top Domains
$topDomains[$key] = $value;
$domaincounter++;
}
elseif($domaincounter >= $qty && $adcounter >= $qty)
{
// Already collected enough entries for both lists
// Exit loop early
break;
}
}
return Array(
'top_queries' => $topDomains,
'top_ads' => $topAds,
);
}
function getRecentItems($qty) {
global $log;
$dns_queries = getDnsQueries($log);
return Array(
'recent_queries' => getRecent($dns_queries, $qty)
);
}
function getIpvType() {
global $log;
$dns_queries = getDnsQueries($log);
$queryTypes = array();
foreach($dns_queries as $query) {
$info = trim(explode(": ", $query)[1]);
$queryType = explode(" ", $info)[0];
if (isset($queryTypes[$queryType])) {
$queryTypes[$queryType]++;
}
else {
$queryTypes[$queryType] = 1;
}
}
return $queryTypes;
}
function resolveIPs(&$array) {
$hostarray = [];
foreach ($array as $key => $value)
{
$hostname = gethostbyaddr($key);
// If we found a hostname for the IP, replace it
if($hostname)
{
// Generate HOST entry
$hostarray["$hostname|$key"] = $value;
}
else
{
// Generate IP entry
$hostarray[$key] = $value;
}
}
$array = $hostarray;
// Sort new array
arsort($array);
}
function getForwardDestinations() {
global $log, $setupVars;
$forwards = getForwards($log);
$destinations = array();
foreach ($forwards as $forward) {
$exploded = explode(" ", trim($forward));
$dest = $exploded[count($exploded) - 1];
if (isset($destinations[$dest])) {
$destinations[$dest]++;
}
else {
$destinations[$dest] = 1;
}
}
if(istrue($setupVars["API_GET_UPSTREAM_DNS_HOSTNAME"]))
{
resolveIPs($destinations);
}
return $destinations;
}
// Check for existance of variable
// and test it only if it exists
function istrue(&$argument) {
$ret = false;
if(isset($argument))
{
if($argument)
{
$ret = true;
}
}
return $ret;
}
function getQuerySources() {
global $log, $setupVars;
$dns_queries = getDnsQueries($log);
$sources = array();
foreach($dns_queries as $query) {
$exploded = explode(" ", $query);
$ip = trim($exploded[count($exploded)-1]);
if (isset($sources[$ip])) {
$sources[$ip]++;
}
else {
$sources[$ip] = 1;
}
}
global $setupVars;
if(isset($setupVars["API_EXCLUDE_CLIENTS"]))
{
excludeFromList($sources, "API_EXCLUDE_CLIENTS");
}
arsort($sources);
$sources = array_slice($sources, 0, 10);
if(istrue($setupVars["API_GET_CLIENT_HOSTNAME"]))
{
resolveIPs($sources);
}
return Array(
'top_sources' => $sources
);
}
$showBlocked = false;
$showPermitted = false;
function setShowBlockedPermitted()
{
global $showBlocked, $showPermitted, $setupVars;
if(isset($setupVars["API_QUERY_LOG_SHOW"]))
{
if($setupVars["API_QUERY_LOG_SHOW"] === "all")
{
$showBlocked = true;
$showPermitted = true;
}
elseif($setupVars["API_QUERY_LOG_SHOW"] === "permittedonly")
{
$showBlocked = false;
$showPermitted = true;
}
elseif($setupVars["API_QUERY_LOG_SHOW"] === "blockedonly")
{
$showBlocked = true;
$showPermitted = false;
}
elseif($setupVars["API_QUERY_LOG_SHOW"] === "nothing")
{
$showBlocked = false;
$showPermitted = false;
}
else
{
// Invalid settings, show everything
$showBlocked = true;
$showPermitted = true;
}
}
else
{
$showBlocked = true;
$showPermitted = true;
}
}
function getAllQueries($orderBy) {
global $log,$showBlocked,$showPermitted,$privacyMode,$setupVars;
$allQueries = array("data" => array());
$dns_queries = getDnsQueries($log);
$hostnames=array();
// Create empty array for gravity
$gravity_domains = getGravity();
$wildcard_domains = getWildcardListContent();
if(isset($_GET["from"]))
{
$from = new DateTime($_GET["from"]);
}
if(isset($_GET["until"]))
{
$until = new DateTime($_GET["until"]);
}
setShowBlockedPermitted();
// Privacy mode?
if($privacyMode)
{
$showPermitted = false;
}
if(!$showBlocked && !$showPermitted)
{
// Nothing to do for us here
return [];
}
foreach ($dns_queries as $query) {
$time = new DateTime(substr($query, 0, 16));
// Check if we want to restrict the time where we want to show queries
if(isset($from))
{
if($time <= $from)
{
continue;
}
}
if(isset($until))
{
if($time >= $until)
{
continue;
}
}
// print_r([$time->getTimestamp(),$_GET["from"],$_GET["until"]]);
$exploded = explode(" ", trim($query));
$domain = $exploded[count($exploded)-3];
$status = "";
if(isset($gravity_domains[$domain]))
{
if($gravity_domains[$domain] > 0)
{
// Exact matching gravity domain
$status = "Pi-holed (exact)";
}
else
{
// Explicitly whitelisted
$status = "OK (whitelisted)";
}
}
else
{
// Test for wildcard blocking
foreach ($wildcard_domains as $entry) {
if(strpos($domain, $entry) !== false)
{
$status = "Pi-holed (wildcard)";
}
}
if(!strlen($status))
{
$status = "OK";
}
}
if((substr($status,0,2) === "Pi" && $showBlocked) || (substr($status,0,2) === "OK" && $showPermitted))
{
$type = substr($exploded[count($exploded)-4], 6, -1);
if(istrue($setupVars["API_GET_CLIENT_HOSTNAME"])) {
$ip = $exploded[count($exploded) - 1];
if (isset($hostnames[$ip])) {
$client = $hostnames[$ip];
} else {
$hostnames[$ip] = gethostbyaddr($ip);
$client = $hostnames[$ip];
}
}
else
{
$client = $exploded[count($exploded)-1];
}
if($orderBy == "orderByClientDomainTime"){
$allQueries['data'][hasHostName($client)][$domain][$time->format('Y-m-d T H:i:s')] = $status;
}elseif ($orderBy == "orderByClientTimeDomain"){
$allQueries['data'][hasHostName($client)][$time->format('Y-m-d T H:i:s')][$domain] = $status;
}elseif ($orderBy == "orderByTimeClientDomain"){
$allQueries['data'][$time->format('Y-m-d T H:i:s')][hasHostName($client)][$domain] = $status;
}elseif ($orderBy == "orderByTimeDomainClient"){
$allQueries['data'][$time->format('Y-m-d T H:i:s')][$domain][hasHostName($client)] = $status;
}elseif ($orderBy == "orderByDomainClientTime"){
$allQueries['data'][$domain][hasHostName($client)][$time->format('Y-m-d T H:i:s')] = $status;
}elseif ($orderBy == "orderByDomainTimeClient"){
$allQueries['data'][$domain][$time->format('Y-m-d T H:i:s')][hasHostName($client)] = $status;
}else{
array_push($allQueries['data'], array(
$time->format('Y-m-d T H:i:s'),
$type,
$domain,
hasHostName($client),
$status,
""
));
}
}
}
return $allQueries;
}
function tailPiholeLog($param) {
// Not using SplFileObject here, since direct
// usage of f-streams will be much faster for
// files as large as the pihole.log
global $logListName;
$file = fopen($logListName,"r");
$offset = intval($param);
if($offset > 0)
{
// Seeks on the file pointer where we want to continue reading is known
fseek($file, $offset);
$lines = [];
while (!feof($file)) {
array_push($lines,fgets($file));
}
return ["offset" => ftell($file), "lines" => $lines];
}
else
{
// Locate the current position of the file read/write pointer
fseek($file, -1, SEEK_END);
// Add one to skip the very last "\n" in the log file
return ["offset" => ftell($file)+1];
}
fclose($file);
}
/******** Private Members ********/
function gravityCount() {
global $gravityListName,$blackListFile;
$preEventHorizon = exec("grep -c ^ $gravityListName");
$blacklist = exec("grep -c ^ $blackListFile");
return ($preEventHorizon + $blacklist);
}
function getDnsQueries(\SplFileObject $log) {
$log->rewind();
$lines = [];
foreach ($log as $line) {
if(strpos($line, ": query[A") !== false) {
$lines[] = $line;
}
}
return $lines;
}
function getDnsQueryDomains(\SplFileObject $log) {
$log->rewind();
$domains = [];
foreach ($log as $line) {
if(strpos($line, ": query[A") !== false) {
$exploded = explode(" ", $line);
$domain = trim($exploded[count($exploded) - 3]);
if (isset($domains[$domain])) {
$domains[$domain]++;
}
else {
$domains[$domain] = 1;
}
}
}
return $domains;
}
function countDnsQueries() {
global $logListName;
return intval(exec("grep -c \": query\\[A\" $logListName"));
}
function getDnsQueriesAll(\SplFileObject $log) {
$log->rewind();
$lines = [];
foreach ($log as $line) {
if(strpos($line, ": query[A") || strpos($line, "gravity.list") || strpos($line, ": forwarded") !== false) {
$lines[] = $line;
}
}
return $lines;
}
function getDomains($file, &$array, $action){
$file->rewind();
foreach ($file as $line) {
// Strip newline (and possibly carriage return) from end of key
$key = rtrim($line);
// if $action = true -> we want that domain to be ADDED to the list
// doesn't harm to do this if it has already been set before
// (e.g. once in gravity list, once in blacklist)
if($action && strlen($key) > 0)
{
// $action is true (we want to add) *and* key is not empty
$array[$key] = 1;
}
elseif(!$action && isset($array[$key]))
{
// $action is false (we want to remove) *and* key is set
$array[$key] = -1;
}
}
}
function getWildcardListContent() {
$rawList = file_get_contents(checkfile("/etc/dnsmasq.d/03-pihole-wildcard.conf"));
$wclist = explode("\n", $rawList);
$list = [];
foreach ($wclist as $entry) {
$expl = explode("/", $entry);
if(count($expl) == 3)
{
array_push($list,$expl[1]);
}
}
return array_unique($list);
}
function getGravity() {
global $gravity,$whitelist,$blacklist;
$domains = [];
// ADD (true) preEventHorizon domains
getDomains($gravity, $domains, true);
// ADD (true) blacklist domains
getDomains($blacklist, $domains, true);
// REMOVE (false) whitelist domains
getDomains($whitelist, $domains, false);
return $domains;
}
function getBlockedQueries(\SplFileObject $log) {
$log->rewind();
$lines = [];
foreach ($log as $line) {
$exploded = explode(" ", str_replace(" "," ",$line));
if(count($exploded) == 8 || count($exploded) == 10) {
// Structure of data is currently like:
// Array
// (
// [0] => Dec
// [1] => 19
// [2] => 11:21:51
// [3] => dnsmasq[2584]:
// [4] => /etc/pihole/gravity.list
// [5] => doubleclick.com
// [6] => is
// [7] => ip.of.pi.hole
// )
// with extra logging enabled
// Array
// (
// [0] => Dec
// [1] => 19
// [2] => 11:21:51
// [3] => dnsmasq[2584]:
// [4] => 1 (identifier)
// [5] => 1.2.3.4/12345
// [6] => /etc/pihole/gravity.list
// [7] => doubleclick.com
// [8] => is
// [9] => ip.of.pi.hole
// )
$list = $exploded[count($exploded)-4];
$is = $exploded[count($exploded)-2];
// Consider only gravity.list as DNS source (not e.g. hostname.list)
if(substr($list, strlen($list) - 12, 12) === "gravity.list" && $is === "is") {
$lines[] = $line;
};
}
}
return $lines;
}
function countBlockedQueries() {
global $logListName;
// Blocked due to gravity entries (ad lists + blacklist)
$gravityblocked = intval(exec("grep -c -e \"gravity\.list.*is\" $logListName"));
// Blocked due to wildcard entries
$wildcard_domains = getWildcardListContent();
$wildcardblocked = 0;
foreach ($wildcard_domains as $domain) {
$wildcardblocked += intval(exec("grep -c -e \"config.*$domain is\" $logListName"));
}
return $gravityblocked +$wildcardblocked;
}
function getForwards(\SplFileObject $log) {
$log->rewind();
$lines = [];
foreach ($log as $line) {
if(strpos($line, ": forwarded") !== false) {
$lines[] = $line;
}
}
return $lines;
}
function excludeFromList(&$array,$key)
{
global $setupVars;
$domains = explode(",",$setupVars[$key]);
foreach ($domains as $domain) {
if(isset($array[$domain]))
{
unset($array[$domain]);
}
}
return $array;
}
function overTime($entries, $gravity_domains) {
$byTimeDomains = [];
$byTimeAds = [];
foreach ($entries as $entry) {
$time = date_create(substr($entry, 0, 16));
$hour = $time->format('G');
$exploded = explode(" ", $entry);
$domain = trim($exploded[count($exploded) - 3]);
if(ispositive($gravity_domains[$domain]))
{
if (isset($byTimeAds[$time])) {
$byTimeAds[$time]++;
}
else {
$byTimeAds[$time] = 1;
}
}
if (isset($byTimeDomains[$time])) {
$byTimeDomains[$time]++;
}
else {
$byTimeDomains[$time] = 1;
}
}
return [$byTimeDomains,$byTimeAds];
}
function overTime10mins($entries, $gravity_domains=[]) {
$byTimeDomains = [];
$byTimeAds = [];
foreach ($entries as $entry) {
$time = date_create(substr($entry, 0, 16));
$hour = $time->format('G');
$minute = $time->format('i');
// 00:00 - 00:09 -> 0
// 00:10 - 00:19 -> 1
// ...
// 12:00 - 12:10 -> 72
// ...
// 15:30 - 15:39 -> 93
// etc.
$time = ($minute-$minute%10)/10 + 6*$hour;
$exploded = explode(" ", $entry);
$domain = trim($exploded[count($exploded) - 3]);
if(ispositive($gravity_domains[$domain]))
{
if (isset($byTimeAds[$time])) {
$byTimeAds[$time]++;
}
else {
$byTimeAds[$time] = 1;
}
}
if (isset($byTimeDomains[$time])) {
$byTimeDomains[$time]++;
}
else {
$byTimeDomains[$time] = 1;
}
}
return [$byTimeDomains,$byTimeAds];
}
function alignTimeArrays(&$times1, &$times2) {
if(count($times1) == 0 || count($times2) < 2) {
return;
}
$max = max(array_merge(array_keys($times1), array_keys($times2)));
$min = min(array_merge(array_keys($times1), array_keys($times2)));
for ($i = $min; $i <= $max; $i++) {
if (!isset($times2[$i])) {
$times2[$i] = 0;
}
if (!isset($times1[$i])) {
$times1[$i] = 0;
}
}
ksort($times1);
ksort($times2);
}
function getRecent($queries, $qty){
$recent = array();
foreach (array_slice($queries, -$qty) as $query) {
$queryArray = array();
$exploded = explode(" ", $query);
$time = date_create(substr($query, 0, 16));
$queryArray['time'] = $time->format('h:i:s a');
$queryArray['domain'] = trim($exploded[count($exploded) - 3]);
$queryArray['ip'] = trim($exploded[count($exploded)-1]);
array_push($recent, $queryArray);
}
return array_reverse($recent);
}
function hasHostName($var){
global $hosts;
foreach ($hosts as $host){
$x = preg_split('/\s+/', $host);
if ( $var == $x[0] ){
$var = $x[1] . "($var)";
}
}
return $var;
}
?>
Single Server Statistics
NOTE: Install the prerequisites (Commented within the script below)
NOTE: The script below may be more up to date at https://gist.github.com/dkittell/74dc56f832ea2b7da1c9cc9fcbb766ca
#!/bin/sh
# Pi-Hole Statistics
#
#
# Created by David Kittell on 4/21/17.
#
clear
# Install Prerequisites - Start
# Install JQ
# sudo apt-get install jq
# Install IPCalc
# sudo apt-get install ipcalc
# Install Network Manager (nmcli)
# sudo apt-get install network-manager
# Install Prerequisites - Stop
# Variables - Start
json=$(curl -s -X GET http://127.0.0.1/admin/api.php?summaryRaw)
#echo ${json}
uptime=$(echo ${json} | jq ".server_up_time" | sed 's/"//g')
domains=$(echo ${json} | jq ".domains_being_blocked")
queries=$(echo ${json} | jq ".dns_queries_today")
blocked=$(echo ${json} | jq ".ads_blocked_today")
percentage=$(printf "%0.2f\n" $(echo ${json} | jq ".ads_percentage_today"))
# Variables - Stop
#lsb_release -a
OS=$(lsb_release -i | cut -d ":" -f2 | tr -d '[:space:]')
OSCode=$(lsb_release -c | cut -d ":" -f2 | tr -d '[:space:]')
OSVer=$(lsb_release -r | cut -d ":" -f2 | tr -d '[:space:]')
#echo "$OS $OSCode $OSVer"
# Network Variables - Start
netAdapter=$(nmcli device status | grep en | cut -d " " -f1)
if [ -z "$netAdapter" ]; then
netAdapter=$(nmcli device status | grep eth | cut -d " " -f1)
fi
#declare netAdapter=$(ip -o link show | awk '{print $2,$9}' | grep UP | cut -d ":" -f1)
#echo $netAdapter
netIP=$(/sbin/ip -o -4 addr list $netAdapter | awk '{print $4}' | cut -d/ -f1)
#echo $netIP
#declare netMask=$(ipcalc -m $netIP | cut -d '=' -f2)
netMask=$(ifconfig "$netAdapter" | sed -rn '2s/ .*:(.*)$/\1/p')
netCIDR=$(ipcalc $netIP/$netMask | grep "Netmask:" | cut -d "=" -f2 | cut -d " " -f2 | tr -d '[:space:]')
netWork=$(ipcalc $netIP/$netMask | grep "Network:" | cut -d "/" -f1 | cut -d " " -f4 | tr -d '[:space:]')
echo "$OS $OSCode $OSVer"
echo " Hostname: $(hostname)"
echo " System Uptime: ${uptime}"
echo "Network Information"
echo " Adapter: $netAdapter"
echo " IP: $netIP"
echo " Netmask: $netMask"
echo " CIDR: $netWork/$netCIDR"
echo "-----------------------------------------------------------------------"
echo "Ad Filter Stats"
echo " Total Blocked Hosts: ${domains}"
echo " Total Queries: ${queries}"
echo " Total Blocked: ${blocked} (${percentage}%)"
Originally Posted on April 21, 2017
Last Updated on June 10, 2019
Last Updated on June 10, 2019
All information on this site is shared with the intention to help. Before any source code or program is ran on a production (non-development) system it is suggested you test it and fully understand what it is doing not just what it appears it is doing. I accept no responsibility for any damage you may do with this code.