Pythonで書かれたsyslogdをWindowsで動かしてみる

ひさしぶりに技術系のネタを。

LANに、あるネットワーク機器が接続されていて、そいつが出力するログを手元のパソコンに保存して読みたい。色々調べてみたところ、その機器にはログをsyslogで他のマシンに送信する機能がついていた。

てことは、手元のWindowsパソコンにsyslogdをインストールして、他のマシンから送られてくるログを受信できるようにしてやればいいはずだ。ということで色々検索してみたところ、Windows用のsyslogdをいくつか見つけることができた。

がしかし、パソコンにはなるべくソフトを入れないようにしたい。とくにバイナリ(exe形式)のソフトは嫌だなーと思いつつ、さらに色々調べてみたところ、Pythonで書かれたsyslogdを発見した。

syslogdといっても、/dev/logを読む機能はないみたい。まさに俺の用途に最適っぽい。ということで試してみた。


このスクリプトは、print と sys.stdout.write でログを画面に表示する試験的なもの。そこで loggerモジュールを使ってファイルに保存するように変更してみた。

#
# Original: http://www.drbeat.li/pycgi/webutil.py/html?py=py/syslogd.py.txt
# (Slightly modified)
# Usage: python syslogd.py c:/log.txt
#

"""A minimalistic syslogd.

2000-06-09/bb:  created
2000-06-12/bb:  added constants from syslog.h,
                config options for time format, stop string
"""

import sys, string, time, socket, re
from SocketServer import *

import logging, logging.handlers

# constants from syslog.h

LOG_EMERG       = 0
LOG_ALERT       = 1
LOG_CRIT        = 2
LOG_ERR         = 3
LOG_WARNING     = 4
LOG_NOTICE      = 5
LOG_INFO        = 6
LOG_DEBUG       = 7

LOG_PRIMASK     = 0x07

def LOG_PRI(p): return p & LOG_PRIMASK
def LOG_MAKEPRI(fac, pri): return fac << 3 | pri

LOG_KERN        = 0 << 3
LOG_USER        = 1 << 3
LOG_MAIL        = 2 << 3
LOG_DAEMON      = 3 << 3
LOG_AUTH        = 4 << 3
LOG_SYSLOG      = 5 << 3
LOG_LPR         = 6 << 3
LOG_NEWS        = 7 << 3
LOG_UUCP        = 8 << 3
LOG_CRON        = 9 << 3
LOG_AUTHPRIV    = 10 << 3
LOG_FTP         = 11 << 3
LOG_LOCAL0      = 16 << 3
LOG_LOCAL1      = 17 << 3
LOG_LOCAL2      = 18 << 3
LOG_LOCAL3      = 19 << 3
LOG_LOCAL4      = 20 << 3
LOG_LOCAL5      = 21 << 3
LOG_LOCAL6      = 22 << 3
LOG_LOCAL7      = 23 << 3

LOG_NFACILITIES = 24
LOG_FACMASK     = 0x03F8
def LOG_FAC(p): return (p & LOG_FACMASK) >> 3

def LOG_MASK(pri): return 1 << pri
def LOG_UPTO(pri): return (1 << pri + 1) - 1
# end syslog.h


def LOG_UNPACK(p): return (p & LOG_FACMASK, LOG_PRI(p))

fac_values = {}     # mapping of facility constants to their values
fac_names = {}      # mapping of values to names
pri_values = {}
pri_names = {}
for i, j in globals().items():
    if i[:4] == 'LOG_' and type(j) == type(0):
        if j > LOG_PRIMASK or i == 'LOG_KERN':
            n, v = fac_names, fac_values
        else:
            n, v = pri_names, pri_values
        i = i[4:].lower()
        v[i] = j
        n[j] = i
del i, j, n, v


conf_pat = re.compile(r'''
    ^\s*            # leading space        
    ([\w*]+)        # facility
    \.([=!]?)       # separator plus optional modifier
    ([\w*]+)        # priority
    \s+
    ([|@]?)         # target type fifo/remote host
    (\S+)           # target
    ''', re.VERBOSE)


class InterruptibleServer:
    "Mix-in class for {TCP,UDP}Server that allows to stop the service"

    def serve(self):
        "Run the service until it is stopped with the stop() method."
        self.running = 1
        while self.running:
            try:
                self.handle_request()
            except KeyboardInterrupt:
                self.stop()

    def stop(self):             # called by handle_request()
        "Stop the service."
        self.running = 0


SYSLOG_PORT = socket.getservbyname('syslog', 'udp')

class Syslogd(ThreadingUDPServer, InterruptibleServer):

    def __init__(self, addr='', port=SYSLOG_PORT, pri=LOG_DEBUG, 
timefmt=None, magic=None, logger=None):

        UDPServer.__init__(self, (addr, port), None)

        self.hostmap = {}       # client address/name mapping
        self.priority = pri
        self.timefmt = timefmt or '%b %d %H:%M:%S'
        self.stop_magic = magic or '_stop'

        #print 'syslogd started'
        logger.info('syslogd started')

    def finish_request(self, (msg, sock), client_address):

        # get the time when the message arrives

        tm = time.time()

        # Messages are in the form "[[float]]<int>message text...\n"

        # The [[float]] and <int> parts and the newline are optional.

        # The float denotes the original event time in case the

        # event comes through a gateway from a client that is not

        # syslog-compatible (Windows NT comes to mind :-).


        # extract original event time, if present

        if msg[:2] == '[[':
            pos = msg.find(']]')
            try:
                tm = float(msg[2:pos])
            except:
                pass
            msg = msg[pos+2:]   # use rest of message

        # extract log facility and priority, if present

        if msg[:1] == '<':
            pos = msg.find('>')
            fac, pri = LOG_UNPACK(int(msg[1:pos]))
            msg = msg[pos+1:]
        elif msg and msg[0] < ' ':
            fac, pri = LOG_KERN, ord(msg[0])
            msg = msg[1:]
        else:
            fac, pri = None, None

        # check if we can discard this message

        if pri is not None and pri > self.priority:
            return

        # build the client address/name mapping

        client = client_address[0]
        try:
            host = self.hostmap[client]
        except KeyError:
            try:
                host = socket.gethostbyaddr(client)[0]
                host = host.split('.')[0]       # keep only the host name
            except socket.error:
                host = client
            self.hostmap[client] = host

        # print the message

        tm = time.strftime(self.timefmt, time.localtime(tm))
        if 0: # fac is not None and pri is not None:
            fp = ' <%s,%s>' % (fac_names[fac], pri_names[pri])
        else:
            fp = ''
        #sys.stdout.write(tm + ' ' + host + fp + ': ' + msg.strip() + '\n')
        logger.info(tm + ' ' + host + fp + ': ' + msg.strip())

        # stop the server if the magic stop string appears in the message

        if msg.find(self.stop_magic) >= 0:
            # print 'stopped.'
            logger.info('stopped.')
            self.stop()

def initLogger():

    filename = None

    if len(sys.argv) >= 2:
        if sys.argv[1][-4:] == '.txt' or sys.argv[1][-4:] == '.log':
            filename = sys.argv[1]

    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    if filename:
        handler = logging.handlers.RotatingFileHandler(filename=filename, 
maxBytes=200000, backupCount=10)
    else:
        handler = logging.StreamHandler()

    logger.addHandler(handler)
    return logger

if __name__ == '__main__':
    logger = initLogger()
    Syslogd(timefmt='%H:%M:%S', logger=logger).serve()

initLogger関数を付け加えた。 以下のように実行する。

python syslogd.py log.txt

……といった感じで実行すると、ログがlog.txtに保存される。RotatingFileHanderのところのmaxBytesがログファイルの最大サイズ。それを超えると、backupCountで指定した数だけバックアップされる。(log.txt.1, log.txt.2…… log.txt.n のように)

止めるときは、以下のようなスクリプトを使う。

import logging 
import logging.handlers

log = logging.getLogger()

syslog = logging.handlers.SysLogHandler(('localhost', 514))

log.addHandler(syslog)

log.warning('_stop')

もちろん、こんなんじゃなくて、Linuxなどでloggerコマンドを使ってもよいはず。localhostから送信すると、うまく受信できないことがあるみたい。(二回実行しないと止まらなかったりする) なんでだろう。

ログを受信するのが楽しくなって (手段が目的になってしまった) 手元の iBook G4のログも飛ばしてみたくなった。 /etc/syslog.conf を書き換えて、

sudo launchctl stop com.apple.syslogd

とする。色々検索してみると、 stopしたあとにstartせよ、と書いてあるところと、startする必要はない、と書いてあるところがある。

launchctl で stop すると com.apple.syslogd は、常駐する設定になっているため、自動で再起動される。

maruko2 Note.

手元のiBook G4 (Tiger)では、stop後にstartしなくてもps axするとsyslogdがでてくるので大丈夫っぽい感じ。

17:49:55 192.168.*.*: Apr 13 17:49:55 ibookg4 mDNSResponder: -1: DNSServiceRegister("iTunes_Ctrl_70C3************", "_dacp._tcp.", "local.", 3689) failed: Client id -1 invalid (-65549)

こんなのがでてくるけどなんだろう。意味不明。(猫に小判だ)

で、ログといえば tail -f なんだけど、それもPythonで書いたやつをつかっている。

以下のスクリプトは、 これ→「ASPN : Python Cookbook : tail -f in Python」をコピって、ちょっと書き加えたもの。

import sys
import os.path
import time
from msvcrt import *


if len(sys.argv) < 2:
    print "ERROR: No file specified"
    sys.exit()

filename = sys.argv[1]

if not os.path.exists(filename):
    print "ERROR: File not found: " + filename
    sys.exit()

if not os.path.isfile(filename):
    print "ERROR: This is not valid file: " + filename
    sys.exit()

f = file(filename, 'r');

try:
    while 1:
        where = f.tell()
        line = f.readline()
        if not line:
            time.sleep(1)
            f.seek(where)
        else:
            print line, # already has newline

        while kbhit():
            print getch()

except KeyboardInterrupt:
    print "Done."
    sys.exit()

快適、快適。