New upstream snapshot.
Debian Janitor
1 year, 2 months ago
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: eliot |
2 | Version: 1.13.0 | |
2 | Version: 0+unknown | |
3 | 3 | Summary: Logging library that tells you why it happened |
4 | 4 | Home-page: https://github.com/itamarst/eliot/ |
5 | 5 | Maintainer: Itamar Turner-Trauring |
6 | 6 | Maintainer-email: itamar@itamarst.org |
7 | 7 | License: Apache 2.0 |
8 | Description: Eliot: Logging that tells you *why* it happened | |
9 | ================================================ | |
10 | ||
11 | .. image:: https://travis-ci.org/itamarst/eliot.png?branch=master | |
12 | :target: http://travis-ci.org/itamarst/eliot | |
13 | :alt: Build Status | |
14 | ||
15 | Python's built-in ``logging`` and other similar systems output a stream of factoids: they're interesting, but you can't really tell what's going on. | |
16 | ||
17 | * Why is your application slow? | |
18 | * What caused this code path to be chosen? | |
19 | * Why did this error happen? | |
20 | ||
21 | Standard logging can't answer these questions. | |
22 | ||
23 | But with a better model you could understand what and why things happened in your application. | |
24 | You could pinpoint performance bottlenecks, you could understand what happened when, who called what. | |
25 | ||
26 | That is what Eliot does. | |
27 | ``eliot`` is a Python logging system that outputs causal chains of **actions**: actions can spawn other actions, and eventually they either **succeed or fail**. | |
28 | The resulting logs tell you the story of what your software did: what happened, and what caused it. | |
29 | ||
30 | Eliot supports a range of use cases and 3rd party libraries: | |
31 | ||
32 | * Logging within a single process. | |
33 | * Causal tracing across a distributed system. | |
34 | * Scientific computing, with `built-in support for NumPy and Dask <https://eliot.readthedocs.io/en/stable/scientific-computing.html>`_. | |
35 | * `Asyncio and Trio coroutines <https://eliot.readthedocs.io/en/stable/generating/asyncio.html>`_ and the `Twisted networking framework <https://eliot.readthedocs.io/en/stable/generating/twisted.html>`_. | |
36 | ||
37 | Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines. | |
38 | ||
39 | Eliot supports Python 3.6, 3.7, 3.8, and 3.9, as well as PyPy3. | |
40 | It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License. | |
41 | ||
42 | Python 2.7 is in legacy support mode, with the last release supported being 1.7; see `here <https://eliot.readthedocs.io/en/stable/python2.html>`_ for details. | |
43 | ||
44 | * `Read the documentation <https://eliot.readthedocs.io>`_. | |
45 | * Download from `PyPI`_ or `conda-forge <https://anaconda.org/conda-forge/eliot>`_. | |
46 | * Need help or have any questions? `File an issue <https://github.com/itamarst/eliot/issues/new>`_ on GitHub. | |
47 | * **Commercial support** is available from `Python⇒Speed <https://pythonspeed.com/services/#eliot>`_. | |
48 | ||
49 | Testimonials | |
50 | ------------ | |
51 | ||
52 | "Eliot has made tracking down causes of failure (in complex external integrations and internal uses) tremendously easier. Our errors are logged to Sentry with the Eliot task UUID. That means we can go from a Sentry notification to a high-level trace of operations—with important metadata at each operation—in a few seconds. We immediately know which user did what in which part of the system." | |
53 | ||
54 | —Jonathan Jacobs | |
55 | ||
56 | .. _Github: https://github.com/itamarst/eliot | |
57 | .. _PyPI: https://pypi.python.org/pypi/eliot | |
58 | ||
59 | 8 | Keywords: logging |
60 | Platform: UNKNOWN | |
61 | 9 | Classifier: Intended Audience :: Developers |
62 | 10 | Classifier: License :: OSI Approved :: Apache Software License |
63 | 11 | Classifier: Operating System :: OS Independent |
67 | 15 | Classifier: Programming Language :: Python :: 3.7 |
68 | 16 | Classifier: Programming Language :: Python :: 3.8 |
69 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
70 | 19 | Classifier: Programming Language :: Python :: Implementation :: CPython |
71 | 20 | Classifier: Programming Language :: Python :: Implementation :: PyPy |
72 | 21 | Classifier: Topic :: System :: Logging |
73 | 22 | Requires-Python: >=3.6.0 |
23 | Provides-Extra: dev | |
74 | 24 | Provides-Extra: journald |
75 | 25 | Provides-Extra: test |
76 | Provides-Extra: dev | |
26 | License-File: LICENSE | |
27 | ||
28 | Eliot: Logging that tells you *why* it happened | |
29 | ================================================ | |
30 | ||
31 | .. image:: https://travis-ci.org/itamarst/eliot.png?branch=master | |
32 | :target: http://travis-ci.org/itamarst/eliot | |
33 | :alt: Build Status | |
34 | ||
35 | Python's built-in ``logging`` and other similar systems output a stream of factoids: they're interesting, but you can't really tell what's going on. | |
36 | ||
37 | * Why is your application slow? | |
38 | * What caused this code path to be chosen? | |
39 | * Why did this error happen? | |
40 | ||
41 | Standard logging can't answer these questions. | |
42 | ||
43 | But with a better model you could understand what and why things happened in your application. | |
44 | You could pinpoint performance bottlenecks, you could understand what happened when, who called what. | |
45 | ||
46 | That is what Eliot does. | |
47 | ``eliot`` is a Python logging system that outputs causal chains of **actions**: actions can spawn other actions, and eventually they either **succeed or fail**. | |
48 | The resulting logs tell you the story of what your software did: what happened, and what caused it. | |
49 | ||
50 | Eliot supports a range of use cases and 3rd party libraries: | |
51 | ||
52 | * Logging within a single process. | |
53 | * Causal tracing across a distributed system. | |
54 | * Scientific computing, with `built-in support for NumPy and Dask <https://eliot.readthedocs.io/en/stable/scientific-computing.html>`_. | |
55 | * `Asyncio and Trio coroutines <https://eliot.readthedocs.io/en/stable/generating/asyncio.html>`_ and the `Twisted networking framework <https://eliot.readthedocs.io/en/stable/generating/twisted.html>`_. | |
56 | ||
57 | Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines. | |
58 | ||
59 | Eliot supports Python 3.6, 3.7, 3.8, 3.9, and 3.10, as well as PyPy3. | |
60 | It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License. | |
61 | ||
62 | * `Read the documentation <https://eliot.readthedocs.io>`_. | |
63 | * Download from `PyPI`_ or `conda-forge <https://anaconda.org/conda-forge/eliot>`_. | |
64 | * Need help or have any questions? `File an issue <https://github.com/itamarst/eliot/issues/new>`_ on GitHub. | |
65 | * **Commercial support** is available from `Python⇒Speed <https://pythonspeed.com/services/#eliot>`_. | |
66 | ||
67 | Testimonials | |
68 | ------------ | |
69 | ||
70 | "Eliot has made tracking down causes of failure (in complex external integrations and internal uses) tremendously easier. Our errors are logged to Sentry with the Eliot task UUID. That means we can go from a Sentry notification to a high-level trace of operations—with important metadata at each operation—in a few seconds. We immediately know which user did what in which part of the system." | |
71 | ||
72 | —Jonathan Jacobs | |
73 | ||
74 | .. _Github: https://github.com/itamarst/eliot | |
75 | .. _PyPI: https://pypi.python.org/pypi/eliot |
28 | 28 | |
29 | 29 | Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines. |
30 | 30 | |
31 | Eliot supports Python 3.6, 3.7, 3.8, and 3.9, as well as PyPy3. | |
31 | Eliot supports Python 3.6, 3.7, 3.8, 3.9, and 3.10, as well as PyPy3. | |
32 | 32 | It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License. |
33 | ||
34 | Python 2.7 is in legacy support mode, with the last release supported being 1.7; see `here <https://eliot.readthedocs.io/en/stable/python2.html>`_ for details. | |
35 | 33 | |
36 | 34 | * `Read the documentation <https://eliot.readthedocs.io>`_. |
37 | 35 | * Download from `PyPI`_ or `conda-forge <https://anaconda.org/conda-forge/eliot>`_. |
0 | python-eliot (1.13.0-2) UNRELEASED; urgency=medium | |
0 | python-eliot (1.14.0+git20211224.1.74e0aa2-1) UNRELEASED; urgency=medium | |
1 | 1 | |
2 | 2 | * Update standards version to 4.6.2, no changes needed. |
3 | * New upstream snapshot. | |
3 | 4 | |
4 | -- Debian Janitor <janitor@jelmer.uk> Sun, 08 Jan 2023 18:34:28 -0000 | |
5 | -- Debian Janitor <janitor@jelmer.uk> Wed, 11 Jan 2023 04:11:56 -0000 | |
5 | 6 | |
6 | 7 | python-eliot (1.13.0-1) unstable; urgency=medium |
7 | 8 |
0 | 0 | What's New |
1 | 1 | ========== |
2 | ||
3 | 1.14.0 | |
4 | ^^^^^^ | |
5 | ||
6 | Features: | |
7 | ||
8 | * ``Action.continue_task`` now takes ``action_task`` and extra fields to use for the action, so the default ``eliot:remote_task`` can be changed. | |
9 | * Added support for Python 3.10. | |
10 | ||
11 | Bug fixes: | |
12 | ||
13 | * Fix infinite recursion when a logging destination raises exceptions forever. Thanks to to @alextatarinov. | |
2 | 14 | |
3 | 15 | 1.13.0 |
4 | 16 | ^^^^^^ |
3 | 3 | ================== |
4 | 4 | |
5 | 5 | The last version of Eliot to support Python 2.7 was release 1.7. |
6 | ||
7 | If you are using Eliot with Python 2, keep the following in mind: | |
8 | ||
9 | * I will provide critical bug fixes for Python 2 until March 2020. | |
10 | I will accept patches for critical bug fixes after that (or you can `pay for my services <https://pythonspeed.com/services/#eliot>`_ to do additional work). | |
11 | * Make sure you use an up-to-date ``setuptools`` and ``pip``; in theory this should result in only downloading versions of the package that support Python 2. | |
12 | * For extra safety, you can pin Eliot in ``setup.py`` or ``requirements.txt`` by setting: ``eliot < 1.8``. | |
13 | * Critical bug fixes for Python 2 will be released as 1.7.1, 1.7.2, etc.. |
235 | 235 | ).encode("ascii") |
236 | 236 | |
237 | 237 | @classmethod |
238 | def continue_task(cls, logger=None, task_id=_TASK_ID_NOT_SUPPLIED): | |
238 | def continue_task( | |
239 | cls, | |
240 | logger=None, | |
241 | task_id=_TASK_ID_NOT_SUPPLIED, | |
242 | *, | |
243 | action_type="eliot:remote_task", | |
244 | _serializers=None, | |
245 | **fields, | |
246 | ): | |
239 | 247 | """ |
240 | 248 | Start a new action which is part of a serialized task. |
241 | 249 | |
245 | 253 | @param task_id: A serialized task identifier, the output of |
246 | 254 | L{Action.serialize_task_id}, either ASCII-encoded bytes or unicode |
247 | 255 | string. Required. |
256 | ||
257 | @param action_type: The type of this action, | |
258 | e.g. C{"yourapp:subsystem:dosomething"}. | |
259 | ||
260 | @param _serializers: Either a L{eliot._validation._ActionSerializers} | |
261 | instance or C{None}. In the latter case no validation or serialization | |
262 | will be done for messages generated by the L{Action}. | |
263 | ||
264 | @param fields: Additional fields to add to the start message. | |
248 | 265 | |
249 | 266 | @return: The new L{Action} instance. |
250 | 267 | """ |
254 | 271 | task_id = task_id.decode("ascii") |
255 | 272 | uuid, task_level = task_id.split("@") |
256 | 273 | action = cls( |
257 | logger, uuid, TaskLevel.fromString(task_level), "eliot:remote_task" | |
274 | logger, uuid, TaskLevel.fromString(task_level), action_type, _serializers | |
258 | 275 | ) |
259 | action._start({}) | |
276 | action._start(fields) | |
260 | 277 | return action |
261 | 278 | |
262 | 279 | # Backwards-compat variants: |
422 | 439 | fields[TASK_UUID_FIELD] = self._identification[TASK_UUID_FIELD] |
423 | 440 | fields[TASK_LEVEL_FIELD] = self._nextTaskLevel().as_list() |
424 | 441 | fields[MESSAGE_TYPE_FIELD] = message_type |
425 | self._logger.write(fields, fields.pop("__eliot_serializer__", None)) | |
442 | # Loggers will hopefully go away... | |
443 | logger = fields.pop("__eliot_logger__", self._logger) | |
444 | logger.write(fields, fields.pop("__eliot_serializer__", None)) | |
426 | 445 | |
427 | 446 | |
428 | 447 | class WrongTask(Exception): |
933 | 952 | |
934 | 953 | If there is no current action, a new UUID will be generated. |
935 | 954 | """ |
936 | # Loggers will hopefully go away... | |
937 | logger = fields.pop("__eliot_logger__", None) | |
938 | 955 | action = current_action() |
939 | 956 | if action is None: |
957 | # Loggers will hopefully go away... | |
958 | logger = fields.pop("__eliot_logger__", None) | |
940 | 959 | action = Action(logger, str(uuid4()), TaskLevel(level=[]), "") |
941 | 960 | action.log(message_type, **fields) |
942 | 961 |
115 | 115 | Byte field names will be converted to Unicode. |
116 | 116 | |
117 | 117 | @type logger: L{eliot.ILogger} or C{None} indicating the default one. |
118 | Should not be set if the action is also set. | |
118 | 119 | |
119 | @param action: The L{Action} which is the context for this message. If | |
120 | C{None}, the L{Action} will be deduced from the current call | |
121 | stack. | |
120 | @param action: The L{Action} which is the context for this message. If | |
121 | C{None}, the L{Action} will be deduced from the current call stack. | |
122 | 122 | """ |
123 | 123 | fields = dict(self._contents) |
124 | 124 | if "message_type" not in fields: |
126 | 126 | if self._serializer is not None: |
127 | 127 | fields["__eliot_serializer__"] = self._serializer |
128 | 128 | if action is None: |
129 | fields["__eliot_logger__"] = logger | |
129 | if logger is not None: | |
130 | fields["__eliot_logger__"] = logger | |
130 | 131 | log_message(**fields) |
131 | 132 | else: |
132 | 133 | action.log(**fields) |
1 | 1 | Implementation of hooks and APIs for outputting log messages. |
2 | 2 | """ |
3 | 3 | |
4 | import sys | |
5 | 4 | import traceback |
6 | 5 | import inspect |
7 | 6 | import json as pyjson |
21 | 20 | from ._validation import ValidationError |
22 | 21 | |
23 | 22 | |
24 | class _DestinationsSendError(Exception): | |
25 | """ | |
26 | An error occured sending to one or more destinations. | |
27 | ||
28 | @ivar errors: A list of tuples output from C{sys.exc_info()}. | |
29 | """ | |
30 | ||
31 | def __init__(self, errors): | |
32 | self.errors = errors | |
33 | Exception.__init__(self, errors) | |
23 | # Action type for log messages due to a (hopefully temporarily) broken | |
24 | # destination. | |
25 | DESTINATION_FAILURE = "eliot:destination_failure" | |
34 | 26 | |
35 | 27 | |
36 | 28 | class BufferingDestination(object): |
69 | 61 | """ |
70 | 62 | self._globalFields.update(fields) |
71 | 63 | |
72 | def send(self, message): | |
64 | def send(self, message, logger=None): | |
73 | 65 | """ |
74 | 66 | Deliver a message to all destinations. |
75 | 67 | |
76 | 68 | The passed in message might be mutated. |
69 | ||
70 | This should never raise an exception. | |
77 | 71 | |
78 | 72 | @param message: A message dictionary that can be serialized to JSON. |
79 | 73 | @type message: L{dict} |
74 | ||
75 | @param logger: The ``ILogger`` that wrote the message, if any. | |
80 | 76 | """ |
81 | 77 | message.update(self._globalFields) |
82 | 78 | errors = [] |
79 | is_destination_error_message = ( | |
80 | message.get("message_type", None) == DESTINATION_FAILURE | |
81 | ) | |
83 | 82 | for dest in self._destinations: |
84 | 83 | try: |
85 | 84 | dest(message) |
85 | except Exception as e: | |
86 | # If the destination is broken not because of a specific | |
87 | # message, but rather continously, we will get a | |
88 | # "eliot:destination_failure" log message logged, and so we | |
89 | # want to ensure it doesn't do infinite recursion. | |
90 | if not is_destination_error_message: | |
91 | errors.append(e) | |
92 | ||
93 | for exception in errors: | |
94 | from ._action import log_message | |
95 | ||
96 | try: | |
97 | new_msg = { | |
98 | MESSAGE_TYPE_FIELD: DESTINATION_FAILURE, | |
99 | REASON_FIELD: safeunicode(exception), | |
100 | EXCEPTION_FIELD: exception.__class__.__module__ | |
101 | + "." | |
102 | + exception.__class__.__name__, | |
103 | "message": _safe_unicode_dictionary(message), | |
104 | } | |
105 | if logger is not None: | |
106 | # This is really only useful for testing, should really | |
107 | # figure out way to get rid of this mechanism... | |
108 | new_msg["__eliot_logger__"] = logger | |
109 | log_message(**new_msg) | |
86 | 110 | except: |
87 | errors.append(sys.exc_info()) | |
88 | if errors: | |
89 | raise _DestinationsSendError(errors) | |
111 | # Nothing we can do here, raising exception to caller will | |
112 | # break business logic, better to have that continue to | |
113 | # work even if logging isn't. | |
114 | pass | |
90 | 115 | |
91 | 116 | def add(self, *destinations): |
92 | 117 | """ |
143 | 168 | """ |
144 | 169 | |
145 | 170 | |
171 | def _safe_unicode_dictionary(dictionary): | |
172 | """ | |
173 | Serialize a dictionary to a unicode string no matter what it contains. | |
174 | ||
175 | The resulting dictionary will loosely follow Python syntax but it is | |
176 | not expected to actually be a lossless encoding in all cases. | |
177 | ||
178 | @param dictionary: A L{dict} to serialize. | |
179 | ||
180 | @return: A L{unicode} string representing the input dictionary as | |
181 | faithfully as can be done without putting in too much effort. | |
182 | """ | |
183 | try: | |
184 | return str( | |
185 | dict( | |
186 | (saferepr(key), saferepr(value)) for (key, value) in dictionary.items() | |
187 | ) | |
188 | ) | |
189 | except: | |
190 | return saferepr(dictionary) | |
191 | ||
192 | ||
146 | 193 | @implementer(ILogger) |
147 | 194 | class Logger(object): |
148 | 195 | """ |
154 | 201 | """ |
155 | 202 | |
156 | 203 | _destinations = Destinations() |
157 | _log_tracebacks = True | |
158 | ||
159 | def _safeUnicodeDictionary(self, dictionary): | |
160 | """ | |
161 | Serialize a dictionary to a unicode string no matter what it contains. | |
162 | ||
163 | The resulting dictionary will loosely follow Python syntax but it is | |
164 | not expected to actually be a lossless encoding in all cases. | |
165 | ||
166 | @param dictionary: A L{dict} to serialize. | |
167 | ||
168 | @return: A L{unicode} string representing the input dictionary as | |
169 | faithfully as can be done without putting in too much effort. | |
170 | """ | |
171 | try: | |
172 | return str( | |
173 | dict( | |
174 | (saferepr(key), saferepr(value)) | |
175 | for (key, value) in dictionary.items() | |
176 | ) | |
177 | ) | |
178 | except: | |
179 | return saferepr(dictionary) | |
180 | 204 | |
181 | 205 | def write(self, dictionary, serializer=None): |
182 | 206 | """ |
192 | 216 | |
193 | 217 | log_message( |
194 | 218 | "eliot:serialization_failure", |
195 | message=self._safeUnicodeDictionary(dictionary), | |
219 | message=_safe_unicode_dictionary(dictionary), | |
196 | 220 | __eliot_logger__=self, |
197 | 221 | ) |
198 | 222 | return |
199 | 223 | |
200 | try: | |
201 | self._destinations.send(dictionary) | |
202 | except _DestinationsSendError as e: | |
203 | from ._action import log_message | |
204 | ||
205 | if self._log_tracebacks: | |
206 | for (exc_type, exception, exc_traceback) in e.errors: | |
207 | # Can't use same Logger as serialization errors because | |
208 | # if destination continues to error out we will get | |
209 | # infinite recursion. So instead we have to manually | |
210 | # construct a Logger that won't retry. | |
211 | logger = Logger() | |
212 | logger._log_tracebacks = False | |
213 | logger._destinations = self._destinations | |
214 | msg = { | |
215 | MESSAGE_TYPE_FIELD: "eliot:destination_failure", | |
216 | REASON_FIELD: safeunicode(exception), | |
217 | EXCEPTION_FIELD: exc_type.__module__ + "." + exc_type.__name__, | |
218 | "message": self._safeUnicodeDictionary(dictionary), | |
219 | "__eliot_logger__": logger, | |
220 | } | |
221 | log_message(**msg) | |
222 | else: | |
223 | # Nothing we can do here, raising exception to caller will | |
224 | # break business logic, better to have that continue to | |
225 | # work even if logging isn't. | |
226 | pass | |
224 | self._destinations.send(dictionary, self) | |
227 | 225 | |
228 | 226 | |
229 | 227 | def exclusively(f): |
7 | 7 | |
8 | 8 | version_json = ''' |
9 | 9 | { |
10 | "date": "2020-12-15T14:09:24-0500", | |
11 | "dirty": false, | |
12 | "error": null, | |
13 | "full-revisionid": "e858c8ef7302e22ca05f37565d929db8e0fab153", | |
14 | "version": "1.13.0" | |
10 | "date": null, | |
11 | "dirty": null, | |
12 | "error": "unable to compute version", | |
13 | "full-revisionid": null, | |
14 | "version": "0+unknown" | |
15 | 15 | } |
16 | 16 | ''' # END VERSION_JSON |
17 | 17 |
18 | 18 | return float(o) |
19 | 19 | if isinstance(o, numpy.integer): |
20 | 20 | return int(o) |
21 | if isinstance(o, (numpy.bool, numpy.bool_)): | |
21 | if isinstance(o, numpy.bool_): | |
22 | 22 | return bool(o) |
23 | 23 | if isinstance(o, numpy.ndarray): |
24 | 24 | if o.size > 10000: |
780 | 780 | }, |
781 | 781 | ) |
782 | 782 | |
783 | def test_continueTaskCustomType(self): | |
784 | """ | |
785 | L{Action.continue_task} uses the provided action type and extra fields. | |
786 | """ | |
787 | originalAction = Action(None, "uniq456", TaskLevel(level=[3, 4]), "mytype") | |
788 | taskId = originalAction.serializeTaskId() | |
789 | logger = MemoryLogger() | |
790 | ||
791 | Action.continue_task(logger, taskId, action_type="custom:action", field="value") | |
792 | assertContainsFields( | |
793 | self, | |
794 | logger.messages[0], | |
795 | { | |
796 | "task_uuid": "uniq456", | |
797 | "task_level": [3, 4, 1, 1], | |
798 | "action_type": "custom:action", | |
799 | "action_status": "started", | |
800 | "field": "value", | |
801 | }, | |
802 | ) | |
803 | ||
783 | 804 | def test_continueTaskNoLogger(self): |
784 | 805 | """ |
785 | 806 | L{Action.continue_task} can be called without a logger. |
105 | 105 | top_action_name: [ |
106 | 106 | {"eliot:remote_task": ["dask:task", "mult"]}, |
107 | 107 | {"eliot:remote_task": ["dask:task", "mult"]}, |
108 | {"eliot:remote_task": ["dask:task"]}, | |
109 | {"eliot:remote_task": ["dask:task"]}, | |
110 | 108 | {"eliot:remote_task": ["dask:task", "finally"]}, |
111 | 109 | ] |
112 | 110 | } |
118 | 116 | ( |
119 | 117 | mult1_msg, |
120 | 118 | mult2_msg, |
121 | reduce1_msg, | |
122 | reduce2_msg, | |
123 | 119 | final_msg, |
124 | 120 | ) = LoggedMessage.ofType(logger.messages, "dask:task") |
125 | 121 | self.assertEqual( |
126 | reduce1_msg.message["dependencies"], [mult1_msg.message["key"]] | |
127 | ) | |
128 | self.assertEqual( | |
129 | reduce2_msg.message["dependencies"], [mult2_msg.message["key"]] | |
130 | ) | |
131 | self.assertEqual( | |
132 | 122 | sorted(final_msg.message["dependencies"]), |
133 | sorted([reduce1_msg.message["key"], reduce2_msg.message["key"]]), | |
123 | sorted([mult1_msg.message["key"], mult2_msg.message["key"]]), | |
134 | 124 | ) |
135 | 125 | |
136 | 126 | # Make sure dependencies are logically earlier in the logs: |
137 | 127 | self.assertTrue( |
138 | mult1_msg.message["task_level"] < reduce1_msg.message["task_level"] | |
128 | mult1_msg.message["task_level"] < final_msg.message["task_level"] | |
139 | 129 | ) |
140 | 130 | self.assertTrue( |
141 | mult2_msg.message["task_level"] < reduce2_msg.message["task_level"] | |
142 | ) | |
143 | self.assertTrue( | |
144 | reduce1_msg.message["task_level"] < final_msg.message["task_level"] | |
145 | ) | |
146 | self.assertTrue( | |
147 | reduce2_msg.message["task_level"] < final_msg.message["task_level"] | |
131 | mult2_msg.message["task_level"] < final_msg.message["task_level"] | |
148 | 132 | ) |
149 | 133 | |
150 | 134 |
26 | 26 | bytesjson as json, |
27 | 27 | to_file, |
28 | 28 | FileDestination, |
29 | _DestinationsSendError, | |
29 | _safe_unicode_dictionary, | |
30 | 30 | ) |
31 | from .._action import start_action | |
31 | 32 | from .._validation import ValidationError, Field, _MessageSerializer |
32 | 33 | from .._traceback import write_traceback |
33 | 34 | from ..testing import assertContainsFields |
371 | 372 | self.assertEqual(dest2, [message]) |
372 | 373 | self.assertEqual(dest3, [message]) |
373 | 374 | |
374 | def test_destinationExceptionMultipleDestinations(self): | |
375 | def test_destination_exception_multiple_destinations(self): | |
375 | 376 | """ |
376 | 377 | If one destination throws an exception, other destinations still |
377 | 378 | get the message. |
385 | 386 | destinations.add(dest3.append) |
386 | 387 | |
387 | 388 | message = {"hello": 123} |
388 | self.assertRaises(_DestinationsSendError, destinations.send, {"hello": 123}) | |
389 | self.assertEqual((dest, dest3), ([message], [message])) | |
390 | ||
391 | def test_destinationExceptionContinue(self): | |
389 | destinations.send(message) | |
390 | self.assertIn(message, dest) | |
391 | self.assertIn(message, dest3) | |
392 | ||
393 | def test_destination_exception_continue(self): | |
392 | 394 | """ |
393 | 395 | If a destination throws an exception, future messages are still |
394 | 396 | sent to it. |
397 | 399 | dest = BadDestination() |
398 | 400 | destinations.add(dest) |
399 | 401 | |
400 | self.assertRaises(_DestinationsSendError, destinations.send, {"hello": 123}) | |
401 | destinations.send({"hello": 200}) | |
402 | self.assertEqual(dest, [{"hello": 200}]) | |
402 | msg1 = {"hello": 123} | |
403 | msg2 = {"world": 456} | |
404 | destinations.send(msg1) | |
405 | self.assertNotIn(msg1, dest) | |
406 | destinations.send(msg2) | |
407 | self.assertIn(msg2, dest) | |
403 | 408 | |
404 | 409 | def test_remove(self): |
405 | 410 | """ |
559 | 564 | logger.write(d, serializer) |
560 | 565 | self.assertEqual(d, original) |
561 | 566 | |
562 | def test_safeUnicodeDictionary(self): | |
563 | """ | |
564 | L{Logger._safeUnicodeDictionary} converts the given dictionary's | |
567 | def test_safe_unicode_dictionary(self): | |
568 | """ | |
569 | L{_safe_unicode_dictionary} converts the given dictionary's | |
565 | 570 | values and keys to unicode using C{safeunicode}. |
566 | 571 | """ |
567 | 572 | |
572 | 577 | dictionary = {badobject(): 123, 123: badobject()} |
573 | 578 | badMessage = "eliot: unknown, unicode() raised exception" |
574 | 579 | self.assertEqual( |
575 | eval(Logger()._safeUnicodeDictionary(dictionary)), | |
580 | eval(_safe_unicode_dictionary(dictionary)), | |
576 | 581 | {badMessage: "123", "123": badMessage}, |
577 | 582 | ) |
578 | 583 | |
579 | def test_safeUnicodeDictionaryFallback(self): | |
584 | def test_safe_unicode_dictionary_fallback(self): | |
580 | 585 | """ |
581 | 586 | If converting the dictionary failed for some reason, |
582 | L{Logger._safeUnicodeDictionary} runs C{repr} on the object. | |
583 | """ | |
584 | self.assertEqual(Logger()._safeUnicodeDictionary(None), "None") | |
585 | ||
586 | def test_safeUnicodeDictionaryFallbackFailure(self): | |
587 | """ | |
588 | If all else fails, L{Logger._safeUnicodeDictionary} just gives up. | |
587 | L{_safe_unicode_dictionary} runs C{repr} on the object. | |
588 | """ | |
589 | self.assertEqual(_safe_unicode_dictionary(None), "None") | |
590 | ||
591 | def test_safe_unicode_dictionary_fallback_failure(self): | |
592 | """ | |
593 | If all else fails, L{_safe_unicode_dictionary} just gives up. | |
589 | 594 | """ |
590 | 595 | |
591 | 596 | class badobject(object): |
593 | 598 | raise TypeError() |
594 | 599 | |
595 | 600 | self.assertEqual( |
596 | Logger()._safeUnicodeDictionary(badobject()), | |
601 | _safe_unicode_dictionary(badobject()), | |
597 | 602 | "eliot: unknown, unicode() raised exception", |
598 | 603 | ) |
599 | 604 | |
627 | 632 | }, |
628 | 633 | ) |
629 | 634 | self.assertIn("RuntimeError: oops", tracebackMessage["traceback"]) |
630 | # Calling _safeUnicodeDictionary multiple times leads to | |
635 | # Calling _safe_unicode_dictionary multiple times leads to | |
631 | 636 | # inconsistent results due to hash ordering, so compare contents: |
632 | 637 | assertContainsFields( |
633 | 638 | self, written[1], {"message_type": "eliot:serialization_failure"} |
637 | 642 | dict((repr(key), repr(value)) for (key, value) in message.items()), |
638 | 643 | ) |
639 | 644 | |
640 | def test_destinationExceptionCaught(self): | |
645 | def test_destination_exception_caught(self): | |
641 | 646 | """ |
642 | 647 | If a destination throws an exception, an appropriate error is |
643 | 648 | logged. |
654 | 659 | dest[0], |
655 | 660 | { |
656 | 661 | "message_type": "eliot:destination_failure", |
657 | "message": logger._safeUnicodeDictionary(message), | |
662 | "message": _safe_unicode_dictionary(message), | |
658 | 663 | "reason": "ono", |
659 | 664 | "exception": "eliot.tests.test_output.MyException", |
660 | 665 | }, |
661 | 666 | ) |
662 | 667 | |
663 | def test_destinationMultipleExceptionsCaught(self): | |
668 | def test_destination_multiple_exceptions_caught(self): | |
664 | 669 | """ |
665 | 670 | If multiple destinations throw an exception, an appropriate error is |
666 | 671 | logged for each. |
704 | 709 | message, |
705 | 710 | { |
706 | 711 | "message_type": "eliot:destination_failure", |
707 | "message": logger._safeUnicodeDictionary(message), | |
712 | "message": _safe_unicode_dictionary(message), | |
708 | 713 | "reason": "ono", |
709 | 714 | "exception": "eliot.tests.test_output.MyException", |
710 | 715 | }, |
711 | 716 | { |
712 | 717 | "message_type": "eliot:destination_failure", |
713 | "message": logger._safeUnicodeDictionary(message), | |
718 | "message": _safe_unicode_dictionary(message), | |
714 | 719 | "reason": zero_divide, |
715 | 720 | "exception": zero_type, |
716 | 721 | }, |
718 | 723 | ), |
719 | 724 | ) |
720 | 725 | |
721 | def test_destinationExceptionCaughtTwice(self): | |
726 | def test_destination_exception_caught_twice(self): | |
722 | 727 | """ |
723 | 728 | If a destination throws an exception, and the logged error about |
724 | 729 | it also causes an exception, then just drop that exception on the |
732 | 737 | |
733 | 738 | logger._destinations.add(always_raise) |
734 | 739 | |
735 | # No exception raised; since everything is dropped no other | |
736 | # assertions to be made. | |
740 | # Just a message. No exception raised; since everything is dropped no | |
741 | # other assertions to be made. | |
737 | 742 | logger.write({"hello": 123}) |
743 | ||
744 | # With an action. No exception raised; since everything is dropped no | |
745 | # other assertions to be made. | |
746 | with start_action(logger, "sys:do"): | |
747 | logger.write({"hello": 123}) | |
738 | 748 | |
739 | 749 | |
740 | 750 | class PEP8Tests(TestCase): |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: eliot |
2 | Version: 1.13.0 | |
2 | Version: 0+unknown | |
3 | 3 | Summary: Logging library that tells you why it happened |
4 | 4 | Home-page: https://github.com/itamarst/eliot/ |
5 | 5 | Maintainer: Itamar Turner-Trauring |
6 | 6 | Maintainer-email: itamar@itamarst.org |
7 | 7 | License: Apache 2.0 |
8 | Description: Eliot: Logging that tells you *why* it happened | |
9 | ================================================ | |
10 | ||
11 | .. image:: https://travis-ci.org/itamarst/eliot.png?branch=master | |
12 | :target: http://travis-ci.org/itamarst/eliot | |
13 | :alt: Build Status | |
14 | ||
15 | Python's built-in ``logging`` and other similar systems output a stream of factoids: they're interesting, but you can't really tell what's going on. | |
16 | ||
17 | * Why is your application slow? | |
18 | * What caused this code path to be chosen? | |
19 | * Why did this error happen? | |
20 | ||
21 | Standard logging can't answer these questions. | |
22 | ||
23 | But with a better model you could understand what and why things happened in your application. | |
24 | You could pinpoint performance bottlenecks, you could understand what happened when, who called what. | |
25 | ||
26 | That is what Eliot does. | |
27 | ``eliot`` is a Python logging system that outputs causal chains of **actions**: actions can spawn other actions, and eventually they either **succeed or fail**. | |
28 | The resulting logs tell you the story of what your software did: what happened, and what caused it. | |
29 | ||
30 | Eliot supports a range of use cases and 3rd party libraries: | |
31 | ||
32 | * Logging within a single process. | |
33 | * Causal tracing across a distributed system. | |
34 | * Scientific computing, with `built-in support for NumPy and Dask <https://eliot.readthedocs.io/en/stable/scientific-computing.html>`_. | |
35 | * `Asyncio and Trio coroutines <https://eliot.readthedocs.io/en/stable/generating/asyncio.html>`_ and the `Twisted networking framework <https://eliot.readthedocs.io/en/stable/generating/twisted.html>`_. | |
36 | ||
37 | Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines. | |
38 | ||
39 | Eliot supports Python 3.6, 3.7, 3.8, and 3.9, as well as PyPy3. | |
40 | It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License. | |
41 | ||
42 | Python 2.7 is in legacy support mode, with the last release supported being 1.7; see `here <https://eliot.readthedocs.io/en/stable/python2.html>`_ for details. | |
43 | ||
44 | * `Read the documentation <https://eliot.readthedocs.io>`_. | |
45 | * Download from `PyPI`_ or `conda-forge <https://anaconda.org/conda-forge/eliot>`_. | |
46 | * Need help or have any questions? `File an issue <https://github.com/itamarst/eliot/issues/new>`_ on GitHub. | |
47 | * **Commercial support** is available from `Python⇒Speed <https://pythonspeed.com/services/#eliot>`_. | |
48 | ||
49 | Testimonials | |
50 | ------------ | |
51 | ||
52 | "Eliot has made tracking down causes of failure (in complex external integrations and internal uses) tremendously easier. Our errors are logged to Sentry with the Eliot task UUID. That means we can go from a Sentry notification to a high-level trace of operations—with important metadata at each operation—in a few seconds. We immediately know which user did what in which part of the system." | |
53 | ||
54 | —Jonathan Jacobs | |
55 | ||
56 | .. _Github: https://github.com/itamarst/eliot | |
57 | .. _PyPI: https://pypi.python.org/pypi/eliot | |
58 | ||
59 | 8 | Keywords: logging |
60 | Platform: UNKNOWN | |
61 | 9 | Classifier: Intended Audience :: Developers |
62 | 10 | Classifier: License :: OSI Approved :: Apache Software License |
63 | 11 | Classifier: Operating System :: OS Independent |
67 | 15 | Classifier: Programming Language :: Python :: 3.7 |
68 | 16 | Classifier: Programming Language :: Python :: 3.8 |
69 | 17 | Classifier: Programming Language :: Python :: 3.9 |
18 | Classifier: Programming Language :: Python :: 3.10 | |
70 | 19 | Classifier: Programming Language :: Python :: Implementation :: CPython |
71 | 20 | Classifier: Programming Language :: Python :: Implementation :: PyPy |
72 | 21 | Classifier: Topic :: System :: Logging |
73 | 22 | Requires-Python: >=3.6.0 |
23 | Provides-Extra: dev | |
74 | 24 | Provides-Extra: journald |
75 | 25 | Provides-Extra: test |
76 | Provides-Extra: dev | |
26 | License-File: LICENSE | |
27 | ||
28 | Eliot: Logging that tells you *why* it happened | |
29 | ================================================ | |
30 | ||
31 | .. image:: https://travis-ci.org/itamarst/eliot.png?branch=master | |
32 | :target: http://travis-ci.org/itamarst/eliot | |
33 | :alt: Build Status | |
34 | ||
35 | Python's built-in ``logging`` and other similar systems output a stream of factoids: they're interesting, but you can't really tell what's going on. | |
36 | ||
37 | * Why is your application slow? | |
38 | * What caused this code path to be chosen? | |
39 | * Why did this error happen? | |
40 | ||
41 | Standard logging can't answer these questions. | |
42 | ||
43 | But with a better model you could understand what and why things happened in your application. | |
44 | You could pinpoint performance bottlenecks, you could understand what happened when, who called what. | |
45 | ||
46 | That is what Eliot does. | |
47 | ``eliot`` is a Python logging system that outputs causal chains of **actions**: actions can spawn other actions, and eventually they either **succeed or fail**. | |
48 | The resulting logs tell you the story of what your software did: what happened, and what caused it. | |
49 | ||
50 | Eliot supports a range of use cases and 3rd party libraries: | |
51 | ||
52 | * Logging within a single process. | |
53 | * Causal tracing across a distributed system. | |
54 | * Scientific computing, with `built-in support for NumPy and Dask <https://eliot.readthedocs.io/en/stable/scientific-computing.html>`_. | |
55 | * `Asyncio and Trio coroutines <https://eliot.readthedocs.io/en/stable/generating/asyncio.html>`_ and the `Twisted networking framework <https://eliot.readthedocs.io/en/stable/generating/twisted.html>`_. | |
56 | ||
57 | Eliot is only used to generate your logs; you will might need tools like Logstash and ElasticSearch to aggregate and store logs if you are using multiple processes across multiple machines. | |
58 | ||
59 | Eliot supports Python 3.6, 3.7, 3.8, 3.9, and 3.10, as well as PyPy3. | |
60 | It is maintained by Itamar Turner-Trauring, and released under the Apache 2.0 License. | |
61 | ||
62 | * `Read the documentation <https://eliot.readthedocs.io>`_. | |
63 | * Download from `PyPI`_ or `conda-forge <https://anaconda.org/conda-forge/eliot>`_. | |
64 | * Need help or have any questions? `File an issue <https://github.com/itamarst/eliot/issues/new>`_ on GitHub. | |
65 | * **Commercial support** is available from `Python⇒Speed <https://pythonspeed.com/services/#eliot>`_. | |
66 | ||
67 | Testimonials | |
68 | ------------ | |
69 | ||
70 | "Eliot has made tracking down causes of failure (in complex external integrations and internal uses) tremendously easier. Our errors are logged to Sentry with the Eliot task UUID. That means we can go from a Sentry notification to a high-level trace of operations—with important metadata at each operation—in a few seconds. We immediately know which user did what in which part of the system." | |
71 | ||
72 | —Jonathan Jacobs | |
73 | ||
74 | .. _Github: https://github.com/itamarst/eliot | |
75 | .. _PyPI: https://pypi.python.org/pypi/eliot |
0 | boltons>=19.0.1 | |
1 | pyrsistent>=0.11.8 | |
0 | 2 | six |
1 | 3 | zope.interface |
2 | pyrsistent>=0.11.8 | |
3 | boltons>=19.0.1 | |
4 | 4 | |
5 | 5 | [:python_version < "3.7" and python_version > "2.7"] |
6 | 6 | aiocontextvars |
7 | 7 | |
8 | 8 | [dev] |
9 | black | |
10 | coverage | |
11 | flake8 | |
9 | 12 | setuptools>=40 |
10 | twine>=1.12.1 | |
11 | coverage | |
12 | 13 | sphinx |
13 | 14 | sphinx_rtd_theme |
14 | flake8 | |
15 | black | |
15 | twine>=1.12.1 | |
16 | 16 | |
17 | 17 | [journald] |
18 | 18 | cffi>=1.1.2 |
19 | 19 | |
20 | 20 | [test] |
21 | 21 | hypothesis>=1.14.0 |
22 | testtools | |
23 | 22 | pytest |
24 | 23 | pytest-xdist |
24 | testtools |
4 | 4 | license_file = LICENSE |
5 | 5 | |
6 | 6 | [versioneer] |
7 | vcs = git | |
7 | VCS = git | |
8 | 8 | style = pep440 |
9 | 9 | versionfile_source = eliot/_version.py |
10 | 10 | versionfile_build = eliot/_version.py |
11 | 11 | tag_prefix = |
12 | 12 | parentdir_prefix = eliot- |
13 | 13 | |
14 | [flake8] | |
15 | max-line-length = 88 | |
16 | extend-ignore = E203 | |
17 | ||
14 | 18 | [egg_info] |
15 | 19 | tag_build = |
16 | 20 | tag_date = 0 |
21 | 21 | "Programming Language :: Python :: 3.7", |
22 | 22 | "Programming Language :: Python :: 3.8", |
23 | 23 | "Programming Language :: Python :: 3.9", |
24 | "Programming Language :: Python :: 3.10", | |
24 | 25 | "Programming Language :: Python :: Implementation :: CPython", |
25 | 26 | "Programming Language :: Python :: Implementation :: PyPy", |
26 | 27 | "Topic :: System :: Logging", |