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
|
+
[](https://pypi.python.org/pypi/systemd-logging)
|
|
30
|
+
[](https://pypi.python.org/pypi/systemd-logging)
|
|
31
|
+
[](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,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
|