Skip to content

Commit

Permalink
initial import; it is NOT tested; sub-watcher is not yet ready to sub…
Browse files Browse the repository at this point in the history
…scribe.
  • Loading branch information
rbucker committed Aug 16, 2011
1 parent 2950e2c commit 95ff362
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 0 deletions.
20 changes: 20 additions & 0 deletions LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2011 Richard Bucker <richard@bucker.net>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

2 changes: 2 additions & 0 deletions MANIFEST.in
@@ -0,0 +1,2 @@
include README.rst
include LICENSE
Empty file removed README
Empty file.
82 changes: 82 additions & 0 deletions README.rst
@@ -0,0 +1,82 @@
=====================================================
ZeroMQLog - A ZeroMQ Pub/Sub Logging Handler for Python
=====================================================

A logging handler for Python that publishes log messages using zeromq's
pub/sub system. You can use this to read or respond to streaming log
data in real time. If you are using sub-watcher then the messages can be
redirected back to redis or syslog as needed with all sorts of filter goodness.

This project and it's layout and details were inspired by Jed Parsons and
python-redis-log (currently hosted on GitHub).

Installation
------------

The current stable release ::

pip install python-zeromq-log

or ::

easy_install python-zeromq-log

The latest from github_ ::

git clone git://github.com/rbucker881/python-zeromq-log.git
cd python-zeromq-log
python setup.py build
python setup.py install --prefix=$HOME # for example

.. _github: https://github.com/rbucker881/python-zeromq-log

Requirements
------------

- redis_
- The `Python redis client`_ by Andy McCurdy
- simplejson_

.. _redis: http://redis.io/
.. _Python redis client: https://github.com/andymccurdy/redis-py
.. _simplejson: https://github.com/simplejson/simplejson

Usage
-----

::

>>> from zeromqlog import handlers, logger
>>> l = logger.ZeroMQLogger('my.logger')
>>> l.addHandler(handlers.ZeroMQHandler.to("my:channel"))
>>> l.info("I like chocolate cake")
>>> l.error("Belts?", exc_info=True)

ZeroMQ (rbucker881/sub-watcher) clients subscribed to ``my:channel`` will get a json log record like the
following (sent from function ``foo()`` in file ``test.py``: ::

{ username: 'richard',
args: [],
name: 'my.logger',
level: 'info',
line_no: 6,
traceback: null,
filename: 'qa.py',
time: '2011-06-02T14:50:08.237052',
msg: 'losing',
funcname: 'bar',
hostname: 'frosty.local' }

If an exception is raised, and ``exc_info`` is ``True``, the log will include
a formatted traceback in ``traceback``.

The date is stored as an ISO 8601 string in GMT.


Contributors
------------

Just in case you missed this at the top of the readme.

This project and it's layout and details were inspired by Jed Parsons and
python-redis-log (currently hosted on GitHub).
21 changes: 21 additions & 0 deletions setup.py
@@ -0,0 +1,21 @@
try:
from setuptools import setup
except ImportError:
from distutils.core import setup

from os import path

README = path.abspath(path.join(path.dirname(__file__), 'README.rst'))

setup(
name='python-zeromq-log',
version='0.0.1',
description='ZeroMQ pub/sub logging handler for python',
long_description=open(README).read(),
author='Richard Bucker',
author_email='richard@bucker.net',
url='https://github.com/rbucker881/python-zeromq-log',
packages=['zeromqlog'],
license='MIT',
install_requires=['pyzmq','simplejson']
)
24 changes: 24 additions & 0 deletions zeromqlog/__init__.py
@@ -0,0 +1,24 @@
"""
zeromqlog - a zeromq logging handler for python
>>> from zeromqlog import handlers, logger
>>> l = logger.ZeroMQLogger('my.logger')
>>> l.addHandler(handlers.ZeroMQHandler.to("my:channel"))
>>> l.info("I like breakfast cereal!")
>>> l.error("Oh snap crackle pop", exc_info=True)
ZeroMQ clients subscribed to my:channel will get a json log record.
depends on : https://github.com/zeromq/pyzmq.git
On errors, if exc_info is True, a printed traceback will be included.
"""

__author__ = 'Richard Bucker <richard@bucker.net>'
__version__ = (0, 0, 1)

import logging
import logger

logging.setLoggerClass(logger.ZeroMQLogger)


94 changes: 94 additions & 0 deletions zeromqlog/handlers.py
@@ -0,0 +1,94 @@
"""copied almost byte for byte from jedp/python-redis-log
There was no copyright in the original. Therefore, none here.
"""
import logging
import zmq
import simplejson as json

class ZeroMQFormatter(logging.Formatter):
def format(self, record):
"""
JSON-encode a record for serializing through redis.
Convert date to iso format, and stringify any exceptions.
"""
data = record._raw.copy()

# serialize the datetime date as utc string
data['time'] = data['time'].isoformat()

# stringify exception data
if data.get('traceback'):
data['traceback'] = self.formatException(data['traceback'])

return json.dumps(data)

class ZeroMQHandler(logging.Handler):
"""
Publish messages to a zmq channel.
As a convenience, the classmethod to() can be used as a
constructor, just as in Andrei Savu's mongodb-log handler.
"""

@classmethod
def to(cklass, channel, url='localhost', port=5555, level=logging.NOTSET):
context = zmq.Context()
publisher = context.socket (zmq.PUB)
return cklass(channel, publisher.bind(url+':'+str(port)), level=level)

def __init__(self, channel, zmq_client, level=logging.NOTSET):
"""
Create a new logger for the given channel and zmq_client.
"""
logging.Handler.__init__(self, level)
self.channel = channel
self.zmq_client = zmq_client
self.formatter = ZeroMQFormatter()

def emit(self, record):
"""
Publish record to redis logging channel
"""
try :
msg = zmq.log.handlers.TOPIC_DELIM.join([self.channel,self.format(record)])
self.zmq_client.send(msg)
except zmq.core.error.ZMQError:
pass

class ZeroMQListHandler(logging.Handler):
"""
Publish messages to redis a redis list.
As a convenience, the classmethod to() can be used as a
constructor, just as in Andrei Savu's mongodb-log handler.
If max_messages is set, trim the list to this many items.
"""

@classmethod
def to(cklass, key, max_messages=None, url='localhost', port=5555, level=logging.NOTSET):
context = zmq.Context()
publisher = context.socket (zmq.PUB)
return cklass(key, max_messages, publisher.bind(url+':'+str(port)), level=level)

def __init__(self, key, max_messages, zmq_client, level=logging.NOTSET):
"""
Create a new logger for the given key and redis_client.
"""
logging.Handler.__init__(self, level)
self.key = key
self.zmq_client = zmq_client
self.formatter = ZeroMQFormatter()
self.max_messages = max_messages

def emit(self, record):
"""
Publish record to redis logging list
"""
try :
self.zmq_client.send(record)
except zmq.core.error.ZMQError:
pass

# __END__
97 changes: 97 additions & 0 deletions zeromqlog/logger.py
@@ -0,0 +1,97 @@
"""copied almost byte for byte from jedp/python-redis-log
There was no copyright in the original. Therefore, none here.
"""
import zmq
import socket
import getpass
import datetime
import inspect
import logging

def levelAsString(level):
return {logging.DEBUG: 'debug',
logging.INFO: 'info',
logging.WARNING: 'warning',
logging.ERROR: 'error',
logging.CRITICAL: 'critical',
logging.FATAL: 'fatal'}.get(level, 'unknown')

def _getCallingContext():
"""
Utility function for the ZeroMQLogRecord.
Returns the module, function, and lineno of the function
that called the logger.
We look way up in the stack. The stack at this point is:
[0] logger.py _getCallingContext (hey, that's me!)
[1] logger.py __init__
[2] logger.py makeRecord
[3] _log
[4] <logging method>
[5] caller of logging method
"""
frames = inspect.stack()

if len(frames) > 4:
context = frames[5]
else:
context = frames[0]

modname = context[1]
lineno = context[2]

if context[3]:
funcname = context[3]
else:
funcname = ""

# python docs say you don't want references to
# frames lying around. Bad things can happen.
del context
del frames

return modname, funcname, lineno


class ZeroMQLogRecord(logging.LogRecord):
def __init__(self, name, lvl, fn, lno, msg, args, exc_info, func=None, extra=None):
logging.LogRecord.__init__(self, name, lvl, fn, lno, msg, args, exc_info, func)

# You can also access the following instance variables via the
# formatter as
# %(hostname)s
# %(username)s
# %(modname)s
# etc.
self.hostname = socket.gethostname()
self.username = getpass.getuser()
self.modname, self.funcname, self.lineno = _getCallingContext()

self._raw = {
'name': name,
'level': levelAsString(lvl),
'filename': fn,
'line_no': self.lineno,
'msg': str(msg),
'args': list(args),
'time': datetime.datetime.utcnow(),
'username': self.username,
'funcname': self.funcname,
'hostname': self.hostname,
'traceback': exc_info
}

class ZeroMQLogger(logging.getLoggerClass()):
def makeRecord(self, name, lvl, fn, lno, msg, args, exc_info, func=None, extra=None):
record = ZeroMQLogRecord(name, lvl, fn, lno, msg, args, exc_info, func=None)

if extra:
for key in extra:
if (key in ["message", "asctime"]) or (key in record.__dict__):
raise KeyError("Attempt to overwrite %r in ZeroMQLogRecord" % key)
record.__dict__[key] = extra[key]
return record


# __END__

0 comments on commit 95ff362

Please sign in to comment.