From b6f6e6d8f23467bf0fcdc1ff35863b0b6f873036 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Tue, 6 May 2025 18:01:40 +1000 Subject: [PATCH 1/3] chore: cleaned up span format --- src/tracer/Tracer.ts | 24 +++++++++++------------- src/tracer/types.ts | 30 ++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/tracer/Tracer.ts b/src/tracer/Tracer.ts index 71d81d0..fe7f81a 100644 --- a/src/tracer/Tracer.ts +++ b/src/tracer/Tracer.ts @@ -1,19 +1,19 @@ -import type { SpanEvent } from './types.js'; -import { IdSortable, utils as idUtils } from '@matrixai/id'; +import type { SpanEvent, SpanId } from './types.js'; +import { IdSortable } from '@matrixai/id'; class Tracer { - protected activeSpans: Map = new Map(); + protected activeSpans: Map = new Map(); protected queue: Array = []; protected resolveWaitChunksP: (() => void) | undefined; protected ended: boolean = false; protected idGen = new IdSortable(); - protected nextId(): string { + protected nextId(): SpanId { const result = this.idGen.next(); if (result.done || result.value == null) { throw new Error('Unexpected end of id generator'); } - return idUtils.toMultibase(result.value, 'base64'); + return result.value.toMultibase('base32hex'); } protected queueSpanEvent(evt: SpanEvent) { @@ -21,28 +21,26 @@ class Tracer { if (this.resolveWaitChunksP != null) this.resolveWaitChunksP(); } - public startSpan(name: string, parentSpanId?: string): string { + public startSpan(name: string, parentSpanId?: SpanId): SpanId { const spanId = this.nextId(); this.activeSpans.set(spanId, name); this.queueSpanEvent({ type: 'start', - id: this.nextId(), - spanId: spanId, - parentSpanId: parentSpanId, + id: spanId, + parentId: parentSpanId, name: name, }); return spanId; } - public endSpan(spanId: string): void { + public endSpan(spanId: SpanId): void { const name = this.activeSpans.get(spanId); if (!name) return; this.activeSpans.delete(spanId); this.queueSpanEvent({ - type: 'end', + type: 'stop', id: this.nextId(), - spanId: spanId, - name: name, + startId: spanId, }); } diff --git a/src/tracer/types.ts b/src/tracer/types.ts index 74b75d4..15c8c7b 100644 --- a/src/tracer/types.ts +++ b/src/tracer/types.ts @@ -1,12 +1,26 @@ -type Span = { - spanId: string; - name: string; - parentSpanId?: string; +type SpanId = string; + +/** + * A span is a virtual concept, not an actual object. A span is made up of + * multiple events. Each event gets its own ID. Each start event must be + * associated with a stop event, otherwise the span is considered to be + * ongoing. + */ + +type SpanStart = { + type: 'start'; + id: SpanId; + parentId?: SpanId; + name?: string; }; -type SpanEvent = Span & { - type: 'start' | 'end'; - id: string; +type SpanStop = { + type: 'stop'; + id: SpanId; + startId: SpanId; + parentId?: SpanId; }; -export type { Span, SpanEvent }; +type SpanEvent = SpanStart | SpanStop; + +export type { SpanId, SpanEvent }; From ea7fd72d5d180ab2c172fc01c18a7ddf3dbdb534 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Tue, 6 May 2025 18:24:26 +1000 Subject: [PATCH 2/3] chore: made `SpanId` opaque to prevent type coersion --- src/tracer/Tracer.ts | 4 ++-- src/tracer/types.ts | 2 +- tests/asciinemaTest.ts | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/tracer/Tracer.ts b/src/tracer/Tracer.ts index fe7f81a..c7766a0 100644 --- a/src/tracer/Tracer.ts +++ b/src/tracer/Tracer.ts @@ -13,7 +13,7 @@ class Tracer { if (result.done || result.value == null) { throw new Error('Unexpected end of id generator'); } - return result.value.toMultibase('base32hex'); + return result.value.toMultibase('base32hex') as SpanId; } protected queueSpanEvent(evt: SpanEvent) { @@ -46,7 +46,7 @@ class Tracer { public async traced( name: string, - parentSpanId: string | undefined, + parentSpanId: SpanId | undefined, fn: () => T | Promise, ): Promise { const fnProm = async () => { diff --git a/src/tracer/types.ts b/src/tracer/types.ts index 15c8c7b..ab8550f 100644 --- a/src/tracer/types.ts +++ b/src/tracer/types.ts @@ -1,4 +1,4 @@ -type SpanId = string; +type SpanId = string & { readonly brand: unique symbol }; /** * A span is a virtual concept, not an actual object. A span is made up of diff --git a/tests/asciinemaTest.ts b/tests/asciinemaTest.ts index 88b6e2c..2b268ca 100644 --- a/tests/asciinemaTest.ts +++ b/tests/asciinemaTest.ts @@ -1,10 +1,11 @@ +import type { SpanId } from '#tracer/index.js'; import fs from 'fs'; import * as fc from 'fast-check'; import tracer from '#tracer/index.js'; let parentIndex = 0; let step = 0; -let nestedIds: Array = []; +let nestedIds: Array = []; type Flags = { hasForkA: boolean; @@ -18,9 +19,9 @@ type Flags = { }; const current: { - parentId?: string; - forkAId?: string; - forkBId?: string; + parentId?: SpanId; + forkAId?: SpanId; + forkBId?: SpanId; flags: Flags; } = { flags: { From 85cdaaa2c7a82591564a0764685b09b613a2ddaf Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 7 May 2025 12:24:30 +1000 Subject: [PATCH 3/3] chore: added option to disable tracing --- src/tracer/Tracer.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tracer/Tracer.ts b/src/tracer/Tracer.ts index c7766a0..a82233b 100644 --- a/src/tracer/Tracer.ts +++ b/src/tracer/Tracer.ts @@ -7,6 +7,7 @@ class Tracer { protected resolveWaitChunksP: (() => void) | undefined; protected ended: boolean = false; protected idGen = new IdSortable(); + protected shouldTrace = false; protected nextId(): SpanId { const result = this.idGen.next(); @@ -16,7 +17,8 @@ class Tracer { return result.value.toMultibase('base32hex') as SpanId; } - protected queueSpanEvent(evt: SpanEvent) { + protected queueSpanEvent(evt: SpanEvent): void { + if (!this.shouldTrace) return; this.queue.push(evt); if (this.resolveWaitChunksP != null) this.resolveWaitChunksP(); } @@ -75,6 +77,13 @@ class Tracer { yield value; } } + + public disableTracing(): void { + this.shouldTrace = false; + this.ended = true; + this.resolveWaitChunksP?.(); + this.queue = []; + } } export default Tracer;