A perl daemon which sends structured logs from the Journal to a Gelf HTTP endpoint (made for Graylog)
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.0 KiB
127 lines
4.0 KiB
#!/usr/bin/perl
|
|
|
|
use warnings;
|
|
use strict;
|
|
use JSON;
|
|
use LWP::UserAgent;
|
|
use Encode qw(encode);
|
|
use Data::Dumper;
|
|
use Compress::Zlib;
|
|
use Getopt::Long;
|
|
use YAML::Tiny;
|
|
|
|
my $config = '/etc/systemd/journal-gelf.yml';
|
|
my $conf = {};
|
|
|
|
if (-e $config) {
|
|
print "Reading config file $config\n";
|
|
my $yaml = YAML::Tiny->read( $config ) or die "Config file $config is invalid\n";
|
|
if (not $yaml->[0]) {
|
|
die "Config file $config is invalid\n"
|
|
}
|
|
$conf = $yaml->[0];
|
|
}
|
|
|
|
GetOptions (
|
|
'state=s' => \$conf->{state},
|
|
'compress!' => \$conf->{compress},
|
|
'url=s' => \$conf->{url},
|
|
'username=s' => \$conf->{username},
|
|
'password=s' => \$conf->{password}
|
|
);
|
|
|
|
$conf->{state} //= '/var/lib/systemd-journal-gelf/state';
|
|
$conf->{compress} //= 1;
|
|
|
|
if (
|
|
not $conf->{url} or
|
|
($conf->{username} and not $conf->{password}) or
|
|
(not $conf->{username} and $conf->{password})
|
|
){
|
|
help();
|
|
die;
|
|
}
|
|
|
|
print "Starting the Systemd Journal GELF uploader daemon\n";
|
|
|
|
my $ua = LWP::UserAgent->new(
|
|
# env_proxy => 1,
|
|
keep_alive => 1
|
|
);
|
|
$ua->default_header( 'Content-Type' => 'application/json' );
|
|
if ( $conf->{compress} ){
|
|
$ua->default_header( 'Accept-Encoding' => HTTP::Message::decodable );
|
|
$ua->default_header( 'Content-Encoding' => 'gzip' );
|
|
}
|
|
|
|
# Check if the state file exists and contains a valid cursor
|
|
my $cursor_arg = '';
|
|
open CURSOR, "+<", $conf->{state};
|
|
if (-e $conf->{state}){
|
|
my $cursor = <CURSOR>;
|
|
close CURSOR;
|
|
if ($cursor and $cursor =~ m/^s=[a-z\d]+;i=[a-z\d]+;b=[a-z\d]+;m=[a-z\d]+;t=[a-z\d]+;x=[a-z\d]+$/){
|
|
print "Valid cursor found in " . $conf->{state} . ", will start back from here\n";
|
|
$cursor_arg = " --after-cursor='" . $cursor . "'";
|
|
} else {
|
|
print $conf->{state} . " contains an invalid cursor, so we're wiping it\n";
|
|
unlink $conf->{state};
|
|
}
|
|
}
|
|
|
|
open JOURNAL, "/usr/bin/journalctl -f -o json$cursor_arg |";
|
|
while (my $entry = <JOURNAL>){
|
|
my $msg = from_json($entry);
|
|
my $gelf = {
|
|
version => 1.1,
|
|
short_message => $msg->{MESSAGE},
|
|
host => $msg->{_HOSTNAME},
|
|
timestamp => int ($msg->{__REALTIME_TIMESTAMP} / (1000 * 1000)),
|
|
level => $msg->{PRIORITY}
|
|
};
|
|
# Now lets look at the message. If it starts with gelf: we can split it and have further
|
|
# fields to send. I use this to handle httpd or nginx logs for example
|
|
if ($msg->{MESSAGE} =~ m/^gelf:([a-zA-Z\d]+=([^\|])\|?)+/){
|
|
$msg->{MESSAGE} =~ s/^gelf://;
|
|
foreach (split /\|/, $msg->{MESSAGE}){
|
|
my ($key,$val) = split /=/, $_;
|
|
$gelf->{'_' . lc $key} = $val;
|
|
}
|
|
}
|
|
foreach (grep !/^MESSAGE|_HOSTNAME|__REALTIME_TIMESTAMP|PRIORITY$/, keys %$msg){
|
|
my $key = lc (($_ =~ m/^_/) ? $_ : '_' . $_);
|
|
$gelf->{$key} = $msg->{$_};
|
|
}
|
|
my $retry = 0;
|
|
my $resp;
|
|
do {
|
|
if ($retry > 0){
|
|
print "Sending message to " . $conf->{url} . " failed : got code " .
|
|
$resp->code . " (" . $resp->message . "). Tring again in $retry seconds\n";
|
|
sleep $retry;
|
|
}
|
|
$resp = $ua->post($conf->{url}, Content => Compress::Zlib::memGzip(encode('utf-8', to_json($gelf))));
|
|
$retry = ($retry > 0) ? $retry * 2 : 1;
|
|
} while ($resp->code != 202 and $retry < 600);
|
|
if ($resp->code == 202){
|
|
open CURSOR, ">", $conf->{state};
|
|
print CURSOR $msg->{__CURSOR};
|
|
close CURSOR
|
|
} else {
|
|
die "Error sending data to GELF server\n";
|
|
}
|
|
}
|
|
|
|
sub help {
|
|
print <<"_EOF"
|
|
|
|
Usage: $0 --url=<URL> [--compress|--no-compress] [--user=production --password=secr3t] [--state=/path/to/file]
|
|
|
|
* --url is the http or https URL where you will push your gelf formated logs. This is mandatory
|
|
* --compress or --no-compress : will turn on or off gzip compression of logs. Default is on, but can be usefull to disable for debugging
|
|
* --username and --password may be used if URL is protected with a basic auth mecanism. Either both or none must be provided
|
|
* --state can be used to specify where to record the last correctly sent message, so we can start from here when
|
|
systemd-journal-gelf is restarted or if there's a network problem. Default value is /var/lib/systemd-journal-gelf/state
|
|
|
|
_EOF
|
|
}
|
|
|