File Coverage

blib/lib/Parse/Syslog/Line.pm
Criterion Covered Total %
statement 129 145 88.9
branch 72 90 80.0
condition 17 30 56.6
subroutine 15 17 88.2
pod 6 6 100.0
total 239 288 82.9


line stmt bran cond sub pod time code
1             # ABSTRACT: Simple syslog line parser
2              
3             package Parse::Syslog::Line;
4              
5 4     4   943736 use warnings;
  4         45  
  4         141  
6 4     4   22 use strict;
  4         9  
  4         84  
7              
8 4     4   87 use Carp;
  4         11  
  4         363  
9 4     4   1236 use Const::Fast;
  4         8555  
  4         26  
10 4     4   1873 use English qw(-no_match_vars);
  4         11916  
  4         23  
11 4     4   1410 use Exporter;
  4         23  
  4         155  
12 4     4   1354 use HTTP::Date qw( str2time );
  4         15263  
  4         668  
13 4     4   1287 use Module::Load qw( load );
  4         4249  
  4         24  
14 4     4   1212 use Module::Loaded qw( is_loaded );
  4         2474  
  4         224  
15 4     4   28 use POSIX qw( strftime tzset );
  4         9  
  4         34  
16              
17             our $VERSION = '4.1';
18              
19             # Default for Handling Parsing
20             our $DateParsing = 1;
21             our $DateTimeCreate = 0;
22             our $EpochCreate = 1;
23             our $NormalizeToUTC = 0;
24             our $OutputTimeZone = 0;
25             our $IgnoreTimeZones = 0;
26              
27             our $ExtractProgram = 1;
28             our $PruneRaw = 0;
29             our $PruneEmpty = 0;
30             our @PruneFields = ();
31             our $FmtDate;
32              
33              
34             my %INT_PRIORITY = (
35             'emerg' => 0,
36             'alert' => 1,
37             'crit' => 2,
38             'err' => 3,
39             'warn' => 4,
40             'notice' => 5,
41             'info' => 6,
42             'debug' => 7,
43             );
44              
45             my %INT_FACILITY = (
46             #
47             # POSIX Facilities
48             'kern' => 0 << 3,
49             'user' => 1 << 3,
50             'mail' => 2 << 3,
51             'daemon' => 3 << 3,
52             'auth' => 4 << 3,
53             'syslog' => 5 << 3,
54             'lpr' => 6 << 3,
55             'news' => 7 << 3,
56             'uucp' => 8 << 3,
57             'cron' => 9 << 3,
58             'authpriv' => 10 << 3,
59             'ftp' => 11 << 3,
60             #
61             # Local Reserved
62             'local0' => 16 << 3,
63             'local1' => 17 << 3,
64             'local2' => 18 << 3,
65             'local3' => 19 << 3,
66             'local4' => 20 << 3,
67             'local5' => 21 << 3,
68             'local6' => 22 << 3,
69             'local7' => 23 << 3,
70             #
71             # Apple Additions
72             'netinfo' => 12 << 3,
73             'remoteauth' => 13 << 3,
74             'install' => 14 << 3,
75             'ras' => 15 << 3,
76             'launchd' => 24 << 3,
77             );
78              
79             const our %LOG_PRIORITY => (
80             %INT_PRIORITY,
81             reverse(%INT_PRIORITY),
82             );
83              
84             const our %LOG_FACILITY => (
85             %INT_FACILITY,
86             reverse(%INT_FACILITY),
87             );
88              
89             const our %CONV_MASK => (
90             priority => 0x07,
91             facility => 0x03f8,
92             );
93              
94              
95             our @ISA = qw(Exporter);
96             our @EXPORT = qw(parse_syslog_line);
97             our @EXPORT_OK = qw(
98             parse_syslog_line
99             preamble_priority preamble_facility
100             %LOG_FACILITY %LOG_PRIORITY
101             get_syslog_timezone set_syslog_timezone
102             use_utc_syslog
103             );
104             our %EXPORT_TAGS = (
105             constants => [ qw( %LOG_FACILITY %LOG_PRIORITY ) ],
106             preamble => [ qw(preamble_priority preamble_facility) ],
107             with_timezones => [ qw(parse_syslog_line set_syslog_timezone get_syslog_timezone use_utc_syslog) ],
108             );
109              
110             # Regex to Extract Data
111             const my %RE => (
112             IPv4 => qr/(?>(?:[0-9]{1,3}\.){3}[0-9]{1,3})/,
113             preamble => qr/(?>^\<(\d+)\>)/,
114             year => qr/(?>^(\d{4}) )/,
115             date => qr/(?>^([A-Za-z]{3}\s+[0-9]+\s+[0-9]{1,2}(?:\:[0-9]{2}){1,2}))/,
116             date_long => qr/^(?>
117             (?:[0-9]{4}\s+)? # Year: Because, Cisco
118             ([.*])? # Cisco adds a * for no ntp, and a . for configured but out of sync
119             [a-zA-Z]{3}\s+[0-9]+ # Date: Jan 1
120             (?:\s+[0-9]{4})? # Year: Because, Cisco
121             \s+ # Date Separator: spaces
122             [0-9]{1,2}(?:\:[0-9]{2}){1,2} # Time: HH:MM or HH:MM:SS
123             (?:\.[0-9]{3,6})? # Time: .DDD(DDD) ms resolution
124             (?:\s+[A-Z]{3,4})? # Timezone, ZZZ or ZZZZ
125             (?:\:?) # Cisco adds a : after the second timestamp
126             )/x,
127             date_iso8601 => qr/(?>^(
128             [0-9]{4}(?:\-[0-9]{2}){2} # Date YYYY-MM-DD
129             (?:\s|T) # Date Separator T or ' '
130             [0-9]{2}(?:\:[0-9]{2}){1,2} # Time HH:MM:SS
131             (?:\.(?:[0-9]{3}){1,2})? # Time: .DDD millisecond or .DDDDDD microsecond resolution
132             (?:[Zz]|[+\-][0-9]{2}\:[0-9]{2}) # UTC Offset +DD:MM or 'Z' indicating UTC-0
133             ))/x,
134             host => qr/^\s*([^:\s]+)\s+/,
135             cisco_hates_you => qr/^\s*[0-9]*:\s+/,
136             program_raw => qr/^\s*([^\[][^:]+):\s*/,
137             program_name => qr/^([^\[\(\ ]+)/,
138             program_sub => qr/(?>\(([^\)]+)\))/,
139             program_pid => qr/(?>\[([^\]]+)\])/,
140             program_netapp => qr/(?>\[([^\]]+)\]:\s*)/,
141             );
142              
143              
144             my %_empty_msg = map { $_ => undef } qw(
145             preamble priority priority_int facility facility_int
146             datetime_raw date_raw date time datetime_str datetime_obj epoch
147             host_raw host domain
148             program_raw program_name program_pid program_sub
149             );
150              
151              
152             my $SYSLOG_TIMEZONE = '';
153             my $DateTimeTried = 0;
154              
155             sub parse_syslog_line {
156 83     83 1 90184 my ($raw_string) = @_;
157              
158             # Initialize everything to undef
159 83 100       763 my %msg = $PruneEmpty ? () : %_empty_msg;
160 83 100       301 $msg{message_raw} = $raw_string unless $PruneRaw;
161              
162             #
163             # grab the preamble:
164 83 100       523 if( $raw_string =~ s/$RE{preamble}//o ) {
165             # Cast to integer
166 54         192 $msg{preamble} = int $1;
167              
168             # Extract Integers
169 54         127 $msg{priority_int} = $msg{preamble} & $CONV_MASK{priority};
170 54         90 $msg{facility_int} = $msg{preamble} & $CONV_MASK{facility};
171              
172             # Lookups
173 54         175 $msg{priority} = $LOG_PRIORITY{ $msg{priority_int} };
174 54         143 $msg{facility} = $LOG_FACILITY{ $msg{facility_int} };
175             }
176              
177             #
178             # Handle Date/Time
179 83         139 my $year;
180 83 100       457 if( $raw_string =~ s/$RE{year}//o ) {
181 3         12 $year = $1;
182             }
183 83 100       702 if( $raw_string =~ s/$RE{date}//o) {
    50          
184 65         173 $msg{datetime_raw} = $1;
185 65 100       165 $msg{datetime_raw} .= " $year"
186             if $year;
187             }
188             elsif( $raw_string =~ s/$RE{date_iso8601}//o) {
189 18         61 $msg{datetime_raw} = $1;
190             }
191 83 50 33     367 if( exists $msg{datetime_raw} && length $msg{datetime_raw} ) {
192 83         157 $msg{date_raw} = $msg{datetime_raw};
193              
194 83 50       169 if ( $DateParsing ) {
195             # if User wants to fight with dates himself, let him :)
196 83 100 66     233 if( $FmtDate && ref $FmtDate eq 'CODE' ) {
197 20         59 @msg{qw(date time epoch datetime_str)} = $FmtDate->($msg{datetime_raw});
198             }
199             else {
200             # Parse the Epoch
201 63         193 $msg{epoch} = HTTP::Date::str2time($msg{datetime_raw});
202              
203             # Format accordingly, keep highest resolution we can
204 63         7379 my $diff = ($msg{epoch} - int($msg{epoch}));
205 63 100       245 my $hires = $diff > 0 ? substr(sprintf('%0.6f', $diff),1) : '';
206 63 100       224 my $tm_fmt = '%FT%T' . $hires . ( $OutputTimeZone ? ($SYSLOG_TIMEZONE eq 'UTC' ? 'Z' : '%z') : '' );
    100          
207              
208             # Set the Date Strings
209             $msg{datetime_str} = $NormalizeToUTC ? strftime($tm_fmt, gmtime $msg{epoch})
210 63 100       372 : strftime($tm_fmt, localtime $msg{epoch});
211             # Split this up into parts
212 63         1088 my @parts = split /[ T]/, $msg{datetime_str};
213 63         133 $msg{date} = $parts[0];
214 63         204 $msg{time} = (split /[+\-Z]/, $parts[1])[0];
215 63 100       327 $msg{offset} = $NormalizeToUTC ? 'Z'
    100          
216             : $parts[1] =~ /([Z+\-].*)/ ? $1
217             : $SYSLOG_TIMEZONE;
218              
219             # Debugging for my sanity
220             printf("TZ=%s Parsed: %s to [%s] %s D:%s T:%s O:%s\n",
221             $SYSLOG_TIMEZONE,
222             @msg{qw(datetime_raw epoch datetime_str date time offset)},
223 63 50       203 ) if $ENV{DEBUG_PARSE_SYSLOG_LINE};
224             }
225             # Use Module::Load to runtime load the DateTime libraries *if* necessary
226 83 100       361 if( $DateTimeCreate ) {
227 6 100 66     239 unless( $DateTimeTried && is_loaded('DateTime') ) {
228 1         33 warn "DateTime seriously degrades performance, please start using 'epoch' and/or 'datetime_str' instead.";
229             eval {
230 1         6 load DateTime;
231 1         73 1;
232 1 50       3 } or do {
233 0         0 my $err = $@;
234 0         0 warn "DateTime unavailable, disabling it: $err";
235 0         0 $DateTimeCreate = 0;
236             };
237 1         2 $DateTimeTried++;
238             }
239 6 50       93 eval {
240 6         123 my %args = (epoch => $msg{epoch});
241 6 50       22 $args{time_zone} = $SYSLOG_TIMEZONE if $SYSLOG_TIMEZONE;
242 6         30 $msg{datetime_obj} = DateTime->from_epoch(%args);
243             } if $DateTimeCreate;
244             }
245             }
246             }
247              
248             #
249             # Host Information:
250 83 100       16502 if( $raw_string =~ s/$RE{host}//o ) {
251 80         188 my $hostStr = $1;
252 80         376 my($ip) = ($hostStr =~ /($RE{IPv4})/o);
253 80 100 66     290 if( defined $ip && length $ip ) {
    50          
254 21         54 $msg{host_raw} = $hostStr;
255 21         50 $msg{host} = $ip;
256             }
257             elsif( length $hostStr ) {
258 59         171 my ($host,$domain) = split /\./, $hostStr, 2;
259 59         118 $msg{host_raw} = $hostStr;
260 59         93 $msg{host} = $host;
261 59         108 $msg{domain} = $domain;
262             }
263             }
264 83 100       337 if( $raw_string =~ s/$RE{cisco_hates_you}//o ) {
265             # Yes, Cisco adds a second timestamp to it's messages, because it hates you.
266 23 100       155 if( $raw_string =~ s/$RE{date_long}//o ) {
267             # Cisco encodes the status of NTP in the second datestamp, so let's pass it back
268 17 100       51 if ( my $ntp = $1 ) {
269 6 50       29 $msg{ntp} = $ntp eq '.' ? 'out of sync'
    100          
270             : $ntp eq '*' ? 'not configured'
271             : 'unknown';
272             }
273             else {
274 11         26 $msg{ntp} = 'ok';
275             }
276             }
277             }
278              
279             #
280             # Parse the Program portion
281 83 100       164 if( $ExtractProgram ) {
282 63 100       301 if( $raw_string =~ s/$RE{program_raw}//o ) {
    50          
283 57         149 $msg{program_raw} = $1;
284 57         185 my $progStr = join ' ', grep {!exists $INT_PRIORITY{$_}} split /\s+/, $msg{program_raw};
  68         298  
285 57 50 33     241 if( defined $progStr && length $progStr) {
286 57 50       269 if( ($msg{program_name}) = ($progStr =~ /$RE{program_name}/o) ) {
287 57 100       155 if (length $msg{program_name} != length $msg{program_raw} ) {
288             (($msg{program_pid}) = ($progStr =~ /$RE{program_pid}/o))
289 14 100       85 || (($msg{program_sub}) = ($progStr =~ /$RE{program_sub}/o))
290             }
291             }
292             }
293             }
294             elsif( $raw_string =~ s/$RE{program_netapp}//o ) {
295             # Check for a [host thing.subthing:level]: tag
296             # or [host:thing.subthing:level]: tag, Thanks NetApp.
297 6         15 my $subStr = $1;
298 6         14 $msg{program_raw} = qq{[$subStr]};
299 6         24 my ($host,$program,$level) = split /[: ]+/, $subStr;
300 6         12 $msg{program_name} = $program;
301 6 0 33     15 if(!exists $msg{priority} && exists $LOG_PRIORITY{$level}) {
302 0         0 $msg{priority} = $level;
303 0         0 $msg{priority_int} = $LOG_PRIORITY{$level};
304             }
305 6         15 $raw_string =~ s/^[ :]+//;
306             }
307             }
308             else {
309 20         42 $raw_string =~ s/^\s+//;
310             }
311              
312             # The left overs should be the message
313 83         169 $msg{content} = $raw_string;
314 83         137 chomp $msg{content};
315 83 100       255 $msg{message} = defined $msg{program_raw} ? "$msg{program_raw}: $msg{content}" : $msg{content};
316              
317 83 100       175 if( $PruneRaw ) {
318 9         59 delete $msg{$_} for grep { $_ =~ /_raw$/ } keys %msg;
  158         297  
319             }
320 83 100       158 if( $PruneEmpty ) {
321 9   100     23 delete $msg{$_} for grep { !defined $msg{$_} || !length $msg{$_} } keys %msg;
  122         297  
322             }
323 83 50       165 if( @PruneFields ) {
324 4     4   11068 no warnings;
  4         9  
  4         2268  
325 0         0 delete $msg{$_} for @PruneFields;
326             }
327 83 50 33     320 delete $msg{epoch} if exists $msg{epoch} and !$EpochCreate;
328              
329             #
330             # Return our hash reference!
331 83         283 return \%msg;
332             }
333              
334              
335             sub preamble_priority {
336 0     0 1 0 my $preamble = int shift;
337              
338 0         0 my %hash = (
339             preamble => $preamble,
340             );
341              
342 0         0 $hash{as_int} = $preamble & $CONV_MASK{priority};
343 0         0 $hash{as_text} = $LOG_PRIORITY{ $hash{as_int} };
344              
345 0         0 return \%hash;
346             }
347              
348              
349             sub preamble_facility {
350 0     0 1 0 my $preamble = int shift;
351              
352 0         0 my %hash = (
353             preamble => $preamble,
354             );
355              
356 0         0 $hash{as_int} = $preamble & $CONV_MASK{facility};
357 0         0 $hash{as_text} = $LOG_FACILITY{ $hash{as_int} };
358              
359 0         0 return \%hash;
360              
361             }
362              
363             sub set_syslog_timezone {
364 8     8 1 30887 my ( $tz_name ) = @_;
365              
366 8 50 66     85 if( defined $tz_name && (!exists $ENV{TZ} || $tz_name ne $ENV{TZ}) ) {
      66        
367 8         50 $ENV{TZ} = $SYSLOG_TIMEZONE = $tz_name;
368 8         148 tzset();
369 8         20 $OutputTimeZone = 1;
370             # Output some useful debug information
371             printf("set_syslog_timezone('%s') results in a timezone of '%s' with offset: %s\n",
372             $tz_name,
373             strftime('%Z', localtime),
374             strftime('%z', localtime),
375 8 50       26 ) if $ENV{DEBUG_PARSE_SYSLOG_LINE};
376             }
377              
378 8         20 return $SYSLOG_TIMEZONE;
379             }
380              
381             sub get_syslog_timezone {
382 1     1 1 699 return $SYSLOG_TIMEZONE;
383             }
384              
385             # If you have a syslog which logs dates in UTC, then processing will be much, much faster
386             sub use_utc_syslog {
387 1     1 1 6379 set_syslog_timezone('UTC');
388 1         3 $NormalizeToUTC = 1;
389 1         2 return;
390             }
391              
392             1; # End of Parse::Syslog::Line
393              
394             __END__
395              
396             =pod
397              
398             =encoding UTF-8
399              
400             =head1 NAME
401              
402             Parse::Syslog::Line - Simple syslog line parser
403              
404             =head1 VERSION
405              
406             version 4.1
407              
408             =head1 SYNOPSIS
409              
410             I wanted a very simple log parser for network based syslog input.
411             Nothing existed that simply took a line and returned a hash ref all
412             parsed out.
413              
414             use Parse::Syslog::Line qw(parse_syslog_line);
415              
416             $Parse::Syslog::Line::DateTimeCreate = 1;
417              
418             my $href = parse_syslog_line( $msg );
419             #
420             # $href = {
421             # preamble => '13',
422             # priority => 'notice',
423             # priority_int => 5,
424             # facility => 'user',
425             # facility_int => 8,
426             # date => 'YYYY-MM-DD',
427             # time => 'HH::MM:SS',
428             # epoch => 1361095933,
429             # datetime_str => ISO 8601 datetime, $NormalizeToUTC = 1 then UTC, else local
430             # datetime_obj => undef, # If $DateTimeCreate = 1, else undef
431             # datetime_raw => 'Feb 17 11:12:13'
432             # date_raw => 'Feb 17 11:12:13'
433             # host_raw => 'hostname', # Hostname as it appeared in the message
434             # host => 'hostname', # Hostname without domain
435             # domain => 'blah.com', # if provided
436             # program_raw => 'sshd(blah)[pid]',
437             # program_name => 'sshd',
438             # program_sub => 'pam_unix',
439             # program_pid => 20345,
440             # content => 'the rest of the message'
441             # message => 'program[pid]: the rest of the message',
442             # message_raw => 'The message as it was passed',
443             # ntp => 'ok', # Only set for Cisco messages
444             # };
445             ...
446              
447             =head1 EXPORT
448              
449             Exported by default:
450             parse_syslog_line( $one_line_of_syslog_message );
451              
452             Optional Exports:
453             :preamble
454             preamble_priority
455             preamble_facility
456              
457             :constants
458             %LOG_FACILITY
459             %LOG_PRIORITY
460              
461             :with_timezones
462             set_syslog_timezone
463             get_syslog_timezone
464             use_utc_syslog
465              
466             =head1 VARIABLES
467              
468             =head2 ExtractProgram
469              
470             If this variable is set to 1 (the default), parse_syslog_line() will try it's
471             best to extract a "program" field from the input. This is the most expensive
472             set of regex in the module, so if you don't need that pre-parsed, you can speed
473             the module up significantly by setting this variable.
474              
475             Vendors who do proprietary non-sense with their syslog formats are to blame for
476             this setting.
477              
478             Usage:
479              
480             $Parse::Syslog::Line::ExtractProgram = 0;
481              
482             =head2 DateParsing
483              
484             If this variable is set to 0 raw date will not be parsed further into components (datetime_str date time epoch).
485             Default is 1 (parsing enabled).
486              
487             Usage:
488              
489             $Parse::Syslog::Line::DateParsing = 0;
490              
491             =head2 DateTimeCreate
492              
493             If this variable is set to 1 (the default), a DateTime object will be returned in the
494             $m->{datetime_obj} field. Otherwise, this will be skipped.
495              
496             NOTE: DateTime timezone calculation is fairly slow. Unless you really need to
497             take timezones into account, you're better off using other modes (below).
498              
499             Usage:
500              
501             $Parse::Syslog::Line::DateTimeCreate = 0;
502              
503             =head2 EpochCreate
504              
505             If this variable is set to 1, the default, the number of seconds from UNIX
506             epoch will be returned in the $m->{epoch} field. Setting this to false will
507             only delete the epoch before returning the hash reference.
508              
509             =head2 NormalizeToUTC
510              
511             When set, the datetime_str will be ISO8601 UTC.
512              
513             =head2 OutputTimeZones
514              
515             Default is false, but is enabled if you call set_syslog_timezone() or
516             use_utc_syslog(). If enabled, this will append the timezone offset to the
517             datetime_str.
518              
519             =head2 FmtDate
520              
521             You can pass your own formatter/parser here. Given a raw datetime string it
522             should output a list containing date, time, epoch, datetime_str,
523             in your wanted format.
524              
525             use Parse::Syslog::Line;
526              
527             local $Parse::Syslog::Line::FmtDate = sub {
528             my ($raw_datestr) = @_;
529             my @elements = (
530             #date
531             #time
532             #epoch
533             #datetime_str
534             );
535             return @elements;
536             };
537              
538             B<NOTE>: No further date processing will be done, you're on your own here.
539              
540             =head2 PruneRaw
541              
542             This variable defaults to 0, set to 1 to delete all keys in the return hash ending in "_raw"
543              
544             Usage:
545              
546             $Parse::Syslog::Line::PruneRaw = 1;
547              
548             =head2 PruneEmpty
549              
550             This variable defaults to 0, set to 1 to delete all keys in the return hash which are undefined.
551              
552             Usage:
553              
554             $Parse::Syslog::Line::PruneEmpty = 1;
555              
556             =head2 PruneFields
557              
558             This should be an array of fields you'd like to be removed from the hash reference.
559              
560             Usage:
561              
562             @Parse::Syslog::Line::PruneFields = qw(date_raw facility_int priority_int);
563              
564             =head1 FUNCTIONS
565              
566             =head2 parse_syslog_line
567              
568             Returns a hash reference of syslog message parsed data.
569              
570             B<NOTE>: Date/time parsing is hard. This module has been optimized to balance
571             common sense and processing speed. Care is taken to ensure that any data input
572             into the system isn't lost, but with the varieties of vendor and admin crafted
573             date formats, we don't always get it right. Feel free to override date
574             processing using by setting the $FmtDate variable or completely disable it with
575             $DateParsing set to 0.
576              
577             =head2 set_syslog_timezone($timezone_name)
578              
579             Sets a timezone $timezone_name for parsed messages. This timezone will be used
580             to calculate offset from UTC if a timezone designation is not present in the
581             message being parsed. This timezone will also serve as the source timezone for
582             the datetime_str field.
583              
584             =head2 get_syslog_timezone
585              
586             Returns the name of the timezone currently set by set_syslog_timezone.
587              
588             =head2 use_utc_syslog
589              
590             A convenient function which sets the syslog timezone to UTC and sets the config
591             variables accordingly. Automatically sets $NormaizeToUTC and datetime_str will
592             be set to the UTC equivalent.
593              
594             =head2 preamble_priority
595              
596             Takes the Integer portion of the syslog messsage and returns
597             a hash reference as such:
598              
599             $prioRef = {
600             'preamble' => 13
601             'as_text' => 'notice',
602             'as_int' => 5,
603             };
604              
605             =head2 preamble_facility
606              
607             Takes the Integer portion of the syslog messsage and returns
608             a hash reference as such:
609              
610             $facRef = {
611             'preamble' => 13
612             'as_text' => 'user',
613             'as_int' => 8,
614             };
615              
616             =head1 DEVELOPMENT
617              
618             This module is developed with Dist::Zilla. To build from the repository, use Dist::Zilla:
619              
620             dzil authordeps --missing |cpanm
621             dzil listdeps --missing |cpanm
622             dzil build
623             dzil test
624              
625             =head1 AUTHOR
626              
627             Brad Lhotsky <brad@divisionbyzero.net>
628              
629             =head1 COPYRIGHT AND LICENSE
630              
631             This software is Copyright (c) 2017 by Brad Lhotsky.
632              
633             This is free software, licensed under:
634              
635             The (three-clause) BSD License
636              
637             =head1 CONTRIBUTORS
638              
639             =for stopwords BartÅ‚omiej Fulanty Csillag Tamas Keedi Kim Mateu X Hunter Neil Bowers Shawn Wilson Tomohiro Hosaka
640              
641             =over 4
642              
643             =item *
644              
645             BartÅ‚omiej Fulanty <starlight@cpan.org>
646              
647             =item *
648              
649             Csillag Tamas <cstamas@digitus.itk.ppke.hu>
650              
651             =item *
652              
653             Keedi Kim <keedi.k@gmail.com>
654              
655             =item *
656              
657             Mateu X Hunter <mhunter@maxmind.com>
658              
659             =item *
660              
661             Neil Bowers <neil@bowers.com>
662              
663             =item *
664              
665             Shawn Wilson <swilson@korelogic.com>
666              
667             =item *
668              
669             Tomohiro Hosaka <bokutin@bokut.in>
670              
671             =back
672              
673             =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
674              
675             =head1 SUPPORT
676              
677             =head2 Websites
678              
679             The following websites have more information about this module, and may be of help to you. As always,
680             in addition to those websites please use your favorite search engine to discover more resources.
681              
682             =over 4
683              
684             =item *
685              
686             MetaCPAN
687              
688             A modern, open-source CPAN search engine, useful to view POD in HTML format.
689              
690             L<http://metacpan.org/release/Parse-Syslog-Line>
691              
692             =item *
693              
694             RT: CPAN's Bug Tracker
695              
696             The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.
697              
698             L<https://rt.cpan.org/Public/Dist/Display.html?Name=Parse-Syslog-Line>
699              
700             =back
701              
702             =head2 Source Code
703              
704             This module's source code is available by visiting:
705             L<https://github.com/reyjrar/Parse-Syslog-Line>
706              
707             =cut