Como extrair logs entre dois registros de data e hora

25

Quero extrair todos os logs entre dois registros de data e hora. Algumas linhas podem não ter o carimbo de data / hora, mas também quero essas linhas. Em resumo, quero todas as linhas que caem sob dois carimbos de hora. Minha estrutura de log se parece com:

[2014-04-07 23:59:58] CheckForCallAction [ERROR] Exception caught in +CheckForCallAction :: null
--Checking user--
Post
[2014-04-08 00:00:03] MobileAppRequestFilter [DEBUG] Action requested checkforcall

Suponha que eu queira extrair tudo entre 2014-04-07 23:00e 2014-04-08 02:00.

Observe que o registro de data e hora de início ou o registro de data e hora de término podem não estar no log, mas quero todas as linhas entre esses dois registros de data e hora.

Amit
fonte
Possível duplicata de stackoverflow.com/questions/7575267/…
Ramesh
Você só precisa fazer isso apenas uma vez ou de forma programática em vários momentos?
Bratchley # 9/14
A razão pela qual pergunto é porque você pode executar dois greps contextuais (um para pegar tudo após o delimitador inicial e outro para parar de imprimir no delimitador final) se você souber os valores literais. Se as datas / horas puderem mudar, o tou poderá gerá-las facilmente, alimentando a entrada do usuário através do date -dcomando e usando-a para construir o padrão de pesquisa.
Bratchley # 9/14
@Ramesh, a pergunta referenciada é muito ampla.
maxschlepzig
@ JoelDavis: Eu quero fazer isso de forma programática. Então, toda vez que eu só preciso digitar o carimbo de data e hora desejado para extrair os registros entre esses registros de hora no meu local / tmp.
quer

Respostas:

19

Você pode usar awkpara isso:

$ awk -F'[]]|[[]' \
  '$0 ~ /^\[/ && $2 >= "2014-04-07 23:00" { p=1 }
   $0 ~ /^\[/ && $2 >= "2014-04-08 02:00" { p=0 }
                                        p { print $0 }' log

Onde:

  • -Fespecifica os caracteres [e ]como separadores de campo usando uma expressão regular
  • $0 referencia uma linha completa
  • $2 referencia o campo de data
  • p é usado como variável booleana que protege a impressão real
  • $0 ~ /regex/ é verdadeiro se regex corresponder $0
  • >=é usado para comparar lexicograficamente a string (equivalente a, por exemplo strcmp())

Variações

A linha de comando acima implementa a correspondência do intervalo de tempo de abertura à direita . Para obter semânticas de intervalo fechado, apenas incremente sua data correta, por exemplo:

$ awk -F'[]]|[[]' \
  '$0 ~ /^\[/ && $2 >= "2014-04-07 23:00"    { p=1 }
   $0 ~ /^\[/ && $2 >= "2014-04-08 02:00:01" { p=0 }
                                           p { print $0 }' log

Caso deseje corresponder os carimbos de data e hora em outro formato, você deve modificar a $0 ~ /^\[/subexpressão. Observe que ele costumava ignorar linhas sem nenhum registro de data e hora da lógica de ativação / desativação da impressão.

Por exemplo, para um formato de carimbo de data e hora como YYYY-MM-DD HH24:MI:SS(sem []chaves), você pode modificar o comando da seguinte maneira:

$ awk \
  '$0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/
      {
        if ($1" "$2 >= "2014-04-07 23:00")     p=1;
        if ($1" "$2 >= "2014-04-08 02:00:01")  p=0;
      }
    p { print $0 }' log

(observe que também o separador de campos foi alterado - para transição em branco / não em branco, o padrão)

maxschlepzig
fonte
Obrigado por compartilhar o script, mas não está verificando o carimbo de data / hora final. Você pode verificar. Além disso, deixe-me saber o que acontece se eu tiver os logs como 2014-04-07 23:59:58. Quero dizer sem chaves
Amit
@Amit, atualizei a resposta
maxschlepzig
Embora eu não acho que este é um problema string (ver minha resposta ), você poderia fazer o seu muito mais legível e, provavelmente, um pouco mais rápido, por não repetir todos os testes: $1 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}/ && $2 ~/[0-2][0-9]:[0-5][0-9]:[0-5][0-9]/ { Time = $1" "$2; if (Time >= "2014-04-07 23:00" ) { p=1 } if (Time >= "2014-04-08 02:00:01" ) { p=0 } } p
Olá Max, Mais uma pequena dúvida .. Se eu tiver algo como Apr-07-2014 10:51:17. Então, quais mudanças eu preciso fazer .. Tentei code$ 0 ~ / ^ [az | AZ] {4} - [0-9] {2} - [0-9] {4} [0-2] [0-9 ]: [0-5] [0-9]: [0-5] [0-9] / && $ 1 "" $ 2> = "07/04/04 - 11:00" {p = 1} $ 0 ~ / ^ [az | AZ] {4} - [0-9] {2} - [0-9] {4} [0-2] [0-9]: [0-5] [0-9]: [0 -5] [0-9] / && $ 1 "" $ 2> = "07/04/04 - 12:00:01" {p = 0} codemas não está funcionando
Amit
@awk_FTW, alterou o código para que o regex seja explicitamente compartilhado.
maxschlepzig
12

Confira dategrep em https://github.com/mdom/dategrep

Descrição:

O dategrep pesquisa os arquivos de entrada nomeados em busca de linhas correspondentes a um período e os imprime no stdout.

Se o dategrep funcionar em um arquivo que pode ser procurado, ele poderá fazer uma pesquisa binária para encontrar a primeira e a última linha para imprimir com bastante eficiência. O dategrep também pode ler do stdin se um dos argumentos do nome do arquivo for apenas um hífen, mas nesse caso, ele precisará analisar todas as linhas que serão mais lentas.

Exemplos de uso:

dategrep --start "12:00" --end "12:15" --format "%b %d %H:%M:%S" syslog
dategrep --end "12:15" --format "%b %d %H:%M:%S" syslog
dategrep --last-minutes 5 --format "%b %d %H:%M:%S" syslog
dategrep --last-minutes 5 --format rsyslog syslog
cat syslog | dategrep --end "12:15" -

Embora essa limitação possa torná-lo inadequado para sua pergunta exata:

No momento, o dategrep morrerá assim que encontrar uma linha que não seja analisável. Em uma versão futura, isso será configurável.

cpugeniusmv
fonte
Eu aprendi sobre esse comando apenas alguns dias atrás, cortesia de onethingwell.org/post/81991115668/dategrep , então parabéns a ele!
precisa saber é o seguinte
3

Uma alternativa awkou uma ferramenta não-padrão é usar o GNU greppara seus greps contextuais. Os GNUs greppermitem especificar o número de linhas após uma correspondência positiva para imprimir -Ae as linhas anteriores para imprimir. -BPor exemplo:

[davisja5@xxxxxxlp01 ~]$ cat test.txt
Ignore this line, please.
This one too while you're at it...
[2014-04-07 23:59:58] CheckForCallAction [ERROR] Exception caught in +CheckForCallAction :: null
--Checking user--
Post
[2014-04-08 00:00:03] MobileAppRequestFilter [DEBUG] Action requested checkforcall
we don't
want these lines.


[davisja5@xxxxxxlp01 ~]$ egrep "^\[2014-04-07 23:59:58\]" test.txt -A 10000 | egrep "^\[2014-04-08 00:00:03\]" -B 10000
[2014-04-07 23:59:58] CheckForCallAction [ERROR] Exception caught in +CheckForCallAction :: null
--Checking user--
Post
[2014-04-08 00:00:03] MobileAppRequestFilter [DEBUG] Action requested checkforcall

O acima diz essencialmente grep para imprimir as 10.000 linhas que seguem a linha que corresponde ao padrão em que você deseja iniciar, efetivamente fazendo sua saída começar onde você deseja e ir até o final (espero), enquanto a segunda egrepno o pipeline diz para imprimir apenas a linha com o delimitador final e as 10.000 linhas antes dele. O resultado final desses dois está começando onde você está querendo e não passa onde você disse para parar.

10.000 é apenas um número que inventei, fique à vontade para alterá-lo para um milhão, se você acha que sua produção será muito longa.

Bratchley
fonte
Como isso funcionará se não houver entrada de log para os intervalos inicial e final? Se o OP quiser tudo entre 14:00 e 15:00, mas não houver entrada de log para 14:00, então?
Ele irá dizer tanto quanto o sedque também está procurando correspondências literais. dategrepé provavelmente a resposta mais correta de todas as respostas dadas (já que você precisa ser "confuso" com relação aos carimbos de data e hora que você aceita), mas, como diz a resposta, eu apenas a mencionei como alternativa. Dito isto, se o log estiver ativo o suficiente para gerar saída suficiente para garantir o corte, provavelmente também haverá algum tipo de entrada para o período de tempo especificado.
precisa saber é o seguinte
0

Usando sed:

#!/bin/bash

E_BADARGS=23

if [ $# -ne "3" ]
then
  echo "Usage: `basename $0` \"<start_date>\" \"<end_date>\" file"
  echo "NOTE:Make sure to put dates in between double quotes"
  exit $E_BADARGS
fi 

isDatePresent(){
        #check if given date exists in file.
        local date=$1
        local file=$2
        grep -q "$date" "$file"
        return $?

}

convertToEpoch(){
    #converts to epoch time
    local _date=$1
    local epoch_date=`date --date="$_date" +%s`
    echo $epoch_date
}

convertFromEpoch(){
    #converts to date/time format from epoch
    local epoch_date=$1
    local _date=`date  --date="@$epoch_date" +"%F %T"`
    echo $_date

}

getDates(){
        # collects all dates at beginning of lines in a file, converts them to epoch and returns a sequence of numbers
        local file="$1"
        local state="$2"
        local i=0
        local date_array=( )
        if [[ "$state" -eq "S" ]];then
            datelist=`cat "$file" | sed -r -e "s/^\[([^\[]+)\].*/\1/" | egrep  "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"`
        elif [[ "$state" -eq "E" ]];then
            datelist=`tac "$file" | sed -r -e "s/^\[([^\[]+)\].*/\1/" | egrep  "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}"`

        else
            echo "Something went wrong while getting dates..." 1>&2
            exit 500
        fi

        while read _date
            do
                epoch_date=`convertToEpoch "$_date"`
                date_array[$i]=$epoch_date
                #echo "$_date" "$epoch_date" 1>&2

            (( i++ ))
            done<<<"$datelist"
        echo ${date_array[@]}   


}

findneighbours(){
    # search next best date if date is not in the file using recursivity
    IFS="$old_IFS"
    local elt=$1
    shift
    local state="$1"
    shift
    local -a array=( "$@" ) 

    index_pivot=`expr ${#array[@]} / 2`
    echo "#array="${#array[@]} ";array="${array[@]} ";index_pivot="$index_pivot 1>&2
    if [ "$index_pivot" -eq 1 -a ${#array[@]} -eq 2 ];then

        if [ "$state" == "E" ];then
            echo ${array[0]}
        elif [ "$state" == "S" ];then
            echo ${array[(( ${#array[@]} - 1 ))]} 
        else
            echo "State" $state "undefined" 1>&2
            exit 100
        fi

    else
        echo "elt with index_pivot="$index_pivot":"${array[$index_pivot]} 1>&2
        if [ $elt -lt ${array[$index_pivot]} ];then
            echo "elt is smaller than pivot" 1>&2
            array=( ${array[@]:0:(($index_pivot + 1)) } )
        else
            echo "elt is bigger than pivot" 1>&2
            array=( ${array[@]:$index_pivot:(( ${#array[@]} - 1 ))} ) 
        fi
        findneighbours "$elt" "$state" "${array[@]}"
    fi
}



findFirstDate(){
    local file="$1"
    echo "Looking for first date in file" 1>&2
    while read line
        do 
            echo "$line" | egrep -q "^\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\]" &>/dev/null
            if [ "$?" -eq "0" ]
            then
                #echo "line=" "$line" 1>&2
                firstdate=`echo "$line" | sed -r -e "s/^\[([^\[]+)\].*/\1/"`
                echo "$firstdate"
                break
            else
                echo $? 1>&2
            fi
        done< <( cat "$file" )



}

findLastDate(){
    local file="$1"
    echo "Looking for last date in file" 1>&2
    while read line
        do 
            echo "$line" | egrep -q "^\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\]" &>/dev/null
            if [ "$?" -eq "0" ]
            then
                #echo "line=" "$line" 1>&2
                lastdate=`echo "$line" | sed -r -e "s/^\[([^\[]+)\].*/\1/"`
                echo "$lastdate"
                break
            else
                echo $? 1>&2
            fi
        done< <( tac "$file" )


}

findBestDate(){

        IFS="$old_IFS"
        local initdate="$1"
        local file="$2"
        local state="$3"
        local first_elts="$4"
        local last_elts="$5"
        local date_array=( )
        local initdate_epoch=`convertToEpoch "$initdate"`   

        if [[ $initdate_epoch -lt $first_elt ]];then
            echo `convertFromEpoch "$first_elt"`
        elif [[ $initdate_epoch -gt $last_elt ]];then
            echo `convertFromEpoch "$last_elt"` 

        else
            date_array=( `getDates "$file" "$state"` )
            echo "date_array="${date_array[@]} 1>&2
            #first_elt=${date_array[0]}
            #last_elt=${date_array[(( ${#date_array[@]} - 1 ))]}

            echo `convertFromEpoch $(findneighbours "$initdate_epoch" "$state" "${date_array[@]}")`

        fi

}


main(){
    init_date_start="$1"
    init_date_end="$2"
    filename="$3"
    echo "problem start.." 1>&2
    date_array=( "$init_date_start","$init_date_end"  )
    flag_array=( 0 0 )
    i=0
    #echo "$IFS" | cat -vte
    old_IFS="$IFS"
    #changing separator to avoid whitespace issue in date/time format
    IFS=,
    for _date in ${date_array[@]}
    do
        #IFS="$old_IFS"
        #echo "$IFS" | cat -vte
        if isDatePresent "$_date" "$filename";then
            if [ "$i" -eq 0 ];then 
                echo "Starting date exists" 1>&2
                #echo "date_start=""$_date" 1>&2
                date_start="$_date"
            else
                echo "Ending date exists" 1>&2
                #echo "date_end=""$_date" 1>&2
                date_end="$_date"
            fi

        else
            if [ "$i" -eq 0 ];then 
                echo "start date $_date not found" 1>&2
            else
                echo "end date $_date not found" 1>&2
            fi
            flag_array[$i]=1
        fi
        #IFS=,
        (( i++ ))
    done

    IFS="$old_IFS"
    if [ ${flag_array[0]} -eq 1 -o ${flag_array[1]} -eq 1 ];then

        first_elt=`convertToEpoch "$(findFirstDate "$filename")"`
        last_elt=`convertToEpoch "$(findLastDate "$filename")"`
        border_dates_array=( "$first_elt","$last_elt" )

        #echo "first_elt=" $first_elt "last_elt=" $last_elt 1>&2
        i=0
        IFS=,
        for _date in ${date_array[@]}
        do
            if [ $i -eq 0 -a ${flag_array[$i]} -eq 1 ];then
                date_start=`findBestDate "$_date" "$filename" "S" "${border_dates_array[@]}"`
            elif [ $i -eq 1 -a ${flag_array[$i]} -eq 1 ];then
                date_end=`findBestDate "$_date" "$filename" "E" "${border_dates_array[@]}"`
            fi

            (( i++ ))
        done
    fi


    sed -r -n "/^\[${date_start}\]/,/^\[${date_end}\]/p" "$filename"

}


main "$1" "$2" "$3"

Copie isso em um arquivo. Se você não deseja ver as informações de depuração, a depuração é enviada para o stderr; basta adicionar "2> / dev / null"

UnX
fonte
11
Isso não exibirá os arquivos de log que não possuem registro de data e hora.
quer
@ Aceite, sim, você já tentou?
UnX
@rMistero, não funcionará porque, se não houver entrada de log às 22:30, o intervalo não será encerrado. Como o OP mencionou, os horários de início e parada podem não estar nos logs. Você pode ajustar seu regex para que ele funcione, mas perderá a resolução e nunca será garantido com antecedência que o intervalo será encerrado no momento certo.
@awk_FTW este foi um exemplo, não usei os carimbos de data e hora fornecidos por Amit. Novamente, regex pode ser usado. Concordo que não funcionará se o carimbo de data / hora não existir quando fornecido explicitamente ou se não houver correspondências de regex de carimbo de data / hora. Eu vou melhorá-lo em breve ..
UNX
"Como o OP mencionou, os horários de início e parada podem não estar nos logs." Não, leia o OP novamente. O OP diz que essas pessoas estarão presentes, mas as linhas intermediárias não começarão necessariamente com um carimbo de data / hora. Nem faz sentido dizer que os horários de parada podem não estar presentes. Como você poderia dizer a qualquer ferramenta onde parar se o marcador de término não estiver garantido? Não haveria critérios para fornecer à ferramenta a indicação de onde parar o processamento.
Bratchley