mirror of https://github.com/zeromq/pyzmq.git
299 lines
9.4 KiB
Markdown
299 lines
9.4 KiB
Markdown
% PyZMQ logging doc, by Min Ragan-Kelley, 2011
|
|
|
|
(logging)=
|
|
|
|
# Asynchronous Logging via PyZMQ
|
|
|
|
```{seealso}
|
|
- The ØMQ guide [coverage](https://zguide.zeromq.org/docs/chapter5/) of PUB/SUB
|
|
messaging
|
|
- Python logging module [documentation](https://docs.python.org/3/library/logging.html)
|
|
```
|
|
|
|
Python provides extensible logging facilities through its {py:mod}`logging` module. This
|
|
module allows for easily extensible logging functionality through the use of
|
|
{py:class}`~logging.Handler` objects. The most obvious case for hooking up pyzmq to
|
|
logging would be to broadcast log messages over a PUB socket, so we have provided a
|
|
{class}`.PUBHandler` class for doing just that.
|
|
|
|
You can use PyZMQ as a log handler with no previous knowledge of how ZMQ works,
|
|
and without writing any ZMQ-specific code in your Python project.
|
|
|
|
## Getting Started
|
|
|
|
Ensure you have installed the pyzmq package from pip, ideally in a
|
|
[virtual environment](https://docs.python.org/3/library/venv.html)
|
|
you created for your project:
|
|
|
|
```
|
|
pip install pyzmq
|
|
```
|
|
|
|
Next, configure logging in your Python module and setup the ZMQ log handler:
|
|
|
|
```
|
|
import logging
|
|
from zmq.log.handlers import PUBHandler
|
|
|
|
zmq_log_handler = PUBHandler('tcp://127.0.0.1:12345')
|
|
logger = logging.getLogger()
|
|
logger.addHandler(zmq_log_handler)
|
|
```
|
|
|
|
Usually, you will add the handler only once in the top-level module of your
|
|
project, on the root logger, just as we did here.
|
|
|
|
You can choose any IP address and port number that works on your system. We
|
|
used `tcp://127.0.0.1:12345` to broadcast events via TCP on the localhost
|
|
interface at port 12345. Make note of what you choose here as you will need it
|
|
later when you listen to the events.
|
|
|
|
Logging messages works exactly like normal. This will send an INFO-level
|
|
message on the logger we configured above, and that message will be
|
|
published on a ZMQ PUB/SUB socket:
|
|
|
|
```
|
|
logger.info('hello world!')
|
|
```
|
|
|
|
You can use this module's built-in command line interface to "tune in" to
|
|
messages broadcast by the log handler. To start the log watcher,
|
|
run this command from a shell that has access to the pyzmq package
|
|
(usually a virtual environment):
|
|
|
|
```
|
|
python -m zmq.log tcp://127.0.0.1:12345
|
|
```
|
|
|
|
Then, in a separate process, run your Python module that emits log
|
|
messages. You should see them appear almost immediately.
|
|
|
|
### Using the Log Watcher
|
|
|
|
The included log watcher command line utility is helpful not only for
|
|
viewing messages, but also a programming guide to build your own ZMQ
|
|
subscriber for log messages.
|
|
|
|
To see what options are available, pass the `--help` parameter:
|
|
|
|
```
|
|
python -m zmq.log --help
|
|
```
|
|
|
|
The log watcher includes features to add a timestamp to the messages,
|
|
align the messages across different error levels, and even colorize
|
|
the output based on error level.
|
|
|
|
### Slow Joiner Problem
|
|
|
|
The great thing about using ZMQ sockets is that you can start the publisher
|
|
and subscribers in any order, and you can start & stop any of them while
|
|
you leave the others running.
|
|
|
|
When using ZMQ for logging, this means you
|
|
can leave the log watcher running while you start & stop your main
|
|
Python module.
|
|
|
|
However, you need to be aware of what the ZMQ project calls the
|
|
["slow joiner problem"](https://zguide.zeromq.org/docs/chapter5/#Slow-Subscriber-Detection-Suicidal-Snail-Pattern) .
|
|
To oversimplify, it means it can take a bit of
|
|
time for subscribers to re-connect to a publisher that has just
|
|
started up again. If the publisher starts and immediately sends a
|
|
message, subscribers will likely miss it.
|
|
|
|
The simplistic workaround when using PyZMQ for logging is to `sleep()`
|
|
briefly after startup, before sending any log messages. See the complete
|
|
example below for more details.
|
|
|
|
### Custom Log Formats
|
|
|
|
A common Python logging recipe encourages
|
|
[use of the current module name](https://docs.python.org/3/howto/logging-cookbook.html#using-logging-in-multiple-modules)
|
|
as the name of the logger. This allows your log messages to reflect your
|
|
code hierarchy in a larger project with minimal configuration.
|
|
|
|
You will need to set a different formatter to see these names in your
|
|
ZMQ-published logs. The setFormatter() method accepts a logging.Formatter
|
|
instance and optionally a log level to apply the handler to. For example:
|
|
|
|
```
|
|
zmq_log_handler = PUBHandler('tcp://127.0.0.1:12345')
|
|
zmq_log_handler.setFormatter(logging.Formatter(fmt='{name} > {message}', style='{'))
|
|
zmq_log_handler.setFormatter(logging.Formatter(fmt='{name} #{lineno:>3} > {message}', style='{'), logging.DEBUG)
|
|
```
|
|
|
|
### Root Topic
|
|
|
|
By default, the PUBHandler and log watcher use the empty string as the
|
|
root topic for published messages. This works well out-of-the-box, but you can
|
|
easily set a different root topic string to take advantage of ZMQ's built-in
|
|
topic filtering mechanism.
|
|
|
|
First, set the root topic on the handler:
|
|
|
|
```python
|
|
zmq_log_handler = PUBHandler("<tcp://127.0.0.1:12345>")
|
|
zmq_log_handler.setRootTopic("custom_topic")
|
|
```
|
|
|
|
Then specify that topic when you start the log watcher:
|
|
|
|
```
|
|
python -m zmq.log -t custom_topic <tcp://127.0.0.1:12345>
|
|
```
|
|
|
|
### Complete example
|
|
|
|
Assuming this project hierarchy:
|
|
|
|
```
|
|
example.py
|
|
greetings.py
|
|
hello.py
|
|
```
|
|
|
|
If you have this in `example.py`:
|
|
|
|
```python
|
|
import logging
|
|
from time import sleep
|
|
from zmq.log.handlers import PUBHandler
|
|
|
|
from greetings import hello
|
|
|
|
zmq_log_handler = PUBHandler("tcp://127.0.0.1:12345")
|
|
zmq_log_handler.setFormatter(logging.Formatter(fmt="{name} > {message}", style="{"))
|
|
zmq_log_handler.setFormatter(
|
|
logging.Formatter(fmt="{name} #{lineno:>3} > {message}", style="{"), logging.DEBUG
|
|
)
|
|
zmq_log_handler.setRootTopic("greeter")
|
|
|
|
logger = logging.getLogger()
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.addHandler(zmq_log_handler)
|
|
|
|
if __name__ == "__main__":
|
|
sleep(0.1)
|
|
msg_count = 5
|
|
logger.warning("Preparing to greet the world...")
|
|
for i in range(1, msg_count + 1):
|
|
logger.debug("Sending message {} of {}".format(i, msg_count))
|
|
hello.world()
|
|
sleep(1.0)
|
|
logger.info("Done!")
|
|
```
|
|
|
|
And this in `hello.py`:
|
|
|
|
```
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def world():
|
|
logger.info('hello world!')
|
|
```
|
|
|
|
You can start a log watcher in one process:
|
|
|
|
```
|
|
python -m zmq.log -t greeter --align tcp://127.0.0.1:12345
|
|
```
|
|
|
|
And then run `example.py` in another process:
|
|
|
|
```
|
|
python example.py
|
|
```
|
|
|
|
You should see the following output from the log watcher:
|
|
|
|
```
|
|
greeter.WARNING | root > Preparing to greet the world...
|
|
greeter.DEBUG | root # 21 > Sending message 1 of 5
|
|
greeter.INFO | greetings.hello > hello world!
|
|
greeter.DEBUG | root # 21 > Sending message 2 of 5
|
|
greeter.INFO | greetings.hello > hello world!
|
|
greeter.DEBUG | root # 21 > Sending message 3 of 5
|
|
greeter.INFO | greetings.hello > hello world!
|
|
greeter.DEBUG | root # 21 > Sending message 4 of 5
|
|
greeter.INFO | greetings.hello > hello world!
|
|
greeter.DEBUG | root # 21 > Sending message 5 of 5
|
|
greeter.INFO | greetings.hello > hello world!
|
|
greeter.INFO | root > Done!
|
|
```
|
|
|
|
## PUB/SUB and Topics
|
|
|
|
The ØMQ PUB/SUB pattern consists of a PUB socket broadcasting messages, and a collection
|
|
of SUB sockets that receive those messages. Each PUB message is a multipart-message, where
|
|
the first part is interpreted as a topic. SUB sockets can subscribe to topics by setting
|
|
their `SUBSCRIBE` sockopt, e.g.:
|
|
|
|
```
|
|
sub = ctx.socket(zmq.SUB)
|
|
sub.setsockopt(zmq.SUBSCRIBE, 'topic1')
|
|
sub.setsockopt(zmq.SUBSCRIBE, 'topic2')
|
|
```
|
|
|
|
When subscribed, the SUB socket will only receive messages where the first part *starts
|
|
with* one of the topics set via `SUBSCRIBE`. The default behavior is to exclude all
|
|
messages, and subscribing to the empty string '' will receive all messages.
|
|
|
|
## PUBHandler
|
|
|
|
The {class}`.PUBHandler` object is created for allowing the python logging to be emitted
|
|
on a PUB socket. The main difference between a PUBHandler and a regular logging Handler is
|
|
the inclusion of topics. For the most basic logging, you can simply create a PUBHandler
|
|
with an interface or a configured PUB socket, and just let it go:
|
|
|
|
```
|
|
pub = context.socket(zmq.PUB)
|
|
pub.bind('tcp://*:12345')
|
|
handler = PUBHandler(pub)
|
|
logger = logging.getLogger()
|
|
logger.addHandler(handler)
|
|
```
|
|
|
|
At this point, all messages logged with the default logger will be broadcast on the pub
|
|
socket.
|
|
|
|
the PUBHandler does work with topics, and the handler has an attribute `root_topic`:
|
|
|
|
```python
|
|
handler.root_topic = "myprogram"
|
|
```
|
|
|
|
Python loggers also have loglevels. The base topic of messages emitted by the PUBHandler
|
|
will be of the form: `<handler.root_topic>.<loglevel>`, e.g. `myprogram.INFO` or
|
|
'whatever.ERROR'. This way, subscribers can easily subscribe to subsets of the logging
|
|
messages. Log messages are always two-part, where the first part is the topic tree, and
|
|
the second part is the actual log message.
|
|
|
|
```python
|
|
logger.info("hello there")
|
|
print(sub.recv_multipart())
|
|
```
|
|
|
|
```
|
|
[b"myprogram.INFO", b"hello there"]
|
|
```
|
|
|
|
### Subtopics
|
|
|
|
You can also add to the topic tree below the loglevel on an individual message basis.
|
|
Assuming your logger is connected to a PUBHandler, you can add as many additional topics
|
|
on the front of the message, which will be added always after the loglevel. A special
|
|
delimiter defined at `zmq.log.handlers.TOPIC_DELIM` is scanned by the PUBHandler, so if
|
|
you pass your own subtopics prior to that symbol, they will be stripped from the message
|
|
and added to the topic tree:
|
|
|
|
```
|
|
>>> log_msg = "hello there"
|
|
>>> subtopic = "sub.topic"
|
|
>>> msg = zmq.log.handlers.TOPIC_DELIM.join([subtopic, log_msg])
|
|
>>> logger.warn(msg)
|
|
>>> print sub.recv_multipart()
|
|
['myprogram.WARN.sub.topic', 'hello there']
|
|
```
|