# Finalsa Common Logger

A robust, JSON-configured logger designed for Finalsa projects with built-in correlation ID tracking, structured logging, and AWS Lambda compatibility.

[![Tests](https://github.com/finalsa/finalsa-common-logger/actions/workflows/test.yml/badge.svg)](https://github.com/finalsa/finalsa-common-logger/actions/workflows/test.yml)
[![Coverage](https://codecov.io/gh/finalsa/finalsa-common-logger/branch/main/graph/badge.svg)](https://codecov.io/gh/finalsa/finalsa-common-logger)

## Features

- 🔗 **Correlation ID Tracking**: Automatic correlation, trace, and span ID injection
- 📊 **Structured JSON Logging**: Clean, parseable JSON output for log aggregation
- ⚡ **AWS Lambda Optimized**: Pre-configured for serverless environments
- 🌐 **Access Log Support**: Specialized formatter for HTTP access logs
- 🎯 **High Performance**: Built on `orjson` for fast JSON serialization
- 🔧 **Flexible Configuration**: Easy setup with sensible defaults
- 🧪 **Well Tested**: 98% test coverage with comprehensive integration tests

## Installation

```bash
# Using uv (recommended)
uv add finalsa-common-logger

# Using pip
pip install finalsa-common-logger
```

## Quick Start

### Basic Usage

```python
import logging
from logging.config import dictConfig
from finalsa.common.logger import LAMBDA_DEFAULT_LOGGER

# Apply the default configuration
dictConfig(LAMBDA_DEFAULT_LOGGER)

# Get a logger and start logging
logger = logging.getLogger(__name__)
logger.info("Hello, structured logging!")
logger.error("Something went wrong", extra={"user_id": 12345, "action": "login"})
```

**Output:**
```json
{"timestamp": "2024-01-15T12:30:45.123456Z", "level": "INFO", "message": "Hello, structured logging!", "correlation_id": "abc-123", "trace_id": "def-456", "span_id": "ghi-789"}
{"timestamp": "2024-01-15T12:30:46.789012Z", "level": "ERROR", "message": "Something went wrong", "user_id": 12345, "action": "login", "correlation_id": "abc-123", "trace_id": "def-456", "span_id": "ghi-789"}
```

### AWS Lambda Example

```python
import logging
from logging.config import dictConfig
from finalsa.common.logger import LAMBDA_DEFAULT_LOGGER

# Configure logging once at module level
dictConfig(LAMBDA_DEFAULT_LOGGER)
logger = logging.getLogger(__name__)

def lambda_handler(event, context):
    logger.info("Lambda invoked", extra={
        "request_id": context.aws_request_id,
        "function_name": context.function_name
    })
    
    try:
        # Your business logic here
        result = process_event(event)
        logger.info("Lambda completed successfully", extra={"result_count": len(result)})
        return {"statusCode": 200, "body": result}
    
    except Exception as e:
        logger.exception("Lambda execution failed", extra={"event": event})
        return {"statusCode": 500, "body": "Internal server error"}
```

### Access Log Example

```python
import logging
from logging.config import dictConfig
from finalsa.common.logger import LAMBDA_DEFAULT_LOGGER
from finalsa.common.logger.formatter import AccessJsonFormatter

# Setup with access log formatter
config = LAMBDA_DEFAULT_LOGGER.copy()
config['formatters']['access'] = {
    'class': 'finalsa.common.logger.formatter.AccessJsonFormatter'
}
config['handlers']['access'] = {
    'class': 'logging.StreamHandler',
    'formatter': 'access',
    'filters': ['correlation_id']
}
config['loggers']['access'] = {
    'handlers': ['access'],
    'level': 'INFO',
    'propagate': False
}

dictConfig(config)
access_logger = logging.getLogger('access')

# Log HTTP access (ip, method, path, protocol, status_code)
access_logger.info("HTTP Request", "192.168.1.100", "POST", "/api/users", "HTTP/1.1", 201)
```

**Output:**
```json
{"timestamp": "2024-01-15T12:30:45.123456Z", "level": "INFO", "client_addr": "192.168.1.100", "method": "POST", "path": "/api/users", "protocol": "HTTP/1.1", "status_code": 201, "correlation_id": "abc-123"}
```

## Configuration

### Default Configuration

The `LAMBDA_DEFAULT_LOGGER` provides a production-ready configuration:

```python
LAMBDA_DEFAULT_LOGGER = {
    'version': 1,
    'disable_existing_loggers': True,
    'filters': {
        'correlation_id': {'()': 'finalsa.common.logger.filter.CorrelationIdFilter'},
    },
    'formatters': {
        'console': {
            'class': 'finalsa.common.logger.CustomJsonFormatter',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'filters': ['correlation_id'],
            'formatter': 'console',
        },
    },
    'loggers': {
        'root': {'handlers': ['console'], 'level': 'INFO', 'propagate': True},
    },
}
```

### Custom Configuration

```python
import logging
from logging.config import dictConfig
from finalsa.common.logger import CorrelationIdFilter, CustomJsonFormatter

# Create your own configuration
CUSTOM_CONFIG = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'correlation_id': {'()': CorrelationIdFilter},
    },
    'formatters': {
        'detailed': {
            'class': CustomJsonFormatter,
            'format': '%(timestamp)s %(level)s %(name)s %(funcName)s:%(lineno)d %(message)s'
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'level': 'DEBUG',
            'filters': ['correlation_id'],
            'formatter': 'detailed',
        },
        'file': {
            'class': 'logging.FileHandler',
            'filename': 'app.log',
            'level': 'INFO',
            'filters': ['correlation_id'],
            'formatter': 'detailed',
        }
    },
    'loggers': {
        'myapp': {
            'handlers': ['console', 'file'],
            'level': 'DEBUG',
            'propagate': False
        },
        'root': {
            'handlers': ['console'],
            'level': 'WARNING',
        }
    },
}

dictConfig(CUSTOM_CONFIG)
```

## Components

### CorrelationIdFilter

Automatically injects correlation, trace, and span IDs into log records using the `finalsa-traceability` context.

```python
from finalsa.common.logger import CorrelationIdFilter

# The filter automatically adds these fields to each log record:
# - correlation_id
# - trace_id  
# - span_id
```

### CustomJsonFormatter

A JSON formatter that adds timestamps and normalizes log levels.

```python
from finalsa.common.logger import CustomJsonFormatter

formatter = CustomJsonFormatter()
# Automatically adds:
# - timestamp (ISO 8601 format with microseconds)
# - level (normalized to uppercase)
# - Converts correlation_id to string if present
```

### AccessJsonFormatter

Specialized formatter for HTTP access logs that parses request details from log record arguments.

```python
from finalsa.common.logger.formatter import AccessJsonFormatter

# Expects log record args in this order:
# (client_ip, http_method, path, protocol, status_code)
access_logger.info("Request", "192.168.1.1", "GET", "/api/health", "HTTP/1.1", 200)
```

## Integration with Finalsa Traceability

This logger integrates seamlessly with `finalsa-traceability` for distributed tracing:

```python
from finalsa.traceability import set_correlation_id, set_trace_id
from finalsa.common.logger import LAMBDA_DEFAULT_LOGGER
import logging

dictConfig(LAMBDA_DEFAULT_LOGGER)
logger = logging.getLogger(__name__)

# Set tracing context
set_correlation_id("user-session-123")
set_trace_id("request-trace-456")

# All subsequent logs will include these IDs
logger.info("Processing user request")  # Will include correlation_id and trace_id
```

## Error Handling

The logger gracefully handles missing dependencies and malformed configurations:

```python
# If finalsa-traceability is not available, correlation IDs will be None
# If log record args are malformed for AccessJsonFormatter, it raises appropriate exceptions
# The logger continues to function even with missing optional fields
```

## Development

### Setup

```bash
git clone https://github.com/finalsa/finalsa-common-logger.git
cd finalsa-common-logger
uv sync
```

### Running Tests

```bash
# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov=finalsa.common.logger --cov-report=term-missing

# Run specific test file
uv run pytest tests/test_formatter.py -v
```

### Type Checking

```bash
uv run mypy finalsa/
```

## Requirements

- Python >= 3.10
- `finalsa-traceability >= 1.0.0`
- `orjson >= 3.10.15`
- `python-json-logger == 3.3.0`

## License

MIT License - see [LICENSE.md](LICENSE.md) for details.

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes with tests
4. Run the test suite (`uv run pytest`)
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request

## Changelog

### v0.1.0
- Initial release with basic JSON logging
- Correlation ID filtering
- AWS Lambda configuration
- Access log formatter
- Comprehensive test suite
