#!/usr/bin/env perl
# helper tool to make gnunet logs more readable
# try 'gnunet-logread -h' for usage

use strict;
use warnings;
my $DEFAULT_SOCKET = '/tmp/gnunet-logread-ipc.sock';

print STDERR <<X if -t STDIN and $#ARGV == -1;
*** For a usage message, try '$0 -h'.
*** For documentation, try 'perldoc $0'.
*** Listening for GNUNET_log events on STDIN. Type CTRL-D to terminate.

X

use Getopt::Std;
my (%opts, $name, $ipc, $msg_level, $msg_regex);
getopts ('i:x:n:s:L:m:fhq', \%opts);

use Pod::Usage qw( pod2usage );
die pod2usage if $opts{h};

use POSIX qw(mkfifo);

use Term::ANSIColor qw(:constants :pushpop);
$Term::ANSIColor::AUTOLOCAL = 1;

my %levels = ( NONE => 0, ERROR => 1, WARNING => 2, INFO => 4, DEBUG => 8 );

# Message type numbers to names
my %msgtypes;
my $prefix = $ENV{GNUNET_PREFIX} || '/usr';
my $filename = "$prefix/include/gnunet/gnunet_protocols.h";
$ipc = $opts{s} || $DEFAULT_SOCKET;

if (open HEADER, $filename)
{
    while (<HEADER>)
    {
        $msgtypes{$2} = $1 if /^\s*#define\s+GNUNET_MESSAGE_TYPE_(\w+)\s+(\d+)/i;
    }
    close HEADER;
} else {
    warn <<X;
Could not read $filename for message codes:
	$!.
Please provide a \$GNUNET_PREFIX environment variable to replace "/usr".
Try also '$0 -h' for help.

X
}

die "You can't read and write the socket at the same time"
  if exists $opts{f} and exists $opts{n};

if ((exists $opts{n} or exists $opts{f}) and not -r $ipc) {
    undef $!;
    die "Could not mkfifo $ipc: $!" unless mkfifo $ipc, 0600;
    system('chgrp', 'gnunet', $ipc);
    die "Could not chgrp $ipc to 'gnunet': $!" if $!;
    chmod 0660, $ipc;
    die "Could not chmod $ipc to allow gnunet group writes: $!" if $!;
}

if (exists $opts{n}) {
    $name = $opts{n};
    $msg_level = $opts{L} && exists $levels{$opts{L}} ? $levels{$opts{L}} : 0;
    $msg_regex = $opts{m};
    print STDERR "RE: /$msg_regex/\n" if defined $msg_regex;
    open O, '>', $ipc or die "Cannot write to $ipc: $!";
}

if (exists $opts{f}) {
    open(I, $ipc) or die "Cannot read from $ipc: $!";
    &perform while <I>;
    close I;
} else {
    &perform while <>;
}
fileno O and close O;
exit;


sub perform {
    if (fileno O) {
        my ($time, $type, $size, $from, $to, $level, $msg);
        if (($time, $type, $size, $from, $to) =
            /^([A-Z][a-z]{2}\ .[0-9]\ [0-9:]{8}(?:-[0-9]{6})?)\ util-client-.*\b
             (?: Received | Transmitting )\ message \b.*?\b
             type \s+ (\d+) \b.*?\b
             size \s+ (\d+) \b.*?\b
             (?: from \s+ (\S+)
               | to   \s+ (\S+) ) /x)
        {
            $from ||= $name;
            $to ||= $name;
            my ($time, $type, $size, $from, $to) = ($1, $2, $3,
                                                $4 || $name, $5 || $name);
            my $msg = exists $msgtypes{$type} ? $msgtypes{$type} : $type;
            my $ofh = select O;
            print O "$time\t$from -> $to\t$msg ($size)\n";
            $| = 1;
            select $ofh;
        }
        if (($time, $level, $msg) =
            /^([A-Z][a-z]{2}\ .[0-9]\ [0-9:]{8}(?:-[0-9]{6})?)
              \s+\S+\s+(\S+)\s+(.+)/x
            and (exists $levels{$level}
                 && $levels{$level} <= $msg_level
                 && (!defined $msg_regex || $msg =~ /$msg_regex/i)))
        {
            print O "$time\t$name\t$level: $msg\n";
        }
    }
    return if $opts{x} and /$opts{x}/io;
    return if $opts{i} and not /$opts{i}/io;

    # Timestamp (e.g. Nov 01 19:36:11-384136)
    s/^([A-Z][a-z]{2} .[0-9] [0-9:]{8}(?:-[0-9]{6})?)/YELLOW $1/e;

    # Log levels
    s/\b(ERROR  )\b/RED $1/ex;
    s/\b(WARNING)\b/YELLOW $1/ex;
    s/\b(INFO   )\b/GREEN $1/ex;
    s/\b(DEBUG  )\b/BRIGHT_BLACK $1/ex;

    # Service names
    # TODO: might read the list from $GNUNET_PREFIX/libexec/gnunet/
    s/\b(multicast|psyc|psycstore|social)\b/BLUE $1/gex;

    # Add message type names
    s/(\s+type\s+)(\d+)/
      $1 . BRIGHT_CYAN (exists $msgtypes{$2} ? $msgtypes{$2} : 'UNKNOWN') .
      CYAN " ($2)"/gei;

    # logread-ipc output
    s/(\s+)([A-Z_]+)( \(\d+\))$/$1 . BRIGHT_CYAN $2 . CYAN $3/e;

    print;
}

__END__

=pod

=head1 NAME

gnunet-logread - a GNUnet log analyzer, colorizer and aggregator

=head1 SYNOPSIS

	<gnunet-service> |& $0 [<options>]
    or
	$0 [<options>] [<logfile>]

 Options:
    -f				Follow input from IPC FIFO socket.

   Regular screen output options:
    -i <regex>			Include only messages that match <regex>.
    -x <regex>			Exclude all messages that match <regex>.
    -q				Quiet: Do not show usage advice to new users.

   Options to forward messages to the IPC FIFO socket:
    -n <component_name>		Name of the component we are forwarding messages for.
    -s </path/to/ipc.sock>	Default = $DEFAULT_SOCKET
    -L <LOGLEVEL>		Minimum level of messages to forward:
                                Log levels: NONE, ERROR, WARNING, INFO, DEBUG.
    -m <regex>		        Only forward messages matching a regular expression.

 See 'perldoc gnunet-logread' for a longer explanation.

=head1 MOTIVATION

GNUnet debug logs are a tedious read, but given a complex system that we
cannot run all parts of in a debugger all the time, some gathering and
structuring of events and message passing is useful.

At first, this tool simply makes logs easier to read. Both if viewed in
real-time or taken from disk. Then it also allows to extract all message
passing events from it and forward them to a special process that aggregates
all message passing events and therefore helps you make sense of all the
inter-process communication (IPC) happening between the various pieces of
the GNUnet system beast.

That master process is simply an extra gnunet-logread that you run in a
separate window and adorn it with the '-f' flag. The submitting processes
instead need to be given a '-n' flag. That is because from the GNUnet logs
it isn't clear which process events belong to. For example you may be
having events taking place in the 'util' subsystem of gnunet-psyc-service
just as much as in the 'util' subsystem of gnunet-multicast-service. In
order to make sense of them it is necessary to manually add that info. This
could be remedied by extending the semantics of the GNUNET_log facility
instead, but that is still subject to further consideration.

=head1 AUTHORS

tg & lynX
