v0.141.1
  1from __future__ import annotations
  2
  3from collections.abc import Sequence
  4
  5from plain import postgres
  6from plain.admin.cards import TrendCard
  7from plain.admin.views import (
  8    AdminModelDetailView,
  9    AdminModelListView,
 10    AdminViewset,
 11    register_viewset,
 12)
 13
 14from .models import Log, Span, Trace
 15
 16
 17class TracesTrendCard(TrendCard):
 18    title = "Traces trend"
 19    model = Trace
 20    datetime_field = "start_time"
 21    size = TrendCard.Sizes.FULL
 22    group_field = "app_version"
 23
 24
 25class SpansTrendCard(TrendCard):
 26    title = "Spans trend"
 27    model = Span
 28    datetime_field = "start_time"
 29    size = TrendCard.Sizes.FULL
 30    group_field = "kind"
 31
 32
 33class LogsTrendCard(TrendCard):
 34    title = "Logs trend"
 35    model = Log
 36    datetime_field = "timestamp"
 37    size = TrendCard.Sizes.FULL
 38    group_field = "level"
 39
 40
 41@register_viewset
 42class TraceViewset(AdminViewset):
 43    class ListView(AdminModelListView):
 44        nav_section = "Observer"
 45        nav_icon = "diagram-3"
 46        model = Trace
 47        description = "Request traces linking spans and logs together."
 48        fields = [
 49            "trace_id",
 50            "request_id",
 51            "session_id",
 52            "user_id",
 53            "start_time",
 54        ]
 55        cards = [TracesTrendCard]
 56        actions = ["Delete"]
 57
 58        def perform_action(self, action: str, target_ids: Sequence[int]) -> None:
 59            if action == "Delete":
 60                Trace.query.filter(id__in=target_ids).delete()
 61
 62    class DetailView(AdminModelDetailView):
 63        model = Trace
 64
 65
 66@register_viewset
 67class SpanViewset(AdminViewset):
 68    class ListView(AdminModelListView):
 69        nav_section = "Observer"
 70        nav_icon = "activity"
 71        model = Span
 72        description = (
 73            "Individual operations within a trace (DB queries, HTTP calls, etc)."
 74        )
 75        fields = [
 76            "name",
 77            "kind",
 78            "status",
 79            "span_id",
 80            "parent_id",
 81            "start_time",
 82        ]
 83        field_templates = {
 84            "kind": "observer/values/span_kind.html",
 85            "status": "observer/values/span_status.html",
 86        }
 87        queryset_order = ["-id"]
 88        cards = [SpansTrendCard]
 89        filters = ["Parents only"]
 90        search_fields = ["name", "span_id", "parent_id"]
 91        actions = ["Delete"]
 92
 93        def perform_action(self, action: str, target_ids: Sequence[int]) -> None:
 94            if action == "Delete":
 95                Span.query.filter(id__in=target_ids).delete()
 96
 97        def get_initial_queryset(self) -> postgres.QuerySet:
 98            return (
 99                super()
100                .get_initial_queryset()
101                .only(
102                    "name",
103                    "kind",
104                    "span_id",
105                    "parent_id",
106                    "start_time",
107                )
108            )
109
110        def filter_queryset(self, queryset: postgres.QuerySet) -> postgres.QuerySet:
111            if self.filter == "Parents only":
112                return queryset.filter(parent_id="")
113            return queryset
114
115    class DetailView(AdminModelDetailView):
116        model = Span
117
118
119@register_viewset
120class LogViewset(AdminViewset):
121    class ListView(AdminModelListView):
122        nav_section = "Observer"
123        nav_icon = "journal-text"
124        model = Log
125        description = "Application logs captured during request processing."
126        fields = [
127            "timestamp",
128            "level",
129            "message",
130            "trace",
131            "span",
132        ]
133        cards = [LogsTrendCard]
134        queryset_order = ["-timestamp"]
135        search_fields = ["message", "level"]
136        actions = ["Delete selected", "Delete all"]
137
138        def perform_action(self, action: str, target_ids: Sequence[int]) -> None:
139            if action == "Delete selected":
140                Log.query.filter(id__in=target_ids).delete()
141            elif action == "Delete all":
142                Log.query.all().delete()
143
144        def get_initial_queryset(self) -> postgres.QuerySet:
145            return (
146                super()
147                .get_initial_queryset()
148                .select_related("trace", "span")
149                .only(
150                    "timestamp",
151                    "level",
152                    "message",
153                    "span__span_id",
154                    "trace__trace_id",
155                )
156            )
157
158    class DetailView(AdminModelDetailView):
159        model = Log