¿Extracción rápida de un rango de tiempo desde el archivo de registro de syslog?


12

Tengo un archivo de registro en el formato estándar de syslog. Se ve así, excepto con cientos de líneas por segundo:

Jan 11 07:48:46 blahblahblah...
Jan 11 07:49:00 blahblahblah...
Jan 11 07:50:13 blahblahblah...
Jan 11 07:51:22 blahblahblah...
Jan 11 07:58:04 blahblahblah...

No rueda exactamente a la medianoche, pero nunca tendrá más de dos días.

A menudo tengo que extraer un segmento de tiempo de este archivo. Me gustaría escribir un script de propósito general para esto, que pueda llamar como:

$ timegrep 22:30-02:00 /logs/something.log

... y haga que retire las líneas desde las 22:30, en adelante a través del límite de medianoche, hasta las 2 am del día siguiente.

Hay algunas advertencias:

  • No quiero tener que molestarme en escribir las fechas en la línea de comandos, solo las horas. El programa debe ser lo suficientemente inteligente como para resolverlos.
  • El formato de fecha de registro no incluye el año, por lo que debe adivinar en función del año actual, pero de todos modos hacer lo correcto alrededor del día de Año Nuevo.
  • Quiero que sea rápido: debería usar el hecho de que las líneas están para buscar en el archivo y usar una búsqueda binaria.

Antes de pasar un montón de tiempo escribiendo esto, ¿ya existe?

Respuestas:


9

Actualización: he reemplazado el código original con una versión actualizada con numerosas mejoras. Llamemos a esto (¿real?) Calidad alfa.

Esta versión incluye:

  • manejo de opciones de línea de comandos
  • validación de formato de fecha de línea de comando
  • algunos trybloques
  • lectura de línea movida a una función

Texto original:

¿Bueno, qué sabes? "¡Busca y encontrarás! Aquí hay un programa de Python que busca en el archivo y utiliza una búsqueda binaria más o menos. Es considerablemente más rápido que el script AWK que escribió otro tipo .

Es (pre?) De calidad alfa. Debería tener trybloques y validación de entrada y muchas pruebas, y sin duda podría ser más Pythonic. Pero aquí está para tu diversión. Ah, y está escrito para Python 2.6.

Nuevo código:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# timegrep.py by Dennis Williamson 20100113
# in response to http://serverfault.com/questions/101744/fast-extraction-of-a-time-range-from-syslog-logfile

# thanks to serverfault user http://serverfault.com/users/1545/mike
# for the inspiration

# Perform a binary search through a log file to find a range of times
# and print the corresponding lines

# tested with Python 2.6

# TODO: Make sure that it works if the seek falls in the middle of
#       the first or last line
# TODO: Make sure it's not blind to a line where the sync read falls
#       exactly at the beginning of the line being searched for and
#       then gets skipped by the second read
# TODO: accept arbitrary date

# done: add -l long and -s short options
# done: test time format

version = "0.01a"

import os, sys
from stat import *
from datetime import date, datetime
import re
from optparse import OptionParser

# Function to read lines from file and extract the date and time
def getdata():
    """Read a line from a file

    Return a tuple containing:
        the date/time in a format such as 'Jan 15 20:14:01'
        the line itself

    The last colon and seconds are optional and
    not handled specially

    """
    try:
        line = handle.readline(bufsize)
    except:
        print("File I/O Error")
        exit(1)
    if line == '':
        print("EOF reached")
        exit(1)
    if line[-1] == '\n':
        line = line.rstrip('\n')
    else:
        if len(line) >= bufsize:
            print("Line length exceeds buffer size")
        else:
            print("Missing newline")
        exit(1)
    words = line.split(' ')
    if len(words) >= 3:
        linedate = words[0] + " " + words[1] + " " + words[2]
    else:
        linedate = ''
    return (linedate, line)
# End function getdata()

# Set up option handling
parser = OptionParser(version = "%prog " + version)

parser.usage = "\n\t%prog [options] start-time end-time filename\n\n\
\twhere times are in the form hh:mm[:ss]"

parser.description = "Search a log file for a range of times occurring yesterday \
and/or today using the current time to intelligently select the start and end. \
A date may be specified instead. Seconds are optional in time arguments."

parser.add_option("-d", "--date", action = "store", dest = "date",
                default = "",
                help = "NOT YET IMPLEMENTED. Use the supplied date instead of today.")

parser.add_option("-l", "--long", action = "store_true", dest = "longout",
                default = False,
                help = "Span the longest possible time range.")

parser.add_option("-s", "--short", action = "store_true", dest = "shortout",
                default = False,
                help = "Span the shortest possible time range.")

parser.add_option("-D", "--debug", action = "store", dest = "debug",
                default = 0, type = "int",
                help = "Output debugging information.\t\t\t\t\tNone (default) = %default, Some = 1, More = 2")

(options, args) = parser.parse_args()

if not 0 <= options.debug <= 2:
    parser.error("debug level out of range")
else:
    debug = options.debug    # 1 = print some debug output, 2 = print a little more, 0 = none

if options.longout and options.shortout:
    parser.error("options -l and -s are mutually exclusive")

if options.date:
    parser.error("date option not yet implemented")

if len(args) != 3:
    parser.error("invalid number of arguments")

start = args[0]
end   = args[1]
file  = args[2]

# test for times to be properly formatted, allow hh:mm or hh:mm:ss
p = re.compile(r'(^[2][0-3]|[0-1][0-9]):[0-5][0-9](:[0-5][0-9])?$')

if not p.match(start) or not p.match(end):
    print("Invalid time specification")
    exit(1)

# Determine Time Range
yesterday = date.fromordinal(date.today().toordinal()-1).strftime("%b %d")
today     = datetime.now().strftime("%b %d")
now       = datetime.now().strftime("%R")

if start > now or start > end or options.longout or options.shortout:
    searchstart = yesterday
else:
    searchstart = today

if (end > start > now and not options.longout) or options.shortout:
    searchend = yesterday
else:
    searchend = today

searchstart = searchstart + " " + start
searchend = searchend + " " + end

try:
    handle = open(file,'r')
except:
    print("File Open Error")
    exit(1)

# Set some initial values
bufsize = 4096  # handle long lines, but put a limit them
rewind  =  100  # arbitrary, the optimal value is highly dependent on the structure of the file
limit   =   75  # arbitrary, allow for a VERY large file, but stop it if it runs away
count   =    0
size    =    os.stat(file)[ST_SIZE]
beginrange   = 0
midrange     = size / 2
oldmidrange  = midrange
endrange     = size
linedate     = ''

pos1 = pos2  = 0

if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstart, searchend))

# Seek using binary search
while pos1 != endrange and oldmidrange != 0 and linedate != searchstart:
    handle.seek(midrange)
    linedate, line = getdata()    # sync to line ending
    pos1 = handle.tell()
    if midrange > 0:             # if not BOF, discard first read
        if debug > 1: print("...partial: (len: {0}) '{1}'".format((len(line)), line))
        linedate, line = getdata()

    pos2 = handle.tell()
    count += 1
    if debug > 0: print("#{0} Beg: {1} Mid: {2} End: {3} P1: {4} P2: {5} Timestamp: '{6}'".format(count, beginrange, midrange, endrange, pos1, pos2, linedate))
    if  searchstart > linedate:
        beginrange = midrange
    else:
        endrange = midrange
    oldmidrange = midrange
    midrange = (beginrange + endrange) / 2
    if count > limit:
        print("ERROR: ITERATION LIMIT EXCEEDED")
        exit(1)

if debug > 0: print("...stopping: '{0}'".format(line))

# Rewind a bit to make sure we didn't miss any
seek = oldmidrange
while linedate >= searchstart and seek > 0:
    if seek < rewind:
        seek = 0
    else:
        seek = seek - rewind
    if debug > 0: print("...rewinding")
    handle.seek(seek)

    linedate, line = getdata()    # sync to line ending
    if debug > 1: print("...junk: '{0}'".format(line))

    linedate, line = getdata()
    if debug > 0: print("...comparing: '{0}'".format(linedate))

# Scan forward
while linedate < searchstart:
    if debug > 0: print("...skipping: '{0}'".format(linedate))
    linedate, line = getdata()

if debug > 0: print("...found: '{0}'".format(line))

if debug > 0: print("Beg: {0} Mid: {1} End: {2} P1: {3} P2: {4} Timestamp: '{5}'".format(beginrange, midrange, endrange, pos1, pos2, linedate))

# Now that the preliminaries are out of the way, we just loop,
#     reading lines and printing them until they are
#     beyond the end of the range we want

while linedate <= searchend:
    print line
    linedate, line = getdata()

if debug > 0: print("Start: '{0}' End: '{1}'".format(searchstart, searchend))
handle.close()

Guau. Realmente necesito aprender Python ...
Stefan Lasiewski

@ Dennis Williamson: veo una línea que contiene if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstar$. ¿Se searchstarsupone que termina con un $, o es un error tipográfico? Me sale un error de sintaxis en esta línea (Línea 159)
Stefan Lasiewski

@Stefan Reemplazaría eso con )).
Bill Weiss

@Stefan: Gracias. Fue un error tipográfico que he arreglado. Para referencia rápida, en su $lugar debería ser t, searchend))lo que dice... searchstart, searchend))
Pausado hasta nuevo aviso.

@Stefan: Perdón por eso. Creo que eso lo tiene.
Pausado hasta nuevo aviso.

0

A partir de una búsqueda rápida en la red, hay cosas que se extraen en función de las palabras clave (como FIRE o similar :) pero nada que extraiga un rango de fechas del archivo.

No parece difícil hacer lo que propones:

  1. Busca la hora de inicio.
  2. Imprime esa línea.
  3. Si la hora de finalización <hora de inicio y la fecha de una línea es> final y <inicio, entonces deténgase.
  4. Si la hora de finalización es> hora de inicio y la fecha de una línea es> final, pare.

Parece sencillo, y podría escribirlo para ti si no te importa Ruby :)


No me importa Ruby, pero el n. ° 1 no es sencillo si quieres hacerlo de manera eficiente en un archivo grande: debes buscar () hasta el punto medio, encontrar la línea más cercana, ver cómo comienza y repetir con Un nuevo punto medio. Es demasiado ineficiente mirar cada línea.
Mike

Dijiste grande, pero no especificaste un tamaño real. ¿Qué tan grande es grande? Peor aún, si hay varios días involucrados, sería bastante fácil encontrar el incorrecto solo usando el tiempo. Después de todo, si cruza el límite de un día, el día en que se ejecuta el script siempre será diferente a la hora de inicio. ¿Encajarán los archivos en la memoria a través de mmap ()?
Michael Graff

Alrededor de 30 GB, en un disco montado en red.
Mike

0

Esto imprimirá el rango de entradas entre una hora de inicio y una hora de finalización en función de cómo se relacionan con la hora actual ("ahora").

Uso:

timegrep [-l] start end filename

Ejemplo:

$ timegrep 18:47 03:22 /some/log/file

La -lopción (larga) produce la salida más larga posible. La hora de inicio se interpretará como ayer si el valor de horas y minutos de la hora de inicio es menor que la hora de finalización y ahora. La hora de finalización se interpretará como hoy si tanto la hora de inicio como la hora de finalización HH: los valores MM son mayores que "ahora".

Suponiendo que "ahora" es "11 de enero a las 19:00", así es como se interpretarán varios ejemplos de horas de inicio y finalización (sin -lexcepción, como se indica):

inicio final rango comenzar rango final
19:01 23:59 ene 10 ene 10
19:01 00:00 ene 10 ene 11
00:00 18:59 ene 11 ene 11
18:59 18:58 10 de enero 10 de enero
19:01 23:59 10 de enero 11 de enero # -l
00:00 18:59 10 de enero 11 de enero # -l
18:59 19:01 10 de enero 11 de enero # -l

Casi todo el script está configurado. Las dos últimas líneas hacen todo el trabajo.

Advertencia: no se realiza la validación de argumentos ni la comprobación de errores. Los casos de borde no se han probado a fondo. Esto fue escrito usando gawkotras versiones de AWK may squawk.

#!/usr/bin/awk -f
BEGIN {
    arg=1
    if ( ARGV[arg] == "-l" ) {
        long = 1
        ARGV[arg++] = ""
    }
    start = ARGV[arg]
    ARGV[arg++] = ""
    end = ARGV[arg]
    ARGV[arg++] = ""

    yesterday = strftime("%b %d", mktime(strftime("%Y %m %d -24 00 00")))
    today = strftime("%b %d")
    now = strftime("%R")

    if ( start > now || start > end || long )
        startdate = yesterday
    else
        startdate = today

    if ( end > now && end > start && start > now && ! long )
        enddate = yesterday
    else
        enddate = today
    fi

startdate = startdate " " start
enddate = enddate " " end
}

$1 " " $2 " " $3 > enddate {exit}
$1 " " $2 " " $3 >= startdate {print}

Creo que AWK es muy eficiente en la búsqueda de archivos. No creo que otra cosa sea necesariamente más rápida para buscar un archivo de texto no indexado .


Parece que pasaste por alto mi tercer punto. Los registros están en el orden de 30 GB: si la primera línea del archivo es 7:00 y la última línea es 23:00, y quiero el segmento entre 22:00 y 22:01, no quiero El guión examina cada línea entre las 7:00 y las 22:00. Quiero que calcule dónde estaría, busque ese punto y haga una nueva estimación hasta que lo encuentre.
Mike

No lo pasé por alto. Expresé mi opinión en el último párrafo.
Pausado hasta nuevo aviso.

0

Un programa C ++ que aplica una búsqueda binaria; necesitaría algunas modificaciones simples (es decir, llamar a strptime) para trabajar con fechas de texto.

http://gitorious.org/bs_grep/

Tenía una versión anterior con soporte para fechas de texto, sin embargo, todavía era demasiado lenta para la escala de nuestros archivos de registro; la elaboración de perfiles dijo que más del 90% del tiempo se gastó en strptime, por lo que modificamos el formato de registro para incluir también una marca de tiempo numérica de Unix.


0

Aunque esta respuesta es demasiado tarde, podría ser beneficiosa para algunos.

He convertido el código de @Dennis Williamson en una clase de Python que se puede usar para otras cosas de Python.

He agregado soporte para múltiples soportes de fecha.

import os
from stat import *
from datetime import date, datetime
import re

# @TODO Support for rotated log files - currently using the current year for 'Jan 01' dates.
class LogFileTimeParser(object):
    """
    Extracts parts of a log file based on a start and enddate
    Uses binary search logic to speed up searching

    Common usage: validate log files during testing

    Faster than awk parsing for big log files
    """
    version = "0.01a"

    # Set some initial values
    BUF_SIZE = 4096  # self.handle long lines, but put a limit to them
    REWIND = 100  # arbitrary, the optimal value is highly dependent on the structure of the file
    LIMIT = 75  # arbitrary, allow for a VERY large file, but stop it if it runs away

    line_date = ''
    line = None
    opened_file = None

    @staticmethod
    def parse_date(text, validate=True):
        # Supports Aug 16 14:59:01 , 2016-08-16 09:23:09 Jun 1 2005  1:33:06PM (with or without seconds, miliseconds)
        for fmt in ('%Y-%m-%d %H:%M:%S %f', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M',
                    '%b %d %H:%M:%S %f', '%b %d %H:%M', '%b %d %H:%M:%S',
                    '%b %d %Y %H:%M:%S %f', '%b %d %Y %H:%M', '%b %d %Y %H:%M:%S',
                    '%b %d %Y %I:%M:%S%p', '%b %d %Y %I:%M%p', '%b %d %Y %I:%M:%S%p %f'):
            try:
                if fmt in ['%b %d %H:%M:%S %f', '%b %d %H:%M', '%b %d %H:%M:%S']:

                    return datetime.strptime(text, fmt).replace(datetime.now().year)
                return datetime.strptime(text, fmt)
            except ValueError:
                pass
        if validate:
            raise ValueError("No valid date format found for '{0}'".format(text))
        else:
            # Cannot use NoneType to compare datetimes. Using minimum instead
            return datetime.min

    # Function to read lines from file and extract the date and time
    def read_lines(self):
        """
        Read a line from a file
        Return a tuple containing:
            the date/time in a format supported in parse_date om the line itself
        """
        try:
            self.line = self.opened_file.readline(self.BUF_SIZE)
        except:
            raise IOError("File I/O Error")
        if self.line == '':
            raise EOFError("EOF reached")
        # Remove \n from read lines.
        if self.line[-1] == '\n':
            self.line = self.line.rstrip('\n')
        else:
            if len(self.line) >= self.BUF_SIZE:
                raise ValueError("Line length exceeds buffer size")
            else:
                raise ValueError("Missing newline")
        words = self.line.split(' ')
        # This results into Jan 1 01:01:01 000000 or 1970-01-01 01:01:01 000000
        if len(words) >= 3:
            self.line_date = self.parse_date(words[0] + " " + words[1] + " " + words[2],False)
        else:
            self.line_date = self.parse_date('', False)
        return self.line_date, self.line

    def get_lines_between_timestamps(self, start, end, path_to_file, debug=False):
        # Set some initial values
        count = 0
        size = os.stat(path_to_file)[ST_SIZE]
        begin_range = 0
        mid_range = size / 2
        old_mid_range = mid_range
        end_range = size
        pos1 = pos2 = 0

        # If only hours are supplied
        # test for times to be properly formatted, allow hh:mm or hh:mm:ss
        p = re.compile(r'(^[2][0-3]|[0-1][0-9]):[0-5][0-9](:[0-5][0-9])?$')
        if p.match(start) or p.match(end):
            # Determine Time Range
            yesterday = date.fromordinal(date.today().toordinal() - 1).strftime("%Y-%m-%d")
            today = datetime.now().strftime("%Y-%m-%d")
            now = datetime.now().strftime("%R")
            if start > now or start > end:
                search_start = yesterday
            else:
                search_start = today
            if end > start > now:
                search_end = yesterday
            else:
                search_end = today
            search_start = self.parse_date(search_start + " " + start)
            search_end = self.parse_date(search_end + " " + end)
        else:
            # Set dates
            search_start = self.parse_date(start)
            search_end = self.parse_date(end)
        try:
            self.opened_file = open(path_to_file, 'r')
        except:
            raise IOError("File Open Error")
        if debug:
            print("File: '{0}' Size: {1} Start: '{2}' End: '{3}'"
                  .format(path_to_file, size, search_start, search_end))

        # Seek using binary search -- ONLY WORKS ON FILES WHO ARE SORTED BY DATES (should be true for log files)
        try:
            while pos1 != end_range and old_mid_range != 0 and self.line_date != search_start:
                self.opened_file.seek(mid_range)
                # sync to self.line ending
                self.line_date, self.line = self.read_lines()
                pos1 = self.opened_file.tell()
                # if not beginning of file, discard first read
                if mid_range > 0:
                    if debug:
                        print("...partial: (len: {0}) '{1}'".format((len(self.line)), self.line))
                    self.line_date, self.line = self.read_lines()
                pos2 = self.opened_file.tell()
                count += 1
                if debug:
                    print("#{0} Beginning: {1} Mid: {2} End: {3} P1: {4} P2: {5} Timestamp: '{6}'".
                          format(count, begin_range, mid_range, end_range, pos1, pos2, self.line_date))
                if search_start > self.line_date:
                    begin_range = mid_range
                else:
                    end_range = mid_range
                old_mid_range = mid_range
                mid_range = (begin_range + end_range) / 2
                if count > self.LIMIT:
                    raise IndexError("ERROR: ITERATION LIMIT EXCEEDED")
            if debug:
                print("...stopping: '{0}'".format(self.line))
            # Rewind a bit to make sure we didn't miss any
            seek = old_mid_range
            while self.line_date >= search_start and seek > 0:
                if seek < self.REWIND:
                    seek = 0
                else:
                    seek -= self.REWIND
                if debug:
                    print("...rewinding")
                self.opened_file.seek(seek)
                # sync to self.line ending
                self.line_date, self.line = self.read_lines()
                if debug:
                    print("...junk: '{0}'".format(self.line))
                self.line_date, self.line = self.read_lines()
                if debug:
                    print("...comparing: '{0}'".format(self.line_date))
            # Scan forward
            while self.line_date < search_start:
                if debug:
                    print("...skipping: '{0}'".format(self.line_date))
                self.line_date, self.line = self.read_lines()
            if debug:
                print("...found: '{0}'".format(self.line))
            if debug:
                print("Beginning: {0} Mid: {1} End: {2} P1: {3} P2: {4} Timestamp: '{5}'".
                      format(begin_range, mid_range, end_range, pos1, pos2, self.line_date))
            # Now that the preliminaries are out of the way, we just loop,
            # reading lines and printing them until they are beyond the end of the range we want
            while self.line_date <= search_end:
                # Exclude our 'Nonetype' values
                if not self.line_date == datetime.min:
                    print self.line
                self.line_date, self.line = self.read_lines()
            if debug:
                print("Start: '{0}' End: '{1}'".format(search_start, search_end))
            self.opened_file.close()
        # Do not display EOFErrors:
        except EOFError as e:
            pass
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.