Szybka ekstrakcja zakresu czasu z pliku dziennika syslog?


12

Mam plik dziennika w standardowym formacie syslog. Wygląda to tak, z wyjątkiem setek linii na sekundę:

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

Nie toczy się dokładnie o północy, ale nigdy nie będzie miał więcej niż dwa dni.

Często muszę wyodrębnić wycinek czasu z tego pliku. Chciałbym w tym celu napisać skrypt ogólnego przeznaczenia, który mogę nazwać:

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

... i niech wyciągnie linie od 22:30, przekraczając granicę północy, do 2 w nocy następnego dnia.

Istnieje kilka zastrzeżeń:

  • Nie chcę zawracać sobie głowy wpisywaniem dat w wierszu poleceń, tylko razy. Program powinien być wystarczająco inteligentny, aby je rozgryźć.
  • Format daty dziennika nie obejmuje roku, więc powinien zgadywać w oparciu o bieżący rok, ale mimo to postępuj właściwie w Nowy Rok.
  • Chcę, aby był szybki - powinien wykorzystywać fakt, że wiersze mają na celu wyszukiwanie w pliku i wyszukiwanie binarne.

Czy zanim to nastąpi, czy już istnieje?

Odpowiedzi:


9

Aktualizacja: zastąpiłem oryginalny kod zaktualizowaną wersją z licznymi ulepszeniami. Nazwijmy to (rzeczywistą?) Jakością alfa.

Ta wersja zawiera:

  • obsługa opcji wiersza poleceń
  • sprawdzanie poprawności formatu daty w wierszu poleceń
  • niektóre trybloki
  • czytanie linii przeniesiono do funkcji

Oryginalny tekst:

Co ty wiesz „Szukajcie”, a znajdziecie! Oto program w języku Python, który szuka w pliku i używa mniej więcej wyszukiwania binarnego. Jest znacznie szybszy niż skrypt AWK napisany przez innego faceta .

Jest to (wcześniej?) Jakość alfa. Powinien mieć trybloki i sprawdzanie poprawności danych wejściowych oraz wiele testów i bez wątpienia powinien być bardziej Pythonic. Ale tutaj jest dla twojej rozrywki. Aha, i jest napisane dla Pythona 2.6.

Nowy kod:

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

Łał. Naprawdę muszę nauczyć się Pythona ...
Stefan Lasiewski

@Dennis Williamson: Widzę wiersz zawierający if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstar$. Czy searchstarma to kończyć $się literą, czy to literówka? W tej linii pojawia się błąd składniowy (wiersz 159)
Stefan Lasiewski

@Stefan Zastąpiłbym to )).
Bill Weiss,

@Stefan: Dzięki. To była literówka, którą naprawiłem. Dla szybkiego odniesienia $powinno być t, searchend))tak, że mówi... searchstart, searchend))
Wstrzymano do odwołania.

@Stefan: Przepraszam za to. Myślę, że o to chodzi.
Wstrzymano do odwołania.

0

Z szybkiego wyszukiwania w sieci istnieją rzeczy, które wyodrębniają na podstawie słów kluczowych (np. FIRE itp.), Ale nic, co wyodrębnia zakres dat z pliku.

Robienie tego, co proponujesz, nie wydaje się trudne:

  1. Wyszukaj czas rozpoczęcia.
  2. Wydrukuj ten wiersz.
  3. Jeśli czas zakończenia <godzina rozpoczęcia, a data wiersza to> koniec i <początek, to zatrzymaj się.
  4. Jeśli czas zakończenia to> czas rozpoczęcia, a data linii to> koniec, zatrzymaj się.

Wydaje się to proste i mógłbym to dla ciebie napisać, jeśli nie masz nic przeciwko Ruby :)


Nie mam nic przeciwko Ruby, ale # 1 nie jest proste, jeśli chcesz to zrobić skutecznie w dużym pliku - musisz szukać () do połowy, znaleźć najbliższą linię, zobaczyć, jak się zaczyna i powtórzyć z nowy punkt środkowy. Zbyt mało wydajne jest patrzenie na każdą linię.
Mike

Powiedziałeś duży, ale nie określiłeś rzeczywistego rozmiaru. Jak duży jest duży? Co gorsza, jeśli wiąże się to z kilkoma dniami, znalezienie niewłaściwego czasu byłoby bardzo łatwe. W końcu, jeśli przekroczysz granicę dnia, dzień, w którym skrypt działa, zawsze będzie inny niż czas rozpoczęcia. Czy pliki zmieszczą się w pamięci za pomocą mmap ()?
Michael Graff,

Około 30 GB na dysku podłączonym do sieci.
Mike

0

Spowoduje to wydrukowanie zakresu wpisów między czasem rozpoczęcia a czasem zakończenia w oparciu o ich związek z bieżącym czasem („teraz”).

Stosowanie:

timegrep [-l] start end filename

Przykład:

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

Opcja -l(długa) powoduje jak najdłuższe wyjście. Czas rozpoczęcia będzie interpretowany jako wczoraj, jeśli wartość godzin i minut czasu rozpoczęcia jest mniejsza niż zarówno godzina zakończenia, jak i teraz. Czas zakończenia będzie interpretowany jak dzisiaj, jeśli zarówno godzina rozpoczęcia, jak i godzina zakończenia GG: MM są większe niż „teraz”.

Zakładając, że „teraz” to „11 stycznia 19:00”, w ten sposób będą interpretowane różne przykładowe czasy rozpoczęcia i zakończenia (bez -lwyjątku jak wspomniano):

początek końca zakresu początek końca końca
19:01 23:59, 10 stycznia, 10 stycznia
19:01 00:00, 10 stycznia 11 stycznia
00:00 18:59 11 stycznia 11 stycznia
18:59 18:58 10 stycznia 10 stycznia
19:01 23:59 10 stycznia 11 stycznia # -l
00:00 18:59 10 stycznia 11 stycznia # -l
18:59 19:01 10 stycznia 11 stycznia # -l

Prawie cały skrypt jest skonfigurowany. Ostatnie dwie linie wykonują całą pracę.

Ostrzeżenie: nie jest wykonywane sprawdzanie poprawności argumentów ani sprawdzanie błędów. Obudowy Edge nie zostały dokładnie przetestowane. Zostało to napisane przy użyciu gawkinnych wersji AWK może skrzeczeć.

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

Myślę, że AWK jest bardzo wydajny w wyszukiwaniu plików. Nie sądzę, aby cokolwiek innego mogło być szybsze w wyszukiwaniu nieindeksowanego pliku tekstowego.


Wygląda na to, że przeoczyłeś mój trzeci punkt kuli. Dzienniki są rzędu 30 GB - jeśli pierwszy wiersz pliku to 7:00, a ostatni wiersz to 23:00, a chcę odcinek między 22:00 a 22:01, nie chcę skrypt przyglądający się każdej linii między 7:00 a 22:00. Chcę, aby oszacował, gdzie to będzie, starał się do tego momentu i dokonał nowej oceny, dopóki go nie znajdzie.
Mike

Nie przeoczyłem tego. Wyraziłem swoją opinię w ostatnim akapicie.
Wstrzymano do odwołania.

0

Program C ++ stosujący wyszukiwanie binarne - do pracy z datami tekstowymi potrzebowałby kilku prostych modyfikacji (np. Wywołania strptime).

http://gitorious.org/bs_grep/

Miałem poprzednią wersję z obsługą dat tekstowych, jednak wciąż była zbyt wolna jak na skalę naszych plików dziennika; z profilowania wynika, że ​​ponad 90% czasu spędzono na strptime, więc właśnie zmodyfikowaliśmy format dziennika, aby uwzględnić także numeryczny znacznik czasu unix.


0

Chociaż ta odpowiedź jest zbyt późna, dla niektórych może być korzystna.

Przekształciłem kod z @Dennis Williamson w klasę Python, której można używać do innych rzeczy w Pythonie.

Dodałem obsługę wielu dat.

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.