@datalayer/core 1.2.17 → 1.2.18
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.
- package/lib/otel/index.d.ts +4 -0
- package/lib/otel/index.js +3 -0
- package/lib/otel/live/filter.d.ts +15 -0
- package/lib/otel/live/filter.js +16 -0
- package/lib/otel/live/index.d.ts +10 -0
- package/lib/otel/live/index.js +14 -0
- package/lib/otel/live/tracer.d.ts +94 -0
- package/lib/otel/live/tracer.js +223 -0
- package/lib/otel/live/useOtelLiveSpans.d.ts +6 -0
- package/lib/otel/live/useOtelLiveSpans.js +22 -0
- package/lib/otel/types.d.ts +17 -0
- package/lib/otel/views/OtelLiveSpans.d.ts +52 -0
- package/lib/otel/views/OtelLiveSpans.js +139 -0
- package/lib/otel/views/OtelSpanDetail.d.ts +6 -0
- package/lib/otel/views/OtelSpanDetail.js +22 -2
- package/lib/otel/views/OtelTracesList.js +30 -14
- package/lib/otel/views/index.d.ts +3 -0
- package/lib/otel/views/index.js +2 -0
- package/package.json +1 -1
package/lib/otel/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type { OtelSpan, OtelLog, OtelMetric, OtelSpanEvent, OtelSpanLink, SignalType, OtelLiveProps, OtelTracesListProps, OtelSpanDetailProps, OtelTimelineProps, OtelLogsListProps, OtelMetricsListProps, OtelSpanTreeProps, OtelSearchBarProps, OtelTimelineRangeSliderProps, OtelQueryRow, OtelQueryResult, } from './types';
|
|
15
15
|
export { toMs, formatDuration, formatTime, serviceColor, kindColor, severityColor, severityVariant, buildSpanTree, flattenSpanTree, } from './utils';
|
|
16
|
+
export { createOtelLiveTracer, traceparentOf, PEER_SERVICE, spanConcerns, spansOfService, useOtelLiveSpans, useOtelLiveTracer, } from './live';
|
|
17
|
+
export type { OtelAttributeValue, OtelAttributes, OtelLiveExport, OtelLiveSpan, OtelLiveTracer, OtelLiveTracerOptions, OtelSpanContext, OtelSpanKindName, OtelStartSpanOptions, } from './live';
|
|
16
18
|
export { resolveOtelAuth, resolveOtelUserUid, buildOtelWebSocketUrl, } from './auth';
|
|
17
19
|
export type { ResolvedOtelAuth } from './auth';
|
|
18
20
|
export { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelStats, useOtelQuery, useOtelSystem, useOtelWebSocket, setOtelOnUnauthorized, } from './hooks';
|
|
@@ -29,6 +31,8 @@ export { OtelMetricsList } from './views';
|
|
|
29
31
|
export { OtelMetricsChart } from './views';
|
|
30
32
|
export type { OtelMetricsChartProps } from './views';
|
|
31
33
|
export { OtelTimeline } from './views';
|
|
34
|
+
export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, unfoldedValue, } from './views';
|
|
35
|
+
export type { OtelLiveSpansProps, OtelSpanFacet } from './views';
|
|
32
36
|
export { OtelTimelineRangeSlider } from './views';
|
|
33
37
|
export { OtelSqlView } from './views';
|
|
34
38
|
export type { OtelSqlViewProps } from './views';
|
package/lib/otel/index.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// ── Utils ─────────────────────────────────────────────────────────────────────
|
|
6
6
|
export { toMs, formatDuration, formatTime, serviceColor, kindColor, severityColor, severityVariant, buildSpanTree, flattenSpanTree, } from './utils.js';
|
|
7
|
+
// ── In the page ───────────────────────────────────────────────────────────────
|
|
8
|
+
export { createOtelLiveTracer, traceparentOf, PEER_SERVICE, spanConcerns, spansOfService, useOtelLiveSpans, useOtelLiveTracer, } from './live/index.js';
|
|
7
9
|
// ── Auth helpers ──────────────────────────────────────────────────────────────
|
|
8
10
|
export { resolveOtelAuth, resolveOtelUserUid, buildOtelWebSocketUrl, } from './auth.js';
|
|
9
11
|
// ── Hooks ─────────────────────────────────────────────────────────────────────
|
|
@@ -20,6 +22,7 @@ export { OtelSearchBar } from './views/index.js';
|
|
|
20
22
|
export { OtelMetricsList } from './views/index.js';
|
|
21
23
|
export { OtelMetricsChart } from './views/index.js';
|
|
22
24
|
export { OtelTimeline } from './views/index.js';
|
|
25
|
+
export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, unfoldedValue, } from './views/index.js';
|
|
23
26
|
export { OtelTimelineRangeSlider } from './views/index.js';
|
|
24
27
|
export { OtelSqlView } from './views/index.js';
|
|
25
28
|
export { OtelSystemView } from './views/index.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which spans concern a service: those it did, and those done toward it — a
|
|
3
|
+
* client span whose `peer.service` names it (the OpenTelemetry attribute for
|
|
4
|
+
* the remote service a call goes to). One message between two services shows
|
|
5
|
+
* in both: sent in one, received in the other.
|
|
6
|
+
*
|
|
7
|
+
* @module otel/live/filter
|
|
8
|
+
*/
|
|
9
|
+
import type { OtelSpan } from '../types';
|
|
10
|
+
/** The OpenTelemetry attribute naming the service a call goes to. */
|
|
11
|
+
export declare const PEER_SERVICE = "peer.service";
|
|
12
|
+
/** Whether a span concerns a service: done by it, or toward it. */
|
|
13
|
+
export declare function spanConcerns(span: OtelSpan, service: string): boolean;
|
|
14
|
+
/** The spans that concern a service; every span without one. */
|
|
15
|
+
export declare function spansOfService(spans: readonly OtelSpan[], service: string | undefined): OtelSpan[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The OpenTelemetry attribute naming the service a call goes to. */
|
|
6
|
+
export const PEER_SERVICE = 'peer.service';
|
|
7
|
+
/** Whether a span concerns a service: done by it, or toward it. */
|
|
8
|
+
export function spanConcerns(span, service) {
|
|
9
|
+
return (span.service_name === service || span.attributes?.[PEER_SERVICE] === service);
|
|
10
|
+
}
|
|
11
|
+
/** The spans that concern a service; every span without one. */
|
|
12
|
+
export function spansOfService(spans, service) {
|
|
13
|
+
return service
|
|
14
|
+
? spans.filter(span => spanConcerns(span, service))
|
|
15
|
+
: [...spans];
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry in the page: a tracer whose spans the OTEL views draw as
|
|
3
|
+
* they happen, and which of them concern a service. Light: no view, no
|
|
4
|
+
* client, nothing sent.
|
|
5
|
+
*
|
|
6
|
+
* @module otel/live
|
|
7
|
+
*/
|
|
8
|
+
export { createOtelLiveTracer, traceparentOf, type OtelAttributeValue, type OtelAttributes, type OtelLiveExport, type OtelLiveSpan, type OtelLiveTracer, type OtelLiveTracerOptions, type OtelSpanContext, type OtelSpanKindName, type OtelStartSpanOptions, } from './tracer';
|
|
9
|
+
export { PEER_SERVICE, spanConcerns, spansOfService } from './filter';
|
|
10
|
+
export { useOtelLiveSpans, useOtelLiveTracer } from './useOtelLiveSpans';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* OpenTelemetry in the page: a tracer whose spans the OTEL views draw as
|
|
7
|
+
* they happen, and which of them concern a service. Light: no view, no
|
|
8
|
+
* client, nothing sent.
|
|
9
|
+
*
|
|
10
|
+
* @module otel/live
|
|
11
|
+
*/
|
|
12
|
+
export { createOtelLiveTracer, traceparentOf, } from './tracer.js';
|
|
13
|
+
export { PEER_SERVICE, spanConcerns, spansOfService } from './filter.js';
|
|
14
|
+
export { useOtelLiveSpans, useOtelLiveTracer } from './useOtelLiveSpans.js';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tracer that lives in the page: spans started and ended in the browser,
|
|
3
|
+
* kept as the OTEL views draw them (`OtelSpan`), as they happen — a span is
|
|
4
|
+
* there from its start (`in_progress`), its events added as they come, its
|
|
5
|
+
* end and status when it ends.
|
|
6
|
+
*
|
|
7
|
+
* What it records is OpenTelemetry: W3C trace and span ids, a span kind, a
|
|
8
|
+
* status, attributes of primitive values (structured values as JSON text, as
|
|
9
|
+
* the GenAI semantic conventions carry arguments and results), events, and an
|
|
10
|
+
* `exception` event for an error. Nothing is sent anywhere: the record is the
|
|
11
|
+
* page's, read by `useOtelLiveSpans` and drawn by `OtelLiveSpans`.
|
|
12
|
+
*
|
|
13
|
+
* A span started without a parent continues the innermost span still open
|
|
14
|
+
* of the same service — what one agent of the page is doing — so a tool call
|
|
15
|
+
* nests under its turn without the caller carrying a context; `parent: null`
|
|
16
|
+
* starts a trace of its own.
|
|
17
|
+
*
|
|
18
|
+
* @module otel/live/tracer
|
|
19
|
+
*/
|
|
20
|
+
import type { OtelSpan, OtelSpanLink } from '../types';
|
|
21
|
+
/** What identifies a span: its trace and itself. */
|
|
22
|
+
export interface OtelSpanContext {
|
|
23
|
+
trace_id: string;
|
|
24
|
+
span_id: string;
|
|
25
|
+
}
|
|
26
|
+
/** An attribute's value, as OpenTelemetry allows it. */
|
|
27
|
+
export type OtelAttributeValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
|
|
28
|
+
/** Attributes; an `undefined` value is left out. */
|
|
29
|
+
export type OtelAttributes = Record<string, OtelAttributeValue | undefined>;
|
|
30
|
+
/** A span's kind, as the OTEL views name it. */
|
|
31
|
+
export type OtelSpanKindName = 'INTERNAL' | 'SERVER' | 'CLIENT' | 'PRODUCER' | 'CONSUMER';
|
|
32
|
+
export interface OtelStartSpanOptions {
|
|
33
|
+
/** The service that does it: the tracer's own unless said. */
|
|
34
|
+
serviceName?: string;
|
|
35
|
+
kind?: OtelSpanKindName;
|
|
36
|
+
attributes?: OtelAttributes;
|
|
37
|
+
/**
|
|
38
|
+
* The span it is part of. Left out: the innermost span still open of the
|
|
39
|
+
* same service; `null`: a trace of its own.
|
|
40
|
+
*/
|
|
41
|
+
parent?: OtelSpanContext | null;
|
|
42
|
+
/** When it started, in ms since the epoch: now unless said. */
|
|
43
|
+
startTime?: number;
|
|
44
|
+
/** The instrumentation scope: the tracer's own unless said. */
|
|
45
|
+
scopeName?: string;
|
|
46
|
+
links?: OtelSpanLink[];
|
|
47
|
+
}
|
|
48
|
+
/** A span being recorded. */
|
|
49
|
+
export interface OtelLiveSpan {
|
|
50
|
+
readonly context: OtelSpanContext;
|
|
51
|
+
/** Whether it has ended. */
|
|
52
|
+
readonly ended: boolean;
|
|
53
|
+
setAttribute(key: string, value: OtelAttributeValue | undefined): void;
|
|
54
|
+
setAttributes(attributes: OtelAttributes): void;
|
|
55
|
+
addEvent(name: string, attributes?: OtelAttributes, time?: number): void;
|
|
56
|
+
setStatus(code: 'OK' | 'ERROR', message?: string): void;
|
|
57
|
+
/** An `exception` event, and the status `ERROR`. */
|
|
58
|
+
recordException(error: unknown, time?: number): void;
|
|
59
|
+
updateName(name: string): void;
|
|
60
|
+
/** End it; a second end is ignored. */
|
|
61
|
+
end(time?: number): void;
|
|
62
|
+
}
|
|
63
|
+
/** The record, saved. */
|
|
64
|
+
export interface OtelLiveExport {
|
|
65
|
+
exportedAt: string;
|
|
66
|
+
spans: OtelSpan[];
|
|
67
|
+
}
|
|
68
|
+
export interface OtelLiveTracer {
|
|
69
|
+
/** The service a span is done by unless it says. */
|
|
70
|
+
readonly serviceName: string;
|
|
71
|
+
startSpan(name: string, options?: OtelStartSpanOptions): OtelLiveSpan;
|
|
72
|
+
/** The innermost span still open of a service (the tracer's own unless said). */
|
|
73
|
+
activeSpan(serviceName?: string): OtelSpanContext | undefined;
|
|
74
|
+
/** The spans, oldest first: a new array each time one changes. */
|
|
75
|
+
spans(): readonly OtelSpan[];
|
|
76
|
+
subscribe(listener: () => void): () => void;
|
|
77
|
+
/** Forget every span; those still open are recorded no more. */
|
|
78
|
+
clear(): void;
|
|
79
|
+
exportSpans(): OtelLiveExport;
|
|
80
|
+
}
|
|
81
|
+
export interface OtelLiveTracerOptions {
|
|
82
|
+
serviceName?: string;
|
|
83
|
+
scopeName?: string;
|
|
84
|
+
/** The clock: `Date.now` unless a test sets it. */
|
|
85
|
+
now?: () => number;
|
|
86
|
+
/** The most spans kept: the oldest ended ones go first. Default 2000. */
|
|
87
|
+
limit?: number;
|
|
88
|
+
/** Random bytes as hex, for ids: `crypto.getRandomValues` unless a test sets it. */
|
|
89
|
+
randomHex?: (bytes: number) => string;
|
|
90
|
+
}
|
|
91
|
+
/** A W3C `traceparent` header for a span: `00-<trace>-<span>-01`. */
|
|
92
|
+
export declare function traceparentOf(context: OtelSpanContext): string;
|
|
93
|
+
/** Make a tracer for the page. */
|
|
94
|
+
export declare function createOtelLiveTracer(options?: OtelLiveTracerOptions): OtelLiveTracer;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** Random bytes as lowercase hex. */
|
|
6
|
+
function cryptoHex(bytes) {
|
|
7
|
+
const values = new Uint8Array(bytes);
|
|
8
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
9
|
+
globalThis.crypto.getRandomValues(values);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
for (let index = 0; index < bytes; index += 1) {
|
|
13
|
+
values[index] = Math.floor(Math.random() * 256);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return Array.from(values, value => value.toString(16).padStart(2, '0')).join('');
|
|
17
|
+
}
|
|
18
|
+
/** A W3C `traceparent` header for a span: `00-<trace>-<span>-01`. */
|
|
19
|
+
export function traceparentOf(context) {
|
|
20
|
+
return `00-${context.trace_id}-${context.span_id}-01`;
|
|
21
|
+
}
|
|
22
|
+
/** Attributes without their `undefined` values. */
|
|
23
|
+
function defined(attributes) {
|
|
24
|
+
const kept = {};
|
|
25
|
+
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
26
|
+
if (value !== undefined) {
|
|
27
|
+
kept[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
/** Make a tracer for the page. */
|
|
33
|
+
export function createOtelLiveTracer(options = {}) {
|
|
34
|
+
const serviceName = options.serviceName ?? 'browser';
|
|
35
|
+
const scopeName = options.scopeName ?? '@datalayer/core/otel/live';
|
|
36
|
+
const now = options.now ?? Date.now;
|
|
37
|
+
const limit = options.limit ?? 2000;
|
|
38
|
+
const randomHex = options.randomHex ?? cryptoHex;
|
|
39
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
40
|
+
let list = [];
|
|
41
|
+
/** The spans still open, by service, innermost last. */
|
|
42
|
+
const open = new Map();
|
|
43
|
+
const listeners = new Set();
|
|
44
|
+
let scheduled = false;
|
|
45
|
+
/** Bumped by `clear`: a span started before it is recorded no more. */
|
|
46
|
+
let generation = 0;
|
|
47
|
+
const notify = () => {
|
|
48
|
+
if (scheduled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
scheduled = true;
|
|
52
|
+
queueMicrotask(() => {
|
|
53
|
+
scheduled = false;
|
|
54
|
+
listeners.forEach(listener => listener());
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const trim = () => {
|
|
58
|
+
let excess = list.length - limit;
|
|
59
|
+
if (excess <= 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
list = list.filter(span => {
|
|
63
|
+
if (excess > 0 && !span.in_progress) {
|
|
64
|
+
excess -= 1;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const closeOpen = (service, spanId) => {
|
|
71
|
+
const stack = open.get(service);
|
|
72
|
+
if (!stack) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const kept = stack.filter(id => id !== spanId);
|
|
76
|
+
if (kept.length) {
|
|
77
|
+
open.set(service, kept);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
open.delete(service);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const tracer = {
|
|
84
|
+
serviceName,
|
|
85
|
+
startSpan(name, start = {}) {
|
|
86
|
+
const service = start.serviceName ?? serviceName;
|
|
87
|
+
const parent = start.parent === undefined
|
|
88
|
+
? tracer.activeSpan(service)
|
|
89
|
+
: (start.parent ?? undefined);
|
|
90
|
+
const context = {
|
|
91
|
+
trace_id: parent?.trace_id ?? randomHex(16),
|
|
92
|
+
span_id: randomHex(8),
|
|
93
|
+
};
|
|
94
|
+
const startedAt = start.startTime ?? now();
|
|
95
|
+
const born = generation;
|
|
96
|
+
let current = {
|
|
97
|
+
trace_id: context.trace_id,
|
|
98
|
+
span_id: context.span_id,
|
|
99
|
+
...(parent ? { parent_span_id: parent.span_id } : {}),
|
|
100
|
+
span_name: name,
|
|
101
|
+
service_name: service,
|
|
102
|
+
kind: start.kind ?? 'INTERNAL',
|
|
103
|
+
start_time: iso(startedAt),
|
|
104
|
+
end_time: iso(startedAt),
|
|
105
|
+
duration_ms: 0,
|
|
106
|
+
status_code: 'UNSET',
|
|
107
|
+
otel_scope_name: start.scopeName ?? scopeName,
|
|
108
|
+
attributes: defined(start.attributes),
|
|
109
|
+
events: [],
|
|
110
|
+
...(start.links?.length ? { links: start.links } : {}),
|
|
111
|
+
in_progress: true,
|
|
112
|
+
};
|
|
113
|
+
list = [...list, current];
|
|
114
|
+
trim();
|
|
115
|
+
open.set(service, [...(open.get(service) ?? []), context.span_id]);
|
|
116
|
+
notify();
|
|
117
|
+
let ended = false;
|
|
118
|
+
const change = (next) => {
|
|
119
|
+
if (born !== generation) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const updated = { ...current, ...next };
|
|
123
|
+
list = list.map(span => (span === current ? updated : span));
|
|
124
|
+
current = updated;
|
|
125
|
+
notify();
|
|
126
|
+
};
|
|
127
|
+
const span = {
|
|
128
|
+
context,
|
|
129
|
+
get ended() {
|
|
130
|
+
return ended;
|
|
131
|
+
},
|
|
132
|
+
setAttribute(key, value) {
|
|
133
|
+
span.setAttributes({ [key]: value });
|
|
134
|
+
},
|
|
135
|
+
setAttributes(attributes) {
|
|
136
|
+
if (ended) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
change({
|
|
140
|
+
attributes: { ...current.attributes, ...defined(attributes) },
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
addEvent(eventName, attributes, time) {
|
|
144
|
+
if (ended) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const event = {
|
|
148
|
+
name: eventName,
|
|
149
|
+
timestamp: iso(time ?? now()),
|
|
150
|
+
attributes: defined(attributes),
|
|
151
|
+
};
|
|
152
|
+
change({ events: [...(current.events ?? []), event] });
|
|
153
|
+
},
|
|
154
|
+
setStatus(code, message) {
|
|
155
|
+
if (ended) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
change({
|
|
159
|
+
status_code: code,
|
|
160
|
+
...(message ? { status_message: message } : {}),
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
recordException(error, time) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
span.addEvent('exception', {
|
|
166
|
+
'exception.type': error instanceof Error ? error.name : typeof error,
|
|
167
|
+
'exception.message': message,
|
|
168
|
+
'exception.stacktrace': error instanceof Error ? error.stack : undefined,
|
|
169
|
+
}, time);
|
|
170
|
+
span.setStatus('ERROR', message);
|
|
171
|
+
},
|
|
172
|
+
updateName(next) {
|
|
173
|
+
if (!ended) {
|
|
174
|
+
change({ span_name: next });
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
end(time) {
|
|
178
|
+
if (ended) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
ended = true;
|
|
182
|
+
closeOpen(service, context.span_id);
|
|
183
|
+
const endedAt = Math.max(time ?? now(), startedAt);
|
|
184
|
+
change({
|
|
185
|
+
end_time: iso(endedAt),
|
|
186
|
+
duration_ms: endedAt - startedAt,
|
|
187
|
+
in_progress: false,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
return span;
|
|
192
|
+
},
|
|
193
|
+
activeSpan(service = serviceName) {
|
|
194
|
+
const stack = open.get(service);
|
|
195
|
+
const spanId = stack?.[stack.length - 1];
|
|
196
|
+
const span = spanId
|
|
197
|
+
? list.find(candidate => candidate.span_id === spanId)
|
|
198
|
+
: undefined;
|
|
199
|
+
return span
|
|
200
|
+
? { trace_id: span.trace_id, span_id: span.span_id }
|
|
201
|
+
: undefined;
|
|
202
|
+
},
|
|
203
|
+
spans() {
|
|
204
|
+
return list;
|
|
205
|
+
},
|
|
206
|
+
subscribe(listener) {
|
|
207
|
+
listeners.add(listener);
|
|
208
|
+
return () => {
|
|
209
|
+
listeners.delete(listener);
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
clear() {
|
|
213
|
+
generation += 1;
|
|
214
|
+
list = [];
|
|
215
|
+
open.clear();
|
|
216
|
+
notify();
|
|
217
|
+
},
|
|
218
|
+
exportSpans() {
|
|
219
|
+
return { exportedAt: iso(now()), spans: [...list] };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
return tracer;
|
|
223
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OtelSpan } from '../types';
|
|
2
|
+
import { type OtelLiveTracer } from './tracer';
|
|
3
|
+
/** A tracer's spans, kept current; none without a tracer. */
|
|
4
|
+
export declare function useOtelLiveSpans(tracer: OtelLiveTracer | null | undefined): readonly OtelSpan[];
|
|
5
|
+
/** A tracer made once for a component (or the one given). */
|
|
6
|
+
export declare function useOtelLiveTracer(given?: OtelLiveTracer | null, serviceName?: string): OtelLiveTracer;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A live tracer's spans, kept current in a component.
|
|
7
|
+
*
|
|
8
|
+
* @module otel/live/useOtelLiveSpans
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
11
|
+
import { createOtelLiveTracer } from './tracer.js';
|
|
12
|
+
const NO_SPANS = [];
|
|
13
|
+
const NO_SUBSCRIPTION = () => () => undefined;
|
|
14
|
+
/** A tracer's spans, kept current; none without a tracer. */
|
|
15
|
+
export function useOtelLiveSpans(tracer) {
|
|
16
|
+
return useSyncExternalStore(tracer ? tracer.subscribe : NO_SUBSCRIPTION, () => (tracer ? tracer.spans() : NO_SPANS), () => (tracer ? tracer.spans() : NO_SPANS));
|
|
17
|
+
}
|
|
18
|
+
/** A tracer made once for a component (or the one given). */
|
|
19
|
+
export function useOtelLiveTracer(given, serviceName) {
|
|
20
|
+
const [own] = useState(() => given ?? createOtelLiveTracer({ serviceName }));
|
|
21
|
+
return given ?? own;
|
|
22
|
+
}
|
package/lib/otel/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module otel/types
|
|
5
5
|
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
6
7
|
/** A single span in a trace. */
|
|
7
8
|
export interface OtelSpan {
|
|
8
9
|
trace_id: string;
|
|
@@ -20,6 +21,12 @@ export interface OtelSpan {
|
|
|
20
21
|
attributes?: Record<string, unknown>;
|
|
21
22
|
events?: OtelSpanEvent[];
|
|
22
23
|
links?: OtelSpanLink[];
|
|
24
|
+
/**
|
|
25
|
+
* The span has started and not ended yet: a live, in-page span
|
|
26
|
+
* (`createOtelLiveTracer`). Its `end_time` is its `start_time` and its
|
|
27
|
+
* `duration_ms` 0 until it ends.
|
|
28
|
+
*/
|
|
29
|
+
in_progress?: boolean;
|
|
23
30
|
/** Nested children (computed client-side from parent_span_id). */
|
|
24
31
|
children?: OtelSpan[];
|
|
25
32
|
/** Depth in a span tree (computed client-side). */
|
|
@@ -96,6 +103,16 @@ export interface OtelTracesListProps {
|
|
|
96
103
|
loading?: boolean;
|
|
97
104
|
selectedSpanId?: string | null;
|
|
98
105
|
onSelectSpan?: (span: OtelSpan) => void;
|
|
106
|
+
/** What is said while there is no span. */
|
|
107
|
+
emptyText?: string;
|
|
108
|
+
/** Narrower columns, for a panel beside other content. */
|
|
109
|
+
compact?: boolean;
|
|
110
|
+
/** Every span with children starts unfolded: a live record, read as it grows. */
|
|
111
|
+
defaultExpanded?: boolean;
|
|
112
|
+
/** A mark drawn before a span's name (an icon, an emoji). */
|
|
113
|
+
renderSpanMark?: (span: OtelSpan) => ReactNode;
|
|
114
|
+
/** A line that says what a span carried, drawn after its name. */
|
|
115
|
+
describeSpan?: (span: OtelSpan) => string | undefined;
|
|
99
116
|
}
|
|
100
117
|
/** Props for the span detail panel. */
|
|
101
118
|
export interface OtelSpanDetailProps {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtelLiveSpans – the spans of a page's own tracer (`createOtelLiveTracer`),
|
|
3
|
+
* drawn as they happen: the traces list (each trace a tree, unfolded), and
|
|
4
|
+
* the selected span's detail (its attributes, events, trace tree, raw data).
|
|
5
|
+
*
|
|
6
|
+
* One service's own (`service`): what it did, and the calls made toward it
|
|
7
|
+
* (`peer.service`). Filters by facets (the service, the span kind, or any
|
|
8
|
+
* value a span carries); *Pause* holds the rows still while the record goes
|
|
9
|
+
* on, *Clear* empties it, *Export* saves the spans as JSON.
|
|
10
|
+
*
|
|
11
|
+
* @module otel/views/OtelLiveSpans
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import type { OtelSpan } from '../types';
|
|
15
|
+
import type { OtelLiveTracer } from '../live/tracer';
|
|
16
|
+
/** A filter menu: the values a span has for it, each to keep or not. */
|
|
17
|
+
export interface OtelSpanFacet {
|
|
18
|
+
label: string;
|
|
19
|
+
/** The span's value for it; `undefined` when it has none. */
|
|
20
|
+
value: (span: OtelSpan) => string | undefined;
|
|
21
|
+
/** A value, in words. */
|
|
22
|
+
name?: (value: string) => string;
|
|
23
|
+
}
|
|
24
|
+
/** The facets unless said: the service and the span kind. */
|
|
25
|
+
export declare const DEFAULT_SPAN_FACETS: readonly OtelSpanFacet[];
|
|
26
|
+
export interface OtelLiveSpansProps {
|
|
27
|
+
/** The tracer whose spans are drawn, kept current. */
|
|
28
|
+
tracer?: OtelLiveTracer | null;
|
|
29
|
+
/** Or the spans, as given. */
|
|
30
|
+
spans?: readonly OtelSpan[];
|
|
31
|
+
/** One service's own: done by it, or toward it. */
|
|
32
|
+
service?: string;
|
|
33
|
+
facets?: readonly OtelSpanFacet[];
|
|
34
|
+
/** Narrower columns, the detail under the list. */
|
|
35
|
+
compact?: boolean;
|
|
36
|
+
/** How tall the list grows before it scrolls. */
|
|
37
|
+
maxHeight?: number | string;
|
|
38
|
+
/** Where the selected span's detail goes. Default `below`. */
|
|
39
|
+
detailPlacement?: 'below' | 'side';
|
|
40
|
+
/** What is said while there is no span. */
|
|
41
|
+
emptyText?: string;
|
|
42
|
+
/** The file the spans are exported as, without `.json`. */
|
|
43
|
+
exportName?: string;
|
|
44
|
+
/** The toolbar's name. */
|
|
45
|
+
label?: string;
|
|
46
|
+
renderSpanMark?: (span: OtelSpan) => React.ReactNode;
|
|
47
|
+
describeSpan?: (span: OtelSpan) => string | undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The spans each facet keeps: an empty selection keeps every value. */
|
|
50
|
+
export declare function filterSpansByFacets(spans: readonly OtelSpan[], facets: readonly OtelSpanFacet[], selected: readonly ReadonlySet<string>[]): OtelSpan[];
|
|
51
|
+
export declare const OtelLiveSpans: React.FC<OtelLiveSpansProps>;
|
|
52
|
+
export default OtelLiveSpans;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2023-2025 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* OtelLiveSpans – the spans of a page's own tracer (`createOtelLiveTracer`),
|
|
8
|
+
* drawn as they happen: the traces list (each trace a tree, unfolded), and
|
|
9
|
+
* the selected span's detail (its attributes, events, trace tree, raw data).
|
|
10
|
+
*
|
|
11
|
+
* One service's own (`service`): what it did, and the calls made toward it
|
|
12
|
+
* (`peer.service`). Filters by facets (the service, the span kind, or any
|
|
13
|
+
* value a span carries); *Pause* holds the rows still while the record goes
|
|
14
|
+
* on, *Clear* empties it, *Export* saves the spans as JSON.
|
|
15
|
+
*
|
|
16
|
+
* @module otel/views/OtelLiveSpans
|
|
17
|
+
*/
|
|
18
|
+
import { useMemo, useState } from 'react';
|
|
19
|
+
import { ActionList, ActionMenu, Box, Button, Text } from '@primer/react';
|
|
20
|
+
import { DownloadIcon, PauseIcon, PlayIcon, TrashIcon, } from '@primer/octicons-react';
|
|
21
|
+
import { toMs } from '../utils.js';
|
|
22
|
+
import { spansOfService } from '../live/filter.js';
|
|
23
|
+
import { useOtelLiveSpans } from '../live/useOtelLiveSpans.js';
|
|
24
|
+
import { OtelTracesList } from './OtelTracesList.js';
|
|
25
|
+
import { OtelSpanDetail } from './OtelSpanDetail.js';
|
|
26
|
+
/** The facets unless said: the service and the span kind. */
|
|
27
|
+
export const DEFAULT_SPAN_FACETS = [
|
|
28
|
+
{ label: 'Service', value: span => span.service_name },
|
|
29
|
+
{ label: 'Kind', value: span => span.kind },
|
|
30
|
+
];
|
|
31
|
+
/** The spans each facet keeps: an empty selection keeps every value. */
|
|
32
|
+
export function filterSpansByFacets(spans, facets, selected) {
|
|
33
|
+
return spans.filter(span => facets.every((facet, index) => {
|
|
34
|
+
const values = selected[index];
|
|
35
|
+
if (!values?.size)
|
|
36
|
+
return true;
|
|
37
|
+
const value = facet.value(span);
|
|
38
|
+
return value !== undefined && values.has(value);
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function toggle(set, value) {
|
|
42
|
+
const next = new Set(set);
|
|
43
|
+
if (next.has(value)) {
|
|
44
|
+
next.delete(value);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
next.add(value);
|
|
48
|
+
}
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
function FacetMenu({ facet, options, selected, onChange, }) {
|
|
52
|
+
const name = facet.name ?? ((value) => value);
|
|
53
|
+
return (_jsxs(ActionMenu, { children: [_jsxs(ActionMenu.Button, { size: "small", "data-otel-facet": facet.label.toLowerCase(), children: [facet.label, selected.size ? ` · ${selected.size}` : ''] }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsxs(ActionList, { selectionVariant: "multiple", "aria-label": facet.label, children: [_jsx(ActionList.Item, { selected: !selected.size, onSelect: () => onChange(new Set()), children: "All" }), _jsx(ActionList.Divider, {}), options.map(option => (_jsx(ActionList.Item, { selected: selected.has(option), onSelect: () => onChange(toggle(selected, option)), children: name(option) }, option)))] }) })] }));
|
|
54
|
+
}
|
|
55
|
+
/** Save the spans as a JSON file. */
|
|
56
|
+
function download(name, record) {
|
|
57
|
+
const blob = new Blob([JSON.stringify(record, null, 2)], {
|
|
58
|
+
type: 'application/json',
|
|
59
|
+
});
|
|
60
|
+
const url = URL.createObjectURL(blob);
|
|
61
|
+
const link = document.createElement('a');
|
|
62
|
+
link.href = url;
|
|
63
|
+
link.download = `${name}.json`;
|
|
64
|
+
link.click();
|
|
65
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
66
|
+
}
|
|
67
|
+
export const OtelLiveSpans = ({ tracer, spans: given, service, facets = DEFAULT_SPAN_FACETS, compact = false, maxHeight = 420, detailPlacement = 'below', emptyText = 'Nothing recorded yet.', exportName = 'otel-spans', label = 'Spans', renderSpanMark, describeSpan, }) => {
|
|
68
|
+
const live = useOtelLiveSpans(given ? null : tracer);
|
|
69
|
+
const everything = given ?? live;
|
|
70
|
+
const [clearedAt, setClearedAt] = useState(0);
|
|
71
|
+
const [held, setHeld] = useState(null);
|
|
72
|
+
const [selected, setSelected] = useState(() => facets.map(() => new Set()));
|
|
73
|
+
const [selectedSpanId, setSelectedSpanId] = useState(null);
|
|
74
|
+
const recorded = useMemo(() => {
|
|
75
|
+
const own = spansOfService(everything, service);
|
|
76
|
+
return clearedAt
|
|
77
|
+
? own.filter(span => toMs(span.start_time) >= clearedAt)
|
|
78
|
+
: own;
|
|
79
|
+
}, [everything, service, clearedAt]);
|
|
80
|
+
const shown = held ?? recorded;
|
|
81
|
+
const waiting = held ? recorded.length - held.length : 0;
|
|
82
|
+
const options = useMemo(() => facets.map(facet => [
|
|
83
|
+
...new Set(recorded
|
|
84
|
+
.map(span => facet.value(span))
|
|
85
|
+
.filter((value) => value !== undefined)),
|
|
86
|
+
].sort()), [facets, recorded]);
|
|
87
|
+
const rows = useMemo(() => filterSpansByFacets(shown, facets, selected), [shown, facets, selected]);
|
|
88
|
+
// The selected span as it is now: a live span changes as it goes.
|
|
89
|
+
const selectedSpan = useMemo(() => selectedSpanId
|
|
90
|
+
? (everything.find(span => span.span_id === selectedSpanId) ?? null)
|
|
91
|
+
: null, [everything, selectedSpanId]);
|
|
92
|
+
const traceSpans = useMemo(() => selectedSpan
|
|
93
|
+
? everything.filter(span => span.trace_id === selectedSpan.trace_id)
|
|
94
|
+
: [], [everything, selectedSpan]);
|
|
95
|
+
const side = detailPlacement === 'side' && !compact;
|
|
96
|
+
return (_jsxs(Box, { "data-otel-live-spans": "", sx: {
|
|
97
|
+
border: '1px solid',
|
|
98
|
+
borderColor: 'border.default',
|
|
99
|
+
borderRadius: 2,
|
|
100
|
+
bg: 'canvas.default',
|
|
101
|
+
color: 'fg.default',
|
|
102
|
+
minWidth: 0,
|
|
103
|
+
}, children: [_jsxs(Box, { role: "toolbar", "aria-label": label, sx: {
|
|
104
|
+
display: 'flex',
|
|
105
|
+
flexWrap: 'wrap',
|
|
106
|
+
gap: 2,
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
p: 2,
|
|
109
|
+
borderBottom: '1px solid',
|
|
110
|
+
borderColor: 'border.default',
|
|
111
|
+
}, children: [facets.map((facet, index) => (_jsx(FacetMenu, { facet: facet, options: options[index] ?? [], selected: selected[index] ?? new Set(), onChange: next => setSelected(prev => facets.map((_, at) => at === index ? next : (prev[at] ?? new Set()))) }, facet.label))), _jsx(Box, { sx: { flex: 1 } }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, "data-otel-span-count": "", children: [rows.length === shown.length
|
|
112
|
+
? `${rows.length} ${rows.length === 1 ? 'span' : 'spans'}`
|
|
113
|
+
: `${rows.length} of ${shown.length}`, waiting > 0 ? ` · ${waiting} new` : ''] }), _jsx(Button, { size: "small", leadingVisual: held ? PlayIcon : PauseIcon, "aria-pressed": held !== null, onClick: () => setHeld(held ? null : recorded), "data-otel-pause": "", children: held ? 'Resume' : 'Pause' }), _jsx(Button, { size: "small", leadingVisual: TrashIcon, onClick: () => {
|
|
114
|
+
setHeld(null);
|
|
115
|
+
setSelectedSpanId(null);
|
|
116
|
+
// One service's view of a shared record clears only itself.
|
|
117
|
+
if (tracer && !given && !service) {
|
|
118
|
+
tracer.clear();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
setClearedAt(Date.now());
|
|
122
|
+
}
|
|
123
|
+
}, "data-otel-clear": "", children: "Clear" }), _jsx(Button, { size: "small", leadingVisual: DownloadIcon, disabled: !recorded.length, onClick: () => download(exportName, {
|
|
124
|
+
exportedAt: new Date().toISOString(),
|
|
125
|
+
spans: [...recorded],
|
|
126
|
+
}), "data-otel-export": "", children: "Export" })] }), _jsxs(Box, { sx: {
|
|
127
|
+
display: side ? 'grid' : 'block',
|
|
128
|
+
gridTemplateColumns: side
|
|
129
|
+
? 'minmax(0, 3fr) minmax(0, 2fr)'
|
|
130
|
+
: undefined,
|
|
131
|
+
}, children: [_jsx(Box, { sx: { maxHeight, overflowY: 'auto', minWidth: 0 }, children: _jsx(OtelTracesList, { spans: rows, selectedSpanId: selectedSpanId, onSelectSpan: span => setSelectedSpanId(span.span_id), emptyText: emptyText, compact: compact, defaultExpanded: true, renderSpanMark: renderSpanMark, describeSpan: describeSpan }) }), selectedSpan && (_jsx(Box, { "data-otel-live-detail": "", sx: {
|
|
132
|
+
maxHeight,
|
|
133
|
+
overflowY: 'auto',
|
|
134
|
+
minWidth: 0,
|
|
135
|
+
borderTop: side ? 'none' : '1px solid',
|
|
136
|
+
borderColor: 'border.default',
|
|
137
|
+
}, children: _jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans, onClose: () => setSelectedSpanId(null) }) }))] })] }));
|
|
138
|
+
};
|
|
139
|
+
export default OtelLiveSpans;
|
|
@@ -8,4 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import type { OtelSpanDetailProps } from '../types';
|
|
11
|
+
/**
|
|
12
|
+
* A value as the detail unfolds it: JSON text of an object or an array (the
|
|
13
|
+
* GenAI conventions carry a tool call's arguments and result so) is the value
|
|
14
|
+
* it says; anything else is itself.
|
|
15
|
+
*/
|
|
16
|
+
export declare function unfoldedValue(value: unknown): unknown;
|
|
11
17
|
export declare const OtelSpanDetail: React.FC<OtelSpanDetailProps>;
|
|
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
*
|
|
12
12
|
* @module otel/OtelSpanDetail
|
|
13
13
|
*/
|
|
14
|
-
import { useState } from 'react';
|
|
14
|
+
import { useMemo, useState } from 'react';
|
|
15
15
|
import { Box, Text, IconButton, UnderlineNav, CounterLabel, Label, } from '@primer/react';
|
|
16
16
|
import { XIcon, ChevronDownIcon, ChevronRightIcon, } from '@primer/octicons-react';
|
|
17
17
|
import { formatDuration, buildSpanTree } from '../utils.js';
|
|
@@ -61,8 +61,28 @@ const CollapsibleSection = ({ title, data, defaultOpen = false }) => {
|
|
|
61
61
|
overflowX: 'auto',
|
|
62
62
|
}, children: Object.entries(data).map(([key, val]) => (_jsx(AttributeRow, { attrKey: key, value: val, depth: 0 }, key))) }))] }));
|
|
63
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* A value as the detail unfolds it: JSON text of an object or an array (the
|
|
66
|
+
* GenAI conventions carry a tool call's arguments and result so) is the value
|
|
67
|
+
* it says; anything else is itself.
|
|
68
|
+
*/
|
|
69
|
+
export function unfoldedValue(value) {
|
|
70
|
+
if (typeof value !== 'string')
|
|
71
|
+
return value;
|
|
72
|
+
const text = value.trim();
|
|
73
|
+
if (!/^[[{]/.test(text))
|
|
74
|
+
return value;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(text);
|
|
77
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : value;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
64
83
|
/** Recursive attribute row – supports nested objects and arrays. */
|
|
65
|
-
const AttributeRow = ({ attrKey, value, depth }) => {
|
|
84
|
+
const AttributeRow = ({ attrKey, value: given, depth }) => {
|
|
85
|
+
const value = useMemo(() => unfoldedValue(given), [given]);
|
|
66
86
|
const [open, setOpen] = useState(depth < 1);
|
|
67
87
|
const isObject = typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
68
88
|
const isArray = Array.isArray(value);
|
|
@@ -19,6 +19,8 @@ import { Blankslate } from '@primer/react/experimental';
|
|
|
19
19
|
import { TelescopeIcon, ChevronRightIcon, ChevronDownIcon, } from '@primer/octicons-react';
|
|
20
20
|
import { formatDuration, formatTime, buildSpanTree } from '../utils.js';
|
|
21
21
|
const GRID_COLS = '140px 1fr 160px 90px';
|
|
22
|
+
/** Narrower: for a panel beside other content. */
|
|
23
|
+
const COMPACT_GRID_COLS = '96px minmax(0, 1fr) 96px 64px';
|
|
22
24
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
23
25
|
/** Extract gen_ai token counts from span attributes. */
|
|
24
26
|
function getTokenUsage(span) {
|
|
@@ -35,9 +37,13 @@ function getTokenUsage(span) {
|
|
|
35
37
|
return null;
|
|
36
38
|
return { input, output };
|
|
37
39
|
}
|
|
38
|
-
export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }) => {
|
|
39
|
-
//
|
|
40
|
-
const [
|
|
40
|
+
export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, emptyText, compact = false, defaultExpanded = false, renderSpanMark, describeSpan, }) => {
|
|
41
|
+
// The span_ids unfolded (or, when every span starts unfolded, folded).
|
|
42
|
+
const [toggled, setToggled] = useState(new Set());
|
|
43
|
+
const expanded = useMemo(() => ({
|
|
44
|
+
has: (spanId) => defaultExpanded ? !toggled.has(spanId) : toggled.has(spanId),
|
|
45
|
+
}), [toggled, defaultExpanded]);
|
|
46
|
+
const gridCols = compact ? COMPACT_GRID_COLS : GRID_COLS;
|
|
41
47
|
// Build the span tree from flat input
|
|
42
48
|
const roots = useMemo(() => buildSpanTree(spans), [spans]);
|
|
43
49
|
// Flatten visible rows based on which nodes are expanded
|
|
@@ -58,7 +64,7 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
58
64
|
return rows;
|
|
59
65
|
}, [roots, expanded]);
|
|
60
66
|
const toggleExpand = useCallback((spanId) => {
|
|
61
|
-
|
|
67
|
+
setToggled(prev => {
|
|
62
68
|
const next = new Set(prev);
|
|
63
69
|
if (next.has(spanId)) {
|
|
64
70
|
next.delete(spanId);
|
|
@@ -73,11 +79,11 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
73
79
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 5 }, children: _jsx(Spinner, { size: "medium" }) }));
|
|
74
80
|
}
|
|
75
81
|
if (spans.length === 0) {
|
|
76
|
-
return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(TelescopeIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No traces found" }), _jsx(Blankslate.Description, { children:
|
|
82
|
+
return (_jsxs(Blankslate, { children: [_jsx(Blankslate.Visual, { children: _jsx(TelescopeIcon, { size: 24 }) }), _jsx(Blankslate.Heading, { children: "No traces found" }), _jsx(Blankslate.Description, { children: emptyText ?? 'Send some telemetry data first.' })] }));
|
|
77
83
|
}
|
|
78
84
|
return (_jsxs(Box, { sx: { width: '100%', flex: 1, minHeight: 0, overflow: 'auto' }, children: [_jsx(Box, { sx: {
|
|
79
85
|
display: 'grid',
|
|
80
|
-
gridTemplateColumns:
|
|
86
|
+
gridTemplateColumns: gridCols,
|
|
81
87
|
gap: 2,
|
|
82
88
|
px: 3,
|
|
83
89
|
position: 'sticky',
|
|
@@ -97,15 +103,16 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
97
103
|
}, children: h }, h))) }), visibleRows.map(({ span, depth, hasChildren }, idx) => {
|
|
98
104
|
const isSelected = selectedSpanId === span.span_id;
|
|
99
105
|
const isExpanded = expanded.has(span.span_id);
|
|
100
|
-
const indent = depth * 20;
|
|
101
|
-
|
|
106
|
+
const indent = depth * (compact ? 12 : 20);
|
|
107
|
+
const description = describeSpan?.(span);
|
|
108
|
+
return (_jsxs(Box, { "data-otel-span": span.span_id, "data-otel-span-name": span.span_name, onClick: () => {
|
|
102
109
|
if (hasChildren) {
|
|
103
110
|
toggleExpand(span.span_id);
|
|
104
111
|
}
|
|
105
112
|
onSelectSpan?.(span);
|
|
106
113
|
}, sx: {
|
|
107
114
|
display: 'grid',
|
|
108
|
-
gridTemplateColumns:
|
|
115
|
+
gridTemplateColumns: gridCols,
|
|
109
116
|
gap: 2,
|
|
110
117
|
px: 3,
|
|
111
118
|
py: '5px',
|
|
@@ -121,7 +128,7 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
121
128
|
bg: isSelected ? 'accent.subtle' : 'canvas.subtle',
|
|
122
129
|
},
|
|
123
130
|
}, children: [_jsx(Text, { sx: {
|
|
124
|
-
fontSize: 1,
|
|
131
|
+
fontSize: compact ? 0 : 1,
|
|
125
132
|
fontFamily: 'mono',
|
|
126
133
|
color: 'fg.muted',
|
|
127
134
|
whiteSpace: 'nowrap',
|
|
@@ -148,12 +155,14 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
148
155
|
justifyContent: 'center',
|
|
149
156
|
color: 'fg.subtle',
|
|
150
157
|
fontSize: 0,
|
|
151
|
-
}, children: "\u2500" })) : null, _jsx(Text, { sx: {
|
|
158
|
+
}, children: "\u2500" })) : null, renderSpanMark?.(span), _jsx(Text, { sx: {
|
|
152
159
|
fontSize: 1,
|
|
153
160
|
overflow: 'hidden',
|
|
154
161
|
textOverflow: 'ellipsis',
|
|
155
162
|
whiteSpace: 'nowrap',
|
|
156
|
-
|
|
163
|
+
flexShrink: description ? 0 : 1,
|
|
164
|
+
maxWidth: description ? '60%' : undefined,
|
|
165
|
+
}, title: span.span_name, children: span.span_name }), span.in_progress && (_jsx(Label, { variant: "attention", size: "small", children: "running" })), span.status_code === 'ERROR' && (_jsx(Label, { variant: "danger", size: "small", children: "ERROR" })), (() => {
|
|
157
166
|
const usage = getTokenUsage(span);
|
|
158
167
|
if (!usage)
|
|
159
168
|
return null;
|
|
@@ -179,7 +188,14 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
179
188
|
alignItems: 'center',
|
|
180
189
|
gap: '1px',
|
|
181
190
|
}, children: [_jsx(Text, { sx: { fontSize: '9px', color: 'fg.subtle' }, children: "\u2199" }), _jsx(Text, { children: usage.output })] }))] }));
|
|
182
|
-
})(),
|
|
191
|
+
})(), description && (_jsx(Text, { sx: {
|
|
192
|
+
fontSize: 1,
|
|
193
|
+
color: 'fg.muted',
|
|
194
|
+
overflow: 'hidden',
|
|
195
|
+
textOverflow: 'ellipsis',
|
|
196
|
+
whiteSpace: 'nowrap',
|
|
197
|
+
minWidth: 0,
|
|
198
|
+
}, title: description, "data-otel-span-description": "", children: description })), span.otel_scope_name && !compact && (_jsx(Label, { size: "small", variant: "secondary", sx: { flexShrink: 0, fontSize: '10px' }, children: span.otel_scope_name }))] }), _jsx(Text, { sx: {
|
|
183
199
|
fontSize: 1,
|
|
184
200
|
color: 'fg.muted',
|
|
185
201
|
overflow: 'hidden',
|
|
@@ -194,6 +210,6 @@ export const OtelTracesList = ({ spans, loading, selectedSpanId, onSelectSpan, }
|
|
|
194
210
|
color: span.duration_ms != null && span.duration_ms > 1000
|
|
195
211
|
? 'danger.fg'
|
|
196
212
|
: 'fg.default',
|
|
197
|
-
}, children: formatDuration(span.duration_ms) })] }, `${span.trace_id}-${span.span_id}-${idx}`));
|
|
213
|
+
}, children: span.in_progress ? '…' : formatDuration(span.duration_ms) })] }, `${span.trace_id}-${span.span_id}-${idx}`));
|
|
198
214
|
})] }));
|
|
199
215
|
};
|
|
@@ -14,6 +14,9 @@ export { OtelSearchBar } from './OtelSearchBar';
|
|
|
14
14
|
export { OtelSpanDetail } from './OtelSpanDetail';
|
|
15
15
|
export { OtelSpanTree } from './OtelSpanTree';
|
|
16
16
|
export { OtelTimeline } from './OtelTimeline';
|
|
17
|
+
export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, } from './OtelLiveSpans';
|
|
18
|
+
export type { OtelLiveSpansProps, OtelSpanFacet } from './OtelLiveSpans';
|
|
19
|
+
export { unfoldedValue } from './OtelSpanDetail';
|
|
17
20
|
export { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider';
|
|
18
21
|
export { OtelSqlView } from './OtelSqlView';
|
|
19
22
|
export type { OtelSqlViewProps } from './OtelSqlView';
|
package/lib/otel/views/index.js
CHANGED
|
@@ -16,6 +16,8 @@ export { OtelSearchBar } from './OtelSearchBar.js';
|
|
|
16
16
|
export { OtelSpanDetail } from './OtelSpanDetail.js';
|
|
17
17
|
export { OtelSpanTree } from './OtelSpanTree.js';
|
|
18
18
|
export { OtelTimeline } from './OtelTimeline.js';
|
|
19
|
+
export { OtelLiveSpans, DEFAULT_SPAN_FACETS, filterSpansByFacets, } from './OtelLiveSpans.js';
|
|
20
|
+
export { unfoldedValue } from './OtelSpanDetail.js';
|
|
19
21
|
export { OtelTimelineRangeSlider } from './OtelTimelineRangeSlider.js';
|
|
20
22
|
export { OtelSqlView } from './OtelSqlView.js';
|
|
21
23
|
export { OtelSystemView } from './OtelSystemView.js';
|