Skip to content

Commit

Permalink
Add Slack error reporting to fastapi_safir_app
Browse files Browse the repository at this point in the history
Add support for Slack error reporting of uncaught exceptions to the
`fastapi_safir_app` project template. We are currently adding this
to all of our applications, so there's no reason for it to not be in
the template, even though this may be eventually replaced with Sentry
or some other exception reporting mechanism.
  • Loading branch information
rra committed Nov 25, 2024
1 parent b67f88a commit 23a7c25
Show file tree
Hide file tree
Showing 14 changed files with 79 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
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
from ..models import Index

__all__ = ["external_router"]

external_router = APIRouter()
external_router = APIRouter(route_class=SlackRouteErrorHandler)
"""FastAPI router for all external handlers."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
18 changes: 12 additions & 6 deletions project_templates/fastapi_safir_app/example/src/example/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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",
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
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
from ..models import Index

__all__ = ["external_router"]

external_router = APIRouter()
external_router = APIRouter(route_class=SlackRouteErrorHandler)
"""FastAPI router for all external handlers."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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" %}
Expand All @@ -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"
)
Expand All @@ -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" %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
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
from ..models import Index

__all__ = ["external_router"]

external_router = APIRouter()
external_router = APIRouter(route_class=SlackRouteErrorHandler)
"""FastAPI router for all external handlers."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
2 changes: 1 addition & 1 deletion project_templates/technote_md/testn-000/technote.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion project_templates/technote_rst/testn-000/technote.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 23a7c25

Please sign in to comment.