FYI, here is a perl script I recently wrote to summarize my backups at one glance so I can identify missed backups, errors, missed files etc. It organizes a lot of data in a simple text file. I call the script daily from cron. It doesn't *need* to be run as 'backuppc' (or root) though it works better if it does.
It's not fully polished but it works well -------------------------------------------------------------------------------- #!/usr/bin/perl #======================================================================== # # BackupPC_summarizeBackups.pl # # # DESCRIPTION # Summarize status of latest backups (full and incremental) with one # (long) tabular line per host. # # Provides the following columns per host # General Status: # HOST Name of host # STATUS Current status # Idle - if idle # NNNNm - Active time in minutes if backing up # Fail - if last backup failed # Man - if set to manual backups (BackupsDisable = 1) # Disab - if backups disabled (BackupsDisable = 2) # LAST Fractional days since last backup (full or incremental) # # Full Backup Status: # FULL Fractional days since last full backup # FILES Number of files in last full # SIZE Size of last full # TIME Time to complete last full (in minutes) # ERRS/BAD Number of errors/bad files in last full # # Incremental Backup Status: # INCR Fractional days since last incremental backup # FILES Number of files in last incremental # SIZE Size of last incremental # TIME Time to complete last incremental (in minutes) # ERRS/BAD Number of errors/bad files in last incremental # # Sanity Checks (worry if current numbers differ substantially from this) # MAX_FILES Maximum files in past $lookback_days (default 365) # MAX_SIZE Max backup size in past $lookback_days (default 365) # # AUTHOR # Jeff Kosowsky # # COPYRIGHT # Copyright (C) 2025 Jeff Kosowsky # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # #======================================================================== # # Version 0.2 June 2025 # # CHANGELOG: # 0.1 (June 2025) # 0.2 (July 2025) # #======================================================================== use strict; use warnings; use lib "/usr/share/backuppc/lib"; use BackupPC::Lib; use POSIX qw(strftime); # Configuration my $lookback_days = 365; # Configurable lookback period for recent backups my $host_width = 20; #Max width of host name # Initialize BackupPC my $bpc = BackupPC::Lib->new('/var/lib/backuppc', undef, undef, 1) or die "Failed to initialize BackupPC: $@\n"; my $Conf = $bpc->ConfigDataRead() or die "Failed to load config: $@\n"; my $now = time; # Get Status and Info my $Status = {}; my $Info = {}; my $err = $bpc->ServerConnect(); if (!$err) { # Read dynamically via Server_Message (requires backuppc or root privilege) my $reply = $bpc->ServerMesg("status hosts"); if (!defined $reply) { warn "ServerMesg for hosts returned undef\n"; } else { my %Status; eval($reply); if ($@) { warn "Eval error for hosts: $@\n"; } else { $Status = \%Status; } } $reply = $bpc->ServerMesg("status info"); # Get system info including pool stats if (!defined $reply) { warn "ServerMesg for info returned undef\n"; } else { my %Info; eval($reply); if ($@) { warn "Eval error for info: $@\n"; } else { $Info = \%Info; } } } else { # Read from 'logDir/status.pl' using StatusDataRead warn "ServerConnect failed, falling back to status.pl\n"; my $log_dir = $bpc->LogDir or die "LogDir not defined\n"; my $status_file = "$log_dir/status.pl"; if (!(-f $status_file && -r $status_file)) { warn "$status_file not found or not readable\n"; } else { # Read from logDir/status.pl using StatusDataRead ($Status, $Info) = $bpc->{storage}->StatusDataRead(); if (!defined $Status || ref($Status) ne 'HASH') { warn "Failed to read status from $status_file\n" unless defined($Status); $Status = {}; } if (!defined $Info || ref($Info) ne 'HASH') { warn "Failed to read valid Info from $status_file\n"; $Info = {}; } } } # Check if BackupPC is running my $backuppc_running = defined($Info->{pid}); # Print warning if BackupPC is not running print "***WARNING*** BackupPC not running!\n" unless $backuppc_running; # Print header printf "%-*s %-8s %-7s | %-7s %-10s %-10s %-7s %-10s | %-7s %-10s %-10s %-7s %-10s | %-10s %-10s\n", $host_width + 1, "HOST", "STATUS", "LAST", "FULL", "FILES", "SIZE", "TIME", "ERRS/BAD", "INCR", "FILES", "SIZE", "TIME", "ERRS/BAD", "MAX_FILES", "MAX_SIZE"; # Get host list my $hosts = $bpc->HostInfoRead() or die "Failed to read hosts: $@\n"; my @host_data; my @host_nobackups; foreach my $host (keys %$hosts) { my $status = "Idle"; my $days_since_last = 999999; my $days_since_full = undef; my $days_since_incr = undef; my $files_full = '-'; my $size_full = '-'; my $time_full = '-'; my $errs_bad_full = '-'; my $files_incr = '-'; my $size_incr = '-'; my $time_incr = '-'; my $errs_bad_incr = '-'; my $max_files = 0; my $max_size = 0; my $has_recent_backup = 0; my $has_valid_backup = 0; my $last_backup_time = 0; # Get current status my $host_status = $Status->{$host} // {}; if (defined $host_status->{job} && $host_status->{job} eq "Backup" && defined $host_status->{reason} && $host_status->{reason} eq "Reason_backup_in_progress") { my $start_time = $host_status->{startTime} // $now; $status = sprintf "%dm", ($now - $start_time) / 60; } elsif (defined $host_status->{backoffTime} && $host_status->{backoffTime} > $now) { $status = sprintf "[%.1fh]", ($host_status->{backoffTime} - $now) / 3600; } elsif (defined $host_status->{reason} && $host_status->{reason} eq "Reason_backup_queued") { $status = "QUEUE"; } elsif (defined $host_status->{reason} && $host_status->{reason} eq "Reason_host_disabled") { $status = "Disab"; } elsif (defined $host_status->{error}) { $status = "Fail"; } else { my $backups_disable = $Conf->{BackupsDisable} // 0; #General config.pl configuration my $host_conf = $bpc->ConfigDataRead($host); #Host-specific configuration in confDir/pc $backups_disable = $host_conf->{BackupsDisable} if $host_conf && defined $host_conf->{BackupsDisable}; if ($backups_disable == 1 && $status eq "Idle") { $status = "Man"; } elsif ($backups_disable == 2 && $status eq "Idle") { $status = "Disab"; } } # Get backups my @backups = $bpc->BackupInfoRead($host); foreach my $backup (@backups) { my $type = $backup->{type} // ''; my $backup_time = $backup->{startTime} || 0; next if !$type || $type eq "partial"; my $n_files = $backup->{nFiles} || 0; my $size = $backup->{size} || 0; # Bytes my $backup_duration = ($backup->{endTime} && $backup->{startTime}) ? commify(int(($backup->{endTime} - $backup->{startTime}) / 60 + 0.5)) : '-'; my $xfer_errs = $backup->{xferErrs} || 0; my $xfer_bad = $backup->{xferBadFile} || 0; if ($type eq "active" && $backup_time) { $status = sprintf "%dm", ($now - $backup_time) / 60; # Time since backup start in minutes $has_valid_backup = 1; } if ($type eq "full" || $type eq "incr") { $has_valid_backup = 1; if ($backup_time > $last_backup_time) { $last_backup_time = $backup_time; $days_since_last = sprintf "%.1f", ($now - $backup_time) / 86400; } } if ($type eq "full" && (!defined $days_since_full || $backup_time >= ($now - ($days_since_full * 86400)))) { $days_since_full = sprintf "%.1f", ($now - $backup_time) / 86400; $files_full = commify($n_files); $size_full = commify(int($size / 1048576)); # MiB $time_full = $backup_duration; $errs_bad_full = sprintf "%d/%d", $xfer_errs, $xfer_bad; } if ($type eq "incr" && (!defined $days_since_incr || $backup_time >= ($now - ($days_since_incr * 86400)))) { $days_since_incr = sprintf "%.1f", ($now - $backup_time) / 86400; $files_incr = commify($n_files); $size_incr = commify(int($size / 1048576)); # MiB $time_incr = $backup_duration; $errs_bad_incr = sprintf "%d/%d", $xfer_errs, $xfer_bad; } if ($backup_time > $now - $lookback_days * 86400) { $has_recent_backup = 1; $max_files = $n_files if $n_files > $max_files; $max_size= $size if $size > $max_size; } } if (!$has_valid_backup) { push @host_nobackups, { host => $host, status => $status, }; next; } my $max_files_output = $has_recent_backup ? commify($max_files) : '-'; my $max_size_output = $has_recent_backup ? commify(int($max_size / 1048576)) : '-'; push @host_data, { host => $host, days_since_last => $days_since_last, status => $status, days_since_full => $days_since_full // '-', files_full => $files_full, size_full => $size_full, time_full => $time_full, errs_bad_full => $errs_bad_full, days_since_incr => $days_since_incr // '-', files_incr => $files_incr, size_incr => $size_incr, time_incr => $time_incr, errs_bad_incr => $errs_bad_incr, max_files => $max_files_output, max_size => $max_size_output, recent_backup => $has_recent_backup, }; } # Sort by LAST column (ascending), then hostname @host_data = sort { $a->{days_since_last} <=> $b->{days_since_last} || $a->{host} cmp $b->{host} } @host_data; # Print hosts with backups my $last_had_star = 0; foreach my $data (@host_data) { if (!$data->{recent_backup} && !$last_had_star) { print "\n"; } printf "%s%-*s %-8s %-7s | %-7s %-10s %-10s %-7s %-10s | %-7s %-10s %-10s %-7s %-10s | %-10s %-10s\n", $data->{recent_backup} ? ' ' : '*', $host_width, substr($data->{host}, 0, $host_width), $data->{status}, $data->{days_since_last} == 999999 ? '-' : $data->{days_since_last}, $data->{days_since_full}, $data->{files_full}, $data->{size_full}, $data->{time_full}, $data->{errs_bad_full}, $data->{days_since_incr}, $data->{files_incr}, $data->{size_incr}, $data->{time_incr}, $data->{errs_bad_incr}, $data->{max_files}, $data->{max_size}; $last_had_star = !$data->{recent_backup}; } # Print hosts with no backups print "\n"; foreach my $data (sort { $a->{host} cmp $b->{host} } @host_nobackups) { printf "%-*s %-8s\n", $host_width+1, "*" . substr($data->{host}, 0, $host_width), $data->{status}; } # Print pool statistics print "\nPool Statistics:\n"; printf "%-15s %-15s %-15s\n", "", "Pool", "CPool"; printf "%-15s %-15s %-15s\n", " " x 15, "-" x 15, "-" x 15; printf "%-15s %-15s %-15s\n", "Files:", commify($Info->{poolFileCnt} || 0), commify($Info->{cpool4FileCnt} || 0); printf "%-15s %-15s %-15s\n", "Size (GiB):", commify(sprintf("%.2f", ($Info->{poolKb} || 0) / (1024**2))), commify(sprintf("%.2f", ($Info->{cpool4Kb} || 0) / (1024**2))); printf "%-15s %-15s %-15s\n", "Max Links:", commify($Info->{poolFileLinkMax} || 0), commify($Info->{cpool4FileLinkMax} || 0); printf "%-15s %-15s %-15s\n", "Removed Files:", commify($Info->{poolFileCntRm} || 0), commify($Info->{cpool4FileCntRm} || 0); # Add commas to numbers sub commify { my $num = shift; return $num if $num eq '-'; # special case you added my ($int, $dec) = $num =~ /^(-?\d+)(\.\d+)?$/; return $num unless defined $int; $int = reverse $int; $int =~ s/(\d{3})(?=\d)/$1,/g; $int = reverse $int; return defined $dec ? "$int$dec" : $int; } -------------------------------------------------------------------------------- G.W. Haywood wrote at about 14:08:11 +0100 on Friday, July 25, 2025: > Hi there, > > On Fri, 25 Jul 2025, Matthew Pounsett wrote: > > > .. what I'm more interested in doing is setting up something to > > interpret BackupPC_serverMesg output so that I can get backup status > > into my actual monitoring system. > > Two things. > > First, there's a script here > > https://github.com/moisseev/BackupPC_report > > which you might find useful. I've run it a couple of times and it > seems to do what it's supposed to do but I can't say more than that. > If it doesn't actually do what you want at least it will be a start. > > Second, please take a look at github issue #365 and please feel free > to comment on the sort of things that you'd find useful in an API. > > -- > > 73, > Ged. > > > _______________________________________________ > BackupPC-users mailing list > BackupPC-users@lists.sourceforge.net > List: https://lists.sourceforge.net/lists/listinfo/backuppc-users > Wiki: https://github.com/backuppc/backuppc/wiki > Project: https://backuppc.github.io/backuppc/ _______________________________________________ BackupPC-users mailing list BackupPC-users@lists.sourceforge.net List: https://lists.sourceforge.net/lists/listinfo/backuppc-users Wiki: https://github.com/backuppc/backuppc/wiki Project: https://backuppc.github.io/backuppc/