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