async-safe-logger 0.1.3__tar.gz

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,583 @@
1
+ Metadata-Version: 2.3
2
+ Name: async-safe-logger
3
+ Version: 0.1.3
4
+ Summary: A simple and secure logging library for async operations
5
+ Author: ZHUZIOK
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: System :: Logging
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Async Safe Logger
22
+
23
+ [中文文档](README_zh-CN.md)
24
+
25
+ A simple, thread-safe, and asyncio-friendly logging wrapper built on top of Python's standard `logging` module.
26
+
27
+ `AsyncSafeLogger` moves log formatting, file I/O, log rotation, retention cleanup, and console output to a dedicated background thread using `QueueHandler` and `QueueListener`.
28
+
29
+ Your application code continues to use Python's standard logging API:
30
+
31
+ ```python
32
+ import logging
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ logger.info("Hello, world!")
37
+ ```
38
+
39
+ No `await` is required for normal logging calls.
40
+
41
+ ---
42
+
43
+ ## Features
44
+
45
+ * **Built on Python's standard `logging` module**
46
+ * **Asyncio-friendly** — logging calls do not perform file or console I/O on the event-loop thread
47
+ * **Thread-safe** — safe to use from multiple threads and coroutines
48
+ * **Background logging** — formatting and output are handled by a dedicated `QueueListener` thread
49
+ * **Automatic log rotation** — rotate log files when they reach the configured size
50
+ * **Log retention cleanup** — automatically remove expired log files
51
+ * **Console and file output** — independently configurable
52
+ * **Multiple independent logger instances** — no required global mutable state
53
+ * **Synchronous and asynchronous lifecycle APIs**
54
+ * **Context manager support**
55
+ * **Zero runtime dependencies** — only Python's standard library is required
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+
61
+ * Python **3.13+**
62
+
63
+ The library currently has no third-party runtime dependencies.
64
+
65
+ ---
66
+
67
+ ## Installation
68
+
69
+ ### Using pip
70
+
71
+ ```bash
72
+ pip install async-safe-logger
73
+ ```
74
+
75
+ ### Using uv
76
+
77
+ If you use [uv](https://docs.astral.sh/uv/), add the package to your project with:
78
+
79
+ ```bash
80
+ uv add async-safe-logger
81
+ ```
82
+
83
+ Or install it directly into the current environment:
84
+
85
+ ```bash
86
+ uv pip install async-safe-logger
87
+ ```
88
+
89
+ ---
90
+
91
+ # Quick Start
92
+
93
+ The simplest usage is to configure the logger and continue using Python's standard `logging` API.
94
+
95
+ ```python
96
+ import logging
97
+
98
+ from async_safe_logger import setup_logging, stop_logging
99
+
100
+ setup_logging()
101
+
102
+ logger = logging.getLogger(__name__)
103
+
104
+ logger.info("Hello, world!")
105
+ logger.warning("Something may be wrong.")
106
+ logger.error("Something went wrong.")
107
+
108
+ stop_logging()
109
+ ```
110
+
111
+ By default, logs are:
112
+
113
+ * written to `logs/app.log`
114
+ * printed to stdout
115
+ * rotated when the file reaches 2 MB
116
+ * retained according to the configured retention policy
117
+
118
+ ---
119
+
120
+ # Asyncio Usage
121
+
122
+ `AsyncSafeLogger` is designed for applications using `asyncio`.
123
+
124
+ Normal logging calls remain synchronous from the caller's perspective:
125
+
126
+ ```python
127
+ import asyncio
128
+ import logging
129
+
130
+ from async_safe_logger import setup_logging, stop_logging_async
131
+
132
+
133
+ async def main():
134
+ setup_logging()
135
+
136
+ logger = logging.getLogger(__name__)
137
+
138
+ logger.info("Running inside asyncio")
139
+ logger.warning("This logging call does not write directly to disk.")
140
+
141
+ try:
142
+ ...
143
+ finally:
144
+ await stop_logging_async()
145
+
146
+
147
+ asyncio.run(main())
148
+ ```
149
+
150
+ ### Why `stop_logging_async()`?
151
+
152
+ Stopping a `QueueListener` may wait for its background thread to finish processing queued log records.
153
+
154
+ The synchronous:
155
+
156
+ ```python
157
+ stop_logging()
158
+ ```
159
+
160
+ may therefore block while the listener shuts down.
161
+
162
+ When running inside an asyncio event loop, prefer:
163
+
164
+ ```python
165
+ await stop_logging_async()
166
+ ```
167
+
168
+ This moves the potentially blocking shutdown operation to a worker thread.
169
+
170
+ ---
171
+
172
+ # How It Works
173
+
174
+ The library uses Python's standard `QueueHandler` and `QueueListener`.
175
+
176
+ The normal logging path looks approximately like this:
177
+
178
+ ```text
179
+ Application
180
+
181
+ │ logger.info(...)
182
+
183
+ logging.Logger
184
+
185
+
186
+ QueueHandler
187
+
188
+ │ put LogRecord into memory queue
189
+
190
+ queue.Queue
191
+
192
+
193
+ QueueListener
194
+
195
+ │ background thread
196
+
197
+ ┌───────────────┬────────────────┐
198
+ │ FileHandler │ ConsoleHandler │
199
+ └───────────────┴────────────────┘
200
+ │ │
201
+ ▼ ▼
202
+ app.log stdout
203
+ ```
204
+
205
+ The important point is that the application thread or asyncio event-loop thread does not directly perform:
206
+
207
+ * file writes
208
+ * console writes
209
+ * log rotation
210
+ * expired-log cleanup
211
+
212
+ Those operations are handled by the background listener thread.
213
+
214
+ This keeps the normal logging path lightweight and avoids performing blocking output operations directly from an asyncio event loop.
215
+
216
+ ---
217
+
218
+ # Configuration
219
+
220
+ The default logger can be configured through `setup_logging()`.
221
+
222
+ For example:
223
+
224
+ ```python
225
+ from async_safe_logger import setup_logging
226
+
227
+ setup_logging(
228
+ log_dir="logs",
229
+ log_filename="app.log",
230
+ level=logging.INFO,
231
+ max_bytes=2 * 1024 * 1024,
232
+ backup_count=20,
233
+ retention_days=5,
234
+ file=True,
235
+ console=True,
236
+ )
237
+ ```
238
+
239
+ ## Configuration Options
240
+
241
+ | Parameter | Default | Description |
242
+ | ---------------- | --------------------: | ----------------------------------------------- |
243
+ | `log_dir` | `"logs"` | Directory used for log files |
244
+ | `log_filename` | `"app.log"` | Log file name |
245
+ | `level` | `logging.INFO` | Minimum logging level |
246
+ | `max_bytes` | `2 MB` | Maximum size of a log file before rotation |
247
+ | `backup_count` | `20` | Number of rotated log files to keep |
248
+ | `retention_days` | `5` | Delete log files older than this number of days |
249
+ | `file` | `True` | Enable file logging |
250
+ | `console` | `True` | Enable console logging |
251
+ | `fmt` | See below | Log message format |
252
+ | `datefmt` | `"%Y-%m-%d %H:%M:%S"` | Timestamp format |
253
+
254
+ Default format:
255
+
256
+ ```text
257
+ %(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s
258
+ ```
259
+
260
+ For example:
261
+
262
+ ```text
263
+ 2026-09-06 20:30:15 | INFO | myapp.main:main:42 - Server started
264
+ ```
265
+
266
+ ---
267
+
268
+ # File and Console Output
269
+
270
+ File and console logging can be independently enabled.
271
+
272
+ ### File only
273
+
274
+ ```python
275
+ AsyncSafeLogger(
276
+ file=True,
277
+ console=False,
278
+ )
279
+ ```
280
+
281
+ ### Console only
282
+
283
+ ```python
284
+ AsyncSafeLogger(
285
+ file=False,
286
+ console=True,
287
+ )
288
+ ```
289
+
290
+ ### File and console
291
+
292
+ ```python
293
+ AsyncSafeLogger(
294
+ file=True,
295
+ console=True,
296
+ )
297
+ ```
298
+
299
+ This is also the default configuration.
300
+
301
+ For safety, the following configuration is rejected:
302
+
303
+ ```python
304
+ AsyncSafeLogger(
305
+ file=False,
306
+ console=False,
307
+ )
308
+ ```
309
+
310
+ It raises `ValueError` instead of silently discarding all log messages.
311
+
312
+ ---
313
+
314
+ # Multiple Independent Logger Systems
315
+
316
+ You can create multiple `AsyncSafeLogger` instances when different subsystems need different configurations.
317
+
318
+ For example:
319
+
320
+ ```python
321
+ import logging
322
+
323
+ from async_safe_logger import AsyncSafeLogger
324
+
325
+
326
+ business_logger = AsyncSafeLogger(
327
+ log_dir="logs/business",
328
+ log_filename="business.log",
329
+ level=logging.INFO,
330
+ )
331
+
332
+ debug_logger = AsyncSafeLogger(
333
+ log_dir="logs/debug",
334
+ log_filename="debug.log",
335
+ level=logging.DEBUG,
336
+ )
337
+
338
+ business_logger.setup()
339
+ debug_logger.setup()
340
+
341
+ logging.getLogger("business").info("Business event")
342
+ logging.getLogger("debug").debug("Debug information")
343
+
344
+ # ...
345
+
346
+ business_logger.stop()
347
+ debug_logger.stop()
348
+ ```
349
+
350
+ Each instance owns its own queue, listener, and handlers.
351
+
352
+ ---
353
+
354
+ # Context Manager
355
+
356
+ `AsyncSafeLogger` supports both synchronous and asynchronous context managers.
357
+
358
+ ## Synchronous
359
+
360
+ ```python
361
+ import logging
362
+
363
+ from async_safe_logger import AsyncSafeLogger
364
+
365
+
366
+ with AsyncSafeLogger() as logger:
367
+ logging.info("Inside the context manager")
368
+ ```
369
+
370
+ The logger is automatically stopped when leaving the `with` block.
371
+
372
+ ## Asynchronous
373
+
374
+ ```python
375
+ import logging
376
+
377
+ from async_safe_logger import AsyncSafeLogger
378
+
379
+
380
+ async with AsyncSafeLogger() as logger:
381
+ logging.info("Inside the async context manager")
382
+ ```
383
+
384
+ When leaving the `async with` block, the logger is shut down asynchronously.
385
+
386
+ This is the recommended pattern when the surrounding application is already asynchronous.
387
+
388
+ ---
389
+
390
+ # Standard Logging Compatibility
391
+
392
+ The main goal of this library is to keep application code compatible with Python's standard logging API.
393
+
394
+ You can continue to use:
395
+
396
+ ```python
397
+ import logging
398
+
399
+ logger = logging.getLogger(__name__)
400
+
401
+ logger.debug("Debug message")
402
+ logger.info("Information")
403
+ logger.warning("Warning")
404
+ logger.error("Error")
405
+ logger.critical("Critical error")
406
+ ```
407
+
408
+ There is no need to replace your application's logging calls with a custom API such as:
409
+
410
+ ```python
411
+ await logger.info(...)
412
+ ```
413
+
414
+ Instead, logging remains familiar:
415
+
416
+ ```python
417
+ logger.info(...)
418
+ ```
419
+
420
+ while the output pipeline is handled asynchronously in the background.
421
+
422
+ ---
423
+
424
+ # Log Rotation and Retention
425
+
426
+ The file handler combines size-based rotation with automatic retention cleanup.
427
+
428
+ For example:
429
+
430
+ ```python
431
+ AsyncSafeLogger(
432
+ log_dir="logs",
433
+ log_filename="app.log",
434
+ max_bytes=2 * 1024 * 1024,
435
+ backup_count=20,
436
+ retention_days=5,
437
+ )
438
+ ```
439
+
440
+ When `app.log` reaches the configured size, it is rotated according to `RotatingFileHandler`.
441
+
442
+ After rotation, log files older than the configured retention period are removed.
443
+
444
+ ---
445
+
446
+ # Graceful Shutdown
447
+
448
+ It is important to stop the logger before the application exits so that queued log records have an opportunity to be processed.
449
+
450
+ For synchronous applications:
451
+
452
+ ```python
453
+ stop_logging()
454
+ ```
455
+
456
+ For asyncio applications:
457
+
458
+ ```python
459
+ await stop_logging_async()
460
+ ```
461
+
462
+ A typical asyncio application should use:
463
+
464
+ ```python
465
+ async def main():
466
+ setup_logging()
467
+
468
+ try:
469
+ ...
470
+ finally:
471
+ await stop_logging_async()
472
+ ```
473
+
474
+ This is especially important for applications that produce logs immediately before shutdown.
475
+
476
+ ---
477
+
478
+ # Design Goals
479
+
480
+ The project intentionally focuses on a small set of goals:
481
+
482
+ ### 1. Keep the standard logging API
483
+
484
+ No custom logging syntax is required.
485
+
486
+ ```python
487
+ logger.info("hello")
488
+ ```
489
+
490
+ remains the primary interface.
491
+
492
+ ### 2. Keep blocking output away from the event loop
493
+
494
+ The application produces a `LogRecord`, which is placed into an in-memory queue.
495
+
496
+ The listener thread performs the actual output work.
497
+
498
+ ### 3. Minimize runtime dependencies
499
+
500
+ The current implementation uses only Python's standard library.
501
+
502
+ ### 4. Support both synchronous and asynchronous applications
503
+
504
+ The same logger can be used by:
505
+
506
+ * normal synchronous applications
507
+ * asyncio applications
508
+ * applications using both threads and asyncio
509
+
510
+ ### 5. Keep the implementation simple
511
+
512
+ The library intentionally builds on well-tested components already provided by Python:
513
+
514
+ * `logging`
515
+ * `QueueHandler`
516
+ * `QueueListener`
517
+ * `RotatingFileHandler`
518
+ * `queue.Queue`
519
+ * `threading`
520
+
521
+ Rather than implementing a completely new logging framework.
522
+
523
+ ---
524
+
525
+ # Important Notes
526
+
527
+ ## Logging is not completely "non-blocking"
528
+
529
+ `AsyncSafeLogger` moves output operations away from the calling thread, but the initial logging operation still performs normal Python logging work and enqueues a `LogRecord`.
530
+
531
+ Therefore, it should be understood as:
532
+
533
+ > **asyncio-friendly / async-safe logging output**
534
+
535
+ rather than a mathematically guaranteed zero-cost or lock-free logging implementation.
536
+
537
+ ## Queue Backpressure
538
+
539
+ The internal queue is currently unbounded.
540
+
541
+ This means normal logging calls do not wait for the background listener to consume records.
542
+
543
+ However, an application that continuously produces logs significantly faster than the output destination can consume them may accumulate an increasing number of `LogRecord` objects in memory.
544
+
545
+ For high-volume logging workloads, monitor queue growth and application memory usage.
546
+
547
+ ## Shutdown
548
+
549
+ Always stop the logger during application shutdown:
550
+
551
+ ```python
552
+ await stop_logging_async()
553
+ ```
554
+
555
+ for asyncio applications, or:
556
+
557
+ ```python
558
+ stop_logging()
559
+ ```
560
+
561
+ for synchronous applications.
562
+
563
+ ---
564
+
565
+ # Project Status
566
+
567
+ This project is currently in an early development stage.
568
+
569
+ API design and implementation details may change before the `1.0.0` release.
570
+
571
+ The current version is:
572
+
573
+ ```text
574
+ 0.1.0
575
+ ```
576
+
577
+ ---
578
+
579
+ # License
580
+
581
+ MIT License.
582
+
583
+ See [`LICENSE`](LICENSE) for details.
@@ -0,0 +1,563 @@
1
+ # Async Safe Logger
2
+
3
+ [中文文档](README_zh-CN.md)
4
+
5
+ A simple, thread-safe, and asyncio-friendly logging wrapper built on top of Python's standard `logging` module.
6
+
7
+ `AsyncSafeLogger` moves log formatting, file I/O, log rotation, retention cleanup, and console output to a dedicated background thread using `QueueHandler` and `QueueListener`.
8
+
9
+ Your application code continues to use Python's standard logging API:
10
+
11
+ ```python
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ logger.info("Hello, world!")
17
+ ```
18
+
19
+ No `await` is required for normal logging calls.
20
+
21
+ ---
22
+
23
+ ## Features
24
+
25
+ * **Built on Python's standard `logging` module**
26
+ * **Asyncio-friendly** — logging calls do not perform file or console I/O on the event-loop thread
27
+ * **Thread-safe** — safe to use from multiple threads and coroutines
28
+ * **Background logging** — formatting and output are handled by a dedicated `QueueListener` thread
29
+ * **Automatic log rotation** — rotate log files when they reach the configured size
30
+ * **Log retention cleanup** — automatically remove expired log files
31
+ * **Console and file output** — independently configurable
32
+ * **Multiple independent logger instances** — no required global mutable state
33
+ * **Synchronous and asynchronous lifecycle APIs**
34
+ * **Context manager support**
35
+ * **Zero runtime dependencies** — only Python's standard library is required
36
+
37
+ ---
38
+
39
+ ## Requirements
40
+
41
+ * Python **3.13+**
42
+
43
+ The library currently has no third-party runtime dependencies.
44
+
45
+ ---
46
+
47
+ ## Installation
48
+
49
+ ### Using pip
50
+
51
+ ```bash
52
+ pip install async-safe-logger
53
+ ```
54
+
55
+ ### Using uv
56
+
57
+ If you use [uv](https://docs.astral.sh/uv/), add the package to your project with:
58
+
59
+ ```bash
60
+ uv add async-safe-logger
61
+ ```
62
+
63
+ Or install it directly into the current environment:
64
+
65
+ ```bash
66
+ uv pip install async-safe-logger
67
+ ```
68
+
69
+ ---
70
+
71
+ # Quick Start
72
+
73
+ The simplest usage is to configure the logger and continue using Python's standard `logging` API.
74
+
75
+ ```python
76
+ import logging
77
+
78
+ from async_safe_logger import setup_logging, stop_logging
79
+
80
+ setup_logging()
81
+
82
+ logger = logging.getLogger(__name__)
83
+
84
+ logger.info("Hello, world!")
85
+ logger.warning("Something may be wrong.")
86
+ logger.error("Something went wrong.")
87
+
88
+ stop_logging()
89
+ ```
90
+
91
+ By default, logs are:
92
+
93
+ * written to `logs/app.log`
94
+ * printed to stdout
95
+ * rotated when the file reaches 2 MB
96
+ * retained according to the configured retention policy
97
+
98
+ ---
99
+
100
+ # Asyncio Usage
101
+
102
+ `AsyncSafeLogger` is designed for applications using `asyncio`.
103
+
104
+ Normal logging calls remain synchronous from the caller's perspective:
105
+
106
+ ```python
107
+ import asyncio
108
+ import logging
109
+
110
+ from async_safe_logger import setup_logging, stop_logging_async
111
+
112
+
113
+ async def main():
114
+ setup_logging()
115
+
116
+ logger = logging.getLogger(__name__)
117
+
118
+ logger.info("Running inside asyncio")
119
+ logger.warning("This logging call does not write directly to disk.")
120
+
121
+ try:
122
+ ...
123
+ finally:
124
+ await stop_logging_async()
125
+
126
+
127
+ asyncio.run(main())
128
+ ```
129
+
130
+ ### Why `stop_logging_async()`?
131
+
132
+ Stopping a `QueueListener` may wait for its background thread to finish processing queued log records.
133
+
134
+ The synchronous:
135
+
136
+ ```python
137
+ stop_logging()
138
+ ```
139
+
140
+ may therefore block while the listener shuts down.
141
+
142
+ When running inside an asyncio event loop, prefer:
143
+
144
+ ```python
145
+ await stop_logging_async()
146
+ ```
147
+
148
+ This moves the potentially blocking shutdown operation to a worker thread.
149
+
150
+ ---
151
+
152
+ # How It Works
153
+
154
+ The library uses Python's standard `QueueHandler` and `QueueListener`.
155
+
156
+ The normal logging path looks approximately like this:
157
+
158
+ ```text
159
+ Application
160
+
161
+ │ logger.info(...)
162
+
163
+ logging.Logger
164
+
165
+
166
+ QueueHandler
167
+
168
+ │ put LogRecord into memory queue
169
+
170
+ queue.Queue
171
+
172
+
173
+ QueueListener
174
+
175
+ │ background thread
176
+
177
+ ┌───────────────┬────────────────┐
178
+ │ FileHandler │ ConsoleHandler │
179
+ └───────────────┴────────────────┘
180
+ │ │
181
+ ▼ ▼
182
+ app.log stdout
183
+ ```
184
+
185
+ The important point is that the application thread or asyncio event-loop thread does not directly perform:
186
+
187
+ * file writes
188
+ * console writes
189
+ * log rotation
190
+ * expired-log cleanup
191
+
192
+ Those operations are handled by the background listener thread.
193
+
194
+ This keeps the normal logging path lightweight and avoids performing blocking output operations directly from an asyncio event loop.
195
+
196
+ ---
197
+
198
+ # Configuration
199
+
200
+ The default logger can be configured through `setup_logging()`.
201
+
202
+ For example:
203
+
204
+ ```python
205
+ from async_safe_logger import setup_logging
206
+
207
+ setup_logging(
208
+ log_dir="logs",
209
+ log_filename="app.log",
210
+ level=logging.INFO,
211
+ max_bytes=2 * 1024 * 1024,
212
+ backup_count=20,
213
+ retention_days=5,
214
+ file=True,
215
+ console=True,
216
+ )
217
+ ```
218
+
219
+ ## Configuration Options
220
+
221
+ | Parameter | Default | Description |
222
+ | ---------------- | --------------------: | ----------------------------------------------- |
223
+ | `log_dir` | `"logs"` | Directory used for log files |
224
+ | `log_filename` | `"app.log"` | Log file name |
225
+ | `level` | `logging.INFO` | Minimum logging level |
226
+ | `max_bytes` | `2 MB` | Maximum size of a log file before rotation |
227
+ | `backup_count` | `20` | Number of rotated log files to keep |
228
+ | `retention_days` | `5` | Delete log files older than this number of days |
229
+ | `file` | `True` | Enable file logging |
230
+ | `console` | `True` | Enable console logging |
231
+ | `fmt` | See below | Log message format |
232
+ | `datefmt` | `"%Y-%m-%d %H:%M:%S"` | Timestamp format |
233
+
234
+ Default format:
235
+
236
+ ```text
237
+ %(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s
238
+ ```
239
+
240
+ For example:
241
+
242
+ ```text
243
+ 2026-09-06 20:30:15 | INFO | myapp.main:main:42 - Server started
244
+ ```
245
+
246
+ ---
247
+
248
+ # File and Console Output
249
+
250
+ File and console logging can be independently enabled.
251
+
252
+ ### File only
253
+
254
+ ```python
255
+ AsyncSafeLogger(
256
+ file=True,
257
+ console=False,
258
+ )
259
+ ```
260
+
261
+ ### Console only
262
+
263
+ ```python
264
+ AsyncSafeLogger(
265
+ file=False,
266
+ console=True,
267
+ )
268
+ ```
269
+
270
+ ### File and console
271
+
272
+ ```python
273
+ AsyncSafeLogger(
274
+ file=True,
275
+ console=True,
276
+ )
277
+ ```
278
+
279
+ This is also the default configuration.
280
+
281
+ For safety, the following configuration is rejected:
282
+
283
+ ```python
284
+ AsyncSafeLogger(
285
+ file=False,
286
+ console=False,
287
+ )
288
+ ```
289
+
290
+ It raises `ValueError` instead of silently discarding all log messages.
291
+
292
+ ---
293
+
294
+ # Multiple Independent Logger Systems
295
+
296
+ You can create multiple `AsyncSafeLogger` instances when different subsystems need different configurations.
297
+
298
+ For example:
299
+
300
+ ```python
301
+ import logging
302
+
303
+ from async_safe_logger import AsyncSafeLogger
304
+
305
+
306
+ business_logger = AsyncSafeLogger(
307
+ log_dir="logs/business",
308
+ log_filename="business.log",
309
+ level=logging.INFO,
310
+ )
311
+
312
+ debug_logger = AsyncSafeLogger(
313
+ log_dir="logs/debug",
314
+ log_filename="debug.log",
315
+ level=logging.DEBUG,
316
+ )
317
+
318
+ business_logger.setup()
319
+ debug_logger.setup()
320
+
321
+ logging.getLogger("business").info("Business event")
322
+ logging.getLogger("debug").debug("Debug information")
323
+
324
+ # ...
325
+
326
+ business_logger.stop()
327
+ debug_logger.stop()
328
+ ```
329
+
330
+ Each instance owns its own queue, listener, and handlers.
331
+
332
+ ---
333
+
334
+ # Context Manager
335
+
336
+ `AsyncSafeLogger` supports both synchronous and asynchronous context managers.
337
+
338
+ ## Synchronous
339
+
340
+ ```python
341
+ import logging
342
+
343
+ from async_safe_logger import AsyncSafeLogger
344
+
345
+
346
+ with AsyncSafeLogger() as logger:
347
+ logging.info("Inside the context manager")
348
+ ```
349
+
350
+ The logger is automatically stopped when leaving the `with` block.
351
+
352
+ ## Asynchronous
353
+
354
+ ```python
355
+ import logging
356
+
357
+ from async_safe_logger import AsyncSafeLogger
358
+
359
+
360
+ async with AsyncSafeLogger() as logger:
361
+ logging.info("Inside the async context manager")
362
+ ```
363
+
364
+ When leaving the `async with` block, the logger is shut down asynchronously.
365
+
366
+ This is the recommended pattern when the surrounding application is already asynchronous.
367
+
368
+ ---
369
+
370
+ # Standard Logging Compatibility
371
+
372
+ The main goal of this library is to keep application code compatible with Python's standard logging API.
373
+
374
+ You can continue to use:
375
+
376
+ ```python
377
+ import logging
378
+
379
+ logger = logging.getLogger(__name__)
380
+
381
+ logger.debug("Debug message")
382
+ logger.info("Information")
383
+ logger.warning("Warning")
384
+ logger.error("Error")
385
+ logger.critical("Critical error")
386
+ ```
387
+
388
+ There is no need to replace your application's logging calls with a custom API such as:
389
+
390
+ ```python
391
+ await logger.info(...)
392
+ ```
393
+
394
+ Instead, logging remains familiar:
395
+
396
+ ```python
397
+ logger.info(...)
398
+ ```
399
+
400
+ while the output pipeline is handled asynchronously in the background.
401
+
402
+ ---
403
+
404
+ # Log Rotation and Retention
405
+
406
+ The file handler combines size-based rotation with automatic retention cleanup.
407
+
408
+ For example:
409
+
410
+ ```python
411
+ AsyncSafeLogger(
412
+ log_dir="logs",
413
+ log_filename="app.log",
414
+ max_bytes=2 * 1024 * 1024,
415
+ backup_count=20,
416
+ retention_days=5,
417
+ )
418
+ ```
419
+
420
+ When `app.log` reaches the configured size, it is rotated according to `RotatingFileHandler`.
421
+
422
+ After rotation, log files older than the configured retention period are removed.
423
+
424
+ ---
425
+
426
+ # Graceful Shutdown
427
+
428
+ It is important to stop the logger before the application exits so that queued log records have an opportunity to be processed.
429
+
430
+ For synchronous applications:
431
+
432
+ ```python
433
+ stop_logging()
434
+ ```
435
+
436
+ For asyncio applications:
437
+
438
+ ```python
439
+ await stop_logging_async()
440
+ ```
441
+
442
+ A typical asyncio application should use:
443
+
444
+ ```python
445
+ async def main():
446
+ setup_logging()
447
+
448
+ try:
449
+ ...
450
+ finally:
451
+ await stop_logging_async()
452
+ ```
453
+
454
+ This is especially important for applications that produce logs immediately before shutdown.
455
+
456
+ ---
457
+
458
+ # Design Goals
459
+
460
+ The project intentionally focuses on a small set of goals:
461
+
462
+ ### 1. Keep the standard logging API
463
+
464
+ No custom logging syntax is required.
465
+
466
+ ```python
467
+ logger.info("hello")
468
+ ```
469
+
470
+ remains the primary interface.
471
+
472
+ ### 2. Keep blocking output away from the event loop
473
+
474
+ The application produces a `LogRecord`, which is placed into an in-memory queue.
475
+
476
+ The listener thread performs the actual output work.
477
+
478
+ ### 3. Minimize runtime dependencies
479
+
480
+ The current implementation uses only Python's standard library.
481
+
482
+ ### 4. Support both synchronous and asynchronous applications
483
+
484
+ The same logger can be used by:
485
+
486
+ * normal synchronous applications
487
+ * asyncio applications
488
+ * applications using both threads and asyncio
489
+
490
+ ### 5. Keep the implementation simple
491
+
492
+ The library intentionally builds on well-tested components already provided by Python:
493
+
494
+ * `logging`
495
+ * `QueueHandler`
496
+ * `QueueListener`
497
+ * `RotatingFileHandler`
498
+ * `queue.Queue`
499
+ * `threading`
500
+
501
+ Rather than implementing a completely new logging framework.
502
+
503
+ ---
504
+
505
+ # Important Notes
506
+
507
+ ## Logging is not completely "non-blocking"
508
+
509
+ `AsyncSafeLogger` moves output operations away from the calling thread, but the initial logging operation still performs normal Python logging work and enqueues a `LogRecord`.
510
+
511
+ Therefore, it should be understood as:
512
+
513
+ > **asyncio-friendly / async-safe logging output**
514
+
515
+ rather than a mathematically guaranteed zero-cost or lock-free logging implementation.
516
+
517
+ ## Queue Backpressure
518
+
519
+ The internal queue is currently unbounded.
520
+
521
+ This means normal logging calls do not wait for the background listener to consume records.
522
+
523
+ However, an application that continuously produces logs significantly faster than the output destination can consume them may accumulate an increasing number of `LogRecord` objects in memory.
524
+
525
+ For high-volume logging workloads, monitor queue growth and application memory usage.
526
+
527
+ ## Shutdown
528
+
529
+ Always stop the logger during application shutdown:
530
+
531
+ ```python
532
+ await stop_logging_async()
533
+ ```
534
+
535
+ for asyncio applications, or:
536
+
537
+ ```python
538
+ stop_logging()
539
+ ```
540
+
541
+ for synchronous applications.
542
+
543
+ ---
544
+
545
+ # Project Status
546
+
547
+ This project is currently in an early development stage.
548
+
549
+ API design and implementation details may change before the `1.0.0` release.
550
+
551
+ The current version is:
552
+
553
+ ```text
554
+ 0.1.0
555
+ ```
556
+
557
+ ---
558
+
559
+ # License
560
+
561
+ MIT License.
562
+
563
+ See [`LICENSE`](LICENSE) for details.
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "async-safe-logger"
3
+ version = "0.1.3"
4
+ description = "A simple and secure logging library for async operations"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ dependencies = []
8
+ classifiers = [
9
+ "Development Status :: 3 - Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3 :: Only",
14
+ "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: System :: Logging",
21
+ ]
22
+
23
+ [[project.authors]]
24
+ name = "ZHUZIOK"
25
+
26
+ [build-system]
27
+ requires = ["uv_build>=0.11.0"]
28
+ build-backend = "uv_build"
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "async-safe-logger"
3
+ version = "0.1.3"
4
+ description = "A simple and secure logging library for async operations"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ dependencies = []
8
+
9
+ authors = [
10
+ { name = "ZHUZIOK" }
11
+ ]
12
+
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: System :: Logging",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["uv_build>=0.11.0"]
30
+ build-backend = "uv_build"
@@ -0,0 +1,5 @@
1
+ from .logger import AsyncSafeLogger
2
+
3
+ __all__ = [
4
+ "AsyncSafeLogger",
5
+ ]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import queue
6
+ import sys
7
+ import threading
8
+ from datetime import datetime, timedelta
9
+ from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
10
+ from pathlib import Path
11
+ from typing import Optional, Union
12
+
13
+
14
+ class _DailyCleanupRotatingFileHandler(RotatingFileHandler):
15
+ __slots__ = (
16
+ "retention_days",
17
+ "_log_dir",
18
+ "_log_stem",
19
+ )
20
+
21
+ def __init__(self, *args, retention_days: int = 5, **kwargs) -> None:
22
+ super().__init__(*args, **kwargs)
23
+ self.retention_days = retention_days
24
+ base_path = Path(self.baseFilename)
25
+ self._log_dir = base_path.parent
26
+ self._log_stem = base_path.name
27
+
28
+ def doRollover(self) -> None:
29
+ super().doRollover()
30
+ self._cleanup_old_logs()
31
+
32
+ def _cleanup_old_logs(self) -> None:
33
+ expire_time = datetime.now() - timedelta(days=self.retention_days)
34
+ for path in self._log_dir.glob(f"{self._log_stem}*"):
35
+ try:
36
+ mtime = datetime.fromtimestamp(path.stat().st_mtime)
37
+ if mtime < expire_time:
38
+ path.unlink()
39
+ except OSError:
40
+ pass
41
+
42
+
43
+ class AsyncSafeLogger:
44
+
45
+ __slots__ = (
46
+ "log_dir",
47
+ "log_file",
48
+ "level",
49
+ "max_bytes",
50
+ "backup_count",
51
+ "retention_days",
52
+ "file",
53
+ "console",
54
+ "fmt",
55
+ "datefmt",
56
+ "_queue",
57
+ "_listener",
58
+ "_queue_handler",
59
+ "_lock",
60
+ )
61
+
62
+ def __init__(
63
+ self,
64
+ log_dir: Union[str, Path] = "logs",
65
+ log_filename: str = "app.log",
66
+ level: int = logging.INFO,
67
+ max_bytes: int = 2 * 1024 * 1024, # 2 MB
68
+ backup_count: int = 20,
69
+ retention_days: int = 5,
70
+ file: bool = True,
71
+ console: bool = True,
72
+ fmt: str = "%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d - %(message)s",
73
+ datefmt: str = "%Y-%m-%d %H:%M:%S",
74
+ ) -> None:
75
+ if not file and not console:
76
+ raise ValueError("The values of 'file' and 'console' cannot both be False; otherwise, the logs will be silently discarded.")
77
+
78
+ self.log_dir = Path(log_dir)
79
+ self.log_file = self.log_dir / log_filename
80
+ self.level = level
81
+ self.max_bytes = max_bytes
82
+ self.backup_count = backup_count
83
+ self.retention_days = retention_days
84
+ self.file = file
85
+ self.console = console
86
+ self.fmt = fmt
87
+ self.datefmt = datefmt
88
+
89
+ self._queue: "queue.Queue[logging.LogRecord]" = queue.Queue()
90
+
91
+ self._listener: Optional[QueueListener] = None
92
+ self._queue_handler: Optional[QueueHandler] = None
93
+ self._lock = threading.Lock()
94
+
95
+ def setup(self) -> None:
96
+ with self._lock:
97
+ if self._listener is not None:
98
+ return
99
+
100
+ formatter = logging.Formatter(fmt=self.fmt, datefmt=self.datefmt)
101
+
102
+ handlers: list[logging.Handler] = []
103
+
104
+ if self.file:
105
+ self.log_dir.mkdir(parents=True, exist_ok=True)
106
+
107
+ file_handler = _DailyCleanupRotatingFileHandler(
108
+ filename=str(self.log_file),
109
+ maxBytes=self.max_bytes,
110
+ backupCount=self.backup_count,
111
+ encoding="utf-8",
112
+ retention_days=self.retention_days,
113
+ )
114
+ file_handler.setLevel(self.level)
115
+ file_handler.setFormatter(formatter)
116
+ handlers.append(file_handler)
117
+
118
+ if self.console:
119
+ console_handler: logging.StreamHandler = logging.StreamHandler(sys.stdout)
120
+ console_handler.setLevel(self.level)
121
+ console_handler.setFormatter(formatter)
122
+ handlers.append(console_handler)
123
+
124
+ # QueueHandler:业务线程/协程只把 LogRecord 丢进内存队列
125
+ self._queue_handler = QueueHandler(self._queue)
126
+
127
+ root_logger = logging.getLogger()
128
+ root_logger.setLevel(self.level)
129
+ root_logger.addHandler(self._queue_handler)
130
+
131
+ # QueueListener:独立后台线程,真正做格式化 + 落盘 + 打印
132
+ self._listener = QueueListener(
133
+ self._queue,
134
+ *handlers,
135
+ respect_handler_level=True,
136
+ )
137
+ self._listener.start()
138
+
139
+ async def setup_async(self) -> None:
140
+ await asyncio.to_thread(self.setup)
141
+
142
+ def stop(self) -> None:
143
+ with self._lock:
144
+ if self._listener is None:
145
+ return
146
+
147
+ root_logger = logging.getLogger()
148
+ if self._queue_handler is not None:
149
+ root_logger.removeHandler(self._queue_handler)
150
+
151
+ self._listener.stop()
152
+ self._listener = None
153
+ self._queue_handler = None
154
+
155
+ async def stop_async(self) -> None:
156
+ await asyncio.to_thread(self.stop)
157
+
158
+ def __enter__(self) -> "AsyncSafeLogger":
159
+ self.setup()
160
+ return self
161
+
162
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
163
+ self.stop()
164
+
165
+ async def __aenter__(self) -> "AsyncSafeLogger":
166
+ await self.setup_async()
167
+ return self
168
+
169
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
170
+ await self.stop_async()