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 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 interface TraceDiagnosticsShape { readonly read: (options: TraceDiagnosticsOptions) => Effect.Effect<ServerTraceDiagnosticsResult>;} export class TraceDiagnostics extends Context.Service<TraceDiagnostics, TraceDiagnosticsShape>()( "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;const decodeTraceRecordJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown)); 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): string | null { return typeof value === "string" && value.trim().length > 0 ? value : null;} function toNumberValue(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null;} function unixNanoToDateTime(value: unknown): DateTime.Utc | null { const text = toStringValue(value); if (!text) return null; try { const millis = Number(BigInt(text) / 1_000_000n); return Option.getOrNull(DateTime.make(millis)); } catch { return null; }} function readExitTag(exit: unknown): string | null { if (!isRecordObject(exit) || !("_tag" in exit)) return null; return toStringValue(exit._tag);} function readExitCause(exit: unknown): string { if (!isRecordObject(exit) || !("cause" in exit)) return "Failure"; return toStringValue(exit.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 platformErrorMessage(error: PlatformError.PlatformError): string { return error.message || String(error);} 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; }} 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: DateTime.Utc | null = null; let lastSpanAt: DateTime.Utc | null = null; 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; const decoded = decodeTraceRecordJson(line); if (Option.isNone(decoded)) { parseErrorCount += 1; continue; } const parsed = decoded.value; 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); if (!name || !traceId || !spanId || durationMs === null || !endedAt) { parseErrorCount += 1; continue; } recordCount += 1; firstSpanAt = startedAt && (firstSpanAt === null || DateTime.isLessThan(startedAt, firstSpanAt)) ? startedAt : firstSpanAt; lastSpanAt = lastSpanAt === null || DateTime.isGreaterThan(endedAt, lastSpanAt) ? endedAt : lastSpanAt; const exitTag = readExitTag(parsed.exit); const isFailure = exitTag === "Failure"; const isInterrupted = exitTag === "Interrupted"; if (isFailure) failureCount += 1; if (isInterrupted) interruptionCount += 1; const spanSummary = spansByName.get(name) ?? { count: 0, failureCount: 0, totalDurationMs: 0, maxDurationMs: 0, }; spanSummary.count += 1; spanSummary.totalDurationMs += durationMs; spanSummary.maxDurationMs = Math.max(spanSummary.maxDurationMs, durationMs); if (isFailure) spanSummary.failureCount += 1; spansByName.set(name, spanSummary); const spanItem = { name, durationMs, endedAt, traceId, spanId }; if (durationMs >= slowSpanThresholdMs) { slowSpanCount += 1; } insertBoundedSlowestSpan(slowestSpans, spanItem); if (isFailure) { const cause = readExitCause(parsed.exit); latestFailures.push({ ...spanItem, cause }); const failureKey = `${name}\0${cause}`; const existing = failuresByKey.get(failureKey); const isLatestFailure = !existing || DateTime.isGreaterThan(endedAt, existing.lastSeenAt); failuresByKey.set(failureKey, { name, cause, count: (existing?.count ?? 0) + 1, lastSeenAt: isLatestFailure ? endedAt : existing!.lastSeenAt, traceId: isLatestFailure ? traceId : existing!.traceId, spanId: isLatestFailure ? spanId : 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 (!level) continue; logLevelCounts[level] = (logLevelCounts[level] ?? 0) + 1; const normalizedLevel = level.toLowerCase(); if ( normalizedLevel !== "warning" && normalizedLevel !== "warn" && normalizedLevel !== "error" && normalizedLevel !== "fatal" ) { continue; } const seenAt = unixNanoToDateTime(rawEvent.timeUnixNano) ?? endedAt; const message = toStringValue(rawEvent.name)?.trim() ?? "Log event"; latestWarningAndErrorLogs.push({ spanName: name, level, message, seenAt, traceId, spanId, }); } } } } 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: Option.fromNullishOr(firstSpanAt), lastSpanAt: Option.fromNullishOr(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 } | { readonly _tag: "Failed"; readonly path: string; readonly message: string }; function readTraceFile( fileSystem: FileSystem.FileSystem, path: string,): Effect.Effect<TraceFileReadResult> { return fileSystem.readFileString(path).pipe( Effect.map((text) => ({ _tag: "Loaded" as const, path, text })), Effect.catch((error: PlatformError.PlatformError) => Effect.succeed( isNotFoundError(error) ? { _tag: "Missing" as const, path } : { _tag: "Failed" as const, path, message: platformErrorMessage(error) }, ), ), );} export const make = Effect.fn("makeTraceDiagnostics")(function* () { const fileSystem = yield* FileSystem.FileSystem; const read: TraceDiagnosticsShape["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)), { concurrency: 1, }, ); const files = results.flatMap((result) => result._tag === "Loaded" ? [{ path: result.path, text: result.text }] : [], ); const readFailure = results.find((result) => result._tag === "Failed"); const readFailureError = readFailure ? ({ kind: "trace-file-read-failed", message: readFailure.message.trim() || `Failed to read ${readFailure.path}.`, } satisfies TraceDiagnosticsErrorSummary) : undefined; if (files.length === 0) { return makeEmptyDiagnostics({ traceFilePath: options.traceFilePath, scannedFilePaths: paths, readAt, slowSpanThresholdMs, error: readFailureError ?? ({ kind: "trace-file-not-found", message: "No local trace files were found.", } satisfies TraceDiagnosticsErrorSummary), }); } return aggregateTraceDiagnostics({ traceFilePath: options.traceFilePath, files, scannedFilePaths: paths, readAt, slowSpanThresholdMs, ...(readFailureError ? { partialFailure: true, error: readFailureError } : {}), }); }, ); return TraceDiagnostics.of({ read });}); export const layer = Layer.effect(TraceDiagnostics, make()); export function readTraceDiagnostics( options: TraceDiagnosticsOptions,): Effect.Effect<ServerTraceDiagnosticsResult, never, TraceDiagnostics> { return Effect.gen(function* () { const diagnostics = yield* TraceDiagnostics; return yield* diagnostics.read(options); });}
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 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 interface TraceDiagnosticsShape {
readonly read: (options: TraceDiagnosticsOptions) => Effect.Effect<ServerTraceDiagnosticsResult>;
}
export class TraceDiagnostics extends Context.Service<TraceDiagnostics, TraceDiagnosticsShape>()(
"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;
const decodeTraceRecordJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
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): string | null {
return typeof value === "string" && value.trim().length > 0 ? value : null;
}
function toNumberValue(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function unixNanoToDateTime(value: unknown): DateTime.Utc | null {
const text = toStringValue(value);
if (!text) return null;
try {
const millis = Number(BigInt(text) / 1_000_000n);
return Option.getOrNull(DateTime.make(millis));
} catch {
return null;
}
}
function readExitTag(exit: unknown): string | null {
if (!isRecordObject(exit) || !("_tag" in exit)) return null;
return toStringValue(exit._tag);
}
function readExitCause(exit: unknown): string {
if (!isRecordObject(exit) || !("cause" in exit)) return "Failure";
return toStringValue(exit.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 platformErrorMessage(error: PlatformError.PlatformError): string {
return error.message || String(error);
}
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;
}
}
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: DateTime.Utc | null = null;
let lastSpanAt: DateTime.Utc | null = null;
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;
const decoded = decodeTraceRecordJson(line);
if (Option.isNone(decoded)) {
parseErrorCount += 1;
continue;
}
const parsed = decoded.value;
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);
if (!name || !traceId || !spanId || durationMs === null || !endedAt) {
parseErrorCount += 1;
continue;
}
recordCount += 1;
firstSpanAt =
startedAt && (firstSpanAt === null || DateTime.isLessThan(startedAt, firstSpanAt))
? startedAt
: firstSpanAt;
lastSpanAt =
lastSpanAt === null || DateTime.isGreaterThan(endedAt, lastSpanAt) ? endedAt : lastSpanAt;
const exitTag = readExitTag(parsed.exit);
const isFailure = exitTag === "Failure";
const isInterrupted = exitTag === "Interrupted";
if (isFailure) failureCount += 1;
if (isInterrupted) interruptionCount += 1;
const spanSummary = spansByName.get(name) ?? {
count: 0,
failureCount: 0,
totalDurationMs: 0,
maxDurationMs: 0,
};
spanSummary.count += 1;
spanSummary.totalDurationMs += durationMs;
spanSummary.maxDurationMs = Math.max(spanSummary.maxDurationMs, durationMs);
if (isFailure) spanSummary.failureCount += 1;
spansByName.set(name, spanSummary);
const spanItem = { name, durationMs, endedAt, traceId, spanId };
if (durationMs >= slowSpanThresholdMs) {
slowSpanCount += 1;
}
insertBoundedSlowestSpan(slowestSpans, spanItem);
if (isFailure) {
const cause = readExitCause(parsed.exit);
latestFailures.push({ ...spanItem, cause });
const failureKey = `${name}\0${cause}`;
const existing = failuresByKey.get(failureKey);
const isLatestFailure = !existing || DateTime.isGreaterThan(endedAt, existing.lastSeenAt);
failuresByKey.set(failureKey, {
name,
cause,
count: (existing?.count ?? 0) + 1,
lastSeenAt: isLatestFailure ? endedAt : existing!.lastSeenAt,
traceId: isLatestFailure ? traceId : existing!.traceId,
spanId: isLatestFailure ? spanId : 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 (!level) continue;
logLevelCounts[level] = (logLevelCounts[level] ?? 0) + 1;
const normalizedLevel = level.toLowerCase();
if (
normalizedLevel !== "warning" &&
normalizedLevel !== "warn" &&
normalizedLevel !== "error" &&
normalizedLevel !== "fatal"
) {
continue;
}
const seenAt = unixNanoToDateTime(rawEvent.timeUnixNano) ?? endedAt;
const message = toStringValue(rawEvent.name)?.trim() ?? "Log event";
latestWarningAndErrorLogs.push({
spanName: name,
level,
message,
seenAt,
traceId,
spanId,
});
}
}
}
}
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: Option.fromNullishOr(firstSpanAt),
lastSpanAt: Option.fromNullishOr(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 }
| { readonly _tag: "Failed"; readonly path: string; readonly message: string };
function readTraceFile(
fileSystem: FileSystem.FileSystem,
path: string,
): Effect.Effect<TraceFileReadResult> {
return fileSystem.readFileString(path).pipe(
Effect.map((text) => ({ _tag: "Loaded" as const, path, text })),
Effect.catch((error: PlatformError.PlatformError) =>
Effect.succeed(
isNotFoundError(error)
? { _tag: "Missing" as const, path }
: { _tag: "Failed" as const, path, message: platformErrorMessage(error) },
),
),
);
}
export const make = Effect.fn("makeTraceDiagnostics")(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const read: TraceDiagnosticsShape["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)),
{
concurrency: 1,
},
);
const files = results.flatMap((result) =>
result._tag === "Loaded" ? [{ path: result.path, text: result.text }] : [],
);
const readFailure = results.find((result) => result._tag === "Failed");
const readFailureError = readFailure
? ({
kind: "trace-file-read-failed",
message: readFailure.message.trim() || `Failed to read ${readFailure.path}.`,
} satisfies TraceDiagnosticsErrorSummary)
: undefined;
if (files.length === 0) {
return makeEmptyDiagnostics({
traceFilePath: options.traceFilePath,
scannedFilePaths: paths,
readAt,
slowSpanThresholdMs,
error:
readFailureError ??
({
kind: "trace-file-not-found",
message: "No local trace files were found.",
} satisfies TraceDiagnosticsErrorSummary),
});
}
return aggregateTraceDiagnostics({
traceFilePath: options.traceFilePath,
files,
scannedFilePaths: paths,
readAt,
slowSpanThresholdMs,
...(readFailureError ? { partialFailure: true, error: readFailureError } : {}),
});
},
);
return TraceDiagnostics.of({ read });
});
export const layer = Layer.effect(TraceDiagnostics, make());
export function readTraceDiagnostics(
options: TraceDiagnosticsOptions,
): Effect.Effect<ServerTraceDiagnosticsResult, never, TraceDiagnostics> {
return Effect.gen(function* () {
const diagnostics = yield* TraceDiagnostics;
return yield* diagnostics.read(options);
});
}