# Recipe — Framework-owned logging (`bootstrap(setup_logging=False)`)

> **Audience:** any consumer whose application framework owns
> its own logging stack — Django (`LOGGING` dictConfig), FastAPI
> behind uvicorn, Flask (`app.logger` / dictConfig), Pyramid,
> aiohttp, Celery workers with framework-side handlers, etc.

This recipe pairs with **BOOTSTRAP-01** (closed in v0.19.0).
The pattern is: let the framework own logging; opt out of
mgf-common's handler install; keep every other piece of
`bootstrap()`'s wiring.

---

## When to use this recipe

Choose `setup_logging=False` when **any** of these is true:

- Your framework's settings module installs handlers at import
  time (Django's `LOGGING` dictConfig fires when the settings
  module loads — before any `AppConfig.ready()` runs).
- Your runtime owns log capture (uvicorn / gunicorn / nginx
  + supervisord / systemd's `StandardOutput=journal+console`
  pipeline).
- Your project pins a deployment-coupled JSON wire shape that
  diverges from mgf-common's built-in shape (Grafana Cloud
  parsers, Datadog log pipelines, ELK index templates, etc.).
- You've already invested in a framework-native handler stack
  and don't want a second copy of every record.

Choose the default `setup_logging=True` when:

- CLI / desktop / non-framework consumers.
- Greenfield service whose logging story is "whatever
  mgf-common ships."
- Standalone scripts and notebooks.

Either way, **identity, OTel, excepthooks, AppContext
registration, the bootstrap-log-buffer replay, and the
`MgfDeprecationWarning` machinery all wire unchanged**. The
flag only controls handler installation + per-component
`level_overrides`.

---

## What `setup_logging=False` preserves

| Wiring step | `setup_logging=True` (default) | `setup_logging=False` |
|---|:-:|:-:|
| Identity (`app_name` / `app_version`) | ✓ | ✓ |
| `AppContext` registration | ✓ | ✓ |
| OTel auto-init (when configured) | ✓ | ✓ |
| `sys.excepthook` + `threading.excepthook` | ✓ | ✓ |
| Bootstrap log buffer (early-record capture) | ✓ | ✓ |
| Bootstrap log buffer **replay** | ✓ (through mgf handlers) | ✓ (through framework handlers) |
| StreamHandler + RotatingFileHandler install | ✓ | ✗ |
| `level_overrides` per logger | ✓ | ✗ |
| `MgfSettings.redaction.*` redactor wiring | ✓ | ✗ |

Two consequences worth naming explicitly:

1. **OTel auto-init still fires.** Setting `setup_logging=False`
   does not disable spans / traces / metrics. Your service
   still emits to the OTLP endpoint (or whatever the
   `MgfSettings.otel.*` config says). This is usually what you
   want — framework consumers typically want OTel even when
   they own logging.
2. **Redaction does NOT install on framework handlers
   automatically.** The `MgfSettings.redaction.*` knobs only
   affect mgf-common's own handlers. If your framework's
   handlers also need redaction, wire `mgf.common.observability.
   redaction.Redactor` into the framework's formatter directly
   — see the [redaction recipe](redaction.md) (when it lands).

---

## Django (`AppConfig.ready()`)

Django's `LOGGING` dictConfig fires at settings-module-import
time, well before any `AppConfig.ready()` callback. By the time
mgf-common's bootstrap runs, the framework's handlers are
already on root. You want `bootstrap()` to leave them alone.

```python
# config/settings/prod.py — your existing dictConfig stays as-is.
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "json": {"()": "apps.core.logging.JsonFormatter"},
    },
    "handlers": {
        "console": {"class": "logging.StreamHandler", "formatter": "json"},
    },
    "loggers": {"": {"handlers": ["console"], "level": "INFO"}},
}

# apps/core/apps.py — bootstrap parks here.
from django.apps import AppConfig


class CoreConfig(AppConfig):
    name = "apps.core"
    _mgf_ctx = None  # AppContext, populated in ready()

    def ready(self) -> None:
        if self._mgf_ctx is not None:
            return  # idempotent; ready() can fire twice in tests
        from mgf.common import bootstrap
        from mgf.common.settings import MgfSettings

        self._mgf_ctx = bootstrap(
            app_name="myapp",
            app_version=__version__,
            settings=MgfSettings.production(),
            setup_logging=False,  # Django LOGGING owns the handler stack
        )
```

The `_mgf_ctx is not None` guard handles Django's
`AppConfig.ready()` being legitimately called twice (test
runners, `runserver`'s autoreloader). For shutdown,
`atexit.register(self._mgf_ctx.shutdown)` from the same
`ready()` flushes OTel exporters cleanly on process exit.

---

## FastAPI (lifespan context manager)

FastAPI's logging story varies — uvicorn configures its own
loggers for access + error; the application can layer on a
project formatter via `logging.config.dictConfig` at module
import. If you've gone the dictConfig route, the same pattern
as Django applies; bootstrap inside the `lifespan`:

```python
# app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from mgf.common import bootstrap
from mgf.common.settings import MgfSettings

from . import __version__


@asynccontextmanager
async def lifespan(app: FastAPI):
    ctx = bootstrap(
        app_name="myapp",
        app_version=__version__,
        settings=MgfSettings.production(),
        setup_logging=False,  # uvicorn / project dictConfig owns handlers
    )
    try:
        yield
    finally:
        ctx.shutdown()


app = FastAPI(lifespan=lifespan)
```

Two notes specific to FastAPI:

- The lifespan runs ONCE per process. Tests using
  `TestClient(app)` enter the lifespan; pytest fixtures that
  share a `TestClient` across tests share the bootstrap.
- Combine with `mgf.common.fastapi.ExceptionTranslationMiddleware`
  + the HTTP-01 hierarchy (see [http recipe](http.md)) — the
  middleware reads `HttpError.http_status` automatically (closed
  PAPER-24 in v0.19.0).

---

## Flask (app factory)

Flask's `app.logger` is a propagating logger; if you've
configured root-level handlers via `logging.config.dictConfig`
at app-factory time (or via the `logging` field of a Flask
extension), bootstrap should not touch them.

```python
# app/__init__.py
from flask import Flask
from mgf.common import bootstrap
from mgf.common.settings import MgfSettings


def create_app() -> Flask:
    app = Flask(__name__)
    _configure_logging(app)  # your project's logging setup

    app._mgf_ctx = bootstrap(
        app_name="myapp",
        app_version=app.config["VERSION"],
        settings=MgfSettings.production(),
        setup_logging=False,  # project dictConfig owns it
    )

    @app.teardown_appcontext
    def _shutdown_mgf(exception):  # noqa: ARG001
        # Run on actual process shutdown via atexit; this
        # teardown is per-request and a no-op for bootstrap.
        return None

    return app
```

Register `atexit.register(app._mgf_ctx.shutdown)` separately
(in your WSGI entry point or `wsgi.py`) so OTel batchers flush
on shutdown.

---

## What you give up

`setup_logging=False` is a deliberate opt-out. Three pieces of
mgf-common's logging story are NOT installed:

1. **The JSON wire shape** (`{ts, level, logger, msg, +flat
   extras}`). Your framework's formatter ships whatever shape
   it ships. If you want mgf-common's shape, you wire
   `mgf.common.observability.JsonFormatter` into your
   framework's handler config directly.
2. **Per-component `level_overrides`.** If you set
   `MgfSettings.logging.level_overrides = {"urllib3": "WARNING"}`
   and pass `setup_logging=False`, the override is silently
   skipped. Set per-logger levels through your framework's
   config instead (Django's `LOGGING["loggers"]`, etc.).
3. **The redaction layer attached to mgf's handlers.** Your
   framework's handlers don't go through `Redactor` unless you
   wire it in yourself.

These trade-offs are usually acceptable: framework consumers
own their wire shape and per-logger levels for deployment
reasons (parsers, log pipelines), and redaction can be applied
at the formatter level without mgf-common's handler.

---

## Cross-references

- **BOOTSTRAP-01** (FEEDBACK.md, shipped v0.19.0) — the upstream
  change that introduced the flag. Read for the original design
  rationale.
- **v0.19.0 cutover guide** (`docs/cutover/v0.19.0.md`
  §BOOTSTRAP-01) — original consumer-facing migration text;
  this recipe is the durable promotion.
- [`docs/standards/web/README.md`](../../standards/web/README.md)
  — front door for web-domain conventions; lists this recipe
  alongside the cloud-native logging + framework-native config
  + HTTP-01 recipes.
- [`docs/public/recipes/cloud-native-logging.md`](cloud-native-logging.md)
  — pairs with this recipe when the framework's handlers
  emit JSON to stdout (LG-06 cloud-native branch).
- `mgf.common.bootstrap` — the function, with its full
  docstring covering `setup_logging` parametrically.
