File Coverage

blib/lib/Log/Dispatch/Gelf.pm
Criterion Covered Total %
statement 74 77 96.1
branch 17 18 94.4
condition 22 26 84.6
subroutine 16 17 94.1
pod 1 3 33.3
total 130 141 92.2


line stmt bran cond sub pod time code
1             package Log::Dispatch::Gelf;
2 5     5   3956 use 5.010;
  5         28  
3 5     5   23 use strict;
  5         9  
  5         94  
4 5     5   21 use warnings;
  5         8  
  5         204  
5              
6             our $VERSION = '1.4.0';
7              
8 5     5   25 use base qw(Log::Dispatch::Output);
  5         7  
  5         2238  
9 5     5   399006 use Params::Validate qw(validate SCALAR HASHREF CODEREF BOOLEAN);
  5         10605  
  5         448  
10              
11 5         505 use Log::GELF::Util qw(
12             parse_size
13             compress
14             enchunk
15             encode
16 5     5   1835 );
  5         314161  
17 5     5   38 use Sys::Hostname;
  5         10  
  5         4390  
18              
19             sub new {
20 20     20 0 25067 my $proto = shift;
21 20   33     90 my $class = ref $proto || $proto;
22              
23 20         32 my $self = bless {}, $class;
24              
25 20         94 $self->_basic_init(@_);
26 20         1593 $self->_init(@_);
27              
28 10         39 return $self;
29             }
30              
31             sub _init {
32 20     20   31 my $self = shift;
33              
34 20         58 Params::Validate::validation_options(allow_extra => 1);
35             my %p = validate(
36             @_,
37             {
38             send_sub => { type => CODEREF, optional => 1 },
39             short_message_sub => { type => CODEREF, optional => 1 },
40             additional_fields => { type => HASHREF, optional => 1 },
41             host => { type => SCALAR, optional => 1 },
42             compress => { type => BOOLEAN, optional => 1 },
43             chunked => { type => SCALAR, default => 0 },
44             socket => {
45             type => HASHREF,
46             optional => 1,
47             callbacks => {
48             protocol_is_tcp_or_udp_or_default => sub {
49 14     14   53 my ($socket) = @_;
50              
51 14   100     61 $socket->{protocol} //= 'udp';
52 14 100       103 die 'socket protocol must be tcp or udp' unless $socket->{protocol} =~ /^(?:tcp|udp)$/;
53             },
54             host_must_be_set => sub {
55 14     14   34 my ($socket) = @_;
56              
57 14 100 100     172 die 'socket host must be set' unless exists $socket->{host} && length $socket->{host} > 0;
58             },
59             port_must_be_number_or_default => sub {
60 13     13   27 my ($socket) = @_;
61              
62 13   100     43 $socket->{port} //= 12201;
63 13 100       145 die 'socket port must be integer' unless $socket->{port} =~ /^\d+$/;
64             },
65             }
66             }
67             }
68 20         1424 );
69              
70 14         203 $p{chunked} = parse_size($p{chunked});
71              
72 12 100 100     479 if (!defined $p{socket} && !defined $p{send_sub}) {
73 1         52 die 'Must be set socket or send_sub';
74             }
75              
76 11 100 100     50 if ( defined $p{socket}
      100        
77             && $p{chunked}
78             && $p{socket}{protocol} ne 'udp'
79             ) {
80 1         25 die 'chunked only applicable to udp';
81             }
82              
83 10   33     48 $self->{host} = $p{host} // hostname();
84 10   100     149 $self->{additional_fields} = $p{additional_fields} // {};
85 10         47 $self->{send_sub} = $p{send_sub};
86 10   100 9   84 $self->{short_message_sub} = $p{short_message_sub} // sub { $_[0] =~ s/\n.*//sr };
  9         84  
87 10         25 $self->{gelf_version} = '1.1';
88 10         16 $self->{chunked} = $p{chunked};
89              
90 10 100       23 if ($p{socket}) {
91 8         20 my $socket = $self->_create_socket($p{socket});
92              
93             $self->{send_sub} = sub {
94 5     5   1163 my ($msg) = @_;
95              
96 5 100       16 $msg = compress($msg) if $p{compress};
97 5         4493 foreach my $chunk (enchunk($msg, $self->{chunked})) {
98 84 100       6651 if ($p{socket}{protocol} ne 'udp') {
99 1         3 $chunk .= "\x00";
100             }
101 84         154 $socket->send($chunk);
102             }
103 8         28 };
104             }
105              
106 10         35 return;
107             }
108              
109             sub _create_socket {
110 8     8   14 my ($self, $socket_opts) = @_;
111              
112 8         34 require IO::Socket::INET;
113             my $socket = IO::Socket::INET->new(
114             PeerAddr => $socket_opts->{host},
115             PeerPort => $socket_opts->{port},
116             Proto => $socket_opts->{protocol},
117 8 50       35 ) or die "Cannot create socket: $!";
118              
119 8         1297 return $socket;
120             }
121              
122             sub log_message {
123 11     11 0 10926 my ($self, %p) = @_;
124              
125 11         21 my %additional_fields;
126 11         20 while (my ($key, $value) = each %{ $self->{additional_fields} }) {
  17         67  
127 6         25 $additional_fields{"_$key"} = $value;
128             }
129              
130 11         20 while (my ($key, $value) = each %{ $p{additional_fields} }) {
  13         57  
131 2         7 $additional_fields{"_$key"} = $value;
132             }
133              
134             my $log_unit = {
135             version => $self->{gelf_version},
136             host => $self->{host},
137             short_message => $self->{short_message_sub}->($p{message}),
138             level => $p{level},
139             full_message => $p{message},
140 10         45 %additional_fields,
141             };
142              
143 10         48 $self->{send_sub}->(encode($log_unit));
144              
145 10         3659 return;
146             }
147              
148             sub log {
149 0     0 1   my $self = shift;
150              
151 0           my %p = validate(
152             @_, {
153             additional_fields => {
154             type => HASHREF,
155             optional => 1,
156             },
157             }
158             );
159              
160 0           $self->SUPER::log(@_);
161             }
162              
163             1;
164             __END__
165              
166             =encoding utf-8
167              
168             =head1 NAME
169              
170             Log::Dispatch::Gelf - Log::Dispatch plugin for Graylog's GELF format.
171              
172             =head1 SYNOPSIS
173              
174             use Log::Dispatch;
175              
176             my $sender = ... # e.g. RabbitMQ queue.
177             my $log = Log::Dispatch->new(
178             outputs => [
179             #some custom sender
180             [
181             'Gelf',
182             min_level => 'debug',
183             additional_fields => { facility => __FILE__ },
184             send_sub => sub { $sender->send($_[0]) },
185             ],
186             #or send to graylog via TCP/UDP socket
187             [
188             'Gelf',
189             min_level => 'debug',
190             additional_fields => { facility => __FILE__ },
191             socket => {
192             host => 'graylog.server',
193             port => 21234,
194             protocol => 'tcp',
195             }
196             ],
197             # define callback to crop your full message to short in your own way
198             [
199             'Gelf',
200             min_level => 'debug',
201             additional_fields => { facility => __FILE__ },
202             send_sub => sub { $sender->send($_[0]) },
203             short_message_sub => sub { substr($_[0], 0, 10) }
204             ],
205             ],
206             );
207             $log->info('It works');
208              
209             $log->log(
210             level => 'info',
211             message => "It works\nMore details.",
212             additional_fields => { test => 1 }
213             );
214              
215             =head1 DESCRIPTION
216              
217             Log::Dispatch::Gelf is a Log::Dispatch plugin which formats the log message
218             according to Graylog's GELF Format version 1.1. It supports sending via a
219             socket (TCP or UDP) or a user provided sender.
220              
221             =head1 CONSTRUCTOR
222              
223             The constructor takes the following parameters in addition to the standard
224             parameters documented in L<Log::Dispatch::Output>:
225              
226             =over
227              
228             =item additional_fields
229              
230             optional hashref of additional fields of the gelf message (no need to prefix
231             them with _, the prefixing is done automatically).
232              
233             =item chunked
234              
235             optional scalar. An integer specifying the chunk size or the special
236             string values 'lan' or 'wan' corresponding to 8154 or 1420 respectively.
237             A zero chunk size means no chunking will be applied.
238              
239             Chunking is only applicable to UDP connections.
240              
241             =item compress
242              
243             optional scalar. If a true value the message will be gzipped with
244             IO::Compress::Gzip.
245              
246             =item send_sub
247              
248             mandatory sub for sending the message to graylog. It is triggered after the
249             gelf message is generated.
250              
251             =item short_message_sub
252              
253             sub for code that will crop your full message to short message. By default
254             it deletes everything after first newline character
255              
256             =item socket
257              
258             optional hashref create tcp or udp (default behavior) socket and set
259             C<send_sub> to sending via socket
260              
261             =back
262              
263             =head1 METHODS
264              
265             =head2 $log->log( level => $, message => $, additional_fields => \% )
266              
267             In addition to the corresponding method in L<Log::Dispatch::Output> this
268             subclassed method takes an optional hashref of additional_fields for the
269             gelf message. As in the corresponding parameter on the constructor there is
270             no need to prefix them with an _. If the same key appears in both the
271             constructor's and method's additional_fields then the method's value will
272             take precedence overriding the constructor's value for the current call.
273              
274             The subclassed log method is still called with all parameters passed on.
275              
276             =head1 LICENSE
277              
278             Copyright (C) Avast Software.
279              
280             This library is free software; you can redistribute it and/or modify
281             it under the same terms as Perl itself.
282              
283             =head1 AUTHOR
284              
285             Miroslav Tynovsky E<lt>tynovsky@avast.comE<gt>
286              
287             =cut