How can I convert a log4j timestamp to milliseconds in Perl? - perl

The log4j logs I have contain timestamps in the following format:
2009-05-10 00:48:41,905
I need to convert it in perl to millseconds since epoch, which in this case would be 124189673005, using the following gawk function. How do I do it in perl?
I have little or no experience in perl, so appreciate if someone can post an entire script that does this
function log4jTimeStampToMillis(log4jts) {
# log4jts is of the form 2009-03-02 20:04:13,474
# extract milliseconds that is after the command
split(log4jts, tsparts, ",");
millis = tsparts[2];
# remove - : from tsstr
tsstr = tsparts[1];
gsub("[-:]", " ", tsstr);
seconds = mktime(tsstr);
print log4jts;
return seconds * 1000 + millis;
}

Though I almost always tell people to go use one of the many excellent modules from the CPAN for this, most of them do have one major drawback - speed. If you're parsing a large number of log files in real-time, that can sometimes be an issue. In those cases, rolling your own can often be a more suitable solution, but there are many pitfalls and nuances that must be considered and handled properly. Hence the preference for using a known-correct, proven, reliable module written by somebody else. :)
However, before I even considered my advice above, I looked at your code and had converted it to perl in my head... therefore, here is a more-or-less direct conversion of your gawk code into perl. I've tried to write it as simply as possible, so as to highlight some of the more delicate parts of dealing with dates and times in perl by hand.
# import the mktime function from the (standard) POSIX module
use POSIX qw( mktime );
sub log4jTimeStampToMillis {
my ($log4jts, $dst) = #_;
# extract the millisecond field
my ($tsstr, $millis) = split( ',', $log4jts );
# extract values to pass to mktime()
my #mktime_args = reverse split( '[-: ]', $tsstr );
# munge values for posix compatibility (ugh)
$mktime_args[3] -= 1;
$mktime_args[4] -= 1;
$mktime_args[5] -= 1900;
# print Dumper \#mktime_args; ## DEBUG
# convert, make sure to account for daylight savings
my $seconds = mktime( #mktime_args, 0, 0, $dst );
# return that time as milliseconds since the epoch
return $seconds * 1000 + $millis;
}
One important difference between my code and yours - my log4jTimeStampToMillis subroutine takes two parameters:
the log timestamp string
whether or not that timestamp is using daylight savings time ( 1 for true, 0 for false )
Of course, you could just add code to detect if that time falls in DST or not and adjust automatically, but I was trying to keep it simple. :)
NOTE: If you uncomment the line marked DEBUG, make sure to add "use Data::Dumper;" before that line in your program so it will work.
Here's an example of how you could test that subroutine:
my $milliseconds = log4jTimeStampToMillis( "2009-05-10 00:48:41,905", 1 );
my $seconds = int( $milliseconds / 1000 );
my $local = scalar localtime( $seconds );
print "ms: $milliseconds\n"; # ms: 1241844521905
print "sec: $seconds\n"; # sec: 1241844521
print "local: $local\n"; # local: Sat May 9 00:48:41 2009

You should take advantage of the great DateTime package, specifically use DateTime::Format::Strptime:
use DateTime;
use DateTime::Format::Strptime;
sub log4jTimeStampToMillis {
my $log4jts=shift(#_);
#see package docs for how the pattern parameter works
my $formatter= new DateTime::Format::Strptime(pattern => '%Y-%m-%d %T,%3N');
my $dayObj = $formatter->parse_datetime($log4jts);
return $dayObj->epoch()*1000+$dayObj->millisecond();
}
print log4jTimeStampToMillis('2009-05-10 10:48:41,905')."\n";
#prints my local version of the TS: 1241952521905
This saves you the pain of figuring out DST yourself (although you'll have to pass your server's TZ to Strptime via the time_zone parameter). It also saves you from dealing with leap everything if it becomes relevant (and I'm sure it will).

Haven't used it, but you might want to check out Time::ParseDate.

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
Date time = dateFormat.parse(log4jts);
long millis = time.getTime();

Related

How to subtract dates in perl and convert it in minutes and hours?

Every time I tried to find the difference of these date strings, there is an error. I wonder if you could help me this.
my $datecreated = '2021-09-06 04:52:38';
my $dateresolved = '2021-09-06 04:52:48';
my $time_elapsed= $dateresolved - $datecreated;
print $time_elapsed;
And I want to convert the result into minutes and hours.
These two timestamps are mere strings. In order to get the duration between these two moments in time ("subtract" them) one needs to build date-time objects from them, in a library that knows how to then find duration between them. One good choice is DateTime
use warnings;
use strict;
use feature 'say';
use DateTime;
use DateTime::Format::Strptime;
my ($ts1, $ts2) = (#ARGV == 2)
? #ARGV : ('2021-09-05 04:52:38', '2021-09-01 04:52:48');
my $strp = DateTime::Format::Strptime->new(
pattern => '%F %T', time_zone => 'floating', on_error => 'croak'
);
my ($dt1, $dt2) = map { $strp->parse_datetime($_) } $ts1, $ts2;
# Get difference in hours and minutes (seconds discarded per question)
my ($hrs, $min) = delta_hm($dt1, $dt2);
say "$hrs hours and $min minutes";
# Or (time-stamp hh:mm in scalar context)
my $ts_hm = delta_hm($dt1, $dt2);
say $ts_hm;
# To get wanted units (hours+minutes here) best use a delta_X
sub delta_hm {
my ($dt1, $dt2) = #_;
my ($min, $sec) = $dt1->delta_ms($dt2)->in_units('minutes', 'seconds');
my $hrs = int( $min / 60 );
$min = $min % ($hrs*60) if $hrs;
return (wantarray) # discard seconds
? ($hrs, $min)
: join ':', map { sprintf "%02d", $_ } $hrs, $min;
}
The hard-coded input time-stamps here are different than the ones in the question; those would make an hour+minute difference a zero, since they differ only in seconds! (Is that intended?) One can also submit two time-stamp strings as input to this program.
Note that a generic duration object makes it harder to convert to any particular desired units
One cannot in general convert between seconds, minutes, days, and months, so this class will never do so. Instead, create the duration with the desired units to begin with, for example by calling the appropriate subtraction/delta method on a DateTime.pm object.
So above I use delta_ms since minutes are easily converted to hours+minutes. Seconds are discarded as the question implies (if that is in fact unintended add them in the routine).
For more general uses one can do
use DateTime::Duration;
my $dur = $dt1->subtract_datetime($dt2);
# Easy to extract parts (components) of the duration
say "Hours: ", $dur->hours, " and minutes: ", $dur->minutes; # NOT conversion
Can do this with the core Time::Piece as well
use warnings;
use strict;
use feature 'say';
use Time::Piece;
my ($ts1, $ts2) = (#ARGV)
? #ARGV : ('2021-09-05 04:52:38', '2021-09-01 04:52:48');
my ($dt1, $dt2) = map { Time::Piece->strptime($_, "%Y-%m-%d %T") } $ts1, $ts2;
# In older module versions the format specifier `%F` (`%Y-%m-%d`) may fail
# so I spell it out here; the %T (for %H:%M:%S) should always be good
# For local times (not UTC) better use Time::Piece::localtime->strptime
my $delta = $dt1 - $dt2;
# say $delta->pretty;
my $hrs = int( $delta->hours );
my $min = int($delta->minutes) - ($hrs//=0)*60;
say "$hrs:$min";
This is much simpler, but watch out for occasional tricky (error-inducing) API of Time::Piece.
Note, while Time::Piece is core, succinct, and much lighter (and correct!), the DateTime is far more rounded and powerful, also with an ecosystem of extensions.
Use Time::Piece which is a standard part of the Perl library since 2007.
#!/usr/bin/perl
use strict;
use warnings;
use Time::Piece;
# Define the format of your inputs
my $format = '%Y-%m-%d %H:%M:%S';
# Convert your date strings into Time::Piece objects
my $datecreated = Time::Piece->strptime('2021-09-06 04:52:38', $format);
my $dateresolved = Time::Piece->strptime('2021-09-06 04:52:48', $format);
# Time::Piece objects can be subtracted from each other.
# This gives the elapsed time in seconds.
my $time_elapsed = $dateresolved - $datecreated;
# Do the calculations to displace the elapsed time in hours,
# minutes and seconds.
printf "%02dh:%02dm:%02ds\n",
$time_elapsed->hours,
$time_elapsed->minutes % 60,
$time_elapsed->seconds % 60;

Fractional part of epoch

I have an epoch value of 1372252864.901871 which needs to be converted to localtime in perl. I know that localtime($val) where $val is set to 1372252864.901871 converts the value to local time. But I also need to add values of 100 msecs or say 86.7 msecs to this and determine the computed local time. How do I do this? Does the function localtime() take care of fractional epoch value that I have given?
If you just want to add to an epoch time and get another epoch time, just do:
1372252864.901871 + .0867
If you are doing something else, no, localtime does not preserve fractional seconds. You probably want to use DateTime to deal with fractional seconds:
use DateTime;
my $time = DateTime->from_epoch(epoch=>1372252864.901871, time_zone=>'local');
$time->add(nanoseconds=>86.7e6);
print $time->epoch + $time->nanosecond / 1e9;
(time_zone isn't needed for this example, but sounds like what you might want for other things you might be doing with this time)
You might want Time::HiRes:
The Time::HiRes module implements a Perl interface to the usleep,
nanosleep, ualarm, gettimeofday, and setitimer/getitimer system calls,
in other words, high resolution time and timers
The localtime function breaks down a raw time like 1372252864 into year, month, day, etc. Any fractional part of the raw time is quietly ignored.
I'm not aware of any function, either built-in or in a module, that supports broken-down times with a resolution finer than 1 second.
But since you already have the fractional seconds, I'd say you can already do what you need to do. Just use localtime (or gmtime) to break down the integer part of your raw timestamp, and then take the fractional part:
$timestamp = 1372252864.901871;
#parts = localtime($timestamp);
$fraction = $timestamp - int($timestamp);
$milliseconds = int($fraction * 1000.0);
It should be easy enough to write your own high-resolution replacement for localtime that does this for you.
If you need to add 86.7 milliseconds to a timestamp, you'll need to use the raw form anyway:
$timestamp = 1372252864.901871;
$timestamp += 86.7/1000.0;
The simplest way that occurs to me is to use Time::HiRes::Value to handle arithmetic on times to 1μs accuracy, and Time::Piece to format the result as a date/time string.
Time::HiRes::Value holds the time as a two-element array; the first field has the whole seconds while the second has the number of microseconds. That's the same format that Time::HiRes functions work with, but there's nothing that I know of that will natively convert to a date/time string so I've used Time::Piece to convert the whole seconds and just appended the fractional seconds.
It looks like this
use strict;
use warnings;
use Time::HiRes::Value;
use Time::Piece;
my $dt = Time::HiRes::Value->new(1372252864.901871);
my $delta = Time::HiRes::Value->new(86.7E-3); # 86.7ms
$dt += $delta;
print dt_to_string($dt), "\n";
sub dt_to_string {
my ($dt) = #_;
Time::Piece->new($dt->[0])->datetime =~ s{(\d+)\z}{$1 + $dt->[1] / 1E6}er;
}
output
2013-06-26T14:21:4.98857

Running into a memory error using Perl and DateTime

I am writing a small tool to parse some application logs for to collect data that is going to be used as the inputs for Zabbix monitoring. I am just wanting to keep data from the logs that are within the past two hours.
The format of the logs is pretty simple, the fields are separated by white space and the first three fields are used to determine the time when the logging was written.
Here is an example of the first three fields of a log line:
Jan 5 13:42:07
What I set out to do was to utilize one of my favorite modules, DateTime. Where I convert the above into a DateTime object and then compare that object to another DateTime object when the utility would be invoked.
Everything was fine an dandy and working nicely until I actually set the utility against the a portion of the logs it would actually be parsing -- only a couple gigabytes in size. The test run was being done on a kitchen invoked Ubuntu virtual box instance on my laptop, so the resources are -- as expected -- rather limited. The script would halt with the words 'Killed' displayed.
Looking into /var/log/messages I would see log lines describing the process being killed due to resource issues.
When I invoked the process again, and then switching to another screen instance to watch top, I noticed that the memory percentage would grow, that swap space would being to be consumed all until the script would again stop with the 'Killed' message.
When I would rerun the script with the DateTime portion commented out, the script would execute as expected.
In the script I have a subroutine which would be called to create a DateTime object based upon the information found in the first three fields of the log line. I have tried where I create the object at the beginning of the subroutine then undef it prior to returning a value at the end of the subroutine, I have tried it where I create a global object ( using our ) and then use the DateTime set_* methods to modify what I thought would be a single object's values.
I have read that perl does not clean up hash memory so that it can be reused by the program--I feel that this is the base of the issue that I am running into.
At this point, I am feel the need to get input of others and that is the reason for this post. All comments and criticisms would be appreciated.
This utility was running on Perl v5.14.2.
This code produces the memory leak:
#!/usr/bin/perl -w
use strict;
use DateTime;
my $month = 1;
my $day = 6;
my $hour = 20;
my $minute = 30;
my $second = 00;
for (my $count = 0; $count <= 25_000_000; $count++) {
my $epoch = &get_epoch( $month, $day, $hour, $minute, $second );
}
sub get_epoch {
my $mon = shift;
my $day = shift;
my $hour = shift;
my $min = shift;
my $sec = shift;
my $temp_dt = DateTime->new(
year => 2015,
month => $mon,
day => $day,
hour => $hour,
minute => $min,
second => $sec,
nanosecond => 500_000_000,
time_zone => 'UTC',
);
return( $temp_dt->epoch );
}
This is a bug in Params::Validate 1.15 and will be fixed very soon.

Computing small time differences in Singapore adds 30 minutes when converted to hh:mm:ss.mmm

I have this Perl script where I need to monitor the execution time of DBI calls.
In Europe (France), I have no problem: 2 seconds execution time is reported 2 seconds.
This same script running on a computer in Singapore reports 30 minutes and 2 seconds.
Why ?
use strict;
use Time::Format qw(%time);
use Time::HiRes qw(gettimeofday);
my $time_start = gettimeofday();
sleep 2; # some action goes here
my $stat_perf = gettimeofday() - $time_start;
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
The output in France is
00:02.000
The same script running in Singapore yields:
30:02.001
Why ?
According to this documentation, the gettimeofday function returns seconds or microseconds since the unix epoch, which is 1/1/1970 UTC. Because it is in UTC, it is not affected by time zones at all.
Also, in your original code you are just using gettimeofday, which is going to be returning timestamps from now, not from 1970. But in your suggested answer, for some reason, you have hard-set the timestamp, which won't help you do much.
Yes, there is history to just about every time zone, including Singapore. You can see it in the TZDB here. But you are incorrect about it being +8:30 at the epoch. It was actually +7:30. You can verify also on this site. But it doesn't matter anyway because like I said, gettimeofday works strictly in UTC.
I think the problem is in how you are interpreting the results. You have as your last line:
print STDOUT $time{'mm:ss.mmm', $stat_perf} . " \n";
But $stat_perf is the elapsed duration of time, not a value that you can treat as a timestamp. You probably shouldn't be passing it to $time, since that will use the local time zone and be expecting a full timestamp.
Also, you may want to use tv_interval, as shown in the examples.
Update
I searched through the CPAN archives and I'm sure somewhere there is a module for formatting an elapsed duration of time, but I can't seem to find it. Anyway, it's not too hard to write this on your own. Here, this should work:
my $min = $stat_perf / 60;
my $sec = ($stat_perf * 1000 % 60000) / 1000;
my $elapsed = sprintf("%02u:%06.3f", $min, $sec);
print STDOUT $elapsed . "\n";
The anser is ...
Singapore is now 08h00 offset from UTC. In 1970, it was offset by 08h30. Asking for the conversion of a few seconds into a string will get us to 1970, not today's date, and timezone.
By requesting
print STDOUT $time{'mm:ss.mmm', 2} . " \n";
the system adjusts to 1970 (epoch) timezone offset.
In order to get a correct result in Singapore, we must shift to after 1982, when Singapore made its last timezone change.
print STDOUT $time{'mm:ss.mmm', 2 + 1356994800} . " \n";
as
UNIX_TIMESTAMP('2013-01-01 00:00:00') = 1356994800
We are only concerned by the time of day portion of the date, so this does it.
Check with
zdump -v Asia/Singapore
This is the trick.
Here is a script that emulates $time{} in converting a real number into a string representing the mm:ss sexagesimal conversion of its integer part, concatenated with the decimal remainder formatted as microseconds.
As this is going to be part of a library, there are protections set to avoid invoking it with bad arguments.
I hope I didn't miss something.
use strict;
use Time::Format qw(%time);
# ----------------------------------------------------------
# A substitute to $time{} as we have issues with TZ offsets at epoch days in some part of the World
# A real to sexagesimal converter
# Format will be set to match $time{'mm:ss.mmm', $stat_perf};
sub microTime {
return '' unless (my ($intertime) = #_);
return '' unless (ref ($intertime) eq '');
return '' unless (sprintf("%s", $intertime) =~ m/^(?:[\d]+)(?:\.(?:[\d]+))?$/);
my $intNum = int($intertime);
"a" =~ /a/; # Resets regex buffers
sprintf ("%.03f", $intertime - $intNum) =~ m,\.([\d]+),;
my $intDec = $1; # It's always defined
my $intUnder = $intNum % 3600;
my $intMin = int($intUnder / 60);
my $intSec = $intUnder % 60;
return sprintf ("%02d:%02d.%03d", $intMin, $intSec, $intDec);
}
my $stat_perf;
$stat_perf = 345.987;
$stat_perf = 345;
$stat_perf = 3945.987;
$stat_perf = 0;
$stat_perf = 3945.918733;
print STDOUT sprintf (" >> %s\n", &microTime ($stat_perf));
print STDOUT sprintf (" == %s\n", $time{'mm:ss.mmm', $stat_perf});

How can I test Perl applications using a changing system time?

I have a web application that I want to run some system tests on, and in order to do that I'm going to need to move the system time around. The application used DateTime all the way through.
Has anyone got any recommendations for how to change the time that DateTime->now reports? The only thing that comes to mind is subclassing DateTime and messing about with all the 'use' lines, but this seems rather invasive.
Note on answers:
All three will work fine, but the Hook::LexWrap one is the one I've chosen because (a) I want to move the clock rather than jiggle it a bit (which is more the purpose of what Time::Mock and friends do); (b) I do, consistently, use DateTime, and I'm happy to have errors come out if I've accidentally not used it; and (c) Hook::LexWrap is simply more elegant than a hack in the symbol table, for all that it does the same thing. (Also, it turns out to be a dependency of some module I already installed, so I didn't even have to CPAN it...)
Rather than taking the high-level approach and wrapping DateTime specifically, you might want to look into the modules Test::MockTime and Time::Mock, which override the low-level functions that DateTime etc. make use of, and (with any luck) will do the right thing on any time-sensitive code. To me it seems like a more robust way to test.
I think Hook::LexWrap is overkill for this situation. It's easier to just redefine such a simple function.
use DateTime;
my $offset;
BEGIN {
$offset = 24 * 60 * 60; # Pretend it's tomorrow
no warnings 'redefine';
sub DateTime::now
{
shift->from_epoch( epoch => ($offset + scalar time), #_ )
}
} # end BEGIN
You can replace my $offset with our $offset if you need to access the $offset from outside the file which contains this code.
You can adjust $offset at any time, if you want to change DateTime's idea of the current time during the run.
The calculation of $offset should probably be more complicated than shown above. For example, to set the "current time" to an absolute time:
my $want = DateTime->new(
year => 2009,
month => 9,
day => 14,
hour => 12,
minute => 0,
second => 0,
time_zone => 'America/Chicago',
);
my $current = DateTime->from_epoch(epoch => scalar time);
$offset = $want->subtract_datetime_absolute($current)->in_units('seconds');
But you probably do want to calculate a fixed number of seconds to add to the current time, so that time will advance normally after that. The problem with using add( days => 1 ); in the redefined now method is that things like DST changes will cause the time to jump at the wrong pseudotime.
You can use code injection via Hook::LexWrap to intercept the now() method.
use Hook::LexWrap;
use DateTime;
# Use real now
test();
{
my $wrapper = wrap 'DateTime::now',
post => sub {
$_[-1] = DateTime->from_epoch( epoch => 0 );
};
# Use fake now
test();
}
# use real now again
test();
sub test {
my $now = DateTime->now;
print "The time is $now\n";
}
When designing a new class with testability in mind, the ideal solution is to be able to inject new date objects.
However, for existing code using DateTime->now and DateTime->today a possible, suitably scoped, solution is below. I include it here as a way to do this without introducing Hook::LexWrap as a dependency and without affecting the behaviour globally.
{
no strict 'refs';
no warnings 'redefine';
local *{'DateTime::today'} = sub {
return DateTime->new(
year => 2012,
month => 5,
day => 31
);
};
say DateTime->today->ymd(); # 2012-05-31
};
say DateTime->today->ymd(); # today