Skip to content

Commit 4ca0fa7

Browse files
authored
Merge pull request #440 from itamarst/407-faster-messages
#407: Make Eliot abstractions more focused on actions, by deprecating Message
2 parents 9ad9148 + cb196db commit 4ca0fa7

24 files changed

+312
-289
lines changed

benchmarks/serialization.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ def run():
2222
start = time.time()
2323
for i in range(N):
2424
with start_action(action_type="my_action"):
25-
with start_action(action_type="my_action2"):
26-
Message.log(
25+
with start_action(action_type="my_action2") as ctx:
26+
ctx.log(
2727
message_type="my_message",
2828
integer=3,
29-
string=b"abcdeft",
29+
string="abcdeft",
3030
string2="dgsjdlkgjdsl",
3131
list=[1, 2, 3, 4],
3232
)

docs/source/generating/actions.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ Consider the following code sample:
9191

9292
.. code-block:: python
9393
94-
from eliot import start_action, start_task, Message
94+
from eliot import start_action, start_task
9595
96-
with start_task(action_type="parent"):
97-
Message.log(message_type="info", x=1)
98-
with start_action(action_type="child"):
99-
Message.log(message_type="info", x=2)
96+
with start_task(action_type="parent") as action:
97+
action.log(message_type="info", x=1)
98+
with start_action(action_type="child") as action:
99+
action.log(message_type="info", x=2)
100100
raise RuntimeError("ono")
101101
102102
All these messages will share the same UUID in their ``task_uuid`` field, since they are all part of the same high-level task.
@@ -197,14 +197,14 @@ You shouldn't log within an action's context after it has been finished:
197197

198198
.. code-block:: python
199199
200-
from eliot import start_action, Message
200+
from eliot import start_action
201201
202202
with start_action(action_type=u"message_late").context() as action:
203-
Message.log(message_type=u"ok")
203+
action.log(message_type=u"ok")
204204
# finish the action:
205205
action.finish()
206206
# Don't do this! This message is being added to a finished action!
207-
Message.log(message_type=u"late")
207+
action.log(message_type=u"late")
208208
209209
As an alternative to ``with``, you can also explicitly run a function within the action context:
210210

docs/source/generating/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ Generating Logs
22
===============
33

44
.. toctree::
5-
messages
65
actions
6+
messages
77
errors
88
loglevels
99
migrating
Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,35 @@
1+
.. _messages:
2+
13
Messages
24
========
35

4-
Basic usage
5-
-----------
6+
Sometimes you don't want to generate actions. sometimes you just want an individual isolated message, the way traditional logging systems work.
7+
Here's how to do that.
68

7-
At its base, Eliot outputs structured messages composed of named fields.
8-
Eliot messages are typically serialized to JSON objects.
9-
Fields therefore can have Unicode names, so either ``unicode`` or ``bytes`` containing UTF-8 encoded Unicode.
10-
Message values must be supported by JSON: ``int``, ``float``, ``None``, ``unicode``, UTF-8 encoded Unicode as ``bytes``, ``dict`` or ``list``.
11-
The latter two can only be composed of other supported types.
9+
When you have an action
10+
-----------------------
1211

13-
You can log a message like this:
12+
If you already have an action object, you can log a message in that action's context:
1413

1514
.. code-block:: python
1615
17-
from eliot import Message
16+
from eliot import start_action
1817
1918
class YourClass(object):
2019
def run(self):
21-
# Log a message with two fields, "key" and "value":
22-
Message.log(key=123, value=u"hello")
23-
24-
You can also create message and then log it later like this:
20+
with start_action(action_type="myaction") as ctx:
21+
ctx.log(message_type="mymessage", key="abc", key2=4)
2522
26-
.. code-block:: python
23+
If you don't have an action
24+
---------------------------
2725

28-
from eliot import Message
26+
If you don't have a reference to an action, or you're worried the function will sometimes be called outside the context of any action at all, you can use ``log_message``:
2927

30-
class YourClass(object):
31-
def run(self):
32-
# Create a message with two fields, "key" and "value":
33-
msg = Message.new(key=123, value=u"hello")
34-
# Write the message:
35-
msg.write()
36-
37-
38-
Message binding
39-
---------------
28+
.. code-block:: python
4029
41-
You can also create a new ``Message`` from an existing one by binding new values.
42-
New values will override ones on the base ``Message``, but ``bind()`` does not mutate the original ``Message``.
30+
from eliot import log_message
4331
44-
.. code-block:: python
32+
def run(x):
33+
log_message(message_type="in_run", xfield=x)
4534
46-
# This message has fields key=123, value=u"hello"
47-
msg = Message.new(key=123, value=u"hello")
48-
# And this one has fields key=123, value=u"other", extra=456
49-
msg2 = msg.bind(value=u"other", extra=456)
35+
The main downside to using this function is that it's a little slower, since it needs to handle the case where there is no action in context.

docs/source/generating/testing.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
Unit Testing Your Logging with Types
2-
====================================
1+
Unit Testing Your Logging
2+
=========================
33

44
Now that you've got some code emitting log messages (or even better, before you've written the code) you can write unit tests to verify it.
55
Given good test coverage all code branches should already be covered by tests unrelated to logging.
66
Logging can be considered just another aspect of testing those code branches.
7+
78
Rather than recreating all those tests as separate functions Eliot provides a decorator the allows adding logging assertions to existing tests.
89

910

@@ -32,7 +33,7 @@ You can also ensure the correct messages were logged.
3233

3334
.. code-block:: python
3435
35-
from eliot import Message
36+
from eliot import log_message
3637
3738
class UserRegistration(object):
3839
@@ -41,7 +42,7 @@ You can also ensure the correct messages were logged.
4142
4243
def register(self, username, password, age):
4344
self.db[username] = (password, age)
44-
Message.log(message_type="user_registration",
45+
log_message(message_type="user_registration",
4546
username=username, password=password,
4647
age=age)
4748

docs/source/generating/twisted.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ To understand why, consider the following example:
7878
7979
@inlineCallbacks # don't use this in real code, use eliot.twisted.inline_callbacks
8080
def go():
81-
with start_action(action_type=u"yourapp:subsystem:frob"):
82-
d = some_deferred_api()
83-
x = yield d
84-
Message.log(message_type=u"some-report", x=x)
81+
with start_action(action_type=u"yourapp:subsystem:frob") as action:
82+
d = some_deferred_api()
83+
x = yield d
84+
action.log(message_type=u"some-report", x=x)
8585
8686
The action started by this generator remains active as ``yield d`` gives up control to the ``inlineCallbacks`` controller.
8787
The next bit of code to run will be considered to be a child of ``action``.

docs/source/introduction.rst

Lines changed: 17 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,12 @@ Why Eliot?
1313

1414
— George Eliot, *Middlemarch*
1515

16-
The log messages generated by a piece of software tell a story: what, where, when, even why and how if you’re lucky. The readers of this story are more often than not other programs: monitoring systems, performance tools, or just filtering the messages down to something a human can actually comprehend. Unfortunately the output of most logging systems is ill-suited to being read by programs. Even worse, most logging systems omit critical information that both humans and their programs need.
16+
The log messages generated by a piece of software ought tell a story: what, where, when, even why and how if you’re lucky.
17+
But most logging systems omit the all-important *why*.
18+
You know that some things happened, but not how they relate to each other.
1719

18-
Problem #1: Text is hard to search
19-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20-
21-
Let’s say you want to find all the log messages about a specific person. A first pass of log messages might look like this:
22-
23-
Sir James Chettam was going to dine at the Grange to-day with another gentleman whom the girls had never seen, and about whom Dorothea felt some venerating expectation.
24-
25-
If Miss Brooke ever attained perfect meekness, it would not be for lack of inward fire.
26-
27-
You could do a text search for log messages containing the text “Dorothea”, but this is likely to fail for some types of searches. You might want to searching for actions involving dinner, but then you would need to search for “dine” and “dinner” and perhaps other words well. A library like `structlog`_ that can generate structured log messages will solve this first problem. You could define a “person” field in your messages and then you can search for all messages where ``person == "Dorothea"`` as well as other structured queries.
28-
29-
.. _structlog: https://structlog.readthedocs.org/
30-
31-
32-
Problem #2: Referring to Entities
33-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34-
35-
Every time a log message is written out you need to decide how to refer to the objects being logged. In the messages we saw above “Dorothea” and “Miss Brooke” are in fact different identifiers for the same person. Having structured messages doesn’t help us find all messages about a specific entity if the object is referred to inconsistently. What you need is infrastructure for converting specific kinds of objects in your code to fields in your structured log messages. Then you can just say “log a message that refers to this Person” and that reusable code will make sure the correct identifier is generated.
36-
37-
38-
Problem #3: Actions
39-
^^^^^^^^^^^^^^^^^^^
20+
The problem: What caused this to happen?
21+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4022

4123
Most log messages in your program are going to involve actions:
4224

@@ -46,74 +28,33 @@ A marriage has a beginning and eventually an end. The end may be successful, pre
4628

4729
Actions also generate other actions: a marriage leads to a trip to Rome, the trip to Rome might lead to a visit to the Vatican Museum, and so on. Other unrelated actions are occurring at the same time, resulting in a forest of actions, with root actions that grow a tree of child actions.
4830

49-
You might want to trace an action from beginning to end, e.g. to measure how long it took to run. You might want to know what high-level action caused a particular unexpected low-level action. You might want to know what actions a specific entity was involved with. None of these are possible in most logging systems since they have no concept of actions to begin with.
50-
51-
52-
Problem #4: Cross-Process Actions
53-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31+
You might want to trace an action from beginning to end, e.g. to measure how long it took to run. You might want to know what high-level action caused a particular unexpected low-level action. You might want to know what actions a specific entity was involved with.
5432

55-
A story may involve many characters in many places at many times.
56-
The novelist has the power to present the internal consciousness of not just one character but many: their ways of thinking, their different perceptions of reality.
33+
None of these are possible in most logging systems since they have no concept of actions.
5734

58-
Similarly, actions in a distributed system may span multiple processes.
59-
An incoming request to one server may cause a ripple of effects reaching many other processes; the logs from a single process in isolation are insufficient to understand what happened and why.
6035

61-
62-
The Solution: Eliot
36+
The solution: Eliot
6337
^^^^^^^^^^^^^^^^^^^
64-
Eliot is designed to solve all of these problems.
65-
For simplicity's sake this example focuses on problems 1 and 3; problem 2 is covered by the :ref:`type system <type system>` and problem 4 by :ref:`cross-process actions <cross process tasks>`.
66-
67-
.. literalinclude:: ../../examples/rometrip_messages.py
68-
69-
Here’s how the log messages generated by the code look, as summarized by the `eliot-tree <https://warehouse.python.org/project/eliot-tree/>`_ tool:
70-
71-
.. code::
72-
73-
68c12428-5d60-49f5-a269-3fb681938f98
74-
+-- honeymoon@1
75-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
76-
77-
361298ae-b6b7-439a-bc9b-ffde68b7860d
78-
+-- visited@1
79-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
80-
|-- place: Rome, Italy
8138

82-
7fe1615c-e442-4bca-b667-7bb435ac6cb8
83-
+-- visited@1
84-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
85-
|-- place: Vatican Museum
39+
Eliot is designed to solve these problems: the basic logging abstraction is the action.
8640

87-
c746230c-627e-4ff9-9173-135568df976c
88-
+-- visited@1
89-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
90-
|-- place: Statue #1
91-
92-
5482ec10-36c6-4194-964f-074e325b9329
93-
+-- visited@1
94-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
95-
|-- place: Statue #2
96-
97-
We can see different messages are related insofar as they refer to the same person, or the same thing… but we can’t trace the relationship in terms of actions. Was looking at a statue the result of visiting Rome? There’s no way we can tell from the log messages. We could manually log start and finish messages but that won’t suffice when we have many interleaved actions involving the same objects. Which of twenty parallel HTTP request tried to insert a row into the database? Chronological messages simply cannot tell us that.
98-
99-
The solution is to introduce two new concepts: actions and tasks. An “action” is something with a start and an end; the end can be successful or it can fail due to an exception. Log messages, as well as log actions, know the log action whose context they are running in. The result is a tree of actions. A “task” is a top-level action, a basic entry point into the program which drives other actions. The task is therefore the root of the tree of actions. For example, an HTTP request received by a web server might be a task.
100-
101-
In our example we have one task (the honeymoon), an action (travel). We will leave looking as a normal log message because it always succeeds, and no other log message will ever need to run its context. Here’s how our code looks now:
41+
An “action” is something with a start and an end; the end can be successful or it can fail due to an exception. Log messages, as well as log actions, know the log action whose context they are running in. The result is a tree of actions.
10242

43+
In the following example we have one top-level action (the honeymoon), which leads to other action (travel):
10344

10445
.. literalinclude:: ../../examples/rometrip_actions.py
10546

106-
Actions provide a Python context manager. When the action or task starts a start message is logged.
47+
Actions provide a Python context manager. When the action starts, a start message is logged.
10748
If the block finishes successfully a success message is logged for the action; if an exception is thrown a failure message is logged for the action with the exception type and contents.
108-
Not shown here but supported by the API is the ability to add fields to the success messages for an action. A similar API supports Twisted’s Deferreds.
10949

50+
By default the messages are machine-parseable JSON, but for human consumption a visualization is better.
11051
Here’s how the log messages generated by the new code look, as summarized by the `eliot-tree <https://warehouse.python.org/project/eliot-tree/>`_ tool:
11152

11253
.. code-block:: console
11354
11455
f9dcc74f-ecda-4543-9e9a-1bb062d199f0
11556
+-- honeymoon@1/started
116-
|-- people: [u'Mrs. Casaubon', u'Mr. Casaubon']
57+
|-- people: ['Mrs. Casaubon', 'Mr. Casaubon']
11758
+-- visited@2,1/started
11859
|-- place: Rome, Italy
11960
+-- visited@2,2,1/started
@@ -128,4 +69,6 @@ Here’s how the log messages generated by the new code look, as summarized by t
12869
+-- visited@2,3/succeeded
12970
+-- honeymoon@3/succeeded
13071
131-
No longer isolated fragments of meaning, our log messages are now a story. Log events have context, you can tell where they came from and what they led to without guesswork. Was looking at a statue the result of the honeymoon? It most definitely was.
72+
No longer isolated fragments of meaning, our log messages are now a story. Log events have context, you can tell where they came from and what they led to without guesswork.
73+
74+
Was looking at a statue the result of the honeymoon? It most definitely was.

docs/source/news.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ What's New
66

77
Features:
88

9+
* ``Message.log()`` has been replaced by top-level function ``log_message()``. Or if you're in the context of action ``ctx``, you can call ``ctx.log()``. See :ref:`messages` for details.
910
* Python 3.8 is now supported.
1011
* The ``eliot-prettyprint`` command line tool now supports a more compact format by using the ``--compact`` argument.
1112
* The ``eliot-prettyprint`` command line tool now supports outputting in local timezones using the ``--local-timezone`` argument.

docs/source/reading/fields.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Message Fields in Depth
22
=======================
33

4+
Structure
5+
---------
6+
7+
Eliot messages are typically serialized to JSON objects.
8+
Fields therefore must have ``str`` as their name.
9+
Message values must be supported by JSON: ``int``, ``float``, ``None``, ``str``, ``dict`` or ``list``.
10+
The latter two can only be composed of other supported types.
11+
412
Built-in Fields
513
---------------
614

eliot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
preserve_context,
2929
current_action,
3030
log_call,
31+
log_message,
3132
)
3233
from ._output import ILogger, Logger, MemoryLogger, to_file, FileDestination
3334
from ._validation import Field, fields, MessageType, ActionType, ValidationError
@@ -119,6 +120,7 @@ def _parse_compat():
119120
"add_global_fields",
120121
"to_file",
121122
"log_call",
123+
"log_message",
122124
"__version__",
123125
# Backwards compat for eliot-tree:
124126
"_parse",

0 commit comments

Comments
 (0)