effect-start 0.31.0 → 0.32.0

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.
Files changed (75) hide show
  1. package/dist/Fetch.d.ts +1 -1
  2. package/dist/Fetch.d.ts.map +1 -1
  3. package/dist/Fetch.js +1 -1
  4. package/dist/Fetch.js.map +1 -1
  5. package/dist/Password.d.ts +26 -0
  6. package/dist/Password.d.ts.map +1 -0
  7. package/dist/Password.js +83 -0
  8. package/dist/Password.js.map +1 -0
  9. package/dist/RouteSse.d.ts +1 -2
  10. package/dist/RouteSse.d.ts.map +1 -1
  11. package/dist/RouteSse.js +2 -2
  12. package/dist/RouteSse.js.map +1 -1
  13. package/dist/_StreamExtra.d.ts.map +1 -1
  14. package/dist/_StreamExtra.js +0 -1
  15. package/dist/_StreamExtra.js.map +1 -1
  16. package/dist/studio/Studio.d.ts +1 -1
  17. package/dist/studio/Studio.d.ts.map +1 -1
  18. package/dist/studio/Studio.js +5 -4
  19. package/dist/studio/Studio.js.map +1 -1
  20. package/dist/studio/StudioErrors.d.ts.map +1 -1
  21. package/dist/studio/StudioErrors.js +2 -2
  22. package/dist/studio/StudioErrors.js.map +1 -1
  23. package/dist/studio/StudioLogger.d.ts.map +1 -1
  24. package/dist/studio/StudioLogger.js +1 -3
  25. package/dist/studio/StudioLogger.js.map +1 -1
  26. package/dist/studio/StudioStore.d.ts +23 -17
  27. package/dist/studio/StudioStore.d.ts.map +1 -1
  28. package/dist/studio/StudioStore.js +48 -44
  29. package/dist/studio/StudioStore.js.map +1 -1
  30. package/dist/studio/StudioTracer.d.ts.map +1 -1
  31. package/dist/studio/StudioTracer.js +10 -4
  32. package/dist/studio/StudioTracer.js.map +1 -1
  33. package/dist/studio/routes/errors/route.d.ts +1 -1
  34. package/dist/studio/routes/errors/route.d.ts.map +1 -1
  35. package/dist/studio/routes/errors/route.js +2 -2
  36. package/dist/studio/routes/errors/route.js.map +1 -1
  37. package/dist/studio/routes/fiberDetail.d.ts +1 -1
  38. package/dist/studio/routes/fiberDetail.js +4 -4
  39. package/dist/studio/routes/fiberDetail.js.map +1 -1
  40. package/dist/studio/routes/fibers/route.d.ts +2 -2
  41. package/dist/studio/routes/fibers/route.js +4 -4
  42. package/dist/studio/routes/fibers/route.js.map +1 -1
  43. package/dist/studio/routes/logs/route.d.ts +1 -1
  44. package/dist/studio/routes/logs/route.d.ts.map +1 -1
  45. package/dist/studio/routes/logs/route.js +2 -2
  46. package/dist/studio/routes/logs/route.js.map +1 -1
  47. package/dist/studio/routes/traceDetail.d.ts +1 -1
  48. package/dist/studio/routes/traceDetail.js +1 -1
  49. package/dist/studio/routes/traceDetail.js.map +1 -1
  50. package/dist/studio/routes/traces/route.d.ts +2 -2
  51. package/dist/studio/routes/traces/route.d.ts.map +1 -1
  52. package/dist/studio/routes/traces/route.js +5 -5
  53. package/dist/studio/routes/traces/route.js.map +1 -1
  54. package/dist/studio/routes/tree.d.ts +8 -8
  55. package/dist/studio/ui/Traces.d.ts +1 -0
  56. package/dist/studio/ui/Traces.d.ts.map +1 -1
  57. package/dist/studio/ui/Traces.js +32 -11
  58. package/dist/studio/ui/Traces.js.map +1 -1
  59. package/package.json +1 -1
  60. package/src/Fetch.ts +2 -2
  61. package/src/Password.ts +130 -0
  62. package/src/RouteSse.ts +4 -4
  63. package/src/_StreamExtra.ts +0 -1
  64. package/src/studio/Studio.ts +6 -5
  65. package/src/studio/StudioErrors.ts +2 -3
  66. package/src/studio/StudioLogger.ts +2 -3
  67. package/src/studio/StudioStore.ts +159 -115
  68. package/src/studio/StudioTracer.ts +11 -5
  69. package/src/studio/routes/errors/route.tsx +3 -1
  70. package/src/studio/routes/fiberDetail.tsx +4 -4
  71. package/src/studio/routes/fibers/route.tsx +4 -4
  72. package/src/studio/routes/logs/route.tsx +3 -1
  73. package/src/studio/routes/traceDetail.tsx +1 -1
  74. package/src/studio/routes/traces/route.tsx +9 -6
  75. package/src/studio/ui/Traces.tsx +41 -13
@@ -114,6 +114,8 @@ export interface StudioMetricSnapshot {
114
114
  export type StudioEvent =
115
115
  | { readonly _tag: "SpanStart"; readonly span: StudioSpan }
116
116
  | { readonly _tag: "SpanEnd"; readonly span: StudioSpan }
117
+ | { readonly _tag: "TraceStart"; readonly traceId: bigint }
118
+ | { readonly _tag: "TraceEnd"; readonly traceId: bigint }
117
119
  | { readonly _tag: "Log"; readonly log: StudioLog }
118
120
  | { readonly _tag: "Error"; readonly error: StudioError }
119
121
  | { readonly _tag: "MetricsSnapshot"; readonly metrics: Array<StudioMetricSnapshot> }
@@ -327,179 +329,221 @@ function deserializeError(row: ErrorRow): StudioError {
327
329
  }
328
330
  }
329
331
 
330
- export function insertSpan(sql: SqlClient.SqlClient, span: StudioSpan) {
331
- return sql`INSERT INTO Span ${sql({
332
- spanId: span.spanId,
333
- traceId: span.traceId,
334
- fiberId: span.fiberId ?? null,
335
- name: span.name,
336
- kind: span.kind,
337
- parentSpanId: span.parentSpanId ?? null,
338
- startTime: span.startTime.toString(),
339
- endTime: span.endTime?.toString() ?? null,
340
- durationMs: span.durationMs ?? null,
341
- status: span.status,
342
- attributes: JSON.stringify(span.attributes),
343
- events: JSON.stringify(serializeBigint(span.events)),
344
- })}`
345
- }
346
-
347
- export function updateSpan(sql: SqlClient.SqlClient, span: StudioSpan) {
348
- return sql`UPDATE Span SET
349
- endTime = ${span.endTime?.toString() ?? null},
350
- durationMs = ${span.durationMs ?? null},
351
- status = ${span.status},
352
- attributes = ${JSON.stringify(span.attributes)},
353
- events = ${JSON.stringify(serializeBigint(span.events))}
354
- WHERE spanId = ${span.spanId}`
355
- }
356
-
357
- export function insertLog(sql: SqlClient.SqlClient, log: StudioLog) {
358
- return sql`INSERT INTO Log ${sql({
359
- id: log.id,
360
- level: log.level,
361
- message: log.message,
362
- fiberId: log.fiberId,
363
- cause: log.cause ?? null,
364
- spans: JSON.stringify(log.spans),
365
- annotations: JSON.stringify(log.annotations),
366
- })}`
367
- }
368
-
369
- export function insertError(sql: SqlClient.SqlClient, error: StudioError) {
370
- return sql`INSERT INTO Error ${sql({
371
- id: error.id,
372
- fiberId: error.fiberId,
373
- interrupted: error.interrupted ? 1 : 0,
374
- prettyPrint: error.prettyPrint,
375
- details: JSON.stringify(error.details),
376
- })}`
332
+ const withSql = <A, E>(
333
+ f: (sql: SqlClient.SqlClient) => Effect.Effect<A, E>,
334
+ ): Effect.Effect<A, E, SqlClient.SqlClient> => Effect.flatMap(SqlClient.SqlClient, f)
335
+
336
+ export function insertSpan(span: StudioSpan) {
337
+ return withSql((sql) =>
338
+ sql`INSERT INTO Span ${sql({
339
+ spanId: span.spanId,
340
+ traceId: span.traceId,
341
+ fiberId: span.fiberId ?? null,
342
+ name: span.name,
343
+ kind: span.kind,
344
+ parentSpanId: span.parentSpanId ?? null,
345
+ startTime: span.startTime.toString(),
346
+ endTime: span.endTime?.toString() ?? null,
347
+ durationMs: span.durationMs ?? null,
348
+ status: span.status,
349
+ attributes: JSON.stringify(span.attributes),
350
+ events: JSON.stringify(serializeBigint(span.events)),
351
+ })}`,
352
+ )
353
+ }
354
+
355
+ export function updateSpan(span: StudioSpan) {
356
+ return withSql(
357
+ (sql) => sql`UPDATE Span SET
358
+ endTime = ${span.endTime?.toString() ?? null},
359
+ durationMs = ${span.durationMs ?? null},
360
+ status = ${span.status},
361
+ attributes = ${JSON.stringify(span.attributes)},
362
+ events = ${JSON.stringify(serializeBigint(span.events))}
363
+ WHERE spanId = ${span.spanId}`,
364
+ )
365
+ }
366
+
367
+ export function insertLog(log: StudioLog) {
368
+ return withSql((sql) =>
369
+ sql`INSERT INTO Log ${sql({
370
+ id: log.id,
371
+ level: log.level,
372
+ message: log.message,
373
+ fiberId: log.fiberId,
374
+ cause: log.cause ?? null,
375
+ spans: JSON.stringify(log.spans),
376
+ annotations: JSON.stringify(log.annotations),
377
+ })}`,
378
+ )
379
+ }
380
+
381
+ export function insertError(error: StudioError) {
382
+ return withSql((sql) =>
383
+ sql`INSERT INTO Error ${sql({
384
+ id: error.id,
385
+ fiberId: error.fiberId,
386
+ interrupted: error.interrupted ? 1 : 0,
387
+ prettyPrint: error.prettyPrint,
388
+ details: JSON.stringify(error.details),
389
+ })}`,
390
+ )
377
391
  }
378
392
 
379
393
  export function upsertFiber(
380
- sql: SqlClient.SqlClient,
381
394
  id: string,
382
395
  parentId: string | undefined,
383
396
  spanName: string | undefined,
384
397
  traceId: bigint | undefined,
385
398
  annotations: Record<string, unknown>,
386
399
  ) {
387
- return sql`INSERT OR REPLACE INTO Fiber ${sql({
388
- id,
389
- parentId: parentId ?? null,
390
- spanName: spanName ?? null,
391
- traceId: traceId ?? null,
392
- annotations: JSON.stringify(annotations),
393
- })}`
394
- }
395
-
396
- export function evict(sql: SqlClient.SqlClient, table: string, capacity: number) {
397
- return Effect.gen(function* () {
398
- const [{ cnt }] = yield* sql<{ cnt: number }>`SELECT count(*) as cnt FROM ${sql(table)}`
399
- if (cnt > capacity) {
400
- const excess = cnt - capacity
401
- yield* sql`DELETE FROM ${sql(table)} WHERE rowid IN (SELECT rowid FROM ${sql(table)} ORDER BY rowid LIMIT ${excess})`
402
- }
403
- })
400
+ return withSql((sql) =>
401
+ sql`INSERT OR REPLACE INTO Fiber ${sql({
402
+ id,
403
+ parentId: parentId ?? null,
404
+ spanName: spanName ?? null,
405
+ traceId: traceId ?? null,
406
+ annotations: JSON.stringify(annotations),
407
+ })}`,
408
+ )
404
409
  }
405
410
 
406
- export function runWrite(effect: Effect.Effect<unknown, SqlClient.SqlError>) {
411
+ export function evict(table: string, capacity: number) {
412
+ return withSql((sql) =>
413
+ Effect.gen(function* () {
414
+ const [{ cnt }] = yield* sql<{ cnt: number }>`SELECT count(*) as cnt FROM ${sql(table)}`
415
+ if (cnt > capacity) {
416
+ const excess = cnt - capacity
417
+ yield* sql`DELETE FROM ${sql(table)} WHERE rowid IN (SELECT rowid FROM ${sql(table)} ORDER BY rowid LIMIT ${excess})`
418
+ }
419
+ }),
420
+ )
421
+ }
422
+
423
+ export function runWrite(effect: Effect.Effect<unknown, SqlClient.SqlError, SqlClient.SqlClient>) {
424
+ const sql = store.sql
425
+ if (!sql) return
407
426
  writeQueue = writeQueue
408
- .then(() => Effect.runPromise(Effect.withTracerEnabled(effect, false)).then(() => undefined))
427
+ .then(() =>
428
+ Effect.runPromise(
429
+ Effect.withTracerEnabled(
430
+ Effect.provideService(effect, SqlClient.SqlClient, sql),
431
+ false,
432
+ ),
433
+ ).then(() => undefined),
434
+ )
409
435
  .catch(() => undefined)
410
436
  }
411
437
 
412
438
  let writeQueue = Promise.resolve<void>(undefined)
413
439
 
414
- const noTrace = <A, E>(effect: Effect.Effect<A, E>): Effect.Effect<A, E> =>
440
+ const noTrace = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
415
441
  Effect.withTracerEnabled(effect, false)
416
442
 
417
- export function allSpans(sql: SqlClient.SqlClient) {
443
+ export function allSpans() {
418
444
  return noTrace(
419
- Effect.map(sql<SpanRow>`SELECT * FROM Span ORDER BY rowid`, (rows) =>
420
- rows.map(deserializeSpan),
445
+ withSql((sql) =>
446
+ Effect.map(sql<SpanRow>`SELECT * FROM Span ORDER BY rowid`, (rows) =>
447
+ rows.map(deserializeSpan),
448
+ ),
421
449
  ),
422
450
  )
423
451
  }
424
452
 
425
- export function allLogs(sql: SqlClient.SqlClient) {
453
+ export function allLogs() {
426
454
  return noTrace(
427
- Effect.map(sql<LogRow>`SELECT * FROM Log ORDER BY rowid`, (rows) => rows.map(deserializeLog)),
455
+ withSql((sql) =>
456
+ Effect.map(sql<LogRow>`SELECT * FROM Log ORDER BY rowid`, (rows) => rows.map(deserializeLog)),
457
+ ),
428
458
  )
429
459
  }
430
460
 
431
- export function allErrors(sql: SqlClient.SqlClient) {
461
+ export function allErrors() {
432
462
  return noTrace(
433
- Effect.map(sql<ErrorRow>`SELECT * FROM Error ORDER BY rowid`, (rows) =>
434
- rows.map(deserializeError),
463
+ withSql((sql) =>
464
+ Effect.map(sql<ErrorRow>`SELECT * FROM Error ORDER BY rowid`, (rows) =>
465
+ rows.map(deserializeError),
466
+ ),
435
467
  ),
436
468
  )
437
469
  }
438
470
 
439
- export function spansByTraceId(sql: SqlClient.SqlClient, traceId: bigint) {
471
+ export function spansByTraceId(traceId: bigint) {
440
472
  return noTrace(
441
- Effect.map(sql<SpanRow>`SELECT * FROM Span WHERE traceId = ${traceId} ORDER BY rowid`, (rows) =>
442
- rows.map(deserializeSpan),
473
+ withSql((sql) =>
474
+ Effect.map(sql<SpanRow>`SELECT * FROM Span WHERE traceId = ${traceId} ORDER BY rowid`, (rows) =>
475
+ rows.map(deserializeSpan),
476
+ ),
443
477
  ),
444
478
  )
445
479
  }
446
480
 
447
- export function spansByFiberId(sql: SqlClient.SqlClient, fiberId: string) {
481
+ export function spansByFiberId(fiberId: string) {
448
482
  return noTrace(
449
- Effect.map(sql<SpanRow>`SELECT * FROM Span WHERE fiberId = ${fiberId} ORDER BY rowid`, (rows) =>
450
- rows.map(deserializeSpan),
483
+ withSql((sql) =>
484
+ Effect.map(sql<SpanRow>`SELECT * FROM Span WHERE fiberId = ${fiberId} ORDER BY rowid`, (rows) =>
485
+ rows.map(deserializeSpan),
486
+ ),
451
487
  ),
452
488
  )
453
489
  }
454
490
 
455
- export function logsByFiberId(sql: SqlClient.SqlClient, fiberId: string) {
491
+ export function logsByFiberId(fiberId: string) {
456
492
  return noTrace(
457
- Effect.map(sql<LogRow>`SELECT * FROM Log WHERE fiberId = ${fiberId} ORDER BY rowid`, (rows) =>
458
- rows.map(deserializeLog),
493
+ withSql((sql) =>
494
+ Effect.map(sql<LogRow>`SELECT * FROM Log WHERE fiberId = ${fiberId} ORDER BY rowid`, (rows) =>
495
+ rows.map(deserializeLog),
496
+ ),
459
497
  ),
460
498
  )
461
499
  }
462
500
 
463
- export function getFiber(sql: SqlClient.SqlClient, fiberId: string) {
501
+ export function getFiber(fiberId: string) {
464
502
  return noTrace(
465
- Effect.map(sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${fiberId}`, (rows) =>
466
- rows.length > 0 ? rows[0] : undefined,
503
+ withSql((sql) =>
504
+ Effect.map(sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${fiberId}`, (rows) =>
505
+ rows.length > 0 ? rows[0] : undefined,
506
+ ),
467
507
  ),
468
508
  )
469
509
  }
470
510
 
471
- export function getParentChain(sql: SqlClient.SqlClient, fiberId: string) {
511
+ export function getParentChain(fiberId: string) {
472
512
  return noTrace(
473
- Effect.gen(function* () {
474
- const chain: Array<string> = []
475
- const visited = new Set<string>()
476
- let current = fiberId
477
- while (true) {
478
- const rows = yield* sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${current}`
479
- if (rows.length === 0 || !rows[0].parentId) break
480
- const parentId = rows[0].parentId
481
- if (visited.has(parentId)) break
482
- chain.push(parentId)
483
- visited.add(parentId)
484
- current = parentId
485
- }
486
- return chain.reverse()
487
- }),
513
+ withSql((sql) =>
514
+ Effect.gen(function* () {
515
+ const chain: Array<string> = []
516
+ const visited = new Set<string>()
517
+ let current = fiberId
518
+ while (true) {
519
+ const rows = yield* sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${current}`
520
+ if (rows.length === 0 || !rows[0].parentId) break
521
+ const parentId = rows[0].parentId
522
+ if (visited.has(parentId)) break
523
+ chain.push(parentId)
524
+ visited.add(parentId)
525
+ current = parentId
526
+ }
527
+ return chain.reverse()
528
+ }),
529
+ ),
488
530
  )
489
531
  }
490
532
 
491
- export function getFiberContext(sql: SqlClient.SqlClient, fiberId: string) {
533
+ export function getFiberContext(fiberId: string) {
492
534
  return noTrace(
493
- Effect.map(
494
- sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${fiberId}`,
495
- (rows): FiberContext | undefined =>
496
- rows.length > 0
497
- ? {
498
- spanName: rows[0].spanName ?? undefined,
499
- traceId: rows[0].traceId != null ? BigInt(rows[0].traceId) : undefined,
500
- annotations: JSON.parse(rows[0].annotations),
501
- }
502
- : undefined,
535
+ withSql((sql) =>
536
+ Effect.map(
537
+ sql<FiberRow>`SELECT * FROM Fiber WHERE id = ${fiberId}`,
538
+ (rows): FiberContext | undefined =>
539
+ rows.length > 0
540
+ ? {
541
+ spanName: rows[0].spanName ?? undefined,
542
+ traceId: rows[0].traceId != null ? BigInt(rows[0].traceId) : undefined,
543
+ annotations: JSON.parse(rows[0].annotations),
544
+ }
545
+ : undefined,
546
+ ),
503
547
  ),
504
548
  )
505
549
  }
@@ -62,11 +62,14 @@ const make = (store: StudioStore.StudioStoreShape): Tracer.Tracer =>
62
62
 
63
63
  StudioStore.runWrite(
64
64
  Effect.zipRight(
65
- StudioStore.insertSpan(store.sql, studioSpan),
66
- StudioStore.evict(store.sql, "Span", store.spanCapacity),
65
+ StudioStore.insertSpan(studioSpan),
66
+ StudioStore.evict("Span", store.spanCapacity),
67
67
  ),
68
68
  )
69
69
  publish(store, { _tag: "SpanStart", span: studioSpan })
70
+ if (parentSpanId === undefined) {
71
+ publish(store, { _tag: "TraceStart", traceId })
72
+ }
70
73
 
71
74
  const attrs = new Map<string, unknown>(Object.entries(attributes))
72
75
  const spanLinks = [...links]
@@ -97,17 +100,20 @@ const make = (store: StudioStore.StudioStoreShape): Tracer.Tracer =>
97
100
  studioSpan.endTime = endTime
98
101
  studioSpan.durationMs = Number(endTime - studioSpan.startTime) / 1_000_000
99
102
  studioSpan.status = Exit.isSuccess(exit) ? "ok" : "error"
100
- StudioStore.runWrite(StudioStore.updateSpan(store.sql, studioSpan))
103
+ StudioStore.runWrite(StudioStore.updateSpan(studioSpan))
101
104
  publish(store, { _tag: "SpanEnd", span: studioSpan })
105
+ if (parentSpanId === undefined) {
106
+ publish(store, { _tag: "TraceEnd", traceId })
107
+ }
102
108
  },
103
109
  attribute(key, value) {
104
110
  attrs.set(key, value)
105
111
  ;(studioSpan.attributes as Record<string, unknown>)[key] = value
106
- StudioStore.runWrite(StudioStore.updateSpan(store.sql, studioSpan))
112
+ StudioStore.runWrite(StudioStore.updateSpan(studioSpan))
107
113
  },
108
114
  event(name, startTime, attributes) {
109
115
  studioSpan.events.push({ name, startTime, attributes })
110
- StudioStore.runWrite(StudioStore.updateSpan(store.sql, studioSpan))
116
+ StudioStore.runWrite(StudioStore.updateSpan(studioSpan))
111
117
  },
112
118
  addLinks(newLinks) {
113
119
  spanLinks.push(...newLinks)
@@ -12,7 +12,7 @@ export default Route.get(
12
12
  const url = new URL(ctx.request.url)
13
13
  const search = url.searchParams.get("errorSearch") || ""
14
14
  const tag = url.searchParams.get("errorTag") || ""
15
- const allErrors = yield* StudioStore.allErrors(StudioStore.store.sql)
15
+ const allErrors = yield* StudioStore.allErrors()
16
16
  const tagSet = new Set<string>()
17
17
  for (const error of allErrors) {
18
18
  for (const d of error.details) {
@@ -67,6 +67,8 @@ export default Route.get(
67
67
  <Errors.ErrorLine error={e} />
68
68
  ))}
69
69
  </div>
70
+
71
+ <div data-init={`@get('${prefix}/errors')`} />
70
72
  </form>
71
73
  </Shell.Shell>
72
74
  )
@@ -12,8 +12,8 @@ export default Route.get(
12
12
  const fiberId = ctx.pathParams.id
13
13
  const fiberName = fiberId.startsWith("#") ? fiberId : `#${fiberId}`
14
14
 
15
- const fiberLogs = yield* StudioStore.logsByFiberId(StudioStore.store.sql, fiberName)
16
- const fiberSpans = yield* StudioStore.spansByFiberId(StudioStore.store.sql, fiberName)
15
+ const fiberLogs = yield* StudioStore.logsByFiberId(fiberName)
16
+ const fiberSpans = yield* StudioStore.spansByFiberId(fiberName)
17
17
 
18
18
  const fiberNum = parseInt(fiberName.slice(1), 10)
19
19
  const counter = StudioStore.fiberIdCounter()
@@ -30,8 +30,8 @@ export default Route.get(
30
30
  else if (fiberNum < counter) alive = "dead"
31
31
  }
32
32
 
33
- const parents = yield* StudioStore.getParentChain(StudioStore.store.sql, fiberName)
34
- const fiberContext = yield* StudioStore.getFiberContext(StudioStore.store.sql, fiberName)
33
+ const parents = yield* StudioStore.getParentChain(fiberName)
34
+ const fiberContext = yield* StudioStore.getFiberContext(fiberName)
35
35
 
36
36
  return (
37
37
  <Shell.Shell prefix={StudioStore.store.prefix} active="fibers">
@@ -8,8 +8,8 @@ import * as Shell from "../../ui/Shell.tsx"
8
8
 
9
9
  export default Route.get(
10
10
  Route.html(function* () {
11
- const logs = yield* StudioStore.allLogs(StudioStore.store.sql)
12
- const spans = yield* StudioStore.allSpans(StudioStore.store.sql)
11
+ const logs = yield* StudioStore.allLogs()
12
+ const spans = yield* StudioStore.allSpans()
13
13
  const fibers = Fibers.collectFibers(logs, spans)
14
14
  return (
15
15
  <Shell.Shell prefix={StudioStore.store.prefix} active="fibers">
@@ -28,8 +28,8 @@ export default Route.get(
28
28
  Stream.filter((e) => e._tag === "SpanStart" || e._tag === "SpanEnd" || e._tag === "Log"),
29
29
  Stream.mapEffect(() =>
30
30
  Effect.gen(function* () {
31
- const logs = yield* StudioStore.allLogs(StudioStore.store.sql)
32
- const spans = yield* StudioStore.allSpans(StudioStore.store.sql)
31
+ const logs = yield* StudioStore.allLogs()
32
+ const spans = yield* StudioStore.allSpans()
33
33
  const fibers = Fibers.collectFibers(logs, spans)
34
34
  const html = Html.renderToString(
35
35
  <Fibers.FiberList fibers={fibers} prefix={StudioStore.store.prefix} />,
@@ -12,7 +12,7 @@ export default Route.get(
12
12
  const url = new URL(ctx.request.url)
13
13
  const level = url.searchParams.get("logLevel") || ""
14
14
  const search = url.searchParams.get("logSearch") || ""
15
- let logs = yield* StudioStore.allLogs(StudioStore.store.sql)
15
+ let logs = yield* StudioStore.allLogs()
16
16
  if (level) logs = logs.filter((l) => l.level === level)
17
17
  if (search) {
18
18
  const lower = search.toLowerCase()
@@ -53,6 +53,8 @@ export default Route.get(
53
53
  <Logs.LogLine log={l} />
54
54
  ))}
55
55
  </div>
56
+
57
+ <div data-init={`@get('${prefix}/logs')`} />
56
58
  </form>
57
59
  </Shell.Shell>
58
60
  )
@@ -18,7 +18,7 @@ export default Route.get(
18
18
  </Shell.Shell>
19
19
  )
20
20
  }
21
- const spans = yield* StudioStore.spansByTraceId(StudioStore.store.sql, traceId)
21
+ const spans = yield* StudioStore.spansByTraceId(traceId)
22
22
 
23
23
  return (
24
24
  <Shell.Shell prefix={StudioStore.store.prefix} active="traces">
@@ -12,7 +12,7 @@ export default Route.get(
12
12
  Route.html(function* (ctx) {
13
13
  const url = new URL(ctx.request.url)
14
14
  const search = url.searchParams.get("traceSearch") || ""
15
- const allSpans = yield* StudioStore.allSpans(StudioStore.store.sql)
15
+ const allSpans = yield* StudioStore.allSpans()
16
16
  const names = Array.from(new Set(allSpans.map((s) => s.name))).sort()
17
17
  let spans = allSpans
18
18
  if (search) {
@@ -52,14 +52,17 @@ export default Route.get(
52
52
  }),
53
53
  Route.sse(
54
54
  Stream.fromPubSub(StudioStore.store.events).pipe(
55
- Stream.filter((e) => e._tag === "SpanStart" || e._tag === "SpanEnd"),
56
- Stream.mapEffect(() =>
55
+ Stream.filter((e) => e._tag === "TraceEnd"),
56
+ Stream.mapEffect((e) =>
57
57
  Effect.gen(function* () {
58
- const spans = yield* StudioStore.allSpans(StudioStore.store.sql)
59
- const html = Html.renderToString(<Traces.TraceGroups spans={spans} />).replace(/\n/g, "")
58
+ const traceSpans = yield* StudioStore.spansByTraceId(e.traceId)
59
+ const traceHtml = Html.renderToString(
60
+ <Traces.TraceGroup id={e.traceId} spans={traceSpans} />,
61
+ )
62
+
60
63
  return {
61
64
  event: "datastar-patch-elements",
62
- data: `selector #traces-container\nmode inner\nelements ${html}`,
65
+ data: `selector .tl-header\nmode after\nelements ${traceHtml}`,
63
66
  }
64
67
  }),
65
68
  ),
@@ -47,6 +47,20 @@ interface TreeSpan {
47
47
  ancestorHasNextSibling: Array<boolean>
48
48
  }
49
49
 
50
+ function sortByStartTime(a: StudioStore.StudioSpan, b: StudioStore.StudioSpan): number {
51
+ if (a.startTime < b.startTime) return -1
52
+ if (a.startTime > b.startTime) return 1
53
+ return 0
54
+ }
55
+
56
+ function pickRootSpan(spans: Array<StudioStore.StudioSpan>): StudioStore.StudioSpan {
57
+ const spanIds = new Set(spans.map((span) => span.spanId))
58
+ return (
59
+ spans.find((span) => !span.parentSpanId || !spanIds.has(span.parentSpanId)) ??
60
+ spans.slice().sort(sortByStartTime)[0]
61
+ )
62
+ }
63
+
50
64
  function buildSpanTree(spans: Array<StudioStore.StudioSpan>): Array<TreeSpan> {
51
65
  const byId = new Map<bigint, StudioStore.StudioSpan>()
52
66
  const childrenOf = new Map<bigint, Array<StudioStore.StudioSpan>>()
@@ -69,23 +83,31 @@ function buildSpanTree(spans: Array<StudioStore.StudioSpan>): Array<TreeSpan> {
69
83
  }
70
84
  }
71
85
 
72
- const sortByStart = (a: StudioStore.StudioSpan, b: StudioStore.StudioSpan) =>
73
- Number(a.startTime - b.startTime)
74
-
75
- roots.sort(sortByStart)
86
+ roots.sort(sortByStartTime)
76
87
  for (const children of childrenOf.values()) {
77
- children.sort(sortByStart)
88
+ children.sort(sortByStartTime)
78
89
  }
79
90
 
80
91
  const result: Array<TreeSpan> = []
92
+ const visited = new Set<bigint>()
81
93
 
82
94
  function walk(
83
95
  span: StudioStore.StudioSpan,
84
96
  depth: number,
85
97
  isLast: boolean,
86
98
  ancestors: Array<boolean>,
99
+ lineage: Set<bigint>,
87
100
  ) {
88
- const children = childrenOf.get(span.spanId) ?? []
101
+ if (lineage.has(span.spanId) || visited.has(span.spanId)) return
102
+
103
+ const nextLineage = new Set(lineage)
104
+ nextLineage.add(span.spanId)
105
+
106
+ const children = (childrenOf.get(span.spanId) ?? []).filter(
107
+ (child) => !nextLineage.has(child.spanId) && !visited.has(child.spanId),
108
+ )
109
+
110
+ visited.add(span.spanId)
89
111
  result.push({
90
112
  span,
91
113
  depth,
@@ -94,12 +116,18 @@ function buildSpanTree(spans: Array<StudioStore.StudioSpan>): Array<TreeSpan> {
94
116
  ancestorHasNextSibling: [...ancestors],
95
117
  })
96
118
  for (let i = 0; i < children.length; i++) {
97
- walk(children[i], depth + 1, i === children.length - 1, [...ancestors, !isLast])
119
+ walk(children[i], depth + 1, i === children.length - 1, [...ancestors, !isLast], nextLineage)
98
120
  }
99
121
  }
100
122
 
101
123
  for (let i = 0; i < roots.length; i++) {
102
- walk(roots[i], 0, i === roots.length - 1, [])
124
+ walk(roots[i], 0, i === roots.length - 1, [], new Set())
125
+ }
126
+
127
+ const remaining = spans.filter((span) => !visited.has(span.spanId)).sort(sortByStartTime)
128
+
129
+ for (let i = 0; i < remaining.length; i++) {
130
+ walk(remaining[i], 0, i === remaining.length - 1, [], new Set())
103
131
  }
104
132
 
105
133
  return result
@@ -225,17 +253,17 @@ export function groupByTraceId(
225
253
  return groups
226
254
  }
227
255
 
228
- export function TraceGroup(options: { spans: Array<StudioStore.StudioSpan> }) {
256
+ export function TraceGroup(options: { id?: bigint; spans: Array<StudioStore.StudioSpan> }) {
229
257
  if (options.spans.length === 0) return null
230
- const root = options.spans.find((s) => !s.parentSpanId) ?? options.spans[0]
231
- const traceId = root.traceId
258
+ const root = pickRootSpan(options.spans)
259
+ const traceId = options.id ?? root.traceId
232
260
  const totalMs = root.durationMs ?? 0
233
261
  const rootStart = root.startTime
234
262
  const hasError = options.spans.some((s) => s.status === "error")
235
263
  const status = hasError ? "error" : root.status
236
264
 
237
265
  return (
238
- <details class="tl-row">
266
+ <details id={`trace-${traceId}`} class="tl-row">
239
267
  <summary class="tl-summary tl-cols">
240
268
  <span class="tl-cell tl-cell-status">
241
269
  <span
@@ -324,7 +352,7 @@ export function TraceDetail(options: { prefix: string; spans: Array<StudioStore.
324
352
  if (options.spans.length === 0) {
325
353
  return <div class="empty">Trace not found</div>
326
354
  }
327
- const root = options.spans.find((s) => !s.parentSpanId) ?? options.spans[0]
355
+ const root = pickRootSpan(options.spans)
328
356
  const traceId = root.traceId
329
357
  const totalMs = root.durationMs ?? 0
330
358
  const rootStart = root.startTime