const std = @import("std");const io_mod = @import("io.zig");const profile_paths = @import("profile_paths.zig"); const Allocator = std.mem.Allocator; const State = struct { configured: bool = false, enabled: bool = false, stderr_enabled: bool = false, scope_filter: ?[]u8 = null, file_path: ?[]u8 = null,}; const default_log_max_bytes: u64 = 2 * 1024 * 1024; pub const Options = struct { file_path: ?[]const u8 = null, stderr_enabled: bool = false, scope_filter: ?[]const u8 = null,}; pub const TraceContext = struct { turn_id: u64 = 0, step_id: u64 = 0, subagent_id: u64 = 0,}; var state_mutex: std.Io.Mutex = .init;var state: State = .{};var next_turn_id = std.atomic.Value(u64).init(1);var next_step_id = std.atomic.Value(u64).init(1); pub fn configureFromEnv(alloc: Allocator, workspace_root: []const u8) void { const options = loadOptionsFromEnv(alloc, workspace_root) catch return; defer if (options.file_path) |path| alloc.free(path); if (options.file_path == null and !options.stderr_enabled) return; configure(options) catch {};} pub fn shutdown() void { if (!state.configured) return; const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (state.scope_filter) |filter| std.heap.c_allocator.free(filter); if (state.file_path) |path| std.heap.c_allocator.free(path); state = .{};} pub fn activeLogPath() ?[]const u8 { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); return state.file_path;} pub fn nextTurnId() u64 { if (comptime @import("builtin").os.tag == .wasi) return 1; return next_turn_id.fetchAdd(1, .seq_cst);} pub fn nextStepId() u64 { if (comptime @import("builtin").os.tag == .wasi) return 1; return next_step_id.fetchAdd(1, .seq_cst);} pub fn logf(scope: []const u8, comptime fmt: []const u8, args: anytype) void { var line: TraceLine = undefined; if (!line.begin(scope)) return; line.print(fmt, args); line.end();} pub fn eventf(scope: []const u8, event: []const u8, ctx: TraceContext, comptime fmt: []const u8, args: anytype) void { var line: TraceLine = undefined; if (!line.beginEvent(scope, event, ctx, fmt.len != 0)) return; if (fmt.len != 0) line.print(fmt, args); line.end();} // Keep generic trace wrappers small by centralizing line assembly here.// Failed lines accept remaining writes but emit nothing.const TraceLine = struct { out: std.Io.Writer.Allocating, failed: bool, noinline fn begin(line: *TraceLine, scope: []const u8) bool { if (!isScopeEnabled(scope)) return false; line.* = .{ .out = .init(std.heap.c_allocator), .failed = false }; line.out.writer.print("{d} [{s}] ", .{ io_mod.milliTimestamp(), scope }) catch { line.failed = true; }; return true; } noinline fn beginEvent(line: *TraceLine, scope: []const u8, event: []const u8, ctx: TraceContext, has_message: bool) bool { if (!line.begin(scope)) return false; if (line.failed) return true; line.appendEventHead(event, ctx, has_message) catch { line.failed = true; }; return true; } fn appendEventHead(line: *TraceLine, event: []const u8, ctx: TraceContext, has_message: bool) !void { try line.out.writer.print("event={s}", .{event}); if (ctx.turn_id != 0) try line.out.writer.print(" turn_id={d}", .{ctx.turn_id}); if (ctx.step_id != 0) try line.out.writer.print(" step_id={d}", .{ctx.step_id}); if (ctx.subagent_id != 0) try line.out.writer.print(" subagent_id={d}", .{ctx.subagent_id}); if (has_message) try line.out.writer.writeByte(' '); } fn print(line: *TraceLine, comptime fmt: []const u8, args: anytype) void { if (line.failed) return; line.out.writer.print(fmt, args) catch { line.failed = true; }; } noinline fn end(line: *TraceLine) void { defer line.out.deinit(); if (line.failed) return; line.out.writer.writeByte('\n') catch return; writeLine(line.out.written()); }}; pub fn preview(text: []const u8, max_len: usize) []const u8 { const trimmed = std.mem.trim(u8, text, " \t\r\n"); const line_end = std.mem.findScalar(u8, trimmed, '\n') orelse trimmed.len; const first_line = std.mem.trimEnd(u8, trimmed[0..line_end], "\r"); if (first_line.len <= max_len) return first_line; return first_line[0..max_len];} pub fn terminalPreview(buf: []u8, text: []const u8) []const u8 { if (buf.len == 0) return buf[0..0]; const first_line = preview(text, 512); var in_idx: usize = 0; var out_idx: usize = 0; while (in_idx < first_line.len and out_idx < buf.len) { const byte = first_line[in_idx]; if (byte == 0x1b) { in_idx = skipTerminalEscape(first_line, in_idx); continue; } buf[out_idx] = if (byte == '\r' or byte == '\n' or byte == '\t') ' ' else if (byte < 0x20 or byte == 0x7f) '?' else byte; out_idx += 1; in_idx += 1; } return buf[0..out_idx];} fn skipTerminalEscape(text: []const u8, start: usize) usize { if (start + 1 >= text.len) return start + 1; const introducer = text[start + 1]; if (introducer == '[') { var i = start + 2; while (i < text.len) : (i += 1) { const byte = text[i]; if (byte >= 0x40 and byte <= 0x7e) return i + 1; } return text.len; } if (introducer == ']') { var i = start + 2; while (i < text.len) : (i += 1) { if (text[i] == 0x07) return i + 1; if (text[i] == 0x1b and i + 1 < text.len and text[i + 1] == '\\') return i + 2; } return text.len; } return start + 2;} pub fn redactedJsonPreview(alloc: Allocator, text: []const u8) ![]u8 { if (text.len == 0) return alloc.dupe(u8, "<empty>"); var parsed = std.json.parseFromSlice(std.json.Value, alloc, text, .{}) catch { return std.fmt.allocPrint(alloc, "<invalid-json bytes={d}>", .{text.len}); }; defer parsed.deinit(); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRedactedJsonValuePreview(&out.writer, parsed.value, 0, .preserve); return out.toOwnedSlice();} pub fn keylessJsonPreview(alloc: Allocator, text: []const u8) ![]u8 { if (text.len == 0) return alloc.dupe(u8, "<empty>"); var parsed = std.json.parseFromSlice(std.json.Value, alloc, text, .{}) catch |err| { if (err == error.OutOfMemory) return err; return std.fmt.allocPrint(alloc, "<invalid-json bytes={d}>", .{text.len}); }; defer parsed.deinit(); return keylessJsonValuePreview(alloc, parsed.value);} pub fn keylessJsonValuePreview(alloc: Allocator, value: std.json.Value) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRedactedJsonValuePreview(&out.writer, value, 0, .omit); return out.toOwnedSlice();} const JsonKeyPolicy = enum { preserve, omit,}; fn writeRedactedJsonValuePreview( writer: anytype, value: std.json.Value, depth: usize, key_policy: JsonKeyPolicy,) !void { switch (value) { .object => |object| { if (depth >= 2) { try writer.print("<object_fields={d}>", .{object.count()}); return; } switch (key_policy) { .preserve => { try writer.writeByte('{'); var it = object.iterator(); var written: usize = 0; while (it.next()) |entry| { if (written > 0) try writer.writeByte(','); if (written >= 6) { try writer.writeAll("..."); break; } try std.json.Stringify.value(entry.key_ptr.*, .{}, writer); try writer.writeByte(':'); try writeRedactedJsonValuePreview(writer, entry.value_ptr.*, depth + 1, key_policy); written += 1; } try writer.writeByte('}'); }, .omit => { try writer.print("<object_fields={d}", .{object.count()}); if (object.count() > 0) { try writer.writeAll(" values=["); var it = object.iterator(); var written: usize = 0; while (it.next()) |entry| { if (written > 0) try writer.writeByte(','); if (written >= 6) { try writer.writeAll("..."); break; } try writeRedactedJsonValuePreview(writer, entry.value_ptr.*, depth + 1, key_policy); written += 1; } try writer.writeByte(']'); } try writer.writeByte('>'); }, } }, .array => |array| try writer.print("<array_len={d}>", .{array.items.len}), .string => |string| try writer.print("<string_bytes={d}>", .{string.len}), .integer, .float, .number_string => try writer.writeAll("<number>"), .bool => try writer.writeAll("<bool>"), .null => try writer.writeAll("<null>"), }} pub fn resetForTest() void { shutdown(); next_turn_id.store(1, .seq_cst); next_step_id.store(1, .seq_cst);} pub fn configureForTest(alloc: Allocator, path: []const u8) !void { const owned = try alloc.dupe(u8, path); defer alloc.free(owned); try configure(.{ .file_path = owned });} pub fn configureForTestWithScopes(alloc: Allocator, path: []const u8, scope_filter: []const u8) !void { const owned = try alloc.dupe(u8, path); defer alloc.free(owned); try configure(.{ .file_path = owned, .scope_filter = scope_filter });} pub fn configure(options: Options) !void { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (state.configured) return; state.configured = true; state.stderr_enabled = options.stderr_enabled; if (options.file_path) |path| { try ensureParentDir(path); rotateIfTooLarge(path, default_log_max_bytes); var file = try std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = false }); file.close(zio); state.file_path = std.heap.c_allocator.dupe(u8, path) catch null; } if (options.scope_filter) |filter| { const trimmed = std.mem.trim(u8, filter, " \t\r\n"); if (trimmed.len > 0) state.scope_filter = try std.heap.c_allocator.dupe(u8, trimmed); } state.enabled = state.stderr_enabled or state.file_path != null;} fn rotateIfTooLarge(path: []const u8, max_bytes: u64) void { const zio = io_mod.getIo(); var file = std.Io.Dir.openFileAbsolute(zio, path, .{}) catch return; var should_truncate = false; { defer file.close(zio); const stat = file.stat(zio) catch return; if (stat.size > max_bytes) should_truncate = true; } if (!should_truncate) return; var truncate = std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = true }) catch return; truncate.close(zio);} pub fn isEnabled() bool { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); return state.enabled;} pub fn isScopeEnabled(scope: []const u8) bool { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (!state.enabled) return false; const filter = state.scope_filter orelse return true; return scopeFilterAllows(filter, scope);} fn writeLine(line: []const u8) void { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (!state.enabled) return; if (state.stderr_enabled) { std.debug.print("{s}", .{line}); } if (state.file_path) |path| { appendLineToFile(zio, path, line); }} fn appendLineToFile(zio: std.Io, path: []const u8, line: []const u8) void { var file = std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = false, .lock = .exclusive, }) catch return; defer file.close(zio); _ = std.c.lseek(file.handle, 0, std.posix.SEEK.END); file.writeStreamingAll(zio, line) catch {};} fn loadOptionsFromEnv(alloc: Allocator, workspace_root: []const u8) !Options { const trace_log = loadOptionalEnv("FX_TRACE_LOG"); const trace_flag = loadOptionalEnv("FX_TRACE"); const trace_stderr = loadOptionalEnv("FX_TRACE_STDERR"); const trace_scopes = loadOptionalEnv("FX_TRACE_SCOPES"); const stderr_enabled = isTruthy(trace_stderr); if (trace_log) |raw_path| { const resolved = try resolveLogPath(alloc, workspace_root, raw_path); return .{ .file_path = resolved, .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes }; } if (isTruthy(trace_flag)) { return .{ .file_path = try defaultLogPath(alloc), .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes, }; } return .{ .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes };} fn loadOptionalEnv(name: []const u8) ?[]const u8 { return io_mod.getenv(name);} fn isTruthy(value: ?[]const u8) bool { const raw = value orelse return false; const trimmed = std.mem.trim(u8, raw, " \t\r\n"); return std.ascii.eqlIgnoreCase(trimmed, "1") or std.ascii.eqlIgnoreCase(trimmed, "true") or std.ascii.eqlIgnoreCase(trimmed, "yes") or std.ascii.eqlIgnoreCase(trimmed, "on");} fn scopeFilterAllows(filter: []const u8, scope: []const u8) bool { var rest = filter; while (true) { const next_comma = std.mem.findScalar(u8, rest, ','); const raw_part = if (next_comma) |idx| rest[0..idx] else rest; const part = std.mem.trim(u8, raw_part, " \t\r\n"); if (part.len > 0 and std.mem.eql(u8, part, scope)) return true; if (next_comma) |idx| { rest = rest[idx + 1 ..]; } else { return false; } }} fn resolveLogPath(alloc: Allocator, workspace_root: []const u8, raw_path: []const u8) ![]u8 { const trimmed = std.mem.trim(u8, raw_path, " \t\r\n"); if (trimmed.len == 0) return error.InvalidTracePath; if (std.fs.path.isAbsolute(trimmed)) return alloc.dupe(u8, trimmed); return std.fs.path.join(alloc, &.{ workspace_root, trimmed });} fn defaultLogPath(alloc: Allocator) ![]u8 { if (io_mod.getenv("HOME")) |home| { return defaultLogPathForHome(alloc, home); } return fallbackLogPathForMillis(alloc, io_mod.milliTimestamp());} fn defaultLogPathForHome(alloc: Allocator, home: []const u8) ![]u8 { return profile_paths.traceLogPath(alloc, home);} fn fallbackLogPathForMillis(alloc: Allocator, millis: i64) ![]u8 { return std.fmt.allocPrint(alloc, "/tmp/fx-trace-{d}.log", .{millis});} fn ensureParentDir(path: []const u8) !void { const parent = std.fs.path.dirname(path) orelse return; try makeAbsolutePath(parent);} fn makeAbsolutePath(path: []const u8) !void { const zio = io_mod.getIo(); std.Io.Dir.createDirAbsolute(zio, path, .default_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => { if (std.fs.path.dirname(path)) |parent| { try makeAbsolutePath(parent); try std.Io.Dir.createDirAbsolute(zio, path, .default_dir); } else { return err; } }, };} fn tmpRoot(alloc: Allocator, tmp: std.testing.TmpDir) ![]u8 { return io_mod.dirRealpathAlloc(alloc, tmp.dir, ".");} fn tmpPath(alloc: Allocator, root: []const u8, name: []const u8) ![]u8 { return std.fs.path.join(alloc, &.{ root, name });} fn readFileForTest(alloc: Allocator, path: []const u8) ![]u8 { const zio = io_mod.getIo(); var file = try std.Io.Dir.openFileAbsolute(zio, path, .{}); defer file.close(zio); return io_mod.readFileToEnd(alloc, &file, 8192);} test "isTruthy parses accepted and rejected values" { try std.testing.expect(isTruthy("1")); try std.testing.expect(isTruthy("true")); try std.testing.expect(isTruthy("YES")); try std.testing.expect(isTruthy("on")); try std.testing.expect(!isTruthy("0")); try std.testing.expect(!isTruthy("")); try std.testing.expect(!isTruthy(" \t\r\n")); try std.testing.expect(!isTruthy(null));Lines 1–500 of 690. Load more
const std = @import("std"); const io_mod = @import("io.zig"); const profile_paths = @import("profile_paths.zig"); const Allocator = std.mem.Allocator; const State = struct { configured: bool = false, enabled: bool = false, stderr_enabled: bool = false, scope_filter: ?[]u8 = null, file_path: ?[]u8 = null, }; const default_log_max_bytes: u64 = 2 * 1024 * 1024; pub const Options = struct { file_path: ?[]const u8 = null, stderr_enabled: bool = false, scope_filter: ?[]const u8 = null, }; pub const TraceContext = struct { turn_id: u64 = 0, step_id: u64 = 0, subagent_id: u64 = 0, }; var state_mutex: std.Io.Mutex = .init; var state: State = .{}; var next_turn_id = std.atomic.Value(u64).init(1); var next_step_id = std.atomic.Value(u64).init(1); pub fn configureFromEnv(alloc: Allocator, workspace_root: []const u8) void { const options = loadOptionsFromEnv(alloc, workspace_root) catch return; defer if (options.file_path) |path| alloc.free(path); if (options.file_path == null and !options.stderr_enabled) return; configure(options) catch {}; } pub fn shutdown() void { if (!state.configured) return; const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (state.scope_filter) |filter| std.heap.c_allocator.free(filter); if (state.file_path) |path| std.heap.c_allocator.free(path); state = .{}; } pub fn activeLogPath() ?[]const u8 { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); return state.file_path; } pub fn nextTurnId() u64 { if (comptime @import("builtin").os.tag == .wasi) return 1; return next_turn_id.fetchAdd(1, .seq_cst); } pub fn nextStepId() u64 { if (comptime @import("builtin").os.tag == .wasi) return 1; return next_step_id.fetchAdd(1, .seq_cst); } pub fn logf(scope: []const u8, comptime fmt: []const u8, args: anytype) void { var line: TraceLine = undefined; if (!line.begin(scope)) return; line.print(fmt, args); line.end(); } pub fn eventf(scope: []const u8, event: []const u8, ctx: TraceContext, comptime fmt: []const u8, args: anytype) void { var line: TraceLine = undefined; if (!line.beginEvent(scope, event, ctx, fmt.len != 0)) return; if (fmt.len != 0) line.print(fmt, args); line.end(); } // Keep generic trace wrappers small by centralizing line assembly here. // Failed lines accept remaining writes but emit nothing. const TraceLine = struct { out: std.Io.Writer.Allocating, failed: bool, noinline fn begin(line: *TraceLine, scope: []const u8) bool { if (!isScopeEnabled(scope)) return false; line.* = .{ .out = .init(std.heap.c_allocator), .failed = false }; line.out.writer.print("{d} [{s}] ", .{ io_mod.milliTimestamp(), scope }) catch { line.failed = true; }; return true; } noinline fn beginEvent(line: *TraceLine, scope: []const u8, event: []const u8, ctx: TraceContext, has_message: bool) bool { if (!line.begin(scope)) return false; if (line.failed) return true; line.appendEventHead(event, ctx, has_message) catch { line.failed = true; }; return true; } fn appendEventHead(line: *TraceLine, event: []const u8, ctx: TraceContext, has_message: bool) !void { try line.out.writer.print("event={s}", .{event}); if (ctx.turn_id != 0) try line.out.writer.print(" turn_id={d}", .{ctx.turn_id}); if (ctx.step_id != 0) try line.out.writer.print(" step_id={d}", .{ctx.step_id}); if (ctx.subagent_id != 0) try line.out.writer.print(" subagent_id={d}", .{ctx.subagent_id}); if (has_message) try line.out.writer.writeByte(' '); } fn print(line: *TraceLine, comptime fmt: []const u8, args: anytype) void { if (line.failed) return; line.out.writer.print(fmt, args) catch { line.failed = true; }; } noinline fn end(line: *TraceLine) void { defer line.out.deinit(); if (line.failed) return; line.out.writer.writeByte('\n') catch return; writeLine(line.out.written()); } }; pub fn preview(text: []const u8, max_len: usize) []const u8 { const trimmed = std.mem.trim(u8, text, " \t\r\n"); const line_end = std.mem.findScalar(u8, trimmed, '\n') orelse trimmed.len; const first_line = std.mem.trimEnd(u8, trimmed[0..line_end], "\r"); if (first_line.len <= max_len) return first_line; return first_line[0..max_len]; } pub fn terminalPreview(buf: []u8, text: []const u8) []const u8 { if (buf.len == 0) return buf[0..0]; const first_line = preview(text, 512); var in_idx: usize = 0; var out_idx: usize = 0; while (in_idx < first_line.len and out_idx < buf.len) { const byte = first_line[in_idx]; if (byte == 0x1b) { in_idx = skipTerminalEscape(first_line, in_idx); continue; } buf[out_idx] = if (byte == '\r' or byte == '\n' or byte == '\t') ' ' else if (byte < 0x20 or byte == 0x7f) '?' else byte; out_idx += 1; in_idx += 1; } return buf[0..out_idx]; } fn skipTerminalEscape(text: []const u8, start: usize) usize { if (start + 1 >= text.len) return start + 1; const introducer = text[start + 1]; if (introducer == '[') { var i = start + 2; while (i < text.len) : (i += 1) { const byte = text[i]; if (byte >= 0x40 and byte <= 0x7e) return i + 1; } return text.len; } if (introducer == ']') { var i = start + 2; while (i < text.len) : (i += 1) { if (text[i] == 0x07) return i + 1; if (text[i] == 0x1b and i + 1 < text.len and text[i + 1] == '\\') return i + 2; } return text.len; } return start + 2; } pub fn redactedJsonPreview(alloc: Allocator, text: []const u8) ![]u8 { if (text.len == 0) return alloc.dupe(u8, "<empty>"); var parsed = std.json.parseFromSlice(std.json.Value, alloc, text, .{}) catch { return std.fmt.allocPrint(alloc, "<invalid-json bytes={d}>", .{text.len}); }; defer parsed.deinit(); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRedactedJsonValuePreview(&out.writer, parsed.value, 0, .preserve); return out.toOwnedSlice(); } pub fn keylessJsonPreview(alloc: Allocator, text: []const u8) ![]u8 { if (text.len == 0) return alloc.dupe(u8, "<empty>"); var parsed = std.json.parseFromSlice(std.json.Value, alloc, text, .{}) catch |err| { if (err == error.OutOfMemory) return err; return std.fmt.allocPrint(alloc, "<invalid-json bytes={d}>", .{text.len}); }; defer parsed.deinit(); return keylessJsonValuePreview(alloc, parsed.value); } pub fn keylessJsonValuePreview(alloc: Allocator, value: std.json.Value) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRedactedJsonValuePreview(&out.writer, value, 0, .omit); return out.toOwnedSlice(); } const JsonKeyPolicy = enum { preserve, omit, }; fn writeRedactedJsonValuePreview( writer: anytype, value: std.json.Value, depth: usize, key_policy: JsonKeyPolicy, ) !void { switch (value) { .object => |object| { if (depth >= 2) { try writer.print("<object_fields={d}>", .{object.count()}); return; } switch (key_policy) { .preserve => { try writer.writeByte('{'); var it = object.iterator(); var written: usize = 0; while (it.next()) |entry| { if (written > 0) try writer.writeByte(','); if (written >= 6) { try writer.writeAll("..."); break; } try std.json.Stringify.value(entry.key_ptr.*, .{}, writer); try writer.writeByte(':'); try writeRedactedJsonValuePreview(writer, entry.value_ptr.*, depth + 1, key_policy); written += 1; } try writer.writeByte('}'); }, .omit => { try writer.print("<object_fields={d}", .{object.count()}); if (object.count() > 0) { try writer.writeAll(" values=["); var it = object.iterator(); var written: usize = 0; while (it.next()) |entry| { if (written > 0) try writer.writeByte(','); if (written >= 6) { try writer.writeAll("..."); break; } try writeRedactedJsonValuePreview(writer, entry.value_ptr.*, depth + 1, key_policy); written += 1; } try writer.writeByte(']'); } try writer.writeByte('>'); }, } }, .array => |array| try writer.print("<array_len={d}>", .{array.items.len}), .string => |string| try writer.print("<string_bytes={d}>", .{string.len}), .integer, .float, .number_string => try writer.writeAll("<number>"), .bool => try writer.writeAll("<bool>"), .null => try writer.writeAll("<null>"), } } pub fn resetForTest() void { shutdown(); next_turn_id.store(1, .seq_cst); next_step_id.store(1, .seq_cst); } pub fn configureForTest(alloc: Allocator, path: []const u8) !void { const owned = try alloc.dupe(u8, path); defer alloc.free(owned); try configure(.{ .file_path = owned }); } pub fn configureForTestWithScopes(alloc: Allocator, path: []const u8, scope_filter: []const u8) !void { const owned = try alloc.dupe(u8, path); defer alloc.free(owned); try configure(.{ .file_path = owned, .scope_filter = scope_filter }); } pub fn configure(options: Options) !void { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (state.configured) return; state.configured = true; state.stderr_enabled = options.stderr_enabled; if (options.file_path) |path| { try ensureParentDir(path); rotateIfTooLarge(path, default_log_max_bytes); var file = try std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = false }); file.close(zio); state.file_path = std.heap.c_allocator.dupe(u8, path) catch null; } if (options.scope_filter) |filter| { const trimmed = std.mem.trim(u8, filter, " \t\r\n"); if (trimmed.len > 0) state.scope_filter = try std.heap.c_allocator.dupe(u8, trimmed); } state.enabled = state.stderr_enabled or state.file_path != null; } fn rotateIfTooLarge(path: []const u8, max_bytes: u64) void { const zio = io_mod.getIo(); var file = std.Io.Dir.openFileAbsolute(zio, path, .{}) catch return; var should_truncate = false; { defer file.close(zio); const stat = file.stat(zio) catch return; if (stat.size > max_bytes) should_truncate = true; } if (!should_truncate) return; var truncate = std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = true }) catch return; truncate.close(zio); } pub fn isEnabled() bool { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); return state.enabled; } pub fn isScopeEnabled(scope: []const u8) bool { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (!state.enabled) return false; const filter = state.scope_filter orelse return true; return scopeFilterAllows(filter, scope); } fn writeLine(line: []const u8) void { const zio = io_mod.getIo(); state_mutex.lockUncancelable(zio); defer state_mutex.unlock(zio); if (!state.enabled) return; if (state.stderr_enabled) { std.debug.print("{s}", .{line}); } if (state.file_path) |path| { appendLineToFile(zio, path, line); } } fn appendLineToFile(zio: std.Io, path: []const u8, line: []const u8) void { var file = std.Io.Dir.createFileAbsolute(zio, path, .{ .truncate = false, .lock = .exclusive, }) catch return; defer file.close(zio); _ = std.c.lseek(file.handle, 0, std.posix.SEEK.END); file.writeStreamingAll(zio, line) catch {}; } fn loadOptionsFromEnv(alloc: Allocator, workspace_root: []const u8) !Options { const trace_log = loadOptionalEnv("FX_TRACE_LOG"); const trace_flag = loadOptionalEnv("FX_TRACE"); const trace_stderr = loadOptionalEnv("FX_TRACE_STDERR"); const trace_scopes = loadOptionalEnv("FX_TRACE_SCOPES"); const stderr_enabled = isTruthy(trace_stderr); if (trace_log) |raw_path| { const resolved = try resolveLogPath(alloc, workspace_root, raw_path); return .{ .file_path = resolved, .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes }; } if (isTruthy(trace_flag)) { return .{ .file_path = try defaultLogPath(alloc), .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes, }; } return .{ .stderr_enabled = stderr_enabled, .scope_filter = trace_scopes }; } fn loadOptionalEnv(name: []const u8) ?[]const u8 { return io_mod.getenv(name); } fn isTruthy(value: ?[]const u8) bool { const raw = value orelse return false; const trimmed = std.mem.trim(u8, raw, " \t\r\n"); return std.ascii.eqlIgnoreCase(trimmed, "1") or std.ascii.eqlIgnoreCase(trimmed, "true") or std.ascii.eqlIgnoreCase(trimmed, "yes") or std.ascii.eqlIgnoreCase(trimmed, "on"); } fn scopeFilterAllows(filter: []const u8, scope: []const u8) bool { var rest = filter; while (true) { const next_comma = std.mem.findScalar(u8, rest, ','); const raw_part = if (next_comma) |idx| rest[0..idx] else rest; const part = std.mem.trim(u8, raw_part, " \t\r\n"); if (part.len > 0 and std.mem.eql(u8, part, scope)) return true; if (next_comma) |idx| { rest = rest[idx + 1 ..]; } else { return false; } } } fn resolveLogPath(alloc: Allocator, workspace_root: []const u8, raw_path: []const u8) ![]u8 { const trimmed = std.mem.trim(u8, raw_path, " \t\r\n"); if (trimmed.len == 0) return error.InvalidTracePath; if (std.fs.path.isAbsolute(trimmed)) return alloc.dupe(u8, trimmed); return std.fs.path.join(alloc, &.{ workspace_root, trimmed }); } fn defaultLogPath(alloc: Allocator) ![]u8 { if (io_mod.getenv("HOME")) |home| { return defaultLogPathForHome(alloc, home); } return fallbackLogPathForMillis(alloc, io_mod.milliTimestamp()); } fn defaultLogPathForHome(alloc: Allocator, home: []const u8) ![]u8 { return profile_paths.traceLogPath(alloc, home); } fn fallbackLogPathForMillis(alloc: Allocator, millis: i64) ![]u8 { return std.fmt.allocPrint(alloc, "/tmp/fx-trace-{d}.log", .{millis}); } fn ensureParentDir(path: []const u8) !void { const parent = std.fs.path.dirname(path) orelse return; try makeAbsolutePath(parent); } fn makeAbsolutePath(path: []const u8) !void { const zio = io_mod.getIo(); std.Io.Dir.createDirAbsolute(zio, path, .default_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => { if (std.fs.path.dirname(path)) |parent| { try makeAbsolutePath(parent); try std.Io.Dir.createDirAbsolute(zio, path, .default_dir); } else { return err; } }, }; } fn tmpRoot(alloc: Allocator, tmp: std.testing.TmpDir) ![]u8 { return io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); } fn tmpPath(alloc: Allocator, root: []const u8, name: []const u8) ![]u8 { return std.fs.path.join(alloc, &.{ root, name }); } fn readFileForTest(alloc: Allocator, path: []const u8) ![]u8 { const zio = io_mod.getIo(); var file = try std.Io.Dir.openFileAbsolute(zio, path, .{}); defer file.close(zio); return io_mod.readFileToEnd(alloc, &file, 8192); } test "isTruthy parses accepted and rejected values" { try std.testing.expect(isTruthy("1")); try std.testing.expect(isTruthy("true")); try std.testing.expect(isTruthy("YES")); try std.testing.expect(isTruthy("on")); try std.testing.expect(!isTruthy("0")); try std.testing.expect(!isTruthy("")); try std.testing.expect(!isTruthy(" \t\r\n")); try std.testing.expect(!isTruthy(null));