package Log::Dispatch::Gelf; use 5.010; use strict; use warnings; our $VERSION = '1.3.1'; use base qw(Log::Dispatch::Output); use Params::Validate qw(validate SCALAR HASHREF CODEREF BOOLEAN); use Log::GELF::Util qw( parse_size compress enchunk encode ); use Sys::Hostname; sub new { my $proto = shift; my $class = ref $proto || $proto; my $self = bless {}, $class; $self->_basic_init(@_); $self->_init(@_); return $self; } sub _init { my $self = shift; Params::Validate::validation_options(allow_extra => 1); my %p = validate( @_, { send_sub => { type => CODEREF, optional => 1 }, additional_fields => { type => HASHREF, optional => 1 }, host => { type => SCALAR, optional => 1 }, compress => { type => BOOLEAN, optional => 1 }, chunked => { type => SCALAR, default => 0 }, socket => { type => HASHREF, optional => 1, callbacks => { protocol_is_tcp_or_udp_or_default => sub { my ($socket) = @_; $socket->{protocol} //= 'udp'; die 'socket protocol must be tcp or udp' unless $socket->{protocol} =~ /^(?:tcp|udp)$/; }, host_must_be_set => sub { my ($socket) = @_; die 'socket host must be set' unless exists $socket->{host} && length $socket->{host} > 0; }, port_must_be_number_or_default => sub { my ($socket) = @_; $socket->{port} //= 12201; die 'socket port must be integer' unless $socket->{port} =~ /^\d+$/; }, } } } ); $p{chunked} = parse_size($p{chunked}); if (!defined $p{socket} && !defined $p{send_sub}) { die 'Must be set socket or send_sub'; } if ( defined $p{socket} && defined $p{chunked} && $p{socket}{protocol} ne 'udp' ) { die 'chunked only applicable to udp'; } $self->{host} = $p{host} // hostname(); $self->{additional_fields} = $p{additional_fields} // {}; $self->{send_sub} = $p{send_sub}; $self->{gelf_version} = '1.1'; $self->{chunked} = $p{chunked}; if ($p{socket}) { my $socket = $self->_create_socket($p{socket}); $self->{send_sub} = sub { my ($msg) = @_; $msg = compress($msg) if $p{compress}; $socket->send($_) foreach enchunk($msg, $self->{chunked}); }; } return; } sub _create_socket { my ($self, $socket_opts) = @_; require IO::Socket::INET; my $socket = IO::Socket::INET->new( PeerAddr => $socket_opts->{host}, PeerPort => $socket_opts->{port}, Proto => $socket_opts->{protocol}, ) or die "Cannot create socket: $!"; return $socket; } sub log_message { my ($self, %p) = @_; (my $short_message = $p{message}) =~ s/\n.*//s; my %additional_fields; while (my ($key, $value) = each %{ $self->{additional_fields} }) { $additional_fields{"_$key"} = $value; } while (my ($key, $value) = each %{ $p{additional_fields} }) { $additional_fields{"_$key"} = $value; } my $log_unit = { version => $self->{gelf_version}, host => $self->{host}, short_message => $short_message, level => $p{level}, full_message => $p{message}, %additional_fields, }; $self->{send_sub}->(encode($log_unit)); return; } sub log { my $self = shift; my %p = validate( @_, { additional_fields => { type => HASHREF, optional => 1, }, } ); $self->SUPER::log(@_); } 1; __END__ =encoding utf-8 =head1 NAME Log::Dispatch::Gelf - Log::Dispatch plugin for Graylog's GELF format. =head1 SYNOPSIS use Log::Dispatch; my $sender = ... # e.g. RabbitMQ queue. my $log = Log::Dispatch->new( outputs => [ #some custom sender [ 'Gelf', min_level => 'debug', additional_fields => { facility => __FILE__ }, send_sub => sub { $sender->send($_[0]) }, ], #or send to graylog via TCP/UDP socket [ 'Gelf', min_level => 'debug', additional_fields => { facility => __FILE__ }, socket => { host => 'graylog.server', port => 21234, protocol => 'tcp', } ] ], ); $log->info('It works'); $log->log( level => 'info', message => "It works\nMore details.", additional_fields => { test => 1 } ); =head1 DESCRIPTION Log::Dispatch::Gelf is a Log::Dispatch plugin which formats the log message according to Graylog's GELF Format version 1.1. It supports sending via a socket (TCP or UDP) or a user provided sender. =head1 CONSTRUCTOR The constructor takes the following parameters in addition to the standard parameters documented in L: =over =item additional_fields optional hashref of additional fields of the gelf message (no need to prefix them with _, the prefixing is done automatically). =item chunked optional scalar. An integer specifying the chunk size or the special string values 'lan' or 'wan' corresponding to 8154 or 1420 respectively. A zero chunk size means no chunking will be applied. Chunking is only applicable to UDP connections. =item compress optional scalar. If a true value the message will be gzipped with IO::Compress::Gzip. =item send_sub mandatory sub for sending the message to graylog. It is triggered after the gelf message is generated. =item socket optional hashref create tcp or udp (default behavior) socket and set C to sending via socket =back =head1 METHODS =head2 $log->log( level => $, message => $, additional_fields => \% ) In addition to the corresponding method in L this subclassed method takes an optional hashref of additional_fields for the gelf message. As in the corresponding parameter on the constructor there is no need to prefix them with an _. If the same key appears in both the constructor's and method's additional_fields then the method's value will take precedence overriding the constructor's value for the current call. The subclassed log method is still called with all parameters passed on. =head1 LICENSE Copyright (C) Avast Software. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR Miroslav Tynovsky Etynovsky@avast.comE =cut