ddapm-test-agent 1.36.0__py3-none-any.whl → 1.38.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.
- ddapm_test_agent/agent.py +98 -13
- ddapm_test_agent/remoteconfig.py +2 -2
- ddapm_test_agent/static/style.css +1679 -0
- ddapm_test_agent/templates/base.html +31 -0
- ddapm_test_agent/templates/config.html +440 -0
- ddapm_test_agent/templates/dashboard.html +90 -0
- ddapm_test_agent/templates/macros.html +40 -0
- ddapm_test_agent/templates/requests.html +1925 -0
- ddapm_test_agent/templates/session_detail.html +37 -0
- ddapm_test_agent/templates/sessions.html +23 -0
- ddapm_test_agent/templates/snapshot_detail.html +410 -0
- ddapm_test_agent/templates/snapshots.html +86 -0
- ddapm_test_agent/templates/trace_detail.html +37 -0
- ddapm_test_agent/templates/tracer_flares.html +640 -0
- ddapm_test_agent/templates/traces.html +24 -0
- ddapm_test_agent/vcr_proxy.py +306 -58
- ddapm_test_agent/web.py +1523 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/METADATA +15 -5
- ddapm_test_agent-1.38.0.dist-info/RECORD +40 -0
- ddapm_test_agent-1.36.0.dist-info/RECORD +0 -26
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/WHEEL +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/entry_points.txt +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/licenses/LICENSE.BSD3 +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/licenses/LICENSE.apache2 +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.38.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="session-detail-page">
|
|
5
|
+
<div class="page-header">
|
|
6
|
+
<h2>Session {{ token }}</h2>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
{% if error %}
|
|
10
|
+
<div class="error-message">
|
|
11
|
+
<p>{{ error }}</p>
|
|
12
|
+
</div>
|
|
13
|
+
{% else %}
|
|
14
|
+
<div class="session-details">
|
|
15
|
+
<div class="session-info">
|
|
16
|
+
<h3>Session Information</h3>
|
|
17
|
+
<p><strong>Token:</strong> {{ token }}</p>
|
|
18
|
+
<p><strong>Traces:</strong> {{ traces|length }}</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{% if traces %}
|
|
22
|
+
<div class="session-traces">
|
|
23
|
+
<h3>Traces in this Session</h3>
|
|
24
|
+
{% for trace in traces %}
|
|
25
|
+
<div class="trace-summary">
|
|
26
|
+
<p><strong>Spans:</strong> {{ trace|length }}</p>
|
|
27
|
+
{% if trace %}
|
|
28
|
+
<p><strong>Root Service:</strong> {{ trace[0].get('service', 'N/A') }}</p>
|
|
29
|
+
{% endif %}
|
|
30
|
+
</div>
|
|
31
|
+
{% endfor %}
|
|
32
|
+
</div>
|
|
33
|
+
{% endif %}
|
|
34
|
+
</div>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endblock %}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
{% from "macros.html" import page_layout, empty_state, action_button %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
{% call page_layout("Sessions") %}
|
|
6
|
+
{% if sessions %}
|
|
7
|
+
<div class="sessions-list">
|
|
8
|
+
{% for session in sessions %}
|
|
9
|
+
<div class="session-item">
|
|
10
|
+
<div class="session-header">
|
|
11
|
+
<h3>{{ session }}</h3>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="session-actions">
|
|
14
|
+
{{ action_button("/sessions/" + session, "View Details") }}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
{% endfor %}
|
|
18
|
+
</div>
|
|
19
|
+
{% else %}
|
|
20
|
+
{{ empty_state("No active sessions") }}
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% endcall %}
|
|
23
|
+
{% endblock %}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="snapshot-detail-page">
|
|
5
|
+
<div class="page-header">
|
|
6
|
+
<h2>{{ filename }}</h2>
|
|
7
|
+
<div class="breadcrumb">
|
|
8
|
+
<a href="/snapshots" class="breadcrumb-link">Snapshots</a>
|
|
9
|
+
<span class="breadcrumb-separator">/</span>
|
|
10
|
+
<span class="breadcrumb-current">{{ filename }}</span>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
{% if error %}
|
|
15
|
+
<div class="error-message">
|
|
16
|
+
<p>{{ error }}</p>
|
|
17
|
+
</div>
|
|
18
|
+
{% else %}
|
|
19
|
+
<div class="snapshot-meta">
|
|
20
|
+
<div class="meta-card">
|
|
21
|
+
<h3>File Information</h3>
|
|
22
|
+
<div class="meta-details">
|
|
23
|
+
<div class="meta-item">
|
|
24
|
+
<span class="meta-label">Filename:</span>
|
|
25
|
+
<span class="meta-value">{{ file_info.filename }}</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="meta-item">
|
|
28
|
+
<span class="meta-label">Size:</span>
|
|
29
|
+
<span class="meta-value">{{ "%.1f" | format(file_info.size / 1024) }} KB</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="meta-item">
|
|
32
|
+
<span class="meta-label">Modified:</span>
|
|
33
|
+
<span class="meta-value">{{ file_info.modified | timestamp_format }}</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="meta-item">
|
|
36
|
+
<span class="meta-label">JSON Valid:</span>
|
|
37
|
+
<span class="meta-value {{ 'valid' if is_valid_json else 'invalid' }}">
|
|
38
|
+
{{ 'Yes' if is_valid_json else 'No' }}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
{% if is_valid_json %}
|
|
42
|
+
<div class="meta-item">
|
|
43
|
+
<span class="meta-label">Traces:</span>
|
|
44
|
+
<span class="meta-value">{{ trace_count }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="meta-item">
|
|
47
|
+
<span class="meta-label">Spans:</span>
|
|
48
|
+
<span class="meta-value">{{ span_count }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
{% endif %}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{% if parse_error %}
|
|
56
|
+
<div class="error-message">
|
|
57
|
+
<p><strong>JSON Parse Error:</strong> {{ parse_error }}</p>
|
|
58
|
+
</div>
|
|
59
|
+
{% endif %}
|
|
60
|
+
|
|
61
|
+
<div class="snapshot-content">
|
|
62
|
+
<div class="content-header">
|
|
63
|
+
<h3>Content</h3>
|
|
64
|
+
<div class="content-actions">
|
|
65
|
+
{% if is_valid_json and trace_count > 0 %}
|
|
66
|
+
<button id="view-toggle" class="action-button" onclick="toggleView()">JSON View</button>
|
|
67
|
+
{% endif %}
|
|
68
|
+
<button class="action-button" onclick="copyToClipboard()">Copy</button>
|
|
69
|
+
<button class="action-button" onclick="toggleWrap()">Toggle Wrap</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{% if is_valid_json and trace_count > 0 %}
|
|
74
|
+
<div id="waterfall-view" class="waterfall-container">
|
|
75
|
+
<!-- Waterfall visualization will be generated here -->
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div id="json-view" class="code-container" style="display: none;">
|
|
79
|
+
<pre id="code-content" class="code-block"><code>{{ raw_content }}</code></pre>
|
|
80
|
+
</div>
|
|
81
|
+
{% else %}
|
|
82
|
+
<div id="json-view" class="code-container">
|
|
83
|
+
<pre id="code-content" class="code-block"><code>{{ raw_content }}</code></pre>
|
|
84
|
+
</div>
|
|
85
|
+
{% endif %}
|
|
86
|
+
</div>
|
|
87
|
+
{% endif %}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<script>
|
|
91
|
+
{% if is_valid_json and trace_count > 0 %}
|
|
92
|
+
const traceData = {{ trace_data | safe }};
|
|
93
|
+
let currentView = 'waterfall';
|
|
94
|
+
|
|
95
|
+
// Service color mapping
|
|
96
|
+
const serviceColorMap = new Map();
|
|
97
|
+
let nextColorIndex = 0;
|
|
98
|
+
|
|
99
|
+
function getServiceColor(serviceName) {
|
|
100
|
+
if (!serviceColorMap.has(serviceName)) {
|
|
101
|
+
serviceColorMap.set(serviceName, nextColorIndex % 8);
|
|
102
|
+
nextColorIndex++;
|
|
103
|
+
}
|
|
104
|
+
return serviceColorMap.get(serviceName);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Generate waterfall view on page load
|
|
108
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
109
|
+
generateWaterfallView();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function toggleView() {
|
|
113
|
+
const toggleButton = document.getElementById('view-toggle');
|
|
114
|
+
const waterfallView = document.getElementById('waterfall-view');
|
|
115
|
+
const jsonView = document.getElementById('json-view');
|
|
116
|
+
|
|
117
|
+
if (currentView === 'waterfall') {
|
|
118
|
+
// Switch to JSON view
|
|
119
|
+
waterfallView.style.display = 'none';
|
|
120
|
+
jsonView.style.display = 'block';
|
|
121
|
+
toggleButton.textContent = 'Waterfall View';
|
|
122
|
+
currentView = 'json';
|
|
123
|
+
} else {
|
|
124
|
+
// Switch to waterfall view
|
|
125
|
+
if (!waterfallView.hasChildNodes()) {
|
|
126
|
+
generateWaterfallView();
|
|
127
|
+
}
|
|
128
|
+
waterfallView.style.display = 'block';
|
|
129
|
+
jsonView.style.display = 'none';
|
|
130
|
+
toggleButton.textContent = 'JSON View';
|
|
131
|
+
currentView = 'waterfall';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generateWaterfallView() {
|
|
136
|
+
const container = document.getElementById('waterfall-view');
|
|
137
|
+
|
|
138
|
+
console.log('Generating waterfall view, traceData:', traceData);
|
|
139
|
+
|
|
140
|
+
if (!traceData) {
|
|
141
|
+
container.innerHTML = '<p class="empty-state">No trace data provided</p>';
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle both single trace (object) and multiple traces (array) formats
|
|
146
|
+
let tracesToProcess = [];
|
|
147
|
+
if (Array.isArray(traceData)) {
|
|
148
|
+
if (traceData.length === 0) {
|
|
149
|
+
container.innerHTML = '<p class="empty-state">No trace data available for waterfall view</p>';
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
tracesToProcess = traceData;
|
|
153
|
+
} else if (typeof traceData === 'object') {
|
|
154
|
+
// Single trace or single span
|
|
155
|
+
tracesToProcess = [traceData];
|
|
156
|
+
} else {
|
|
157
|
+
container.innerHTML = '<p class="empty-state">Invalid trace data format</p>';
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let html = '<div class="waterfall-traces">';
|
|
162
|
+
let hasValidTraces = false;
|
|
163
|
+
|
|
164
|
+
tracesToProcess.forEach((trace, traceIndex) => {
|
|
165
|
+
console.log('Processing trace:', trace);
|
|
166
|
+
|
|
167
|
+
let spans = [];
|
|
168
|
+
|
|
169
|
+
// Handle different trace formats
|
|
170
|
+
if (Array.isArray(trace)) {
|
|
171
|
+
if (trace.length === 0) return;
|
|
172
|
+
spans = trace;
|
|
173
|
+
} else if (typeof trace === 'object' && trace.span_id) {
|
|
174
|
+
// Single span
|
|
175
|
+
spans = [trace];
|
|
176
|
+
} else {
|
|
177
|
+
console.log('Skipping trace with unsupported format:', trace);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hasValidTraces = true;
|
|
182
|
+
|
|
183
|
+
// Find the earliest start time and calculate total duration for this trace
|
|
184
|
+
const validSpans = spans.filter(span => span && typeof span === 'object');
|
|
185
|
+
if (validSpans.length === 0) return;
|
|
186
|
+
|
|
187
|
+
let minStart = Math.min(...validSpans.map(span => span.start || 0));
|
|
188
|
+
let maxEnd = Math.max(...validSpans.map(span => (span.start || 0) + (span.duration || 0)));
|
|
189
|
+
let totalDuration = maxEnd - minStart;
|
|
190
|
+
|
|
191
|
+
// Handle case where all spans have 0 duration
|
|
192
|
+
if (totalDuration === 0) {
|
|
193
|
+
totalDuration = Math.max(...validSpans.map(span => span.duration || 0));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const traceId = validSpans[0].trace_id !== undefined ? validSpans[0].trace_id : traceIndex;
|
|
197
|
+
|
|
198
|
+
html += `<div class="waterfall-trace">
|
|
199
|
+
<div class="trace-header" onclick="toggleTrace(${traceIndex})">
|
|
200
|
+
<div class="trace-header-left">
|
|
201
|
+
<span class="trace-toggle" id="trace-toggle-${traceIndex}">▼</span>
|
|
202
|
+
<h4>Trace ${traceId}</h4>
|
|
203
|
+
</div>
|
|
204
|
+
<span class="trace-duration">${formatDuration(totalDuration)}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="spans-timeline" id="trace-spans-${traceIndex}">`;
|
|
207
|
+
|
|
208
|
+
// Sort spans by start time and build hierarchy
|
|
209
|
+
const sortedSpans = [...validSpans].sort((a, b) => (a.start || 0) - (b.start || 0));
|
|
210
|
+
const spanHierarchy = buildSpanHierarchy(sortedSpans);
|
|
211
|
+
|
|
212
|
+
spanHierarchy.forEach(spanInfo => {
|
|
213
|
+
html += renderSpan(spanInfo, minStart, totalDuration, 0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
html += '</div></div>';
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
html += '</div>';
|
|
220
|
+
|
|
221
|
+
if (!hasValidTraces) {
|
|
222
|
+
container.innerHTML = '<p class="empty-state">No valid trace data found for waterfall view</p>';
|
|
223
|
+
} else {
|
|
224
|
+
container.innerHTML = html;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildSpanHierarchy(spans) {
|
|
229
|
+
const spanMap = new Map();
|
|
230
|
+
const rootSpans = [];
|
|
231
|
+
|
|
232
|
+
// Create span objects with children arrays
|
|
233
|
+
spans.forEach(span => {
|
|
234
|
+
spanMap.set(span.span_id, { ...span, children: [] });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Build parent-child relationships
|
|
238
|
+
spans.forEach(span => {
|
|
239
|
+
if (span.parent_id && span.parent_id !== 0 && spanMap.has(span.parent_id)) {
|
|
240
|
+
spanMap.get(span.parent_id).children.push(spanMap.get(span.span_id));
|
|
241
|
+
} else {
|
|
242
|
+
rootSpans.push(spanMap.get(span.span_id));
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return rootSpans;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderSpan(span, traceStart, totalDuration, depth) {
|
|
250
|
+
const start = span.start || 0;
|
|
251
|
+
const duration = span.duration || 0;
|
|
252
|
+
const relativeStart = start - traceStart;
|
|
253
|
+
const startPercent = totalDuration > 0 ? (relativeStart / totalDuration) * 100 : 0;
|
|
254
|
+
const widthPercent = totalDuration > 0 ? (duration / totalDuration) * 100 : 0;
|
|
255
|
+
|
|
256
|
+
const service = span.service || 'unknown';
|
|
257
|
+
const resource = span.resource || span.name || 'unknown';
|
|
258
|
+
const spanClass = span.error ? 'span-error' : 'span-normal';
|
|
259
|
+
const serviceColorClass = `service-color-${getServiceColor(service)}`;
|
|
260
|
+
const indentStyle = `margin-left: ${depth * 20}px;`;
|
|
261
|
+
|
|
262
|
+
const spanId = `span-${span.trace_id || 0}-${span.span_id || Math.random()}`;
|
|
263
|
+
|
|
264
|
+
let html = `
|
|
265
|
+
<div class="waterfall-span ${spanClass}" style="${indentStyle}">
|
|
266
|
+
<div class="span-row">
|
|
267
|
+
<div class="span-info" onclick="toggleSpan('${spanId}')">
|
|
268
|
+
<div class="span-label">
|
|
269
|
+
<span class="span-toggle" id="toggle-${spanId}">▶</span>
|
|
270
|
+
<div class="span-names">
|
|
271
|
+
<span class="service-name">${service}</span>
|
|
272
|
+
<span class="operation-name">${resource}</span>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="span-timing">
|
|
276
|
+
<span class="span-duration">${formatDuration(duration)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<div class="span-bar-container">
|
|
280
|
+
<div class="span-bar ${spanClass} ${serviceColorClass}"
|
|
281
|
+
style="left: ${startPercent}%; width: ${Math.max(widthPercent, 0.1)}%;"
|
|
282
|
+
title="${service}.${span.resource || span.name} - ${formatDuration(duration)}">
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div class="span-details" id="details-${spanId}" style="display: none;">
|
|
287
|
+
${renderSpanMetadata(span)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>`;
|
|
290
|
+
|
|
291
|
+
// Render children recursively
|
|
292
|
+
span.children.forEach(child => {
|
|
293
|
+
html += renderSpan(child, traceStart, totalDuration, depth + 1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return html;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatDuration(nanoseconds) {
|
|
300
|
+
if (nanoseconds < 1000) return `${nanoseconds}ns`;
|
|
301
|
+
if (nanoseconds < 1000000) return `${(nanoseconds / 1000).toFixed(1)}μs`;
|
|
302
|
+
if (nanoseconds < 1000000000) return `${(nanoseconds / 1000000).toFixed(1)}ms`;
|
|
303
|
+
return `${(nanoseconds / 1000000000).toFixed(2)}s`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function toggleTrace(traceIndex) {
|
|
307
|
+
const toggle = document.getElementById(`trace-toggle-${traceIndex}`);
|
|
308
|
+
const spans = document.getElementById(`trace-spans-${traceIndex}`);
|
|
309
|
+
|
|
310
|
+
if (spans.style.display === 'none') {
|
|
311
|
+
spans.style.display = 'block';
|
|
312
|
+
toggle.textContent = '▼';
|
|
313
|
+
} else {
|
|
314
|
+
spans.style.display = 'none';
|
|
315
|
+
toggle.textContent = '▶';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function toggleSpan(spanId) {
|
|
320
|
+
const toggle = document.getElementById(`toggle-${spanId}`);
|
|
321
|
+
const details = document.getElementById(`details-${spanId}`);
|
|
322
|
+
|
|
323
|
+
if (details.style.display === 'none') {
|
|
324
|
+
details.style.display = 'block';
|
|
325
|
+
toggle.textContent = '▼';
|
|
326
|
+
} else {
|
|
327
|
+
details.style.display = 'none';
|
|
328
|
+
toggle.textContent = '▶';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function renderSpanMetadata(span) {
|
|
333
|
+
const fields = [
|
|
334
|
+
{ label: 'Span ID', value: span.span_id },
|
|
335
|
+
{ label: 'Trace ID', value: span.trace_id },
|
|
336
|
+
{ label: 'Parent ID', value: span.parent_id || 'None' },
|
|
337
|
+
{ label: 'Service', value: span.service || 'Not set' },
|
|
338
|
+
{ label: 'Name', value: span.name },
|
|
339
|
+
{ label: 'Resource', value: span.resource },
|
|
340
|
+
{ label: 'Type', value: span.type || 'Not set' },
|
|
341
|
+
{ label: 'Start', value: span.start ? new Date(span.start / 1000000).toISOString() : 'Not set' },
|
|
342
|
+
{ label: 'Duration', value: span.duration ? formatDuration(span.duration) : 'Not set' },
|
|
343
|
+
{ label: 'Error', value: span.error ? 'Yes' : 'No' }
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
let html = '<div class="span-metadata">';
|
|
347
|
+
html += '<h5>Span Details</h5>';
|
|
348
|
+
html += '<div class="metadata-grid">';
|
|
349
|
+
|
|
350
|
+
fields.forEach(field => {
|
|
351
|
+
if (field.value !== undefined && field.value !== null && field.value !== '') {
|
|
352
|
+
html += `
|
|
353
|
+
<div class="metadata-item">
|
|
354
|
+
<span class="metadata-label">${field.label}:</span>
|
|
355
|
+
<span class="metadata-value">${field.value}</span>
|
|
356
|
+
</div>`;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Add meta tags if present
|
|
361
|
+
if (span.meta && Object.keys(span.meta).length > 0) {
|
|
362
|
+
html += '<div class="metadata-section"><h6>Meta</h6>';
|
|
363
|
+
for (const [key, value] of Object.entries(span.meta)) {
|
|
364
|
+
html += `
|
|
365
|
+
<div class="metadata-item">
|
|
366
|
+
<span class="metadata-label">${key}:</span>
|
|
367
|
+
<span class="metadata-value">${value}</span>
|
|
368
|
+
</div>`;
|
|
369
|
+
}
|
|
370
|
+
html += '</div>';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Add metrics if present
|
|
374
|
+
if (span.metrics && Object.keys(span.metrics).length > 0) {
|
|
375
|
+
html += '<div class="metadata-section"><h6>Metrics</h6>';
|
|
376
|
+
for (const [key, value] of Object.entries(span.metrics)) {
|
|
377
|
+
html += `
|
|
378
|
+
<div class="metadata-item">
|
|
379
|
+
<span class="metadata-label">${key}:</span>
|
|
380
|
+
<span class="metadata-value">${value}</span>
|
|
381
|
+
</div>`;
|
|
382
|
+
}
|
|
383
|
+
html += '</div>';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
html += '</div></div>';
|
|
387
|
+
return html;
|
|
388
|
+
}
|
|
389
|
+
{% endif %}
|
|
390
|
+
|
|
391
|
+
function copyToClipboard() {
|
|
392
|
+
const content = document.getElementById('code-content').textContent;
|
|
393
|
+
navigator.clipboard.writeText(content).then(function() {
|
|
394
|
+
alert('Content copied to clipboard!');
|
|
395
|
+
}).catch(function(err) {
|
|
396
|
+
console.error('Failed to copy content: ', err);
|
|
397
|
+
alert('Failed to copy content');
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function toggleWrap() {
|
|
402
|
+
const codeBlock = document.getElementById('code-content');
|
|
403
|
+
if (codeBlock.style.whiteSpace === 'pre-wrap') {
|
|
404
|
+
codeBlock.style.whiteSpace = 'pre';
|
|
405
|
+
} else {
|
|
406
|
+
codeBlock.style.whiteSpace = 'pre-wrap';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
</script>
|
|
410
|
+
{% endblock %}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
{% from "macros.html" import page_layout, empty_state, action_button, error_message %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
{% set extra_header %}
|
|
6
|
+
<div class="snapshot-info">
|
|
7
|
+
<span class="snapshot-dir">Directory: {{ snapshot_dir }}</span>
|
|
8
|
+
</div>
|
|
9
|
+
{% endset %}
|
|
10
|
+
|
|
11
|
+
{% call page_layout("Snapshots", extra_header=extra_header) %}
|
|
12
|
+
{% if error %}
|
|
13
|
+
{{ error_message(error) }}
|
|
14
|
+
{% endif %}
|
|
15
|
+
|
|
16
|
+
{% if snapshots %}
|
|
17
|
+
<div class="snapshots-controls">
|
|
18
|
+
<div class="snapshots-stats">
|
|
19
|
+
<p><strong><span id="total-count">{{ snapshots|length }}</span></strong> snapshot files found (<span id="filtered-count">{{ snapshots|length }}</span> shown)</p>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="filter-controls">
|
|
22
|
+
<input type="text" id="filename-filter" placeholder="Filter by filename..." class="filter-input">
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="snapshots-table">
|
|
26
|
+
<table>
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Filename</th>
|
|
30
|
+
<th>Size</th>
|
|
31
|
+
<th>Modified</th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
{% for snapshot in snapshots %}
|
|
36
|
+
<tr class="snapshot-row" onclick="window.location.href='/snapshots/{{ snapshot.filename }}'" style="cursor: pointer;">
|
|
37
|
+
<td class="filename-cell">{{ snapshot.filename }}</td>
|
|
38
|
+
<td class="size-cell">{{ "%.1f" | format(snapshot.size / 1024) }} KB</td>
|
|
39
|
+
<td class="modified-cell">{{ snapshot.modified | timestamp_format }}</td>
|
|
40
|
+
</tr>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
</div>
|
|
45
|
+
{% else %}
|
|
46
|
+
{% if error %}
|
|
47
|
+
{{ empty_state("Unable to load snapshots") }}
|
|
48
|
+
{% else %}
|
|
49
|
+
{{ empty_state("No snapshot files found in " + snapshot_dir) }}
|
|
50
|
+
{% endif %}
|
|
51
|
+
{% endif %}
|
|
52
|
+
{% endcall %}
|
|
53
|
+
|
|
54
|
+
<script>
|
|
55
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
56
|
+
const filterInput = document.getElementById('filename-filter');
|
|
57
|
+
const filteredCountSpan = document.getElementById('filtered-count');
|
|
58
|
+
const table = document.querySelector('.snapshots-table table tbody');
|
|
59
|
+
|
|
60
|
+
if (filterInput && table) {
|
|
61
|
+
filterInput.addEventListener('input', function() {
|
|
62
|
+
const filterValue = this.value.toLowerCase().trim();
|
|
63
|
+
const rows = table.getElementsByTagName('tr');
|
|
64
|
+
let visibleCount = 0;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < rows.length; i++) {
|
|
67
|
+
const filenameCell = rows[i].querySelector('.filename-cell');
|
|
68
|
+
if (filenameCell) {
|
|
69
|
+
const filename = filenameCell.textContent.toLowerCase();
|
|
70
|
+
if (filename.includes(filterValue)) {
|
|
71
|
+
rows[i].style.display = '';
|
|
72
|
+
visibleCount++;
|
|
73
|
+
} else {
|
|
74
|
+
rows[i].style.display = 'none';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (filteredCountSpan) {
|
|
80
|
+
filteredCountSpan.textContent = visibleCount;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
86
|
+
{% endblock %}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="trace-detail-page">
|
|
5
|
+
<div class="page-header">
|
|
6
|
+
<h2>Trace {{ trace_id }}</h2>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
{% if error %}
|
|
10
|
+
<div class="error-message">
|
|
11
|
+
<p>{{ error }}</p>
|
|
12
|
+
</div>
|
|
13
|
+
{% elif trace %}
|
|
14
|
+
<div class="trace-details">
|
|
15
|
+
<div class="spans-list">
|
|
16
|
+
{% for span in trace %}
|
|
17
|
+
<div class="span-item">
|
|
18
|
+
<div class="span-header">
|
|
19
|
+
<h4>{{ span.get('name', 'Unknown') }}</h4>
|
|
20
|
+
<span class="span-id">ID: {{ span.get('span_id') }}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="span-details">
|
|
23
|
+
<p><strong>Service:</strong> {{ span.get('service', 'N/A') }}</p>
|
|
24
|
+
<p><strong>Resource:</strong> {{ span.get('resource', 'N/A') }}</p>
|
|
25
|
+
<p><strong>Duration:</strong> {{ span.get('duration', 'N/A') }}</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
{% else %}
|
|
32
|
+
<div class="empty-state">
|
|
33
|
+
<p>No trace data available</p>
|
|
34
|
+
</div>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</div>
|
|
37
|
+
{% endblock %}
|