log2trace-ui — Comprehensive Demo Guide
v1.0.0-RC1 A zero-dependency Web Component that transforms distributed-system log records into interactive OpenTelemetry trace waterfalls — entirely client-side. Every configurable aspect of the library is demonstrated below, from the simplest one-liner to a fully wired-up advanced setup.
Installation — two options:
<script type="module" src="./log2trace.min.js"></script>
import 'log2trace-ui'; // registers custom elements
After loading, three custom HTML elements are available globally:
<trace-visualizer>,
<trace-filter>, and
<span-kind-rule>.
1Minimal OTel trace from URL
The simplest possible usage: point data-url at a pre-built
OTLP JSON file.
The component fetches it, parses the OTel structure, and renders the waterfall.
full-width stretches it to fill the container.
<trace-visualizer
data-url="./otel-trace.json"
full-width>
</trace-visualizer>
<script type="module" src="./dist/log2trace.min.js"></script>
2Direct traceData setter
Instead of a URL fetch, assign a TraceData object directly to the
.traceData property. Useful when your data is already in memory or
comes from a non-HTTP source. The OTel structure uses nanosecond timestamps as strings.
const el = document.querySelector('#ex2');
el.traceData = {
resourceSpans: [
{
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'frontend' } }] },
scopeSpans: [{
scope: { name: 'my-tracer' },
spans: [
{
traceId: 'aaaa', spanId: 's1', name: 'HTTP GET /home',
kind: 2, // Server
startTimeUnixNano: '1700000000000000000',
endTimeUnixNano: '1700000000250000000',
status: { code: 1 }
}
]
}]
},
{
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'backend' } }] },
scopeSpans: [{
scope: { name: 'my-tracer' },
spans: [
{
traceId: 'aaaa', spanId: 's2', parentSpanId: 's1', name: 'db.query users',
kind: 3, // Client
startTimeUnixNano: '1700000000050000000',
endTimeUnixNano: '1700000000210000000',
status: { code: 1 }
},
{
traceId: 'aaaa', spanId: 's3', parentSpanId: 's1', name: 'cache.get session',
kind: 1, // Internal
startTimeUnixNano: '1700000000010000000',
endTimeUnixNano: '1700000000040000000',
status: { code: 1 }
}
]
}]
}
]
};
3Log transformation — HTML attributes
Your logs can stay in their native format. Declare field mappings as HTML attributes and the
component applies transformLogs() internally. Dot-path notation
(text.action) traverses nested objects.
trace-id-field, span-name-field,
service-name-field, timestamp-field.
All others are optional enhancements.
<trace-visualizer id="ex3" full-width
trace-id-field="requestId"
span-name-field="action"
service-name-field="service"
timestamp-field="timestamp">
</trace-visualizer>
// Logs are arbitrary objects — no schema required
document.querySelector('#ex3').logData = {
logs: SIMPLE_LOGS, // defined at page level (see data section below)
config: {
traceIdField: 'requestId',
spanNameField: 'action',
serviceNameField:'service',
timestampField: 'timestamp',
}
};
4Log transformation — JS .logData setter
The .logData setter accepts { logs, config } directly —
no HTML attributes needed. The config object mirrors all
TraceVisualizerConfig transform fields.
Use this when building dynamic UIs or when config comes from an API.
This example also demonstrates spanGroupFields — grouping multiple log
entries into a single span when they share the same service + action combination.
document.querySelector('#ex4').logData = {
logs: SIMPLE_LOGS,
config: {
traceIdField: 'requestId',
spanGroupFields: ['service', 'action'], // group logs by service + action
spanNameField: 'action',
serviceNameField: 'service',
timestampField: 'timestamp',
endTimeField: 'endTimestamp', // optional explicit end time
statusCodeField: 'statusCode', // non-zero → error status
}
};
5Display options
Control visual appearance: row height, padding, background color, legend visibility,
and the detail panel width. All can be set as HTML attributes or via the .config
setter.
<trace-visualizer
full-width
show-legend
span-height="40"
span-padding="8"
background-color="#f8f9ff"
detail-panel-width="35%">
</trace-visualizer>
6Custom color scheme
Map each SpanKind numeric value to a CSS hex color via the
color-scheme attribute (JSON string) or the colorScheme
config property. Keys correspond to: 0 Unspecified, 1 Internal,
2 Server, 3 Client, 4 Producer, 5 Consumer.
<trace-visualizer
full-width show-legend
color-scheme='{
"0": "#b0bec5",
"1": "#7e57c2",
"2": "#26a69a",
"3": "#ef5350",
"4": "#ff7043",
"5": "#ab47bc"
}'>
</trace-visualizer>
7Explicit width & height sizing
By default the component fits its content (width=0, height=0).
Set explicit pixel dimensions to constrain it, or use full-width to stretch
horizontally. When height is set, the timeline area becomes scrollable.
<trace-visualizer width="700" height="250"></trace-visualizer>
<trace-visualizer full-width></trace-visualizer>
8Text filter (local)
Add a <trace-filter type="text" source="local"> child element to render
a text input above the timeline. source="local" means filtering is applied
client-side — no server round-trip. Try typing a service or action name.
<trace-visualizer full-width>
<trace-filter
field="spanName"
label="Operation"
type="text"
source="local"
placeholder="Filter by operation name..."
width="280"
debounce="300">
</trace-filter>
</trace-visualizer>
9Dropdown filter — auto & static options
options-source="auto" derives dropdown choices from the loaded data automatically.
Alternatively, provide a static JSON array via the options attribute — either
plain strings or {"value","label"} objects.
<trace-visualizer full-width>
<!-- Auto-populated from data -->
<trace-filter
field="serviceName"
label="Service"
type="dropdown"
source="local"
options-source="auto"
placeholder="All services">
</trace-filter>
<!-- Static options with value/label pairs -->
<trace-filter
field="spanKind"
label="Span Kind"
type="dropdown"
source="local"
options='[
{"value":"1","label":"Internal"},
{"value":"2","label":"Server"},
{"value":"3","label":"Client"}
]'
placeholder="All kinds">
</trace-filter>
</trace-visualizer>
10Checkbox filter — errors only
The special field "hasError" filters to spans whose status code is Error (2).
Use it with type="checkbox" to give users a one-click "show errors only" toggle.
<trace-visualizer full-width>
<trace-filter
field="hasError"
label="Errors Only"
type="checkbox"
source="local">
</trace-filter>
</trace-visualizer>
11Datetime-range filter
Renders two datetime inputs (From / To). The auto-range attribute
pre-fills min/max from the loaded data. The filter value is an object
{ from?: string, to?: string } with ISO 8601 strings.
<trace-visualizer full-width>
<trace-filter
field="startTime"
label="Time Range"
type="datetime-range"
source="local"
auto-range>
</trace-filter>
</trace-visualizer>
12Multiselect filter
Like dropdown but allows selecting multiple values simultaneously.
The filter value is a string[]. Use options-source="auto"
or provide a static options array.
<trace-visualizer full-width>
<trace-filter
field="serviceName"
label="Services"
type="multiselect"
source="local"
options-source="auto"
width="220">
</trace-filter>
</trace-visualizer>
13Wildcard search field="*"
Setting field="*" searches across all span fields: name, service,
span ID, all attributes, all event names and event attributes. Great as a global search box.
<trace-visualizer full-width>
<trace-filter
field="*"
label="Search"
type="text"
source="local"
placeholder="Search all fields..."
width="320">
</trace-filter>
</trace-visualizer>
14All filter types combined
Multiple <trace-filter> elements render a filter bar.
Local filters use AND logic — a span must satisfy every active filter simultaneously.
The target attribute controls whether the filter matches span-level fields
("span", default) or individual log/event attributes ("log").
<trace-visualizer full-width show-legend>
<!-- Wildcard global search -->
<trace-filter field="*" label="Search" type="text" source="local"
placeholder="Search all..." width="220"></trace-filter>
<!-- Span name substring -->
<trace-filter field="spanName" label="Operation" type="text" source="local"
placeholder="e.g. query" width="200"></trace-filter>
<!-- Service dropdown (auto) -->
<trace-filter field="serviceName" label="Service" type="dropdown" source="local"
options-source="auto" placeholder="All"></trace-filter>
<!-- Errors only -->
<trace-filter field="hasError" label="Errors Only" type="checkbox"
source="local"></trace-filter>
<!-- Datetime range -->
<trace-filter field="startTime" label="Time Range" type="datetime-range"
source="local" auto-range></trace-filter>
<!-- Multi-service select -->
<trace-filter field="serviceName" label="Services" type="multiselect" source="local"
options-source="auto" width="180"></trace-filter>
</trace-visualizer>
15External filter & custom fetchCallback
source="external" filters are not applied client-side — their values are
forwarded to the server via query parameters (or to your fetchCallback).
required gates the initial fetch until the field is filled.
auto-fetch="false" shows a Search button instead of auto-triggering on change.
The fetchCallback receives the URL and all active external filter values.
This example mocks the server with a 600 ms delay so you can see the loading state.
<trace-visualizer id="ex15" full-width auto-fetch="false">
<trace-filter
field="requestId"
label="Request ID"
type="dropdown"
source="external"
required
options='["req-001","req-002"]'
placeholder="Select a trace...">
</trace-filter>
<trace-filter
field="service"
label="Service"
type="multiselect"
source="external"
options='["api","auth","db"]'
width="160">
</trace-filter>
</trace-visualizer>
document.querySelector('#ex15').fetchCallback = async (url, filters) => {
console.log('Fetching with filters:', filters);
// Simulate a 600 ms server round-trip
await new Promise(r => setTimeout(r, 600));
// Return data filtered by the selected requestId
const id = filters.requestId;
const logs = SIMPLE_LOGS.filter(l => !id || l.requestId === id);
return logs; // component will call transformLogs() if transform config is set
};
16Span-kind classification rules
<span-kind-rule> children classify spans based on log field values.
Rules are checked in order; the first match wins. Use match-field +
match-value for a single-field check, or the match JSON
attribute for a multi-field AND check. Unmatched spans fall back to
default-span-kind.
<trace-visualizer full-width show-legend default-span-kind="Internal">
<!-- Single-field shorthand -->
<span-kind-rule kind="Server" match-field="service" match-value="api"></span-kind-rule>
<span-kind-rule kind="Consumer" match-field="service" match-value="auth"></span-kind-rule>
<!-- Multi-field JSON match (all conditions must match) -->
<span-kind-rule kind="Client"
match='{"service":"db","action":"db.query"}'>
</span-kind-rule>
</trace-visualizer>
17span-selected event
Fired when the user clicks a span bar. event.detail.span is the full
Span OTel object. Use this to drive custom side panels, analytics, or
cross-component navigation.
document.querySelector('#ex17').addEventListener('span-selected', (e) => {
const { span } = e.detail;
document.querySelector('#ex17-output').textContent =
JSON.stringify({
name: span.name,
kind: span.kind,
spanId: span.spanId,
parentId: span.parentSpanId,
status: span.status,
startNs: span.startTimeUnixNano,
endNs: span.endTimeUnixNano,
attributes: span.attributes,
}, null, 2);
});
// click a span above to see its data here
18filter-changed event
Fired whenever any filter value changes. The event detail contains
{ field, source, value, allFilters }. Use this for analytics, URL state
sync, or custom logic that runs alongside the built-in filtering.
document.querySelector('#ex18').addEventListener('filter-changed', (e) => {
const log = document.querySelector('#ex18-output');
const entry = `[${new Date().toLocaleTimeString()}] field="${e.detail.field}" `
+ `source="${e.detail.source}" value=${JSON.stringify(e.detail.value)}`;
log.textContent = entry + '\n' + log.textContent;
});
// change a filter above to see events here
19Keyboard navigation
Click anywhere on the visualizer to focus it, then use the keyboard to navigate spans and control the timeline. No configuration required — this is built in.
| Key | Action |
|---|---|
| ↑ / ↓ | Select previous / next span |
| ← / → | Pan timeline left / right |
| + / = | Zoom in |
| - / _ | Zoom out |
| 0 / Home | Reset zoom to 100 % |
| Enter / Space | Open detail panel for selected span |
| Escape | Close detail panel |
20loadTraceData() & loadAndTransform() methods
These async methods let you trigger a fetch imperatively — for example, from a button click
or a WebSocket message. Both return a Promise<void> that resolves when
rendering is complete.
const el = document.querySelector('#ex20');
// Load a pre-built OTel trace from a URL
async function loadOtel() {
await el.loadTraceData('./otel-trace.json');
}
// Fetch raw logs and transform them
async function loadLogs() {
await el.loadAndTransform('./logs.json', {
traceIdField: 'text.BTMID',
spanNameField: 'text.Action',
serviceNameField: 'text.Application',
timestampField: 'text.CreateDate',
});
}
21Empty & error states
The component renders a built-in placeholder when there is no data, and an error message
when a fetch fails. An empty TraceData object produces the "no spans" state.
<!-- No data-url and no programmatic data → empty state -->
<trace-visualizer full-width></trace-visualizer>
<trace-visualizer data-url="./does-not-exist.json" full-width></trace-visualizer>
document.querySelector('#ex21c').traceData = { resourceSpans: [] };
22transformLogs() standalone utility
Import transformLogs directly to convert log arrays to OTel
TraceData without rendering anything. Useful for pre-processing, serialization,
or feeding data to other OTel-compatible tools.
import { transformLogs } from './dist/log2trace.min.js';
const traceData = transformLogs(SIMPLE_LOGS, {
traceIdField: 'requestId',
spanGroupFields: ['service', 'action'],
spanNameField: 'action',
serviceNameField: 'service',
timestampField: 'timestamp',
statusCodeField: 'statusCode',
});
// traceData is a standard OTLP TraceData object
console.log(JSON.stringify(traceData, null, 2));
TraceData JSON// click the button above to run the transformation
23Full advanced setup
Everything combined: log transformation with parentSpanLookupFields,
span-kind classification rules, all filter types, a custom color scheme,
a span-selected event listener, and a mock fetchCallback.
This is the pattern for a production integration.
<trace-visualizer id="ex23" full-width show-legend
auto-fetch="false"
trace-id-field="requestId"
span-group-fields="service,action"
span-name-field="action"
service-name-field="service"
timestamp-field="timestamp"
end-time-field="endTimestamp"
status-code-field="statusCode"
parent-span-lookup-fields="parentService"
default-span-kind="Internal"
span-height="32"
span-padding="6"
detail-panel-width="38%"
color-scheme='{"0":"#90a4ae","1":"#7e57c2","2":"#26a69a","3":"#ef5350","4":"#ff7043","5":"#ab47bc"}'>
<!-- Span-kind rules -->
<span-kind-rule kind="Server" match-field="service" match-value="api"></span-kind-rule>
<span-kind-rule kind="Consumer" match-field="service" match-value="auth"></span-kind-rule>
<span-kind-rule kind="Client" match='{"service":"db","action":"db.query"}'></span-kind-rule>
<!-- Filters -->
<trace-filter field="requestId" label="Trace" type="dropdown"
source="external" required options='["req-001","req-002"]'
placeholder="Select trace..."></trace-filter>
<trace-filter field="*" label="Search" type="text" source="local"
placeholder="Search all fields..." width="220"></trace-filter>
<trace-filter field="serviceName" label="Services" type="multiselect"
source="local" options-source="auto" width="180"></trace-filter>
<trace-filter field="hasError" label="Errors Only"
type="checkbox" source="local"></trace-filter>
<trace-filter field="startTime" label="Time Range"
type="datetime-range" source="local" auto-range></trace-filter>
</trace-visualizer>
const el23 = document.querySelector('#ex23');
// Mock server: filter by requestId, simulate 500 ms latency
el23.fetchCallback = async (url, filters) => {
await new Promise(r => setTimeout(r, 500));
const id = filters.requestId;
return id ? SIMPLE_LOGS.filter(l => l.requestId === id) : SIMPLE_LOGS;
};
// React to span selection
el23.addEventListener('span-selected', (e) => {
document.querySelector('#ex23-output').textContent =
`Selected: "${e.detail.span.name}" (${e.detail.span.spanId})`;
});
// click a span above to see its name here