plain.observer 0.5.0__py3-none-any.whl → 0.6.1__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 +30 -0
- plain/observer/admin.py +46 -1
- plain/observer/config.py +22 -1
- plain/observer/logging.py +75 -0
- plain/observer/migrations/0004_trace_app_name_trace_app_version.py +23 -0
- plain/observer/migrations/0005_log_log_plainobserv_trace_i_fcfb7d_idx_and_more.py +67 -0
- plain/observer/migrations/0006_remove_log_logger.py +16 -0
- plain/observer/models.py +136 -19
- plain/observer/otel.py +55 -16
- plain/observer/templates/observer/partials/log.html +15 -0
- plain/observer/templates/observer/partials/span.html +299 -0
- plain/observer/templates/observer/trace.html +124 -352
- plain/observer/templates/observer/traces.html +50 -28
- plain/observer/views.py +15 -9
- {plain_observer-0.5.0.dist-info → plain_observer-0.6.1.dist-info}/METADATA +2 -2
- plain_observer-0.6.1.dist-info/RECORD +33 -0
- plain_observer-0.5.0.dist-info/RECORD +0 -27
- {plain_observer-0.5.0.dist-info → plain_observer-0.6.1.dist-info}/WHEEL +0 -0
- {plain_observer-0.5.0.dist-info → plain_observer-0.6.1.dist-info}/licenses/LICENSE +0 -0
plain/observer/otel.py
CHANGED
|
@@ -10,6 +10,7 @@ from opentelemetry.semconv.attributes import url_attributes
|
|
|
10
10
|
from opentelemetry.trace import SpanKind, format_span_id, format_trace_id
|
|
11
11
|
|
|
12
12
|
from plain.http.cookie import unsign_cookie_value
|
|
13
|
+
from plain.logs import app_logger
|
|
13
14
|
from plain.models.otel import suppress_db_tracing
|
|
14
15
|
from plain.runtime import settings
|
|
15
16
|
|
|
@@ -218,15 +219,12 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
218
219
|
trace_info = self._traces[trace_id]
|
|
219
220
|
trace_info["mode"] = mode
|
|
220
221
|
|
|
221
|
-
# Clean up old traces if too many
|
|
222
|
-
if len(self._traces) > 1000:
|
|
223
|
-
# Remove oldest 100 traces
|
|
224
|
-
oldest_ids = sorted(self._traces.keys())[:100]
|
|
225
|
-
for old_id in oldest_ids:
|
|
226
|
-
del self._traces[old_id]
|
|
227
|
-
|
|
228
222
|
span_id = f"0x{format_span_id(span.get_span_context().span_id)}"
|
|
229
223
|
|
|
224
|
+
# Enable DEBUG logging only for PERSIST mode (when logs are captured)
|
|
225
|
+
if trace_info["mode"] == ObserverMode.PERSIST.value:
|
|
226
|
+
app_logger.debug_mode.start()
|
|
227
|
+
|
|
230
228
|
# Store span (we know mode is truthy if we get here)
|
|
231
229
|
trace_info["active_otel_spans"][span_id] = span
|
|
232
230
|
|
|
@@ -246,6 +244,10 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
246
244
|
|
|
247
245
|
trace_info = self._traces[trace_id]
|
|
248
246
|
|
|
247
|
+
# Disable DEBUG logging only for PERSIST mode spans
|
|
248
|
+
if trace_info["mode"] == ObserverMode.PERSIST.value:
|
|
249
|
+
app_logger.debug_mode.end()
|
|
250
|
+
|
|
249
251
|
# Move span from active to completed
|
|
250
252
|
if trace_info["active_otel_spans"].pop(span_id, None):
|
|
251
253
|
trace_info["completed_otel_spans"].append(span)
|
|
@@ -264,16 +266,29 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
264
266
|
|
|
265
267
|
# Export if in persist mode
|
|
266
268
|
if trace_info["mode"] == ObserverMode.PERSIST.value:
|
|
269
|
+
# Get and remove logs for this trace
|
|
270
|
+
from .logging import observer_log_handler
|
|
271
|
+
|
|
272
|
+
if observer_log_handler:
|
|
273
|
+
logs = observer_log_handler.pop_logs_for_trace(trace_id)
|
|
274
|
+
else:
|
|
275
|
+
logs = []
|
|
276
|
+
|
|
267
277
|
logger.debug(
|
|
268
|
-
"Exporting %d spans for trace %s",
|
|
278
|
+
"Exporting %d spans and %d logs for trace %s",
|
|
269
279
|
len(trace_info["span_models"]),
|
|
280
|
+
len(logs),
|
|
270
281
|
trace_id,
|
|
271
282
|
)
|
|
272
283
|
# The trace is done now, so we can get a more accurate summary
|
|
273
284
|
trace_info["trace"].summary = trace_info["trace"].get_trace_summary(
|
|
274
285
|
trace_info["span_models"]
|
|
275
286
|
)
|
|
276
|
-
self._export_trace(
|
|
287
|
+
self._export_trace(
|
|
288
|
+
trace=trace_info["trace"],
|
|
289
|
+
spans=trace_info["span_models"],
|
|
290
|
+
logs=logs,
|
|
291
|
+
)
|
|
277
292
|
|
|
278
293
|
# Clean up trace
|
|
279
294
|
del self._traces[trace_id]
|
|
@@ -315,19 +330,40 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
315
330
|
|
|
316
331
|
return trace_info["trace"].get_trace_summary(span_models)
|
|
317
332
|
|
|
318
|
-
def _export_trace(self, trace,
|
|
319
|
-
"""Export trace and
|
|
320
|
-
from .models import Span, Trace
|
|
333
|
+
def _export_trace(self, *, trace, spans, logs):
|
|
334
|
+
"""Export trace, spans, and logs to the database."""
|
|
335
|
+
from .models import Log, Span, Trace
|
|
321
336
|
|
|
322
337
|
with suppress_db_tracing():
|
|
323
338
|
try:
|
|
324
339
|
trace.save()
|
|
325
340
|
|
|
326
|
-
for
|
|
327
|
-
|
|
341
|
+
for span in spans:
|
|
342
|
+
span.trace = trace
|
|
328
343
|
|
|
329
344
|
# Bulk create spans
|
|
330
|
-
Span.objects.bulk_create(
|
|
345
|
+
Span.objects.bulk_create(spans)
|
|
346
|
+
|
|
347
|
+
# Create log models if we have logs
|
|
348
|
+
if logs:
|
|
349
|
+
# Create a mapping of span_id to span_model
|
|
350
|
+
span_id_to_model = {
|
|
351
|
+
span_model.span_id: span_model for span_model in spans
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
log_models = []
|
|
355
|
+
for log_entry in logs:
|
|
356
|
+
log_model = Log(
|
|
357
|
+
trace=trace,
|
|
358
|
+
timestamp=log_entry["timestamp"],
|
|
359
|
+
level=log_entry["level"],
|
|
360
|
+
message=log_entry["message"],
|
|
361
|
+
span=span_id_to_model.get(log_entry["span_id"]),
|
|
362
|
+
)
|
|
363
|
+
log_models.append(log_model)
|
|
364
|
+
|
|
365
|
+
Log.objects.bulk_create(log_models)
|
|
366
|
+
|
|
331
367
|
except Exception as e:
|
|
332
368
|
logger.warning(
|
|
333
369
|
"Failed to export trace to database: %s",
|
|
@@ -339,8 +375,11 @@ class ObserverSpanProcessor(SpanProcessor):
|
|
|
339
375
|
if settings.OBSERVER_TRACE_LIMIT > 0:
|
|
340
376
|
try:
|
|
341
377
|
if Trace.objects.count() > settings.OBSERVER_TRACE_LIMIT:
|
|
378
|
+
excess_count = (
|
|
379
|
+
Trace.objects.count() - settings.OBSERVER_TRACE_LIMIT
|
|
380
|
+
)
|
|
342
381
|
delete_ids = Trace.objects.order_by("start_time")[
|
|
343
|
-
:
|
|
382
|
+
:excess_count
|
|
344
383
|
].values_list("id", flat=True)
|
|
345
384
|
Trace.objects.filter(id__in=delete_ids).delete()
|
|
346
385
|
except Exception as e:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div style="padding-left: {{ event.span_level * 1 }}rem;" class="border-l border-white/10">
|
|
2
|
+
<div class="ml-px px-2 py-1 text-xs flex items-start space-x-2">
|
|
3
|
+
<div class="w-4 h-4 mr-2 flex items-center justify-center">
|
|
4
|
+
<svg class="w-3 h-3 transform transition-transform" fill="currentColor" viewBox="0 0 20 20">
|
|
5
|
+
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/>
|
|
6
|
+
</svg>
|
|
7
|
+
</div>
|
|
8
|
+
<span class="text-white/40 whitespace-nowrap tabular-nums" title="{{ log.timestamp|localtime }}">{{ log.timestamp|localtime|strftime("%-I:%M:%S %p") }}</span>
|
|
9
|
+
<span data-level="{{ log.level }}" class="font-mono text-white/90 break-words
|
|
10
|
+
data-[level='DEBUG']:text-stone-400
|
|
11
|
+
data-[level='WARNING']:text-yellow-400
|
|
12
|
+
data-[level='ERROR']:text-red-400
|
|
13
|
+
data-[level='CRITICAL']:text-red-300">{{ log.message }}</span>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
{% set span_start_offset = ((span.start_time - trace.start_time).total_seconds() * 1000) %}
|
|
2
|
+
{% set start_percent = (span_start_offset / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
3
|
+
{% set width_percent = (span.duration_ms() / trace.duration_ms() * 100) if trace.duration_ms() > 0 else 0 %}
|
|
4
|
+
|
|
5
|
+
<div style="padding-left: {{ event.span_level * 1 }}rem;" class="border-l border-white/20">
|
|
6
|
+
<details class="rounded bg-white/5 min-w-[600px] ml-px">
|
|
7
|
+
<summary class="cursor-pointer hover:bg-white/10 transition-colors px-2 py-1 list-none [&::-webkit-details-marker]:hidden">
|
|
8
|
+
<div class="flex items-center">
|
|
9
|
+
<div class="w-4 h-4 mr-2 flex items-center justify-center">
|
|
10
|
+
<svg class="w-3 h-3 transform transition-transform details-open:rotate-90" fill="currentColor" viewBox="0 0 20 20">
|
|
11
|
+
<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" />
|
|
12
|
+
</svg>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="w-80 flex items-center space-x-2">
|
|
16
|
+
<div class="text-white/40 whitespace-nowrap tabular-nums" title="{{ span.start_time|localtime }}">
|
|
17
|
+
{{ span.start_time|localtime|strftime("%-I:%M:%S %p") }}
|
|
18
|
+
</div>
|
|
19
|
+
<div class="flex-grow whitespace-nowrap truncate text-white/90">{{ span.name }}</div>
|
|
20
|
+
|
|
21
|
+
{% if span.annotations %}
|
|
22
|
+
<div class="flex items-center space-x-1 flex-shrink-0">
|
|
23
|
+
{% for annotation in span.annotations %}
|
|
24
|
+
<span class="w-4 h-4 inline-flex justify-center items-center text-xs rounded-full
|
|
25
|
+
data-[severity='warning']:bg-amber-500/20
|
|
26
|
+
data-[severity='warning']:text-amber-400
|
|
27
|
+
data-[severity='error']:bg-red-500/20
|
|
28
|
+
data-[severity='error']:text-red-400
|
|
29
|
+
data-[severity='info']:bg-blue-500/20
|
|
30
|
+
data-[severity='info']:text-blue-400"
|
|
31
|
+
data-severity="{{ annotation.severity }}"
|
|
32
|
+
title="{{ annotation.message }}">
|
|
33
|
+
!
|
|
34
|
+
</span>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endif %}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="flex-1 px-4 min-w-[300px]">
|
|
41
|
+
<div class="relative h-6 bg-white/2 rounded-sm">
|
|
42
|
+
<div
|
|
43
|
+
class="absolute top-1 bottom-1 rounded-sm transition-opacity hover:opacity-80
|
|
44
|
+
data-[kind='SERVER']:bg-blue-500
|
|
45
|
+
data-[kind='CLIENT']:bg-emerald-500
|
|
46
|
+
data-[kind='CONSUMER']:bg-amber-500
|
|
47
|
+
data-[kind='PRODUCER']:bg-purple-500
|
|
48
|
+
data-[kind='INTERNAL']:bg-gray-500
|
|
49
|
+
bg-white/30"
|
|
50
|
+
data-kind="{{ span.kind }}"
|
|
51
|
+
style="left: {{ start_percent }}%; width: {{ width_percent }}%;"
|
|
52
|
+
title="{{ span.name }} - {{ span.duration_ms() }}ms">
|
|
53
|
+
</div>
|
|
54
|
+
<div
|
|
55
|
+
class="absolute inset-0 flex items-center justify-start pl-1 text-xs text-white/80 font-medium whitespace-nowrap pointer-events-none"
|
|
56
|
+
style="left: {{ start_percent }}%; width: {{ width_percent }}%;">
|
|
57
|
+
{{ "%.2f"|format(span.duration_ms()) }}ms
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</summary>
|
|
63
|
+
<div class="p-4 pt-2 bg-white/3 border-t border-white/20">
|
|
64
|
+
{% if span.sql_query %}
|
|
65
|
+
<div class="mb-6 bg-white/3 rounded-lg border border-white/20 overflow-hidden
|
|
66
|
+
{% if span.annotations %}ring-2 ring-amber-500/50{% endif %}">
|
|
67
|
+
<div class="bg-white/10 px-4 py-2 border-b border-white/20 flex items-center justify-between">
|
|
68
|
+
<h4 class="text-sm font-semibold text-emerald-400 flex items-center">
|
|
69
|
+
<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">
|
|
70
|
+
<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"/>
|
|
71
|
+
<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"/>
|
|
72
|
+
<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"/>
|
|
73
|
+
<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"/>
|
|
74
|
+
</svg>
|
|
75
|
+
Database Query
|
|
76
|
+
</h4>
|
|
77
|
+
{% if span.annotations %}
|
|
78
|
+
<div class="flex items-center space-x-1">
|
|
79
|
+
{% for annotation in span.annotations %}
|
|
80
|
+
<span class="px-2 py-0.5 text-xs rounded-full
|
|
81
|
+
data-[severity='warning']:bg-amber-500/20
|
|
82
|
+
data-[severity='warning']:text-amber-400
|
|
83
|
+
data-[severity='error']:bg-red-500/20
|
|
84
|
+
data-[severity='error']:text-red-400
|
|
85
|
+
data-[severity='info']:bg-blue-500/20
|
|
86
|
+
data-[severity='info']:text-blue-400"
|
|
87
|
+
data-severity="{{ annotation.severity }}">
|
|
88
|
+
{{ annotation.message }}
|
|
89
|
+
</span>
|
|
90
|
+
{% endfor %}
|
|
91
|
+
</div>
|
|
92
|
+
{% endif %}
|
|
93
|
+
</div>
|
|
94
|
+
<div class="p-4">
|
|
95
|
+
<pre class="text-xs text-white/80 font-mono whitespace-pre-wrap overflow-x-auto"><code>{{ span.get_formatted_sql() }}</code></pre>
|
|
96
|
+
|
|
97
|
+
{% if span.sql_query_params %}
|
|
98
|
+
<div class="mt-4 pt-4 border-t border-white/20">
|
|
99
|
+
<h5 class="text-xs font-semibold text-white/40 mb-2">Query Parameters</h5>
|
|
100
|
+
<div class="space-y-1">
|
|
101
|
+
{% for param_key, param_value in span.sql_query_params.items() %}
|
|
102
|
+
<div class="flex text-xs">
|
|
103
|
+
<span class="text-white/50 min-w-0 flex-shrink-0 pr-2 font-mono">{{ param_key }}:</span>
|
|
104
|
+
<span class="text-white/70 break-words font-mono">{{ param_value }}</span>
|
|
105
|
+
</div>
|
|
106
|
+
{% endfor %}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
{% endif %}
|
|
110
|
+
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
{% endif %}
|
|
114
|
+
|
|
115
|
+
{% if span.source_code_location %}
|
|
116
|
+
<div class="mb-6 bg-white/3 rounded-lg border border-white/20 overflow-hidden">
|
|
117
|
+
<div class="bg-white/10 px-4 py-2 border-b border-white/20 flex items-center justify-between">
|
|
118
|
+
<h4 class="text-sm font-semibold text-blue-400 flex items-center">
|
|
119
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-code-slash w-4 h-4 mr-2" viewBox="0 0 16 16">
|
|
120
|
+
<path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0m6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0"/>
|
|
121
|
+
</svg>
|
|
122
|
+
Source Code Location
|
|
123
|
+
</h4>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="p-4">
|
|
126
|
+
<div class="space-y-2 text-xs">
|
|
127
|
+
{% for key, value in span.source_code_location.items() %}
|
|
128
|
+
{% if key == "Stacktrace" %}
|
|
129
|
+
<div class="mt-3 pt-3 border-t border-white/10">
|
|
130
|
+
<details class="cursor-pointer">
|
|
131
|
+
<summary class="text-white/50 text-xs font-mono mb-2 hover:text-white/70 transition-colors">{{ key }}</summary>
|
|
132
|
+
<div class="bg-white/5 rounded p-3 max-h-64 overflow-y-auto">
|
|
133
|
+
<pre class="text-white/80 font-mono text-xs whitespace-pre-wrap break-words"><code>{{ value }}</code></pre>
|
|
134
|
+
</div>
|
|
135
|
+
</details>
|
|
136
|
+
</div>
|
|
137
|
+
{% else %}
|
|
138
|
+
<div class="flex">
|
|
139
|
+
<span class="text-white/50 min-w-0 flex-shrink-0 pr-2 font-mono w-20">{{ key }}</span>
|
|
140
|
+
<span class="text-white/70 break-words font-mono">{{ value }}</span>
|
|
141
|
+
</div>
|
|
142
|
+
{% endif %}
|
|
143
|
+
{% endfor %}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
{% endif %}
|
|
148
|
+
|
|
149
|
+
{% if span.get_exception_stacktrace() %}
|
|
150
|
+
<div class="mb-6 bg-red-900/20 rounded-lg border border-red-600/30 overflow-hidden">
|
|
151
|
+
<div class="bg-red-900/40 px-4 py-2 border-b border-red-600/30">
|
|
152
|
+
<h4 class="text-sm font-semibold text-red-300 flex items-center">
|
|
153
|
+
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
154
|
+
<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"/>
|
|
155
|
+
</svg>
|
|
156
|
+
Exception Stacktrace
|
|
157
|
+
</h4>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="p-4">
|
|
160
|
+
<pre class="text-xs text-red-100 font-mono whitespace-pre-wrap overflow-x-auto"><code>{{ span.get_exception_stacktrace() }}</code></pre>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
{% endif %}
|
|
164
|
+
|
|
165
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
166
|
+
<div>
|
|
167
|
+
<h4 class="text-sm font-semibold text-white/70 mb-3">Basic Information</h4>
|
|
168
|
+
<div class="space-y-2 text-xs">
|
|
169
|
+
<div class="flex">
|
|
170
|
+
<span class="text-white/40 w-20">ID:</span>
|
|
171
|
+
<span class="text-white/80 font-mono">{{ span.span_id }}</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="flex">
|
|
174
|
+
<span class="text-white/40 w-20">Name:</span>
|
|
175
|
+
<span class="text-white/80">{{ span.name }}</span>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="flex">
|
|
178
|
+
<span class="text-white/40 w-20">Kind:</span>
|
|
179
|
+
<span class="px-2 py-0.5 rounded text-xs font-medium
|
|
180
|
+
data-[kind='SERVER']:bg-blue-500/20 data-[kind='SERVER']:text-blue-300
|
|
181
|
+
data-[kind='CLIENT']:bg-emerald-500/20 data-[kind='CLIENT']:text-emerald-300
|
|
182
|
+
data-[kind='CONSUMER']:bg-amber-500/20 data-[kind='CONSUMER']:text-amber-300
|
|
183
|
+
data-[kind='PRODUCER']:bg-purple-500/20 data-[kind='PRODUCER']:text-purple-300
|
|
184
|
+
data-[kind='INTERNAL']:bg-gray-500/20 data-[kind='INTERNAL']:text-gray-300
|
|
185
|
+
bg-gray-500/20 text-gray-300"
|
|
186
|
+
data-kind="{{ span.kind }}">
|
|
187
|
+
{{ span.kind }}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="flex">
|
|
191
|
+
<span class="text-white/40 w-20">Duration:</span>
|
|
192
|
+
<span class="text-white/80">{{ "%.2f"|format(span.duration_ms() or 0) }}ms</span>
|
|
193
|
+
</div>
|
|
194
|
+
{% if span.parent_id %}
|
|
195
|
+
<div class="flex">
|
|
196
|
+
<span class="text-white/40 w-20">Parent:</span>
|
|
197
|
+
<span class="text-white/80 font-mono text-xs">{{ span.parent_id }}</span>
|
|
198
|
+
</div>
|
|
199
|
+
{% endif %}
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div>
|
|
204
|
+
<h4 class="text-sm font-semibold text-white/70 mb-3">Timing</h4>
|
|
205
|
+
<div class="space-y-2 text-xs">
|
|
206
|
+
<div class="flex">
|
|
207
|
+
<span class="text-white/40 w-20">Started:</span>
|
|
208
|
+
<span class="text-white/80">{{ span.start_time|localtime|strftime("%-I:%M:%S.%f %p") }}</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="flex">
|
|
211
|
+
<span class="text-white/40 w-20">Ended:</span>
|
|
212
|
+
<span class="text-white/80">{{ span.end_time|localtime|strftime("%-I:%M:%S.%f %p") }}</span>
|
|
213
|
+
</div>
|
|
214
|
+
{% if span.status and span.status != '' and span.status != 'UNSET' %}
|
|
215
|
+
<div class="flex">
|
|
216
|
+
<span class="text-white/40 w-20">Status:</span>
|
|
217
|
+
<span class="px-2 py-0.5 rounded text-xs font-medium
|
|
218
|
+
data-[status='ERROR']:bg-red-500/20 data-[status='ERROR']:text-red-300
|
|
219
|
+
data-[status='OK']:bg-green-500/20 data-[status='OK']:text-green-300
|
|
220
|
+
bg-yellow-500/20 text-yellow-300"
|
|
221
|
+
data-status="{{ span.status }}">
|
|
222
|
+
{{ span.status }}
|
|
223
|
+
</span>
|
|
224
|
+
</div>
|
|
225
|
+
{% endif %}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{% if span.attributes %}
|
|
231
|
+
<div class="mt-6">
|
|
232
|
+
<h4 class="text-sm font-semibold text-white/70 mb-3">Attributes</h4>
|
|
233
|
+
<div class="bg-white/3 rounded p-3 max-h-48 overflow-y-auto">
|
|
234
|
+
<div class="space-y-1 text-xs">
|
|
235
|
+
{% for key, value in span.attributes.items() %}
|
|
236
|
+
<div class="flex">
|
|
237
|
+
<span class="text-white/40 min-w-0 flex-shrink-0 pr-2">{{ key }}:</span>
|
|
238
|
+
<span class="text-white/80 break-words">{{ value }}</span>
|
|
239
|
+
</div>
|
|
240
|
+
{% endfor %}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
{% endif %}
|
|
245
|
+
|
|
246
|
+
{% if span.events %}
|
|
247
|
+
<div class="mt-6">
|
|
248
|
+
<h4 class="text-sm font-semibold text-white/70 mb-3">Events ({{ span.events|length }})</h4>
|
|
249
|
+
<div class="bg-white/3 rounded p-3 max-h-48 overflow-y-auto">
|
|
250
|
+
<div class="space-y-3 text-xs">
|
|
251
|
+
{% for event in span.events %}
|
|
252
|
+
<div class="border-l-2 border-white/20 pl-3">
|
|
253
|
+
<div class="flex items-center justify-between mb-1">
|
|
254
|
+
<div class="text-white/80 font-medium">{{ event.name }}</div>
|
|
255
|
+
<div class="text-white/40 text-xs">
|
|
256
|
+
{% set formatted_time = span.format_event_timestamp(event.timestamp) %}
|
|
257
|
+
{% if formatted_time.__class__.__name__ == 'datetime' %}
|
|
258
|
+
{{ formatted_time|localtime|strftime("%-I:%M:%S.%f %p") }}
|
|
259
|
+
{% else %}
|
|
260
|
+
{{ formatted_time }}
|
|
261
|
+
{% endif %}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
{% if event.attributes %}
|
|
265
|
+
<div class="space-y-1">
|
|
266
|
+
{% for key, value in event.attributes.items() %}
|
|
267
|
+
<div class="flex">
|
|
268
|
+
<span class="text-white/40 min-w-0 flex-shrink-0 pr-2">{{ key }}:</span>
|
|
269
|
+
<pre class="text-white/80 whitespace-pre-wrap break-words font-mono text-xs">{{ value }}</pre>
|
|
270
|
+
</div>
|
|
271
|
+
{% endfor %}
|
|
272
|
+
</div>
|
|
273
|
+
{% endif %}
|
|
274
|
+
</div>
|
|
275
|
+
{% endfor %}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{% endif %}
|
|
280
|
+
|
|
281
|
+
{% if span.links %}
|
|
282
|
+
<div class="mt-6">
|
|
283
|
+
<h4 class="text-sm font-semibold text-white/70 mb-3">Links ({{ span.links|length }})</h4>
|
|
284
|
+
<div class="bg-stone-800/50 rounded p-3">
|
|
285
|
+
<div class="space-y-2 text-xs">
|
|
286
|
+
{% for link in span.links %}
|
|
287
|
+
<div class="border-l-2 border-white/20 pl-2">
|
|
288
|
+
<div class="text-white/80 font-mono">{{ link.context.trace_id }}</div>
|
|
289
|
+
<div class="text-white/40 font-mono">{{ link.context.span_id }}</div>
|
|
290
|
+
</div>
|
|
291
|
+
{% endfor %}
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
{% endif %}
|
|
296
|
+
|
|
297
|
+
</div>
|
|
298
|
+
</details>
|
|
299
|
+
</div>
|