Skip to content

Instantly share code, notes, and snippets.

@mpasternacki
Last active December 16, 2015 03:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mpasternacki/5371061 to your computer and use it in GitHub Desktop.
Save mpasternacki/5371061 to your computer and use it in GitHub Desktop.
A runner script to run any command and save its stdout and stderr in a timestamped log file, ready to be harvested by Logstash. Adds JSON metadata, and optionally locks the command, ensuring it doesn't run in multiple copies at the same time.
#!/usr/bin/env perl -w
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# Version 2, December 2004
#
# Copyright (C) 2013 Maciej Pasternacki <maciej@pasternacki.net>
#
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document, and changing it is allowed as long
# as the name is changed.
#
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
# 0. You just DO WHAT THE FUCK YOU WANT TO.
use strict;
use Cwd;
use Fcntl qw(:flock);
use File::Basename;
use File::Spec;
use Getopt::Long;
use IO::Select;
use IPC::Open3;
use Time::HiRes;
use Time::Piece;
use JSON;
our $pid = 0;
our $exited = 0;
our $lock = 0;
our $timeout;
our $name;
our $basedir;
our $command;
our $chdir;
sub say {
my ( $tag, $message, %meta ) = @_;
my ( $sec, $usec ) = Time::HiRes::gettimeofday;
my $line = sprintf "%s.%.06dZ %s[%d] %s %s",
Time::Piece->gmtime($sec)->datetime, $usec,
$name, $pid, uc($tag),
$message;
$line .= " ".encode_json(\%meta) if %meta;
print LOGFILE "$line\n";
}
sub handle {
foreach my $fh ( @_ ) {
my $tag = ( $fh == \*CHILD_OUT ? 'stdout' :
$fh == \*CHILD_ERR ? 'stderr' :
'wtf' );
while ( <$fh> ) {
chomp;
/^$/ and next;
say $tag, $_;
}
}
}
my ( $_help, $_debug );
if ( !GetOptions( 'timeout=i' => \$timeout,
'name=s' => \$name,
'dir=s' => \$basedir,
'chdir=s' => \$chdir,
'lock' => \$lock,
'debug' => \$_debug,
'help' => \$_help )
|| $_help
|| $#ARGV < 0) {
print <<END;
USAGE: $0 [OPTIONS] -- command to run ...
OPTIONS:
--timeout,-t SECONDS
--name,-n NAME Name of process (default: basename of given command)
--dir,-d DIR Base directory to write files to (default: current dir)
--chdir,-c DIR Change directory before running command
--lock,-l Don't allow running multiple instances of process at the same time
--debug Print log to stderr instead of file
END
exit !$_help;
}
chdir $chdir if defined $chdir;
$name ||= basename($ARGV[0]);
$basedir ||= getcwd;
$command = join(' ', @ARGV);
open LOGFILE, $_debug ? ">&STDERR" : ">>".File::Spec->catfile($basedir, $name).".log";
unless ( flock(LOGFILE, ($lock ? LOCK_EX : LOCK_SH)|LOCK_NB) ) {
say 'locked', "Can't obtain lock", reason => $!;
exit -1;
}
$SIG{CHLD} = sub {
say 'sigchld', $command if $_debug;
$exited = 1;
};
$pid = open3('<&0', \*CHILD_OUT, \*CHILD_ERR, @ARGV);
say 'start', $command, lock => $lock, timeout => $timeout, cwd => getcwd;
if ( defined $timeout ) {
$SIG{ALRM} = sub {
say 'timeout', $command;
# TODO: graceful kill?
kill 'TERM', $pid;
};
alarm $timeout;
}
our $sel = IO::Select->new(\*CHILD_OUT, \*CHILD_ERR);
while ( my @ready = $sel->can_read(0.1) ) {
handle @ready;
last if $exited;
}
handle(\*CHILD_OUT, \*CHILD_ERR);
waitpid $pid, 0;
my ($rv, $sig) = ($?>>8, $?&127);
my %meta;
$meta{status} = $rv;
$meta{signal} = $sig if $sig;
say 'finished', $command, %meta;
exit ($rv || ($sig?-1:0));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment