Skip to content

Commit

Permalink
add: t.p.logger.SyslogObserver
Browse files Browse the repository at this point in the history
  • Loading branch information
ioggstream committed Aug 18, 2014
1 parent 370dabf commit 991156e
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 0 deletions.
7 changes: 7 additions & 0 deletions twisted/python/logger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ def oops(self, data):
# From ._json
"eventAsJSON", "eventFromJSON",
"jsonFileLogObserver", "eventsFromJSONLogFile",

# From ._syslog
"SyslogObserver", "textSyslogObserver"
]

from ._levels import InvalidLogLevelError, LogLevel
Expand Down Expand Up @@ -123,3 +126,7 @@ def oops(self, data):
eventAsJSON, eventFromJSON,
jsonFileLogObserver, eventsFromJSONLogFile
)

from ._syslog import (
SyslogObserver, textSyslogObserver
)
162 changes: 162 additions & 0 deletions twisted/python/logger/_syslog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# -*- test-case-name: twisted.python.logger.test.test_file -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
#
# Author: Roberto Polli <roberto.polli@babel.it>

"""
File log observer.
"""

from zope.interface import implementer
import syslog as stdsyslog

from ._observer import ILogObserver
from ._format import formatTime
from ._format import timeFormatRFC3339
from ._format import formatEventAsClassicLogText
from ._levels import LogLevel

# These defaults come from the Python syslog docs.
DEFAULT_OPTIONS = 0
DEFAULT_FACILITY = stdsyslog.LOG_USER

# Mappings to Python's syslog module
toSyslogLevelMapping = {
LogLevel.debug: stdsyslog.LOG_DEBUG,
LogLevel.info: stdsyslog.LOG_INFO,
LogLevel.warn: stdsyslog.LOG_WARNING,
LogLevel.error: stdsyslog.LOG_ERR,
LogLevel.critical: stdsyslog.LOG_ALERT,
}
fromSyslogLevelMapping = dict([
(value, key) for (key, value)
in toSyslogLevelMapping.items()
])

@implementer(ILogObserver)
class SyslogObserver(object):
"""
A log observer for logging to syslog.
See L{twisted.python.log} for context.
This logObserver will automatically use LOG_ALERT priority for logged
failures (such as from C{log.err()}), but you can use any priority and
facility by setting the 'C{syslogPriority}' and 'C{syslogFacility}' keys in
the event dict.
"""
openlog = stdsyslog.openlog

def __init__(self, formatEvent, prefix, options=DEFAULT_OPTIONS,
facility=DEFAULT_FACILITY, syslog=stdsyslog.syslog):
"""
@type prefix: C{str}
@param prefix: The syslog prefix to use.
@type options: C{int}
@param options: A bitvector represented as an integer of the syslog
options to use.
@type facility: C{int}
@param facility: An indication to the syslog daemon of what sort of
program this is (essentially, an additional arbitrary metadata
classification for messages sent to syslog by this observer).
@param formatEvent: A callable that formats an event.
@type formatEvent: L{callable} that takes an C{event} argument and
returns a formatted event as L{unicode}.
"""
self.openlog(prefix, options, facility)

if False:
self._encoding = "utf-8"
else:
self._encoding = None

self.formatEvent = formatEvent
self.syslog = stdsyslog.syslog

def __call__(self, event):
"""
Write event to file.
@param event: An event.
@type event: L{dict}
"""
text = self.formatEvent(event)

if text is None:
text = u""

# Set priority by loglevel and eventually
# override it if log_failure or syslogPriority is defined
try:
priority = toSyslogLevelMapping[event['log_level']]
except KeyError:
priority = stdsyslog.LOG_INFO

if "log_failure" in event:
text = u"\n".join((text, event["log_failure"].getTraceback()))
priority = stdsyslog.LOG_ALERT
elif 'syslogPriority' in event:
priority = int(event['syslogPriority'])


if self._encoding is not None:
text = text.encode(self._encoding)

if text:
facility = 0
if 'syslogFacility' in event:
facility = int(event['syslogFacility'])
# write multi-line text
lines = text.split('\n')
while lines[-1:] == ['']:
lines.pop()

firstLine = True
for line in lines:
if firstLine:
firstLine = False
else:
# indent further lines
line = '\t' + line

self.syslog(priority | facility, text)



def textSyslogObserver(prefix="Twisted", options=DEFAULT_OPTIONS,
facility=DEFAULT_FACILITY, timeFormat=timeFormatRFC3339):
"""
Create a L{SyslogObserver} that emits text to a specified syslog object.
@type prefix: C{str}
@param prefix: The syslog prefix to use.
@type options: C{int}
@param options: A bitvector represented as an integer of the syslog
options to use.
@type facility: C{int}
@param facility: An indication to the syslog daemon of what sort of
program this is (essentially, an additional arbitrary metadata
classification for messages sent to syslog by this observer).
@param timeFormat: The format to use when adding timestamp prefixes to
logged events. If C{None}, or for events with no C{"log_timestamp"}
key, the default timestamp prefix of C{u"-"} is used.
@type timeFormat: L{unicode} or C{None}
@return: A file log observer.
@rtype: L{SyslogObserver}
"""
def formatEvent(event):
return formatEventAsClassicLogText(
event, formatTime=lambda e: formatTime(e, timeFormat)
)

return SyslogObserver(formatEvent, prefix, options=DEFAULT_OPTIONS,
facility=DEFAULT_FACILITY)
183 changes: 183 additions & 0 deletions twisted/python/logger/test/test_syslog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
#
# Author: Roberto Polli <roberto.polli@babel.it>

"""
Test cases for L{twisted.python.logger._file}.
"""

from zope.interface.verify import verifyObject, BrokenMethodImplementation

from twisted.trial.unittest import TestCase

from twisted.python.failure import Failure
from twisted.python.compat import unicode
from .._observer import ILogObserver
from .._syslog import SyslogObserver
from .._syslog import textSyslogObserver

def mock_syslog(list_buffer):
"""
A mock function to collect syslog() invocations into
a given list_buffer.
"""
def tmp(*args, **kwds):
list_buffer.append((args, kwds))
return tmp

class SyslogObserverTests(TestCase):
"""
Tests for L{SyslogObserver}.
"""

def test_interface(self):
"""
L{SyslogObserver} is an L{ILogObserver}.
"""
observer = SyslogObserver(lambda e: unicode(e), prefix="test_syslog")
try:
verifyObject(ILogObserver, observer)
except BrokenMethodImplementation as e:
self.fail(e)



def test_observeWrites(self):
"""
L{SyslogObserver} writes to the given file when it observes events.
"""
result = []
observer = SyslogObserver(lambda e: unicode(e), prefix="test_syslog")
observer.syslog = mock_syslog(result)
event = dict(x=1)
observer(event)
#unpack the syslog event
(syslog_flags, syslog_text), _ = result[0]
self.assertEquals(syslog_text, unicode(event))



def _test_observeWrites(self, what, count):
"""
Verify that observer performs an expected number of writes when the
formatter returns a given value.
@param what: the value for the formatter to return.
@type what: L{unicode}
@param count: the expected number of writes.
@type count: L{int}
"""
try:
fileHandle = DummyFile()
observer = SyslogObserver(lambda e: what, prefix="test_syslog")
event = dict(x=1)
observer(event)
self.assertEquals(fileHandle.writes, count)

finally:
fileHandle.close()


def test_observeWritesNone(self):
"""
L{SyslogObserver} does not write to the given file when it observes
events and C{formatEvent} returns C{None}.
"""
self._test_observeWrites(None, 0)


def test_observeWritesEmpty(self):
"""
L{SyslogObserver} does not write to the given file when it observes
events and C{formatEvent} returns C{u""}.
"""
self._test_observeWrites(u"", 0)


def test_observeFailure(self):
"""
If the C{"log_failure"} key exists in an event, the observer should
append the failure's traceback to the output.
"""

result = []
observer = SyslogObserver(lambda e: unicode(e), prefix="test_syslog")
observer.syslog = mock_syslog(result)
try:
1 / 0
except ZeroDivisionError:
failure = Failure()

event = dict(log_failure=failure)
observer(event)
(syslog_flags, output), _ = result[0]
self.assertTrue(
output.startswith("{0}\nTraceback ".format(unicode(event))),
"Incorrect output:\n{0}".format(output)
)





class TextSyslogObserverTests(TestCase):
"""
Tests for L{textSyslogObserver}.
"""

def test_returnsSyslogObserver(self):
"""
L{textSyslogObserver} returns a L{SyslogObserver}.
"""
observer = textSyslogObserver(prefix="Twisted")
self.assertIsInstance(observer, SyslogObserver)



def test_timeFormat(self):
"""
Returned L{SyslogObserver} has the correct text message.
"""
result = []
observer = textSyslogObserver(prefix="test_syslog", timeFormat=u"%f")
observer.syslog = mock_syslog(result)
observer(dict(log_format=u"XYZZY", log_time=1.23456))
(syslog_flags, syslog_text), _ = result[0]
self.assertEquals(syslog_text, u"234560 [-#-] XYZZY\n")



class DummyFile(object):
"""
File that counts writes and flushes.
"""

def __init__(self):
self.writes = 0
self.flushes = 0


def write(self, data):
"""
Write data.
@param data: data
@type data: L{unicode} or L{bytes}
"""
self.writes += 1


def flush(self):
"""
Flush buffers.
"""
self.flushes += 1


def close(self):
"""
Close.
"""
pass

0 comments on commit 991156e

Please sign in to comment.