shoe-box-spanoptic 0.0.0-stage → 0.0.1

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/README.md CHANGED
@@ -1,3 +1,194 @@
1
- # Temporary Holding Version
1
+ # shoe-box-spanoptic
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TUI OTEL visualizer for Effect programs. Visualize your Effect traces in real-time with a terminal UI.
4
+
5
+ Inspired by [Kit Langton's spanoptic](https://x.com/kitlangton/status/1994799261406695946).
6
+
7
+ ## Features
8
+
9
+ - Real-time trace visualization with animated spinners
10
+ - Interactive span navigation with arrow keys
11
+ - Span details panel showing attributes and logs
12
+ - Live logs panel at the bottom of the screen
13
+ - Export traces to JSON for later replay
14
+ - Replay previously exported traces
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ bun add shoe-box-spanoptic
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Programmatic API
25
+
26
+ Wrap your Effect program with `traceVisualize` to visualize traces:
27
+
28
+ ```typescript
29
+ import { BunRuntime } from '@effect/platform-bun';
30
+ import { Effect } from 'effect';
31
+ import { traceVisualize, withTracedSpan } from 'shoe-box-spanoptic';
32
+ import type { TraceExport } from 'shoe-box-spanoptic';
33
+
34
+ let lastExport: TraceExport | undefined;
35
+
36
+ const program = Effect.gen(function* () {
37
+ yield* Effect.log('Starting...');
38
+
39
+ yield* Effect.gen(function* () {
40
+ yield* Effect.log('Fetching data');
41
+ yield* Effect.annotateCurrentSpan('user.id', 123);
42
+ yield* Effect.sleep('500 millis');
43
+ yield* Effect.log('Data fetched');
44
+ return { id: 123, name: 'Alice' };
45
+ }).pipe(withTracedSpan('fetchUser'));
46
+
47
+ yield* Effect.log('Done!');
48
+ }).pipe(withTracedSpan('main'));
49
+
50
+ // Run with trace visualization
51
+ traceVisualize(program, {
52
+ serviceName: 'my-app',
53
+ // Called when the user presses 'e' to export
54
+ onExport: (data) => {
55
+ lastExport = data;
56
+ },
57
+ }).pipe(BunRuntime.runMain);
58
+ ```
59
+
60
+ ### CLI
61
+
62
+ The package includes a CLI for running demos and replaying traces:
63
+
64
+ ```bash
65
+ # Run the built-in demo
66
+ bunx shoe-box-spanoptic demo
67
+
68
+ # Run demo and export to a specific file
69
+ bunx shoe-box-spanoptic demo --output trace.json
70
+
71
+ # Replay a previously exported trace
72
+ bunx shoe-box-spanoptic replay trace.json
73
+ ```
74
+
75
+ ## TUI Layout
76
+
77
+ - **Left panel**: Trace tree with spans and spinners for running operations
78
+ - **Right panel**: Details for the selected span (attributes, status, duration)
79
+ - **Bottom panel**: Logs for the selected span (updates as you navigate)
80
+
81
+ ## TUI Controls
82
+
83
+ | Key | Action |
84
+ | -------------- | ------------------------------------------ |
85
+ | `↑` / `k` | Move selection up |
86
+ | `↓` / `j` | Move selection down |
87
+ | `e` | Export trace (calls `onExport`) |
88
+ | `q` / `Escape` | Quit (once the program has finished) |
89
+ | `Ctrl+C` | Exit immediately |
90
+
91
+ ## API Reference
92
+
93
+ ### `traceVisualize(effect, options)`
94
+
95
+ Runs an Effect program with OTEL tracing and visualizes it in a TUI. The
96
+ returned effect succeeds with the program's result once the user quits.
97
+
98
+ ```typescript
99
+ const traceVisualize: <A, E, R>(
100
+ effect: Effect.Effect<A, E, R>,
101
+ options: TraceVisualizeOptions,
102
+ ) => Effect.Effect<A, E, Exclude<R, Scope.Scope>>;
103
+
104
+ type TraceVisualizeOptions = {
105
+ readonly serviceName: string;
106
+ readonly onExport?: (data: TraceExport) => void;
107
+ };
108
+ ```
109
+
110
+ Logs are attached to the tracing span that is current when they are emitted,
111
+ so both `Effect.withSpan` and `withTracedSpan` spans collect logs.
112
+
113
+ ### `withTracedSpan(name, options?)`
114
+
115
+ Wraps an effect in a tracing span and an Effect log span of the same name. The
116
+ log span adds the span name to console log output when the program runs
117
+ outside the TUI.
118
+
119
+ ```typescript
120
+ import { withTracedSpan } from 'shoe-box-spanoptic';
121
+
122
+ const myEffect = Effect.gen(function* () {
123
+ yield* Effect.log('This log will be associated with mySpan');
124
+ return 'result';
125
+ }).pipe(withTracedSpan('mySpan'));
126
+ ```
127
+
128
+ ### `replayVisualize(data, options?)`
129
+
130
+ Replays a previously exported trace in the TUI.
131
+
132
+ ```typescript
133
+ const replayVisualize: (
134
+ data: TraceExport,
135
+ options?: { readonly onExport?: (data: TraceExport) => void },
136
+ ) => Effect.Effect<void>;
137
+ ```
138
+
139
+ ### `encodeTraceExport` / `decodeTraceExport`
140
+
141
+ Convert a `TraceExport` to and from the pretty-printed JSON file the CLI writes
142
+ (`TraceExportJson` is the underlying `Schema`):
143
+
144
+ ```typescript
145
+ import { decodeTraceExport, replayVisualize } from 'shoe-box-spanoptic';
146
+
147
+ const program = Effect.gen(function* () {
148
+ const fs = yield* FileSystem.FileSystem;
149
+ const data = yield* decodeTraceExport(yield* fs.readFileString('trace.json'));
150
+ yield* replayVisualize(data);
151
+ });
152
+ ```
153
+
154
+ ### Types
155
+
156
+ `LogEntry`, `SpanInfo` and `TraceExport` are exported both as Effect `Schema`s
157
+ and as their (readonly) types:
158
+
159
+ ```typescript
160
+ type TraceExport = {
161
+ readonly version: 1;
162
+ readonly serviceName: string;
163
+ readonly exportedAt: number;
164
+ readonly spans: ReadonlyArray<SpanInfo>;
165
+ };
166
+
167
+ type SpanInfo = {
168
+ readonly spanId: string;
169
+ readonly parentSpanId?: string | undefined;
170
+ readonly name: string;
171
+ readonly startTime: number;
172
+ readonly endTime: number | null;
173
+ readonly status: 'running' | 'ok' | 'error';
174
+ readonly errorMessage?: string | undefined;
175
+ readonly attributes: { readonly [key: string]: unknown };
176
+ readonly logs: ReadonlyArray<LogEntry>;
177
+ };
178
+
179
+ type LogEntry = {
180
+ readonly timestamp: number;
181
+ readonly level: string; // 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | ...
182
+ readonly message: string;
183
+ readonly spanName: string | null;
184
+ };
185
+ ```
186
+
187
+ ## Requirements
188
+
189
+ - Effect 4 (`effect` and `@effect/opentelemetry` 4.0.6)
190
+ - Bun (`@opentui/core` loads its native renderer through `bun:ffi`)
191
+
192
+ ## License
193
+
194
+ MIT
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.mjs ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bun
2
+ import { c as decodeTraceExport, l as encodeTraceExport, n as traceVisualize, r as withTracedSpan, t as replayVisualize } from "./src-Cnnn7RBN.mjs";
3
+ import { Clock, Console, Effect, FileSystem, Option, Random, References } from "effect";
4
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
5
+ import { Argument, Command, Flag } from "effect/cli";
6
+ // src/demo-program.ts
7
+ const randomDelay = (min, max) => Effect.flatMap(Random.nextIntBetween(min, max), (ms) => Effect.sleep(ms));
8
+ const fetchUser = Effect.gen(function* () {
9
+ yield* Effect.log("Fetching user from database...");
10
+ yield* Effect.annotateCurrentSpan("user.id", 1);
11
+ yield* Effect.annotateCurrentSpan("user.role", "admin");
12
+ yield* randomDelay(300, 600);
13
+ yield* Effect.log("User fetched successfully");
14
+ return {
15
+ id: 1,
16
+ name: "Alice"
17
+ };
18
+ }).pipe(withTracedSpan("fetchUser"));
19
+ const processData = Effect.gen(function* () {
20
+ yield* Effect.log("Starting data processing");
21
+ yield* randomDelay(200, 400);
22
+ yield* Effect.logDebug("Processed 42 records");
23
+ yield* Effect.log("Data processing complete");
24
+ }).pipe(withTracedSpan("processData"));
25
+ const connectToDatabase = Effect.gen(function* () {
26
+ yield* Effect.log("Establishing database connection");
27
+ yield* randomDelay(100, 200);
28
+ yield* Effect.log("Connected to PostgreSQL");
29
+ }).pipe(withTracedSpan("connectToDatabase"));
30
+ const queryUsers = Effect.gen(function* () {
31
+ yield* Effect.log("Executing query: SELECT * FROM users");
32
+ yield* Effect.annotateCurrentSpan("db.table", "users");
33
+ yield* Effect.annotateCurrentSpan("db.rows_returned", 42);
34
+ yield* randomDelay(250, 500);
35
+ yield* Effect.logDebug("Query returned 42 rows in 234ms");
36
+ }).pipe(withTracedSpan("queryUsers"));
37
+ const queryOrders = Effect.gen(function* () {
38
+ yield* Effect.log("Executing query: SELECT * FROM orders");
39
+ yield* Effect.annotateCurrentSpan("db.table", "orders");
40
+ yield* Effect.annotateCurrentSpan("db.rows_returned", 156);
41
+ yield* randomDelay(300, 600);
42
+ yield* Effect.logDebug("Query returned 156 rows in 412ms");
43
+ yield* Effect.logWarning("Large result set - consider pagination");
44
+ }).pipe(withTracedSpan("queryOrders"));
45
+ const databaseOperations = Effect.gen(function* () {
46
+ yield* Effect.log("Starting database operations");
47
+ yield* connectToDatabase;
48
+ yield* queryUsers;
49
+ yield* queryOrders;
50
+ yield* Effect.log("Database operations complete");
51
+ }).pipe(withTracedSpan("databaseOperations"));
52
+ const sendNotifications = Effect.gen(function* () {
53
+ yield* Effect.log("Sending notifications to 3 users");
54
+ yield* randomDelay(400, 800);
55
+ yield* Effect.logInfo("Email sent to user@example.com");
56
+ yield* Effect.logInfo("Push notification sent");
57
+ }).pipe(withTracedSpan("sendNotifications"));
58
+ const updateCache = Effect.gen(function* () {
59
+ yield* Effect.log("Invalidating cache entries");
60
+ yield* randomDelay(150, 300);
61
+ yield* Effect.logDebug("Cache key: user:1 invalidated");
62
+ }).pipe(withTracedSpan("updateCache"));
63
+ const trackAnalytics = Effect.gen(function* () {
64
+ yield* Effect.log("Recording analytics event");
65
+ yield* randomDelay(200, 500);
66
+ yield* Effect.logDebug("Event: page_view recorded");
67
+ }).pipe(withTracedSpan("trackAnalytics"));
68
+ const generateReport = Effect.gen(function* () {
69
+ yield* Effect.log("Generating monthly report");
70
+ yield* randomDelay(400, 700);
71
+ yield* Effect.logInfo("Report generated: report_2024_01.pdf");
72
+ yield* Effect.log("Report saved to S3");
73
+ }).pipe(withTracedSpan("generateReport"));
74
+ /** A simulated API request handler: sequential, nested and parallel spans. */
75
+ const demoProgram = Effect.gen(function* () {
76
+ yield* Effect.log("Starting API request handler");
77
+ const user = yield* fetchUser;
78
+ yield* processData;
79
+ yield* databaseOperations;
80
+ yield* Effect.all([
81
+ sendNotifications,
82
+ updateCache,
83
+ trackAnalytics
84
+ ], { concurrency: "unbounded" }).pipe(withTracedSpan("sideEffects"));
85
+ yield* generateReport;
86
+ yield* Effect.sleep("200 millis").pipe(withTracedSpan("finalize"));
87
+ yield* Effect.log("Request completed successfully");
88
+ return user;
89
+ }).pipe(withTracedSpan("main"));
90
+ // src/cli.ts
91
+ /**
92
+ * Spanoptic CLI - TUI OTEL visualizer for Effect programs
93
+ *
94
+ * Usage:
95
+ * spanoptic demo [--output <file>] - Run a demo trace visualization
96
+ * spanoptic replay <file> - Replay a previously exported trace
97
+ */
98
+ const writeTrace = Effect.fnUntraced(function* (data, output) {
99
+ const fs = yield* FileSystem.FileSystem;
100
+ const now = yield* Clock.currentTimeMillis;
101
+ const filename = Option.getOrElse(output, () => `trace-${now}.json`);
102
+ yield* fs.writeFileString(filename, yield* encodeTraceExport(data));
103
+ yield* Console.log(`\nTrace exported to ${filename}`);
104
+ });
105
+ const exportHandler = Effect.fnUntraced(function* (output) {
106
+ const context = yield* Effect.context();
107
+ return (data) => {
108
+ Effect.runForkWith(context)(writeTrace(data, output).pipe(Effect.catch((error) => Console.error(String(error)))));
109
+ };
110
+ });
111
+ const demoCommand = Command.make("demo", { output: Flag.File("output").pipe(Flag.withAlias("o"), Flag.withDescription("Where to write the trace when you press e"), Flag.optional) }, Effect.fnUntraced(function* ({ output }) {
112
+ yield* traceVisualize(demoProgram, {
113
+ serviceName: "spanoptic-demo",
114
+ onExport: yield* exportHandler(output)
115
+ });
116
+ })).pipe(Command.withDescription("Run a demo trace visualization"));
117
+ const replayCommand = Command.make("replay", { file: Argument.File("file", { mustExist: true }).pipe(Argument.withDescription("A trace exported with e")) }, Effect.fnUntraced(function* ({ file }) {
118
+ const fs = yield* FileSystem.FileSystem;
119
+ const data = yield* decodeTraceExport(yield* fs.readFileString(file));
120
+ yield* Console.log(`Replaying trace from ${file}`);
121
+ yield* Console.log(`Service: ${data.serviceName}`);
122
+ yield* Console.log(`Spans: ${data.spans.length}`);
123
+ yield* Console.log("");
124
+ yield* Effect.sleep("500 millis");
125
+ yield* replayVisualize(data, { onExport: yield* exportHandler(Option.none()) });
126
+ })).pipe(Command.withDescription("Replay a previously exported trace"));
127
+ const spanoptic = Command.make("spanoptic", {}, () => Console.log("Spanoptic - TUI OTEL visualizer for Effect programs\n\nUse --help to see available commands.")).pipe(Command.withDescription("TUI OTEL visualizer for Effect programs"), Command.withSubcommands([demoCommand, replayCommand]));
128
+ Command.run(spanoptic, { version: "0.0.1" }).pipe(Effect.provideService(References.MinimumLogLevel, "None"), Effect.provide(BunServices.layer), BunRuntime.runMain);
129
+ export {};
130
+
131
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/demo-program.ts","../src/cli.ts"],"sourcesContent":["import { Effect, Random } from 'effect';\n\nimport { withTracedSpan } from './index.ts';\n\nconst randomDelay = (min: number, max: number) =>\n Effect.flatMap(Random.nextIntBetween(min, max), (ms) => Effect.sleep(ms));\n\nconst fetchUser = Effect.gen(function* () {\n yield* Effect.log('Fetching user from database...');\n yield* Effect.annotateCurrentSpan('user.id', 1);\n yield* Effect.annotateCurrentSpan('user.role', 'admin');\n yield* randomDelay(300, 600);\n yield* Effect.log('User fetched successfully');\n return { id: 1, name: 'Alice' };\n}).pipe(withTracedSpan('fetchUser'));\n\nconst processData = Effect.gen(function* () {\n yield* Effect.log('Starting data processing');\n yield* randomDelay(200, 400);\n yield* Effect.logDebug('Processed 42 records');\n yield* Effect.log('Data processing complete');\n}).pipe(withTracedSpan('processData'));\n\nconst connectToDatabase = Effect.gen(function* () {\n yield* Effect.log('Establishing database connection');\n yield* randomDelay(100, 200);\n yield* Effect.log('Connected to PostgreSQL');\n}).pipe(withTracedSpan('connectToDatabase'));\n\nconst queryUsers = Effect.gen(function* () {\n yield* Effect.log('Executing query: SELECT * FROM users');\n yield* Effect.annotateCurrentSpan('db.table', 'users');\n yield* Effect.annotateCurrentSpan('db.rows_returned', 42);\n yield* randomDelay(250, 500);\n yield* Effect.logDebug('Query returned 42 rows in 234ms');\n}).pipe(withTracedSpan('queryUsers'));\n\nconst queryOrders = Effect.gen(function* () {\n yield* Effect.log('Executing query: SELECT * FROM orders');\n yield* Effect.annotateCurrentSpan('db.table', 'orders');\n yield* Effect.annotateCurrentSpan('db.rows_returned', 156);\n yield* randomDelay(300, 600);\n yield* Effect.logDebug('Query returned 156 rows in 412ms');\n yield* Effect.logWarning('Large result set - consider pagination');\n}).pipe(withTracedSpan('queryOrders'));\n\nconst databaseOperations = Effect.gen(function* () {\n yield* Effect.log('Starting database operations');\n yield* connectToDatabase;\n yield* queryUsers;\n yield* queryOrders;\n yield* Effect.log('Database operations complete');\n}).pipe(withTracedSpan('databaseOperations'));\n\nconst sendNotifications = Effect.gen(function* () {\n yield* Effect.log('Sending notifications to 3 users');\n yield* randomDelay(400, 800);\n yield* Effect.logInfo('Email sent to user@example.com');\n yield* Effect.logInfo('Push notification sent');\n}).pipe(withTracedSpan('sendNotifications'));\n\nconst updateCache = Effect.gen(function* () {\n yield* Effect.log('Invalidating cache entries');\n yield* randomDelay(150, 300);\n yield* Effect.logDebug('Cache key: user:1 invalidated');\n}).pipe(withTracedSpan('updateCache'));\n\nconst trackAnalytics = Effect.gen(function* () {\n yield* Effect.log('Recording analytics event');\n yield* randomDelay(200, 500);\n yield* Effect.logDebug('Event: page_view recorded');\n}).pipe(withTracedSpan('trackAnalytics'));\n\nconst generateReport = Effect.gen(function* () {\n yield* Effect.log('Generating monthly report');\n yield* randomDelay(400, 700);\n yield* Effect.logInfo('Report generated: report_2024_01.pdf');\n yield* Effect.log('Report saved to S3');\n}).pipe(withTracedSpan('generateReport'));\n\n/** A simulated API request handler: sequential, nested and parallel spans. */\nexport const demoProgram = Effect.gen(function* () {\n yield* Effect.log('Starting API request handler');\n const user = yield* fetchUser;\n yield* processData;\n yield* databaseOperations;\n yield* Effect.all([sendNotifications, updateCache, trackAnalytics], {\n concurrency: 'unbounded',\n }).pipe(withTracedSpan('sideEffects'));\n yield* generateReport;\n yield* Effect.sleep('200 millis').pipe(withTracedSpan('finalize'));\n yield* Effect.log('Request completed successfully');\n return user;\n}).pipe(withTracedSpan('main'));\n","#!/usr/bin/env bun\n/**\n * Spanoptic CLI - TUI OTEL visualizer for Effect programs\n *\n * Usage:\n * spanoptic demo [--output <file>] - Run a demo trace visualization\n * spanoptic replay <file> - Replay a previously exported trace\n */\n\nimport { BunRuntime, BunServices } from '@effect/platform-bun';\nimport { Clock, Console, Effect, FileSystem, Option, References } from 'effect';\nimport { Argument, Command, Flag } from 'effect/cli';\n\nimport { demoProgram } from './demo-program.ts';\nimport {\n decodeTraceExport,\n encodeTraceExport,\n replayVisualize,\n traceVisualize,\n} from './index.ts';\nimport type { TraceExport } from './index.ts';\n\nconst writeTrace = Effect.fnUntraced(function* (\n data: TraceExport,\n output: Option.Option<string>,\n) {\n const fs = yield* FileSystem.FileSystem;\n const now = yield* Clock.currentTimeMillis;\n const filename = Option.getOrElse(output, () => `trace-${now}.json`);\n yield* fs.writeFileString(filename, yield* encodeTraceExport(data));\n yield* Console.log(`\\nTrace exported to ${filename}`);\n});\n\n// onExport is a plain callback fired from a key press, so the write is\n// forked with the command's services rather than awaited by the TUI.\nconst exportHandler = Effect.fnUntraced(function* (\n output: Option.Option<string>,\n) {\n const context = yield* Effect.context<FileSystem.FileSystem>();\n return (data: TraceExport) => {\n Effect.runForkWith(context)(\n writeTrace(data, output).pipe(\n Effect.catch((error) => Console.error(String(error))),\n ),\n );\n };\n});\n\nconst demoCommand = Command.make(\n 'demo',\n {\n output: Flag.File('output').pipe(\n Flag.withAlias('o'),\n Flag.withDescription('Where to write the trace when you press e'),\n Flag.optional,\n ),\n },\n Effect.fnUntraced(function* ({ output }) {\n yield* traceVisualize(demoProgram, {\n serviceName: 'spanoptic-demo',\n onExport: yield* exportHandler(output),\n });\n }),\n).pipe(Command.withDescription('Run a demo trace visualization'));\n\nconst replayCommand = Command.make(\n 'replay',\n {\n file: Argument.File('file', { mustExist: true }).pipe(\n Argument.withDescription('A trace exported with e'),\n ),\n },\n Effect.fnUntraced(function* ({ file }) {\n const fs = yield* FileSystem.FileSystem;\n const data = yield* decodeTraceExport(yield* fs.readFileString(file));\n\n yield* Console.log(`Replaying trace from ${file}`);\n yield* Console.log(`Service: ${data.serviceName}`);\n yield* Console.log(`Spans: ${data.spans.length}`);\n yield* Console.log('');\n // Leave the summary on screen briefly before the TUI takes over.\n yield* Effect.sleep('500 millis');\n\n yield* replayVisualize(data, {\n onExport: yield* exportHandler(Option.none()),\n });\n }),\n).pipe(Command.withDescription('Replay a previously exported trace'));\n\nconst spanoptic = Command.make('spanoptic', {}, () =>\n Console.log(\n 'Spanoptic - TUI OTEL visualizer for Effect programs\\n\\nUse --help to see available commands.',\n ),\n).pipe(\n Command.withDescription('TUI OTEL visualizer for Effect programs'),\n Command.withSubcommands([demoCommand, replayCommand]),\n);\n\nCommand.run(spanoptic, { version: '0.0.1' }).pipe(\n // Effect logs would paint over the TUI; traceVisualize captures its own.\n Effect.provideService(References.MinimumLogLevel, 'None'),\n // oxlint-disable-next-line effecttsgo/strict-effect-provide -- this is the entry point\n Effect.provide(BunServices.layer),\n BunRuntime.runMain,\n);\n"],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ import * as OtelTracer from "@effect/opentelemetry/OtelTracer";
2
+ import { Effect, Schema, Tracer } from "effect";
3
+ export declare const LogEntry: Schema.Struct<{
4
+ readonly timestamp: Schema.Finite;
5
+ readonly level: Schema.String;
6
+ readonly message: Schema.String;
7
+ readonly spanName: Schema.NullOr<Schema.String>;
8
+ }>;
9
+ export type LogEntry = typeof LogEntry.Type;
10
+ export declare const SpanInfo: Schema.Struct<{
11
+ readonly spanId: Schema.String;
12
+ readonly parentSpanId: Schema.optional<Schema.String>;
13
+ readonly name: Schema.String;
14
+ readonly startTime: Schema.Finite;
15
+ readonly endTime: Schema.NullOr<Schema.Finite>;
16
+ readonly status: Schema.Literals<readonly ["running", "ok", "error"]>;
17
+ readonly errorMessage: Schema.optional<Schema.String>;
18
+ readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
19
+ readonly logs: Schema.$Array<Schema.Struct<{
20
+ readonly timestamp: Schema.Finite;
21
+ readonly level: Schema.String;
22
+ readonly message: Schema.String;
23
+ readonly spanName: Schema.NullOr<Schema.String>;
24
+ }>>;
25
+ }>;
26
+ export type SpanInfo = typeof SpanInfo.Type;
27
+ /** Exported trace data for replay. */
28
+ export declare const TraceExport: Schema.Struct<{
29
+ readonly version: Schema.Literal<1>;
30
+ readonly serviceName: Schema.String;
31
+ readonly exportedAt: Schema.Finite;
32
+ readonly spans: Schema.$Array<Schema.Struct<{
33
+ readonly spanId: Schema.String;
34
+ readonly parentSpanId: Schema.optional<Schema.String>;
35
+ readonly name: Schema.String;
36
+ readonly startTime: Schema.Finite;
37
+ readonly endTime: Schema.NullOr<Schema.Finite>;
38
+ readonly status: Schema.Literals<readonly ["running", "ok", "error"]>;
39
+ readonly errorMessage: Schema.optional<Schema.String>;
40
+ readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
41
+ readonly logs: Schema.$Array<Schema.Struct<{
42
+ readonly timestamp: Schema.Finite;
43
+ readonly level: Schema.String;
44
+ readonly message: Schema.String;
45
+ readonly spanName: Schema.NullOr<Schema.String>;
46
+ }>>;
47
+ }>>;
48
+ }>;
49
+ export type TraceExport = typeof TraceExport.Type;
50
+ /** A trace export as the pretty-printed JSON file the CLI writes and replays. */
51
+ export declare const TraceExportJson: Schema.fromJsonString<Schema.Struct<{
52
+ readonly version: Schema.Literal<1>;
53
+ readonly serviceName: Schema.String;
54
+ readonly exportedAt: Schema.Finite;
55
+ readonly spans: Schema.$Array<Schema.Struct<{
56
+ readonly spanId: Schema.String;
57
+ readonly parentSpanId: Schema.optional<Schema.String>;
58
+ readonly name: Schema.String;
59
+ readonly startTime: Schema.Finite;
60
+ readonly endTime: Schema.NullOr<Schema.Finite>;
61
+ readonly status: Schema.Literals<readonly ["running", "ok", "error"]>;
62
+ readonly errorMessage: Schema.optional<Schema.String>;
63
+ readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
64
+ readonly logs: Schema.$Array<Schema.Struct<{
65
+ readonly timestamp: Schema.Finite;
66
+ readonly level: Schema.String;
67
+ readonly message: Schema.String;
68
+ readonly spanName: Schema.NullOr<Schema.String>;
69
+ }>>;
70
+ }>>;
71
+ }>>;
72
+ export declare const encodeTraceExport: (input: {
73
+ readonly version: 1;
74
+ readonly serviceName: string;
75
+ readonly exportedAt: number;
76
+ readonly spans: readonly {
77
+ readonly spanId: string;
78
+ readonly parentSpanId?: string | undefined;
79
+ readonly name: string;
80
+ readonly startTime: number;
81
+ readonly endTime: number | null;
82
+ readonly status: "error" | "ok" | "running";
83
+ readonly errorMessage?: string | undefined;
84
+ readonly attributes: {
85
+ readonly [x: string]: unknown;
86
+ };
87
+ readonly logs: readonly {
88
+ readonly timestamp: number;
89
+ readonly level: string;
90
+ readonly message: string;
91
+ readonly spanName: string | null;
92
+ }[];
93
+ }[];
94
+ }, options?: import("effect/SchemaAST").ParseOptions) => import("effect/Effect").Effect<string, Schema.SchemaError, never>;
95
+ export declare const decodeTraceExport: (input: string, options?: import("effect/SchemaAST").ParseOptions) => import("effect/Effect").Effect<{
96
+ readonly version: 1;
97
+ readonly serviceName: string;
98
+ readonly exportedAt: number;
99
+ readonly spans: readonly {
100
+ readonly spanId: string;
101
+ readonly parentSpanId?: string | undefined;
102
+ readonly name: string;
103
+ readonly startTime: number;
104
+ readonly endTime: number | null;
105
+ readonly status: "error" | "ok" | "running";
106
+ readonly errorMessage?: string | undefined;
107
+ readonly attributes: {
108
+ readonly [x: string]: unknown;
109
+ };
110
+ readonly logs: readonly {
111
+ readonly timestamp: number;
112
+ readonly level: string;
113
+ readonly message: string;
114
+ readonly spanName: string | null;
115
+ }[];
116
+ }[];
117
+ }, Schema.SchemaError, never>;
118
+ export type TraceVisualizeOptions = {
119
+ readonly serviceName: string;
120
+ /** Called when the user presses `e` to export the trace. */
121
+ readonly onExport?: (data: TraceExport) => void;
122
+ };
123
+ /**
124
+ * Wraps an Effect program with OTEL tracing and visualizes the trace
125
+ * in a real-time TUI with spinners for active spans.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * import { BunRuntime } from "@effect/platform-bun"
130
+ * import { Effect } from "effect"
131
+ * import { traceVisualize, withTracedSpan } from "shoe-box-spanoptic"
132
+ *
133
+ * const program = Effect.gen(function* () {
134
+ * yield* Effect.log("Starting...")
135
+ * yield* Effect.sleep("100 millis")
136
+ * yield* Effect.log("Done!")
137
+ * return "done"
138
+ * }).pipe(withTracedSpan("main"))
139
+ *
140
+ * BunRuntime.runMain(traceVisualize(program, { serviceName: "my-app" }))
141
+ * ```
142
+ */
143
+ export declare const traceVisualize: <A, E, R>(effect: Effect.Effect<A, E, R>, options: TraceVisualizeOptions) => Effect.Effect<A, E, Exclude<Exclude<Exclude<R, OtelTracer.OtelTracer>, never>, import("effect/Scope").Scope>>;
144
+ /**
145
+ * Replays a previously exported trace in the TUI.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * import { BunRuntime, BunServices } from "@effect/platform-bun"
150
+ * import { Effect, FileSystem } from "effect"
151
+ * import { decodeTraceExport, replayVisualize } from "shoe-box-spanoptic"
152
+ *
153
+ * const program = Effect.gen(function* () {
154
+ * const fs = yield* FileSystem.FileSystem
155
+ * const data = yield* decodeTraceExport(yield* fs.readFileString("trace.json"))
156
+ * yield* replayVisualize(data)
157
+ * })
158
+ * BunRuntime.runMain(program.pipe(Effect.provide(BunServices.layer)))
159
+ * ```
160
+ */
161
+ export declare const replayVisualize: (data: TraceExport, options?: Pick<TraceVisualizeOptions, 'onExport'>) => Effect.Effect<void, never, never>;
162
+ /**
163
+ * Wraps an effect in an Effect log span and a tracing span of the same name.
164
+ * The TUI attaches logs to the current tracing span, so plain
165
+ * `Effect.withSpan` works too; the log span adds the span name to console
166
+ * log output when the program runs outside the TUI.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * const myEffect = Effect.gen(function* () {
171
+ * yield* Effect.log("This log will be associated with mySpan");
172
+ * return "result";
173
+ * }).pipe(withTracedSpan("mySpan"));
174
+ * ```
175
+ */
176
+ export declare const withTracedSpan: (name: string, options?: Tracer.SpanOptionsNoTrace) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Tracer.ParentSpan>>;
177
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/trace.ts","../src/index.ts"],"mappings":";;qBAEa,UAAQ,OAAA;;;;;;YAMT,kBAAkB,SAAS;qBAK1B,UAAQ,OAAA;;;;;;;;;;;;;;;;YAYT,kBAAkB,SAAS;;qBAG1B,aAAW,OAAA;;;;;;;;;;;;;;;;;;;;;YAMZ,qBAAqB,YAAY;;qBAGhC,iBAAe,OAAA,eAAA,OAAA;;;;;;;;;;;;;;;;;;;;;qBAEf,oBAAiB;;;;;;;;;;;;;;;;;;;;;;GAAuC,qCAAA,yCAAA,eAAA,OAAA;qBACxD,oBAAiB,eAAA,qCAAA,yCAAA;;;;;;;;;;;;;;;;;;;;;;GAAuC,OAAA;YCiBzD;WACD;;WAEA,YAAY,MAAM;;;;;;;;;;;;;;;;;;;;;;qBA2lBhB,iBAAkB,GAAG,GAAG,GAAC,QAC5B,OAAO,OAAO,GAAG,GAAG,IAAE,SACrB,0BAAqB,OAAA,OAAA,GAAA,GAAA,QAAA,QAAA,QAAA,GAAA,WAAA,4CAAA;;;;;;;;;;;;;;;;;;qBAyDnB,kBAAe,MACpB,aAAW,UACP,KAAK,uCAAkC,OAAA;;;;;;;;;;;;;;;qBA0BtC,iBAAc,cACZ,UAAY,OAAO,wBAC/B,GAAG,GAAG,GAAC,QAAU,OAAO,OAAO,GAAG,GAAG,OAAE,OAAA,OAAA,GAAA,GAAA,QAAA,GAAA,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export { i as LogEntry, a as SpanInfo, o as TraceExport, s as TraceExportJson, c as decodeTraceExport, l as encodeTraceExport, t as replayVisualize, n as traceVisualize, r as withTracedSpan } from "./src-Cnnn7RBN.mjs";