diff --git a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/config.py b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/config.py index 10726c0..f1ff2e3 100644 --- a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/config.py +++ b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/config.py @@ -16,8 +16,16 @@ class Config(UWSAppSettings): """Configuration for example-uws.""" + model_config = SettingsConfigDict( + env_prefix="EXAMPLE_UWS_", case_sensitive=False + ) + name: str = Field("example-uws", title="Name of application") + log_level: LogLevel = Field( + LogLevel.INFO, title="Log level of the application's logger" + ) + path_prefix: str = Field( "/example-uws", title="URL prefix for application" ) @@ -26,12 +34,10 @@ class Config(UWSAppSettings): Profile.development, title="Application logging profile" ) - log_level: LogLevel = Field( - LogLevel.INFO, title="Log level of the application's logger" - ) - - model_config = SettingsConfigDict( - env_prefix="EXAMPLE_UWS_", case_sensitive=False + slack_webhook: SecretStr | None = Field( + None, + title="Slack webhook for alerts", + description="If set, alerts will be posted to this Slack webhook", ) @property diff --git a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/external.py b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/external.py index 1182a15..b4c418e 100644 --- a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/external.py +++ b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/external.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends from safir.dependencies.logger import logger_dependency from safir.metadata import get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from structlog.stdlib import BoundLogger from ..config import config @@ -12,7 +13,7 @@ __all__ = ["external_router"] -external_router = APIRouter() +external_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all external handlers.""" diff --git a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/internal.py b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/internal.py index 8096f14..bec3a34 100644 --- a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/internal.py +++ b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/handlers/internal.py @@ -10,12 +10,13 @@ from fastapi import APIRouter from safir.metadata import Metadata, get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from ..config import config __all__ = ["internal_router"] -internal_router = APIRouter() +internal_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all internal handlers.""" diff --git a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/main.py b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/main.py index f7bbcf1..7c1f2ca 100644 --- a/project_templates/fastapi_safir_app/example-uws/src/exampleuws/main.py +++ b/project_templates/fastapi_safir_app/example-uws/src/exampleuws/main.py @@ -15,6 +15,7 @@ from safir.dependencies.http_client import http_client_dependency from safir.logging import configure_logging, configure_uvicorn_logging from safir.middleware.x_forwarded import XForwardedMiddleware +from safir.slack.webhook import SlackRouteErrorHandler from .config import config, uws from .handlers.external import external_router @@ -65,3 +66,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # Install error handlers. uws.install_error_handlers(app) + +# Configure Slack alerts. +if config.slack_webhook: + logger = structlog.get_logger("exampleuws") + SlackRouteErrorHandler.initialize( + config.slack_webhook, "example-uws", logger + ) + logger.debug("Initialized Slack webhook") diff --git a/project_templates/fastapi_safir_app/example/src/example/config.py b/project_templates/fastapi_safir_app/example/src/example/config.py index 72ac076..38640eb 100644 --- a/project_templates/fastapi_safir_app/example/src/example/config.py +++ b/project_templates/fastapi_safir_app/example/src/example/config.py @@ -12,8 +12,16 @@ class Config(BaseSettings): """Configuration for example.""" + model_config = SettingsConfigDict( + env_prefix="EXAMPLE_", case_sensitive=False + ) + name: str = Field("example", title="Name of application") + log_level: LogLevel = Field( + LogLevel.INFO, title="Log level of the application's logger" + ) + path_prefix: str = Field( "/example", title="URL prefix for application" ) @@ -22,12 +30,10 @@ class Config(BaseSettings): Profile.development, title="Application logging profile" ) - log_level: LogLevel = Field( - LogLevel.INFO, title="Log level of the application's logger" - ) - - model_config = SettingsConfigDict( - env_prefix="EXAMPLE_", case_sensitive=False + slack_webhook: SecretStr | None = Field( + None, + title="Slack webhook for alerts", + description="If set, alerts will be posted to this Slack webhook", ) diff --git a/project_templates/fastapi_safir_app/example/src/example/handlers/external.py b/project_templates/fastapi_safir_app/example/src/example/handlers/external.py index fa1ccd4..8ac6b18 100644 --- a/project_templates/fastapi_safir_app/example/src/example/handlers/external.py +++ b/project_templates/fastapi_safir_app/example/src/example/handlers/external.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends from safir.dependencies.logger import logger_dependency from safir.metadata import get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from structlog.stdlib import BoundLogger from ..config import config @@ -12,7 +13,7 @@ __all__ = ["external_router"] -external_router = APIRouter() +external_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all external handlers.""" diff --git a/project_templates/fastapi_safir_app/example/src/example/handlers/internal.py b/project_templates/fastapi_safir_app/example/src/example/handlers/internal.py index ed956e4..c59985f 100644 --- a/project_templates/fastapi_safir_app/example/src/example/handlers/internal.py +++ b/project_templates/fastapi_safir_app/example/src/example/handlers/internal.py @@ -10,12 +10,13 @@ from fastapi import APIRouter from safir.metadata import Metadata, get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from ..config import config __all__ = ["internal_router"] -internal_router = APIRouter() +internal_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all internal handlers.""" diff --git a/project_templates/fastapi_safir_app/example/src/example/main.py b/project_templates/fastapi_safir_app/example/src/example/main.py index e649f64..486701e 100644 --- a/project_templates/fastapi_safir_app/example/src/example/main.py +++ b/project_templates/fastapi_safir_app/example/src/example/main.py @@ -15,6 +15,7 @@ from safir.dependencies.http_client import http_client_dependency from safir.logging import configure_logging, configure_uvicorn_logging from safir.middleware.x_forwarded import XForwardedMiddleware +from safir.slack.webhook import SlackRouteErrorHandler from .config import config from .handlers.external import external_router @@ -58,3 +59,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # Add middleware. app.add_middleware(XForwardedMiddleware) + +# Configure Slack alerts. +if config.slack_webhook: + logger = structlog.get_logger("example") + SlackRouteErrorHandler.initialize( + config.slack_webhook, "example", logger + ) + logger.debug("Initialized Slack webhook") diff --git a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/config.py b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/config.py index 0c9b96f..e94c805 100644 --- a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/config.py +++ b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/config.py @@ -2,7 +2,7 @@ from __future__ import annotations -from pydantic import Field +from pydantic import Field, SecretStr from pydantic_settings import {% if cookiecutter.flavor != "UWS" %}BaseSettings, {% endif %}SettingsConfigDict from safir.logging import LogLevel, Profile {%- if cookiecutter.flavor == "UWS" %} @@ -18,8 +18,16 @@ class Config({% if cookiecutter.flavor == "UWS" %}UWSAppSettings{% else %}BaseSettings{% endif %}): """Configuration for {{ cookiecutter.name }}.""" + model_config = SettingsConfigDict( + env_prefix="{{ cookiecutter.name | upper | replace('-', '_') }}_", case_sensitive=False + ) + name: str = Field("{{ cookiecutter.name }}", title="Name of application") + log_level: LogLevel = Field( + LogLevel.INFO, title="Log level of the application's logger" + ) + path_prefix: str = Field( "/{{ cookiecutter.name | lower }}", title="URL prefix for application" ) @@ -28,12 +36,10 @@ class Config({% if cookiecutter.flavor == "UWS" %}UWSAppSettings{% else %}BaseSe Profile.development, title="Application logging profile" ) - log_level: LogLevel = Field( - LogLevel.INFO, title="Log level of the application's logger" - ) - - model_config = SettingsConfigDict( - env_prefix="{{ cookiecutter.name | upper | replace('-', '_') }}_", case_sensitive=False + slack_webhook: SecretStr | None = Field( + None, + title="Slack webhook for alerts", + description="If set, alerts will be posted to this Slack webhook", ) {%- if cookiecutter.flavor == "UWS" %} diff --git a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/external.py b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/external.py index 338c308..5c403f7 100644 --- a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/external.py +++ b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/external.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends from safir.dependencies.logger import logger_dependency from safir.metadata import get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from structlog.stdlib import BoundLogger from ..config import config @@ -12,7 +13,7 @@ __all__ = ["external_router"] -external_router = APIRouter() +external_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all external handlers.""" diff --git a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/internal.py b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/internal.py index 8482c5f..9c6afe5 100644 --- a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/internal.py +++ b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/handlers/internal.py @@ -10,12 +10,13 @@ from fastapi import APIRouter from safir.metadata import Metadata, get_metadata +from safir.slack.webhook import SlackRouteErrorHandler from ..config import config __all__ = ["internal_router"] -internal_router = APIRouter() +internal_router = APIRouter(route_class=SlackRouteErrorHandler) """FastAPI router for all internal handlers.""" diff --git a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/main.py b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/main.py index a0c6201..0667652 100644 --- a/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/main.py +++ b/project_templates/fastapi_safir_app/{{cookiecutter.name}}/src/{{cookiecutter.module_name}}/main.py @@ -11,10 +11,12 @@ from contextlib import asynccontextmanager from importlib.metadata import metadata, version +import structlog from fastapi import FastAPI from safir.dependencies.http_client import http_client_dependency from safir.logging import configure_logging, configure_uvicorn_logging from safir.middleware.x_forwarded import XForwardedMiddleware +from safir.slack.webhook import SlackRouteErrorHandler from .config import config{% if cookiecutter.flavor == "UWS" %}, uws{% endif %} from .handlers.external import external_router @@ -73,3 +75,11 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # Install error handlers. uws.install_error_handlers(app) {%- endif %} + +# Configure Slack alerts. +if config.slack_webhook: + logger = structlog.get_logger("{{ cookiecutter.module_name }}") + SlackRouteErrorHandler.initialize( + config.slack_webhook, "{{ cookiecutter.name }}", logger + ) + logger.debug("Initialized Slack webhook") diff --git a/project_templates/technote_md/testn-000/technote.toml b/project_templates/technote_md/testn-000/technote.toml index 8aa7d52..116c434 100644 --- a/project_templates/technote_md/testn-000/technote.toml +++ b/project_templates/technote_md/testn-000/technote.toml @@ -4,7 +4,7 @@ series_id = "TESTN" canonical_url = "https://testn-000.lsst.io" github_url = "https://github.com/lsst/testn-000" github_default_branch = "main" -date_created = 2024-11-25T21:31:10Z +date_created = 2024-11-25T21:45:02Z organization.name = "Vera C. Rubin Observatory" organization.ror = "https://ror.org/048g3cy84" license.id = "CC-BY-4.0" diff --git a/project_templates/technote_rst/testn-000/technote.toml b/project_templates/technote_rst/testn-000/technote.toml index ce19c4b..bf045d1 100644 --- a/project_templates/technote_rst/testn-000/technote.toml +++ b/project_templates/technote_rst/testn-000/technote.toml @@ -4,7 +4,7 @@ series_id = "TESTN" canonical_url = "https://testn-000.lsst.io" github_url = "https://github.com/lsst/testn-000" github_default_branch = "main" -date_created = 2024-11-25T21:31:10Z +date_created = 2024-11-25T21:45:02Z organization.name = "Vera C. Rubin Observatory" organization.ror = "https://ror.org/048g3cy84" license.id = "CC-BY-4.0"