File Coverage

blib/lib/Log/GELF/Util.pm
Criterion Covered Total %
statement 142 148 95.9
branch 40 48 83.3
condition 10 21 47.6
subroutine 30 30 100.0
pod 11 11 100.0
total 233 258 90.3


line stmt bran cond sub pod time code
1             package Log::GELF::Util;
2 6     6   328437 use 5.010;
  6         22  
3 6     6   32 use strict;
  6         13  
  6         107  
4 6     6   35 use warnings;
  6         12  
  6         159  
5              
6             require Exporter;
7 6     6   2501 use Readonly;
  6         17622  
  6         635  
8              
9             our (
10             $VERSION,
11             @ISA,
12             @EXPORT_OK,
13             %EXPORT_TAGS,
14             $GELF_MSG_MAGIC,
15             $ZLIB_MAGIC,
16             $GZIP_MAGIC,
17             %LEVEL_NAME_TO_NUMBER,
18             %LEVEL_NUMBER_TO_NAME,
19             %GELF_MESSAGE_FIELDS,
20             $LEVEL_NAME_REGEX,
21             );
22              
23             $VERSION = "0.96";
24              
25 6         519 use Params::Validate qw(
26             validate
27             validate_pos
28             validate_with
29             SCALAR
30             ARRAYREF
31             HASHREF
32 6     6   2694 );
  6         43411  
33 6     6   2693 use Time::HiRes qw(time);
  6         5817  
  6         28  
34 6     6   3848 use Sys::Syslog qw(:macros);
  6         93029  
  6         1303  
35 6     6   2495 use Sys::Hostname;
  6         4773  
  6         310  
36 6     6   1198 use JSON::MaybeXS qw(encode_json decode_json);
  6         19455  
  6         330  
37 6     6   3100 use IO::Compress::Gzip qw(gzip $GzipError);
  6         179547  
  6         688  
38 6     6   2509 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
  6         57617  
  6         572  
39 6     6   3763 use IO::Compress::Deflate qw(deflate $DeflateError);
  6         13246  
  6         658  
40 6     6   2705 use IO::Uncompress::Inflate qw(inflate $InflateError);
  6         6269  
  6         614  
41 6     6   2359 use Math::Random::MT qw(irand);
  6         4988  
  6         33  
42              
43             Readonly $GELF_MSG_MAGIC => pack('C*', 0x1e, 0x0f);
44             Readonly $ZLIB_MAGIC => pack('C*', 0x78, 0x9c);
45             Readonly $GZIP_MAGIC => pack('C*', 0x1f, 0x8b);
46              
47             Readonly %LEVEL_NAME_TO_NUMBER => (
48             emerg => LOG_EMERG,
49             alert => LOG_ALERT,
50             crit => LOG_CRIT,
51             err => LOG_ERR,
52             warn => LOG_WARNING,
53             notice => LOG_NOTICE,
54             info => LOG_INFO,
55             debug => LOG_DEBUG,
56             );
57              
58             Readonly %LEVEL_NUMBER_TO_NAME => (
59             &LOG_EMERG => 'emerg',
60             &LOG_ALERT => 'alert',
61             &LOG_CRIT => 'crit',
62             &LOG_ERR => 'err',
63             &LOG_WARNING => 'warn',
64             &LOG_NOTICE => 'notice',
65             &LOG_INFO => 'info',
66             &LOG_DEBUG => 'debug',
67             );
68              
69             Readonly %GELF_MESSAGE_FIELDS => (
70             version => 1,
71             host => 1,
72             short_message => 1,
73             full_message => 1,
74             timestamp => 1,
75             level => 1,
76             facility => 0,
77             line => 0,
78             file => 0,
79             );
80              
81             my $ln = '^(' .
82             (join '|', (keys %LEVEL_NAME_TO_NUMBER)) .
83             ')\w*$';
84             $LEVEL_NAME_REGEX = qr/$ln/i;
85             undef $ln;
86              
87             @ISA = qw(Exporter);
88             @EXPORT_OK = qw(
89             $GELF_MSG_MAGIC
90             $ZLIB_MAGIC
91             $GZIP_MAGIC
92             %LEVEL_NAME_TO_NUMBER
93             %LEVEL_NUMBER_TO_NAME
94             %GELF_MESSAGE_FIELDS
95             validate_message
96             encode
97             decode
98             compress
99             uncompress
100             enchunk
101             dechunk
102             is_chunked
103             decode_chunk
104             parse_level
105             parse_size
106             );
107              
108             push @{ $EXPORT_TAGS{all} }, @EXPORT_OK ;
109             Exporter::export_ok_tags('all');
110              
111             sub validate_message {
112             my %p = validate_with(
113             params => \@_,
114             allow_extra => 1,
115             spec => {
116             version => {
117             default => '1.1',
118             callbacks => {
119             version_check => sub {
120 2     2   66 my $version = shift;
121 2 100       49 $version =~ /^1\.1$/
122             or die 'version must be 1.1, supplied $version';
123             },
124             },
125             },
126             host => { type => SCALAR, default => hostname() },
127             short_message => { type => SCALAR },
128             full_message => { type => SCALAR, optional => 1 },
129             timestamp => {
130             type => SCALAR,
131             default => time(),
132             callbacks => {
133             ts_format => sub {
134 2     2   29 my $ts = shift;
135 2 100       45 $ts =~ /^\d+(?:\.\d+)*$/
136             or die 'bad timestamp';
137             },
138             },
139             },
140             level => { type => SCALAR, default => 1 },
141             facility => {
142             type => SCALAR,
143             optional => 1,
144             },
145             line => {
146             type => SCALAR,
147             optional => 1,
148             callbacks => {
149             facility_check => sub {
150 3     3   81 my $line = shift;
151 3 100       71 $line =~ /^\d+$/
152             or die 'line must be a number';
153             },
154             },
155             },
156 26     26 1 14216 file => {
157             type => SCALAR,
158             optional => 1,
159             },
160             },
161             );
162              
163 19         853 $p{level} = parse_level($p{level});
164              
165 18         71 foreach my $key ( keys %p ) {
166              
167 88 50       1813 if ( ! $key =~ /^[\w\.\-]+$/ ) {
168 0         0 die "invalid field name '$key'";
169             }
170              
171 88 100 66     346 if ( $key eq '_id' ||
      66        
172             ! ( exists $GELF_MESSAGE_FIELDS{$key} || $key =~ /^_/ )
173             ) {
174 2         43 die "invalid field '$key'";
175             }
176              
177 86 100 66     804 if ( exists $GELF_MESSAGE_FIELDS{$key}
178             && $GELF_MESSAGE_FIELDS{$key} == 0 ) {
179             # field is deprecated
180 5         103 warn "$key is deprecated, send as additional field instead";
181             }
182             }
183              
184 16         398 return \%p;
185             }
186              
187             sub encode {
188 9     9 1 7606 my @p = validate_pos(
189             @_,
190             { type => HASHREF },
191             );
192              
193 8         36 return encode_json(validate_message(@p));
194             }
195              
196             sub decode {
197 3     3 1 2190 my @p = validate_pos(
198             @_,
199             { type => SCALAR },
200             );
201              
202 2         7 my $msg = shift @p;
203              
204 2         12 return validate_message(decode_json($msg));
205             }
206              
207             sub compress {
208             my @p = validate_pos(
209             @_,
210             { type => SCALAR },
211             {
212             type => SCALAR,
213             default => 'gzip',
214             callbacks => {
215             compress_type => sub {
216 5     5   13 my $level = shift;
217 5 100       75 $level =~ /^(?:zlib|gzip)$/
218             or die 'compression type must be gzip (default) or zlib';
219             },
220             },
221             },
222 9     9 1 3259 );
223              
224 6         33 my ($message, $type) = @p;
225            
226 6         16 my $method = \&gzip;
227 6         11 my $error = \$GzipError;
228 6 100       20 if ( $type eq 'zlib' ) {
229 3         8 $method = \&deflate;
230 3         6 $error = \$DeflateError;
231             }
232              
233 6         9 my $msgz;
234 6 50       13 &{$method}(\$message => \$msgz)
  6         25  
235 0         0 or die "compress failed: ${$error}";
236              
237 6         11247 return $msgz;
238             }
239              
240             sub uncompress {
241 16     16 1 7986 my @p = validate_pos(
242             @_,
243             { type => SCALAR }
244             );
245            
246 14         38 my $message = shift @p;
247            
248 14         29 my $msg_magic = substr $message, 0, 2;
249            
250 14         25 my $method;
251             my $error;
252 14 100       39 if ($ZLIB_MAGIC eq $msg_magic) {
    100          
253 2         14 $method = \&inflate;
254 2         7 $error = \$InflateError;
255             }
256             elsif ($GZIP_MAGIC eq $msg_magic) {
257 11         112 $method = \&gunzip;
258 11         18 $error = \$GunzipError;
259             }
260             else {
261             #assume plain message
262 1         35 return $message;
263             }
264              
265 13         23 my $msg;
266 13 50       23 &{$method}(\$message => \$msg)
  13         41  
267 0         0 or die "uncompress failed: ${$error}";
268              
269 13         21278 return $msg;
270             }
271              
272             sub enchunk {
273 11     11 1 5576 my @p = validate_pos(
274             @_,
275             { type => SCALAR },
276             { type => SCALAR, default => 'wan' },
277             { type => SCALAR, default => pack('L*', irand(),irand()) },
278             );
279              
280 10         348 my ($message, $size, $message_id) = @p;
281              
282 10 100       32 if ( length $message_id != 8 ) {
283 1         9 die "message id must be 8 bytes";
284             }
285              
286 9         22 $size = parse_size($size);
287              
288 7 100 100     41 if ( $size > 0
289             && length $message > $size
290             ) {
291 5         8 my @chunks;
292 5         14 while (length $message) {
293 87         204 push @chunks, substr $message, 0, $size, '';
294             }
295              
296 5         8 my $n_chunks = scalar @chunks;
297 5 50       14 die 'Message too big' if $n_chunks > 128;
298              
299 5         13 my $sequence_count = pack('C*', $n_chunks);
300              
301 5         7 my @chunks_w_header;
302 5         9 my $sequence_number = 0;
303 5         10 foreach my $chunk (@chunks) {
304 87         484 push @chunks_w_header,
305             $GELF_MSG_MAGIC
306             . $message_id
307             . pack('C*',$sequence_number++)
308             . $sequence_count
309             . $chunk;
310             }
311              
312 5         71 return @chunks_w_header;
313             }
314             else {
315 2         10 return ($message);
316             }
317             }
318              
319             sub dechunk {
320 297     297 1 2208 my @p = validate_pos(
321             @_,
322             { type => ARRAYREF },
323             { type => HASHREF },
324             );
325              
326 297         809 my ($accumulator, $chunk) = @_;
327              
328 297 0 33     669 if ( ! exists $chunk->{id}
      0        
      0        
329             && exists $chunk->{sequence_number}
330             && exists $chunk->{sequence_count}
331             && exists $chunk->{data}
332             ) {
333 0         0 die 'malformed chunk';
334             }
335              
336 297 50       617 if ($chunk->{sequence_number} > $chunk->{sequence_count} ) {
337 0         0 die 'chunk sequence number > count';
338             }
339              
340 297         585 $accumulator->[$chunk->{sequence_number}] = $chunk->{data};
341              
342 297 100       395 if ( (scalar grep {defined} @{$accumulator}) == $chunk->{sequence_count} ) {
  6342         9756  
  297         497  
343 12         24 return join '', @{$accumulator};
  12         55  
344             }
345             else {
346 285         787 return;
347             }
348             }
349              
350             sub is_chunked {
351 307     307 1 3173 my @p = validate_pos(
352             @_,
353             { type => SCALAR },
354             );
355            
356 306         712 my $chunk = shift @p;
357            
358 306         831 return $GELF_MSG_MAGIC eq substr $chunk, 0, 2;
359             }
360              
361             sub decode_chunk {
362 305     305 1 16228 my @p = validate_pos(
363             @_,
364             { type => SCALAR },
365             );
366            
367 304         760 my $encoded_chunk = shift;
368              
369 304 50       531 if ( is_chunked($encoded_chunk) ) {
370            
371 304         1732 my $id = substr $encoded_chunk, 2, 8;
372 304         613 my $seq_no = unpack('C', substr $encoded_chunk, 10, 1);
373 304         525 my $seq_cnt = unpack('C', substr $encoded_chunk, 11, 1);
374 304         486 my $data = substr $encoded_chunk, 12;
375            
376             return {
377 304         1023 id => $id,
378             sequence_number => $seq_no,
379             sequence_count => $seq_cnt,
380             data => $data,
381             };
382             }
383             else {
384 0         0 die "message not chunked";
385             }
386             }
387              
388             sub parse_level {
389 42     42 1 18553 my @p = validate_pos(
390             @_,
391             { type => SCALAR }
392             );
393            
394 40         134 my $level = shift @p;
395              
396 40 100       318 if ( $level =~ $LEVEL_NAME_REGEX ) {
    100          
397 17         99 return $LEVEL_NAME_TO_NUMBER{$1};
398             }
399             elsif ( $level =~ /^(?:0|1|2|3|4|5|6|7)$/ ) {
400 19         74 return $level;
401             }
402             else {
403 4         33 die "level must be between 0 and 7 or a valid log level string";
404             }
405             }
406              
407             sub parse_size {
408             my @p = validate_pos(
409             @_,
410             {
411             type => SCALAR,
412             callbacks => {
413             compress_type => sub {
414 16     16   35 my $size = shift;
415 16 100       244 $size =~ /^(?:lan|wan|\d+)$/i
416             or die 'chunk size must be "lan", "wan", a positve integer, or 0 (no chunking)';
417             },
418             },
419             },
420 18     18 1 7373 );
421              
422 12         66 my $size = lc(shift @p);
423              
424             # These default values below were determined by
425             # examining the code for Graylog's implementation. See
426             # https://github.com/Graylog2/gelf-rb/blob/master/lib/gelf/notifier.rb#L62
427             # I believe these are determined by likely MTU defaults
428             # and possible heasers like so...
429             # WAN: 1500 - 8 b (UDP header) - 60 b (max IP header) - 12 b (chunking header) = 1420 b
430             # LAN: 8192 - 8 b (UDP header) - 20 b (min IP header) - 12 b (chunking header) = 8152 b
431             # Note that based on my calculation the Graylog LAN
432             # default may be 2 bytes too big (8154)
433             # See http://stackoverflow.com/questions/14993000/the-most-reliable-and-efficient-udp-packet-size
434             # For some discussion. I don't think this is an exact science!
435              
436 12 100       60 if ( $size eq 'wan' ) {
    100          
437 3         5 $size = 1420;
438             }
439             elsif ( $size eq 'lan' ) {
440 2         4 $size = 8152;
441             }
442              
443 12         40 return $size;
444             }
445              
446             1;
447             __END__
448              
449             =encoding utf-8
450              
451             =head1 NAME
452              
453             Log::GELF::Util - Utility functions for Graylog's GELF format.
454              
455             =head1 SYNOPSIS
456              
457             use Log::GELF::Util qw( encode );
458              
459             my $msg = encode( { short_message => 'message', } );
460              
461              
462             use Log::GELF::Util qw( :all );
463              
464             sub process_chunks {
465              
466             my @accumulator;
467             my $msg;
468              
469             do {
470             $msg = dechunk(
471             \@accumulator,
472             decode_chunk(shift())
473             );
474             } until ($msg);
475              
476             return uncompress($msg);
477             };
478              
479             my $hr = validate_message( short_message => 'message' );
480              
481             =head1 DESCRIPTION
482              
483             Log::GELF::Util is a collection of functions and data structures useful
484             when working with Graylog's GELF Format version 1.1. It strives to support
485             all of the features and options as described in the L<GELF
486             specification|http://docs.graylog.org/en/latest/pages/gelf.html>.
487              
488             =head1 FUNCTIONS
489              
490             =head2 validate_message( short_message => $ )
491              
492             Returns a HASHREF representing the validated message with any defaulted
493             values added to the data structure.
494              
495             Takes the following message parameters as per the GELF message
496             specification:
497              
498             =over
499              
500             =item short_message
501              
502             Mandatory string, a short descriptive message
503              
504             =item version
505              
506             String, must be '1.1' which is the default.
507              
508             =item host
509              
510             String, defaults to hostname() from L<Sys::Hostname>.
511              
512             =item timestamp
513              
514             Timestamp, defaults to time() from L<Time::HiRes>.
515              
516             =item level
517              
518             Integer, equal to the standard syslog levels, default is 1 (ALERT).
519              
520             =item facility
521              
522             Deprecated, a warning will be issued.
523              
524             =item line
525              
526             Deprecated, a warning will be issued.
527              
528             =item file
529              
530             Deprecated, a warning will be issued.
531              
532             =item _[additional_field]
533              
534             Parameters prefixed with an underscore (_) will be treated as an additional
535             field. Allowed characters in field names are any word character (letter,
536             number, underscore), dashes and dots. As per the specification '_id' is
537             disallowed.
538              
539             =back
540              
541             =head2 encode( \% )
542              
543             Accepts a HASHREF representing a GELF message. The message will be
544             validated with L</validate_message>.
545              
546             Returns a JSON encoded string representing the message.
547              
548             =head2 decode( $ )
549              
550             Accepts a JSON encoded string representing the message. This will be
551             converted to a hashref and validated with L</validate_message>.
552              
553             Returns a HASHREF representing the validated message with any defaulted
554             values added to the data structure.
555              
556             =head2 compress( $ [, $] )
557              
558             Accepts a string and compresses it. The second parameter is optional and
559             can take the value C<zlib> or C<gzip>, defaulting to C<gzip>.
560              
561             Returns a compressed string.
562              
563             =head2 uncompress( $ )
564              
565             Accepts a string and uncompresses it. The compression method (C<gzip> or
566             C<zlib>) is determined automatically. Uncompressed strings are passed
567             through unaltered.
568              
569             Returns an uncompressed string.
570              
571             =head2 enchunk( $ [, $, $] )
572              
573             Accepts an encoded message (JSON string) and chunks it according to the
574             GELF chunking protocol.
575              
576             The optional second parameter is the maximum size of the chunks to produce,
577             this must be a positive integer or the special strings C<lan> or C<wan>,
578             see L</parse_size>. Defaults to C<wan>. A zero chunk size means no chunking
579             will be applied.
580              
581             The optional third parameter is the message id used to identify associated
582             chunks. This must be 8 bytes. It defaults to 8 bytes of randomness generated
583             by L<Math::Random::MT>.
584              
585             If the message size is greater than the maximum size then an array of
586             chunks is retuned, otherwise the message is retuned unaltered as the first
587             element of an array.
588              
589             =head2 dechunk( \@, \% )
590              
591             This facilitates reassembling a GELF message from a stream of chunks.
592              
593             It accepts an ARRAYREF for accumulating the chunks and a HASHREF
594             representing a decoded message chunk as produced by L</decode_chunk>.
595              
596             It returns undef if the accumulator is not complete, i.e. all chunks have
597             not yet been passed it.
598              
599             Once the accumulator is complete it returns the de-chunked message in the
600             form of a string. Note that this message may still be compressed.
601              
602             Here is an example usage:
603              
604             sub process_chunks {
605              
606             my @accumulator;
607             my $msg;
608              
609             do {
610             $msg = dechunk(
611             \@accumulator,
612             decode_chunk(shift())
613             );
614             } until ($msg);
615              
616             return uncompress($msg);
617             };
618              
619             =head2 is_chunked( $ )
620              
621             Accepts a string and returns a true value if it is a GELF message chunk.
622              
623             =head2 decode_chunk( $ )
624              
625             Accepts a GELF message chunk and returns an ARRAYREF representing the
626             unpacked chunk. Dies if the input is not a GELF chunk.
627              
628             The message consists of the following keys:
629              
630             id
631             sequence_number
632             sequence_count
633             data
634              
635             =head2 parse_level( $ )
636              
637             Accepts a C<syslog> style level in the form of a number (1-7) or a string
638             being one of C<emerg>, C<alert>, C<crit>, C<err>, C<warn>, C<notice>,
639             C<info>, or C<debug>. Dies upon invalid input.
640              
641             The string forms may also be elongated and will still be accepted. For
642             example C<err> and C<error> are equivalent.
643              
644             The associated syslog level is returned in numeric form.
645              
646             =head2 parse_size( $ )
647              
648             Accepts an integer specifying the chunk size or the special string values
649             C<lan> or C<wan> corresponding to 8154 or 1420 respectively. An explanation
650             of these values is in the code.
651              
652             Returns the passed size or the value corresponding to the C<lan> or C<wan>.
653              
654             L</parse_size> dies upon invalid input.
655              
656             =head1 CONSTANTS
657              
658             All Log::Gelf::Util constants are Readonly perl structures. You must use
659             sigils when referencing them. They can be imported individually and are
660             included when importing ':all';
661              
662             =head2 $GELF_MSG_MAGIC
663              
664             The magic number used to identify a GELF message chunk.
665              
666             =head2 $ZLIB_MAGIC
667              
668             The magic number used to identify a Zlib deflated message.
669              
670             =head2 $GZIP_MAGIC
671              
672             The magic number used to identify a gzipped message.
673              
674             =head2 %LEVEL_NAME_TO_NUMBER
675              
676             A HASH mapping the level names to numbers.
677              
678             =head2 %LEVEL_NUMBER_TO_NAME
679              
680             A HASH mapping the level numbers to names.
681              
682             =head2 %GELF_MESSAGE_FIELDS
683              
684             A HASH where each key is a valid core GELF message field name. Deprecated
685             fields are associated with a false value.
686              
687             =head1 LICENSE
688              
689             Copyright (C) Strategic Data.
690              
691             This library is free software; you can redistribute it and/or modify it
692             under the same terms as Perl itself.
693              
694             =head1 AUTHOR
695              
696             Adam Clarke E<lt>adamc@strategicdata.com.auE<gt>
697              
698             =cut