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,364 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-3">
|
|
2
|
+
<div>
|
|
3
|
+
<h2 class="text-lg font-semibold">{{ trace.root_span_name }}</h2>
|
|
4
|
+
<div class="text-xs text-stone-500 mt-1">
|
|
5
|
+
{{ trace.start_time|localtime|strftime("%b %-d, %-I:%M %p") }} • {{ "%.1f"|format(trace.duration_ms() or 0) }}ms
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex items-center space-x-2">
|
|
9
|
+
<button
|
|
10
|
+
onclick="copyTraceAsJson('{{ trace.id }}', this)"
|
|
11
|
+
class="p-1.5 rounded-sm bg-stone-700 text-stone-300 hover:bg-stone-600 cursor-pointer transition-colors"
|
|
12
|
+
title="Copy trace data as JSON">
|
|
13
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
14
|
+
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
|
|
15
|
+
<path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
{% if show_delete_button|default(true) %}
|
|
19
|
+
<button
|
|
20
|
+
hx-delete="?trace_id={{ trace.id }}"
|
|
21
|
+
plain-hx-action="trace"
|
|
22
|
+
hx-swap="morph:innerHTML"
|
|
23
|
+
hx-target="#main-content"
|
|
24
|
+
hx-confirm="Delete this trace?"
|
|
25
|
+
class="p-1.5 rounded-sm bg-stone-700 text-stone-300 hover:bg-red-600 hover:text-white cursor-pointer transition-colors"
|
|
26
|
+
title="Delete this trace">
|
|
27
|
+
<svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
28
|
+
<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"/>
|
|
29
|
+
<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"/>
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
{% endif %}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-stone-500 mb-4">
|
|
36
|
+
<div>
|
|
37
|
+
<span class="text-stone-400">Trace ID:</span> <span class="font-mono">{{ trace.trace_id }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
{% if trace.request_id %}
|
|
40
|
+
<div>
|
|
41
|
+
<span class="text-stone-400">Request:</span> <span class="font-mono">{{ trace.request_id }}</span>
|
|
42
|
+
</div>
|
|
43
|
+
{% endif %}
|
|
44
|
+
{% if trace.user_id %}
|
|
45
|
+
<div>
|
|
46
|
+
<span class="text-stone-400">User:</span> {{ trace.user_id }}
|
|
47
|
+
</div>
|
|
48
|
+
{% endif %}
|
|
49
|
+
{% if trace.session_id %}
|
|
50
|
+
<div>
|
|
51
|
+
<span class="text-stone-400">Session:</span> {{ trace.session_id }}
|
|
52
|
+
</div>
|
|
53
|
+
{% endif %}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Spans waterfall visualization -->
|
|
57
|
+
<div class="mt-4 space-y-1 text-xs">
|
|
58
|
+
{% for span in trace.get_annotated_spans() %}
|
|
59
|
+
|
|
60
|
+
<!-- Calculate relative positioning for waterfall -->
|
|
61
|
+
{% set span_start_offset = ((span.start_time - trace.start_time).total_seconds() * 1000) %}
|
|
62
|
+
{% set start_percent = (span_start_offset / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
63
|
+
{% set width_percent = (span.duration_ms() / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
64
|
+
|
|
65
|
+
<div style="padding-left: {{ span.level * 1 }}rem;" class="border-l border-stone-700">
|
|
66
|
+
<details id="{{ trace.id }}-span-{{ loop.index }}" class="rounded bg-white/5 hover:bg-white/10 transition-colors min-w-[600px] ml-px">
|
|
67
|
+
<summary class="cursor-pointer p-2 list-none [&::-webkit-details-marker]:hidden">
|
|
68
|
+
<div class="flex items-center">
|
|
69
|
+
<div class="w-4 h-4 mr-2 flex items-center justify-center">
|
|
70
|
+
<svg class="w-3 h-3 transform transition-transform details-open:rotate-90" fill="currentColor" viewBox="0 0 20 20">
|
|
71
|
+
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="w-80 flex items-center space-x-2">
|
|
76
|
+
<div class="text-stone-400 whitespace-nowrap">
|
|
77
|
+
{{ span.start_time|localtime|strftime("%-I:%M:%S %p") }}
|
|
78
|
+
</div>
|
|
79
|
+
<div class="flex-grow whitespace-nowrap truncate">{{ span.name }}</div>
|
|
80
|
+
|
|
81
|
+
{% if span.annotations %}
|
|
82
|
+
<div class="flex items-center space-x-1 flex-shrink-0">
|
|
83
|
+
{% for annotation in span.annotations %}
|
|
84
|
+
<span class="w-4 h-4 inline-flex justify-center items-center text-xs rounded-full
|
|
85
|
+
data-[severity='warning']:bg-amber-500/20
|
|
86
|
+
data-[severity='warning']:text-amber-400
|
|
87
|
+
data-[severity='error']:bg-red-500/20
|
|
88
|
+
data-[severity='error']:text-red-400
|
|
89
|
+
data-[severity='info']:bg-blue-500/20
|
|
90
|
+
data-[severity='info']:text-blue-400"
|
|
91
|
+
data-severity="{{ annotation.severity }}"
|
|
92
|
+
title="{{ annotation.message }}">
|
|
93
|
+
!
|
|
94
|
+
</span>
|
|
95
|
+
{% endfor %}
|
|
96
|
+
</div>
|
|
97
|
+
{% endif %}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="flex-1 px-4 min-w-[300px]">
|
|
101
|
+
<div class="relative h-6 bg-stone-800/50 rounded-sm">
|
|
102
|
+
<div
|
|
103
|
+
class="absolute top-1 bottom-1 rounded-sm transition-opacity hover:opacity-80
|
|
104
|
+
data-[kind='SERVER']:bg-blue-500
|
|
105
|
+
data-[kind='CLIENT']:bg-emerald-500
|
|
106
|
+
data-[kind='CONSUMER']:bg-amber-500
|
|
107
|
+
data-[kind='PRODUCER']:bg-purple-500
|
|
108
|
+
data-[kind='INTERNAL']:bg-gray-500
|
|
109
|
+
bg-stone-600"
|
|
110
|
+
data-kind="{{ span.kind }}"
|
|
111
|
+
style="left: {{ start_percent }}%; width: {{ width_percent }}%;"
|
|
112
|
+
title="{{ span.name }} - {{ span.duration_ms() }}ms">
|
|
113
|
+
</div>
|
|
114
|
+
<div
|
|
115
|
+
class="absolute inset-0 flex items-center justify-start pl-1 text-xs text-white/80 font-medium whitespace-nowrap pointer-events-none"
|
|
116
|
+
style="left: {{ start_percent }}%; width: {{ width_percent }}%;">
|
|
117
|
+
{{ "%.2f"|format(span.duration_ms()) }}ms
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</summary>
|
|
123
|
+
<div class="p-4 pt-2 bg-stone-900/50 border-t border-stone-700">
|
|
124
|
+
{% if span.sql_query %}
|
|
125
|
+
<div class="mb-6 bg-stone-800 rounded-lg border border-stone-600 overflow-hidden
|
|
126
|
+
{% if span.annotations %}ring-2 ring-amber-500/50{% endif %}">
|
|
127
|
+
<div class="bg-stone-700 px-4 py-2 border-b border-stone-600 flex items-center justify-between">
|
|
128
|
+
<h4 class="text-sm font-semibold text-emerald-500 flex items-center">
|
|
129
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database-fill w-4 h-4 mr-2" viewBox="0 0 16 16">
|
|
130
|
+
<path d="M3.904 1.777C4.978 1.289 6.427 1 8 1s3.022.289 4.096.777C13.125 2.245 14 2.993 14 4s-.875 1.755-1.904 2.223C11.022 6.711 9.573 7 8 7s-3.022-.289-4.096-.777C2.875 5.755 2 5.007 2 4s.875-1.755 1.904-2.223"/>
|
|
131
|
+
<path d="M2 6.161V7c0 1.007.875 1.755 1.904 2.223C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777C13.125 8.755 14 8.007 14 7v-.839c-.457.432-1.004.751-1.49.972C11.278 7.693 9.682 8 8 8s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
|
|
132
|
+
<path d="M2 9.161V10c0 1.007.875 1.755 1.904 2.223C4.978 12.711 6.427 13 8 13s3.022-.289 4.096-.777C13.125 11.755 14 11.007 14 10v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
|
|
133
|
+
<path d="M2 12.161V13c0 1.007.875 1.755 1.904 2.223C4.978 15.711 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13v-.839c-.457.432-1.004.751-1.49.972-1.232.56-2.828.867-4.51.867s-3.278-.307-4.51-.867c-.486-.22-1.033-.54-1.49-.972"/>
|
|
134
|
+
</svg>
|
|
135
|
+
Database Query
|
|
136
|
+
</h4>
|
|
137
|
+
{% if span.annotations %}
|
|
138
|
+
<div class="flex items-center space-x-1">
|
|
139
|
+
{% for annotation in span.annotations %}
|
|
140
|
+
<span class="px-2 py-0.5 text-xs rounded-full
|
|
141
|
+
data-[severity='warning']:bg-amber-500/20
|
|
142
|
+
data-[severity='warning']:text-amber-400
|
|
143
|
+
data-[severity='error']:bg-red-500/20
|
|
144
|
+
data-[severity='error']:text-red-400
|
|
145
|
+
data-[severity='info']:bg-blue-500/20
|
|
146
|
+
data-[severity='info']:text-blue-400"
|
|
147
|
+
data-severity="{{ annotation.severity }}">
|
|
148
|
+
{{ annotation.message }}
|
|
149
|
+
</span>
|
|
150
|
+
{% endfor %}
|
|
151
|
+
</div>
|
|
152
|
+
{% endif %}
|
|
153
|
+
</div>
|
|
154
|
+
<div class="p-4">
|
|
155
|
+
<pre class="text-xs text-stone-200 font-mono whitespace-pre-wrap overflow-x-auto"><code>{{ span.get_formatted_sql() }}</code></pre>
|
|
156
|
+
|
|
157
|
+
{% if span.sql_query_params %}
|
|
158
|
+
<div class="mt-4 pt-4 border-t border-stone-600">
|
|
159
|
+
<h5 class="text-xs font-semibold text-stone-400 mb-2">Query Parameters</h5>
|
|
160
|
+
<div class="space-y-1">
|
|
161
|
+
{% for param_key, param_value in span.sql_query_params.items() %}
|
|
162
|
+
<div class="flex text-xs">
|
|
163
|
+
<span class="text-stone-500 min-w-0 flex-shrink-0 pr-2 font-mono">{{ param_key }}:</span>
|
|
164
|
+
<span class="text-stone-300 break-words font-mono">{{ param_value }}</span>
|
|
165
|
+
</div>
|
|
166
|
+
{% endfor %}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
{% endif %}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
{% endif %}
|
|
173
|
+
|
|
174
|
+
{% if span.get_exception_stacktrace() %}
|
|
175
|
+
<div class="mb-6 bg-red-900/20 rounded-lg border border-red-600/30 overflow-hidden">
|
|
176
|
+
<div class="bg-red-900/40 px-4 py-2 border-b border-red-600/30">
|
|
177
|
+
<h4 class="text-sm font-semibold text-red-300 flex items-center">
|
|
178
|
+
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
179
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"/>
|
|
180
|
+
</svg>
|
|
181
|
+
Exception Stacktrace
|
|
182
|
+
</h4>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="p-4">
|
|
185
|
+
<pre class="text-xs text-red-100 font-mono whitespace-pre-wrap overflow-x-auto"><code>{{ span.get_exception_stacktrace() }}</code></pre>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
{% endif %}
|
|
189
|
+
|
|
190
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
191
|
+
<div>
|
|
192
|
+
<h4 class="text-sm font-semibold text-stone-300 mb-3">Basic Information</h4>
|
|
193
|
+
<div class="space-y-2 text-xs">
|
|
194
|
+
<div class="flex">
|
|
195
|
+
<span class="text-stone-400 w-20">ID:</span>
|
|
196
|
+
<span class="text-stone-200 font-mono">{{ span.span_id }}</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="flex">
|
|
199
|
+
<span class="text-stone-400 w-20">Name:</span>
|
|
200
|
+
<span class="text-stone-200">{{ span.name }}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="flex">
|
|
203
|
+
<span class="text-stone-400 w-20">Kind:</span>
|
|
204
|
+
<span class="px-2 py-0.5 rounded text-xs font-medium
|
|
205
|
+
data-[kind='SERVER']:bg-blue-500/20 data-[kind='SERVER']:text-blue-300
|
|
206
|
+
data-[kind='CLIENT']:bg-emerald-500/20 data-[kind='CLIENT']:text-emerald-300
|
|
207
|
+
data-[kind='CONSUMER']:bg-amber-500/20 data-[kind='CONSUMER']:text-amber-300
|
|
208
|
+
data-[kind='PRODUCER']:bg-purple-500/20 data-[kind='PRODUCER']:text-purple-300
|
|
209
|
+
data-[kind='INTERNAL']:bg-gray-500/20 data-[kind='INTERNAL']:text-gray-300
|
|
210
|
+
bg-gray-500/20 text-gray-300"
|
|
211
|
+
data-kind="{{ span.kind }}">
|
|
212
|
+
{{ span.kind }}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="flex">
|
|
216
|
+
<span class="text-stone-400 w-20">Duration:</span>
|
|
217
|
+
<span class="text-stone-200">{{ "%.2f"|format(span.duration_ms() or 0) }}ms</span>
|
|
218
|
+
</div>
|
|
219
|
+
{% if span.parent_id %}
|
|
220
|
+
<div class="flex">
|
|
221
|
+
<span class="text-stone-400 w-20">Parent:</span>
|
|
222
|
+
<span class="text-stone-200 font-mono text-xs">{{ span.parent_id }}</span>
|
|
223
|
+
</div>
|
|
224
|
+
{% endif %}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div>
|
|
229
|
+
<h4 class="text-sm font-semibold text-stone-300 mb-3">Timing</h4>
|
|
230
|
+
<div class="space-y-2 text-xs">
|
|
231
|
+
<div class="flex">
|
|
232
|
+
<span class="text-stone-400 w-20">Started:</span>
|
|
233
|
+
<span class="text-stone-200">{{ span.start_time|localtime|strftime("%-I:%M:%S.%f %p") }}</span>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="flex">
|
|
236
|
+
<span class="text-stone-400 w-20">Ended:</span>
|
|
237
|
+
<span class="text-stone-200">{{ span.end_time|localtime|strftime("%-I:%M:%S.%f %p") }}</span>
|
|
238
|
+
</div>
|
|
239
|
+
{% if span.status and span.status != '' and span.status != 'UNSET' %}
|
|
240
|
+
<div class="flex">
|
|
241
|
+
<span class="text-stone-400 w-20">Status:</span>
|
|
242
|
+
<span class="px-2 py-0.5 rounded text-xs font-medium
|
|
243
|
+
data-[status='ERROR']:bg-red-500/20 data-[status='ERROR']:text-red-300
|
|
244
|
+
data-[status='OK']:bg-green-500/20 data-[status='OK']:text-green-300
|
|
245
|
+
bg-yellow-500/20 text-yellow-300"
|
|
246
|
+
data-status="{{ span.status }}">
|
|
247
|
+
{{ span.status }}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
{% endif %}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{% if span.attributes %}
|
|
256
|
+
<div class="mt-6">
|
|
257
|
+
<h4 class="text-sm font-semibold text-stone-300 mb-3">Attributes</h4>
|
|
258
|
+
<div class="bg-stone-800/50 rounded p-3 max-h-48 overflow-y-auto">
|
|
259
|
+
<div class="space-y-1 text-xs">
|
|
260
|
+
{% for key, value in span.attributes.items() %}
|
|
261
|
+
<div class="flex">
|
|
262
|
+
<span class="text-stone-400 min-w-0 flex-shrink-0 pr-2">{{ key }}:</span>
|
|
263
|
+
<span class="text-stone-200 break-words">{{ value }}</span>
|
|
264
|
+
</div>
|
|
265
|
+
{% endfor %}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
{% endif %}
|
|
270
|
+
|
|
271
|
+
{% if span.events %}
|
|
272
|
+
<div class="mt-6">
|
|
273
|
+
<h4 class="text-sm font-semibold text-stone-300 mb-3">Events ({{ span.events|length }})</h4>
|
|
274
|
+
<div class="bg-stone-800/50 rounded p-3 max-h-48 overflow-y-auto">
|
|
275
|
+
<div class="space-y-3 text-xs">
|
|
276
|
+
{% for event in span.events %}
|
|
277
|
+
<div class="border-l-2 border-stone-600 pl-3">
|
|
278
|
+
<div class="flex items-center justify-between mb-1">
|
|
279
|
+
<div class="text-stone-200 font-medium">{{ event.name }}</div>
|
|
280
|
+
<div class="text-stone-400 text-xs">
|
|
281
|
+
{% set formatted_time = span.format_event_timestamp(event.timestamp) %}
|
|
282
|
+
{% if formatted_time.__class__.__name__ == 'datetime' %}
|
|
283
|
+
{{ formatted_time|localtime|strftime("%-I:%M:%S.%f %p") }}
|
|
284
|
+
{% else %}
|
|
285
|
+
{{ formatted_time }}
|
|
286
|
+
{% endif %}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
{% if event.attributes %}
|
|
290
|
+
<div class="space-y-1">
|
|
291
|
+
{% for key, value in event.attributes.items() %}
|
|
292
|
+
<div class="flex">
|
|
293
|
+
<span class="text-stone-400 min-w-0 flex-shrink-0 pr-2">{{ key }}:</span>
|
|
294
|
+
<pre class="text-stone-200 whitespace-pre-wrap break-words font-mono text-xs">{{ value }}</pre>
|
|
295
|
+
</div>
|
|
296
|
+
{% endfor %}
|
|
297
|
+
</div>
|
|
298
|
+
{% endif %}
|
|
299
|
+
</div>
|
|
300
|
+
{% endfor %}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
{% endif %}
|
|
305
|
+
|
|
306
|
+
{% if span.links %}
|
|
307
|
+
<div class="mt-6">
|
|
308
|
+
<h4 class="text-sm font-semibold text-stone-300 mb-3">Links ({{ span.links|length }})</h4>
|
|
309
|
+
<div class="bg-stone-800/50 rounded p-3">
|
|
310
|
+
<div class="space-y-2 text-xs">
|
|
311
|
+
{% for link in span.links %}
|
|
312
|
+
<div class="border-l-2 border-stone-600 pl-2">
|
|
313
|
+
<div class="text-stone-200 font-mono">{{ link.context.trace_id }}</div>
|
|
314
|
+
<div class="text-stone-400 font-mono">{{ link.context.span_id }}</div>
|
|
315
|
+
</div>
|
|
316
|
+
{% endfor %}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
{% endif %}
|
|
321
|
+
</div>
|
|
322
|
+
</details>
|
|
323
|
+
</div>
|
|
324
|
+
{% else %}
|
|
325
|
+
<div>No spans...</div>
|
|
326
|
+
{% endfor %}
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<style>
|
|
330
|
+
/* Custom details arrow animation */
|
|
331
|
+
details[open] summary svg {
|
|
332
|
+
transform: rotate(90deg);
|
|
333
|
+
}
|
|
334
|
+
</style>
|
|
335
|
+
|
|
336
|
+
<script>
|
|
337
|
+
async function copyTraceAsJson(traceId, button) {
|
|
338
|
+
try {
|
|
339
|
+
const response = await fetch(`?trace_id=${traceId}&format=json`);
|
|
340
|
+
const data = await response.json();
|
|
341
|
+
|
|
342
|
+
// Pretty print the JSON
|
|
343
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
344
|
+
|
|
345
|
+
// Copy to clipboard
|
|
346
|
+
await navigator.clipboard.writeText(jsonString);
|
|
347
|
+
|
|
348
|
+
// Show success feedback
|
|
349
|
+
const originalTitle = button.title;
|
|
350
|
+
button.title = 'Copied!';
|
|
351
|
+
button.classList.remove('bg-stone-700', 'hover:bg-stone-600');
|
|
352
|
+
button.classList.add('bg-green-600', 'hover:bg-green-700');
|
|
353
|
+
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
button.title = originalTitle;
|
|
356
|
+
button.classList.remove('bg-green-600', 'hover:bg-green-700');
|
|
357
|
+
button.classList.add('bg-stone-700', 'hover:bg-stone-600');
|
|
358
|
+
}, 2000);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('Failed to copy trace data:', error);
|
|
361
|
+
alert('Failed to copy trace data. See console for details.');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
</script>
|