April 12, 2007
syslog2IRC
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
syslog2IRC
==========
Receive syslog messages via UDP and show them on IRC.
Requires the `python-irclib`_ package.
Setup your ``syslog.conf``::
*.* @host-to-send-log-messages-to-and-this-script-runs-on
For more information, see `RFC 3164`, "The BSD syslog Protocol".
:Copyright: 2007 Jochen Kupperschmidt
:Date: 12-Apr-2007
:License: GNU General Public License, Version 2
.. _python-irclib: http://python-irclib.sourceforge.net/
.. _RFC 3164: http://tools.ietf.org/html/rfc3164
"""
from Queue import Queue
from SocketServer import BaseRequestHandler, ThreadingUDPServer
from threading import Thread
from time import sleep, strftime, strptime
from ircbot import SingleServerIRCBot
from irclib import nm_to_n
# ---------------------------------------------------------------- #
# syslog stuff
class SyslogMessage(object):
"""A syslog message."""
FACILITIES = {
0: 'kernel messages',
1: 'user-level messages',
2: 'mail system',
3: 'system daemons',
4: 'security/authorization messages',
5: 'messages generated internally by syslogd',
6: 'line printer subsystem',
7: 'network news subsystem',
8: 'UUCP subsystem',
9: 'clock daemon',
10: 'security/authorization messages',
11: 'FTP daemon',
12: 'NTP subsystem',
13: 'log audit',
14: 'log alert',
15: 'clock daemon',
16: 'local use 0 (local0)',
17: 'local use 1 (local1)',
18: 'local use 2 (local2)',
19: 'local use 3 (local3)',
20: 'local use 4 (local4)',
21: 'local use 5 (local5)',
22: 'local use 6 (local6)',
23: 'local use 7 (local7)',
}
SEVERITIES = {
0: 'Emergency',
1: 'Alert',
2: 'Critical',
3: 'Error',
4: 'Warning',
5: 'Notice',
6: 'Informational',
7: 'Debug',
}
def __init__(self, data):
# 1024 bytes max says RFC.
self.payload = data[:1024]
self.parse_priority()
self.parse_header()
def parse_priority(self):
"""Extract and resolve priority."""
prio, self.payload = self.payload.split('>', 1)
self.priority_id = int(prio[1:])
self.facility_id, self.severity_id = divmod(self.priority_id, 8)
self.facility = self.FACILITIES[self.facility_id]
self.severity = self.SEVERITIES[self.severity_id]
def parse_header(self):
"""Try to extract a RFC-compliant TIMESTAMP/HOSTNAME header."""
try:
self.timestamp = strptime(self.payload[:15], '%b %d %H:%M:%S')
except ValueError:
# Header is not RFC-compliant.
self.timestamp, self.hostname = None, None
else:
self.hostname, self.payload = self.payload[16:].split(' ', 1)
def __str__(self):
s = ''
if self.timestamp is not None:
s += '[%s] ' % strftime('%Y-%m-%d %H:%M:%S', self.timestamp)
if self.hostname is not None:
s += '(%s) ' % self.hostname
s += '[%s]: %s' % (self.severity, self.payload)
return s
class SyslogRequestHandler(BaseRequestHandler):
"""Handler for syslog messages."""
def handle(self):
try:
msg = SyslogMessage(self.request[0].strip())
except ValueError:
msg = 'Invalid message.'
else:
self.server.queue.put((self.client_address, msg))
print ('%s:%d' % self.client_address), str(msg)
class SyslogReceiveServer(ThreadingUDPServer):
"""UDP server that waits for syslog messages."""
def __init__(self):
ThreadingUDPServer.__init__(self, ('', 514), SyslogRequestHandler)
self.queue = Queue()
# ---------------------------------------------------------------- #
# IRC bot stuff
class SyslogBot(SingleServerIRCBot):
def __init__(self, server_list, channel_list, nickname='Syslog',
realname='syslog'):
SingleServerIRCBot.__init__(self, server_list, nickname, realname)
self.channel_list = channel_list
def on_welcome(self, conn, event):
"""Join channels after connect."""
print 'Connected to %s:%d.' % conn.socket.getsockname()
for channel, key in self.channel_list:
conn.join(channel, key)
def on_nicknameinuse(self, conn, event):
"""Choose another nickname if conflicting."""
self._nickname += '_'
conn.nick(self._nickname)
def on_ctcp(self, conn, event):
"""Answer CTCP PING and VERSION queries."""
whonick = nm_to_n(event.source())
message = event.arguments()[0].lower()
if message == 'version':
conn.notice(whonick, 'Syslog2IRC')
elif message == 'ping':
conn.pong(whonick)
def on_privmsg(self, conn, event):
"""React on private messages.
Die, for example.
"""
whonick = nm_to_n(event.source())
message = event.arguments()[0]
if message == 'die!':
print 'Shutting down as requested by %s...' % whonick
self.die('Shutting down.')
def say(self, msg):
"""Say message to channels."""
for channel, key in self.channel_list:
self.connection.privmsg(channel, msg)
# ---------------------------------------------------------------- #
def process_queue(announce_callback, queue, delay=2):
"""Process received messages in queue."""
while True:
sleep(delay)
try:
addr, msg = queue.get()
except Empty:
continue
announce_callback('%s:%d ' % addr + str(msg))
if __name__ == '__main__':
# Set IRC connection parameters.
irc_active = True
irc_servers = [('irc.example.com', 6667)]
irc_channels = [('#examplechannel', 'secret')]
if irc_active:
# Prepare and start IRC bot.
bot = SyslogBot(irc_servers, irc_channels)
Thread(target=bot.start).start()
announce = bot.say
else:
# Just display messages locally.
def announce(s):
print s
# Prepare and start syslog message receiver.
receiver = SyslogReceiveServer()
Thread(target=receiver.serve_forever).start()
process_queue(announce, receiver.queue)