Skip to content

Commit 13d52a6

Browse files
committed
feat: add force_color support for Docker and non-TTY environments
Add force_color parameter and FORCE_COLOR environment variable support to enable colored output in Docker containers and CI/CD pipelines where stdout is not a TTY. Changes: - Add force_color parameter to get_logger() function with auto-detection from FORCE_COLOR env var - Pass force_color to ColoredFormatter to enable ANSI color codes in non-TTY environments - Update README.md with usage examples for both environment variable and parameter approaches - Add CLAUDE.md with comprehensive codebase documentation for AI-assisted development
1 parent 4f9a394 commit 13d52a6

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**python-simple-logger** is a Python logging library providing colored console/file logging with duplicate log filtering and sensitive data masking. Built on top of `colorlog`, it extends Python's standard logging with custom log levels (SUCCESS, HASH, STEP) and filters.
8+
9+
## Development Commands
10+
11+
### Testing
12+
13+
```bash
14+
# Run tests across all Python versions (3.9, 3.10, 3.11, 3.12, 3.13)
15+
tox
16+
17+
# Run tests for current environment only
18+
uv run pytest simple_logger/tests
19+
20+
# Run tests with coverage (auto-configured via pytest.ini)
21+
uv run pytest simple_logger/tests -v
22+
```
23+
24+
**Coverage Requirements:**
25+
- Minimum coverage: 93% (enforced in pyproject.toml)
26+
- Coverage report: HTML output in `.tests_coverage/`
27+
- Test files excluded from coverage (see `tool.coverage.run.omit`)
28+
29+
### Code Quality
30+
31+
```bash
32+
# Format code (ruff auto-fix enabled)
33+
uv run ruff format .
34+
35+
# Lint code
36+
uv run ruff check .
37+
38+
# Type checking
39+
uv run mypy simple_logger/
40+
```
41+
42+
**Quality Standards:**
43+
- Line length: 120 characters
44+
- Type hints required (mypy strict mode: disallow_untyped_defs, disallow_incomplete_defs)
45+
- Ruff preview mode enabled with auto-fix
46+
47+
## Architecture
48+
49+
### Core Components
50+
51+
**`simple_logger/logger.py`** - Single-file library with four main components:
52+
53+
1. **`SimpleLogger`** (lines 60-79)
54+
- Custom logger class extending Python's `logging.Logger`
55+
- Adds custom log levels: SUCCESS (32), HASH (33), STEP (34)
56+
- Provides convenience methods: `.success()`, `.step()`, `.hash()`
57+
- Set as default logger class via `logging.setLoggerClass(SimpleLogger)` (line 82)
58+
59+
2. **`WrapperLogFormatter`** (lines 55-57)
60+
- Extends `colorlog.ColoredFormatter`
61+
- Overrides `formatTime()` to use ISO 8601 timestamps
62+
- Configured with color mapping for all log levels
63+
64+
3. **Filters**
65+
- `DuplicateFilter` (lines 18-37): Suppresses repeated logs, appends count summary
66+
- `RedactingFilter` (lines 40-52): Masks sensitive patterns with regex
67+
68+
4. **`get_logger()`** (lines 85-166)
69+
- Factory function returning singleton loggers (cached in `LOGGERS` dict)
70+
- Handles both console and file handlers with rotation
71+
- Auto-detects `FORCE_COLOR` env var for non-TTY environments (Docker/CI)
72+
73+
### Key Design Patterns
74+
75+
**Singleton Logger Pattern:**
76+
```python
77+
if LOGGERS.get(name):
78+
return LOGGERS[name] # Return cached logger
79+
```
80+
Ensures one logger instance per name.
81+
82+
**Force Color Detection (lines 119-123):**
83+
```python
84+
if force_color is None:
85+
force_color_env = os.environ.get("FORCE_COLOR", "").lower()
86+
force_color = force_color_env in ("1", "true")
87+
```
88+
Critical for Docker logs - passes `force_color` to ColoredFormatter to enable ANSI codes in non-TTY environments.
89+
90+
## Important Implementation Details
91+
92+
### Adding New Features
93+
94+
When modifying `get_logger()`:
95+
- Parameters must be added to function signature
96+
- Update docstring with new parameter
97+
- Consider singleton behavior - changes affect all future logger creations, not existing ones
98+
- Update README.md with usage examples
99+
100+
### Color Output
101+
102+
The `force_color` parameter/env var is essential for Docker environments where `sys.stdout.isatty()` returns False. Without it, colorlog disables colors even though Docker can display ANSI codes.
103+
104+
### Testing
105+
106+
Tests use pytest fixtures for logger instances (`basic_logger`, `logger_with_mask`). When adding features:
107+
- Test both parameter and environment variable paths
108+
- Use `tmp_log_file` fixture for file output tests
109+
- Verify both console and file handler behavior
110+
111+
## Release Process
112+
113+
This project uses `release-it` for versioning and releases:
114+
115+
```bash
116+
# Prerequisites (one-time setup)
117+
export GITHUB_TOKEN=<token>
118+
sudo npm install --global release-it
119+
120+
# Create release
121+
git pull
122+
release-it # Interactive prompts guide the process
123+
```
124+
125+
Version is managed in `pyproject.toml` (currently 2.0.17).
126+
127+
## Build System
128+
129+
- **Package manager:** uv (with lock file)
130+
- **Build backend:** hatchling
131+
- **Distribution:** PyPI at `python-simple-logger`
132+
- **Include:** Only `simple_logger/` directory in builds

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,13 @@ hashed_logger.info(er = get"This is my secret -> sec1234abc")
5757
>>> This is my token *****
5858
>>> This is my apikey *****
5959
>>> This is my secret *****
60+
61+
# Force colored output in non-TTY environments (Docker, CI/CD)
62+
# Option 1: Use FORCE_COLOR environment variable
63+
import os
64+
os.environ["FORCE_COLOR"] = "1" # or set in Dockerfile/docker-compose.yml
65+
logger = get_logger(name=__name__)
66+
67+
# Option 2: Use force_color parameter
68+
logger = get_logger(name=__name__, force_color=True)
6069
```

simple_logger/logger.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
import os
45
import re
56
from datetime import datetime
67
from logging.handlers import RotatingFileHandler
@@ -91,6 +92,7 @@ def get_logger(
9192
mask_sensitive: bool = False,
9293
duplicate_filter: bool = True,
9394
mask_sensitive_patterns: list[str] | None = None,
95+
force_color: bool | None = None,
9496
) -> logging.Logger:
9597
"""
9698
Get logger object for logging.
@@ -104,6 +106,8 @@ def get_logger(
104106
file_backup_count (int): max number of log files to keep
105107
mask_sensitive (bool): whether to mask sensitive information
106108
mask_sensitive_patterns (list[str]): list of patterns to mask
109+
force_color (bool or None): force colored output even in non-TTY environments.
110+
If None, will check FORCE_COLOR environment variable.
107111
108112
Returns:
109113
Logger: logger object
@@ -112,6 +116,12 @@ def get_logger(
112116
if LOGGERS.get(name):
113117
return LOGGERS[name]
114118

119+
# Determine force_color setting
120+
if force_color is None:
121+
# Check FORCE_COLOR environment variable
122+
force_color_env = os.environ.get("FORCE_COLOR", "").lower()
123+
force_color = force_color_env in ("1", "true")
124+
115125
logger_obj = logging.getLogger(name)
116126
log_formatter = WrapperLogFormatter(
117127
fmt="%(asctime)s %(name)s %(log_color)s%(levelname)s%(reset)s %(message)s",
@@ -126,6 +136,7 @@ def get_logger(
126136
"STEP": "bold_cyan",
127137
},
128138
secondary_log_colors={},
139+
force_color=force_color,
129140
)
130141

131142
if console:

0 commit comments

Comments
 (0)