Come posso usare bash (grep / sed / etc) per prendere una sezione di un file di log tra 2 timestamp?

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

Domanda

Ho una serie di registri di posta: mail.log mail.log.0 mail.log.1.gz mail.log.2.gz

ognuno di questi file contiene righe ordinate cronologicamente che iniziano con timestamp come:

3 maggio 13:21:12 ...

Come posso facilmente prendere ogni voce del registro dopo una certa data / ora e prima di un'altra data / ora usando bash (e i relativi strumenti della riga di comando) senza confrontare ogni singola riga? Tieni presente che le mie date prima e dopo potrebbero non corrispondere esattamente a nessuna voce nei file di registro.

Mi sembra che sia necessario determinare l'offset della prima riga maggiore del timestamp iniziale e l'offset dell'ultima riga inferiore al timestamp finale e tagliare in qualche modo quella sezione.

È stato utile?

Soluzione 2

Ecco un'idea di base su come farlo:

  1. Esamina il datestamp sul file per vedere se è irrilevante
  2. Se potrebbe essere pertinente, decomprimere se necessario ed esaminare la prima e ultima riga del file per vedere se contiene l'ora di inizio o di fine.
  3. In caso affermativo, utilizzare una funzione ricorsiva per determinare se contiene l'ora di inizio nella prima o nella seconda metà del file. Utilizzando una funzione ricorsiva, penso che potresti trovare qualsiasi data in un file di log da un milione di righe con circa 20 confronti.
  4. echo i file di registro in ordine dall'offset della prima voce all'offset dell'ultima voce (non più confronti)

Quello che non so è: come leggere al meglio l'ennesima riga di un file (quanto è efficiente usare tail n + ** n | head 1 **?)

Qualche aiuto?

Altri suggerimenti

Converti le tue date min / max in " secondi dall'epoca " ;,

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

Converti le prime n parole in ciascuna riga del registro allo stesso

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

Confronta e getta via le linee fino a raggiungere MIN ,

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

Confronta e stampa le linee fino a raggiungere MAX ,

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

Esci quando superi MAX .

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

L'intero script minmaxlog.sh si presenta così,

#!/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

L'ho eseguito su questo file 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

come questo,

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

Devi guardare ogni singola riga nell'intervallo che vuoi (per dire se è nell'intervallo che vuoi) quindi suppongo che tu intenda non tutte le righe nel file. Come minimo, dovrai guardare tutte le righe del file fino al primo compreso, compreso il primo, al di fuori del tuo intervallo (suppongo che le righe siano nell'ordine di data / ora).

Questo è un modello abbastanza semplice:

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

Puoi scrivere questo in awk, Perl, Python, anche COBOL se devi ma la logica è sempre la stessa.

Individuare prima i numeri di riga (con dire grep) e poi semplicemente stampare alla cieca quell'intervallo di righe non sarà d'aiuto poiché grep deve anche guardare tutte le righe ( tutte , non solo fino al primo al di fuori dell'intervallo, e molto probabilmente due volte , uno per la prima riga e uno per l'ultima).

Se questo è qualcosa che farai abbastanza spesso, potresti prendere in considerazione l'idea di spostare lo sforzo da "ogni volta che lo fai" a "una volta, quando il file è stabilizzato". Un esempio potrebbe essere quello di caricare le righe del file di registro in un database, indicizzato per data / ora.

L'installazione richiede un po 'di tempo, ma le tue query diventeranno molto più veloci. Non sto necessariamente sostenendo un database: probabilmente potresti ottenere lo stesso effetto suddividendo i file di registro in registri orari in questo modo:

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

Quindi per un determinato momento, sai esattamente da dove iniziare e smettere di cercare. L'intervallo da 2009/01 / 01-15: 22 a 2009/01 / 05-09: 07 comporterebbe:

  • alcuni (l'ultimo bit) del file 2009/01/01 / 1500.txt .
  • tutti i file 2009/01/01/1 [6-9] *. txt .
  • tutti i file 2009/01/01/2 * .txt .
  • tutti i file 2009/01/0 [2-4] / *. txt .
  • tutti i file 2009/01/05/0 [0-8] *. txt .
  • alcuni (il primo bit) del file 2009/01/05 / 0900.txt .

Certo, scriverei uno script per restituire quelle righe invece di provare a farlo manualmente ogni volta.

Forse puoi provare questo:

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

Potrebbe essere possibile in un ambiente Bash, ma dovresti davvero sfruttare gli strumenti che hanno un supporto più integrato per lavorare con Stringhe e Date. Ad esempio, Ruby sembra avere la capacità integrata di analizzare il formato della data. Può quindi convertirlo in un Unix Timestamp facilmente comparabile (un numero intero positivo che rappresenta i secondi dall'epoca).

irb> require 'time'
# => true

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

È quindi possibile scrivere facilmente uno script Ruby:

  • Fornisci una data di inizio e fine. Converti quelli in questo numero Unix Timestamp.
  • Esegui la scansione dei file di registro riga per riga, convertendo la data nel suo timestamp Unix e controlla se rientra nell'intervallo delle date di inizio e fine.

Nota: la conversione in un numero intero di data / ora Unix è utile perché il confronto di numeri interi è molto semplice ed efficiente da eseguire.

Hai menzionato " senza confrontare ogni singola riga. " Sarà difficile "indovinare" nel punto in cui nel file di registro le voci iniziano a essere troppo vecchie o troppo nuove senza controllare tutti i valori tra. Tuttavia, se c'è davvero una tendenza monotonicamente crescente, allora sai immediatamente quando interrompere l'analisi delle linee, perché non appena la voce successiva è troppo nuova (o vecchia, a seconda del layout dei dati) sai che puoi interrompere la ricerca. Tuttavia, c'è il problema di trovare la prima riga nell'intervallo desiderato.


Ho appena notato la tua modifica. Ecco cosa direi:

Se sei veramente preoccupato di trovare in modo efficiente quella voce di inizio e fine, allora potresti fare una ricerca binaria per ognuna. Oppure, se questo sembra eccessivo o troppo difficile con gli strumenti bash, potresti avere un'euristica di leggere solo il 5% delle righe (1 su ogni 20), per avvicinarti rapidamente alla risposta esatta e quindi perfezionarla se lo desideri. Questi sono solo alcuni suggerimenti per migliorare le prestazioni.

So che questo thread è vecchio, ma mi sono appena imbattuto in esso dopo aver recentemente trovato una soluzione a una riga per le mie esigenze:

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

In questo caso, il mio file ha record con valori separati da virgola e il timestamp nel primo campo. È possibile utilizzare qualsiasi formato di data / ora valido per i timestamp di inizio e fine e, se lo si desidera, sostituire queste variabili di shell.

Se vuoi scrivere su un nuovo file, usa il normale reindirizzamento dell'output ( > newfile ) aggiunto alla fine di cui sopra.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top