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.

@@ -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
@@ -0,0 +1,10 @@
1
+ from plain.urls import Router, path
2
+
3
+ from . import views
4
+
5
+
6
+ class ObserverRouter(Router):
7
+ namespace = "observer"
8
+ urls = [
9
+ path("", views.ObserverTracesView, name="traces"),
10
+ ]
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.