ctfy.sdk.admin_resources.observability

client.admin.observability — dashboard stats, health, audit log (admin).

  1"""``client.admin.observability`` — dashboard stats, health, audit log (admin)."""
  2
  3from __future__ import annotations
  4
  5from typing import Any
  6
  7from ctfy.sdk._helpers import _extract_items, _raise_for_status
  8from ctfy.sdk.base import BaseHttpClient
  9from ctfy.server.models import (
 10    Activity,
 11    ActivityTimeseries,
 12    AdminHealthFlags,
 13    AdminOverview,
 14    AdminSolveMatrix,
 15    AdminTimeseries,
 16    AdminTrafficSummary,
 17)
 18
 19
 20class AdminObservabilityResource:
 21    """The admin dashboard's numbers: overview snapshot, solve grid, health
 22    flags, metric timeseries, traffic tally, and the unfiltered audit log."""
 23
 24    def __init__(self, http: BaseHttpClient) -> None:
 25        self._http = http
 26
 27    def overview(self) -> AdminOverview:
 28        """Dashboard snapshot: counts of users / teams / competitions /
 29        instances / solves, plus the latest 24h of activity."""
 30        resp = self._http.request("GET", "/admin/overview")
 31        _raise_for_status(resp)
 32        return AdminOverview.model_validate(resp.json())
 33
 34    def solve_matrix(self) -> AdminSolveMatrix:
 35        """Per-team × per-challenge solve grid for the admin scoreboard."""
 36        resp = self._http.request("GET", "/admin/solve-matrix")
 37        _raise_for_status(resp)
 38        return AdminSolveMatrix.model_validate(resp.json())
 39
 40    def health_flags(self) -> AdminHealthFlags:
 41        """Boolean flags surfaced on the admin top-bar (e.g. node down,
 42        OAuth not configured, secret-key ephemeral)."""
 43        resp = self._http.request("GET", "/admin/health-flags")
 44        _raise_for_status(resp)
 45        return AdminHealthFlags.model_validate(resp.json())
 46
 47    def timeseries(
 48        self,
 49        *,
 50        metric: str,
 51        window: int = 86400,
 52        bucket: int = 3600,
 53    ) -> AdminTimeseries:
 54        """Bucketed counts for one metric. ``metric`` ∈ ``"launches"``,
 55        ``"submissions"``, ``"solves"``, ``"running_instances"``,
 56        ``"teams"``. ``window`` and ``bucket`` are in seconds."""
 57        resp = self._http.request(
 58            "GET",
 59            "/admin/timeseries",
 60            params={"metric": metric, "window": window, "bucket": bucket},
 61        )
 62        _raise_for_status(resp)
 63        return AdminTimeseries.model_validate(resp.json())
 64
 65    def traffic_summary(self) -> AdminTrafficSummary:
 66        """Per-team and per-instance HTTP request counts (from archived
 67        instance records' mitmproxy flow tally)."""
 68        resp = self._http.request("GET", "/admin/traffic/summary")
 69        _raise_for_status(resp)
 70        return AdminTrafficSummary.model_validate(resp.json())
 71
 72    def activities(
 73        self,
 74        *,
 75        actor_id: str = "",
 76        team_id: str = "",
 77        challenge_id: str = "",
 78        event: str = "",
 79        since: str = "",
 80        until: str = "",
 81        offset: int = 0,
 82        limit: int = 50,
 83    ) -> list[Activity]:
 84        """Admin activity log — the unfiltered audit surface (every
 85        actor, including admin-only events). ``since`` / ``until`` are
 86        ISO-8601 timestamps; omit to leave unbounded."""
 87        params: dict[str, Any] = {"offset": offset, "limit": limit}
 88        params.update(
 89            {
 90                k: v
 91                for k, v in (
 92                    ("actor_id", actor_id),
 93                    ("team_id", team_id),
 94                    ("challenge_id", challenge_id),
 95                    ("event", event),
 96                    ("since", since),
 97                    ("until", until),
 98                )
 99                if v
100            }
101        )
102        resp = self._http.request("GET", "/admin/activities", params=params)
103        _raise_for_status(resp)
104        return _extract_items(resp.json(), Activity)
105
106    def activities_timeseries(
107        self, *, window: int = 86400, bucket: int = 3600
108    ) -> ActivityTimeseries:
109        """Bucketed admin activity-event counts. ``window`` + ``bucket``
110        are in seconds."""
111        resp = self._http.request(
112            "GET",
113            "/admin/activities/timeseries",
114            params={"window": window, "bucket": bucket},
115        )
116        _raise_for_status(resp)
117        return ActivityTimeseries.model_validate(resp.json())
class AdminObservabilityResource:
 21class AdminObservabilityResource:
 22    """The admin dashboard's numbers: overview snapshot, solve grid, health
 23    flags, metric timeseries, traffic tally, and the unfiltered audit log."""
 24
 25    def __init__(self, http: BaseHttpClient) -> None:
 26        self._http = http
 27
 28    def overview(self) -> AdminOverview:
 29        """Dashboard snapshot: counts of users / teams / competitions /
 30        instances / solves, plus the latest 24h of activity."""
 31        resp = self._http.request("GET", "/admin/overview")
 32        _raise_for_status(resp)
 33        return AdminOverview.model_validate(resp.json())
 34
 35    def solve_matrix(self) -> AdminSolveMatrix:
 36        """Per-team × per-challenge solve grid for the admin scoreboard."""
 37        resp = self._http.request("GET", "/admin/solve-matrix")
 38        _raise_for_status(resp)
 39        return AdminSolveMatrix.model_validate(resp.json())
 40
 41    def health_flags(self) -> AdminHealthFlags:
 42        """Boolean flags surfaced on the admin top-bar (e.g. node down,
 43        OAuth not configured, secret-key ephemeral)."""
 44        resp = self._http.request("GET", "/admin/health-flags")
 45        _raise_for_status(resp)
 46        return AdminHealthFlags.model_validate(resp.json())
 47
 48    def timeseries(
 49        self,
 50        *,
 51        metric: str,
 52        window: int = 86400,
 53        bucket: int = 3600,
 54    ) -> AdminTimeseries:
 55        """Bucketed counts for one metric. ``metric`` ∈ ``"launches"``,
 56        ``"submissions"``, ``"solves"``, ``"running_instances"``,
 57        ``"teams"``. ``window`` and ``bucket`` are in seconds."""
 58        resp = self._http.request(
 59            "GET",
 60            "/admin/timeseries",
 61            params={"metric": metric, "window": window, "bucket": bucket},
 62        )
 63        _raise_for_status(resp)
 64        return AdminTimeseries.model_validate(resp.json())
 65
 66    def traffic_summary(self) -> AdminTrafficSummary:
 67        """Per-team and per-instance HTTP request counts (from archived
 68        instance records' mitmproxy flow tally)."""
 69        resp = self._http.request("GET", "/admin/traffic/summary")
 70        _raise_for_status(resp)
 71        return AdminTrafficSummary.model_validate(resp.json())
 72
 73    def activities(
 74        self,
 75        *,
 76        actor_id: str = "",
 77        team_id: str = "",
 78        challenge_id: str = "",
 79        event: str = "",
 80        since: str = "",
 81        until: str = "",
 82        offset: int = 0,
 83        limit: int = 50,
 84    ) -> list[Activity]:
 85        """Admin activity log — the unfiltered audit surface (every
 86        actor, including admin-only events). ``since`` / ``until`` are
 87        ISO-8601 timestamps; omit to leave unbounded."""
 88        params: dict[str, Any] = {"offset": offset, "limit": limit}
 89        params.update(
 90            {
 91                k: v
 92                for k, v in (
 93                    ("actor_id", actor_id),
 94                    ("team_id", team_id),
 95                    ("challenge_id", challenge_id),
 96                    ("event", event),
 97                    ("since", since),
 98                    ("until", until),
 99                )
100                if v
101            }
102        )
103        resp = self._http.request("GET", "/admin/activities", params=params)
104        _raise_for_status(resp)
105        return _extract_items(resp.json(), Activity)
106
107    def activities_timeseries(
108        self, *, window: int = 86400, bucket: int = 3600
109    ) -> ActivityTimeseries:
110        """Bucketed admin activity-event counts. ``window`` + ``bucket``
111        are in seconds."""
112        resp = self._http.request(
113            "GET",
114            "/admin/activities/timeseries",
115            params={"window": window, "bucket": bucket},
116        )
117        _raise_for_status(resp)
118        return ActivityTimeseries.model_validate(resp.json())

The admin dashboard's numbers: overview snapshot, solve grid, health flags, metric timeseries, traffic tally, and the unfiltered audit log.

AdminObservabilityResource(http: ctfy.sdk.base.BaseHttpClient)
25    def __init__(self, http: BaseHttpClient) -> None:
26        self._http = http
def overview(self) -> ctfy.server.models.AdminOverview:
28    def overview(self) -> AdminOverview:
29        """Dashboard snapshot: counts of users / teams / competitions /
30        instances / solves, plus the latest 24h of activity."""
31        resp = self._http.request("GET", "/admin/overview")
32        _raise_for_status(resp)
33        return AdminOverview.model_validate(resp.json())

Dashboard snapshot: counts of users / teams / competitions / instances / solves, plus the latest 24h of activity.

def solve_matrix(self) -> ctfy.server.models.AdminSolveMatrix:
35    def solve_matrix(self) -> AdminSolveMatrix:
36        """Per-team × per-challenge solve grid for the admin scoreboard."""
37        resp = self._http.request("GET", "/admin/solve-matrix")
38        _raise_for_status(resp)
39        return AdminSolveMatrix.model_validate(resp.json())

Per-team × per-challenge solve grid for the admin scoreboard.

def health_flags(self) -> ctfy.server.models.AdminHealthFlags:
41    def health_flags(self) -> AdminHealthFlags:
42        """Boolean flags surfaced on the admin top-bar (e.g. node down,
43        OAuth not configured, secret-key ephemeral)."""
44        resp = self._http.request("GET", "/admin/health-flags")
45        _raise_for_status(resp)
46        return AdminHealthFlags.model_validate(resp.json())

Boolean flags surfaced on the admin top-bar (e.g. node down, OAuth not configured, secret-key ephemeral).

def timeseries( self, *, metric: str, window: int = 86400, bucket: int = 3600) -> ctfy.server.models.AdminTimeseries:
48    def timeseries(
49        self,
50        *,
51        metric: str,
52        window: int = 86400,
53        bucket: int = 3600,
54    ) -> AdminTimeseries:
55        """Bucketed counts for one metric. ``metric`` ∈ ``"launches"``,
56        ``"submissions"``, ``"solves"``, ``"running_instances"``,
57        ``"teams"``. ``window`` and ``bucket`` are in seconds."""
58        resp = self._http.request(
59            "GET",
60            "/admin/timeseries",
61            params={"metric": metric, "window": window, "bucket": bucket},
62        )
63        _raise_for_status(resp)
64        return AdminTimeseries.model_validate(resp.json())

Bucketed counts for one metric. metric"launches", "submissions", "solves", "running_instances", "teams". window and bucket are in seconds.

def traffic_summary(self) -> ctfy.server.models.AdminTrafficSummary:
66    def traffic_summary(self) -> AdminTrafficSummary:
67        """Per-team and per-instance HTTP request counts (from archived
68        instance records' mitmproxy flow tally)."""
69        resp = self._http.request("GET", "/admin/traffic/summary")
70        _raise_for_status(resp)
71        return AdminTrafficSummary.model_validate(resp.json())

Per-team and per-instance HTTP request counts (from archived instance records' mitmproxy flow tally).

def activities( self, *, actor_id: str = '', team_id: str = '', challenge_id: str = '', event: str = '', since: str = '', until: str = '', offset: int = 0, limit: int = 50) -> list[ctfy.server.models.Activity]:
 73    def activities(
 74        self,
 75        *,
 76        actor_id: str = "",
 77        team_id: str = "",
 78        challenge_id: str = "",
 79        event: str = "",
 80        since: str = "",
 81        until: str = "",
 82        offset: int = 0,
 83        limit: int = 50,
 84    ) -> list[Activity]:
 85        """Admin activity log — the unfiltered audit surface (every
 86        actor, including admin-only events). ``since`` / ``until`` are
 87        ISO-8601 timestamps; omit to leave unbounded."""
 88        params: dict[str, Any] = {"offset": offset, "limit": limit}
 89        params.update(
 90            {
 91                k: v
 92                for k, v in (
 93                    ("actor_id", actor_id),
 94                    ("team_id", team_id),
 95                    ("challenge_id", challenge_id),
 96                    ("event", event),
 97                    ("since", since),
 98                    ("until", until),
 99                )
100                if v
101            }
102        )
103        resp = self._http.request("GET", "/admin/activities", params=params)
104        _raise_for_status(resp)
105        return _extract_items(resp.json(), Activity)

Admin activity log — the unfiltered audit surface (every actor, including admin-only events). since / until are ISO-8601 timestamps; omit to leave unbounded.

def activities_timeseries( self, *, window: int = 86400, bucket: int = 3600) -> ctfy.server.models.ActivityTimeseries:
107    def activities_timeseries(
108        self, *, window: int = 86400, bucket: int = 3600
109    ) -> ActivityTimeseries:
110        """Bucketed admin activity-event counts. ``window`` + ``bucket``
111        are in seconds."""
112        resp = self._http.request(
113            "GET",
114            "/admin/activities/timeseries",
115            params={"window": window, "bucket": bucket},
116        )
117        _raise_for_status(resp)
118        return ActivityTimeseries.model_validate(resp.json())

Bucketed admin activity-event counts. window + bucket are in seconds.