# 04 — `MgfSettings`: the library's own typed config

`MgfSettings` is the typed root for every behavioural knob in
`mgf-common` itself: logging level, OTel endpoint, redaction
keys, vault backend, etc. Subclass of
[`BaseAppSettings`](03_config.md). This doc also covers the
canonical "composing your app's settings with `MgfSettings`"
pattern (subsumes the old `COMPOSING_SETTINGS.md`).

> **Read first:** [`library_common.md`](library_common.md),
> [`03_config.md`](03_config.md).

---

## Motivation

Pre-, library behaviour was scattered across function args
(`setup_logging(level=..., file=..., redactor=...)`), env vars
(`<APP>_LOG_HTTP_SINK`, `<APP>_CRASH_SINK`), and module
constants. No single place to ask "what will the library do?"

`MgfSettings` is the single typed root. Five sub-models
(`logging` / `otel` / `crash` / `redaction` / `vault`) cover
every knob; the diagnostic CLI's `mgf-common explain` prints
the resolved state with each field's source (default / preset /
env / pyproject / init).

---

## Goal

```python
from mgf.common.settings import MgfSettings, OtelSettings

settings = MgfSettings.production(
    otel=OtelSettings(enabled=True, endpoint="...", sample_rate=0.05),
)
```

Pass it to `bootstrap(settings=...)`. Done.

For more knobs, set fields directly. For env-var overrides,
prefix with `MGF_*` (single or double underscore both work).

---

## Quick start

### Factory shortcuts

Three intent-driven classmethods bundle named scenarios with
sensible defaults. User-supplied kwargs always win.

```python
from mgf.common.settings import MgfSettings

MgfSettings.dev() # DEBUG + text logging, OTel off
MgfSettings.production() # explicit preset, JSON logging, OTel sample 0.05
MgfSettings.testing() # WARNING-level logging, OTel off, deterministic
```

### Fine-grained construction

```python
from mgf.common.settings import (
    MgfSettings,
    LoggingSettings,
    OtelSettings,
    RedactionSettings,
)

settings = MgfSettings(
    logging=LoggingSettings(
        level="INFO",
        format="json",
        level_overrides={"urllib3.connectionpool": "WARNING"},
    ),
    otel=OtelSettings(
        enabled=True,
        endpoint="http://otel-collector:4318",
        sample_rate=0.1,
    ),
    redaction=RedactionSettings(
        extra_keys=frozenset({"db_password", "vault_key"}),
        strict_mode=False,
    ),
)
```

### Env-var overrides

```bash
# Single underscore (+ ergonomic)
export MGF_LOGGING_LEVEL=DEBUG
export MGF_OTEL_SERVICE_NAMESPACE=my-team

# Double underscore (legacy -; still supported)
export MGF_LOGGING__LEVEL=DEBUG

# Both forms work — longest-field-name-first parsing
# disambiguates field names containing underscores.
```

### `[tool.mgf]` in pyproject.toml

```toml
# pyproject.toml
[tool.mgf]
preset = "production"

[tool.mgf.logging]
level = "INFO"
format = "json"

[tool.mgf.otel]
enabled = true
sample_rate = 0.05
```

`MgfSettings()` walks up from CWD looking for `pyproject.toml`.
Disable for hermetic tests with `MGF_DISABLE_PYPROJECT=1`.

---

## Public API

### `MgfSettings` and sub-models

| Name | Purpose |
|---|---|
| [`MgfSettings`](../../PUBLIC_API.md#mgfcommonsettings) | The root. Composes the five sub-models + `preset`. |
| [`LoggingSettings`](../../PUBLIC_API.md#mgfcommonsettings) | `level / format / file / journald / syslog_address / http_sink / rotating_max_bytes / rotating_backup_count / level_overrides`. |
| [`OtelSettings`](../../PUBLIC_API.md#mgfcommonsettings) | `enabled / endpoint / service_namespace / sample_rate / resource_attributes / httpx_instrumented / subprocess_instrumented / metrics_enabled`. |
| [`CrashSettings`](../../PUBLIC_API.md#mgfcommonsettings) | `sinks / sentry_dsn / otlp_endpoint / http_url / state_dir`. |
| [`RedactionSettings`](../../PUBLIC_API.md#mgfcommonsettings) | `extra_keys / extra_value_patterns / enabled_pattern_groups / strict_mode`. |
| [`VaultSettings`](../../PUBLIC_API.md#mgfcommonsettings) | `backend / config` (typed configs preferred — see [`07_vault.md`](07_vault.md)). |
| [`LogLevel`](../../PUBLIC_API.md#mgfcommonsettings) / [`LogFormat`](../../PUBLIC_API.md#mgfcommonsettings) / [`PresetName`](../../PUBLIC_API.md#mgfcommonsettings) | Type aliases. |

### Presets

`PresetName = Literal["explicit", "dev", "systemd", "docker", "auto"]`

| Preset | Behaviour |
|---|---|
| `explicit` | No auto-detection; every field at its declared default. |
| `dev` | DEBUG + text logging; journald off; OTel off. |
| `systemd` | JSON + journald. |
| `docker` | JSON; no file sink (container runtime captures stdout). |
| `auto` | detection: TTY → text/json; `JOURNAL_STREAM` → journald; `OTEL_EXPORTER_OTLP_ENDPOINT` → OTel enabled. Default for `bootstrap()` with no explicit settings. |

Composable: `MgfSettings(preset=["docker", "auto"])` applies
left-to-right, later wins on field conflicts.

Code:
[`src/mgf/common/settings.py`](../../src/mgf/common/settings.py).

---

## Composing with your app's settings

Your app has its own typed settings (database URLs, feature
flags). The canonical pattern: **compose**, don't subclass
`MgfSettings`.

```python
# myapp/settings.py
from typing import Annotated
from mgf.common.config import BaseAppSettings, Field, settings_config
from mgf.common.settings import MgfSettings

class AppSettings(BaseAppSettings):
    model_config = settings_config(
        env_prefix="MYAPP_",
        env_nested_delimiter="__",
    )

    # Domain fields:
    database_url: str = "sqlite:///./app.db"
    request_timeout_seconds: Annotated[int, Field(ge=1, le=300)] = 30

    # mgf-common's settings as a sub-field:
    mgf: MgfSettings = Field(default_factory=MgfSettings)
```

```python
# myapp/main.py
from mgf.common import bootstrap
from myapp.settings import AppSettings

def main() -> int:
    app_settings = AppSettings()
    with bootstrap(settings=app_settings.mgf) as ctx:
        run_app_logic(app_settings, ctx)
        return 0
```

### Why composition (not subclassing)

- `MgfSettings` may add new sub-models in future minors. A
  subclass inherits them automatically — your config file
  silently grows new sections without your code asking for
  them.
- `MgfSettings.env_prefix = "MGF_"`. Your subclass either
  inherits it (your domain field becomes `MGF_DATABASE_URL` —
  confusing) or overrides it (now `MgfSettings`'s own fields
  read from `MYAPP_LOGGING__LEVEL` instead of
  `MGF_LOGGING__LEVEL`, drifting from the published env
  contract).

Composition sidesteps both. `AppSettings` owns the `MYAPP_`
namespace; `mgf-common`'s sub-field gets `MYAPP_MGF__*` env
vars (one-level nesting) — unambiguous.

### Config-file shape

```yaml
# ~/.config/myapp/config.yaml
database_url: postgresql://localhost/myapp
request_timeout_seconds: 60

mgf:
  logging:
    level: INFO
    format: json
  otel:
    enabled: true
    endpoint: http://otel-collector:4318
```

The `mgf:` section is exactly what `MgfSettings` accepts at
construction.

### Env-var aliases

```bash
# Domain fields
export MYAPP_DATABASE_URL=postgresql://prod-db/myapp

# mgf-common's sub-fields (one level of nesting)
export MYAPP_MGF__LOGGING__LEVEL=WARNING
export MYAPP_MGF__OTEL__SAMPLE_RATE=0.05
```

---

## What NOT to do

### Don't subclass `MgfSettings`

```python
# DON'T:
class AppSettings(MgfSettings):
    database_url: str = "..."  # inherits MGF_ prefix; future-incompat
```

### Don't pass the whole `AppSettings` to `bootstrap()`

```python
# DON'T:
with bootstrap(settings=app_settings): # wrong type — bootstrap wants MgfSettings
    ...

# DO:
with bootstrap(settings=app_settings.mgf):
    ...
```

### Don't read `MgfSettings.logging.level` for runtime decisions

```python
# DON'T:
if app_settings.mgf.logging.level == "DEBUG":
    log.debug("...")

# DO:
if log.isEnabledFor(logging.DEBUG):
    log.debug("...")
```

The standard pattern respects per-logger level overrides AND
any handler filters. `MgfSettings.logging.level` is the
configuration input; the runtime check belongs to the logging
hierarchy.

---

## Diagnostic CLI

```bash
mgf-common explain # active settings with provenance
mgf-common explain --preset dev # what would dev look like?
mgf-common validate config.yaml # schema-validate without applying
mgf-common doctor # install + wiring health checks
```

See [`09_cli.md`](09_cli.md).

---

## Links

- **Public API:** [`PUBLIC_API.md` `mgf.common.settings` section](../../PUBLIC_API.md#mgfcommonsettings)
- **Engineering standards:**
  [`CONFIGURATION.md`](../standards/CONFIGURATION.md) (CF-01..CF-12)
- **Examples:** [`examples/02_config/`](../../examples/02_config/)
  — three layers, plus the `[tool.mgf]` pyproject.toml
  pattern in `examples/06_full_app/`
- **Reference consumer:**
  [`example_app/src/example_app/settings.py`](../../example_app/src/example_app/settings.py)
  — composition pattern in a real shape
- **Code:**
  [`src/mgf/common/settings.py`](../../src/mgf/common/settings.py),
  [`_env_source.py`](../../src/mgf/common/_env_source.py),
  [`config/_pyproject_source.py`](../../src/mgf/common/config/_pyproject_source.py)
- **Related docs:**
  - [`03_config.md`](03_config.md) — `BaseAppSettings` (the
    base) + atomic loader
  - [`01_lifecycle.md`](01_lifecycle.md) —
    `bootstrap(settings=...)`
  - [`09_cli.md`](09_cli.md) — `mgf-common explain` provenance
