Como posso usar bash (grep / sed / etc) para pegar uma seção de um arquivo de log entre 2 marcas de tempo?

StackOverflow https://stackoverflow.com/questions/827930

Pergunta

Eu tenho um conjunto de registros de e-mail: mail.log mail.log.0 mail.log.1.gz mail.log.2.gz

cada um desses arquivos contêm linhas cronologicamente ordenados que começam com timestamps como:

03 de maio 13:21:12 ...

Como posso agarrar facilmente a cada entrada de log após uma data certa / hora e antes de uma outra data / hora usando bash (e ferramentas de linha de comando relacionados) sem comparar cada linha? Tenha em mente que as minhas datas antes e depois pode não coincidir exatamente nenhuma entrada nos arquivos de log.

Parece-me que eu preciso para determinar o deslocamento do primeiro maior linha do que o timestamp partida, e o deslocamento da última linha menos do que o timestamp terminando, e corte que a secção de alguma forma.

Foi útil?

Solução 2

Aqui uma ideia básica de como fazê-lo:

  1. Examine o datestamp no arquivo para ver se ele é irrelevent
  2. Se poderia ser relevent, unzip se necessário, e examinar as linhas primeira e última do arquivo para ver se ele contém a hora de início ou fim.
  3. Se isso acontecer, use uma função recursiva para determinar se ele contém a hora de início na primeira ou na segunda metade do arquivo. Usando uma função recursiva Eu acho que você poderia encontrar qualquer data em um arquivo de log milhões de linha com cerca de 20 comparações.
  4. ecoar o arquivo de log (s), a fim do deslocamento da primeira entrada para o deslocamento da última entrada (sem mais comparações)

O que eu não sei é: a melhor forma de ler a linha enésima de um arquivo (o quão eficiente é a utilização cauda n + ** n | cabeça 1 **?)

Qualquer ajuda?

Outras dicas

Converta seus mínimo de datas / max em "segundos desde época",

MIN=`date --date="$1" +%s`
MAX=`date --date="$2" +%s`

Converter as primeiras palavras n em cada linha do registro para o mesmo,

L_DATE=`echo $LINE | awk '{print $1 $2 ... $n}'`
L_DATE=`date --date="$L_DATE" +%s`

Comparar e jogar fora as linhas até chegar MIN,

if (( $MIN > $L_DATE )) ; then continue ; fi

Compare e imprimir linhas até chegar MAX,

if (( $L_DATE <= $MAX )) ; then echo $LINE ; fi

Sair quando você exceder MAX.

if (( $L_DATE > $MAX )) ; then exit 0 ; fi

Todo o roteiro minmaxlog.sh parece com isso,

#!/usr/bin/env bash

MIN=`date --date="$1" +%s`
MAX=`date --date="$2" +%s`

while true ; do
    read LINE
    if [ "$LINE" = "" ] ; then break ; fi

    L_DATE=`echo $LINE | awk '{print $1 " " $2 " " $3 " " $4}'`
    L_DATE=`date --date="$L_DATE" +%s`

    if (( $MIN > $L_DATE  )) ; then continue ; fi
    if (( $L_DATE <= $MAX )) ; then echo $LINE ; fi
    if (( $L_DATE >  $MAX )) ; then break ; fi
done

Eu corri-lo neste arquivo minmaxlog.input ,

May 5 12:23:45 2009 first line
May 6 12:23:45 2009 second line
May 7 12:23:45 2009 third line
May 9 12:23:45 2009 fourth line
June 1 12:23:45 2009 fifth line
June 3 12:23:45 2009 sixth line

como este,

./minmaxlog.sh "May 6" "May 8" < minmaxlog.input

Você tem que olhar para cada linha no intervalo que deseja (para dizer se é na faixa que você quer), então eu estou supondo que você quer dizer não cada linha no arquivo. No mínimo, você terá que olhar para cada linha no arquivo até e incluindo o primeiro fora da sua gama (estou assumindo as linhas são em data do pedido / hora).

Este é um padrão bastante simples:

state = preprint
for every line in file:
    if line.date >= startdate:
        state = print
    if line.date > enddate:
        exit for loop
    if state == print:
        print line

Você pode escrever isso em awk, Perl, Python, mesmo COBOL, se for preciso, mas a lógica é sempre a mesma.

Localizando os números de linha em primeiro lugar (com grep por exemplo) e, em seguida, apenas cega imprimir esse intervalo linha não vai ajudar desde grep também tem de olhar para todas as linhas ( todas deles, não apenas até a primeira fora da faixa, e, provavelmente, duas vezes , uma para a primeira linha e um para o último).

Se isto é algo que você vai fazer, muitas vezes, você pode querer considerar mudar o esforço de 'cada vez que você fazê-lo' para 'uma vez, quando o arquivo é estabilizado'. Um exemplo seria para carregar as linhas do arquivo de log em um banco de dados, indexados pela data / hora.

Isso leva um tempo para se configurar, mas irá resultar em suas consultas tornando-se muito mais rápido. Eu não estou necessariamente defendendo um banco de dados - você provavelmente poderia conseguir o mesmo efeito, dividindo os arquivos de log em registos de hora em hora assim:

2009/
  01/
    01/
      0000.log
      0100.log
      : :
      2300.log
    02/
    : :

Depois de um determinado tempo, você sabe exatamente por onde começar e parar de olhar. A gama 2009/01/01-15:22 através 2009/01/05-09:07 resultaria em:

  • alguns (o último bit) do 2009/01/01/1500.txt arquivo.
  • toda a 2009/01/01/1[6-9]*.txt arquivos.
  • toda a 2009/01/01/2*.txt arquivos.
  • toda a 2009/01/0[2-4]/*.txt arquivos.
  • toda a 2009/01/05/0[0-8]*.txt arquivos.
  • alguns (o primeiro bit) do 2009/01/05/0900.txt arquivo.

É claro, eu ia escrever um script para retornar essas linhas, em vez de tentar fazê-lo manualmente de cada vez.

Talvez você pode tentar o seguinte:

sed -n "/BEGIN_DATE/,/END_DATE/p" logfile

Pode ser possível em um ambiente Bash, mas você deve realmente tirar proveito das ferramentas que têm mais suporte embutido para trabalhar com Cordas e datas. Por exemplo rubi parece ter construído na capacidade de analisar o formato da data. Ele pode, em seguida, convertê-lo em um sistema Unix Timestamp (um inteiro positivo representando os segundos desde a época) facilmente comparáveis.

irb> require 'time'
# => true

irb> Time.parse("May 3 13:21:12").to_i
# => 1241371272  

Você pode então escrever um script Ruby:

  • Fornecer uma data de início e fim. Converter aqueles a esta Unix Timestamp Number.
  • Verificar a linha de arquivos de log por linha, convertendo a data em seu Unix Timestamp e verificar se isso está na faixa das datas de início e fim.

Nota:. Convertendo para um inteiro Unix Timestamp primeiro é bom porque comparando inteiros é muito fácil e eficiente de fazer

Você mencionou "sem comparar cada linha." Vai ser difícil "palpite" no lugar do arquivo de log as entradas começa a ser demasiado velho ou muito novo sem verificar todos os valores entre os dois. No entanto, se há de fato uma tendência monótona crescente, então você sabe imediatamente quando parar de analisar linhas, porque assim que a próxima entrada é muito novo (ou velho, dependendo do layout dos dados) você sabe que você pode parar de procurar. Ainda assim, há o problema de encontrar a primeira linha em sua faixa desejada.


Eu só notei a sua edição. Aqui está o que eu diria:

Se você é realmente preocupado em encontrar de forma eficiente que a entrada de início e fim, então você poderia fazer uma busca binária para cada um. Ou, se isso parece um exagero ou muito difícil com ferramentas festança você poderia ter uma heurística de ler apenas 5% das linhas (1 em cada 20), para obter rapidamente um perto de resposta exata e, em seguida, refinando que se desejar. Estas são apenas algumas sugestões para melhorias de desempenho.

Eu sei que esta discussão é antiga, mas eu só tropeçou em cima dele depois de recentemente encontrar uma solução uma linha para as minhas necessidades:

awk -v ts_start="2018-11-01" -v ts_end="2018-11-15" -F, '$1>=ts_start && $1<ts_end' myfile

Neste caso, o meu arquivo tem registros com valores separados por vírgula e o timestamp no primeiro campo. Você pode usar qualquer formato timestamp válido para a data e hora de início e fim, e substituí-los vai desembolsar variáveis, se desejar.

Se você quiser escrever para um novo arquivo, basta usar redirecionamento de saída normal (> newfile) anexado ao final do acima.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top