Швидке вилучення діапазону часу з лог-файлу syslog?


12

У мене лог-файл у стандартному форматі syslog. Це виглядає приблизно так, за винятком сотень рядків в секунду:

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

Він не котиться рівно опівночі, але в ньому ніколи не буде більше двох днів.

Мені часто доводиться витягувати часовий фрагмент із цього файлу. Я хотів би написати для цього сценарій загального призначення, який я можу назвати так:

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

... і дозвольте йому витягнути лінії з 22:30, далі через півночі, до 2 ранку наступного дня.

Є кілька застережень:

  • Мені не хочеться турбуватися, вводячи дату (-и) в командному рядку, лише рази. Програма повинна бути достатньо розумною, щоб зрозуміти їх.
  • Формат дати журналу не включає рік, тому він повинен здогадуватися, виходячи з поточного року, але, тим не менш, робити все правильно навколо Нового року.
  • Я хочу, щоб це було швидко - він повинен використовувати той факт, що рядки є для того, щоб шукати навколо файлу та використовувати двійковий пошук.

Перш ніж я витратити купу часу на написання цього, чи воно вже існує?

Відповіді:


9

Оновлення: Я замінив оригінальний код оновленою версією з численними вдосконаленнями. Назвемо цю (справжню?) Альфа-якість.

Ця версія включає:

  • обробка параметрів командного рядка
  • Перевірка формату дати в командному рядку
  • деякі tryблоки
  • зчитування рядків перейшло у функцію

Оригінальний текст:

Ну що ти знаєш? "Шукайте", і ви знайдете! Ось програма Python, яка шукає навколо файлу і використовує більш-менш бінарний пошук. Це значно швидше, ніж той сценарій AWK, який написав інший хлопець .

Це (попередньо?) Альфа-якість. Він повинен мати tryблоки та перевірку вхідних даних та багато тестування, і, без сумніву, може бути більш пітонічним. Але ось це для вашого розваги. О, і це написано для Python 2.6.

Новий код:

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

Ого. Мені справді потрібно вивчити Python ...
Стефан Ласєвський

@Денніс Вільямсон: Я бачу рядок, що містить if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstar$. Чи searchstarналежить закінчитися $символом "чи", чи це помилка друку? Я отримую синтаксичну помилку в цьому рядку (рядок 159)
Стефан Ласєвський

@Stefan Я замінив би це )).
Білл Вайс

@Stefan: Дякую Це була помилка, яку я виправив. Для швидкого ознайомлення, $натомість має бути t, searchend))так, що сказано... searchstart, searchend))
Призупинено до подальшого повідомлення.

@Stefan: Вибачте за це. Я думаю, що це вдалося.
Призупинено до подальшого повідомлення.

0

Швидкий пошук в Інтернеті - це речі, які витягуються на основі ключових слів (наприклад, FIRE або подібних :), але нічого, що витягує діапазон дат з файлу.

Не здається важким зробити те, що ви пропонуєте:

  1. Шукати час початку.
  2. Роздрукуйте цей рядок.
  3. Якщо час закінчення <час початку, а дата рядка> кінець і <початок, то зупиніться.
  4. Якщо час закінчення> час початку, а дата рядка - кінець, зупиніть.

Здається, прямо, і я міг би написати це для вас, якщо ви не заперечуєте Рубі :)


Я не проти Рубі, але №1 не є простим, якщо ви хочете зробити це ефективно у великому файлі - вам потрібно шукати () на півдорозі, знайти найближчий рядок, подивитися, як він починається, і повторити з нова середина. Це надто неефективно, щоб дивитися на кожен рядок.
Майк

Ви сказали великий, але не вказали фактичний розмір. Наскільки велика велика? Гірше, якщо задіяно кілька днів, було б досить легко знайти неправильний, лише використовуючи час. Зрештою, якщо ви перетнете межу дня, день запуску сценарію завжди буде іншим, ніж час початку. Чи помістяться файли в пам'яті через mmap ()?
Майкл Графф

На мережевому диску близько 30 Гб.
Майк

0

Це надрукує діапазон записів між початковим та кінцевим часом, залежно від того, як вони відносяться до поточного часу ("зараз").

Використання:

timegrep [-l] start end filename

Приклад:

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

Параметр -l(довгий) викликає найдовший можливий вихід. Час початку буде трактуватися як вчора, якщо значення годин і хвилин часу початку менше, ніж часу закінчення та зараз. Час закінчення буде інтерпретуватися як сьогодні, якщо значення часу початку та часу закінчення HH: MM перевищують значення "зараз".

Якщо припустити, що "зараз" є "11 січня 19:00", то так трактуються різні приклади часу початку та закінчення (без -lвинятку, як зазначено):

start end end діапазон begin end end
19:01 23:59 10 січня 10 січня
19:01 00:00 10 січня 11 січня
00:00 18:59 11 січня 11 січня
18:59 18:58 10 січня 10 січня
19:01 23:59 10 січня 11 січня # -л
00:00 18:59 січ 10 січ. 11 -л
18:59 19:01 10 січня 11 січня # -л

Майже весь сценарій налаштований. Останні два рядки виконують всю роботу.

Попередження: перевірка аргументу чи перевірка помилок не проводиться. Корпусні кромки не були ретельно перевірені. Це було написано з використанням gawkінших версій 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}

Я думаю, що AWK дуже ефективний у пошуку файлів. Я не думаю, що нічого іншого обов’язково не буде швидше при пошуку невкладеного текстового файлу.


Здається, ви пропустили мою третю точку кулі. Журнали складають порядку 30 ГБ - якщо перший рядок файлу - 7:00, а останній - 23:00, а я хочу фрагмент між 22:00 та 22:01, я не хочу сценарій переглядає кожен рядок між 7:00 та 22:00. Я хочу, щоб він міг оцінити, де він би знаходився, шукати до цього моменту і робити нову оцінку, поки не знайде.
Майк

Я цього не помічав. Я висловив свою думку в останньому абзаці.
Призупинено до подальшого повідомлення.

0

Програма C ++, яка застосовує двійковий пошук - для роботи з текстовими датами знадобиться кілька простих модифікацій (тобто виклик strptime).

http://gitorious.org/bs_grep/

У мене була попередня версія з підтримкою дат тексту, проте вона все ще була надто повільною для масштабу наших файлів журналів; Профілінг зазначив, що понад 90% часу було проведено в режимі стриптизу, тому ми просто змінили формат журналу, щоб включити також числову часову позначку unix.


0

Незважаючи на те, що ця відповідь є занадто пізно, для когось це може бути корисним.

Я перетворив код від @Dennis Williamson в клас Python, який можна використовувати для інших матеріалів python.

Я додав підтримку для декількох підтримок дат.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.