Developer-Dashboard
view release on metacpan or search on metacpan
lib/Developer/Dashboard/Collector.pm view on Meta::CPAN
# _iso8601_to_epoch($timestamp)
# Converts one dashboard ISO-8601 timestamp string into epoch seconds.
# Input: timestamp string in YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DDTHH:MM:SS+HHMM form.
# Output: UTC epoch integer.
sub _iso8601_to_epoch {
my ( $self, $timestamp ) = @_;
my ( $year, $month, $day, $hour, $minute, $second, $zone ) =
$timestamp =~ /\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|[+-]\d{4}|[+-]\d{2}:\d{2})\z/;
die "Unsupported collector log timestamp $timestamp\n" if !defined $zone;
my $offset_seconds = 0;
if ( $zone ne 'Z' ) {
my ( $sign, $offset_hour, $offset_minute ) = $zone =~ /\A([+-])(\d{2}):?(\d{2})\z/;
$offset_seconds = ( $offset_hour * 3600 ) + ( $offset_minute * 60 );
$offset_seconds *= -1 if $sign eq '-';
}
return timegm( $second, $minute, $hour, $day, $month - 1, $year ) - $offset_seconds;
}
# _with_trailing_newline($text)
# Ensures a text block ends with one newline for deterministic log formatting.
# Input: text string.
# Output: text string with a trailing newline.
sub _with_trailing_newline {
my ($text) = @_;
$text = '' if !defined $text;
return $text =~ /\n\z/ ? $text : $text . "\n";
}
# _atomic_write_json($file, $data)
# Atomically writes JSON to a file using a pending temporary file.
# Input: target file path and Perl data structure.
# Output: written file path string.
sub _atomic_write_json {
my ( $self, $file, $data ) = @_;
return $self->_atomic_write_text( $file, json_encode($data) );
}
# _atomic_write_text($file, $text)
# Atomically writes raw text to a file using a pending temporary file.
# Input: target file path and text string.
# Output: written file path string.
sub _atomic_write_text {
my ( $self, $file, $text ) = @_;
my $tmp = "$file.pending";
open my $fh, '>:raw', $tmp or die "Unable to write $tmp: $!";
print {$fh} $text;
close $fh;
$self->{paths}->secure_file_permissions($tmp);
unlink $file if -f $file;
rename $tmp, $file or die "Unable to rename $tmp to $file: $!";
$self->{paths}->secure_file_permissions($file);
return $file;
}
# _read_status_file($file)
# Reads one collector status JSON file, returning undef when it is missing or
# invalid instead of exploding across status polling paths.
# Input: collector status file path string.
# Output: decoded status hash reference or undef.
sub _read_status_file {
my ( $self, $file ) = @_;
return if !-f $file;
open my $fh, '<:raw', $file or die "Unable to read $file: $!";
local $/;
my $raw = <$fh>;
my $data = eval { json_decode($raw) };
return $data if !$@;
return;
}
# _slurp($file)
# Reads an entire file or returns an empty string when missing.
# Input: file path string.
# Output: file content string.
sub _slurp {
my ($file) = @_;
return '' if !-f $file;
open my $fh, '<:raw', $file or die "Unable to read $file: $!";
local $/;
return scalar <$fh>;
}
# _now_iso8601()
# Returns the current local timestamp in ISO-8601 form with timezone offset.
# Input: none.
# Output: timestamp string.
sub _now_iso8601 {
my @t = localtime();
return strftime( '%Y-%m-%dT%H:%M:%S%z', @t );
}
1;
__END__
=head1 NAME
Developer::Dashboard::Collector - file-backed collector storage
=head1 SYNOPSIS
my $collector = Developer::Dashboard::Collector->new(paths => $paths);
$collector->write_job('sample', { name => 'sample', command => 'true' });
=head1 DESCRIPTION
This module owns the on-disk storage model for collector job definitions,
status records, latest outputs, and persisted collector log transcripts. It
also applies collector log retention rules when housekeeping asks it to rotate
those transcripts.
=head1 METHODS
=head2 new, collector_paths, write_job, read_job, write_result, write_status, read_status, read_output, collector_exists, append_log_entry, rotate_log, read_log, inspect_collector, list_collectors
Construct and manage collector storage.
=for comment FULL-POD-DOC START
( run in 0.713 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )