systemd-logging 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: systemd-logging
3
+ Version: 1.0.2
4
+ Summary: Simplifies logging for systemd
5
+ Project-URL: Homepage, https://github.com/idlesign/systemd-logging
6
+ Author-email: Igor Starikov <idlesign@yandex.ru>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Keywords: journal,logging,systemd
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: ctyped>=0.7.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ # systemd-logging
26
+
27
+ <https://github.com/idlesign/systemd-logging>
28
+
29
+ [![release](https://img.shields.io/pypi/v/systemd-logging.svg)](https://pypi.python.org/pypi/systemd-logging)
30
+ [![lic](https://img.shields.io/pypi/l/systemd-logging.svg)](https://pypi.python.org/pypi/systemd-logging)
31
+ [![coverage](https://img.shields.io/coveralls/idlesign/systemd-logging/master.svg)](https://coveralls.io/r/idlesign/systemd-logging)
32
+
33
+ ## Description
34
+
35
+ *Simplifies logging for systemd*
36
+
37
+ **Requires Python 3.10+**
38
+
39
+ * No need to compile (pure Python), uses `libsystemd.so`.
40
+ * Simplified configuration.
41
+ * Just logging. Nothing more.
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ import logging
47
+
48
+ from systemdlogging.toolbox import init_systemd_logging
49
+
50
+ # This one line in most cases would be enough.
51
+ # By default it attaches systemd logging handler to a root Python logger.
52
+ init_systemd_logging() # Returns True if initialization went fine.
53
+
54
+ # Now you can use logging as usual.
55
+ logger = logging.getLogger(__name__)
56
+ logger.setLevel(logging.DEBUG)
57
+
58
+ logger.debug('My debug message')
59
+
60
+ try:
61
+ raise ValueError('Log me please')
62
+
63
+ except ValueError:
64
+ # Additional context can be passed in extra.context.
65
+ logger.exception('Something terrible just happened', extra={
66
+ 'message_id': True, # Generate message ID automatically.
67
+ 'context': {
68
+ 'FIELD1': 'one',
69
+ 'FIELD2': 'two',
70
+ }
71
+ }, stack_info=True)
72
+ ```
@@ -0,0 +1,7 @@
1
+ systemdlogging/__init__.py,sha256=vANbxWqJ8cObeyPJkH6BJy4jnBYu3ExbKEi0OllMAH0,18
2
+ systemdlogging/backend_ctyped.py,sha256=SVOiGY1Sico75GyhPV_k4PHRuF4YEGFR3I-kdr5MXS8,632
3
+ systemdlogging/toolbox.py,sha256=1TkZjn6PaOeLNRkY3mbZKQxFDJNrIh9Brx8GBSzXCnk,6020
4
+ systemd_logging-1.0.2.dist-info/METADATA,sha256=MY4TU3-k8AqPiOdRErAb0mesVcU5mXnpRV6_NkLTWek,2342
5
+ systemd_logging-1.0.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
6
+ systemd_logging-1.0.2.dist-info/licenses/LICENSE,sha256=-xlA3bnU7uvYqzNxgTYUlDOBlW7_2KyJ1JsvHIGBgAU,1508
7
+ systemd_logging-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2019-2026, Igor `idle sign` Starikov
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the systemd-logging nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
@@ -0,0 +1 @@
1
+ VERSION = '1.0.2'
@@ -0,0 +1,33 @@
1
+ import logging
2
+
3
+ from ctyped.exceptions import CtypedException
4
+ from ctyped.toolbox import Library
5
+
6
+ LOGGER = logging.getLogger(__name__)
7
+
8
+
9
+ def send_stub(*fmt): # pragma: nocover
10
+ """Dummy send function to be used when the real one
11
+ is not available (e.g. no libsystemd found).
12
+
13
+ """
14
+ return None
15
+
16
+
17
+ try:
18
+ lib = Library('libsystemd.so', prefix='sd_journal_')
19
+
20
+ except CtypedException: # pragma: nocover
21
+
22
+ send = send_stub
23
+
24
+ LOGGER.warning("systemd library is not available and won't be used for logging")
25
+
26
+ else:
27
+
28
+ @lib.function()
29
+ def send(*fmt) -> int: # pragma: nocover
30
+ ...
31
+
32
+
33
+ lib.bind_types()
@@ -0,0 +1,193 @@
1
+ import logging
2
+ import os
3
+ import syslog
4
+ import uuid
5
+
6
+ from .backend_ctyped import send, send_stub
7
+
8
+ PRIORITY_MAP = {
9
+ logging.CRITICAL: syslog.LOG_CRIT,
10
+ logging.ERROR: syslog.LOG_ERR,
11
+ logging.WARNING: syslog.LOG_WARNING,
12
+ logging.INFO: syslog.LOG_INFO,
13
+ logging.DEBUG: syslog.LOG_DEBUG,
14
+ }
15
+
16
+
17
+ def log_message(*, level: int, msg: str, context: dict | None = None) -> bool:
18
+ """Sends a message to systemd.
19
+
20
+ .. warning:: Low level function. You may want to
21
+ use more convenient `init_systemd_logging()` or `SystemdHandler` and `SystemdFormatter`.
22
+
23
+ Natively supported fields:
24
+ http://0pointer.de/public/systemd-man/systemd.journal-fields.html
25
+
26
+ :param level: Logging level.
27
+ :param msg: Message text to send.
28
+ :param context: Additional context to send within message.
29
+
30
+ """
31
+ args = {
32
+ 'MESSAGE': msg,
33
+ 'PRIORITY': PRIORITY_MAP.get(level, level),
34
+ }
35
+
36
+ args.update(context or {})
37
+
38
+ return send(
39
+ # escape % in values to prevent segfaults
40
+ # while trying to format things like %s, %d, etc.
41
+ *[f'{key}={val}'.replace('%', '%%').encode() for key, val in args.items()],
42
+ None
43
+ ) == 0
44
+
45
+
46
+ class SystemdHandler(logging.Handler):
47
+ """Allows passing log messages to systemd.
48
+
49
+ Additional information may be passed in ``extra`` dictionary:
50
+
51
+ * context - Dictionary with additional information to attach to a message.
52
+ * message_id - Message ID (usually UUID to assign to the message).
53
+ If ``True`` ID will be generated automatically
54
+
55
+ .. code-block: python
56
+
57
+ logger.exception('This is my exceptional log entry.', extra={
58
+ 'message_id': True,
59
+ 'context': {
60
+ 'MY_FIELD': 'My value',
61
+ }
62
+ }, stack_info=True)
63
+
64
+ Fills in the following fields automatically:
65
+
66
+ * CODE_FILE - Filename
67
+ * CODE_LINE - Code line number
68
+ * CODE_FUNC - Code function name
69
+ * CODE_MODULE - Python module name
70
+ * LOGGER - Logger name
71
+ * THREAD_ID - Thread ID if any
72
+ * THREAD_NAME - Thread name if any
73
+ * PROCESS_NAME - Process name if any
74
+ * PRIORITY - Priority based on logging level
75
+
76
+ Optionally fills in:
77
+ * TRACEBACK - Traceback information if available
78
+ * STACK - Stacktrace information if available
79
+ * MESSAGE_ID - Message ID (if context.message_id=True)
80
+
81
+ """
82
+
83
+ syslog_id: str = ''
84
+ """SYSLOG_IDENTIFIER to add to message. If not set command name is used."""
85
+
86
+ def emit(self, record: logging.LogRecord):
87
+
88
+ context = {
89
+ # Generic
90
+ 'CODE_FILE': record.pathname,
91
+ 'CODE_LINE': record.lineno,
92
+ 'CODE_FUNC': record.funcName,
93
+ # Custom
94
+ 'CODE_MODULE': record.module,
95
+ 'LOGGER': record.name,
96
+ 'THREAD_ID': record.thread,
97
+ 'THREAD_NAME': record.threadName,
98
+ 'PROCESS_NAME': record.processName,
99
+ }
100
+
101
+ try:
102
+ exc_info = record.exc_info
103
+ if exc_info:
104
+ context['TRACEBACK'] = self.formatter.formatException(exc_info)
105
+
106
+ stack_info = record.stack_info
107
+ if stack_info:
108
+ context['STACK'] = self.formatter.formatStack(stack_info)
109
+
110
+ syslog_id = self.syslog_id
111
+ if syslog_id:
112
+ context['SYSLOG_IDENTIFIER'] = syslog_id
113
+
114
+ msg = self.format(record)
115
+
116
+ message_id = getattr(record, 'message_id', None)
117
+
118
+ if message_id:
119
+
120
+ if message_id is True:
121
+ # Generate message ID automatically.
122
+ message_id = uuid.uuid3(uuid.NAMESPACE_OID, '|'.join(str(val) for val in (
123
+ msg, record.levelno, record.pathname, record.funcName
124
+ ))).hex
125
+
126
+ context['MESSAGE_ID'] = message_id
127
+
128
+ context_ = getattr(record, 'context', {})
129
+ context.update(context_)
130
+
131
+ log_message(level=record.levelno, msg=msg, context=context)
132
+
133
+ except Exception: # pragma: nocover
134
+ self.handleError(record)
135
+
136
+
137
+ class SystemdFormatter(logging.Formatter):
138
+ """Formatter for systemd."""
139
+
140
+ def format(self, record):
141
+ # This one is just like its parent just doesn't
142
+ # append traceback etc. to the message.
143
+ record.message = record.getMessage()
144
+ return self.formatMessage(record)
145
+
146
+
147
+ def check_for_systemd() -> bool:
148
+ """Checks whether systemd library is available and
149
+ the current process is run under systemd (and thus its output
150
+ is connected to journald).
151
+
152
+ """
153
+ if send is send_stub:
154
+ return False
155
+
156
+ journal_stream = os.environ.get("JOURNAL_STREAM") # systemd v231+
157
+
158
+ if journal_stream:
159
+ # Output streams might have been replaced by a wrapping process,
160
+ # so verify that stdout/stderr are still connected to the journal.
161
+ return any(
162
+ f"{stat.st_dev}:{stat.st_ino}" == journal_stream
163
+ for stat in (os.fstat(fd) for fd in (1, 2))
164
+ )
165
+
166
+ return bool(os.environ.get("INVOCATION_ID")) # Works with systemd v232+
167
+
168
+
169
+ def init_systemd_logging(*, logger: logging.Logger | None = None, syslog_id: str = '') -> bool:
170
+ """Initializes logging to send log messages to systemd.
171
+
172
+ Returns boolean indicating initialization went fine (see also `check_for_systemd()`).
173
+
174
+ If it wasn't one may either to fallback to another logging handler
175
+ or leave it as it is possibly sacrificing some log context.
176
+
177
+ :param logger: Logger to attach systemd logging handler to.
178
+ If not set handler is attached to a root logger.
179
+
180
+ :param syslog_id: Value to be used in SYSLOG_IDENTIFIER message field.
181
+
182
+ """
183
+ systemd_ok = check_for_systemd()
184
+
185
+ if systemd_ok:
186
+ handler = SystemdHandler()
187
+ handler.syslog_id = syslog_id
188
+ handler.setFormatter(SystemdFormatter())
189
+
190
+ logger = logger or logging.getLogger()
191
+ logger.addHandler(handler)
192
+
193
+ return systemd_ok