Skip to content

Commit cb57026

Browse files
committed
Version 1.1.0: restructure Handler to better match output types
1 parent 14b5e88 commit cb57026

File tree

6 files changed

+101
-56
lines changed

6 files changed

+101
-56
lines changed

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ Please note that if `is_supported()` returns `False` then none of the module's o
2121

2222

2323
## Usage
24-
Pyoslog currently provides the following methods:
24+
25+
```python
26+
import pyoslog
27+
if pyoslog.is_supported():
28+
pyoslog.log('This is an OS_LOG_TYPE_DEFAULT message via pyoslog')
29+
```
30+
31+
### Available methods
32+
Pyoslog provides the following methods from Apple's [unified logging header](https://opensource.apple.com/source/xnu/xnu-3789.21.4/libkern/os/log.h.auto.html):
2533
- [`os_log_create`](https://developer.apple.com/documentation/os/1643744-os_log_create)
2634
- [`os_log_type_enabled`](https://developer.apple.com/documentation/os/1643749-os_log_type_enabled) (and [`info`](https://developer.apple.com/documentation/os/os_log_info_enabled)/[`debug`](https://developer.apple.com/documentation/os/os_log_debug_enabled) variants)
2735
- [`os_log_with_type`](https://developer.apple.com/documentation/os/os_log_with_type)
@@ -73,16 +81,14 @@ Use the pyoslog `Handler` to direct messages to pyoslog:
7381

7482
```python
7583
import logging, pyoslog
76-
logger = logging.getLogger('My app name')
77-
logger.setLevel(logging.DEBUG)
7884
handler = pyoslog.Handler()
7985
handler.setSubsystem('org.example.your-app', 'filter-category')
86+
logger = logging.getLogger()
8087
logger.addHandler(handler)
81-
logger.debug('message')
88+
logger.error('message')
8289
```
8390

84-
To configure the Handler's output type, use `handler.setLevel` with a level from the logging module.
85-
These are mapped internally to the `OS_LOG_TYPE` values – for example, `handler.setLevel(logging.DEBUG)` will configure the Handler to output messages of type `OS_LOG_TYPE_DEBUG`.
91+
Logger levels are mapped internally to the `OS_LOG_TYPE_*` values – for example, `logger.debug('message')` will generate a message of type `OS_LOG_TYPE_DEBUG`.
8692

8793
### Receiving log messages
8894
Logs can be viewed using Console.app or the `log` command.
@@ -107,7 +113,7 @@ The pyoslog module handles this for you – there is no need to `del` or release
107113

108114

109115
## Limitations
110-
As noted above, while the macOS `os_log` API allows use of a format string with many methods, this name is required to be a C string literal.
116+
As noted above, while the macOS `os_log` API allows use of a format string with many methods, this parameter is required to be a C string literal.
111117
As a result, pyoslog hardcodes all format strings to `"%{public}s"`.
112118

113119

build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
set -Eeuo pipefail
33

44
# this build script is quite forceful about setup - make sure not to mess up the system python
5-
PYTHON_VENV=$(python -c "import sys; sys.stdout.write('1') if hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix else sys.stdout.write('0')")
5+
PYTHON_VENV=$(python3 -c "import sys; sys.stdout.write('1') if hasattr(sys, 'real_prefix') or sys.base_prefix != sys.prefix else sys.stdout.write('0')")
66
if [ "$PYTHON_VENV" == 0 ]; then
77
echo 'Warning: not running in a Python virtual environment. Please either activate a venv or edit the script to confirm this action'
88
exit 1

docs/reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ Handler
5656

5757
.. autoclass:: pyoslog.Handler
5858
:members:
59+
:exclude-members: emit

pyoslog/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = 'pyoslog'
2-
__version__ = '1.0.1'
2+
__version__ = '1.1.0'
33
__description__ = 'Send messages to the macOS unified logging system (os_log)'
44
__author__ = 'Simon Robinson'
55
__author_email__ = '[email protected]'

pyoslog/handler.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from typing import Union
32

43
from .core import *
54

@@ -8,39 +7,36 @@
87

98

109
class Handler(logging.Handler):
11-
"""This logging Handler simply forwards all messages to pyoslog"""
10+
"""This logging Handler forwards all messages to pyoslog. The logging level (set as normal via :py:func:`setLevel`)
11+
is converted to the matching pyoslog.OS_LOG_TYPE_* type, and messages outputted to the unified log."""
1212

1313
def __init__(self) -> None:
1414
"""Initialise a Handler instance, logging to OS_LOG_DEFAULT at OS_LOG_TYPE_DEFAULT"""
1515
logging.Handler.__init__(self)
16-
1716
self._log_object = OS_LOG_DEFAULT
18-
self._log_type = OS_LOG_TYPE_DEFAULT
19-
20-
def setLevel(self, level: Union[int, str]) -> None:
21-
"""Sets the log level, mapping logging.<level> to pyoslog.OS_LOG_TYPE_<equivalent level>."""
22-
super().setLevel(level) # normalises level Union[int, str] to int
23-
24-
pyoslog_type = OS_LOG_TYPE_DEFAULT # logging.NOTSET or unmatched value
25-
26-
if self.level >= logging.CRITICAL:
27-
pyoslog_type = OS_LOG_TYPE_FAULT
28-
elif self.level >= logging.ERROR:
29-
pyoslog_type = OS_LOG_TYPE_ERROR
30-
elif self.level >= logging.WARNING:
31-
pyoslog_type = OS_LOG_TYPE_DEFAULT
32-
elif self.level >= logging.INFO:
33-
pyoslog_type = OS_LOG_TYPE_INFO
34-
elif self.level >= logging.DEBUG:
35-
pyoslog_type = OS_LOG_TYPE_DEBUG
36-
37-
self._log_type = pyoslog_type
3817

18+
@staticmethod
19+
def _get_pyoslog_type(level: int) -> int:
20+
if level >= logging.CRITICAL:
21+
return OS_LOG_TYPE_FAULT
22+
elif level >= logging.ERROR:
23+
return OS_LOG_TYPE_ERROR
24+
elif level >= logging.WARNING:
25+
return OS_LOG_TYPE_DEFAULT
26+
elif level >= logging.INFO:
27+
return OS_LOG_TYPE_INFO
28+
elif level >= logging.DEBUG:
29+
return OS_LOG_TYPE_DEBUG
30+
31+
return OS_LOG_TYPE_DEFAULT # logging.NOTSET
32+
33+
# named to match logging class norms rather than PEP 8 recommendations
3934
# noinspection PyPep8Naming
4035
def setSubsystem(self, subsystem: str, category: str = 'default') -> None:
41-
"""Sets the subsystem (in reverse DNS notation), and optionally a category to allow further filtering."""
36+
"""Sets the subsystem (typically reverse DNS notation), and optionally a category to allow further filtering."""
4237
self._log_object = os_log_create(subsystem, category)
4338

4439
def emit(self, record: logging.LogRecord) -> None:
45-
"""Emit a record, sending its contents to pyoslog."""
46-
os_log_with_type(self._log_object, self._log_type, self.format(record))
40+
"""Emit a record, sending its contents to pyoslog at a matching level to our own. (note: excluded from built
41+
documentation as this method is not intended to be called directly.)"""
42+
os_log_with_type(self._log_object, Handler._get_pyoslog_type(record.levelno), self.format(record))

tests/test_handler.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,38 +32,80 @@ def setUp(self):
3232

3333
self.handler = pyoslog.Handler()
3434
self.assertEqual(self.handler._log_object, pyoslog.OS_LOG_DEFAULT)
35-
self.assertEqual(self.handler._log_type, pyoslog.OS_LOG_TYPE_DEFAULT)
35+
self.assertEqual(self.handler.level, logging.NOTSET)
36+
37+
# far more thorough testing is in test_setup.py (setSubsystem essentially duplicates os_log_create)
38+
self.handler.setSubsystem(pyoslog_test_globals.LOG_SUBSYSTEM, pyoslog_test_globals.LOG_CATEGORY)
39+
self.assertIsInstance(self.handler._log_object, pyoslog_core.os_log_t)
40+
self.assertEqual(str(self.handler._log_object), '<os_log_t (%s:%s)>' % (pyoslog_test_globals.LOG_SUBSYSTEM,
41+
pyoslog_test_globals.LOG_CATEGORY))
42+
43+
self.logger = logging.getLogger('Pyoslog test logger')
44+
self.logger.addHandler(self.handler)
45+
46+
# so that we don't receive framework messages (e.g., 'PlugIn CFBundle 0x122f9cf90 </System/Library/Frameworks/
47+
# OSLog.framework> (framework, loaded) is now unscheduled for unloading')
48+
self.logger.setLevel(logging.DEBUG)
3649

3750
# noinspection PyUnresolvedReferences
3851
log_scope = OSLog.OSLogStoreScope(OSLog.OSLogStoreCurrentProcessIdentifier)
3952
# noinspection PyUnresolvedReferences
4053
self.log_store, error = OSLog.OSLogStore.storeWithScope_error_(log_scope, None)
4154
self.assertIsNone(error)
4255

43-
def test_setLevel(self):
44-
for invalid_type in [None, [], (0, 1, 2), {'key': 'value'}]:
45-
self.assertRaises(TypeError, self.handler.setLevel, invalid_type)
56+
def test_emit(self):
57+
logging_methods = [
58+
(self.logger.debug, pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_DEBUG),
59+
(self.logger.info, pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_INFO),
60+
(self.logger.warning, pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_DEFAULT),
61+
(self.logger.error, pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_ERROR),
62+
(self.logger.critical, pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_FAULT)
63+
]
64+
for log_method, log_type in logging_methods:
65+
sent_message = 'Handler message via %s / 0x%x (%s)' % (log_method, log_type.value, log_type)
66+
log_method(sent_message)
67+
received_message = pyoslog_test_globals.get_latest_log_message(self.log_store)
68+
69+
# see test_logging.py for further details about this approach
70+
if pyoslog.os_log_type_enabled(self.handler._log_object, log_type.value):
71+
self.assertEqual(pyoslog_test_globals.oslog_level_to_type(received_message.level()), log_type)
72+
self.assertEqual(received_message.subsystem(), pyoslog_test_globals.LOG_SUBSYSTEM)
73+
self.assertEqual(received_message.category(), pyoslog_test_globals.LOG_CATEGORY)
74+
self.assertEqual(received_message.composedMessage(), sent_message)
75+
else:
76+
print('Skipped: custom log object Handler tests with disabled type 0x%x (%s)' % (
77+
log_type.value, log_type))
4678

47-
for level in [logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]:
48-
self.handler.setLevel(level)
49-
self.assertEqual(self.handler._log_type, pyoslog_test_globals.logging_level_to_type(level))
79+
sent_message = 'Handler message via logger.exception()'
80+
self.logger.exception(sent_message, exc_info=False) # intended to only be called from an exception
81+
received_message = pyoslog_test_globals.get_latest_log_message(self.log_store)
82+
self.assertEqual(pyoslog_test_globals.oslog_level_to_type(received_message.level()),
83+
pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_ERROR)
84+
self.assertEqual(received_message.subsystem(), pyoslog_test_globals.LOG_SUBSYSTEM)
85+
self.assertEqual(received_message.category(), pyoslog_test_globals.LOG_CATEGORY)
86+
self.assertEqual(received_message.composedMessage(), sent_message)
5087

51-
self.handler.setLevel(level + 5) # the logging module allows any positive integers, default 0-50
52-
self.assertEqual(self.handler._log_type, pyoslog_test_globals.logging_level_to_type(level))
88+
# logging.NOTSET should map to OS_LOG_TYPE_DEFAULT (but we don't test output as explained above)
89+
self.assertEqual(self.handler._get_pyoslog_type(logging.NOTSET),
90+
pyoslog_test_globals.TestLogTypes.OS_LOG_TYPE_DEFAULT)
5391

54-
def test_setSubsystem(self):
55-
# far more thorough testing is in test_setup.py (setSubsystem essentially duplicates os_log_create)
56-
self.handler.setSubsystem(pyoslog_test_globals.LOG_SUBSYSTEM, pyoslog_test_globals.LOG_CATEGORY)
57-
self.assertIsInstance(self.handler._log_object, pyoslog_core.os_log_t)
58-
self.assertEqual(str(self.handler._log_object), '<os_log_t (%s:%s)>' % (pyoslog_test_globals.LOG_SUBSYSTEM,
59-
pyoslog_test_globals.LOG_CATEGORY))
92+
logging_levels = [logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]
93+
for log_level in logging_levels:
94+
expected_type = pyoslog_test_globals.logging_level_to_type(log_level)
95+
sent_message = 'Handler message via logger.log() at level %d / 0x%x (%s)' % (
96+
log_level, expected_type.value, expected_type)
97+
self.logger.log(log_level, sent_message)
98+
received_message = pyoslog_test_globals.get_latest_log_message(self.log_store)
6099

61-
def test_emit(self):
62-
# far more thorough testing is in test_logging.py (emit essentially duplicates os_log_with_type)
63-
sent_record = logging.LogRecord('test', logging.DEBUG, '', 0, 'Handler log message', None, None)
64-
self.handler.emit(sent_record)
65-
received_message = pyoslog_test_globals.get_latest_log_message(self.log_store)
66-
self.assertEqual(received_message.composedMessage(), sent_record.message)
100+
# see test_logging.py for further details about this approach
101+
if pyoslog.os_log_type_enabled(self.handler._log_object, expected_type.value):
102+
self.assertEqual(pyoslog_test_globals.oslog_level_to_type(received_message.level()), expected_type)
103+
self.assertEqual(received_message.subsystem(), pyoslog_test_globals.LOG_SUBSYSTEM)
104+
self.assertEqual(received_message.category(), pyoslog_test_globals.LOG_CATEGORY)
105+
self.assertEqual(received_message.composedMessage(), sent_message)
106+
else:
107+
print('Skipped: custom log object Handler tests with disabled type 0x%x (%s)' % (
108+
expected_type.value, expected_type))
67109

68110

69111
if __name__ == '__main__':

0 commit comments

Comments
 (0)