diff --git a/docs/reference/api.md b/docs/reference/api.md index 5c34360..47005b3 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,29 +1,73 @@ # API Reference -## Checks +This is the auto-generated reference for django-watchman's Python API. +For a higher-level overview see the [Getting Started](../getting-started.md) +guide and the [Configuration](../configuration.md) page. -::: watchman.checks +--- -## Constants +## Views -::: watchman.constants +The Django views that power watchman's HTTP endpoints. -## Decorators +::: watchman.views + options: + show_source: false + members: + - status + - bare_status + - ping + - dashboard + - run_checks -::: watchman.decorators +## Checks + +Built-in health-check functions for Django backing services. Each function +can be referenced by its dotted path in +[`WATCHMAN_CHECKS`][watchman.settings.WATCHMAN_CHECKS]. + +::: watchman.checks + options: + members: + - caches + - databases + - email + - storage ## Settings +All settings are read from your Django `settings` module with sensible +defaults. See the [Configuration](../configuration.md) guide for usage +details. + ::: watchman.settings ## URLs -::: watchman.urls +Include these in your root URL configuration with +`url(r'^watchman/', include('watchman.urls'))`. -## Utils +| URL pattern | View | Name | +|--------------|-----------------------------------------------|-------------| +| `/` | [`status`][watchman.views.status] | `status` | +| `/dashboard/`| [`dashboard`][watchman.views.dashboard] | `dashboard` | +| `/ping/` | [`ping`][watchman.views.ping] | `ping` | -::: watchman.utils +## Constants -## Views +::: watchman.constants -::: watchman.views +## Decorators + +::: watchman.decorators + options: + show_source: false + members: + - check + - parse_auth_header + - token_required + - auth + +## Utils + +::: watchman.utils diff --git a/mkdocs.yml b/mkdocs.yml index b9acaa9..fbff2ec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,8 +49,10 @@ plugins: options: show_source: true show_root_heading: true - heading_level: 2 + heading_level: 3 members_order: source + filters: + - "!^_" markdown_extensions: - admonition diff --git a/watchman/checks.py b/watchman/checks.py index 4599d16..5740e6d 100644 --- a/watchman/checks.py +++ b/watchman/checks.py @@ -1,3 +1,11 @@ +"""Built-in health check functions for Django backing services. + +Each public function (e.g. `caches`, `databases`, `email`, `storage`) returns +a dictionary suitable for inclusion in the watchman JSON response. They can be +referenced by their dotted Python path in the +[`WATCHMAN_CHECKS`][watchman.settings.WATCHMAN_CHECKS] setting. +""" + import uuid from pathlib import PurePath from typing import Any @@ -77,16 +85,60 @@ def _check_storage() -> CheckStatus: def caches() -> dict[str, list[CheckResult]]: + """Check all configured caches by writing, reading, and deleting a key. + + Iterates over every cache defined in + [`WATCHMAN_CACHES`][watchman.settings.WATCHMAN_CACHES] (defaults to + Django's `CACHES` setting). + + Returns: + A dictionary of the form: + + {"caches": [{"default": {"ok": True}}, ...]} + """ return {"caches": _check_caches(watchman_settings.WATCHMAN_CACHES)} def databases() -> dict[str, list[CheckResult]]: + """Check all configured databases by executing a ``SELECT 1`` query. + + Iterates over every database defined in + [`WATCHMAN_DATABASES`][watchman.settings.WATCHMAN_DATABASES] (defaults to + Django's `DATABASES` setting). + + Returns: + A dictionary of the form: + + {"databases": [{"default": {"ok": True}}, ...]} + """ return {"databases": _check_databases(watchman_settings.WATCHMAN_DATABASES)} def email() -> dict[str, CheckResult]: + """Check the email backend by sending a test message. + + Only included when + [`WATCHMAN_ENABLE_PAID_CHECKS`][watchman.settings.WATCHMAN_ENABLE_PAID_CHECKS] + is ``True`` or when explicitly listed in + [`WATCHMAN_CHECKS`][watchman.settings.WATCHMAN_CHECKS]. + + Returns: + A dictionary of the form: + + {"email": {"ok": True}} + """ return {"email": _check_email()} def storage() -> dict[str, CheckResult]: + """Check the default file storage backend. + + Creates a temporary file, reads it back, and deletes it using Django's + default storage. + + Returns: + A dictionary of the form: + + {"storage": {"ok": True}} + """ return {"storage": _check_storage()} diff --git a/watchman/constants.py b/watchman/constants.py index aa41322..4ca3bbe 100644 --- a/watchman/constants.py +++ b/watchman/constants.py @@ -1,7 +1,13 @@ +"""Default check tuples used to populate [`WATCHMAN_CHECKS`][watchman.settings.WATCHMAN_CHECKS].""" + DEFAULT_CHECKS: tuple[str, ...] = ( "watchman.checks.caches", "watchman.checks.databases", "watchman.checks.storage", ) +"""Checks included by default: caches, databases, and storage.""" PAID_CHECKS: tuple[str, ...] = ("watchman.checks.email",) +"""Checks that may incur cost (e.g. sending an email). Only included when +[`WATCHMAN_ENABLE_PAID_CHECKS`][watchman.settings.WATCHMAN_ENABLE_PAID_CHECKS] +is ``True``.""" diff --git a/watchman/decorators.py b/watchman/decorators.py index a9db0e0..d7bce8d 100644 --- a/watchman/decorators.py +++ b/watchman/decorators.py @@ -1,3 +1,5 @@ +"""Decorators used to protect and wrap watchman views and checks.""" + import logging import traceback from collections.abc import Callable @@ -14,8 +16,10 @@ def check(func: Callable[..., CheckResult]) -> Callable[..., CheckResult]: - """ - Decorator which wraps checks and returns an error response on failure. + """Decorator that wraps a check function and converts exceptions into error results. + + If the wrapped function raises, the exception is caught and returned as a + `CheckStatus` with ``ok=False``, the error message, and a stacktrace. """ def wrapped(*args: Any, **kwargs: Any) -> CheckResult: @@ -71,12 +75,14 @@ def parse_auth_header(auth_header: str) -> str: def token_required( view_func: Callable[..., HttpResponse], ) -> Callable[..., HttpResponse]: - """ - Decorator which ensures that one of the WATCHMAN_TOKENS is provided if set. + """Decorator that enforces token-based authentication on a view. - WATCHMAN_TOKEN_NAME can also be set if the token GET parameter must be - customized. + When [`WATCHMAN_TOKENS`][watchman.settings.WATCHMAN_TOKENS] (or the + deprecated `WATCHMAN_TOKEN`) is set, the request must supply a matching + token via the ``Authorization`` header or a query parameter named by + [`WATCHMAN_TOKEN_NAME`][watchman.settings.WATCHMAN_TOKEN_NAME]. + Returns an ``HTTP 403`` response when the token is missing or invalid. """ def _get_passed_token(request: HttpRequest) -> str | None: diff --git a/watchman/settings.py b/watchman/settings.py index 9a3ada6..f4a05d0 100644 --- a/watchman/settings.py +++ b/watchman/settings.py @@ -1,39 +1,71 @@ +"""Resolved watchman settings with their defaults. + +All settings are read from your Django ``settings`` module via ``getattr`` +with sensible defaults. See the [Configuration](../configuration.md) guide for +full usage details. +""" + from typing import Any from django.conf import settings from watchman.constants import DEFAULT_CHECKS, PAID_CHECKS -# TODO: these should not be module level (https://github.com/mwarkentin/django-watchman/issues/13) WATCHMAN_ENABLE_PAID_CHECKS: bool = getattr( settings, "WATCHMAN_ENABLE_PAID_CHECKS", False ) +"""Include paid checks (e.g. email) in the default check list. Default: ``False``.""" + WATCHMAN_AUTH_DECORATOR: str | None = getattr( settings, "WATCHMAN_AUTH_DECORATOR", "watchman.decorators.token_required" ) -# TODO: Remove for django-watchman 1.0 +"""Dotted path to a decorator applied to protected views. Set to ``None`` to +disable authentication. Default: ``"watchman.decorators.token_required"``.""" + WATCHMAN_TOKEN: str | None = getattr(settings, "WATCHMAN_TOKEN", None) +"""*Deprecated* -- use [`WATCHMAN_TOKENS`][watchman.settings.WATCHMAN_TOKENS] +instead. Will be removed in django-watchman 1.0.""" + WATCHMAN_TOKENS: str | None = getattr(settings, "WATCHMAN_TOKENS", None) +"""Comma-separated list of accepted authentication tokens. Default: ``None`` +(no token required).""" + WATCHMAN_TOKEN_NAME: str = getattr(settings, "WATCHMAN_TOKEN_NAME", "watchman-token") +"""Name of the query-string parameter used to pass the token. +Default: ``"watchman-token"``.""" + WATCHMAN_ERROR_CODE: int = getattr(settings, "WATCHMAN_ERROR_CODE", 500) +"""HTTP status code returned when a check fails. Default: ``500``.""" + WATCHMAN_EMAIL_SENDER: str = getattr( settings, "WATCHMAN_EMAIL_SENDER", "watchman@example.com" ) +"""``From`` address for the email check. Default: ``"watchman@example.com"``.""" + WATCHMAN_EMAIL_RECIPIENTS: list[str] = getattr( settings, "WATCHMAN_EMAIL_RECIPIENTS", ["to@example.com"] ) +"""List of ``To`` addresses for the email check. Default: ``["to@example.com"]``.""" + WATCHMAN_EMAIL_HEADERS: dict[str, str] = getattr(settings, "WATCHMAN_EMAIL_HEADERS", {}) +"""Extra headers added to the test email. Default: ``{}``.""" WATCHMAN_CACHES: dict[str, Any] = getattr(settings, "WATCHMAN_CACHES", settings.CACHES) +"""Cache aliases to check. Defaults to Django's ``CACHES`` setting.""" + WATCHMAN_DATABASES: dict[str, Any] = getattr( settings, "WATCHMAN_DATABASES", settings.DATABASES ) +"""Database aliases to check. Defaults to Django's ``DATABASES`` setting.""" WATCHMAN_DISABLE_APM: bool = getattr(settings, "WATCHMAN_DISABLE_APM", False) +"""Suppress APM tracing (New Relic, Datadog) for watchman views. Default: ``False``.""" WATCHMAN_STORAGE_PATH: str = getattr( settings, "WATCHMAN_STORAGE_PATH", settings.MEDIA_ROOT ) +"""Subdirectory within the default storage backend used for the storage check. +Defaults to ``MEDIA_ROOT``.""" _checks: tuple[str, ...] = DEFAULT_CHECKS @@ -41,5 +73,11 @@ _checks = DEFAULT_CHECKS + PAID_CHECKS WATCHMAN_CHECKS: tuple[str, ...] = getattr(settings, "WATCHMAN_CHECKS", _checks) +"""Tuple of dotted paths to check functions that watchman will execute. +Defaults to [`DEFAULT_CHECKS`][watchman.constants.DEFAULT_CHECKS] (plus +[`PAID_CHECKS`][watchman.constants.PAID_CHECKS] when +[`WATCHMAN_ENABLE_PAID_CHECKS`][watchman.settings.WATCHMAN_ENABLE_PAID_CHECKS] +is ``True``).""" EXPOSE_WATCHMAN_VERSION: bool = getattr(settings, "EXPOSE_WATCHMAN_VERSION", False) +"""Add an ``X-Watchman-Version`` header to responses. Default: ``False``.""" diff --git a/watchman/utils.py b/watchman/utils.py index 078d28d..a357051 100644 --- a/watchman/utils.py +++ b/watchman/utils.py @@ -1,3 +1,5 @@ +"""Utility helpers used internally by watchman checks and views.""" + from collections.abc import Generator from typing import Any @@ -9,6 +11,7 @@ def get_cache(cache_name: str) -> BaseCache: + """Return the Django cache backend for *cache_name*.""" return django_cache.caches[cache_name] @@ -16,6 +19,17 @@ def get_checks( check_list: list[str] | None = None, skip_list: list[str] | None = None, ) -> Generator[Any]: + """Yield callable check functions from [`WATCHMAN_CHECKS`][watchman.settings.WATCHMAN_CHECKS]. + + Args: + check_list: If provided, only checks whose dotted path is in this list + are yielded. + skip_list: If provided, checks whose dotted path is in this list are + excluded. + + Yields: + Callable check functions resolved via `import_string`. + """ checks_to_run = frozenset(WATCHMAN_CHECKS) if check_list is not None: diff --git a/watchman/views.py b/watchman/views.py index 4a1098a..769271f 100644 --- a/watchman/views.py +++ b/watchman/views.py @@ -1,3 +1,12 @@ +"""Django views that expose watchman health-check endpoints. + +The three main endpoints are: + +* **status** -- JSON response with the results of all configured checks. +* **dashboard** -- HTML page summarising check results. +* **ping** -- Lightweight ``pong`` response for simple uptime monitoring. +""" + import warnings from typing import Any @@ -56,6 +65,17 @@ def _disable_apm() -> None: def run_checks(request: HttpRequest) -> tuple[dict[str, Any], bool]: + """Execute all configured health checks and return the aggregated results. + + Reads ``check`` and ``skip`` query parameters from *request* to allow + callers to filter which checks are executed. + + Returns: + A ``(checks, ok)`` tuple where *checks* is a dictionary of check + results and *ok* is ``False`` when any check reported an error + (and [`WATCHMAN_ERROR_CODE`][watchman.settings.WATCHMAN_ERROR_CODE] + is not ``200``). + """ _deprecation_warnings() if settings.WATCHMAN_DISABLE_APM: @@ -89,6 +109,23 @@ def run_checks(request: HttpRequest) -> tuple[dict[str, Any], bool]: @auth @non_atomic_requests def status(request: HttpRequest) -> HttpResponse: + """Return JSON health-check results for all configured checks. + + Protected by the configured + [`WATCHMAN_AUTH_DECORATOR`][watchman.settings.WATCHMAN_AUTH_DECORATOR]. + + **Example response:** + + { + "caches": [{"default": {"ok": true}}], + "databases": [{"default": {"ok": true}}], + "storage": {"ok": true} + } + + Query parameters: + check: Run only the specified checks (repeatable). + skip: Skip the specified checks (repeatable). + """ checks, ok = run_checks(request) if not checks: @@ -111,12 +148,24 @@ def status(request: HttpRequest) -> HttpResponse: @non_atomic_requests def bare_status(request: HttpRequest) -> HttpResponse: + """Return an empty ``text/plain`` response whose status code reflects overall health. + + Unlike [`status`][watchman.views.status], this view has **no** auth + decorator and returns no body -- only the HTTP status code matters. + Useful for load-balancer health checks that only inspect the status code. + """ checks, ok = run_checks(request) http_code = 200 if ok else settings.WATCHMAN_ERROR_CODE return HttpResponse(status=http_code, content_type="text/plain") def ping(request: HttpRequest) -> HttpResponse: + """Return a plain-text ``pong`` response. + + This is the simplest possible liveness probe -- it does **not** run any + backing-service checks. Useful for Kubernetes liveness probes or simple + uptime pings. + """ if settings.WATCHMAN_DISABLE_APM: _disable_apm() return HttpResponse("pong", content_type="text/plain") @@ -125,6 +174,15 @@ def ping(request: HttpRequest) -> HttpResponse: @auth @non_atomic_requests def dashboard(request: HttpRequest) -> HttpResponse: + """Render an HTML dashboard showing the status of all configured checks. + + Protected by the configured + [`WATCHMAN_AUTH_DECORATOR`][watchman.settings.WATCHMAN_AUTH_DECORATOR]. + + Query parameters: + check: Run only the specified checks (repeatable). + skip: Skip the specified checks (repeatable). + """ checks, overall_status = run_checks(request) expanded_checks: dict[str, Any] = {}