import type { ServerTraceDiagnosticsErrorKind, ServerTraceDiagnosticsFailureSummary, ServerTraceDiagnosticsLogEvent, ServerTraceDiagnosticsRecentFailure, ServerTraceDiagnosticsResult, ServerTraceDiagnosticsSpanOccurrence, ServerTraceDiagnosticsSpanSummary,} from "@t3tools/contracts";import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PlatformError from "effect/PlatformError";import * as Result from "effect/Result";import * as Schema from "effect/Schema"; interface TraceRecordLike { readonly name?: unknown; readonly traceId?: unknown; readonly spanId?: unknown; readonly startTimeUnixNano?: unknown; readonly endTimeUnixNano?: unknown; readonly durationMs?: unknown; readonly exit?: unknown; readonly events?: unknown;} interface TraceEventLike { readonly name?: unknown; readonly timeUnixNano?: unknown; readonly attributes?: unknown;} export interface TraceDiagnosticsOptions { readonly traceFilePath: string; readonly maxFiles: number; readonly slowSpanThresholdMs?: number; readonly readAt?: DateTime.Utc;} export class TraceFileReadError extends Schema.TaggedErrorClass<TraceFileReadError>()( "TraceFileReadError", { traceFilePath: Schema.String, causeTag: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to read local trace file '${this.traceFilePath}'.`; }} export class TraceDiagnostics extends Context.Service< TraceDiagnostics, { readonly read: ( options: TraceDiagnosticsOptions, ) => Effect.Effect<ServerTraceDiagnosticsResult>; }>()("t3/diagnostics/TraceDiagnostics") {} interface TraceDiagnosticsInput { readonly traceFilePath: string; readonly files: ReadonlyArray<{ readonly path: string; readonly text: string }>; readonly scannedFilePaths?: ReadonlyArray<string>; readonly slowSpanThresholdMs?: number; readonly readAt: DateTime.Utc; readonly error?: TraceDiagnosticsErrorSummary; readonly partialFailure?: boolean;} interface TraceDiagnosticsErrorSummary { readonly kind: ServerTraceDiagnosticsErrorKind; readonly message: string;} const DEFAULT_SLOW_SPAN_THRESHOLD_MS = 1_000;const TOP_LIMIT = 10;const RECENT_LIMIT = 20;function toRotatedTracePaths(traceFilePath: string, maxFiles: number): ReadonlyArray<string> { const backupCount = Math.max(0, Math.floor(maxFiles)); const backups = Array.from( { length: backupCount }, (_, index) => `${traceFilePath}.${backupCount - index}`, ); return [...backups, traceFilePath];} function isRecordObject(value: unknown): value is TraceRecordLike { return typeof value === "object" && value !== null;} function toStringValue(value: unknown): Option.Option<string> { return typeof value === "string" && value.trim().length > 0 ? Option.some(value) : Option.none();} function toNumberValue(value: unknown): Option.Option<number> { return typeof value === "number" && Number.isFinite(value) ? Option.some(value) : Option.none();} function unixNanoToDateTime(value: unknown): Option.Option<DateTime.Utc> { const text = toStringValue(value); if (Option.isNone(text)) return Option.none(); try { const millis = Number(BigInt(text.value) / 1_000_000n); return DateTime.make(millis); } catch { return Option.none(); }} function readExitTag(exit: unknown): Option.Option<string> { if (!isRecordObject(exit) || !("_tag" in exit)) return Option.none(); return toStringValue(exit._tag);} function readExitCause(exit: unknown): string { if (!isRecordObject(exit) || !("cause" in exit)) return "Failure"; return Option.getOrElse( Option.map(toStringValue(exit.cause), (cause) => cause.trim()), () => "Failure", );} function isTraceEvent(value: unknown): value is TraceEventLike { return typeof value === "object" && value !== null;} function readEventAttributes(event: TraceEventLike): Readonly<Record<string, unknown>> { return typeof event.attributes === "object" && event.attributes !== null ? (event.attributes as Readonly<Record<string, unknown>>) : {};} function makeEmptyDiagnostics(input: { readonly traceFilePath: string; readonly scannedFilePaths: ReadonlyArray<string>; readonly readAt: DateTime.Utc; readonly slowSpanThresholdMs: number; readonly error?: TraceDiagnosticsErrorSummary; readonly partialFailure?: boolean;}): ServerTraceDiagnosticsResult { return { traceFilePath: input.traceFilePath, scannedFilePaths: [...input.scannedFilePaths], readAt: input.readAt, recordCount: 0, parseErrorCount: 0, firstSpanAt: Option.none(), lastSpanAt: Option.none(), failureCount: 0, interruptionCount: 0, slowSpanThresholdMs: input.slowSpanThresholdMs, slowSpanCount: 0, logLevelCounts: {}, topSpansByCount: [], slowestSpans: [], commonFailures: [], latestFailures: [], latestWarningAndErrorLogs: [], partialFailure: input.partialFailure ? Option.some(true) : Option.none(), error: Option.fromNullishOr(input.error), };} function isNotFoundError(error: PlatformError.PlatformError): boolean { return error.reason._tag === "NotFound";} function insertBoundedSlowestSpan( slowestSpans: ServerTraceDiagnosticsSpanOccurrence[], span: ServerTraceDiagnosticsSpanOccurrence,): void { if ( slowestSpans.length >= TOP_LIMIT && span.durationMs <= slowestSpans[slowestSpans.length - 1]!.durationMs ) { return; } slowestSpans.push(span); slowestSpans.sort((left, right) => right.durationMs - left.durationMs); if (slowestSpans.length > TOP_LIMIT) { slowestSpans.length = TOP_LIMIT; }} function earliestDateTime( current: Option.Option<DateTime.Utc>, candidate: DateTime.Utc,): Option.Option<DateTime.Utc> { return Option.match(current, { onNone: () => Option.some(candidate), onSome: (value) => (DateTime.isLessThan(candidate, value) ? Option.some(candidate) : current), });} function latestDateTime( current: Option.Option<DateTime.Utc>, candidate: DateTime.Utc,): Option.Option<DateTime.Utc> { return Option.match(current, { onNone: () => Option.some(candidate), onSome: (value) => DateTime.isGreaterThan(candidate, value) ? Option.some(candidate) : current, });} export function aggregateTraceDiagnostics( input: TraceDiagnosticsInput,): ServerTraceDiagnosticsResult { const readAt = input.readAt; const slowSpanThresholdMs = input.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS; const scannedFilePaths = input.scannedFilePaths ?? input.files.map((file) => file.path); if (input.files.length === 0) { return makeEmptyDiagnostics({ traceFilePath: input.traceFilePath, scannedFilePaths, readAt, slowSpanThresholdMs, error: input.error ?? { kind: "trace-file-not-found", message: "No local trace files were found.", }, ...(input.partialFailure ? { partialFailure: true } : {}), }); } let parseErrorCount = 0; let recordCount = 0; let failureCount = 0; let interruptionCount = 0; let slowSpanCount = 0; let firstSpanAt: Option.Option<DateTime.Utc> = Option.none(); let lastSpanAt: Option.Option<DateTime.Utc> = Option.none(); const spansByName = new Map< string, { count: number; failureCount: number; totalDurationMs: number; maxDurationMs: number } >(); const failuresByKey = new Map<string, ServerTraceDiagnosticsFailureSummary>(); const latestFailures: ServerTraceDiagnosticsRecentFailure[] = []; const slowestSpans: ServerTraceDiagnosticsSpanOccurrence[] = []; const latestWarningAndErrorLogs: ServerTraceDiagnosticsLogEvent[] = []; const logLevelCounts: Record<string, number> = {}; for (const file of input.files) { const lines = file.text.split(/\r?\n/); for (const line of lines) { if (line.trim().length === 0) continue; let parsed: unknown; try { parsed = JSON.parse(line); } catch { parseErrorCount += 1; continue; } if (!isRecordObject(parsed)) { parseErrorCount += 1; continue; } const name = toStringValue(parsed.name); const traceId = toStringValue(parsed.traceId); const spanId = toStringValue(parsed.spanId); const durationMs = toNumberValue(parsed.durationMs); const endedAt = unixNanoToDateTime(parsed.endTimeUnixNano); const startedAt = unixNanoToDateTime(parsed.startTimeUnixNano); const spanFields = Option.all({ name, traceId, spanId, durationMs, endedAt }); if (Option.isNone(spanFields)) { parseErrorCount += 1; continue; } const { name: spanName, traceId: spanTraceId, spanId: spanSpanId, durationMs: spanDurationMs, endedAt: spanEndedAt, } = spanFields.value; recordCount += 1; if (Option.isSome(startedAt)) { firstSpanAt = earliestDateTime(firstSpanAt, startedAt.value); } lastSpanAt = latestDateTime(lastSpanAt, spanEndedAt); const exitTag = readExitTag(parsed.exit); const isFailure = Option.isSome(exitTag) && exitTag.value === "Failure"; const isInterrupted = Option.isSome(exitTag) && exitTag.value === "Interrupted"; if (isFailure) failureCount += 1; if (isInterrupted) interruptionCount += 1; const spanSummary = spansByName.get(spanName) ?? { count: 0, failureCount: 0, totalDurationMs: 0, maxDurationMs: 0, }; spanSummary.count += 1; spanSummary.totalDurationMs += spanDurationMs; spanSummary.maxDurationMs = Math.max(spanSummary.maxDurationMs, spanDurationMs); if (isFailure) spanSummary.failureCount += 1; spansByName.set(spanName, spanSummary); const spanItem = { name: spanName, durationMs: spanDurationMs, endedAt: spanEndedAt, traceId: spanTraceId, spanId: spanSpanId, }; if (spanDurationMs >= slowSpanThresholdMs) { slowSpanCount += 1; } insertBoundedSlowestSpan(slowestSpans, spanItem); if (isFailure) { const cause = readExitCause(parsed.exit); latestFailures.push({ ...spanItem, cause }); const failureKey = `${spanName}\0${cause}`; const existing = failuresByKey.get(failureKey); const isLatestFailure = !existing || DateTime.isGreaterThan(spanEndedAt, existing.lastSeenAt); failuresByKey.set(failureKey, { name: spanName, cause, count: (existing?.count ?? 0) + 1, lastSeenAt: isLatestFailure ? spanEndedAt : existing!.lastSeenAt, traceId: isLatestFailure ? spanTraceId : existing!.traceId, spanId: isLatestFailure ? spanSpanId : existing!.spanId, }); } if (Array.isArray(parsed.events)) { for (const rawEvent of parsed.events) { if (!isTraceEvent(rawEvent)) continue; const attributes = readEventAttributes(rawEvent); const level = toStringValue(attributes["effect.logLevel"]); if (Option.isNone(level)) continue; logLevelCounts[level.value] = (logLevelCounts[level.value] ?? 0) + 1; const normalizedLevel = level.value.toLowerCase(); if ( normalizedLevel !== "warning" && normalizedLevel !== "warn" && normalizedLevel !== "error" && normalizedLevel !== "fatal" ) { continue; } const seenAt = Option.getOrElse( unixNanoToDateTime(rawEvent.timeUnixNano), () => spanEndedAt, ); const message = Option.getOrElse( Option.map(toStringValue(rawEvent.name), (eventName) => eventName.trim()), () => "Log event", ); latestWarningAndErrorLogs.push({ spanName, level: level.value, message, seenAt, traceId: spanTraceId, spanId: spanSpanId, }); } } } } const topSpansByCount: ServerTraceDiagnosticsSpanSummary[] = [...spansByName.entries()] .map(([name, span]) => ({ name, count: span.count, failureCount: span.failureCount, totalDurationMs: span.totalDurationMs, averageDurationMs: span.count > 0 ? span.totalDurationMs / span.count : 0, maxDurationMs: span.maxDurationMs, })) .toSorted((left, right) => right.count - left.count || right.maxDurationMs - left.maxDurationMs) .slice(0, TOP_LIMIT); return { traceFilePath: input.traceFilePath, scannedFilePaths, readAt, recordCount, parseErrorCount, firstSpanAt, lastSpanAt, failureCount, interruptionCount, slowSpanThresholdMs, slowSpanCount, logLevelCounts, topSpansByCount, slowestSpans, commonFailures: [...failuresByKey.values()] .toSorted( (left, right) => right.count - left.count || DateTime.toEpochMillis(right.lastSeenAt) - DateTime.toEpochMillis(left.lastSeenAt), ) .slice(0, TOP_LIMIT), latestFailures: latestFailures .toSorted( (left, right) => DateTime.toEpochMillis(right.endedAt) - DateTime.toEpochMillis(left.endedAt), ) .slice(0, RECENT_LIMIT), latestWarningAndErrorLogs: latestWarningAndErrorLogs .toSorted( (left, right) => DateTime.toEpochMillis(right.seenAt) - DateTime.toEpochMillis(left.seenAt), ) .slice(0, RECENT_LIMIT), partialFailure: input.partialFailure ? Option.some(true) : Option.none(), error: Option.fromNullishOr(input.error), };} type TraceFileReadResult = | { readonly _tag: "Loaded"; readonly path: string; readonly text: string } | { readonly _tag: "Missing"; readonly path: string }; function readTraceFile( fileSystem: FileSystem.FileSystem, path: string,): Effect.Effect<TraceFileReadResult, TraceFileReadError> { return fileSystem.readFileString(path).pipe( Effect.map((text): TraceFileReadResult => ({ _tag: "Loaded", path, text })), Effect.catchTags({ PlatformError: (cause) => isNotFoundError(cause) ? Effect.succeed<TraceFileReadResult>({ _tag: "Missing", path }) : Effect.fail( new TraceFileReadError({ traceFilePath: path, causeTag: cause.reason._tag, cause, }), ), }), );} export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const read: TraceDiagnostics["Service"]["read"] = Effect.fn("TraceDiagnostics.read")( function* (options) { const readAt = options.readAt ?? (yield* DateTime.now); const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS; const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles); const results = yield* Effect.all( paths.map((path) => readTraceFile(fileSystem, path).pipe( Effect.tapError((cause) => Effect.logWarning("Failed to read local trace file.").pipe( Effect.annotateLogs({ traceFilePath: cause.traceFilePath, errorTag: cause._tag, causeTag: cause.causeTag, }), ), ), Effect.result, ), ), { concurrency: 1, }, ); const files = results.flatMap((result) => Result.isSuccess(result) && result.success._tag === "Loaded" ? [{ path: result.success.path, text: result.success.text }] : [], ); const readFailure = results.find(Result.isFailure); const readFailureError = readFailure ? ({ kind: "trace-file-read-failed", message: readFailure.failure.message, } satisfies TraceDiagnosticsErrorSummary) : undefined; if (files.length === 0) { return makeEmptyDiagnostics({ traceFilePath: options.traceFilePath, scannedFilePaths: paths,Lines 1–500 of 535. Load more
import type { ServerTraceDiagnosticsErrorKind, ServerTraceDiagnosticsFailureSummary, ServerTraceDiagnosticsLogEvent, ServerTraceDiagnosticsRecentFailure, ServerTraceDiagnosticsResult, ServerTraceDiagnosticsSpanOccurrence, ServerTraceDiagnosticsSpanSummary, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; interface TraceRecordLike { readonly name?: unknown; readonly traceId?: unknown; readonly spanId?: unknown; readonly startTimeUnixNano?: unknown; readonly endTimeUnixNano?: unknown; readonly durationMs?: unknown; readonly exit?: unknown; readonly events?: unknown; } interface TraceEventLike { readonly name?: unknown; readonly timeUnixNano?: unknown; readonly attributes?: unknown; } export interface TraceDiagnosticsOptions { readonly traceFilePath: string; readonly maxFiles: number; readonly slowSpanThresholdMs?: number; readonly readAt?: DateTime.Utc; } export class TraceFileReadError extends Schema.TaggedErrorClass<TraceFileReadError>()( "TraceFileReadError", { traceFilePath: Schema.String, causeTag: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to read local trace file '${this.traceFilePath}'.`; } } export class TraceDiagnostics extends Context.Service< TraceDiagnostics, { readonly read: ( options: TraceDiagnosticsOptions, ) => Effect.Effect<ServerTraceDiagnosticsResult>; } >()("t3/diagnostics/TraceDiagnostics") {} interface TraceDiagnosticsInput { readonly traceFilePath: string; readonly files: ReadonlyArray<{ readonly path: string; readonly text: string }>; readonly scannedFilePaths?: ReadonlyArray<string>; readonly slowSpanThresholdMs?: number; readonly readAt: DateTime.Utc; readonly error?: TraceDiagnosticsErrorSummary; readonly partialFailure?: boolean; } interface TraceDiagnosticsErrorSummary { readonly kind: ServerTraceDiagnosticsErrorKind; readonly message: string; } const DEFAULT_SLOW_SPAN_THRESHOLD_MS = 1_000; const TOP_LIMIT = 10; const RECENT_LIMIT = 20; function toRotatedTracePaths(traceFilePath: string, maxFiles: number): ReadonlyArray<string> { const backupCount = Math.max(0, Math.floor(maxFiles)); const backups = Array.from( { length: backupCount }, (_, index) => `${traceFilePath}.${backupCount - index}`, ); return [...backups, traceFilePath]; } function isRecordObject(value: unknown): value is TraceRecordLike { return typeof value === "object" && value !== null; } function toStringValue(value: unknown): Option.Option<string> { return typeof value === "string" && value.trim().length > 0 ? Option.some(value) : Option.none(); } function toNumberValue(value: unknown): Option.Option<number> { return typeof value === "number" && Number.isFinite(value) ? Option.some(value) : Option.none(); } function unixNanoToDateTime(value: unknown): Option.Option<DateTime.Utc> { const text = toStringValue(value); if (Option.isNone(text)) return Option.none(); try { const millis = Number(BigInt(text.value) / 1_000_000n); return DateTime.make(millis); } catch { return Option.none(); } } function readExitTag(exit: unknown): Option.Option<string> { if (!isRecordObject(exit) || !("_tag" in exit)) return Option.none(); return toStringValue(exit._tag); } function readExitCause(exit: unknown): string { if (!isRecordObject(exit) || !("cause" in exit)) return "Failure"; return Option.getOrElse( Option.map(toStringValue(exit.cause), (cause) => cause.trim()), () => "Failure", ); } function isTraceEvent(value: unknown): value is TraceEventLike { return typeof value === "object" && value !== null; } function readEventAttributes(event: TraceEventLike): Readonly<Record<string, unknown>> { return typeof event.attributes === "object" && event.attributes !== null ? (event.attributes as Readonly<Record<string, unknown>>) : {}; } function makeEmptyDiagnostics(input: { readonly traceFilePath: string; readonly scannedFilePaths: ReadonlyArray<string>; readonly readAt: DateTime.Utc; readonly slowSpanThresholdMs: number; readonly error?: TraceDiagnosticsErrorSummary; readonly partialFailure?: boolean; }): ServerTraceDiagnosticsResult { return { traceFilePath: input.traceFilePath, scannedFilePaths: [...input.scannedFilePaths], readAt: input.readAt, recordCount: 0, parseErrorCount: 0, firstSpanAt: Option.none(), lastSpanAt: Option.none(), failureCount: 0, interruptionCount: 0, slowSpanThresholdMs: input.slowSpanThresholdMs, slowSpanCount: 0, logLevelCounts: {}, topSpansByCount: [], slowestSpans: [], commonFailures: [], latestFailures: [], latestWarningAndErrorLogs: [], partialFailure: input.partialFailure ? Option.some(true) : Option.none(), error: Option.fromNullishOr(input.error), }; } function isNotFoundError(error: PlatformError.PlatformError): boolean { return error.reason._tag === "NotFound"; } function insertBoundedSlowestSpan( slowestSpans: ServerTraceDiagnosticsSpanOccurrence[], span: ServerTraceDiagnosticsSpanOccurrence, ): void { if ( slowestSpans.length >= TOP_LIMIT && span.durationMs <= slowestSpans[slowestSpans.length - 1]!.durationMs ) { return; } slowestSpans.push(span); slowestSpans.sort((left, right) => right.durationMs - left.durationMs); if (slowestSpans.length > TOP_LIMIT) { slowestSpans.length = TOP_LIMIT; } } function earliestDateTime( current: Option.Option<DateTime.Utc>, candidate: DateTime.Utc, ): Option.Option<DateTime.Utc> { return Option.match(current, { onNone: () => Option.some(candidate), onSome: (value) => (DateTime.isLessThan(candidate, value) ? Option.some(candidate) : current), }); } function latestDateTime( current: Option.Option<DateTime.Utc>, candidate: DateTime.Utc, ): Option.Option<DateTime.Utc> { return Option.match(current, { onNone: () => Option.some(candidate), onSome: (value) => DateTime.isGreaterThan(candidate, value) ? Option.some(candidate) : current, }); } export function aggregateTraceDiagnostics( input: TraceDiagnosticsInput, ): ServerTraceDiagnosticsResult { const readAt = input.readAt; const slowSpanThresholdMs = input.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS; const scannedFilePaths = input.scannedFilePaths ?? input.files.map((file) => file.path); if (input.files.length === 0) { return makeEmptyDiagnostics({ traceFilePath: input.traceFilePath, scannedFilePaths, readAt, slowSpanThresholdMs, error: input.error ?? { kind: "trace-file-not-found", message: "No local trace files were found.", }, ...(input.partialFailure ? { partialFailure: true } : {}), }); } let parseErrorCount = 0; let recordCount = 0; let failureCount = 0; let interruptionCount = 0; let slowSpanCount = 0; let firstSpanAt: Option.Option<DateTime.Utc> = Option.none(); let lastSpanAt: Option.Option<DateTime.Utc> = Option.none(); const spansByName = new Map< string, { count: number; failureCount: number; totalDurationMs: number; maxDurationMs: number } >(); const failuresByKey = new Map<string, ServerTraceDiagnosticsFailureSummary>(); const latestFailures: ServerTraceDiagnosticsRecentFailure[] = []; const slowestSpans: ServerTraceDiagnosticsSpanOccurrence[] = []; const latestWarningAndErrorLogs: ServerTraceDiagnosticsLogEvent[] = []; const logLevelCounts: Record<string, number> = {}; for (const file of input.files) { const lines = file.text.split(/\r?\n/); for (const line of lines) { if (line.trim().length === 0) continue; let parsed: unknown; try { parsed = JSON.parse(line); } catch { parseErrorCount += 1; continue; } if (!isRecordObject(parsed)) { parseErrorCount += 1; continue; } const name = toStringValue(parsed.name); const traceId = toStringValue(parsed.traceId); const spanId = toStringValue(parsed.spanId); const durationMs = toNumberValue(parsed.durationMs); const endedAt = unixNanoToDateTime(parsed.endTimeUnixNano); const startedAt = unixNanoToDateTime(parsed.startTimeUnixNano); const spanFields = Option.all({ name, traceId, spanId, durationMs, endedAt }); if (Option.isNone(spanFields)) { parseErrorCount += 1; continue; } const { name: spanName, traceId: spanTraceId, spanId: spanSpanId, durationMs: spanDurationMs, endedAt: spanEndedAt, } = spanFields.value; recordCount += 1; if (Option.isSome(startedAt)) { firstSpanAt = earliestDateTime(firstSpanAt, startedAt.value); } lastSpanAt = latestDateTime(lastSpanAt, spanEndedAt); const exitTag = readExitTag(parsed.exit); const isFailure = Option.isSome(exitTag) && exitTag.value === "Failure"; const isInterrupted = Option.isSome(exitTag) && exitTag.value === "Interrupted"; if (isFailure) failureCount += 1; if (isInterrupted) interruptionCount += 1; const spanSummary = spansByName.get(spanName) ?? { count: 0, failureCount: 0, totalDurationMs: 0, maxDurationMs: 0, }; spanSummary.count += 1; spanSummary.totalDurationMs += spanDurationMs; spanSummary.maxDurationMs = Math.max(spanSummary.maxDurationMs, spanDurationMs); if (isFailure) spanSummary.failureCount += 1; spansByName.set(spanName, spanSummary); const spanItem = { name: spanName, durationMs: spanDurationMs, endedAt: spanEndedAt, traceId: spanTraceId, spanId: spanSpanId, }; if (spanDurationMs >= slowSpanThresholdMs) { slowSpanCount += 1; } insertBoundedSlowestSpan(slowestSpans, spanItem); if (isFailure) { const cause = readExitCause(parsed.exit); latestFailures.push({ ...spanItem, cause }); const failureKey = `${spanName}\0${cause}`; const existing = failuresByKey.get(failureKey); const isLatestFailure = !existing || DateTime.isGreaterThan(spanEndedAt, existing.lastSeenAt); failuresByKey.set(failureKey, { name: spanName, cause, count: (existing?.count ?? 0) + 1, lastSeenAt: isLatestFailure ? spanEndedAt : existing!.lastSeenAt, traceId: isLatestFailure ? spanTraceId : existing!.traceId, spanId: isLatestFailure ? spanSpanId : existing!.spanId, }); } if (Array.isArray(parsed.events)) { for (const rawEvent of parsed.events) { if (!isTraceEvent(rawEvent)) continue; const attributes = readEventAttributes(rawEvent); const level = toStringValue(attributes["effect.logLevel"]); if (Option.isNone(level)) continue; logLevelCounts[level.value] = (logLevelCounts[level.value] ?? 0) + 1; const normalizedLevel = level.value.toLowerCase(); if ( normalizedLevel !== "warning" && normalizedLevel !== "warn" && normalizedLevel !== "error" && normalizedLevel !== "fatal" ) { continue; } const seenAt = Option.getOrElse( unixNanoToDateTime(rawEvent.timeUnixNano), () => spanEndedAt, ); const message = Option.getOrElse( Option.map(toStringValue(rawEvent.name), (eventName) => eventName.trim()), () => "Log event", ); latestWarningAndErrorLogs.push({ spanName, level: level.value, message, seenAt, traceId: spanTraceId, spanId: spanSpanId, }); } } } } const topSpansByCount: ServerTraceDiagnosticsSpanSummary[] = [...spansByName.entries()] .map(([name, span]) => ({ name, count: span.count, failureCount: span.failureCount, totalDurationMs: span.totalDurationMs, averageDurationMs: span.count > 0 ? span.totalDurationMs / span.count : 0, maxDurationMs: span.maxDurationMs, })) .toSorted((left, right) => right.count - left.count || right.maxDurationMs - left.maxDurationMs) .slice(0, TOP_LIMIT); return { traceFilePath: input.traceFilePath, scannedFilePaths, readAt, recordCount, parseErrorCount, firstSpanAt, lastSpanAt, failureCount, interruptionCount, slowSpanThresholdMs, slowSpanCount, logLevelCounts, topSpansByCount, slowestSpans, commonFailures: [...failuresByKey.values()] .toSorted( (left, right) => right.count - left.count || DateTime.toEpochMillis(right.lastSeenAt) - DateTime.toEpochMillis(left.lastSeenAt), ) .slice(0, TOP_LIMIT), latestFailures: latestFailures .toSorted( (left, right) => DateTime.toEpochMillis(right.endedAt) - DateTime.toEpochMillis(left.endedAt), ) .slice(0, RECENT_LIMIT), latestWarningAndErrorLogs: latestWarningAndErrorLogs .toSorted( (left, right) => DateTime.toEpochMillis(right.seenAt) - DateTime.toEpochMillis(left.seenAt), ) .slice(0, RECENT_LIMIT), partialFailure: input.partialFailure ? Option.some(true) : Option.none(), error: Option.fromNullishOr(input.error), }; } type TraceFileReadResult = | { readonly _tag: "Loaded"; readonly path: string; readonly text: string } | { readonly _tag: "Missing"; readonly path: string }; function readTraceFile( fileSystem: FileSystem.FileSystem, path: string, ): Effect.Effect<TraceFileReadResult, TraceFileReadError> { return fileSystem.readFileString(path).pipe( Effect.map((text): TraceFileReadResult => ({ _tag: "Loaded", path, text })), Effect.catchTags({ PlatformError: (cause) => isNotFoundError(cause) ? Effect.succeed<TraceFileReadResult>({ _tag: "Missing", path }) : Effect.fail( new TraceFileReadError({ traceFilePath: path, causeTag: cause.reason._tag, cause, }), ), }), ); } export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const read: TraceDiagnostics["Service"]["read"] = Effect.fn("TraceDiagnostics.read")( function* (options) { const readAt = options.readAt ?? (yield* DateTime.now); const slowSpanThresholdMs = options.slowSpanThresholdMs ?? DEFAULT_SLOW_SPAN_THRESHOLD_MS; const paths = toRotatedTracePaths(options.traceFilePath, options.maxFiles); const results = yield* Effect.all( paths.map((path) => readTraceFile(fileSystem, path).pipe( Effect.tapError((cause) => Effect.logWarning("Failed to read local trace file.").pipe( Effect.annotateLogs({ traceFilePath: cause.traceFilePath, errorTag: cause._tag, causeTag: cause.causeTag, }), ), ), Effect.result, ), ), { concurrency: 1, }, ); const files = results.flatMap((result) => Result.isSuccess(result) && result.success._tag === "Loaded" ? [{ path: result.success.path, text: result.success.text }] : [], ); const readFailure = results.find(Result.isFailure); const readFailureError = readFailure ? ({ kind: "trace-file-read-failed", message: readFailure.failure.message, } satisfies TraceDiagnosticsErrorSummary) : undefined; if (files.length === 0) { return makeEmptyDiagnostics({ traceFilePath: options.traceFilePath, scannedFilePaths: paths,