Быстрое извлечение временного диапазона из лог-файла системного журнала?

12

У меня есть файл журнала в стандартном формате системного журнала. Это выглядит так, за исключением сотен строк в секунду:

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блоки и проверку входных данных и много тестов, и, несомненно, может быть более Pythonic. Но здесь это для вашего удовольствия. О, и это написано для 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 Lasiewski
@ Стефан, я бы заменил это на )).
Билл Вайс
@ Стефан: Спасибо. Это была опечатка, которую я исправил. Для быстрой справки, $вместо этого следует t, searchend))так говорит... searchstart, searchend))
не Приостановлено до дальнейшего уведомления.
@ Стефан: Извините за это. Я думаю, что понял.
Приостановлено до дальнейшего уведомления.
0

Из быстрого поиска в сети есть вещи, которые извлекаются на основе ключевых слов (например, ОГОНЬ или тому подобное :), но ничего, что извлекает диапазон дат из файла.

Это не кажется трудным сделать то, что вы предлагаете:

  1. Ищите время начала.
  2. Распечатайте эту строку.
  3. Если время окончания <время начала и дата строки> конец и <начало, то остановитесь.
  4. Если время окончания> время начала, а дата строки> конец, остановите.

Кажется прямо, и я мог бы написать это для вас, если вы не против Руби :)

Майкл Графф
источник
Я не возражаю против Ruby, но # 1 непросто, если вы хотите сделать это эффективно в большом файле - вам нужно искать () до середины, найти ближайшую строку, посмотреть, как она начинается, и повторить с новая середина. Слишком неэффективно смотреть на каждую строчку.
Майк
Вы сказали большой, но не указали фактический размер. Насколько велика велика? Хуже того, если задействовано несколько дней, было бы довольно легко найти неправильный, используя только время. В конце концов, если вы пересекаете границу дня, день, когда выполняется скрипт, всегда будет отличаться от времени начала. Будут ли файлы помещаться в память через mmap ()?
Майкл Графф
Около 30 ГБ, на сетевом диске.
Майк
0

Это напечатает диапазон записей между временем начала и временем окончания, основываясь на том, как они соотносятся с текущим временем («сейчас»).

Использование:

timegrep [-l] start end filename

Пример:

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

Опция -l(long) приводит к максимально возможному выходу. Время начала будет интерпретироваться как вчера, если значение времени начала в часах и минутах меньше, чем время окончания и сейчас. Время окончания будет интерпретироваться как сегодня, если значения времени начала и окончания ЧЧ: ММ больше, чем «сейчас».

Предполагая, что «сейчас» - это «11 января 19:00», вот как будут интерпретироваться различные времена начала и окончания примера ( -lза исключением случаев, указанных выше):

начальный конец диапазона
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 января # -l
00:00 18:59 10 января 11 января # -l
18:59 19:01 10 января 11 января # -l

Почти весь сценарий настроен. Последние две строки делают всю работу.

Предупреждение: проверка аргументов или проверка ошибок не производится. Крайние случаи не были полностью проверены. Это было написано с использованием gawkдругих версий AWK возможно.

#!/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% времени было потрачено в strptime, поэтому мы просто изменили формат журнала, включив также числовую метку времени 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
Джеффри Девлу
источник