Metadata-Version: 2.5
Name: concurrent-log-handler
Version: 0.9.30
Summary: RotatingFileHandler replacement with concurrency, gzip and Windows support. Size and time based rotation.
Project-URL: Homepage, https://github.com/Preston-Landers/concurrent-log-handler
Author-email: Preston Landers <planders@utexas.edu>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: QueueHandler,QueueListener,linux,logging,portalocker,rotate,unix,windows
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Requires-Python: >=3.6
Requires-Dist: portalocker>=2.6.0
Provides-Extra: dev
Requires-Dist: black>=26.1.0; extra == 'dev'
Requires-Dist: coverage; extra == 'dev'
Requires-Dist: hatch; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-mock; extra == 'dev'
Requires-Dist: pytest-repeat; extra == 'dev'
Requires-Dist: pytest-sugar; extra == 'dev'
Requires-Dist: ruff>=0.15.2; extra == 'dev'
Description-Content-Type: text/markdown

<!-- markdownlint-disable MD026 -->

# Concurrent Log Handler (CLH)

[![PyPI version](https://img.shields.io/pypi/v/concurrent-log-handler.svg)](https://pypi.org/project/concurrent-log-handler/)
[![Downloads](https://img.shields.io/pypi/dm/concurrent-log-handler.svg)](https://pepy.tech/project/concurrent-log-handler)
[![Stars](https://img.shields.io/github/stars/Preston-Landers/concurrent-log-handler.svg)](https://github.com/Preston-Landers/concurrent-log-handler/stargazers)
[![Forks](https://img.shields.io/github/forks/Preston-Landers/concurrent-log-handler.svg)](https://github.com/Preston-Landers/concurrent-log-handler/network/members)
[![Python versions](https://img.shields.io/pypi/pyversions/concurrent-log-handler.svg)](https://pypi.org/project/concurrent-log-handler/)
[![License](https://img.shields.io/pypi/l/concurrent-log-handler.svg)](https://github.com/Preston-Landers/concurrent-log-handler/blob/main/LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/Preston-Landers/concurrent-log-handler/clh_tests.yaml?branch=master)](https://github.com/Preston-Landers/concurrent-log-handler/actions)
[![Contributors](https://img.shields.io/github/contributors/Preston-Landers/concurrent-log-handler.svg)](https://github.com/Preston-Landers/concurrent-log-handler/graphs/contributors)

The `concurrent-log-handler` package provides robust logging handlers for
Python's standard `logging` package (PEP 282). It enables multiple processes
(and threads) to safely write to a single log file, with built-in support for
size-based and time-based log rotation and optional log compression.

This package is meant for applications that run in multiple processes,
potentially across different hosts sharing a network drive, and require a
centralized logging solution without the complexity of external logging
services.

NOTE: If you're reading this and the links to files like
[CHANGELOG.md](CHANGELOG.md) don't work, or links within the document either,
try viewing
[this document on GitHub](https://github.com/Preston-Landers/concurrent-log-handler/)
instead.

## What's new

See [CHANGELOG.md](CHANGELOG.md) for details.

- **Version 0.9.30**: (October 2026)
  - Mark Python 3.15 as supported.
  - Fix permission race window between file create and chmod when the
    `chmod` and/or `owner` kwargs are configured. A different-user process
    opening the lock file, log file, or rotated `.gz` file during the
    create/chmod window could get `PermissionError`. Files are now
    pre-created atomically with the correct permissions, with a fallback for
    filesystems without hard links. Issue
    [#87](https://github.com/Preston-Landers/concurrent-log-handler/issues/87)
  - Fix lost records when a process writes to a log file that another user
    owns while `chmod` or `owner` is configured.
  - `ConcurrentTimedRotatingFileHandler`: apply `umask`, `chmod`, and
    `owner` to the main log file from the start, and apply `chmod` and
    `owner` to rotated `.gz` files. Before, a process running as another
    user could fail to create its handler, with `PermissionError`.
  - Fix `do_gzip()` silently ignoring the configured `umask` on rotated
    `.gz` files (the call ran outside `_alter_umask()`). The handler's
    `umask` setting is now honored on the rotation path.
  - Fix a deadlock (new in 0.9.29) in a child process forked while another
    thread was writing a log record.
  - Fix loss of a rotated log file on the rotation after a failed gzip (for
    example, disk full).
  - Remove a duplicate stale-file check from each write (about 13% less time
    per write when `maxBytes` is set).
- **Version 0.9.29**: (February 2026)
  - Fix race conditions when a handler created before `fork()` is used by
    multiple child processes. Child processes that inherit a handler now
    automatically reopen the lock file for independent lock isolation, and an
    in-process threading lock prevents concurrent threads from bypassing flock()
    serialization.
    - Re-initializing logging post-fork (see
      [Usage Guidelines](#important-usage-guidelines)) is still recommended.
  - Depend on portalocker >= 2.6.0 instead of 1.6.0. Earlier versions of
    portalocker on Windows can be problematic.
  - Add `finalize_handler_configuration()` hook to
    ConcurrentTimedRotatingFileHandler to allow customization of the handler
    before the first rollover.
- **Version 0.9.28**: (June 10th, 2025)
  - Fixes errors when apps, esp. asyncio based, try to log during interpreter
    shutdown. Issue
    [#80](https://github.com/Preston-Landers/concurrent-log-handler/issues/80)
  - Fix missing rollovers when a worker was restarted before the next logging
    event, but after the last scheduled rollover.
    [Issue #81](https://github.com/Preston-Landers/concurrent-log-handler/issues/81)
- **Version 0.9.27**: (June 6th, 2025)
  - Fixes Issue
    [#73](https://github.com/Preston-Landers/concurrent-log-handler/issues/73)
    Fix timed rotation handler's file cleanup logic.
  - Fixes Issue
    [#79](https://github.com/Preston-Landers/concurrent-log-handler/issues/79)
    Harden timed handler's rollover mechanism against timestamp errors or other
    sync corruption.

- **Important Notice (June 2025): Background Logging Utility Removed**
  - The `concurrent_log_handler.queue` module was removed in version 0.9.28.
  - It has compatibility issues with complex logging setups and other robustness
    concerns.
  - **Recommendation:**
    - Simply use the standard CLH handlers (`ConcurrentRotatingFileHandler` or
      `ConcurrentTimedRotatingFileHandler`) directly in your application.
      Synchronous logging calls are simpler, more reliable, and performant
      enough for most use cases.
    - If you need non-blocking logging calls, use the standard library patterns
      shown in [Performance Patterns](docs/patterns.md).
  - The core CLH handlers remain fully supported and are not affected by this
    deprecation.

- **Version 0.9.26**: (May 2025)
  - Improved performance, especially on POSIX systems.
  - Added testing for Python 3.13 and improved project configuration and
    documentation.

## Key Features

- **Concurrent Logging:** Multiple processes and threads safely write to the
  same log file.
- **File Rotation:** Size-based and time-based rotation with optional
  compression.
- **Cross-Platform:** Windows and POSIX support with reliable file locking.
- **Customizable:** Control naming, permissions, line endings, and lock file
  placement.
- **Performance Optimized:** Keeps files open between writes for better
  performance.
- **Python 3.6 through current versions:** Modern Python support.
- **Focused Design:** Reliably handles file operations. For non-blocking
  behavior, see our recommended
  [Application-Level Performance Patterns](./docs/patterns.md), including
  patterns for
  [graceful degradation](./docs/patterns.md#pattern-2-graceful-degradation) to
  synchronous logging, or using sync logging only for
  [higher priority](./docs/patterns.md#pattern-3-critical-vs-background-logging)
  levels.

## Primary Use Cases

CLH is primarily designed for scenarios where:

- Multiple processes of a Python application need to log to a shared file.
- These processes might be on the same machine or on different machines
  accessing a shared network drive.
- Log files need to be automatically rotated based on size or time.

Note that this package is not primarily intended for intensive high-throughput
logging scenarios, but rather for general-purpose logging in multi-process
applications.

### Alternatives to CLH

While CLH offers a robust file-based solution, consider these alternatives for
different needs:

- **Cloud Logging Services:** Azure Monitor, AWS CloudWatch Logs, Google Cloud
  Logging, Logstash, etc. These are excellent for distributed systems and offer
  advanced analysis features.
- **Custom Logging Server:** Implement a centralized logging server as
  demonstrated in the
  [Python Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html#sending-and-receiving-logging-events-across-a-network).
  CLH's `QueueHandler` and `QueueListener` can be adapted for this pattern.

## Installation

Install the package using `pip`:

```bash
pip install concurrent-log-handler
```

This will also install `portalocker`. On Windows, `portalocker` has a dependency
on `pywin32`. Note that we now require at least portalocker 2.6.0, which among
other things, explicitly declares its dependency on `pywin32` if needed.

To install from source:

```bash
python setup.py install
```

## Quick Start: Basic Usage

Here's a simple example using `ConcurrentRotatingFileHandler` for size-based
rotation:

```python
import logging
from concurrent_log_handler import ConcurrentRotatingFileHandler
import os

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# Use an absolute path for the log file
logfile = os.path.abspath("mylogfile.log")

# Configure the handler: rotate after 512KB, keep 5 backups
# Mode "a" for append
rotate_handler = ConcurrentRotatingFileHandler(
    logfile, "a", maxBytes=512 * 1024, backupCount=5
)
logger.addHandler(rotate_handler)

logger.info("This is an exciting log message!")
logger.info("Multiple processes can write here concurrently.")
```

For a few more basic code examples, see [src/example.py](./src/example.py). For
more advanced usage including non-blocking patterns, see the
[Performance Patterns](./docs/patterns.md) guide.

## Important Usage Guidelines

To ensure correct and reliable operation when using CLH in a multi-process
environment, please keep the following in mind:

1. **Handler Instantiation per Process:**
   - Each process _must_ create its own instance of the CLH handler
     (`ConcurrentRotatingFileHandler` or `ConcurrentTimedRotatingFileHandler`).
   - You **cannot** serialize a handler instance and reuse it in another
     process.
     - For example, you cannot pass it from a parent to a child process via
       `multiprocessing` in spawn mode.
   - This limitation is because the file lock objects and other internal states
     within the handler cannot be safely serialized and shared across process
     boundaries.
   - This requirement **does not** apply to threads within the same process;
     threads can share a single CLH instance.
   - This requirement also applies to child processes created via `fork()`
     (e.g., Gunicorn with `--preload`, uWSGI, or `multiprocessing` with the fork
     start method). Forked children inherit the parent's file descriptors, which
     causes file locks to silently fail to serialize between processes. See item
     3 below for the recommended pattern.

2. **Multiprocessing and Spawn mode:**

   Just to reemphasize the point above:
   - If you use `multiprocessing` with the default `spawn` start method (the
     default on Windows and macOS), each child process must create its own CLH
     handler instance.
   - In your child process startup code, instantiate the handler as shown in the
     example above.
   - Usually this means you can call your standard logging setup function in the
     child.
   - Don't just initialize your logging code in the parent process and allow
     child processes to inherit loggers.

3. **Using `fork()` (Gunicorn, uWSGI, multiprocessing with `fork` start
   method):**

   On Linux, depending on the version of Python, `multiprocessing` defaults to
   the `fork` start method. Frameworks like Gunicorn with `--preload` and uWSGI
   also fork worker processes from a parent that has already initialized
   logging.

   While CLH v0.9.29+ has internal safeguards for this scenario, the cleanest
   approach is to re-initialize logging in each child process. Python 3.7+
   provides `os.register_at_fork()` for this:

   ```python
   import logging
   import logging.config
   import os

   _LOGGING_CONFIG = None  # Store your logging config dict
   _fork_handler_registered = False

   def setup_logging():
       global _LOGGING_CONFIG, _fork_handler_registered

       config = {
           # Your logging.config.dictConfig()-compatible dict here
       }
       _LOGGING_CONFIG = config
       logging.config.dictConfig(config)

       if hasattr(os, "register_at_fork") and not _fork_handler_registered:
           _fork_handler_registered = True
           os.register_at_fork(after_in_child=_reset_logging_in_child)

   def _reset_logging_in_child():
       """Called automatically in forked child processes."""
       if not _LOGGING_CONFIG:
           return
       # Close inherited handlers (releases stale file descriptors and locks)
       root = logging.getLogger()
       for handler in root.handlers:
           handler.close()
       root.handlers.clear()

       # Re-initialize from scratch — child gets its own file descriptors
       logging.config.dictConfig(_LOGGING_CONFIG)
   ```

   **Why this matters:** File locks (`flock`) are tied to the _open file
   description_, not the file descriptor number. When a child inherits the
   parent's lock-file FD, both processes share the same lock object — meaning
   `flock()` calls in the child don't actually block against the parent or
   siblings. Re-creating the handler gives each process its own independent file
   description.

   For Gunicorn specifically, you can call your logging setup in the `post_fork`
   hook:

   ```python
   # gunicorn.conf.py
   def post_fork(server, worker):
       from myapp.logging import setup_logging
       setup_logging()
   ```

   On platforms where `os.register_at_fork` is unavailable, or when using the
   `spawn` start method, simply call your logging setup function at the start of
   each worker process.

4. **Consistent Configuration:**
   - All processes writing to the _same log file_ **must** use identical
     settings for the CLH handler (e.g., `maxBytes`, `backupCount`, `use_gzip`,
     rotation interval, etc.).
   - Do not mix CLH handlers with other logging handlers (like
     `RotatingFileHandler` from the standard library) writing to the same file.
     This can lead to unpredictable behavior and data loss.

5. **Networked/Cloud Storage:**
   - When logging to files on network shares (NFS, SMB/CIFS) or cloud-synced
     folders (Dropbox, Google Drive, OneDrive), ensure that the advisory file
     locking provided by `portalocker` works correctly in your specific
     environment.
   - The `lock_file_directory` option allows you to place the lock file in a
     different location (e.g., a local fast filesystem) than the log file
     itself. This can resolve issues with locking on certain network shares.
     However, if multiple hosts write to the same shared log, they _must_ all
     have access to this common lock file location.
   - Alternatively, configure your cloud sync software to ignore CLH lock files
     (typically `.<logfilename>.lock` or files in the `lock_file_directory`).
   - If you run into problems, try the `keep_file_open=False` option to close
     the log file after each write. This may help with certain networked
     filesystems but can impact performance.

6. **One Handler Instance per Log File:**
   - If your application writes to multiple distinct log files, each log file
     requires its own dedicated CLH handler instance within each process.

   - It is possible to have multiple CLH handlers that point to the same log
     file, for example, if you want to log different log levels or formats to
     the same file. However, all other rotation settings must be identical
     across these handlers.

## Handler Details

CLH provides two main handler classes:

### `ConcurrentRotatingFileHandler` (Size-based Rotation)

This handler rotates logs when the file size exceeds `maxBytes`. Note that the
actual file sizes may exceed `maxBytes`. How much larger depends on the size of
the log message being written when the rollover occurs.

```python
from concurrent_log_handler import ConcurrentRotatingFileHandler

# Example: Rotate at 10MB, keep 3 backups, compress rotated logs
handler = ConcurrentRotatingFileHandler(
    "app.log",
    maxBytes=10 * 1024 * 1024,
    backupCount=3,
    use_gzip=True
)
```

### `ConcurrentTimedRotatingFileHandler` (Time-based Rotation)

This handler rotates logs at specified time intervals (e.g., hourly, daily,
weekly). It can _also_ rotate based on size if `maxBytes` is set to a non-zero
value.

By default, it rotates hourly and does not perform size-based rotation
(`maxBytes=0`).

```python
from concurrent_log_handler import ConcurrentTimedRotatingFileHandler

# Example: Rotate daily, keep 7 backups, also rotate if file exceeds 20MB
handler = ConcurrentTimedRotatingFileHandler(
    filename="app_timed.log",
    when="D",  # 'D' for daily, 'H' for hourly, 'M' for minute, 'W0'-'W6' for weekly
    interval=1,
    backupCount=7,
    maxBytes=20 * 1024 * 1024,  # Optional size-based rotation
    use_gzip=True
)
```

It's recommended to use keyword arguments when configuring this class due to the
number of parameters and their ordering. For more details on time-based rotation
options (`when`, `interval`, `utc`), refer to the Python standard library
documentation for
[`TimedRotatingFileHandler`](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler).

#### Customizing via Subclassing

If you need to customize the handler behavior (e.g., use a custom filename
suffix format), you can subclass `ConcurrentTimedRotatingFileHandler` and
override the `finalize_handler_configuration()` hook method. This method is
called after parent class initialization but before the first potential
rollover, allowing you to safely customize attributes like `self.suffix` or
`self.namer`.

```python
from concurrent_log_handler import ConcurrentTimedRotatingFileHandler

class CustomSuffixHandler(ConcurrentTimedRotatingFileHandler):
    def __init__(self, filename, custom_suffix=None, **kwargs):
        self.custom_suffix = custom_suffix
        super().__init__(filename, **kwargs)

    def finalize_handler_configuration(self):
        """Override to customize handler before first rollover."""
        super().finalize_handler_configuration()
        if self.custom_suffix:
            self.suffix = self.custom_suffix

# Use custom suffix format
handler = CustomSuffixHandler(
    "app.log",
    custom_suffix="%Y-%m-%d_%H-%M-%S.log",
    when="D",
    backupCount=7
)
```

### Common Configuration Options

Both handlers share several configuration options (passed as keyword arguments):

- `logfile` / `filename`: Path to the log file.
- `mode`: File open mode (default: `'a'` for append).
- `backupCount`: Number of rotated log files to keep.
- `encoding`: Log file encoding (e.g., `'utf-8'`).
- `delay`: Ignored. Both handlers always open the log file when the first
  record is written.
- `use_gzip`: (Default: `False`) If `True`, compresses rotated log files using
  gzip.
- `owner`: Tuple `(uid, gid)` or `['username', 'groupname']` to set file
  ownership on Unix.
- `chmod`: Integer file mode (e.g., `0o640`) to set permissions on Unix.
- `lock_file_directory`: Path to a directory where lock files should be stored,
  instead of next to the log file.
- `newline`: (Default: platform-specific) Specify newline characters. E.g., `''`
  (let `terminator` handle it).
- `terminator`: (Default: platform-specific, e.g., `\n` on POSIX, `\r\n` on
  Windows). Specify record terminator.
  - To force Windows-style CRLF on Unix:
    `kwargs={'newline': '', 'terminator': '\r\n'}`
  - To force Unix-style LF on Windows:
    `kwargs={'newline': '', 'terminator': '\n'}`
- `namer`: A callable function to customize the naming of rotated files. See
  `BaseRotatingHandler.namer` in Python docs.
- `keep_file_open`: Defaults to `True` for enhanced performance by keeping the
  log file (and lock file) open between writes. This is recommended for most use
  cases.
  - Set to `False` if you need to ensure the log file is closed after each
    write, for example, for compatibility with certain networked filesystems.
  - On Windows, the log file will always be closed after writes to allow for
    rotation, but this option still affects whether the lock file is kept open.

---

## Non-Blocking Logging Patterns (Optional)

CLH handlers are **synchronous by design** for maximum reliability and
predictability. However, some applications need non-blocking logging to prevent
thread blocking during logging bursts or high-frequency logging scenarios.

### Quick Note on Performance:

With CLH's recent performance improvements (v0.9.26+), many applications won't
need non-blocking patterns. Test your specific use case first - synchronous
logging is simpler and more reliable.

### When You Might Need Non-Blocking Logging:

- Multiple threads logging heavily during error conditions
- Web application request threads that shouldn't block on I/O
- High-frequency logging in performance-critical applications
- Applications with strict latency requirements

### Quick Start: The Recommended Approach

For most non-blocking needs, use Python's standard library `QueueHandler`:

```python
import logging
import logging.handlers
import queue
import atexit
from concurrent_log_handler import ConcurrentRotatingFileHandler

# Create queue and handlers
log_queue = queue.Queue(maxsize=10000)
file_handler = ConcurrentRotatingFileHandler("app.log", maxBytes=10 * 1024 * 1024)
queue_handler = logging.handlers.QueueHandler(log_queue)

# Set up background processing
listener = logging.handlers.QueueListener(log_queue, file_handler)
listener.start()
atexit.register(listener.stop)

# Configure logger
logging.getLogger().addHandler(queue_handler)
logging.info("This won't block your thread!")
```

### Complete Guide

For comprehensive patterns including:

- **Web framework integration** (Django, Flask, FastAPI)
- **Graceful degradation** when queues fill up
- **Production monitoring** and error handling
- **Critical vs background logging** strategies
- **Performance comparisons** and best practices

**See the complete guide: [Performance Patterns](docs/patterns.md)**

This approach uses Python's standard library tools, giving you full control
while keeping CLH focused on reliable file operations.

## Logging Configuration File Usage (`fileConfig`)

You can configure CLH using Python's `logging.config.fileConfig`:

```ini
[loggers]
keys = root

[handlers]
keys = concurrentRotatingFile, concurrentTimedRotatingFile

[formatters]
keys = simpleFormatter

[logger_root]
level = INFO
handlers = concurrentRotatingFile, concurrentTimedRotatingFile

[handler_concurrentRotatingFile]
class = concurrent_log_handler.ConcurrentRotatingFileHandler
level = INFO
formatter = simpleFormatter
args = ("rotating_size.log", "a")
kwargs = {'maxBytes': 10485760, 'backupCount': 5, 'use_gzip': True}
# For Python < 3.7, kwargs are not supported. You might need to subclass or use code config.

[handler_concurrentTimedRotatingFile]
class = concurrent_log_handler.ConcurrentTimedRotatingFileHandler
level = INFO
formatter = simpleFormatter
args = ("rotating_timed.log",) # filename
kwargs = {'when': 'H', 'interval': 1, 'backupCount': 24, 'use_gzip': True}

[formatter_simpleFormatter]
format = %(asctime)s - %(name)s - %(levelname)s - %(message)s
```

**Note:** Ensure you `import concurrent_log_handler` in your Python code
_before_ calling `fileConfig()`. Python 3.7+ is recommended for `kwargs` support
in config files.

## Migration from Deprecated Background Logging

**⚠️ Removal Timeline: The `queue` module will be removed in v1.0.0 (planned for
Summer 2025)**

**If you're currently using
`concurrent_log_handler.queue.setup_logging_queues()`:**

This utility is deprecated and will be removed in v1.0.0 due to compatibility
issues with complex logging setups. It was provided for convenience but is not
an integral part of CLH's functionality.

**Important**: consider whether you need non-blocking logging at all. Typical
applications can get reasonable performance with the normal (synchronous) CLH
handlers.

### Migration Steps:

1. **Remove** calls to `setup_logging_queues()`

2. **Replace** with the standard library patterns shown above or in
   [Performance Patterns](docs/patterns.md)

   **or**: simply use the CLH handlers directly in your application without the
   queue utility.

3. **Test** your application with the new approach

### Why the Change?

The old utility:

- ❌ Modified all handlers globally (monkey patching)
- ❌ Had compatibility issues with advanced logging libraries
- ❌ Used unbounded queues (memory risk)
- ❌ Lacked proper error handling

The new approach:

- ✅ Uses standard library patterns
- ✅ Gives applications explicit control
- ✅ Works with any logging setup
- ✅ Provides better error visibility

**For detailed migration examples, see
[Performance Patterns](docs/patterns.md).**

## Best Practices and Limitations

- **`maxBytes` is a Guideline:** The actual log file size might slightly exceed
  `maxBytes` because the check is performed _before_ writing a new log message.
  The file can grow by the size of that last message. This behavior prioritizes
  preserving log records. Standard `RotatingFileHandler` is stricter but may
  truncate.

- **`backupCount` Performance:** Avoid excessively high `backupCount` values
  (e.g., \> 20-50). Renaming many files during rotation can be slow, and this
  occurs while the log file is locked. Consider increasing `maxBytes` instead if
  you need to retain more history in fewer files.

- **Gzip Compression:** Enabling `use_gzip` adds CPU overhead.

- **Restart on Configuration Change:** If you change logging settings (e.g.,
  rotation parameters), it's often best to restart all application processes to
  ensure all writers use the new, consistent configuration.

## For Developers (Contributing)

If you plan to modify or contribute to CLH:

1. **Clone the repository:**

   ```bash
   git clone https://github.com/Preston-Landers/concurrent-log-handler.git
   cd concurrent-log-handler
   ```

2. **Create a virtual environment and activate it:**

   ```bash
   python -m venv venv
   source venv/bin/activate  # On Windows: venv\Scripts\activate
   ```

3. **Install in editable mode with development dependencies:**

   ```bash
   pip install -e ".[dev]"
   ```

   This installs `hatch`, `black` and other development tools.

4. **Run tests:**

   ```bash
   # Run tests on the current (venv) Python version
   pytest

   # or run:
   hatch test

   # Generate coverage report
   pytest --cov --cov-report=html --cov-report=xml --cov-report=lcov --cov-report=term-missing

   # Tests across all supported Python versions in the GitHub Actions matrix
   # However, Python 3.6 and 3.7 are not supported by GitHub Actions due to their EOL status.
   ```

5. **Build for distribution:**

   ```bash
   hatch build --clean
   ```

   The distributable files will be in the `dist/` folder. To upload (maintainers
   only):

   ```bash
   pip install twine
   twine upload dist/*
   ```

## Historical Note

This package is a fork of Lowell Alleman's `ConcurrentLogHandler` 0.9.1. The
fork was created to address a hanging/deadlocking issue
([Launchpad Bug \#1265150](https://bugs.launchpad.net/python-concurrent-log-handler/+bug/1265150))
and has since incorporated numerous other fixes, features, and modernizations.

## Project Links

- **GitHub Repository:**
  [https://github.com/Preston-Landers/concurrent-log-handler](https://github.com/Preston-Landers/concurrent-log-handler)
- **PyPI Package:**
  [https://pypi.org/project/concurrent-log-handler/](https://pypi.org/project/concurrent-log-handler/)

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.

## Contributors

The original version was by Lowell Alleman. Subsequent contributions are listed
in [CONTRIBUTORS.md](CONTRIBUTORS.md).

## License

This project is licensed under the terms of the [LICENSE file](./LICENSE)
(Apache 2.0 terms).
