A feature-rich, configurable logging module with structured JSON output
Project description
JH Logger
A feature-rich, configurable logging module that provides structured JSON output with comprehensive error tracking, third-party service integration, and enhanced security features. Built for Jacaranda Health's microservices ecosystem.
New in v1.1.0: Built-in OpenTelemetry and Temporal context injection, enhanced bound logger API, and configurable JSON formatting.
🌟 Features
✅ Configurable Log Levels
- Support for all standard log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
- Dynamic log level changes at runtime
- Enum-based level specification for type safety
✅ Enhanced Error Information
- DEBUG Level: Shows full traceback for detailed debugging
- ERROR Level: Captures exception details (type, message, traceback)
- File Context: Automatically captures filename, function name, class name, and line number
- Module Information: Includes module path and calling context
- Security: Uses relative file paths instead of full absolute paths to avoid exposing system information
✅ Structured JSON Output
- All logs are formatted as JSON with 4-space indentation for readability
- Consistent structure across all log entries with sorted keys
- Support for arbitrary data fields
✅ Third-Party Service Integration
- CloudWatch: Automatic CloudWatch logging in non-development environments
- Sentry: Exception capturing and monitoring for ERROR/CRITICAL levels
- Environment-aware configuration
✅ Rich Context Information
- System information (country, environment, service name, process ID)
- UTC timestamp in ISO format
- Caller information (filename, function, class, line number)
- Custom data fields support
✅ Built-in Observability Integration (New in v1.1.0)
- OpenTelemetry: Automatic
trace_idandspan_idinjection when spans are active - Temporal: Automatic workflow/activity context (
workflow_id,activity_type,task_queue,attempt) - Enhanced Bound Logger: Preserves full jhlogger API after binding with
bind() - Configurable JSON: Compact formatting for production, pretty formatting for development
- Per-Instance Configuration: True isolation - multiple loggers with the same name work independently
📦 Installation
Production Installation
# Basic installation
pip install jhlogger
# With OpenTelemetry support (v1.1.0+)
pip install jhlogger[otel]
# With Temporal support (v1.1.0+)
pip install jhlogger[temporal]
# With full observability stack (v1.1.0+)
pip install jhlogger[observability]
Development Installation
git clone https://github.com/Jacaranda-Health/jhlogger.git
cd jhlogger
uv sync --all-extras
With UV (recommended)
# Create virtual environment and install dependencies
uv sync
# For development with all extras
uv sync --all-extras
With Pip (Local Development)
# Basic installation
pip install -e .
# With development dependencies
pip install -e .[dev]
# With documentation dependencies
pip install -e .[docs]
# All extras
pip install -e .[dev,docs]
🚀 Quick Start
Basic Usage
from jhlogger import info, error, debug, ConfigurableLogger
# Simple logging
info("Application started successfully")
error("Something went wrong", data={"error_code": "E001"})
# With exception
try:
result = 10 / 0
except Exception as e:
error("Division failed", exception=e, data={"operation": "10/0"})
# Debug with full traceback
debug("Debug information", data={"step": "initialization"})
Custom Logger Instance
from jhlogger import ConfigurableLogger, LogLevel
logger = ConfigurableLogger(
name="my-service",
log_level=LogLevel.DEBUG,
enable_cloudwatch=True,
include_system_info=True
)
logger.info("Custom logger initialized")
New in v1.1.0: Observability Integration
from jhlogger import ConfigurableLogger, LogLevel
# Logger with built-in OpenTelemetry and Temporal context injection
logger = ConfigurableLogger(
name="my-service",
log_level=LogLevel.INFO,
enable_otel_context=True, # Automatic trace_id/span_id injection
enable_temporal_context=True, # Automatic workflow/activity context
json_indent=None, # Compact JSON for production
)
# Enhanced bound logger (preserves full API)
request_logger = logger.bind(request_id="req_123", user_id=456)
request_logger.info("Processing request") # Includes request_id and user_id
request_logger.error("Request failed", exception=e) # Full jhlogger API preserved
# Unbind context
unbound_logger = request_logger.unbind("request_id")
Sample Output with OpenTelemetry and Temporal Context:
{
"caller": {
"file_path": "workers/messages.py",
"function": "run_workflow",
"line_number": 84
},
"trace_id": "8f0de49b683ccf048b470d109025df6e",
"span_id": "cdbe9e6318742af9",
"workflow_id": "messages-02223f6f-0c14-4853-aa31-3708f527ff3e",
"activity_type": "process_message",
"task_queue": "messages_queue_KE",
"attempt": 1,
"event": "Processing workflow step",
"level": "info",
"timestamp": "2026-03-25T19:00:16.570964Z"
}
Class-Based Usage
from jhlogger import create_logger
class UserService:
def __init__(self):
self.logger = create_logger(name="user-service")
def create_user(self, username, email):
self.logger.info("Creating user", data={
"username": username,
"email": email
})
try:
# Your logic here
self.logger.info("User created successfully")
except Exception as e:
self.logger.error("Failed to create user", exception=e)
📊 Log Output Format
Standard Log Entry
{
"class": "UserService",
"country": "KE",
"data": {
"ip_address": "192.168.1.100",
"user_id": "12345",
"username": "john_doe"
},
"environment": "production",
"event": "User logged in successfully",
"filename": "user_service.py",
"file_path": "app/services/user_service.py",
"function": "login_user",
"level": "info",
"line_number": 45,
"module": "services.user_service",
"process_id": 1234,
"service": "rapid-pro-service",
"timestamp": "2025-09-18T10:30:00.123456Z",
"timestamp_utc": "2025-09-18T10:30:00.123456+00:00"
}
DEBUG Level (includes traceback)
{
"event": "Debug information",
"level": "debug",
"timestamp": "2025-09-18T10:30:00.123456Z",
"traceback": [
" File \"main.py\", line 10, in <module>",
" debug_function()",
" File \"main.py\", line 5, in debug_function",
" logger.debug(\"Debug info\")"
]
}
ERROR Level (includes exception details)
{
"event": "Database connection failed",
"exception": {
"args": ["Unable to connect to database"],
"message": "Unable to connect to database",
"traceback": [
"Traceback (most recent call last):",
" File \"app.py\", line 25, in connect_db",
" conn = database.connect()",
"ConnectionError: Unable to connect to database"
],
"type": "ConnectionError"
},
"level": "error",
"timestamp": "2025-09-18T10:30:00.123456Z"
}
⚙️ Configuration
Environment Variables
The logger respects these environment variables:
APP_ENVorFLASK_ENV: Application environment (development, staging, production)COUNTRY: Country code for log grouping (defaults to system locale)
ConfigurableLogger Parameters
ConfigurableLogger(
name="service-name", # Logger name
log_level=LogLevel.INFO, # Minimum log level
enable_cloudwatch=True, # Enable CloudWatch logging
enable_sentry=True, # Enable Sentry integration
enable_bugsnag=True, # Enable Bugsnag integration (deprecated)
cloudwatch_log_group="custom-group", # Custom CloudWatch group
include_system_info=True, # Include system information
custom_processors=[], # Additional structlog processors
# New in v1.1.0: Observability Integration
enable_otel_context=True, # Auto-inject OpenTelemetry trace_id/span_id
enable_temporal_context=True, # Auto-inject Temporal workflow/activity context
json_indent=4, # JSON indentation (None=compact, int=pretty)
)
v1.1.0 Observability Features:
enable_otel_context: Automatically injectstrace_idandspan_idwhen OpenTelemetry spans are activeenable_temporal_context: Automatically injectsworkflow_id,activity_type,task_queue, andattemptwhen running inside Temporal workflows/activitiesjson_indent: Controls JSON formatting - useNonefor compact production logs,4for pretty development logs
🔧 Advanced Usage
Enhanced Bound Context Logging (v1.1.0)
# Bind context that applies to all subsequent logs
bound_logger = logger.bind(
request_id="req_123",
session_id="sess_456"
)
# Enhanced bound logger preserves full jhlogger API
bound_logger.info("Processing request") # Includes bound context
bound_logger.error("Request failed", exception=e) # Full error handling preserved
bound_logger.warning("Rate limit approaching") # Also includes context
# Chain binding and unbinding
user_logger = bound_logger.bind(user_id=789)
final_logger = user_logger.unbind("session_id") # Remove specific context
Dynamic Log Level Changes
logger = ConfigurableLogger(log_level=LogLevel.WARNING)
logger.info("This won't show") # Below WARNING level
logger.set_level(LogLevel.DEBUG)
logger.info("Now this will show") # Now visible
Structured Data Logging
complex_data = {
"user_profile": {
"id": "usr_123",
"name": "Jane Doe",
"roles": ["admin", "user"]
},
"request_info": {
"method": "POST",
"endpoint": "/api/users",
"duration_ms": 156
}
}
logger.info("User operation completed", data=complex_data)
Multiple Logger Instances (v1.1.0)
Multiple ConfigurableLogger instances work independently, even with the same name:
# Create two loggers with the same name but different configurations
logger1 = ConfigurableLogger(name="app", log_level=LogLevel.DEBUG)
logger2 = ConfigurableLogger(name="app", log_level=LogLevel.WARNING)
# Each maintains independent log levels and handlers
logger1.debug("Debug message") # ✅ Appears (DEBUG level)
logger2.debug("Debug message") # ❌ Filtered (WARNING level)
logger1.warning("Warning message") # ✅ Appears
logger2.warning("Warning message") # ✅ Appears
# Changing one doesn't affect the other
logger1.set_level(LogLevel.CRITICAL)
logger2.warning("Still works") # ✅ Still appears from logger2
logger1.warning("Now filtered") # ❌ Now filtered from logger1
# Both show the same name in logs but are internally isolated
# logger1 uses internal name: "app.a1b2c3d4"
# logger2 uses internal name: "app.e5f6g7h8"
🔒 Security Features
- Relative File Paths: Uses relative paths instead of absolute paths to prevent system information exposure
- Sanitized Output: Avoids leaking sensitive system details in logs
- Configurable Information: Control what system information is included
- Context Suppression: Uses
contextlib.suppressfor robust error handling
🧪 Development & Testing
Development Setup
# Clone the repository
git clone https://github.com/Jacaranda-Health/jhlogger.git
cd jhlogger
# Install with UV (recommended)
uv sync --all-extras
# Or with pip
pip install -e .[dev]
# Install pre-commit hooks
pre-commit install
Available Commands
# Format code
make format
# Run linting
make lint
# Run tests
make test
# Run tests with coverage
make test-cov
# Clean build artifacts
make clean
Running Tests
# Run all tests with coverage
make test
# Run tests with detailed coverage report
make test-cov
# View coverage report
open htmlcov/index.html
Current test coverage: 87% (173 statements, 22 missing)
📋 Development Standards
- Code Formatting: Black (100 character line length)
- Import Sorting: isort (Black-compatible profile)
- Linting: flake8 with flake8-simplify
- Type Checking: mypy (when available)
- Testing: pytest with coverage reporting
- Pre-commit Hooks: Automated formatting and linting
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
🤝 Support
- 📫 Issues: GitHub Issues
- 📖 Documentation: README.md
- 🔗 Repository: GitHub Repository
🌍 Real-World Integration Example
JHLogger v1.1.0 has been successfully integrated into production Temporal workflows with full OpenTelemetry tracing. Here's a real example from a message processing system:
from jhlogger import ConfigurableLogger, LogLevel
# Configure logger with built-in observability
logger = ConfigurableLogger(
name="temporal-message-processor",
log_level=LogLevel.INFO,
enable_otel_context=True, # Automatic trace context
enable_temporal_context=True, # Automatic workflow context
json_indent=None, # Compact for production
)
# In your Temporal worker
def process_messages_workflow():
workflow_logger = logger.bind(country="KE", operation="message_processing")
workflow_logger.info("Starting message processing workflow")
# Automatically includes: trace_id, span_id, workflow_id, task_queue
Actual Production Output:
{
"trace_id": "8f0de49b683ccf048b470d109025df6e",
"span_id": "cdbe9e6318742af9",
"workflow_id": "messages-02223f6f-0c14-4853-aa31-3708f527ff3e-KE",
"task_queue": "messages_queue_KE",
"country": "KE",
"operation": "message_processing",
"event": "Starting message processing workflow",
"level": "info",
"timestamp": "2026-03-25T19:00:16.570964Z"
}
Benefits Achieved:
- ✅ 70% less code: Eliminated custom OpenTelemetry and Temporal processors
- ✅ Automatic context: No manual trace or workflow context injection needed
- ✅ Enhanced debugging: Rich caller information and structured data
- ✅ Production ready: Configurable JSON formatting and error handling
🏷️ Changelog
v1.0.0 (Initial Release)
- ✅ Configurable log levels with dynamic changes
- ✅ Structured JSON output with 4-space indentation and 100-char line length
- ✅ Full traceback for DEBUG, exception details for ERROR/CRITICAL
- ✅ Automatic caller information detection (file, function, class, line)
- ✅ Third-party service integration (CloudWatch, Sentry)
- ✅ Relative file paths for security
- ✅ Environment-aware configuration
- ✅ Comprehensive test suite (87% coverage, 36 tests)
- ✅ Pre-commit hooks for code quality
- ✅ Modern Python packaging with pyproject.toml
- ✅ UV-compatible dependency management
- ✅ Updated datetime handling (Python 3.13 compatible)
Made with ❤️ for Jacaranda Health's logging needs
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jhlogger-1.1.0.tar.gz.
File metadata
- Download URL: jhlogger-1.1.0.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
347e4174af794eb597d87568cd4a59364ac3eb9b267a9aabe0e5cdb4b6710ad7
|
|
| MD5 |
c8ca9f8061dff485c263fa4b8fdc06f2
|
|
| BLAKE2b-256 |
dcd7667d8f987061d8558564994bdc280565eae895515f498971f431accfcfb1
|
Provenance
The following attestation bundles were made for jhlogger-1.1.0.tar.gz:
Publisher:
publish-to-pypi.yml on Jacaranda-Health/jhlogger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jhlogger-1.1.0.tar.gz -
Subject digest:
347e4174af794eb597d87568cd4a59364ac3eb9b267a9aabe0e5cdb4b6710ad7 - Sigstore transparency entry: 1205885980
- Sigstore integration time:
-
Permalink:
Jacaranda-Health/jhlogger@76e19fb70820508d192e70b4a295dc1dee9d6366 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Jacaranda-Health
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@76e19fb70820508d192e70b4a295dc1dee9d6366 -
Trigger Event:
release
-
Statement type:
File details
Details for the file jhlogger-1.1.0-py3-none-any.whl.
File metadata
- Download URL: jhlogger-1.1.0-py3-none-any.whl
- Upload date:
- Size: 13.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1681cc95dbc7485f3e41169fd663e82dd05aa46fb161c467550ebc786bd4a4fc
|
|
| MD5 |
9d9f3cf3fd5901acde376b2f357f5b62
|
|
| BLAKE2b-256 |
8600257eeef08d50b7fa1a98862f4b78eb5ef3bbdf4ab49157b5647716a7f6d2
|
Provenance
The following attestation bundles were made for jhlogger-1.1.0-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on Jacaranda-Health/jhlogger
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jhlogger-1.1.0-py3-none-any.whl -
Subject digest:
1681cc95dbc7485f3e41169fd663e82dd05aa46fb161c467550ebc786bd4a4fc - Sigstore transparency entry: 1205886001
- Sigstore integration time:
-
Permalink:
Jacaranda-Health/jhlogger@76e19fb70820508d192e70b4a295dc1dee9d6366 -
Branch / Tag:
refs/tags/v1.1.0 - Owner: https://github.com/Jacaranda-Health
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@76e19fb70820508d192e70b4a295dc1dee9d6366 -
Trigger Event:
release
-
Statement type: