Skip to content

Commit 52032a5

Browse files
authored
Support crt logs in 'logging' module (#728)
1 parent 6468ae9 commit 52032a5

6 files changed

Lines changed: 569 additions & 10 deletions

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,50 @@ Please note that on Mac, once a private key is used with a certificate, that cer
5757
static: certificate has an existing certificate-key pair that was previously imported into the Keychain. Using key from Keychain instead of the one provided.
5858
```
5959

60+
## Logging
61+
62+
aws-crt-python provides two ways to configure logging from the CRT's native C libraries.
63+
64+
### Option 1: Python logging integration (recommended)
65+
66+
Route CRT log messages through Python's standard `logging` module. Each CRT subsystem gets a hierarchical logger under `awscrt` (e.g. `awscrt.io.event-loop`, `awscrt.http.http-connection`, `awscrt.s3.S3Client`), so you can filter by package or subsystem.
67+
68+
```python
69+
import logging
70+
from awscrt.logging import init_logging, CRT_LOG_FORMAT
71+
72+
handler = logging.StreamHandler()
73+
handler.setFormatter(logging.Formatter(CRT_LOG_FORMAT))
74+
logging.getLogger('awscrt').addHandler(handler)
75+
init_logging(logging.DEBUG)
76+
```
77+
78+
To filter to a specific CRT package:
79+
80+
```python
81+
# Only show IO logs at DEBUG, silence everything else
82+
logging.getLogger('awscrt').setLevel(logging.WARNING)
83+
logging.getLogger('awscrt.io').setLevel(logging.DEBUG)
84+
```
85+
86+
### Option 2: Direct file/stdout logging
87+
88+
Write CRT log output directly to a file, stdout, or stderr. This bypasses Python's logging module and is useful for quick debugging.
89+
90+
```python
91+
from awscrt.io import LogLevel, init_logging
92+
93+
init_logging(LogLevel.Debug, 'stdout') # or 'stderr', or a file path
94+
```
95+
96+
### Which should I use?
97+
98+
Use `awscrt.logging.init_logging` (Option 1) if you want CRT logs integrated with your application's existing Python logging setup, with per-subsystem filtering.
99+
100+
Use `awscrt.io.init_logging` (Option 2) if you just want to dump all CRT logs to a file or console quickly.
101+
102+
These are mutually exclusive — use one or the other, not both.
103+
60104
## Crash Handler
61105

62106
You can enable the crash handler by setting the environment variable `AWS_CRT_CRASH_HANDLER=1` . This will print the callstack to `stderr` in the event of a fatal error.

awscrt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'crypto',
1010
'http',
1111
'io',
12+
'logging',
1213
'mqtt',
1314
'mqtt5',
1415
'mqtt_request_response',

awscrt/logging.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0.
3+
4+
"""Python logging integration for the AWS CRT."""
5+
6+
import logging
7+
import threading
8+
from enum import IntEnum
9+
10+
import _awscrt
11+
from awscrt.io import LogLevel
12+
13+
__all__ = ['init_logging', 'set_log_level', 'logf', 'LogSubject', 'CRT_LOG_FORMAT']
14+
15+
_CRT_TO_PY_LEVEL = {
16+
0: logging.NOTSET,
17+
1: logging.CRITICAL,
18+
2: logging.ERROR,
19+
3: logging.WARNING,
20+
4: logging.INFO,
21+
5: logging.DEBUG,
22+
6: logging.DEBUG,
23+
}
24+
25+
_PY_TO_CRT_LEVEL = {
26+
logging.NOTSET: LogLevel.NoLogs,
27+
logging.CRITICAL: LogLevel.Fatal,
28+
logging.ERROR: LogLevel.Error,
29+
logging.WARNING: LogLevel.Warn,
30+
logging.INFO: LogLevel.Info,
31+
logging.DEBUG: LogLevel.Trace,
32+
}
33+
34+
# Default Format supported by native loggers.
35+
CRT_LOG_FORMAT = '%(asctime)s [%(threadName)s] %(levelname)s %(name)s - %(message)s'
36+
37+
38+
class LogSubject(IntEnum):
39+
"""Log subject identifiers for CRT subsystems."""
40+
# aws-c-common (package ID 0)
41+
COMMON_GENERAL = 0x000
42+
COMMON_TASK_SCHEDULER = 0x001
43+
44+
# aws-c-io (package ID 1)
45+
IO_GENERAL = 0x400
46+
IO_EVENT_LOOP = 0x401
47+
IO_SOCKET = 0x402
48+
IO_SOCKET_HANDLER = 0x403
49+
IO_TLS = 0x404
50+
IO_ALPN = 0x405
51+
IO_DNS = 0x406
52+
IO_PKI = 0x407
53+
IO_CHANNEL = 0x408
54+
IO_CHANNEL_BOOTSTRAP = 0x409
55+
IO_FILE_UTILS = 0x40A
56+
IO_SHARED_LIBRARY = 0x40B
57+
58+
# aws-c-http (package ID 2)
59+
HTTP_GENERAL = 0x800
60+
HTTP_CONNECTION = 0x801
61+
HTTP_SERVER = 0x802
62+
HTTP_STREAM = 0x803
63+
HTTP_CONNECTION_MANAGER = 0x804
64+
HTTP_WEBSOCKET = 0x805
65+
HTTP_WEBSOCKET_SETUP = 0x806
66+
67+
# aws-c-event-stream (package ID 4)
68+
EVENT_STREAM_GENERAL = 0x1000
69+
EVENT_STREAM_CHANNEL_HANDLER = 0x1001
70+
EVENT_STREAM_RPC_SERVER = 0x1002
71+
EVENT_STREAM_RPC_CLIENT = 0x1003
72+
73+
# aws-c-mqtt (package ID 5)
74+
MQTT_GENERAL = 0x1400
75+
MQTT_CLIENT = 0x1401
76+
MQTT_TOPIC_TREE = 0x1402
77+
78+
# aws-c-auth (package ID 6)
79+
AUTH_GENERAL = 0x1800
80+
AUTH_PROFILE = 0x1801
81+
AUTH_CREDENTIALS_PROVIDER = 0x1802
82+
AUTH_SIGNING = 0x1803
83+
84+
# aws-c-cal (package ID 7)
85+
CAL_GENERAL = 0x1C00
86+
CAL_ECC = 0x1C01
87+
CAL_HASH = 0x1C02
88+
CAL_HMAC = 0x1C03
89+
CAL_DER = 0x1C04
90+
CAL_LIBCRYPTO_RESOLVE = 0x1C05
91+
CAL_RSA = 0x1C06
92+
CAL_ED25519 = 0x1C07
93+
94+
# aws-c-s3 (package ID 14)
95+
S3_GENERAL = 0x3800
96+
S3_CLIENT = 0x3801
97+
98+
# aws-c-sdkutils (package ID 15)
99+
SDKUTILS_GENERAL = 0x3C00
100+
SDKUTILS_PROFILE = 0x3C01
101+
SDKUTILS_ENDPOINTS_PARSING = 0x3C02
102+
SDKUTILS_ENDPOINTS_RESOLVE = 0x3C03
103+
SDKUTILS_ENDPOINTS_GENERAL = 0x3C04
104+
SDKUTILS_PARTITIONS_PARSING = 0x3C05
105+
SDKUTILS_ENDPOINTS_REGEX = 0x3C06
106+
107+
108+
_PACKAGE_ID_TO_MODULE = {
109+
0: 'common',
110+
1: 'io',
111+
2: 'http',
112+
4: 'event-stream',
113+
5: 'mqtt',
114+
6: 'auth',
115+
7: 'cal',
116+
14: 's3',
117+
15: 'sdkutils',
118+
}
119+
120+
121+
def _python_logging_callback(crt_level, message, subject_id, subject_name, thread_name):
122+
"""Called from C for each CRT log message."""
123+
module = _PACKAGE_ID_TO_MODULE.get(subject_id >> 10, 'unknown')
124+
logger = logging.getLogger('awscrt.{}.{}'.format(module, subject_name))
125+
py_level = _CRT_TO_PY_LEVEL.get(crt_level, logging.DEBUG)
126+
# `There is the possibility that “dummy thread objects” are created. These are
127+
# thread objects corresponding to “alien threads”, which are threads of control
128+
# started outside the threading module, such as directly from C code.`
129+
# https://docs.python.org/3/library/threading.html#thread-objects
130+
#
131+
# As per current conventions, dummy thread objects have thread names
132+
# starting with "Dummy". Eg. Dummy-1, Dummy-8
133+
if threading.current_thread().name.startswith("Dummy") and thread_name is not None:
134+
threading.current_thread().name = thread_name
135+
logger.log(py_level, '%s', message)
136+
137+
138+
def init_logging(level: int = logging.DEBUG):
139+
"""Initialize CRT logging, routing output through Python's logging module.
140+
141+
Log messages appear under the ``awscrt`` logger hierarchy, with each CRT
142+
subsystem as a child logger (e.g. ``awscrt.io.event-loop``,
143+
``awscrt.s3.S3Client``).
144+
145+
This is mutually exclusive with :func:`~awscrt.io.init_logging` -- use one
146+
or the other, not both. Can only be called once.
147+
148+
Example usage::
149+
150+
import logging
151+
from awscrt.logging import init_logging
152+
153+
logging.basicConfig(level=logging.DEBUG)
154+
init_logging(logging.DEBUG)
155+
156+
Args:
157+
level (int): Python logging level (e.g. ``logging.DEBUG``,
158+
``logging.WARNING``).
159+
160+
Raises:
161+
RuntimeError: If CRT logging has already been initialized.
162+
"""
163+
root_logger = logging.getLogger('awscrt')
164+
165+
try:
166+
crt_level = _PY_TO_CRT_LEVEL[level]
167+
except KeyError:
168+
raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")
169+
170+
if root_logger.level == logging.NOTSET:
171+
root_logger.setLevel(_CRT_TO_PY_LEVEL.get(int(crt_level), logging.DEBUG))
172+
173+
_awscrt.init_python_logging(int(crt_level), _python_logging_callback)
174+
175+
176+
def set_log_level(level: int):
177+
"""Change the CRT log level. :func:`init_logging` must have been called first.
178+
179+
Set log level to logging.NOTSET to disable the logger. Cleaning up the logger
180+
is dangerous in a multi-threaded environment.
181+
182+
Args:
183+
level (int): Python logging level (e.g. ``logging.DEBUG``,
184+
``logging.WARNING``).
185+
"""
186+
try:
187+
crt_level = _PY_TO_CRT_LEVEL[level]
188+
except KeyError:
189+
raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")
190+
_awscrt.set_log_level(int(crt_level))
191+
192+
193+
def log(level: int, subject: LogSubject, message: str):
194+
"""Log a message through the CRT's configured logger.
195+
196+
Args:
197+
level (int): Python logging level (e.g. logging.DEBUG, logging.INFO).
198+
subject (LogSubject): Log subject identifying the subsystem.
199+
message (str): The message to log.
200+
"""
201+
try:
202+
crt_level = _PY_TO_CRT_LEVEL[level]
203+
except KeyError:
204+
raise ValueError(f"Invalid log level: {level}. Use logging.DEBUG, INFO, WARNING, ERROR, or CRITICAL")
205+
_awscrt.logger_log(int(crt_level), int(subject), message)

source/io.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ PyObject *aws_py_init_logging(PyObject *self, PyObject *args);
2727
* Change logging level of the global logger.
2828
*/
2929
PyObject *aws_py_set_log_level(PyObject *self, PyObject *args);
30+
PyObject *aws_py_init_python_logging(PyObject *self, PyObject *args);
3031

3132
/**
3233
* Returns True if ALPN is available, False if it is not.

0 commit comments

Comments
 (0)