A coworker wanted to check a family of log files to be sure that a given task never appeared on multiple nodes at the same time. Log entries are on single, whitespace-separated lines, and the last field records a task’s start time, e.g.,
1251475056672590000_1732248586_4
Of the three underscore–separated fields, the first is a timestamp, the second we don’t care about, and the third is a task identifier.
This task is straightforward with Perl. The diamond operator (or null filehandle, as described in the “I/O Operators” section of the perlop manpage) takes care of the boilerplate for iterating over the paths on the command line, opening them, and reading each line. The scalar $ARGV
contains the name of the current file.
By default, split
separates fields by whitespace, so (split)[-1]
gives us the last field, from which we then grab the time and task with a regular expression and record its presence by pushing the entry’s path and line number onto an array associated with that time/task pair. After we’ve processed the logs, these arrays should all be singletons.
The continue clause is a little weird but necessary because the special variable $.
, the current line number, does not reset on <>
’s implicit opens. ARGV
is a handle on the file being read.
With this data structure, detecting duplicates is a search for time/task pairs with multiple hits. We count duplicates and let the user know what we found.
#! /usr/bin/perl
use warnings;
use strict;
# e.g., $hits = @{ $seen{$time}{$task} };
my %seen;
sub num { $a <=> $b }
while (<>) {
if ((split)[-1] =~ /^(\d+)_\d+_(\d+)$/) {
my($time,$task) = ($1,$2);
push @{ $seen{$time}{$task} } => "$ARGV:$.";
}
else {
die "$0: $ARGV:$.: bad timestamp/task field\n";
}
}
continue {
close ARGV if eof;
}
my $duplicates = 0;
foreach my $time (sort num keys %seen) {
foreach my $task (sort num keys %{ $seen{$time} }) {
my @hits = @{ $seen{$time}{$task} };
next if @hits == 1;
$duplicates += @hits - 1;
warn "$0: duplicates for time=$time, task=$task:\n",
map " - $_\n", @hits;
}
}
my $s = $duplicates == 1 ? "" : "s";
print "$0: $duplicates duplicate$s detected.\n";
exit $duplicates == 0 ? 0 : 1;
For comparison, I implemented the same log checker in Haskell. The function allInputs
emulates Perl’s diamond operator, and instead of a multi–level hash, the association is more direct: time/task pair to a list of hits.
module Main where
import Control.Monad (liftM)
import Data.List (sort)
import Data.Map (empty,filter,fromListWith,toList,unionWith)
import Prelude hiding (filter)
import System.Environment (getArgs,getProgName)
import System.Exit (ExitCode(..),exitWith)
import Text.Printf (printf)
type Time = String
type Task = String
data Duplicates =
Duplicates { timestamp :: Time
, taskId :: Task
, locations :: [(FilePath, Int)]
}
main :: IO ()
main = do
logs <- allInputs
let multi = dups logs
n = sum $ map (subtract 1 . length . locations) multi
mapM_ (msg . lines . dupmsg) multi
msg $ ndups n
exitWith $ if n == 0
then ExitSuccess
else ExitFailure 1
where
msg info = do me <- getProgName
putStrLn $ me ++ ": " ++ head info
mapM_ putStrLn (tail info)
ndups 1 = ["1 duplicate detected"]
ndups n = [show n ++ " duplicates detected"]
dupmsg (Duplicates tm task ls) = unlines $
printf "duplicates for time=%s, task=%s:" tm task :
map (\(path,n) -> printf " - %s:%d" path n) ls
allInputs :: IO [(FilePath, String)]
allInputs = getArgs >>= go
where go [] = ((:[]) . (,) "-") `liftM` getContents
go fs = mapM readFile fs >>= return . zip fs
dups :: [(FilePath, String)] -> [Duplicates]
dups = map (\((tm,task),ds) -> Duplicates tm task ds) .
sort .
toList .
filter ((> 1) . length) .
foldl (unionWith (++)) empty .
map (\(path, contents) ->
fromListWith (++) $
map (wrap path . getTimeTask) $
zip [1..] $ lines contents)
where
wrap path (tm,task,n) = ((tm,task), [(path,n)])
getTimeTask :: (Int,String) -> (Time,Task,Int)
getTimeTask (n,line) = (tm,tsk,n)
where
[tm,_,tsk] = splitBy '_' (last $ words line)
splitBy :: Eq a => a -> [a] -> [[a]]
splitBy _ [] = []
splitBy x xs = h : splitBy x t
where (h,rest) = break (== x) xs
t = drop 1 rest