# App Factory, Logging & Config Reference

## `application.py` — WSGI Entry Point

```python
# ddtrace MUST be patched before any other imports — this is non-negotiable.
import ddtrace
from <svc> import config

if config.ENVIRONMENT not in (config.QA_ENVIRONMENT, config.PROD_ENVIRONMENT):
    ddtrace.tracer.enabled = False
ddtrace.patch_all(futures=True, httplib=True, sqlalchemy=True)  # noqa

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from <svc> import api, config, handlers  # noqa — handler import registers routes

if config.SENTRY:
    sentry_sdk.init(
        dsn=config.SENTRY,
        integrations=[FlaskIntegration()],
        environment=config.ENVIRONMENT,
        release=config.SERVICE_VERSION,
        enable_tracing=True,
    )

app = api.app
```

**Rules:**
- `ddtrace.patch_all()` is always the first executed code, before any service imports.
- `import handlers` (and any additional `*_handlers` modules) must appear after ddtrace patching.
- The `# noqa` comment on `ddtrace.patch_all()` suppresses flake8's "import not at top" warning — do not remove it.
- `app = api.app` at the bottom is what uWSGI binds to via `--module application:app`.

## `dev.py` — Local Development Runner

```python
from <svc>.api import app

if __name__ == "__main__":
    app.run(debug=True, port=5000)
```

## `<svc>/api.py` — Flask App Factory

```python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from owslogger import flask_logger, flask_request

from <svc> import config

app = Flask(config.SERVICE_NAME)
app.config["SQLALCHEMY_DATABASE_URI"] = config.DB_URL
app.config["JSON_SORT_KEYS"] = False  # preserve key ordering in JSON responses

db = SQLAlchemy(app)

flask_logger.setup(
    app,
    config.ENVIRONMENT,
    config.LOGGER_NAME,
    config.LOGGER_LEVEL,
    config.SERVICE_NAME,
    config.SERVICE_VERSION,
    exclude_paths=[config.HEALTH_CHECK_PATH],
)
flask_request.setup(app, config.ENVIRONMENT, add_request_context=True)

# Snowflake connection pool — set up after app is created
if config.ENVIRONMENT != config.TEST_ENVIRONMENT:
    from snowflake_connector import set_default_sessionmaker
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    from sqlalchemy.pool import QueuePool

    snowflake_engine = create_engine(
        "snowflake://",
        creator=lambda: __import__('<svc>.connectors.snowflake', fromlist=['get_connection']).get_connection(),
        poolclass=QueuePool,
        pool_size=config.SNOWFLAKE_POOL_SIZE,
        pool_recycle=config.SNOWFLAKE_POOL_RECYCLE,
        max_overflow=config.SNOWFLAKE_POOL_MAX_OVERFLOW,
    )
    set_default_sessionmaker(sessionmaker(bind=snowflake_engine))

# Swagger — dev only
if config.ENVIRONMENT == config.DEV_ENVIRONMENT:
    from flasgger import Swagger
    Swagger(app, template_file=config.SWAGGER_FILE_PATH)
```

## `<svc>/config.py` — All Configuration

```python
import logging
import os
from os.path import abspath, dirname, join, pardir

from dotenv import load_dotenv

dotenv_path = abspath(join(dirname(__file__), pardir, ".env"))
load_dotenv(dotenv_path, override=True)

# ── Environments ────────────────────────────────────────────────────────────
DEV_ENVIRONMENT  = "dev"
TEST_ENVIRONMENT = "test"
QA_ENVIRONMENT   = "qa"
PROD_ENVIRONMENT = "prod"

ENVIRONMENT = os.environ.get("Environment") or DEV_ENVIRONMENT  # capital-E "Environment"

# ── Service identity ─────────────────────────────────────────────────────────
SERVICE_NAME    = "ows-<domain>"
SERVICE_VERSION = "1.0.0"
LOGGER_NAME     = "ows1"
LOGGER_LEVEL    = logging.WARNING if ENVIRONMENT == PROD_ENVIRONMENT else logging.INFO

# ── Secrets (dev: env vars; qa/prod: AWS Secrets Manager) ───────────────────
if ENVIRONMENT == DEV_ENVIRONMENT or ENVIRONMENT == TEST_ENVIRONMENT:
    DB_URL     = os.environ.get("DB_URL") or "sqlite://"
    REDIS_HOST = os.environ.get("REDIS_HOST")
    REDIS_PORT = int(os.environ.get("REDIS_PORT", 6379))
    SENTRY     = os.environ.get("SENTRY_DSN")

    # Snowflake key-pair for local dev (reads from ~/.ssh/snowflake/rsa_key.p8)
    _sf_key_path = os.path.expanduser("~/.ssh/snowflake/rsa_key.p8")
    if os.path.exists(_sf_key_path):
        from cryptography.hazmat.primitives.serialization import (
            Encoding, NoEncryption, PrivateFormat, load_pem_private_key,
        )
        with open(_sf_key_path, "rb") as _f:
            _private_key = load_pem_private_key(_f.read(), password=None)
        SNOWFLAKE_PRIVATE_KEY = _private_key.private_bytes(
            Encoding.DER, PrivateFormat.PKCS8, NoEncryption()
        )
    else:
        SNOWFLAKE_PRIVATE_KEY = None
else:
    import base64
    from secrets_manager.flask_ext import FlaskSecretsManager

    _sm = FlaskSecretsManager(
        application_context=False,
        environment=ENVIRONMENT,
        service_name=SERVICE_NAME,
    )

    DB_URL                = _sm.get_cred("DB_URL")
    REDIS_HOST            = _sm.get_cred("REDIS_HOST")
    REDIS_PORT            = int(_sm.get_cred("REDIS_PORT") or 6379)
    SENTRY                = _sm.get_cred("SENTRY_DSN")
    SNOWFLAKE_PRIVATE_KEY = base64.b64decode(_sm.get_cred("SNOWFLAKE_PRIVATE_KEY"))

    # Hard fail if Sentry is unconfigured in QA/prod — never silently skip
    if not SENTRY and ENVIRONMENT in (QA_ENVIRONMENT, PROD_ENVIRONMENT):
        raise Exception(f"Sentry is not configured in {ENVIRONMENT}")

# ── Snowflake ────────────────────────────────────────────────────────────────
SNOWFLAKE_ACCOUNT   = os.environ.get("SNOWFLAKE_ACCOUNT", "orchard")
SNOWFLAKE_USER      = os.environ.get("SNOWFLAKE_USER", "")
SNOWFLAKE_DATABASE  = os.environ.get("SNOWFLAKE_DATABASE", "")
SNOWFLAKE_WAREHOUSE = os.environ.get("SNOWFLAKE_WAREHOUSE", "")
SNOWFLAKE_ROLE      = os.environ.get("SNOWFLAKE_ROLE", "")
SNOWFLAKE_HOME      = os.environ.get("SNOWFLAKE_HOME", "/var/app/.snowflake")

SNOWFLAKE_POOL_SIZE        = 15   # matches uWSGI --cheaper-initial worker count
SNOWFLAKE_POOL_RECYCLE     = 4 * 55 * 60   # 4h 55min (Snowflake ~4hr idle timeout)
SNOWFLAKE_POOL_MAX_OVERFLOW = 5

# ── Route paths ──────────────────────────────────────────────────────────────
# All route path strings live here — never inline in @app.route(...)
HEALTH_CHECK_PATH = "/hello/"

# Example domain paths:
# MY_RESOURCE_PATH        = "/my-resource/<resource_id>"
# MY_RESOURCE_LIST_PATH   = "/my-resource/"

# ── Other constants ───────────────────────────────────────────────────────────
SWAGGER_FILE_PATH = abspath(join(dirname(__file__), pardir, "spec", f"{SERVICE_NAME}-1.0.0.yaml"))
```

## `.env.shadow` — Committed Placeholder

```bash
# Copy this file to .env and fill in values for local development.
# NEVER commit .env — it is in .gitignore.

Environment=dev

DB_URL=sqlite://
REDIS_HOST=
REDIS_PORT=6379
SENTRY_DSN=

SNOWFLAKE_ACCOUNT=orchard
SNOWFLAKE_USER=
SNOWFLAKE_DATABASE=
SNOWFLAKE_WAREHOUSE=
SNOWFLAKE_ROLE=
```

## `software-catalog.yaml`

```yaml
apiVersion: v3
kind: service
metadata:
  name: ows-<domain>
  description: "<One-sentence description of what this service does>"
  owner: insights
  tags:
    - public:false
spec:
  type: web
```

## Critical Config Rules

1. `ENVIRONMENT` reads from the env var named `"Environment"` (capital E) — this is the Orchard convention.
2. In `TEST_ENVIRONMENT`, `DB_URL` must be `sqlite://` so SQLAlchemy uses in-memory SQLite.
3. `SENTRY` must raise at startup if unset in QA/prod — never log a warning and continue.
4. `SNOWFLAKE_POOL_SIZE` must equal the uWSGI `--cheaper-initial` worker count (both default to 15) to prevent connection exhaustion.
5. `SNOWFLAKE_HOME` env var must be set — Snowflake connector 4.4.0+ writes files there; without it, it writes to `/root` which may not be accessible as the `uwsgi` user.
