# Logging Configuration - Developer Guide

## Configuration

**Architecture:** loguru with automatic daily rotation

```python
# src/logging_config.py (already configured)
from loguru import logger

def setup_logging(log_dir: str = "logs"):
    logger.add(
        f"{log_dir}/mater_{{time:YYYY-MM-DD}}.log",
        rotation="10 MB",      
        retention=10,          
        level="DEBUG",
        format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
        backtrace=True,        
        diagnose=True,         
    )
```

**Initialization:** Automatic in `cli.py` at startup of any command.

---

## Adding logging to a new function

### Command functions (CLI entry points)

**Ultra-simple pattern in 3 lines:**

```python
from loguru import logger

@logger.catch  # ← 1. Add decorator
def your_command(config):
    try:
        # Your code
    except SpecificError as e:
        click.echo("❌ User message")
        raise  # ← 2. Add raise after click.echo()
```

### Utility functions (non-CLI)

**Let exceptions propagate naturally:**

```python
# ✅ GOOD - Utility function
def load_mapping_file(path):
    # No try/except - let exceptions bubble up
    data = json.load(path.open())
    return data

# ❌ BAD - Don't do this in utilities
def load_mapping_file(path):
    try:
        data = json.load(path.open())
        return data
    except json.JSONDecodeError as e:
        click.echo(f"❌ Invalid JSON: {e}")  # ← Don't add user messages here
        raise
```

**Rule:** `click.echo()` + `raise` **only in command functions** (CLI entry points).

---

## Rules

### ✅ Do

1. **Decorator**: `@logger.catch` on all public command functions
2. **Raise**: After each `click.echo()` in command function except blocks
3. **Console messages**: Keep formatted messages for users
4. **Utility functions**: Let exceptions propagate naturally (no try/except)

### ❌ Don't

1. `logger.error()` in except blocks (duplicate with @logger.catch)
2. Remove existing try/except from command functions (user messages are important)
3. Forget the `raise` (logging won't happen)
4. Add `click.echo()` in utility functions (only in commands)

---

## Examples

### Simple function (no specific exceptions)

```python
from loguru import logger

@logger.catch
def process_data(data):
    # Normal code - all exceptions automatically logged
    result = transform(data)
    return result
```

### Command function with formatted messages

```python
from loguru import logger

@logger.catch
def validate_config(config):
    try:
        # Validation logic
        check_paths(config)
        
    except ValidationError as e:
        # Formatted message for user
        click.echo("❌ Configuration validation error:")
        for error in e.errors():
            click.echo(f"  {error['loc']}: {error['msg']}")
        raise  # Logger captures automatically with full traceback
        
    except FileNotFoundError as e:
        # Message + instructions for user
        click.echo(f"⚠️ File not found: {e}")
        click.echo("💡 Run: mater-cli init")
        raise  # Logger captures automatically
```

### Utility function (called by command)

```python
# No decorator needed - exception bubbles up to command function
def load_dimensions_from_input(path: Path) -> List[Dict]:
    """Load dimensions from input data - let exceptions propagate"""
    if not path.exists():
        raise FileNotFoundError(f"Path does not exist: {path}")
    
    data = json.loads(path.read_text())  # JSONDecodeError propagates naturally
    return extract_dimensions(data)
```

---

## Function hierarchy

```
CLI Command (with @logger.catch + try/except + click.echo + raise)
  └─> Utility function (no decorator, no try/except - just raise)
      └─> Helper function (no decorator, no try/except - just raise)
          └─> Exception bubbles up to CLI Command
              └─> Logger captures with full traceback
```

**Example:**

```python
# Utility - no logging code
def load_json(path):
    return json.loads(path.read_text())  # Exception propagates

# Command - logging code here
@logger.catch
def map_dimensions(config):
    try:
        data = load_json(config.path)  # JSONDecodeError bubbles up here
    except json.JSONDecodeError as e:
        click.echo(f"❌ Invalid JSON: {e}")  # User message
        raise  # Logger captures
```

---

## Result

**Console (user):**
```
❌ Configuration validation error:
  simulation.start_time: Field required
```

**Logs (developer) - `logs/mater_2025-01-16.log`:**
```
2025-01-16 14:23:45 | ERROR | mater.validation:validate_config:42
Traceback (most recent call last):
  File "validation.py", line 35, in validate_config
    check_paths(config)
    └ <MaterConfig start_time=None>
pydantic.ValidationError: Field required
[COMPLETE TRACEBACK WITH VARIABLES AND MATER CONTEXT]
```

---

## Special cases

### External scripts (subprocess)

To capture stdout/stderr from external Python scripts:

```python
if result.returncode != 0:
    logger.error(
        f"Script failed: {script.name}\n"
        f"STDOUT: {result.stdout}\n"
        f"STDERR: {result.stderr}"
    )
```

**Note:** `logger.error()` is OK here because we're capturing output from an external process, not a Python exception.

### Mentioning logs to users

**General rule:** Don't mention logs to users - they want actionable instructions.

```python
# ✅ GOOD - Clear, actionable message
except FileNotFoundError as e:
    click.echo(f"❌ File not found: {e}")
    click.echo("💡 Run: mater-cli dimensions map")
    raise

# ❌ BAD - Verbose, not actionable for end users
except FileNotFoundError as e:
    click.echo(f"❌ File not found: {e}")
    click.echo("💡 Run: mater-cli dimensions map")
    click.echo(f"📋 See logs/mater_{date}.log for details")  # Unnecessary
    raise

# ✅ ACCEPTABLE - Only for truly unexpected errors
except Exception as e:
    click.echo(f"❌ Unexpected error: {e}")
    click.echo(f"📋 Check logs/mater_{datetime.now():%Y-%m-%d}.log for details")
    raise
```

**Principle:** Logs are for developers, not end users.

---

## Verification

To test that logging works:

```bash
# Run command
uv run mater-cli simulation run

# Check logs
cat logs/mater_$(date +%Y-%m-%d).log
```

You should see complete traceback with variable values on error.

---

## Summary

| Function type | Pattern |
|--------------|---------|
| **CLI command** | `@logger.catch` + try/except + `click.echo()` + `raise` |
| **Utility function** | No logging code - let exceptions propagate |
| **Helper function** | No logging code - let exceptions propagate |

**Golden rule:** Only CLI entry points handle user messages and logging. Everything else just raises.

---

## References

- **loguru docs:** https://loguru.readthedocs.io
- **@logger.catch:** https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.catch