plain.observer 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of plain.observer might be problematic. Click here for more details.
- plain/observer/CHANGELOG.md +1 -0
- plain/observer/README.md +3 -0
- plain/observer/__init__.py +0 -0
- plain/observer/admin.py +102 -0
- plain/observer/cli.py +23 -0
- plain/observer/config.py +36 -0
- plain/observer/core.py +63 -0
- plain/observer/default_settings.py +9 -0
- plain/observer/migrations/0001_initial.py +96 -0
- plain/observer/migrations/__init__.py +0 -0
- plain/observer/models.py +355 -0
- plain/observer/otel.py +335 -0
- plain/observer/templates/admin/observer/trace_detail.html +10 -0
- plain/observer/templates/observer/_trace_detail.html +364 -0
- plain/observer/templates/observer/traces.html +288 -0
- plain/observer/templates/toolbar/observer.html +42 -0
- plain/observer/templates/toolbar/observer_button.html +45 -0
- plain/observer/urls.py +10 -0
- plain/observer/views.py +105 -0
- plain_observer-0.0.0.dist-info/METADATA +16 -0
- plain_observer-0.0.0.dist-info/RECORD +23 -0
- plain_observer-0.0.0.dist-info/WHEEL +4 -0
- plain_observer-0.0.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Querystats</title>
|
|
7
|
+
{% tailwind_css %}
|
|
8
|
+
{% htmx_js %}
|
|
9
|
+
</head>
|
|
10
|
+
<body class="text-stone-300 overscroll-contain" hx-ext="morph">
|
|
11
|
+
|
|
12
|
+
<div id="main-content">
|
|
13
|
+
{% if traces %}
|
|
14
|
+
<div class="flex h-full">
|
|
15
|
+
<aside id="sidebar" class="fixed left-0 top-0 bottom-0 w-82 overflow-auto bg-stone-950 border-r border-stone-800">
|
|
16
|
+
<div class="sticky top-0 bg-stone-950 p-4 pb-2 border-b border-stone-800/50 z-10">
|
|
17
|
+
<div class="flex items-center justify-between mb-3">
|
|
18
|
+
<h3 class="text-sm font-semibold text-stone-300">Traces ({{ traces|length }})</h3>
|
|
19
|
+
<div class="flex items-center space-x-2">
|
|
20
|
+
<button
|
|
21
|
+
hx-get="."
|
|
22
|
+
hx-swap="morph:innerHTML"
|
|
23
|
+
hx-target="#main-content"
|
|
24
|
+
class="p-1.5 rounded-sm bg-stone-800 text-stone-300 hover:bg-stone-700 cursor-pointer transition-colors"
|
|
25
|
+
title="Refresh traces">
|
|
26
|
+
<svg class="htmx-request:animate-spin" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
27
|
+
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
|
28
|
+
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
hx-delete="."
|
|
33
|
+
plain-hx-action="traces"
|
|
34
|
+
hx-swap="morph:innerHTML"
|
|
35
|
+
hx-target="#main-content"
|
|
36
|
+
class="p-1.5 rounded-sm bg-stone-800 text-stone-300 hover:bg-red-600 hover:text-white cursor-pointer transition-colors"
|
|
37
|
+
title="Clear all traces">
|
|
38
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
39
|
+
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
|
40
|
+
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<!-- Simplified mode control -->
|
|
46
|
+
<div class="flex items-center justify-between text-xs">
|
|
47
|
+
<div class="flex items-center space-x-2">
|
|
48
|
+
<span class="text-stone-400">Mode:</span>
|
|
49
|
+
<select
|
|
50
|
+
hx-post="."
|
|
51
|
+
hx-trigger="change"
|
|
52
|
+
hx-swap="morph:innerHTML"
|
|
53
|
+
hx-target="#main-content"
|
|
54
|
+
name="observe_action"
|
|
55
|
+
class="bg-stone-800 text-stone-300 rounded px-2 py-1 text-xs border border-stone-700 focus:border-stone-600 focus:outline-none cursor-pointer">
|
|
56
|
+
<option value="summary" {% if observer.is_summarizing() %}selected{% endif %}>Summary</option>
|
|
57
|
+
<option value="persist" {% if observer.is_persisting() %}selected{% endif %}>Recording</option>
|
|
58
|
+
<option disabled>───────</option>
|
|
59
|
+
<option value="disable" {% if observer.is_disabled() %}selected{% endif %} class="text-stone-500">Disabled</option>
|
|
60
|
+
</select>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="text-stone-500">
|
|
63
|
+
{% if observer.is_persisting() %}
|
|
64
|
+
<div class="w-2 h-2 bg-red-500 rounded-full animate-pulse" title="Recording"></div>
|
|
65
|
+
{% endif %}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="p-4 pt-2">
|
|
70
|
+
<ul class="space-y-1">
|
|
71
|
+
{% for trace_item in traces %}
|
|
72
|
+
<li>
|
|
73
|
+
<a
|
|
74
|
+
href="?trace_id={{ trace_item.id }}"
|
|
75
|
+
hx-boost="true"
|
|
76
|
+
class="block w-full text-left p-3 rounded-lg border transition-all duration-200 group focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 {% if trace and trace_item.id == trace.id %}bg-stone-800 border-stone-600 text-white{% else %}border-stone-800 hover:border-stone-600 hover:bg-stone-900/50{% endif %}"
|
|
77
|
+
data-trace-id="{{ trace_item.id }}">
|
|
78
|
+
<div class="flex items-start justify-between mb-2">
|
|
79
|
+
{% if trace_item.root_span_name %}
|
|
80
|
+
<div class="text-sm font-medium text-stone-200 truncate pr-2">{{ trace_item.root_span_name }}</div>
|
|
81
|
+
{% else %}
|
|
82
|
+
<div class="text-sm font-medium text-stone-400 truncate pr-2">{{ trace_item.trace_id }}</div>
|
|
83
|
+
{% endif %}
|
|
84
|
+
<div class="text-xs text-stone-500 bg-stone-800 px-2 py-0.5 rounded-full font-medium whitespace-nowrap">
|
|
85
|
+
{{ "%.1f"|format(trace_item.duration_ms() or 0) }}ms
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="text-xs text-stone-400 mb-1">
|
|
89
|
+
{{ trace_item.start_time|localtime|strftime("%-I:%M %p") }}
|
|
90
|
+
</div>
|
|
91
|
+
{% if trace_item.request_id %}
|
|
92
|
+
<div class="text-xs text-stone-600 truncate">
|
|
93
|
+
<span class="text-stone-500">Request</span> <span class="font-mono">{{ trace_item.request_id }}</span>
|
|
94
|
+
</div>
|
|
95
|
+
{% endif %}
|
|
96
|
+
<div class="flex items-center justify-between mt-2 pt-2 border-t border-stone-800 group-hover:border-stone-700">
|
|
97
|
+
<div class="text-xs text-stone-500">
|
|
98
|
+
{{ trace_item.spans.count() }} span{{ trace_item.spans.count()|pluralize }}
|
|
99
|
+
</div>
|
|
100
|
+
{% if trace_item.user_id %}
|
|
101
|
+
<div class="text-xs text-stone-500 bg-stone-800/50 px-1.5 py-0.5 rounded">
|
|
102
|
+
User: {{ trace_item.user_id }}
|
|
103
|
+
</div>
|
|
104
|
+
{% endif %}
|
|
105
|
+
</div>
|
|
106
|
+
</a>
|
|
107
|
+
</li>
|
|
108
|
+
{% endfor %}
|
|
109
|
+
</ul>
|
|
110
|
+
</div>
|
|
111
|
+
</aside>
|
|
112
|
+
|
|
113
|
+
<main id="content" class="flex-1 p-6 overflow-auto overscroll-contain ml-82">
|
|
114
|
+
{% htmxfragment "trace" %}
|
|
115
|
+
{% set show_delete_button = True %}
|
|
116
|
+
{% include "observer/_trace_detail.html" %}
|
|
117
|
+
{% endhtmxfragment %}
|
|
118
|
+
</main>
|
|
119
|
+
</div>
|
|
120
|
+
{% elif observer.is_enabled() %}
|
|
121
|
+
<div class="flex items-center justify-center min-h-screen p-6">
|
|
122
|
+
<div class="text-center max-w-2xl w-full">
|
|
123
|
+
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-6">
|
|
124
|
+
<!-- Icon and status -->
|
|
125
|
+
<div class="flex-shrink-0">
|
|
126
|
+
<div class="p-3 bg-stone-800/50 rounded-full mb-3 sm:mb-0">
|
|
127
|
+
{% if observer.is_summarizing() %}
|
|
128
|
+
<svg width="32" height="32" fill="currentColor" class="text-yellow-500" viewBox="0 0 16 16">
|
|
129
|
+
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
|
130
|
+
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
|
131
|
+
</svg>
|
|
132
|
+
{% else %}
|
|
133
|
+
<svg width="32" height="32" fill="currentColor" class="text-red-500 animate-pulse" viewBox="0 0 16 16">
|
|
134
|
+
<circle cx="8" cy="8" r="8"/>
|
|
135
|
+
</svg>
|
|
136
|
+
{% endif %}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<!-- Content -->
|
|
141
|
+
<div class="flex-1 text-center sm:text-left">
|
|
142
|
+
<h3 class="text-lg font-medium text-stone-200 mb-1">
|
|
143
|
+
{% if observer.is_summarizing() %}
|
|
144
|
+
Toolbar Summary Only
|
|
145
|
+
{% else %}
|
|
146
|
+
Recording Traces
|
|
147
|
+
{% endif %}
|
|
148
|
+
</h3>
|
|
149
|
+
<p class="text-sm text-stone-400 mb-4">
|
|
150
|
+
{% if observer.is_summarizing() %}
|
|
151
|
+
Performance summary is displayed in real-time. No traces are being stored.
|
|
152
|
+
{% else %}
|
|
153
|
+
Waiting for requests... Traces will appear here automatically.
|
|
154
|
+
{% endif %}
|
|
155
|
+
</p>
|
|
156
|
+
|
|
157
|
+
<!-- Actions -->
|
|
158
|
+
<div class="flex flex-col sm:flex-row items-center justify-center sm:justify-start gap-2">
|
|
159
|
+
{% if observer.is_summarizing() %}
|
|
160
|
+
<form method="post" action="." style="display: inline;">
|
|
161
|
+
{{ csrf_input }}
|
|
162
|
+
<input type="hidden" name="observe_action" value="persist">
|
|
163
|
+
<button type="submit" class="flex items-center space-x-2 px-3 py-1.5 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 cursor-pointer transition-colors">
|
|
164
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
165
|
+
<circle cx="8" cy="8" r="8"/>
|
|
166
|
+
</svg>
|
|
167
|
+
<span>Record Session Traces</span>
|
|
168
|
+
</button>
|
|
169
|
+
</form>
|
|
170
|
+
{% elif observer.is_persisting() %}
|
|
171
|
+
<button
|
|
172
|
+
hx-get="."
|
|
173
|
+
hx-swap="morph:innerHTML"
|
|
174
|
+
hx-target="#main-content"
|
|
175
|
+
class="flex items-center space-x-2 px-3 py-1.5 text-sm rounded-lg bg-stone-800 text-stone-300 hover:bg-stone-700 cursor-pointer transition-colors">
|
|
176
|
+
<svg class="htmx-request:animate-spin" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
177
|
+
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
|
178
|
+
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
|
179
|
+
</svg>
|
|
180
|
+
<span class="htmx-request:hidden">Check for Traces</span>
|
|
181
|
+
<span class="hidden htmx-request:inline">Checking...</span>
|
|
182
|
+
</button>
|
|
183
|
+
<form method="post" action="." style="display: inline;">
|
|
184
|
+
{{ csrf_input }}
|
|
185
|
+
<input type="hidden" name="observe_action" value="summary">
|
|
186
|
+
<button type="submit" class="flex items-center space-x-2 px-3 py-1.5 text-sm rounded-lg bg-stone-700 text-stone-200 hover:bg-stone-600 cursor-pointer transition-colors">
|
|
187
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
188
|
+
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
|
189
|
+
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
|
190
|
+
</svg>
|
|
191
|
+
<span>Stop Recording</span>
|
|
192
|
+
</button>
|
|
193
|
+
</form>
|
|
194
|
+
{% endif %}
|
|
195
|
+
|
|
196
|
+
<!-- Subtle disable option -->
|
|
197
|
+
<form method="post" action="." style="display: inline;">
|
|
198
|
+
{{ csrf_input }}
|
|
199
|
+
<input type="hidden" name="observe_action" value="disable">
|
|
200
|
+
<button type="submit" class="px-3 py-1.5 text-sm text-stone-500 hover:text-stone-400 transition-colors">
|
|
201
|
+
Disable Observer
|
|
202
|
+
</button>
|
|
203
|
+
</form>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
{% else %}
|
|
210
|
+
<div class="flex items-center justify-center min-h-screen p-6">
|
|
211
|
+
<div class="text-center max-w-2xl w-full">
|
|
212
|
+
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-6">
|
|
213
|
+
<!-- Icon -->
|
|
214
|
+
<div class="flex-shrink-0">
|
|
215
|
+
<div class="p-3 bg-stone-800/50 rounded-full mb-3 sm:mb-0">
|
|
216
|
+
<svg width="32" height="32" fill="currentColor" class="text-stone-500" viewBox="0 0 16 16">
|
|
217
|
+
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
|
218
|
+
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
|
219
|
+
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.708zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
|
|
220
|
+
</svg>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Content -->
|
|
225
|
+
<div class="flex-1 text-center sm:text-left">
|
|
226
|
+
<h3 class="text-lg font-medium text-stone-200 mb-1">Observer is Disabled</h3>
|
|
227
|
+
<p class="text-sm text-stone-400 mb-4">
|
|
228
|
+
Enable observer to start monitoring your application's performance and traces.
|
|
229
|
+
</p>
|
|
230
|
+
|
|
231
|
+
<!-- Mode descriptions in compact grid -->
|
|
232
|
+
<div class="bg-stone-800/30 rounded-lg p-3 mb-4">
|
|
233
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
234
|
+
<div class="text-left">
|
|
235
|
+
<h4 class="text-xs font-medium text-yellow-400 mb-1 flex items-center">
|
|
236
|
+
<svg width="12" height="12" fill="currentColor" class="mr-1.5" viewBox="0 0 16 16">
|
|
237
|
+
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
|
238
|
+
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
|
239
|
+
</svg>
|
|
240
|
+
Summary Mode
|
|
241
|
+
</h4>
|
|
242
|
+
<p class="text-xs text-stone-400">Monitor performance in real-time without saving traces.</p>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="text-left">
|
|
245
|
+
<h4 class="text-xs font-medium text-red-400 mb-1 flex items-center">
|
|
246
|
+
<svg width="12" height="12" fill="currentColor" class="mr-1.5" viewBox="0 0 16 16">
|
|
247
|
+
<circle cx="8" cy="8" r="8"/>
|
|
248
|
+
</svg>
|
|
249
|
+
Recording Mode
|
|
250
|
+
</h4>
|
|
251
|
+
<p class="text-xs text-stone-400">Record and store traces for detailed analysis.</p>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<!-- Actions -->
|
|
257
|
+
<div class="flex flex-col sm:flex-row items-center sm:items-start gap-2">
|
|
258
|
+
<form method="post" action=".">
|
|
259
|
+
{{ csrf_input }}
|
|
260
|
+
<input type="hidden" name="observe_action" value="summary">
|
|
261
|
+
<button type="submit" class="flex items-center justify-center space-x-2 px-4 py-1.5 text-sm rounded-lg bg-stone-700 text-stone-200 hover:bg-stone-600 cursor-pointer transition-colors w-full sm:w-auto">
|
|
262
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
263
|
+
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
|
264
|
+
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
|
265
|
+
</svg>
|
|
266
|
+
<span>Enable Summary</span>
|
|
267
|
+
</button>
|
|
268
|
+
</form>
|
|
269
|
+
<form method="post" action=".">
|
|
270
|
+
{{ csrf_input }}
|
|
271
|
+
<input type="hidden" name="observe_action" value="persist">
|
|
272
|
+
<button type="submit" class="flex items-center justify-center space-x-2 px-4 py-1.5 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 cursor-pointer transition-colors w-full sm:w-auto">
|
|
273
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
274
|
+
<circle cx="8" cy="8" r="8"/>
|
|
275
|
+
</svg>
|
|
276
|
+
<span>Start Recording Session</span>
|
|
277
|
+
</button>
|
|
278
|
+
</form>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
{% endif %}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
</body>
|
|
288
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<div id="observer-traces" class="h-full">
|
|
2
|
+
<div class="px-6 py-4 text-center">
|
|
3
|
+
<p>Loading spans...</p>
|
|
4
|
+
</div>
|
|
5
|
+
</div>
|
|
6
|
+
<script>
|
|
7
|
+
(function() {
|
|
8
|
+
var container = document.getElementById('observer-traces');
|
|
9
|
+
var loaded = false;
|
|
10
|
+
var parent = container.parentNode;
|
|
11
|
+
var observerMode = "{{ observer.mode() }}";
|
|
12
|
+
|
|
13
|
+
// Auto-enable summary mode if no mode is set
|
|
14
|
+
if (!observerMode || observerMode === "None") {
|
|
15
|
+
var form = new FormData();
|
|
16
|
+
form.append('observe_action', 'summary');
|
|
17
|
+
|
|
18
|
+
fetch("{{ url('observer:traces') }}", {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
body: form,
|
|
21
|
+
credentials: 'same-origin'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
26
|
+
entries.forEach(function(entry) {
|
|
27
|
+
if (entry.isIntersecting && !loaded) {
|
|
28
|
+
loaded = true;
|
|
29
|
+
var iframe = document.createElement('iframe');
|
|
30
|
+
iframe.src = "{{ url('observer:traces') }}";
|
|
31
|
+
iframe.frameBorder = "0";
|
|
32
|
+
iframe.style.width = "100%";
|
|
33
|
+
iframe.style.height = "100%";
|
|
34
|
+
container.innerHTML = '';
|
|
35
|
+
container.appendChild(iframe);
|
|
36
|
+
observer.disconnect();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}, { root: parent, threshold: 0 });
|
|
40
|
+
observer.observe(container);
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{% if observer.is_enabled() %}
|
|
2
|
+
<form method="post" action="{{ url('observer:traces') }}" style="display: inline;">
|
|
3
|
+
<input type="hidden" name="redirect_url" value="{{ request.get_full_path() }}">
|
|
4
|
+
{% if observer.is_summarizing() %}
|
|
5
|
+
{# Toggle from summary to persist #}
|
|
6
|
+
<input type="hidden" name="observe_action" value="persist">
|
|
7
|
+
{% else %}
|
|
8
|
+
{# Toggle from persist to summary #}
|
|
9
|
+
<input type="hidden" name="observe_action" value="summary">
|
|
10
|
+
{% endif %}
|
|
11
|
+
<button
|
|
12
|
+
class="inline-flex items-center cursor-pointer text-xs rounded-full px-2.5 py-px bg-white/20 text-white/80 whitespace-nowrap hover:bg-white/30"
|
|
13
|
+
type="submit"
|
|
14
|
+
data-toolbar-tab="Observer"
|
|
15
|
+
title="Toggle observer mode ({% if observer.is_summarizing() %}summary{% elif observer.is_persisting() %}persist{% else %}disabled{% endif %} mode){% if observer.get_current_trace_summary() %} - {{ observer.get_current_trace_summary() }}{% endif %} - Click to switch to {% if observer.is_summarizing() %}persist{% else %}summary{% endif %} mode">
|
|
16
|
+
{% if observer.is_persisting() %}
|
|
17
|
+
<span class="relative inline-flex size-2 mr-2.5">
|
|
18
|
+
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
|
19
|
+
<span class="relative inline-flex size-2 rounded-full bg-red-500"></span>
|
|
20
|
+
</span>
|
|
21
|
+
{% elif observer.is_summarizing() %}
|
|
22
|
+
<span class="relative inline-flex size-2 mr-2.5">
|
|
23
|
+
<span class="relative inline-flex size-2 rounded-full bg-zinc-500"></span>
|
|
24
|
+
</span>
|
|
25
|
+
{% endif %}
|
|
26
|
+
{% if observer.get_current_trace_summary() %}
|
|
27
|
+
<span class="font-mono tracking-tight">{{ observer.get_current_trace_summary() }}</span>
|
|
28
|
+
{% else %}
|
|
29
|
+
{% if observer.is_persisting() %}Recording{% elif observer.is_summarizing() %}Summary{% else %}Observing{% endif %}
|
|
30
|
+
{% endif %}
|
|
31
|
+
</button>
|
|
32
|
+
</form>
|
|
33
|
+
{% else %}
|
|
34
|
+
<form method="post" action="{{ url('observer:traces') }}" style="display: inline;">
|
|
35
|
+
<input type="hidden" name="redirect_url" value="{{ request.get_full_path() }}">
|
|
36
|
+
<input type="hidden" name="observe_action" value="summary">
|
|
37
|
+
<button
|
|
38
|
+
type="submit"
|
|
39
|
+
class="cursor-pointer text-xs rounded-full px-2 py-px bg-white/20 text-white/80 whitespace-nowrap hover:bg-white/30"
|
|
40
|
+
title="Enable observer">
|
|
41
|
+
<span class="rounded-full bg-zinc-500 w-2 h-2 inline-block mr-1"></span>
|
|
42
|
+
Observer disabled
|
|
43
|
+
</button>
|
|
44
|
+
</form>
|
|
45
|
+
{% endif %}
|
plain/observer/urls.py
ADDED
plain/observer/views.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from plain.auth.views import AuthViewMixin
|
|
4
|
+
from plain.htmx.views import HTMXViewMixin
|
|
5
|
+
from plain.http import JsonResponse, Response, ResponseRedirect
|
|
6
|
+
from plain.runtime import settings
|
|
7
|
+
from plain.views import TemplateView
|
|
8
|
+
|
|
9
|
+
from .core import Observer
|
|
10
|
+
from .models import Trace
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ObserverTracesView(AuthViewMixin, HTMXViewMixin, TemplateView):
|
|
14
|
+
template_name = "observer/traces.html"
|
|
15
|
+
admin_required = True
|
|
16
|
+
|
|
17
|
+
@cached_property
|
|
18
|
+
def observer(self):
|
|
19
|
+
"""Get the Observer instance for this request."""
|
|
20
|
+
return Observer(self.request)
|
|
21
|
+
|
|
22
|
+
def check_auth(self):
|
|
23
|
+
# Allow the view if we're in DEBUG
|
|
24
|
+
if settings.DEBUG:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
super().check_auth()
|
|
28
|
+
|
|
29
|
+
def get_response(self):
|
|
30
|
+
response = super().get_response()
|
|
31
|
+
# So we can load it in the toolbar
|
|
32
|
+
response.headers["X-Frame-Options"] = "SAMEORIGIN"
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
def get_template_context(self):
|
|
36
|
+
context = super().get_template_context()
|
|
37
|
+
context["observer"] = self.observer
|
|
38
|
+
context["traces"] = Trace.objects.all()
|
|
39
|
+
if trace_id := self.request.query_params.get("trace_id"):
|
|
40
|
+
context["trace"] = Trace.objects.filter(id=trace_id).first()
|
|
41
|
+
else:
|
|
42
|
+
context["trace"] = context["traces"].first()
|
|
43
|
+
return context
|
|
44
|
+
|
|
45
|
+
def get(self):
|
|
46
|
+
# Check if JSON format is requested
|
|
47
|
+
if self.request.query_params.get("format") == "json":
|
|
48
|
+
if trace_id := self.request.query_params.get("trace_id"):
|
|
49
|
+
if trace := Trace.objects.filter(id=trace_id).first():
|
|
50
|
+
return JsonResponse(trace.as_dict())
|
|
51
|
+
return JsonResponse({"error": "Trace not found"}, status=404)
|
|
52
|
+
|
|
53
|
+
return super().get()
|
|
54
|
+
|
|
55
|
+
def htmx_post_enable_summary(self):
|
|
56
|
+
"""Enable summary mode via HTMX."""
|
|
57
|
+
response = Response(status_code=204)
|
|
58
|
+
response.headers["HX-Refresh"] = "true"
|
|
59
|
+
self.observer.enable_summary_mode(response)
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
def htmx_post_enable_persist(self):
|
|
63
|
+
"""Enable full persist mode via HTMX."""
|
|
64
|
+
response = Response(status_code=204)
|
|
65
|
+
response.headers["HX-Refresh"] = "true"
|
|
66
|
+
self.observer.enable_persist_mode(response)
|
|
67
|
+
return response
|
|
68
|
+
|
|
69
|
+
def htmx_post_disable(self):
|
|
70
|
+
"""Disable observer via HTMX."""
|
|
71
|
+
response = Response(status_code=204)
|
|
72
|
+
response.headers["HX-Refresh"] = "true"
|
|
73
|
+
self.observer.disable(response)
|
|
74
|
+
return response
|
|
75
|
+
|
|
76
|
+
def htmx_delete_traces(self):
|
|
77
|
+
"""Clear all traces via HTMX DELETE."""
|
|
78
|
+
Trace.objects.all().delete()
|
|
79
|
+
response = Response(status_code=204)
|
|
80
|
+
response.headers["HX-Refresh"] = "true"
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
def htmx_delete_trace(self):
|
|
84
|
+
"""Delete a specific trace via HTMX DELETE."""
|
|
85
|
+
trace_id = self.request.query_params.get("trace_id")
|
|
86
|
+
Trace.objects.get(id=trace_id).delete()
|
|
87
|
+
response = Response(status_code=204)
|
|
88
|
+
response.headers["HX-Refresh"] = "true"
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
def post(self):
|
|
92
|
+
"""A standard, non-htmx post used by the button html (where htmx may not be available)."""
|
|
93
|
+
|
|
94
|
+
observe_action = self.request.data["observe_action"]
|
|
95
|
+
|
|
96
|
+
response = ResponseRedirect(self.request.data.get("redirect_url", "."))
|
|
97
|
+
|
|
98
|
+
if observe_action == "summary":
|
|
99
|
+
self.observer.enable_summary_mode(response) # Default to summary mode
|
|
100
|
+
elif observe_action == "persist":
|
|
101
|
+
self.observer.enable_persist_mode(response)
|
|
102
|
+
elif observe_action == "disable":
|
|
103
|
+
self.observer.disable(response)
|
|
104
|
+
|
|
105
|
+
return response
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.observer
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: On-page telemetry and observability tools for Plain.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: opentelemetry-sdk>=1.34.1
|
|
10
|
+
Requires-Dist: plain-admin<1.0.0
|
|
11
|
+
Requires-Dist: plain<1.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# plain.observer
|
|
15
|
+
|
|
16
|
+
**Monitor.**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
plain/observer/CHANGELOG.md,sha256=bOl9N42H3a0_nUQjhLCJR1Zqj2DOgRCLstzBbYpDmbw,27
|
|
2
|
+
plain/observer/README.md,sha256=5wM48-iE8i7xOjIK8KCgZ-Vmp2xpDtX73UnE5QeNnd4,31
|
|
3
|
+
plain/observer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
plain/observer/admin.py,sha256=d5ekU48GiiQ70K8ulSd0v_7P1v12U8rIQ8LbiNgKv0Y,2874
|
|
5
|
+
plain/observer/cli.py,sha256=JBJzOYl3V973WQbtj8AEde6zxfWdEZx-u3g7c15-4lU,544
|
|
6
|
+
plain/observer/config.py,sha256=ivLgpCD87VgE8FL095zIoTdIBsRhB0VPKxxaWc38Ouw,1078
|
|
7
|
+
plain/observer/core.py,sha256=D9vX0GP8JBB8-NFAKrOwPSHl_wdZo1h0A5C6xKsJVjA,2245
|
|
8
|
+
plain/observer/default_settings.py,sha256=JN2jT2wfa6f80EqU0p4Ox_47xyxL-Ym5-_pftY7xj2U,197
|
|
9
|
+
plain/observer/models.py,sha256=cD72rht7Bb-8YARYvylBc9W0garebWgFsH_vfx1ubT8,11999
|
|
10
|
+
plain/observer/otel.py,sha256=KmdMCJqUvm6OY8di5p3_0TnNC3pI_lDc8O9dbDRVPZ4,12501
|
|
11
|
+
plain/observer/urls.py,sha256=LcsKBWRqt8gs385tV9z-j6kSewzWsOWftadi59QqIIE,194
|
|
12
|
+
plain/observer/views.py,sha256=TrwatLFf9M67ax4EhF_P1Q3X_o3hzsAEr4Po48peiQM,3673
|
|
13
|
+
plain/observer/migrations/0001_initial.py,sha256=I-4DNr_WK2Gx428Ql3pG3-W8C0xvk0FVTZnd5swIAQ4,3376
|
|
14
|
+
plain/observer/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
plain/observer/templates/admin/observer/trace_detail.html,sha256=qW03sNNKGivPVQMI8rFtSCXGTOH_qw_nmXhpIZIu4zo,161
|
|
16
|
+
plain/observer/templates/observer/_trace_detail.html,sha256=q98IdA28m7vlel2ND090MoFE5Kmw8_yLExJd57-MUus,21631
|
|
17
|
+
plain/observer/templates/observer/traces.html,sha256=dJPp2pcFDJOLmHihH5-qnVAgAJEYGCBy6Fn1OreaWjw,20700
|
|
18
|
+
plain/observer/templates/toolbar/observer.html,sha256=uaDKiWR7EYqC1kEXE-uHDlE7nfFEMR_zmOgvlKwQHJ4,1365
|
|
19
|
+
plain/observer/templates/toolbar/observer_button.html,sha256=BXzka5LyvhQPBLFBY_QSNQOfdFtbIN3SUF3ZjSVGSjw,2565
|
|
20
|
+
plain_observer-0.0.0.dist-info/METADATA,sha256=Gt44f0UBXrlS1ln2oAFmD4dOVIic9oqGK85nytYniug,429
|
|
21
|
+
plain_observer-0.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
22
|
+
plain_observer-0.0.0.dist-info/licenses/LICENSE,sha256=YZdq6Pz8ivjs97eSVLRmoGDI1hjEikX6N49DfM0DWio,1500
|
|
23
|
+
plain_observer-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Dropseed, LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|