Mesclar por data vários arquivos de log que também incluem linhas desatualizadas (por exemplo, rastreamentos de pilha)

6

Como posso mesclar arquivos de log, ou seja, arquivos que são classificados por hora, mas que também têm várias linhas, onde apenas a primeira linha tem a hora e as demais não.

log1

01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar

log2

01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3

Resultado esperado

01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar

Se não fosse pelas linhas que não são de timestamp que começam com um dígito, um simples sort -nm log1 log2 faria .

Existe uma maneira fácil em uma linha de cmd unix / linux para fazer o trabalho?

Editar Como esses arquivos de log geralmente estão nos gigabytes, a mesclagem deve ser feita sem reclassificar os arquivos de log (já classificados) e sem carregar os arquivos completamente na memória.

    
por Eugene Beresovsky 08.10.2014 / 01:59

3 respostas

9

Difícil. Embora seja possível usar date e bash arrays, esse é realmente o tipo de coisa que se beneficiaria de uma linguagem de programação real. Em Perl, por exemplo:

$ perl -ne '$d=$1 if /(.+?),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar

Aqui está a mesma coisa sem sentido em um script comentado:

#!/usr/bin/env perl

## Read each input line, saving it 
## as $_. This while loop is equivalent
## to perl -ne 
while (<>) {
    ## If this line has a comma
    if (/(.+?),/) {
        ## Save everything up to the 1st 
        ## comma as $date
        $date=$1;
    }
    ## Add the current line to the %k hash.
    ## The hash's keys are the dates and the 
    ## contents are the lines.
    $k{$date}.=$_;
}

## Get the sorted list of hash keys
@dates=sort(keys(%k));
## Now that we have them sorted, 
## print each set of lines.
foreach $date (@dates) {
    print "$k{$date}";
}

Observe que isso pressupõe que todas as linhas de data e somente as linhas de data contenham uma vírgula. Se esse não for o caso, você pode usar isso:

perl -ne '$d=$1 if /^(\d+:\d+:\d+\.\d+),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*

A abordagem acima precisa manter todo o conteúdo dos arquivos na memória. Se isso é um problema, aqui está um que não tem:

$ perl -pe 's/\n/
$ sort -m <(perl -pe 's/\n/
$ alias a="perl -pe 's/\n/
$ perl -ne '$d=$1 if /(.+?),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/'" $ sort -m <(a log1) <(a log2) | perl -lne 's/[
#!/usr/bin/env perl

## Read each input line, saving it 
## as $_. This while loop is equivalent
## to perl -ne 
while (<>) {
    ## If this line has a comma
    if (/(.+?),/) {
        ## Save everything up to the 1st 
        ## comma as $date
        $date=$1;
    }
    ## Add the current line to the %k hash.
    ## The hash's keys are the dates and the 
    ## contents are the lines.
    $k{$date}.=$_;
}

## Get the sorted list of hash keys
@dates=sort(keys(%k));
## Now that we have them sorted, 
## print each set of lines.
foreach $date (@dates) {
    print "$k{$date}";
}
\r]/\n/g; printf'
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log1) \ <(perl -pe 's/\n/
perl -ne '$d=$1 if /^(\d+:\d+:\d+\.\d+),/; $k{$d}.=$_; END{print $k{$_} for sort keys(%k);}' log*
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log2) | perl -lne 's/[
$ perl -pe 's/\n/
$ sort -m <(perl -pe 's/\n/
$ alias a="perl -pe 's/\n/%pre%/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/'"
$ sort -m <(a log1) <(a log2) | perl -lne 's/[%pre%\r]/\n/g; printf'
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log1) \ <(perl -pe 's/\n/%pre%/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log2) | perl -lne 's/[%pre%\r]/\n/g; printf'
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log* | sort -n | perl -lne 's/%pre%/\n/g; printf' 01:02:03.6497,2224,0022 foo foo1 2foo foo3 01:03:03.6497,2224,0022 FOO FOO1 2FOO FOO3 01:04:03.6497,2224,0022 bar 1bar bar2 3bar
\r]/\n/g; printf'
/; s/^/\n/ if /^\d+:\d+:\d+\.\d+/' log* | sort -n | perl -lne 's/%pre%/\n/g; printf' 01:02:03.6497,2224,0022 foo foo1 2foo foo3 01:03:03.6497,2224,0022 FOO FOO1 2FOO FOO3 01:04:03.6497,2224,0022 bar 1bar bar2 3bar

Este simplesmente coloca todas as linhas entre registros de data e hora sucessivos em uma única linha, substituindo novas linhas por sort (se isso puder estar em seus arquivos de log, use qualquer sequência de caracteres que você saiba que nunca estará lá). Isso passou para tr e, em seguida, %code% para recuperar as linhas.

Como muito corretamente apontado pelo OP, todas as soluções acima precisam ser reclassificadas e não levar em conta que os arquivos podem ser mesclados. Aqui está um que faz, mas que ao contrário dos outros só funcionará em dois arquivos:

%pre%

E se você salvar o comando perl como um alias, poderá obter:

%pre%     
por 08.10.2014 / 15:05
1

Uma maneira de fazer isso (obrigado @terdon pela ideia de substituição de nova linha):

  1. Concatear todas as multilinhas em linhas únicas substituindo essas novas linhas por, por exemplo, NUL em cada arquivo de entrada
  2. Faça um sort -m nos arquivos substituídos
  3. Substituir o NUL por novas linhas

Exemplo

Como a concatenação multilinha é usada mais de uma vez, vamos alias embora:

alias a="awk '{ if (match(\
sort -m <(a log1) <(a log2) | tr '
merge-logs log1 log2
' '\n'
, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\./, _))\ { if (NR == 1) printf \"%s\", \
x=""
for f in "$@";
do
 x="$x <(awk '{ if (match(\
alias a="awk '{ if (match(\
sort -m <(a log1) <(a log2) | tr '
merge-logs log1 log2
' '\n'
, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\./, _))\ { if (NR == 1) printf \"%s\", \
x=""
for f in "$@";
do
 x="$x <(awk '{ if (match(\%pre%, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\./, _)) { if (NR == 1) printf \"%s\", \%pre%; else printf \"\n%s\", \%pre% } else printf \"\0%s\", \%pre% } END { print \"\" }' $f)"
done

eval "sort -m $x | tr '%pre%' '\n'"
; else printf \"\n%s\", \%pre% }\ else printf \"\0%s\", \%pre% } END { print \"\" }'"
, /^[0-9]{2}:[0-9]{2}:[0-9]{2}\./, _)) { if (NR == 1) printf \"%s\", \%pre%; else printf \"\n%s\", \%pre% } else printf \"\0%s\", \%pre% } END { print \"\" }' $f)" done eval "sort -m $x | tr '%pre%' '\n'"
; else printf \"\n%s\", \%pre% }\ else printf \"\0%s\", \%pre% } END { print \"\" }'"

Aqui está o comando merge, usando o alias acima:

%pre%

Como script de shell

Para usá-lo assim

%pre%

Eu coloquei em um script de shell:

%pre%

Não tenho certeza se posso oferecer um número variável de arquivos de log sem recorrer ao mal eval .

    
por 11.11.2014 / 05:21
0

Ao usar o Java é uma opção para você, tente log-merger :

java -jar log-merger-0.0.3-jar-with-dependencies.jar -f 1 -tf "HH:MM:ss.SSS" -d "," -i log1,log2
01:02:03.6497,2224,0022 foo
foo1
2foo
foo3
01:03:03.6497,2224,0022 FOO
FOO1
2FOO
FOO3
01:04:03.6497,2224,0022 bar
1bar
bar2
3bar
    
por 01.09.2015 / 16:00