The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/perl
use strict;
use Cwd ();
use POSIX ();
my %opts;
my @logs;
my $selected_log;
my $host = {};
my %months = (
Jan => 1, Feb => 2, Mar => 3, Apr => 4, May => 5, Jun => 6,
Jul => 7, Aug => 8, Sep => 9, Oct => 10, Nov => 11, Dec => 12,
);
my $now = time();
my $columns;
my $line_count = 0;
get_opts();
get_logs();
select_log();
if ($opts{action} eq "list") {
show_list();
}
elsif ($opts{action} eq "detailed_list") {
show_detailed_list();
}
elsif ($opts{action} eq "info") {
show_info();
}
elsif ($opts{action} eq "stats") {
show_stats();
}
elsif ($opts{action} eq "graph") {
show_graph();
}
elsif ($opts{action} eq "print") {
print "$selected_log->{pfile}\n";
}
else {
if (!-t STDOUT) {
system "cat", $selected_log->{pfile};
}
elsif ($ENV{PAGER}) {
system "$ENV{PAGER} $selected_log->{pfile}";
}
else {
system "less", "-RIMS", $selected_log->{pfile};
}
}
sub show_stats {
my $stats = {};
loop_errors(\&process_error_for_stats, $stats);
my $file = $selected_log->{pfile};
my $size = -s $file;
$size = human_readable($size);
print "$file\n";
print "size $size\n";
my $count = $stats->{errors} || 0;
print "$count errors in $line_count lines\n";
my $from = $stats->{from};
my $fromstr = datestr($from);
my $to = $stats->{to};
my $tostr = datestr($to);
my $time_diff = time_diff_str2($from, $to);
if ($time_diff) {
print "spanning $time_diff\n";
}
print "from [$fromstr] to [$tostr]\n";
}
sub process_error_for_stats {
my ($e, $stats) = @_;
$stats->{errors}++;
if (!$stats->{from}) {
$stats->{from} = $e->{date};
}
$stats->{to} = $e->{date};
}
sub show_graph {
my @entries;
loop_errors(\&add_error_to_graph, \@entries);
my $max = 0;
for my $entry (@entries) {
$entry->{total} ||= 0;
if ($entry->{total} > $max) {
$max = $entry->{total};
}
}
$columns = `tput cols`;
my $count = 0;
my $prev;
for my $entry (@entries) {
$count++;
$entry->{count} = $count;
show_graph_entry($entry, $prev, $max);
$prev = $entry;
}
}
sub add_error_to_graph {
my ($e, $entries) = @_;
my $entry = $entries->[-1];
if (!$entry) {
$entry = {};
$entry->{start} = $e->{date};
push @$entries, $entry;
}
elsif ($e->{date} >= $entry->{start} + $opts{interval}) {
while (1) {
my $entry2 = {};
$entry2->{start} = $entry->{start} + $opts{interval};
$entry = $entry2;
push @$entries, $entry;
if ($e->{date} < $entry->{start} + $opts{interval}) {
last;
}
}
}
$entry->{total}++;
}
sub show_graph_entry {
my ($entry, $prev, $max) = @_;
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($entry->{start});
$mon += 1;
my $hour2;
if ($hour == 0) {
$hour2 = 12;
}
elsif ($hour > 12) {
$hour2 = $hour - 12;
}
else {
$hour2 = $hour;
}
my $offset = 16;
$entry->{monday} = "$mon/$mday";
if ($opts{interval} >= 24 * 60 * 60) {
printf "%-5s: ", "$mon/$mday";
}
elsif ($opts{interval} >= 60 * 60) {
if (!$prev || $prev->{monday} ne $entry->{monday}) {
printf "%-5s %2d: ", "$mon/$mday", $hour2;
}
else {
printf " %2d: ", $hour2;
}
}
else {
$offset += 3;
if (!$prev || $prev->{monday} ne $entry->{monday}) {
printf "%-5s %2d:%02d: ", "$mon/$mday", $hour2, $min;
}
else {
printf " %2d:%02d: ", $hour2, $min;
}
}
my $bar = "";
if ($entry->{total}) {
my $size = int(($entry->{total} / $max) * ($columns - $offset));
$bar = "#" x $size;
if ($size) {
$bar .= " ";
}
$bar .= $entry->{total};
}
print "$bar\n";
}
sub get_rotations {
my ($file) = @_;
my $dir = dirname($file);
opendir my $dh, $dir or return ();
my $array = [];
for my $name (readdir $dh) {
next if $name =~ /^\.\.?$/;
my $dfile = "$dir/$name";
if ($dfile =~ /^\Q$file\E.(.*?)(\.gz)?$/) {
my $r = {};
$r->{file} = $dfile;
$r->{name} = $1;
push @$array, $r;
}
}
closedir $dh;
$array = natural_sort($array, "file");
return $array;
}
# Sorts files in a way that treats numbers as numbers, so if you have
# files file1, file2, file10 it wouldn't sort it like file1, file10, file2.
sub natural_sort {
my ($array, $key) = @_;
my @array2;
for my $entry (@$array) {
my $value = $entry->{$key};
$value =~ s/(\d+)/sprintf("%05d", $1)/ge;
push @array2, [$value, $entry];
}
@array2 = sort {$a->[0] cmp $b->[0]} @array2;
my @array3;
for my $entry (@array2) {
push @array3, $entry->[1];
}
return \@array3;
}
sub show_info {
loop_errors(\&show_error_info);
}
sub loop_errors {
my ($func, $arg) = @_;
my $cmd = "";
my $file = $selected_log->{pfile};
if ($file =~ /\.gz$/) {
$cmd = "gzip -dc $file";
}
if ($opts{max}) {
if ($opts{offset}) {
my $amount = $opts{offset} + $opts{max};
if ($cmd) {
$cmd .= "| tail -n $amount | head -n $opts{max}";
}
else {
$cmd = "tail -n $amount $file | head -n $opts{max}";
}
}
else {
if ($cmd) {
$cmd .= "| tail -n $opts{max}";
}
else {
$cmd = "tail -n $opts{max} $file";
}
}
}
my $fh;
if ($cmd) {
open $fh, "-|", $cmd or die "Can't open $file: $!\n";
}
else {
open $fh, "<", $file or die "Can't open $file: $!\n";
}
my $e1;
my $filter = $opts{filter};
if ($filter) {
$filter = qr{$filter}i;
}
$line_count = 0;
while (my $line = <$fh>) {
if ($filter && $line !~ $filter) {
next;
}
$line_count++;
my $e2 = parse_error_line($line);
if (!$e1) {
$e1 = $e2;
}
elsif (same_error($e1, $e2)) {
$e1->{mesg} .= "\n$e2->{mesg}";
}
else {
$func->($e1, $arg);
$e1 = $e2;
}
}
if ($e1) {
$func->($e1, $arg);
}
close $fh;
}
sub show_error_info {
my ($e) = @_;
my $datestr = datestr($e->{date}) || "Unknown time";
my $line = "[$datestr]";
if ($e->{ip}) {
$line .= " from IP $e->{ip}";
}
if ($e->{script}) {
$line .= " in $e->{script}"
}
$line .= ":";
print "$line\n$e->{mesg}\n\n";
}
sub datestr {
my ($date) = @_;
return "" if !$date;
my $datestr = POSIX::strftime("%a %b %d, %Y %I:%M:%S %p", localtime($date));
return $datestr;
}
# returns a string like "17 hours 5 minutes 2 seconds"
sub time_diff_str2 {
my ($date1, $date2) = @_;
return "" if !$date1 || !$date2;
my $obj = time_diff_obj($date1, $date2);
my $str = "";
for my $key ("year", "month", "day", "hour", "minute", "second") {
if ($obj->{$key}) {
if ($str) {
$str .= " ";
}
$str .= "$obj->{$key} $key";
if ($obj->{$key} != 1) {
$str .= "s";
}
}
}
if ($date1 == $date2) {
$str = "0 seconds";
}
return $str;
}
# returns a string like "17 hours ago" or "in 5 seconds"
sub time_diff_str {
my ($date1, $date2) = @_;
return "" if !$date1 || !$date2;
my $obj = time_diff_obj($date1, $date2);
my $str = "";
for my $key ("year", "month", "day", "hour", "minute", "second") {
if ($obj->{$key}) {
$str .= "$obj->{$key} $key";
if ($obj->{$key} != 1) {
$str .= "s";
}
last;
}
}
if ($date1 == $date2) {
$str = "0 seconds";
}
if ($date1 <= $date2) {
$str .= " ago";
}
else {
$str = "in $str";
}
return $str;
}
sub time_diff_obj {
my ($date1, $date2) = @_;
my $from = $date1 < $date2 ? $date1 : $date2;
my $to = $date1 < $date2 ? $date2 : $date1;
my ($fsec, $fmin, $fhour, $fmday, $fmon, $fyear, $fwday, $fyday, $fisdst) = localtime($from);
my ($tsec, $tmin, $thour, $tmday, $tmon, $tyear, $twday, $tyday, $tisdst) = localtime($to);
my $year_diff = 0;
my $mon_diff;
if ($tyear > $fyear) {
$mon_diff = 12 - $fmon + $tmon + 12 * ($tyear - $fyear - 1);
$year_diff = int($mon_diff / 12);
$mon_diff = $mon_diff % 12;
}
else {
$mon_diff = $tmon - $fmon;
}
my %obj;
if ($year_diff) {
$obj{year} = $year_diff;
}
my $inner = $from;
if ($mon_diff) {
if ($tmday < $fmday) {
if ($mon_diff > 1) {
$obj{month} = $mon_diff - 1;
$inner = POSIX::mktime($fsec, $fmin, $fhour, $fmday, $fmon + $mon_diff - 1, $fyear + $year_diff, 0, 0, -1);
}
}
else {
$obj{month} = $mon_diff;
$inner = POSIX::mktime($fsec, $fmin, $fhour, $fmday, $fmon + $mon_diff, $fyear + $year_diff, 0, 0, -1);
}
}
# $inner is less than a month away from the $to date at this point, so you can focus on only days, hours, etc at this point
my $remaining_time = $to - $inner;
my $day_diff = int($remaining_time / (24 * 60 * 60));
if ($day_diff) {
$obj{day} = $day_diff;
}
$remaining_time -= $day_diff * 24 * 60 * 60;
my $hour_diff = int($remaining_time / (60 * 60));
if ($hour_diff) {
$obj{hour} = $hour_diff;
}
$remaining_time -= $hour_diff * 60 * 60;
my $minute_diff = int($remaining_time / 60);
if ($minute_diff) {
$obj{minute} = $minute_diff;
}
$remaining_time -= $minute_diff * 60;
if ($remaining_time) {
$obj{second} = $remaining_time;
}
return \%obj;
}
sub parse_error_line {
my ($line) = @_;
return undef if !$line;
my $e = {};
$e->{date} = 0;
$e->{client} = "";
$e->{script} = "";
$e->{type} = "";
if ($line !~ /^\[/) {
chomp $line;
$e->{mesg} = $line;
return $e;
}
if ($line !~ /^\[ ([^\]]+) \] \s* \[ ([\w:]+) \] \s* (.*)/x) {
return undef;
}
$e->{datestr} = $1;
$e->{type} = $2;
$line = $3;
$e->{date} = parse_date($e->{datestr});
if ($line =~ /^\[ \s* pid \s+ ([^\]]+) \] \s (.*)/x) {
$e->{pid} = $1;
$line = $2;
if ($e->{pid} =~ /(\d+):tid \s+ (\d+)/x) {
$e->{pid} = $1;
$e->{tid} = $2;
}
}
if ($line =~ /^\[ \s* client \s+ ([^\]]+) \] \s (.*)/x) {
$e->{client} = $1;
$line = $2;
if ($e->{client} =~ /^(\d+\.\d+\.\d+\.\d+):(\d+)/) {
$e->{ip} = $1;
$e->{port} = $2;
}
}
if ($line =~ /^\[ ([^\]]+) \] \s* (\S+): \s (.*)/x) {
$e->{datestr2} = $1;
$e->{script} = $2;
$line = $3;
}
if ($line =~ /^(AH\d+): \s (.*)/x) {
$e->{ah} = $1;
$line = $2;
}
if ($line =~ /^(.*?), \s* referer: \s* (http\S+)$/x) {
$e->{mesg} = $1;
$e->{referer} = $2;
}
else {
$e->{mesg} = $line;
}
return $e;
}
# For dates that look like "Tue Sep 08 13:58:21.123456 2015"
sub parse_date {
my ($str) = @_;
return () if !$str;
$str =~ m{^(\w+) \s+ (\w+) \s+ (\d+) \s+ (\d+) : (\d+) : (\d+) (\.(\d+))? \s+ (\d+)}x
or die "Can't parse date \"$str\"\n";
my $dow = $1;
my $monthstr = $2;
my $day = $3;
my $hour = $4;
my $min = $5;
my $sec = $6;
my $usec = $8;
my $year = $9;
my $month = $months{$monthstr} or die "Unknown month $monthstr\n";
my $time = POSIX::mktime($sec, $min, $hour, $day, $month - 1, $year - 1900, 0, 0, -1);
return $time;
}
sub same_error {
my ($e1, $e2) = @_;
if (!$e1 || !$e2) {
return 0;
}
elsif (!$e1->{date} && !$e2->{date}) {
return 1;
}
elsif ($e1->{pid} && $e2->{pid}) {
if ($e1->{pid} eq $e2->{pid}) {
return 1;
}
else {
return 0;
}
}
elsif (
$e1->{date} eq $e2->{date} &&
$e1->{type} eq $e2->{type} &&
$e1->{client} eq $e2->{client} &&
$e1->{script} eq $e2->{script}
) {
return 1;
}
else {
return 0;
}
}
sub show_list {
for my $log (@logs) {
my $selected = $log->{selected} ? "*" : " ";
my $size = "-";
my $updated = "";
if (-e $log->{file}) {
$size = human_readable(-s $log->{file});
my $mtime = (stat($log->{file}))[9];
$updated = time_diff_str($mtime, $now);
}
my $file = $log->{file};
my $line = sprintf "%s %-60s %-10s %s", $selected, $file, $size, $updated;
print "$line\n";
}
}
sub show_detailed_list {
for my $log (@logs) {
my $size = "-";
my $updated = "";
if (-e $log->{file}) {
$size = human_readable(-s $log->{file});
my $mtime = (stat($log->{file}))[9];
$updated = datestr($mtime) . " (" . time_diff_str($mtime, $now) . ")";
}
my $vhost = $log->{vhost};
print "$log->{file}\n";
if ($log->{selected}) {
print " selected\n";
}
print " size $size\n";
if ($updated) {
print " updated $updated\n";
}
if ($vhost) {
if ($vhost->{file}) {
print " config $vhost->{file}\n";
}
if ($vhost->{docroot}) {
print " docroot $vhost->{docroot}\n";
}
if ($vhost->{absdocroot}) {
print " absdocroot $vhost->{absdocroot}\n";
}
}
my $rotations = get_rotations($log->{file});
for my $r (@$rotations) {
my $size = human_readable(-s $r->{file});
my $mtime = (stat($r->{file}))[9];
my $updated = datestr($mtime) . " (" . time_diff_str($mtime, $now) . ")";
print " rotation $r->{name} $size $updated\n";
}
}
}
sub human_readable {
my ($size) = @_;
my @power = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB");
my $i = 0;
my $abs_size = abs $size;
for ($i = 0; $i < @power; $i++) {
last if $abs_size < 1024;
$abs_size /= 1024;
}
my $str = sprintf("%.1f %s", $abs_size, $power[$i]);
$str =~ s/\.0//;
$str = "-$str" if $size < 0;
return $str;
}
sub select_log {
my $name = $opts{name};
if ($name) {
my $regex = qr/^$name/;
for my $log (@logs) {
if ($log->{name} eq $name || $log->{file} eq $name || $log->{name} =~ $regex) {
select_log_rotation($log);
last;
}
}
# If the name doesn't match one of the discovered log files,
# treat it as a filename.
if (!$selected_log) {
my $log = add_log($name);
select_log_rotation($log);
}
return;
}
# Select the log that is for the docroot closest to your current
# working directory, it's not just a contained directory of the
# docroot, because you can be one level above the docroot, you
# would still want the error log for it. for example, you were in
# /home/username123/www/foo.com and the log is for a docroot at
# /home/username123/www/foo.com/public_html. Also you could be in
# /home/username123/www/foo.com/public_html/images. So it will choose the
# log with the most characters at the beginning matching.
#
# We make sure to append a / to directory names to ensure we choose
# the exact matching directory when two exist with same prefix for
# example you were in /www/foo.com and there are logs for docroots
# /www/foo.com and /www/foo.comYEP, you want to see the log for
# /www/foo.com.
if (@logs) {
my $cwd = Cwd::cwd();
$cwd .= "/" if $cwd !~ m{/$};
for my $log (@logs) {
my $docroot = "";
if ($log->{vhost} && $log->{vhost}{absdocroot}) {
$docroot = $log->{vhost}{absdocroot} . "/";
}
$log->{begcmp} = begcmp($cwd, $docroot);
}
my @logs_sorted = sort {
return $b->{begcmp} <=> $a->{begcmp} || $a->{name} cmp $b->{name};
} @logs;
select_log_rotation($logs_sorted[0]);
}
}
sub select_log_rotation {
my ($log) = @_;
$selected_log = $log;
$log->{selected} = 1;
my $file = $log->{file};
my $rot = $opts{rotation};
my $pfile; # pfile for processed file (includes rotation part)
if (!$rot) {
$pfile = $file;
}
elsif (-e "$file.$rot.gz") {
$pfile = "$file.$rot.gz";
}
elsif (-e "$file.$rot") {
$pfile = "$file.$rot";
}
elsif (-e "$file-$rot.gz") {
$pfile = "$file-$rot.gz";
}
elsif (-e "$file-$rot") {
$pfile = "$file-$rot";
}
elsif ($rot =~ /^\d+$/) {
my $rotations = get_rotations($file);
my $r = $rotations->[- $rot];
if ($r) {
$pfile = $r->{file};
}
}
if (!$pfile) {
die "Unable to find log file.\n";
}
$log->{pfile} = $pfile;
}
sub add_log {
my ($file) = @_;
my $log = {};
$log->{file} = $file;
$log->{name} = basename($file);
push @logs, $log;
return $log;
}
sub get_logs {
get_logs_from_apache_conf();
my $file = "/usr/local/apache/logs/error_log";
if (-e $file) {
add_log($file);
}
process_log_list();
@logs = sort {
return $a->{file} cmp $b->{file};
} @logs;
}
# The list is processed to remove duplicates and remove logs for
# overridden virtualhosts.
sub process_log_list {
my %logs;
my @logs_new;
for my $log (@logs) {
my $vhost = $log->{vhost};
if ($vhost && $vhost->{invalid}) {
next;
}
if ($host->{servroot} && $log->{file} !~ /^\//) {
$log->{file} = "$host->{servroot}/$log->{file}";
}
if ($logs{$log->{file}}) {
next;
}
$logs{$log->{file}} = $log;
push @logs_new, $log;
}
@logs = @logs_new;
}
# Returns the number of characters that are the same from the beginning
# between two strings.
# abc and def is 0
# abc and abcdef is 3
sub begcmp {
my ($a, $b) = @_;
my $count = 0;
my $min = length($a) < length($b) ? length($a) : length($b);
my $i = 0;
for ($i = 0; $i < $min; $i++) {
if (substr($a, $i, 1) ne substr($b, $i, 1)) {
return $i;
}
}
return $i;
}
sub get_logs_from_apache_conf {
my @files = `find /etc/apache2 /etc/httpd 2>/dev/null`;
my $vhost = $host;
my %vhosts;
for my $file (sort @files) {
chomp $file;
next if $file =~ /\bavailable\b/;
open my $fh, $file or next;
while (my $line = <$fh>) {
chomp $line;
if ($line =~ /^\s*<VirtualHost(|\s+([^>]*))>/ims) {
$vhost = {vname => $2};
}
elsif ($line =~ /^\s*<\/VirtualHost>/ims) {
$vhost->{name} = vhost_name($vhost);
delete $vhost->{vname};
# delete $vhost->{sname};
# The first <VirtualHost> is used if multiple identical named ones exist
if ($vhosts{$vhost->{name}}) {
$vhost->{invalid} = 1;
}
else {
$vhosts{$vhost->{name}} = $vhost;
}
$vhost = $host;
}
elsif ($line =~ /^\s*ServerName\s+(\S+)/ims) {
$vhost->{sname} = $1;
}
elsif ($line =~ /^\s*DocumentRoot\s+(.+)/ims) {
$vhost->{docroot} = unquote($1);
$vhost->{absdocroot} = Cwd::abs_path($vhost->{docroot});
}
elsif ($line =~ /^\s*ServerRoot\s+(\S+)/ims) {
$vhost->{servroot} = unquote($1);
}
elsif ($line =~ /^\s*ErrorLog(\s+(request|connection))?\s+(.+)/ims) {
my $log_file = unquote($3);
$log_file = env_replace($log_file);
my $log = add_log($log_file);
$log->{vhost} = $vhost;
$vhost->{file} = $file;
}
}
close $fh;
}
}
# Error log file names in the configs might use environment variables
# in it's definition, so we replace those variables with their values.
sub env_replace {
my ($file) = @_;
$ENV{APACHE_LOG_DIR} ||= "/var/log/apache2";
$file =~ s/\$\{(\w+)\}/$ENV{$1}/ge;
return $file;
}
sub basename {
my ($file) = @_;
$file =~ /([^\/]+)$/;
my $name = $1;
return $name;
}
sub dirname {
my ($file) = @_;
$file =~ /^((.*)\/)/;
my $dir = $2 || ".";
return $dir;
}
sub unquote {
my ($file) = @_;
return $file if !$file;
if ($file =~ /^"((\\"|[^"])*)"/) {
$file = $1;
}
elsif ($file =~ /^'((\\'|[^'])*)'/) {
$file = $1;
}
$file =~ s/\\(["'])/$1/g;
return $file;
}
sub vhost_name {
my ($vhost) = @_;
my @name;
if ($vhost->{sname}) {
push @name, $vhost->{sname};
}
if ($vhost->{vname}) {
push @name, $vhost->{vname};
}
my $name = join " ", @name;
return $name;
}
sub get_opts {
my @args;
while (my $arg = shift @ARGV) {
if ($arg =~ /^--?$/) {
push @args, @ARGV;
last;
}
elsif ($arg eq "-m") {
my $max = shift @ARGV;
if (!defined $max || $max !~ /^\d+$/) {
die "-m option requires a numerical argument.\n";
}
$opts{max} = $max;
}
elsif ($arg eq "-o") {
my $offset = shift @ARGV;
if (!defined $offset || $offset !~ /^\d+$/) {
die "-o option requires a numerical argument.\n";
}
$opts{offset} = $offset;
}
elsif ($arg eq "-f") {
my $filter = shift @ARGV;
$opts{filter} = $filter;
$opts{action} ||= "info";
}
elsif ($arg eq "-g") {
$opts{action} = "graph";
my $interval = $ARGV[0];
if (defined $interval && $interval !~ /^-/) {
shift @ARGV;
}
else {
$interval = undef;
}
if (!$interval || $interval eq "h") {
$opts{interval} = 60 * 60;
}
elsif ($interval eq "d") {
$opts{interval} = 24 * 60 * 60;
}
elsif ($interval =~ /^\d+$/) {
$opts{interval} = $interval;
}
else {
die "-g argument must be h, d, or a number.\n";
}
}
elsif ($arg =~ /^(--?help|-h)$/) {
usage();
}
elsif ($arg eq "-s") {
$opts{action} = "stats";
}
elsif ($arg eq "-i") {
$opts{action} = "info";
}
elsif ($arg eq "-l") {
$opts{action} = "list";
}
elsif ($arg eq "-L") {
$opts{action} = "detailed_list";
}
elsif ($arg eq "-p") {
$opts{action} = "print";
}
elsif ($arg eq "-r") {
$opts{rotation} = shift @ARGV;
}
elsif ($arg =~ /^-/) {
die "Invalid argument '$arg'\n";
}
else {
push @args, $arg;
}
}
if (@args > 1) {
die "Too many arguments\n";
}
$opts{name} = shift @args;
$opts{action} ||= "less";
}
sub usage {
print <<EOUSAGE;
Usage: elog [<options>] [<name>]
-f <regex> filter based on regex
-g [<interval>] graph errors
-h show this help text
-i show info spread vertically
-l list available logs
-L list available logs with details
-m <n> process a maximum of n errors, starting from the end
-o <n> process errors starting at an offset from the end
-p print log path
-r <n> rotation number
-s show statistics
<name> name of the log you are trying to access (regex),
if name contains a "/", name is treated as a file name,
default is the error log for the cwd.
By default, this command will open the log in \$PAGER or less(1)
EOUSAGE
exit;
}
__END__
=head1 NAME
elog - An Apache error log viewer
=head1 SYNOPSIS
elog [<options>] [<name>]
=head1 OPTIONS
-f <regex> filter based on regex
-g [<interval>] graph errors
-h show this help text
-i show info spread vertically
-l list available logs
-L list available logs with details
-m <n> process a maximum of n errors, starting from the end
-o <n> process errors starting at an offset from the end
-p print log path
-r <n> rotation number
-s show statistics
<name> name of the log you are trying to access (regex),
if name contains a "/", name is treated as a file name,
default is the error log for the cwd.
=head1 DESCRIPTION
This program will show the Apache error log associated with the
directory you are currently inside of.
Many people set up web servers with each website inside their own
directory in $HOME or /var/www. While working on these sites, for
example /var/www/coolsite.com/, you can run `elog` with no arguments
and it will show the error log for that site inside of less(1).
If you define the $PAGER environment variable, `elog` will use that
program instead of less(1).
If you want to view another site's error log, provide `elog` with an
expression that partially matches the name of that website's log
after the `elog` command. For example, `elog foo`.
To see a list of all the error logs on the server use `elog -l`.
More detailed information, such as what rotations exist for each
log, use `elog -L`.
To specify an older rotation of an error log, use the -r option.
For example `elog -r 2`, might show the /var/log/httpd/foo.error_log.2.gz
file. If that rotation doesn't exist, it will choose the 2nd in the list
shown when you use the -L option.
The way it determines which error log to show is by parsing Apache
config files in either /etc/httpd or /etc/apache2. An ErrorLog line
tells where the error log is, a DocRoot line tells which directory
that error log is for.
The -p option will show the path the selected error log file.
The -f option will filter based on a given regex for the -i, -s, or -g option.
The -s option will show statistics about the error log file such
as how many errors there were, and their time frame.
The -i option will show each error on a line by itself with extra info (time, ip,
etc) on the line before.
The -m option limits the maximum number of errors shown with the -i, -s, or
the -g option, starting from the end of the log (most recent).
The -o option sets an offset to the errors shown with the -i option, so
"elog -i -m 1" shows the last error, "elog -i -m 1 -o 1" shows the second
to last error.
The -g option will show a graph of the number of errors in hourly intervals. If provided an argument, it can be h for hourly, d for daily, or a number of seconds.
=head1 METACPAN
=head1 AUTHOR
Jacob Gelbman E<lt>gelbman@gmail.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2017 by Jacob Gelbman
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.18.2 or,
at your option, any later version of Perl 5 you may have available.
=cut