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.1: Environment-controlled compact JSON output, caller metadata, and direct CloudWatch shipping for lower CloudWatch ingestion costs.

🌟 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: Captures filename, function name, class name, and line number when enabled for debugging
  • 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

  • Logs are formatted as JSON with compact production/staging output by default
  • Pretty JSON is still available for local debugging
  • Consistent structure across all log entries with sorted keys
  • Support for arbitrary data fields

Third-Party Service Integration

  • CloudWatch: Optional direct CloudWatch logging with environment-variable controls
  • 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) is opt-in for deployed environments
  • 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)
  • JHLOGGER_JSON_INDENT: JSON indentation. Use none, null, or compact for one-line JSON.
  • JHLOGGER_COMPACT_JSON or LOG_COMPACT_JSON: Boolean override for compact JSON output.
  • JHLOGGER_INCLUDE_CALLER or LOG_INCLUDE_CALLER: Boolean override for caller metadata.
  • JHLOGGER_INCLUDE_SYSTEM_INFO or LOG_INCLUDE_SYSTEM_INFO: Boolean override for system metadata.
  • DISABLE_CLOUDWATCH: Set to true to disable direct CloudWatch shipping.
  • JHLOGGER_ENABLE_CLOUDWATCH or ENABLE_DIRECT_CLOUDWATCH_LOGS: Boolean override for direct CloudWatch shipping.

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
    include_caller_info=None,               # Env-aware caller metadata default
    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=None,                       # None=compact, int=pretty
)

By default, production and staging use compact one-line JSON and omit caller metadata. Development and test environments keep pretty JSON and caller metadata for easier debugging.

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.1.tar.gz (31.7 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.1-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

File details

Details for the file jhlogger-1.1.1.tar.gz.

File metadata

  • Download URL: jhlogger-1.1.1.tar.gz
  • Upload date:
  • Size: 31.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jhlogger-1.1.1.tar.gz
Algorithm Hash digest
SHA256 f091f1a89d17c32a61768522d07891495e99f322b4784c79b513fec8e51b0115
MD5 20e54e2f1d3fb5411138e96cfd05d76b
BLAKE2b-256 6d46bca31daf9bc3e220c24b19cefdc8fd6041678814fdfb14b515d568c321ae

See more details on using hashes here.

Provenance

The following attestation bundles were made for jhlogger-1.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: jhlogger-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jhlogger-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4861f19e0f563cb3920ad5ce167900a66c1034b96cf7bb9257938d2b5169ac85
MD5 b5b1a7ead25300ea961051bc88692e06
BLAKE2b-256 003f9571ef716a4c52ed56a2959ddafd258cbb86ec6350be16b5585916c48f09

See more details on using hashes here.

Provenance

The following attestation bundles were made for jhlogger-1.1.1-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