Extraction rapide d'une plage de temps à partir du fichier journal syslog?

12

J'ai un fichier journal au format syslog standard. Cela ressemble à ceci, sauf avec des centaines de lignes par seconde:

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...

Il ne roule pas exactement à minuit, mais il n'aura jamais plus de deux jours.

Je dois souvent extraire une tranche de temps de ce fichier. Je voudrais écrire un script à usage général pour cela, que je peux appeler comme:

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

... et faites-lui retirer les lignes à partir de 22h30, en traversant la frontière de minuit, jusqu'à 2 heures du matin le lendemain.

Il y a quelques mises en garde:

  • Je ne veux pas avoir à me soucier de taper les dates sur la ligne de commande, juste les heures. Le programme doit être suffisamment intelligent pour les comprendre.
  • Le format de la date du journal n'inclut pas l'année, donc il devrait deviner en fonction de l'année en cours, mais néanmoins faire la bonne chose autour du jour de l'an.
  • Je veux que ce soit rapide - il devrait utiliser le fait que les lignes sont pour chercher dans le fichier et utiliser une recherche binaire.

Avant de passer beaucoup de temps à écrire ceci, existe-t-il déjà?

Mike
la source

Réponses:

9

Mise à jour: j'ai remplacé le code d'origine par une version mise à jour avec de nombreuses améliorations. Appelons cette qualité alpha (réelle?).

Cette version comprend:

  • gestion des options de ligne de commande
  • validation du format de date en ligne de commande
  • quelques tryblocs
  • lecture de ligne déplacée dans une fonction

Texte original:

Bien, que sait-tu? "Cherchez et vous trouverez! Voici un programme Python qui cherche autour du fichier et utilise une recherche binaire plus ou moins. C'est considérablement plus rapide que ce script AWK écrit par un autre gars .

C'est (pré?) De qualité alpha. Il devrait avoir des tryblocs et une validation d'entrée et beaucoup de tests et pourrait sans aucun doute être plus Pythonic. Mais ici, c'est pour votre amusement. Oh, et c'est écrit pour Python 2.6.

Nouveau code:

#!/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()
En pause jusqu'à nouvel ordre.
la source
Sensationnel. J'ai vraiment besoin d'apprendre le Python ...
Stefan Lasiewski
@Dennis Williamson: Je vois une ligne contenant if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstar$. Est-ce searchstarcensé se terminer par une $ou est-ce une faute de frappe? Je reçois une erreur de syntaxe sur cette ligne (ligne 159)
Stefan Lasiewski
@Stefan, je remplacerais cela par )).
Bill Weiss
@Stefan: Merci. C'était une faute de frappe que j'ai corrigée. Pour référence rapide, le $devrait plutôt être t, searchend))ainsi il est dit... searchstart, searchend))
pause jusqu'à nouvel ordre.
@Stefan: Désolé pour ça. Je pense que ça y est.
pause jusqu'à nouvel ordre.
0

À partir d'une recherche rapide sur le net, il y a des choses qui sont extraites en fonction de mots clés (comme FIRE ou autre :) mais rien qui extrait une plage de dates du fichier.

Il ne semble pas difficile de faire ce que vous proposez:

  1. Recherchez l'heure de début.
  2. Imprimez cette ligne.
  3. Si l'heure de fin <heure de début et la date d'une ligne est> fin et <début, alors arrêtez.
  4. Si l'heure de fin est> heure de début et que la date d'une ligne est> fin, arrêtez.

Cela semble simple, et je pourrais l'écrire pour vous si cela ne vous dérange pas Ruby :)

Michael Graff
la source
Cela ne me dérange pas Ruby, mais # 1 n'est pas simple si vous voulez le faire efficacement dans un grand fichier - vous devez rechercher () à mi-chemin, trouver la ligne la plus proche, voir comment cela commence et répéter avec un nouveau point médian. C'est trop inefficace pour regarder chaque ligne.
Mike
Vous avez dit grand, mais vous n'avez pas spécifié de taille réelle. À quel point est grand est grand? Pire encore, s'il y a plusieurs jours, il serait assez facile de trouver le mauvais en utilisant uniquement le temps. Après tout, si vous franchissez une limite de jour, le jour d'exécution du script sera toujours différent de l'heure de début. Les fichiers tiendront-ils en mémoire via mmap ()?
Michael Graff
Environ 30 Go, sur un disque monté en réseau.
Mike
0

Cela imprimera la plage d'entrées entre une heure de début et une heure de fin en fonction de leur relation avec l'heure actuelle ("maintenant").

Usage:

timegrep [-l] start end filename

Exemple:

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

L' -loption (longue) entraîne la sortie la plus longue possible. L'heure de début sera interprétée comme hier si la valeur des heures et des minutes de l'heure de début est inférieure à l'heure de fin et maintenant. L'heure de fin sera interprétée comme aujourd'hui si les valeurs de l'heure de début et de l'heure de fin HH: MM sont supérieures à "maintenant".

En supposant que "maintenant" est "11 janvier 19:00", voici comment divers exemples d'heures de début et de fin seront interprétés (sans, -lsauf indication contraire):

début fin plage début fin plage
19:01 23:59 10 janv. 10 janv.
19:01 00:00 10 janv. 11 janv.
00:00 18:59 11 janv. 11 janv.
18:59 18:58 10 janv.10 janv.
19:01 23:59 10 janv. 11 janv. # -L
00:00 18:59 10 janv. 11 janv. # -L
18:59 19:01 10 janv. 11 janv. # -L

Presque tout le script est configuré. Les deux dernières lignes font tout le travail.

Avertissement: aucune validation d'argument ni vérification d'erreur n'est effectuée. Les boîtiers Edge n'ont pas été testés de manière approfondie. Cela a été écrit en utilisant d' gawkautres versions d'AWK peut 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}

Je pense qu'AWK est très efficace pour rechercher des fichiers. Je ne pense pas que quoi que ce soit d'autre soit nécessairement plus rapide pour rechercher un fichier texte non indexé .

En pause jusqu'à nouvel ordre.
la source
Il semble que vous ayez oublié mon troisième point. Les journaux sont de l'ordre de 30 Go - si la première ligne du fichier est 7h00 et la dernière ligne est 23h00, et je veux la tranche entre 22h00 et 22h01, je ne veux pas le script regardant chaque ligne entre 7h00 et 22h00. Je veux qu'il estime où il serait, cherche à ce point et fasse une nouvelle estimation jusqu'à ce qu'il le trouve.
Mike
Je ne l'ai pas oublié. J'ai exprimé mon opinion dans le dernier paragraphe.
pause jusqu'à nouvel ordre.
0

Un programme C ++ appliquant une recherche binaire - il aurait besoin de quelques modifications simples (c'est-à-dire appeler strptime) pour fonctionner avec des dates de texte.

http://gitorious.org/bs_grep/

J'avais une version précédente avec prise en charge des dates de texte, mais elle était encore trop lente pour l'échelle de nos fichiers journaux; le profilage a déclaré que plus de 90% du temps était passé en strptime, nous avons donc juste modifié le format du journal pour inclure également un horodatage numérique unix.


la source
0

Même si cette réponse est beaucoup trop tardive, elle pourrait être bénéfique pour certains.

J'ai converti le code de @Dennis Williamson en une classe Python qui peut être utilisée pour d'autres choses en python.

J'ai ajouté la prise en charge de plusieurs supports de date.

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
Jeffrey Devloo
la source