Metadata-Version: 2.4
Name: jhlogger
Version: 1.1.0
Summary: A feature-rich, configurable logging module with structured JSON output
Home-page: https://github.com/Jacaranda-Health/jhlogger
Author: JH Dev
Author-email: JH Dev <dev@jacarandahealth.org>
License: MIT
Project-URL: Homepage, https://github.com/Jacaranda-Health/jhlogger
Project-URL: Bug Tracker, https://github.com/Jacaranda-Health/jhlogger/issues
Project-URL: Documentation, https://github.com/Jacaranda-Health/jhlogger/blob/main/README.md
Project-URL: Source Code, https://github.com/Jacaranda-Health/jhlogger
Keywords: logging,json,structured,cloudwatch,sentry,configurable,traceback,debug,monitoring,opentelemetry,temporal
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: structlog>=23.0.0
Requires-Dist: watchtower>=3.0.0
Requires-Dist: sentry-sdk>=1.0.0
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.0.0; extra == "otel"
Provides-Extra: temporal
Requires-Dist: temporalio>=1.0.0; extra == "temporal"
Provides-Extra: observability
Requires-Dist: opentelemetry-api>=1.0.0; extra == "observability"
Requires-Dist: temporalio>=1.0.0; extra == "observability"
Provides-Extra: dev
Requires-Dist: black>=24.8.0; extra == "dev"
Requires-Dist: flake8>=5.0.4; extra == "dev"
Requires-Dist: flake8-simplify>=0.22.0; extra == "dev"
Requires-Dist: isort>=5.13.2; extra == "dev"
Requires-Dist: pre-commit>=3.5.0; extra == "dev"
Requires-Dist: pytest>=8.3.5; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: mypy>=1.14.1; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=5.0.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == "docs"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# JH Logger

[![Python Support](https://img.shields.io/badge/python-3.8%2B-blue)](https://python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/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

```bash
# 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

```bash
git clone https://github.com/Jacaranda-Health/jhlogger.git
cd jhlogger
uv sync --all-extras
```

### With UV (recommended)

```bash
# Create virtual environment and install dependencies
uv sync

# For development with all extras
uv sync --all-extras
```

### With Pip (Local Development)

```bash
# 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

```python
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

```python
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

```python
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:**
```json
{
    "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

```python
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

```json
{
  "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)

```json
{
  "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)

```json
{
  "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

```python
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)

```python
# 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

```python
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

```python
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:

```python
# 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

```bash
# 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

```bash
# 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

```bash
# 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](LICENSE) file for details.

## 🤝 Support

- 📫 **Issues**: [GitHub Issues](https://github.com/Jacaranda-Health/jhlogger/issues)
- 📖 **Documentation**: [README.md](https://github.com/Jacaranda-Health/jhlogger/blob/main/README.md)
- 🔗 **Repository**: [GitHub Repository](https://github.com/Jacaranda-Health/jhlogger)

## 🌍 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:

```python
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:**
```json
{
    "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**
