File Coverage

blib/lib/Log/GELF/Util.pm
Criterion Covered Total %
statement 143 148 96.6
branch 41 48 85.4
condition 12 21 57.1
subroutine 30 30 100.0
pod 11 11 100.0
total 237 258 91.8


line stmt bran cond sub pod time code
1             package Log::GELF::Util;
2 6     6   926468 use 5.010;
  6         31  
3 6     6   38 use strict;
  6         29  
  6         209  
4 6     6   34 use warnings;
  6         11  
  6         411  
5              
6             require Exporter;
7 6     6   3529 use Readonly;
  6         31267  
  6         949  
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 = "1.03";
24              
25 6         753 use Params::Validate qw(
26             validate
27             validate_pos
28             validate_with
29             SCALAR
30             ARRAYREF
31             HASHREF
32 6     6   3656 );
  6         74323  
33 6     6   52 use Time::HiRes qw(time);
  6         31  
  6         58  
34 6     6   4904 use Sys::Syslog qw(:macros);
  6         169155  
  6         2069  
35 6     6   4526 use Sys::Hostname;
  6         8108  
  6         1013  
36 6     6   1561 use JSON::MaybeXS qw(encode_json decode_json);
  6         45469  
  6         488  
37 6     6   6228 use IO::Compress::Gzip qw(gzip $GzipError);
  6         297271  
  6         1013  
38 6     6   3524 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
  6         100678  
  6         876  
39 6     6   4023 use IO::Compress::Deflate qw(deflate $DeflateError);
  6         21523  
  6         935  
40 6     6   3560 use IO::Uncompress::Inflate qw(inflate $InflateError);
  6         8916  
  6         782  
41 6     6   2603 use Math::Random::MT qw(irand);
  6         6500  
  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   36 my $version = shift;
121 2 100       51 $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   96 my $ts = shift;
135 2 100       52 $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   97 my $line = shift;
151 3 100       62 $line =~ /^\d+$/
152             or die 'line must be a number';
153             },
154             },
155             },
156 28     28 1 225706 file => {
157             type => SCALAR,
158             optional => 1,
159             },
160             },
161             );
162              
163 21         1129 $p{level} = parse_level($p{level});
164              
165 20         75 foreach my $key ( keys %p ) {
166              
167 100 100       2124 if ( ! ($key =~ /^[\w\.\-]+$/) ) {
168 1         12 die "invalid field name '$key'";
169             }
170              
171 99 100 100     352 if ( $key eq '_id' ||
      66        
172             ! ( exists $GELF_MESSAGE_FIELDS{$key} || $key =~ /^_/ )
173             ) {
174 1         33 die "invalid field '$key'";
175             }
176              
177 98 100 100     821 if ( exists $GELF_MESSAGE_FIELDS{$key}
178             && $GELF_MESSAGE_FIELDS{$key} == 0 ) {
179             # field is deprecated
180 5         80 warn "$key is deprecated, send as additional field instead";
181             }
182             }
183              
184 18         494 return \%p;
185             }
186              
187             sub encode {
188 9     9 1 306779 my @p = validate_pos(
189             @_,
190             { type => HASHREF },
191             );
192              
193 8         78 return encode_json(validate_message(@p));
194             }
195              
196             sub decode {
197 3     3 1 3856 my @p = validate_pos(
198             @_,
199             { type => SCALAR },
200             );
201              
202 2         9 my $msg = shift @p;
203              
204 2         21 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   15 my $level = shift;
217 5 100       62 $level =~ /^(?:zlib|gzip)$/
218             or die 'compression type must be gzip (default) or zlib';
219             },
220             },
221             },
222 9     9 1 202985 );
223              
224 6         47 my ($message, $type) = @p;
225            
226 6         15 my $method = \&gzip;
227 6         12 my $error = \$GzipError;
228 6 100       23 if ( $type eq 'zlib' ) {
229 3         10 $method = \&deflate;
230 3         9 $error = \$DeflateError;
231             }
232              
233 6         10 my $msgz;
234 6 50       15 &{$method}(\$message => \$msgz)
  6         29  
235 0         0 or die "compress failed: ${$error}";
236              
237 6         15255 return $msgz;
238             }
239              
240             sub uncompress {
241 16     16 1 11975 my @p = validate_pos(
242             @_,
243             { type => SCALAR }
244             );
245            
246 14         59 my $message = shift @p;
247            
248 14         32 my $msg_magic = substr $message, 0, 2;
249            
250 14         26 my $method;
251             my $error;
252 14 100       54 if ($ZLIB_MAGIC eq $msg_magic) {
    100          
253 2         19 $method = \&inflate;
254 2         6 $error = \$InflateError;
255             }
256             elsif ($GZIP_MAGIC eq $msg_magic) {
257 11         123 $method = \&gunzip;
258 11         19 $error = \$GunzipError;
259             }
260             else {
261             #assume plain message
262 1         38 return $message;
263             }
264              
265 13         26 my $msg;
266 13 50       24 &{$method}(\$message => \$msg)
  13         50  
267 0         0 or die "uncompress failed: ${$error}";
268              
269 13         27188 return $msg;
270             }
271              
272             sub enchunk {
273 11     11 1 7107 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         323 my ($message, $size, $message_id) = @p;
281              
282 10 100       32 if ( length $message_id != 8 ) {
283 1         14 die "message id must be 8 bytes";
284             }
285              
286 9         21 $size = parse_size($size);
287              
288 7 100 100     32 if ( $size > 0
289             && length $message > $size
290             ) {
291 5         8 my @chunks;
292 5         14 while (length $message) {
293 87         145 push @chunks, substr $message, 0, $size, '';
294             }
295              
296 5         9 my $n_chunks = scalar @chunks;
297 5 50       10 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         8 my $sequence_number = 0;
303 5         10 foreach my $chunk (@chunks) {
304 87         376 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         85 return @chunks_w_header;
313             }
314             else {
315 2         9 return ($message);
316             }
317             }
318              
319             sub dechunk {
320 306     306 1 2612 my @p = validate_pos(
321             @_,
322             { type => ARRAYREF },
323             { type => HASHREF },
324             );
325              
326 306         954 my ($accumulator, $chunk) = @_;
327              
328 306 0 33     694 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 306 50       679 if ($chunk->{sequence_number} > $chunk->{sequence_count} ) {
337 0         0 die 'chunk sequence number > count';
338             }
339              
340 306         672 $accumulator->[$chunk->{sequence_number}] = $chunk->{data};
341              
342 306 100       409 if ( (scalar grep {defined} @{$accumulator}) == $chunk->{sequence_count} ) {
  6757         10486  
  306         700  
343 12         19 return join '', @{$accumulator};
  12         85  
344             }
345             else {
346 294         1139 return;
347             }
348             }
349              
350             sub is_chunked {
351 316     316 1 219875 my @p = validate_pos(
352             @_,
353             { type => SCALAR },
354             );
355            
356 315         747 my $chunk = shift @p;
357            
358 315         1072 return $GELF_MSG_MAGIC eq substr $chunk, 0, 2;
359             }
360              
361             sub decode_chunk {
362 314     314 1 25917 my @p = validate_pos(
363             @_,
364             { type => SCALAR },
365             );
366            
367 313         762 my $encoded_chunk = shift;
368              
369 313 50       574 if ( is_chunked($encoded_chunk) ) {
370            
371 313         2026 my $id = substr $encoded_chunk, 2, 8;
372 313         802 my $seq_no = unpack('C', substr $encoded_chunk, 10, 1);
373 313         575 my $seq_cnt = unpack('C', substr $encoded_chunk, 11, 1);
374 313         511 my $data = substr $encoded_chunk, 12;
375            
376             return {
377 313         1444 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 44     44 1 28541 my @p = validate_pos(
390             @_,
391             { type => SCALAR }
392             );
393            
394 42         168 my $level = shift @p;
395              
396 42 100       447 if ( $level =~ $LEVEL_NAME_REGEX ) {
    100          
397 17         134 return $LEVEL_NAME_TO_NUMBER{$1};
398             }
399             elsif ( $level =~ /^(?:0|1|2|3|4|5|6|7)$/ ) {
400 21         85 return $level;
401             }
402             else {
403 4         39 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   36 my $size = shift;
415 16 100       245 $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 242605 );
421              
422 12         79 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       47 if ( $size eq 'wan' ) {
    100          
437 3         5 $size = 1420;
438             }
439             elsif ( $size eq 'lan' ) {
440 2         5 $size = 8152;
441             }
442              
443 12         48 return $size;
444             }
445              
446             1;
447             __END__