Skip to main content

A feature-rich, configurable logging module with structured JSON output

Project description

JH Logger

Python Support License: MIT Code style: black

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_id and span_id injection 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_ENV or FLASK_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 injects trace_id and span_id when OpenTelemetry spans are active
  • enable_temporal_context: Automatically injects workflow_id, activity_type, task_queue, and attempt when running inside Temporal workflows/activities
  • json_indent: Controls JSON formatting - use None for compact production logs, 4 for 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.suppress for 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

🌍 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jhlogger-1.1.0.tar.gz (29.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jhlogger-1.1.0-py3-none-any.whl (13.6 kB view details)

Uploaded Python 3

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

Hashes for jhlogger-1.1.0.tar.gz
Algorithm Hash digest
SHA256 347e4174af794eb597d87568cd4a59364ac3eb9b267a9aabe0e5cdb4b6710ad7
MD5 c8ca9f8061dff485c263fa4b8fdc06f2
BLAKE2b-256 dcd7667d8f987061d8558564994bdc280565eae895515f498971f431accfcfb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for jhlogger-1.1.0.tar.gz:

Publisher: publish-to-pypi.yml on Jacaranda-Health/jhlogger

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for jhlogger-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1681cc95dbc7485f3e41169fd663e82dd05aa46fb161c467550ebc786bd4a4fc
MD5 9d9f3cf3fd5901acde376b2f357f5b62
BLAKE2b-256 8600257eeef08d50b7fa1a98862f4b78eb5ef3bbdf4ab49157b5647716a7f6d2

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page