#!/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 = ; 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, "journalctl -f -o json$cursor_arg |"; while (my $entry = ){ 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} }; 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= [--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 }