| 1 | package Parse::Syslog::Period::Parser; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | use warnings; |
|---|
| 5 | use Carp qw/croak/; |
|---|
| 6 | use base qw/ Class::Data::Inheritable Class::Accessor /; |
|---|
| 7 | use DateTime; |
|---|
| 8 | use Tie::IxHash; |
|---|
| 9 | use Path::Class qw(); |
|---|
| 10 | use UNIVERSAL::require; |
|---|
| 11 | use Data::Dumper; |
|---|
| 12 | |
|---|
| 13 | our $VERSION = '0.01'; |
|---|
| 14 | |
|---|
| 15 | __PACKAGE__->mk_classdata( parse_delay => 10 ); |
|---|
| 16 | |
|---|
| 17 | __PACKAGE__->mk_ro_accessors(qw/ |
|---|
| 18 | file |
|---|
| 19 | from |
|---|
| 20 | to |
|---|
| 21 | rotate |
|---|
| 22 | rotate_at |
|---|
| 23 | rotate_statefile |
|---|
| 24 | patterned_file |
|---|
| 25 | logfiles |
|---|
| 26 | /); |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | tie( my %macro_map, 'Tie::IxHash' ); |
|---|
| 30 | %macro_map = ( |
|---|
| 31 | '$YEAR' => { pattern => '%Y', rotate => 'daily' }, |
|---|
| 32 | '$MONTH' => { pattern => '%m', rotate => 'monthly' }, |
|---|
| 33 | '$WEEK' => { pattern => '%W', rotate => 'weekly' }, |
|---|
| 34 | '$DAY' => { pattern => '%d', rotate => 'daily' }, |
|---|
| 35 | '$HOUR' => { pattern => '%H', rotate => 'hourly' }, |
|---|
| 36 | ); |
|---|
| 37 | |
|---|
| 38 | my %increment_map = ( |
|---|
| 39 | yearly => { years => 1 }, |
|---|
| 40 | monthly => { months=> 1 }, |
|---|
| 41 | weekly => { weeks => 1 }, |
|---|
| 42 | daily => { days => 1 }, |
|---|
| 43 | hourly => { hours => 1 }, |
|---|
| 44 | ); |
|---|
| 45 | |
|---|
| 46 | sub new { |
|---|
| 47 | my $class = shift; |
|---|
| 48 | my $self = bless $_[0], $class; |
|---|
| 49 | $self->_init; |
|---|
| 50 | return $self; |
|---|
| 51 | } |
|---|
| 52 | |
|---|
| 53 | sub _init { |
|---|
| 54 | my $self = shift; |
|---|
| 55 | my $class = ref $self; |
|---|
| 56 | $self->reset; |
|---|
| 57 | |
|---|
| 58 | $self->{parse_module}->require or croak $@; |
|---|
| 59 | |
|---|
| 60 | $self->from or croak "required 'from'"; |
|---|
| 61 | $self->from->set_time_zone('local'); |
|---|
| 62 | DateTime->compare( $self->from, DateTime->now ) == 1 |
|---|
| 63 | and croak "'from' must be past from now"; |
|---|
| 64 | $self->{from_time} = $self->from->epoch; |
|---|
| 65 | |
|---|
| 66 | if ( $self->to ) { |
|---|
| 67 | $self->to->set_time_zone( 'local' ); |
|---|
| 68 | DateTime->compare( $self->from, $self->to ) == 1 |
|---|
| 69 | and croak "must be future 'to' from 'from'"; |
|---|
| 70 | $self->{to_time} = $self->to->epoch; |
|---|
| 71 | $self->{delayed_to} = $self->to->clone->add( seconds => $class->parse_delay ); |
|---|
| 72 | $self->{delayed_to_time} = $self->{delayed_to}->epoch; |
|---|
| 73 | } |
|---|
| 74 | |
|---|
| 75 | $self->rotate eq 'macro'? $self->set_logfiles_by_macro |
|---|
| 76 | : $self->set_logfiles_by_legacy; |
|---|
| 77 | return $self; |
|---|
| 78 | } |
|---|
| 79 | |
|---|
| 80 | sub reset { |
|---|
| 81 | my $self = shift; |
|---|
| 82 | $self->{sp} = undef; |
|---|
| 83 | $self->{exit_flag} = 0; |
|---|
| 84 | $self->{target_idx} = 0; |
|---|
| 85 | return $self; |
|---|
| 86 | } |
|---|
| 87 | |
|---|
| 88 | sub set_logfiles_by_macro { |
|---|
| 89 | my $self = shift; |
|---|
| 90 | |
|---|
| 91 | my $from = $self->from; |
|---|
| 92 | my $to = $self->{delayed_to} ||= DateTime->now->set_time_zone( 'local' ); |
|---|
| 93 | $self->{logfiles} = []; |
|---|
| 94 | $self->{rotate} = undef; |
|---|
| 95 | $self->{patterned_file} = $self->file; |
|---|
| 96 | |
|---|
| 97 | for my $macro ( keys %macro_map ) { |
|---|
| 98 | my ($pattern, $rotate) = @{ $macro_map{$macro} }{qw/ pattern rotate /}; |
|---|
| 99 | $self->{patterned_file} =~ s/\Q$macro\E/$pattern/g |
|---|
| 100 | and $self->{rotate} = $rotate; |
|---|
| 101 | } |
|---|
| 102 | |
|---|
| 103 | if ( my $rotate = $self->rotate ) { |
|---|
| 104 | my $increment = $increment_map{$rotate}; |
|---|
| 105 | ( my $truncate_to = (keys %$increment)[0] ) =~ s/s$//; |
|---|
| 106 | my $rotated_on = $from->clone->truncate( to => $truncate_to ); |
|---|
| 107 | |
|---|
| 108 | while ( DateTime->compare( $rotated_on, $to ) != 1 ) { |
|---|
| 109 | my $logfile = { |
|---|
| 110 | name => $rotated_on->strftime( $self->patterned_file ), |
|---|
| 111 | year => $rotated_on->year, |
|---|
| 112 | }; |
|---|
| 113 | push @{ $self->logfiles }, $logfile; |
|---|
| 114 | $rotated_on->add( $increment ); |
|---|
| 115 | } |
|---|
| 116 | |
|---|
| 117 | } else { |
|---|
| 118 | $self->{logfiles} = [ |
|---|
| 119 | { name => $self->patterned_file, |
|---|
| 120 | year => $from->year, |
|---|
| 121 | }, |
|---|
| 122 | ]; |
|---|
| 123 | } |
|---|
| 124 | |
|---|
| 125 | return $self; |
|---|
| 126 | } |
|---|
| 127 | |
|---|
| 128 | sub set_logfiles_by_legacy { |
|---|
| 129 | my $self = shift; |
|---|
| 130 | |
|---|
| 131 | my $from = $self->from; |
|---|
| 132 | my $to = $self->{delayed_to} ||= DateTime->now->set_time_zone( 'local' ); |
|---|
| 133 | $self->{logfiles} = []; |
|---|
| 134 | |
|---|
| 135 | my $rotate = $self->rotate |
|---|
| 136 | or croak "required rotate"; |
|---|
| 137 | ( $rotate =~ /^(monthly|weekly|daily)$/ ) |
|---|
| 138 | or croak "illegal rotate type '$rotate'"; |
|---|
| 139 | |
|---|
| 140 | my $at = $self->rotate_at |
|---|
| 141 | or croak "required rotate_at(hour and minute)"; |
|---|
| 142 | my ($rotate_hour, $rotate_minute) = @$at{qw/ hour minute /}; |
|---|
| 143 | ( $rotate_hour and $rotate_minute ) |
|---|
| 144 | or croak "rotate_at must contain hour and minute"; |
|---|
| 145 | |
|---|
| 146 | my ($unit_type) = %{ $increment_map{$rotate} }; |
|---|
| 147 | |
|---|
| 148 | my $last_rotated_date = $self->_parse_rotatestatus( |
|---|
| 149 | $self->file, |
|---|
| 150 | $self->rotate_statefile, |
|---|
| 151 | ); |
|---|
| 152 | |
|---|
| 153 | if ( !$last_rotated_date ) { |
|---|
| 154 | $self->{logfiles} = [ |
|---|
| 155 | { name => $self->file, |
|---|
| 156 | year => DateTime->now->set_time_zone( 'local' )->year, |
|---|
| 157 | }, |
|---|
| 158 | ]; |
|---|
| 159 | return; |
|---|
| 160 | } |
|---|
| 161 | |
|---|
| 162 | my $last_rotated_on = DateTime->new( |
|---|
| 163 | %{ $last_rotated_date }, |
|---|
| 164 | hour => $rotate_hour, |
|---|
| 165 | minute => $rotate_minute, |
|---|
| 166 | time_zone => 'local', |
|---|
| 167 | ); |
|---|
| 168 | |
|---|
| 169 | if ( DateTime->compare( $from, $last_rotated_on ) == 1 ) { |
|---|
| 170 | $self->{logfiles} = [ |
|---|
| 171 | { name => $self->file, |
|---|
| 172 | year => $last_rotated_on->year, |
|---|
| 173 | }, |
|---|
| 174 | ]; |
|---|
| 175 | return; |
|---|
| 176 | } |
|---|
| 177 | |
|---|
| 178 | my $from_dur = $self->_dur_by_unittype( $last_rotated_on, $from, $unit_type ); |
|---|
| 179 | my $from_suffix = $from_dur + 1; |
|---|
| 180 | |
|---|
| 181 | my $to_suffix; |
|---|
| 182 | if ( DateTime->compare( $to, $last_rotated_on ) == 1) { |
|---|
| 183 | $to_suffix = 0; |
|---|
| 184 | } else { |
|---|
| 185 | my $to_dur = $self->_dur_by_unittype( $last_rotated_on, $to, $unit_type ); |
|---|
| 186 | $to_suffix = $to_dur + 1; |
|---|
| 187 | } |
|---|
| 188 | |
|---|
| 189 | my $rotated_on = $last_rotated_on->clone->subtract( $unit_type => $from_dur + 1 ); |
|---|
| 190 | |
|---|
| 191 | for my $num ( reverse $to_suffix .. $from_suffix ) { |
|---|
| 192 | my $name = ( $num == 0 )? $self->file : sprintf( "%s.%d", $self->file, $num ); |
|---|
| 193 | my $logfile = { |
|---|
| 194 | name => $name, |
|---|
| 195 | year => $rotated_on->year, |
|---|
| 196 | }; |
|---|
| 197 | push @{ $self->logfiles }, $logfile; |
|---|
| 198 | $rotated_on->add( $unit_type => 1 ); |
|---|
| 199 | } |
|---|
| 200 | } |
|---|
| 201 | |
|---|
| 202 | sub _dur_by_unittype { |
|---|
| 203 | my ( $self, $dt1, $dt2, $type ) = @_; |
|---|
| 204 | return $type eq 'days' ? $dt1->delta_days( $dt2 )->in_units( $type ) |
|---|
| 205 | : $type eq 'weeks' ? $dt1->delta_days( $dt2 )->in_units( $type ) |
|---|
| 206 | : $type eq 'months'? $dt1->delta_md( $dt2 )->in_units( $type ) |
|---|
| 207 | : croak; |
|---|
| 208 | } |
|---|
| 209 | |
|---|
| 210 | sub _parse_rotatestatus { |
|---|
| 211 | my ( $self, $file, $statefile ) = @_; |
|---|
| 212 | $statefile ||= '/var/lib/logrotate.status'; |
|---|
| 213 | -f $statefile |
|---|
| 214 | or croak "can't find logrotate statefile. please set rotate_statefile"; |
|---|
| 215 | my $state_h = Path::Class::file( $statefile )->open( 'r' ) or croak $!; |
|---|
| 216 | my ( $year, $month, $day ); |
|---|
| 217 | |
|---|
| 218 | while ( my $line = <$state_h> ) { |
|---|
| 219 | chomp $line; |
|---|
| 220 | my ( $name, $status ) = $line =~ /^"(.*?)"[ ](.*)$/mxs; |
|---|
| 221 | ( $name and $status ) or next; |
|---|
| 222 | $name eq $file or next; |
|---|
| 223 | ( $year, $month, $day ) = split "-", $status; |
|---|
| 224 | ( $year and $month and $day ) |
|---|
| 225 | or croak "parsing $statefile failed"; |
|---|
| 226 | last; |
|---|
| 227 | } |
|---|
| 228 | |
|---|
| 229 | close $state_h or croak $!; |
|---|
| 230 | |
|---|
| 231 | return { |
|---|
| 232 | year => $year, |
|---|
| 233 | month => $month, |
|---|
| 234 | day => $day, |
|---|
| 235 | }; |
|---|
| 236 | } |
|---|
| 237 | |
|---|
| 238 | sub next { |
|---|
| 239 | my $self = shift; |
|---|
| 240 | return if $self->{exit_flag}; |
|---|
| 241 | |
|---|
| 242 | while ( my $sp = $self->{sp} ||= $self->get_next_sp ) { |
|---|
| 243 | my $sl = $sp->next or do { undef $self->{sp}; next; }; |
|---|
| 244 | |
|---|
| 245 | ## parse : from_time <= timestamp < delayed_to_time |
|---|
| 246 | ## return: from_time <= timestamp < to_time |
|---|
| 247 | next if $sl->{timestamp} < $self->{from_time}; |
|---|
| 248 | return $sl unless $self->{to_time}; |
|---|
| 249 | |
|---|
| 250 | if ( $self->{delayed_to_time} <= $sl->{timestamp} ) { |
|---|
| 251 | $self->{exit_flag} = 1; |
|---|
| 252 | return; |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | return $sl if $sl->{timestamp} < $self->{to_time}; |
|---|
| 256 | } |
|---|
| 257 | |
|---|
| 258 | return; |
|---|
| 259 | } |
|---|
| 260 | |
|---|
| 261 | sub get_next_sp { |
|---|
| 262 | my $self = shift; |
|---|
| 263 | |
|---|
| 264 | while ( my $logfile = $self->logfiles->[ $self->{target_idx}++ ] ) { |
|---|
| 265 | my ($name, $year) = @$logfile{qw/ name year /}; |
|---|
| 266 | next unless -r $name; |
|---|
| 267 | |
|---|
| 268 | return $self->{parse_module}->new( |
|---|
| 269 | $name, |
|---|
| 270 | year => $year, |
|---|
| 271 | %{ $self->{for_parse_module} }, |
|---|
| 272 | ); |
|---|
| 273 | } |
|---|
| 274 | |
|---|
| 275 | return; |
|---|
| 276 | } |
|---|
| 277 | |
|---|
| 278 | |
|---|
| 279 | 1; |
|---|
| 280 | |
|---|
| 281 | |
|---|
| 282 | =head1 NAME |
|---|
| 283 | |
|---|
| 284 | Parse::Syslog::Period::Parser - The great new Parse::Syslog::Period::Parser! |
|---|
| 285 | |
|---|
| 286 | =head1 VERSION |
|---|
| 287 | |
|---|
| 288 | Version 0.01 |
|---|
| 289 | |
|---|
| 290 | |
|---|
| 291 | =head1 SYNOPSIS |
|---|
| 292 | |
|---|
| 293 | Quick summary of what the module does. |
|---|
| 294 | |
|---|
| 295 | Perhaps a little code snippet. |
|---|
| 296 | |
|---|
| 297 | use Parse::Syslog::Period::Parser; |
|---|
| 298 | |
|---|
| 299 | my $foo = Parse::Syslog::Period::Parser->new(); |
|---|
| 300 | ... |
|---|
| 301 | |
|---|
| 302 | =head1 EXPORT |
|---|
| 303 | |
|---|
| 304 | A list of functions that can be exported. You can delete this section |
|---|
| 305 | if you don't export anything, such as for a purely object-oriented module. |
|---|
| 306 | |
|---|
| 307 | =head1 FUNCTIONS |
|---|
| 308 | |
|---|
| 309 | =head2 function1 |
|---|
| 310 | |
|---|
| 311 | |
|---|
| 312 | =head2 function2 |
|---|
| 313 | |
|---|
| 314 | |
|---|
| 315 | =head1 AUTHOR |
|---|
| 316 | |
|---|
| 317 | ryo kuroda, C<< <lamanotrama at gmail.com> >> |
|---|
| 318 | |
|---|
| 319 | =head1 BUGS |
|---|
| 320 | |
|---|
| 321 | Please report any bugs or feature requests to C<bug-parse-syslog-period-parser at rt.cpan.org>, or through |
|---|
| 322 | the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Parse-Syslog-Period>. I will be notified, and then you'll |
|---|
| 323 | automatically be notified of progress on your bug as I make changes. |
|---|
| 324 | |
|---|
| 325 | |
|---|
| 326 | |
|---|
| 327 | |
|---|
| 328 | =head1 SUPPORT |
|---|
| 329 | |
|---|
| 330 | You can find documentation for this module with the perldoc command. |
|---|
| 331 | |
|---|
| 332 | perldoc Parse::Syslog::Period |
|---|
| 333 | |
|---|
| 334 | |
|---|
| 335 | You can also look for information at: |
|---|
| 336 | |
|---|
| 337 | =over 4 |
|---|
| 338 | |
|---|
| 339 | =item * RT: CPAN's request tracker |
|---|
| 340 | |
|---|
| 341 | L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Parse-Syslog-Period> |
|---|
| 342 | |
|---|
| 343 | =item * AnnoCPAN: Annotated CPAN documentation |
|---|
| 344 | |
|---|
| 345 | L<http://annocpan.org/dist/Parse-Syslog-Period> |
|---|
| 346 | |
|---|
| 347 | =item * CPAN Ratings |
|---|
| 348 | |
|---|
| 349 | L<http://cpanratings.perl.org/d/Parse-Syslog-Period> |
|---|
| 350 | |
|---|
| 351 | =item * Search CPAN |
|---|
| 352 | |
|---|
| 353 | L<http://search.cpan.org/dist/Parse-Syslog-Period> |
|---|
| 354 | |
|---|
| 355 | =back |
|---|
| 356 | |
|---|
| 357 | |
|---|
| 358 | =head1 ACKNOWLEDGEMENTS |
|---|
| 359 | |
|---|
| 360 | |
|---|
| 361 | =head1 COPYRIGHT & LICENSE |
|---|
| 362 | |
|---|
| 363 | Copyright 2008 ryo kuroda, all rights reserved. |
|---|
| 364 | |
|---|
| 365 | This program is free software; you can redistribute it and/or modify it |
|---|
| 366 | under the same terms as Perl itself. |
|---|
| 367 | |
|---|
| 368 | |
|---|
| 369 | =cut |
|---|
| 370 | |
|---|
| 371 | 1; # End of Parse::Syslog::Period::Parser |
|---|