Ekstraksi cepat rentang waktu dari syslog logfile?


12

Saya punya logfile dalam format syslog standar. Ini terlihat seperti ini, kecuali dengan ratusan baris per detik:

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

Itu tidak berputar tepat tengah malam, tetapi tidak akan pernah memiliki lebih dari dua hari di dalamnya.

Saya sering harus mengekstrak kutu waktu dari file ini. Saya ingin menulis skrip tujuan umum untuk ini, yang dapat saya sebut seperti:

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

... dan suruh untuk menarik garis dari 22:30, seterusnya melintasi batas tengah malam, sampai 02:00 hari berikutnya.

Ada beberapa peringatan:

  • Saya tidak ingin repot mengetik tanggal pada baris perintah, hanya kali. Program harus cukup pintar untuk mengetahuinya.
  • Format tanggal log tidak termasuk tahun, jadi itu harus menebak berdasarkan tahun ini, tetapi tetap melakukan hal yang benar di sekitar Hari Tahun Baru.
  • Saya ingin itu cepat - harus menggunakan fakta bahwa garis-garis untuk mencari di dalam file dan menggunakan pencarian biner.

Sebelum saya menghabiskan banyak waktu menulis ini, apakah sudah ada?

Jawaban:


9

Pembaruan: Saya telah mengganti kode asli dengan versi yang diperbarui dengan banyak perbaikan. Sebut ini kualitas alpha (aktual?).

Versi ini termasuk:

  • penanganan opsi baris perintah
  • validasi format tanggal baris perintah
  • beberapa tryblok
  • membaca garis pindah ke fungsi

Teks asli:

Nah, apa yang kamu tahu? "Carilah" dan kamu akan menemukan! Berikut adalah program Python yang mencari-cari di dalam file dan menggunakan pencarian biner yang kurang lebih. Ini jauh lebih cepat daripada naskah AWK yang ditulis orang lain .

Ini (pra?) Berkualitas alpha. Itu harus memiliki tryblok dan validasi input dan banyak pengujian dan tidak diragukan lagi bisa lebih Pythonic. Tapi ini untuk hiburanmu. Oh, dan ini ditulis untuk Python 2.6.

Kode baru:

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

Wow. Saya benar-benar perlu belajar Python ...
Stefan Lasiewski

@ Dennis Williamson: Saya melihat baris yang berisi if debug > 0: print("File: '{0}' Size: {1} Today: '{2}' Now: {3} Start: '{4}' End: '{5}'".format(file, size, today, now, searchstar$. Apakah searchstarseharusnya diakhiri dengan $, atau apakah itu salah cetak? Saya mendapatkan kesalahan sintaks pada baris ini (Baris 159)
Stefan Lasiewski

@Stefan saya akan menggantinya dengan )).
Bill Weiss

@Stefan: Terima kasih. Itu kesalahan ketik yang saya perbaiki. Untuk referensi cepat, $alih-alih seharusnya t, searchend))begitu katanya... searchstart, searchend))
Berhenti sebentar sampai pemberitahuan lebih lanjut.

@Stefan: Maaf soal itu. Saya pikir itu sudah benar.
Dijeda sampai pemberitahuan lebih lanjut.

0

Dari pencarian cepat di internet, ada hal-hal yang diekstrak berdasarkan kata kunci (seperti KEBAKARAN atau semacamnya :) tetapi tidak ada yang mengekstrak rentang tanggal dari file.

Tampaknya tidak sulit untuk melakukan apa yang Anda usulkan:

  1. Cari waktu mulai.
  2. Cetak garis itu.
  3. Jika waktu berakhir <waktu mulai, dan tanggal baris> berakhir dan <mulai, maka berhentilah.
  4. Jika waktu berakhirnya> waktu mulai, dan tanggal baris> akhir, hentikan.

Tampaknya lurus ke depan, dan saya bisa menulis untuk Anda jika Anda tidak keberatan Ruby :)


Saya tidak keberatan Ruby, tetapi # 1 tidak langsung jika Anda ingin melakukannya secara efisien dalam file besar - Anda perlu mencari () ke titik setengah, menemukan garis terdekat, melihat bagaimana itu dimulai, dan ulangi dengan titik tengah baru. Terlalu efisien untuk melihat setiap baris.
Mike

Anda mengatakan besar, tetapi tidak menentukan ukuran sebenarnya. Seberapa besar besar? Lebih buruk lagi, jika ada beberapa hari yang terlibat, akan sangat mudah untuk menemukan yang salah hanya menggunakan waktu. Lagi pula, jika Anda melewati batas hari, hari skrip berjalan akan selalu berbeda dari waktu mulai. Apakah file-file tersebut sesuai dengan memori melalui mmap ()?
Michael Graff

Sekitar 30 GB, pada disk yang dipasang di jaringan.
Mike

0

Ini akan mencetak rentang entri antara waktu mulai dan waktu selesai berdasarkan bagaimana mereka berhubungan dengan waktu saat ini ("sekarang").

Pemakaian:

timegrep [-l] start end filename

Contoh:

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

Opsi -l(panjang) menyebabkan output terpanjang yang mungkin. Waktu mulai akan ditafsirkan sebagai kemarin jika nilai jam dan menit dari waktu mulai kurang dari waktu akhir dan sekarang. Waktu akhir akan ditafsirkan sebagai hari ini jika waktu mulai dan waktu selesai HH: Nilai MM lebih besar dari "sekarang".

Dengan asumsi bahwa "sekarang" adalah "11 Januari 19:00", ini adalah bagaimana berbagai contoh waktu mulai dan akhir akan ditafsirkan (tanpa -lkecuali sebagaimana disebutkan):

mulai ujung mulai mulai ujung rentang
19:01 23:59 10 Jan 10
19:01 00:00 10 Jan 11 Januari
00:00 18:59 11 Januari 11 Januari
18:59 18:58 10 Jan 10
19:01 23:59 10 Jan 11 # -l
00:00 18:59 10 Jan 11 # -l
18:59 19:01 10 Jan 11 # -l

Hampir semua skrip diatur. Dua baris terakhir melakukan semua pekerjaan.

Peringatan: tidak ada validasi argumen atau pengecekan kesalahan yang dilakukan. Kasing tepi belum diuji secara menyeluruh. Ini ditulis menggunakan gawkversi lain dari AWK mungkin berkotek.

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

Saya pikir AWK sangat efisien dalam mencari file. Saya tidak berpikir hal lain akan lebih cepat dalam mencari file teks yang tidak diindeks .


Sepertinya Anda mengabaikan poin saya yang ketiga. Log berada di urutan 30 GB - jika baris pertama file adalah 7:00 dan baris terakhir adalah 23:00, dan saya ingin irisan antara 22:00 dan 22:01, saya tidak ingin script melihat setiap baris antara 7: 00-22: 00. Saya ingin memperkirakan di mana itu akan, mencari ke titik itu, dan membuat perkiraan baru sampai menemukannya.
mike

Saya tidak mengabaikannya. Saya menyatakan pendapat saya di paragraf terakhir.
Dijeda sampai pemberitahuan lebih lanjut.

0

Program C ++ yang menerapkan pencarian biner - perlu beberapa modifikasi sederhana (mis. Memanggil strptime) untuk bekerja dengan tanggal teks.

http://gitorious.org/bs_grep/

Saya memiliki versi sebelumnya dengan dukungan untuk tanggal teks, namun masih terlalu lambat untuk skala file log kami; profiling mengatakan bahwa lebih dari 90% dari waktu dihabiskan dalam strptime, jadi, kami hanya memodifikasi format log untuk menyertakan cap waktu unix numerik juga.


0

Meskipun jawaban ini terlalu terlambat, mungkin bermanfaat bagi sebagian orang.

Saya telah mengubah kode dari @Dennis Williamson menjadi kelas Python yang dapat digunakan untuk hal-hal python lainnya.

Saya telah menambahkan dukungan untuk banyak dukungan tanggal.

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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.