From f8c03bb6f6ff7cf71c7e04077059173859496cd2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 21 Sep 2025 00:21:14 -0500 Subject: [PATCH] logging: document GHOSTTY_LOG and make it more flexible --- HACKING.md | 30 ++++++++++++++++++ src/Surface.zig | 3 ++ src/apprt/gtk/class/application.zig | 6 +++- src/build/GhosttyXcodebuild.zig | 2 +- src/build/mdgen/ghostty_1_footer.md | 13 ++++++++ src/build/mdgen/ghostty_5_header.md | 39 +++++++++++++++++++---- src/cli/args.zig | 2 +- src/global.zig | 23 ++++++-------- src/main_ghostty.zig | 49 ++++++++++++++++------------- 9 files changed, 124 insertions(+), 43 deletions(-) diff --git a/HACKING.md b/HACKING.md index 0a4bbef20..bde50ec99 100644 --- a/HACKING.md +++ b/HACKING.md @@ -93,6 +93,36 @@ produced. > may ask you to fix it and close the issue. It isn't a maintainers job to > review a PR so broken that it requires significant rework to be acceptable. +## Logging + +Ghostty can write logs to a number of destinations. On all platforms, logging to +`stderr` is available. Depending on the platform and how Ghostty was launched, +logs sent to `stderr` may be stored by the system and made available for later +retrieval. + +On Linux if Ghostty is launched by the default `systemd` user service, you can use +`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`. + +On macOS logging to the macOS unified log is available and enabled by default. +Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. + +Ghostty's logging can be configured in two ways. The first is by what +optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug` +optimizations debug logs will be output to `stderr`. If Ghostty is compiled with +any other optimization the debug logs will not be output to `stderr`. + +Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used +to control which destinations receive logs. Ghostty currently defines two +destinations: + +- `stderr` - logging to `stderr`. +- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. + ## Linting ### Prettier diff --git a/src/Surface.zig b/src/Surface.zig index 19c2662c1..96aaf84d8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -607,6 +607,9 @@ pub fn init( }; errdefer env.deinit(); + // don't leak GHOSTTY_LOG to any subprocesses + env.remove("GHOSTTY_LOG"); + // Initialize our IO backend var io_exec = try termio.Exec.init(alloc, .{ .command = command, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index d404304d0..c951cc6ac 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -8,6 +8,8 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const build_config = @import("../../../build_config.zig"); +const state = &@import("../../../global.zig").state; const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); const cgroup = @import("../cgroup.zig"); @@ -2677,7 +2679,9 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void { /// disable it. @"vulkan-disable": bool = false, } = .{ - .opengl = config.@"gtk-opengl-debug", + // `gtk-opengl-debug` dumps logs directly to stderr so both must be true + // to enable OpenGL debugging. + .opengl = state.logging.stderr and config.@"gtk-opengl-debug", }; var gdk_disable: struct { diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 27691d744..5ca4c5e9a 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -151,7 +151,7 @@ pub fn init( // This overrides our default behavior and forces logs to show // up on stderr (in addition to the centralized macOS log). - open.setEnvironmentVariable("GHOSTTY_LOG", "1"); + open.setEnvironmentVariable("GHOSTTY_LOG", "stderr,macos"); // Configure how we're launching open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run"); diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index 88aa16273..a63a85fd4 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -37,6 +37,19 @@ precedence over the XDG environment locations. : **WINDOWS ONLY:** alternate location to search for configuration files. +**GHOSTTY_LOG** + +: The `GHOSTTY_LOG` environment variable can be used to control which +destinations receive logs. Ghostty currently defines two destinations: + +: - `stderr` - logging to `stderr`. +: - `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +: Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. + # BUGS See GitHub issues: diff --git a/src/build/mdgen/ghostty_5_header.md b/src/build/mdgen/ghostty_5_header.md index b9d4cb751..2b12f546a 100644 --- a/src/build/mdgen/ghostty_5_header.md +++ b/src/build/mdgen/ghostty_5_header.md @@ -73,16 +73,12 @@ the public config files of many Ghostty users for examples and inspiration. ## Configuration Errors If your configuration file has any errors, Ghostty does its best to ignore -them and move on. Configuration errors currently show up in the log. The log -is written directly to stderr, so it is up to you to figure out how to access -that for your system (for now). On macOS, you can also use the system `log` CLI -utility with `log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. +them and move on. Configuration errors will be logged. ## Debugging Configuration You can verify that configuration is being properly loaded by looking at the -debug output of Ghostty. Documentation for how to view the debug output is in -the "building Ghostty" section at the end of the README. +debug output of Ghostty. In the debug output, you should see in the first 20 lines or so messages about loading (or not loading) a configuration file, as well as any errors it may have @@ -93,3 +89,34 @@ will fall back to default values for erroneous keys. You can also view the full configuration Ghostty is loading using `ghostty +show-config` from the command-line. Use the `--help` flag to additional options for that command. + +## Logging + +Ghostty can write logs to a number of destinations. On all platforms, logging to +`stderr` is available. Depending on the platform and how Ghostty was launched, +logs sent to `stderr` may be stored by the system and made available for later +retrieval. + +On Linux if Ghostty is launched by the default `systemd` user service, you can use +`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`. + +On macOS logging to the macOS unified log is available and enabled by default. +--Use the system `log` CLI to view Ghostty's logs: `sudo log stream level debug +--predicate 'subsystem=="com.mitchellh.ghostty"'`. + +Ghostty's logging can be configured in two ways. The first is by what +optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug` +optimizations debug logs will be output to `stderr`. If Ghostty is compiled with +any other optimization the debug logs will not be output to `stderr`. + +Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used +to control which destinations receive logs. Ghostty currently defines two +destinations: + +- `stderr` - logging to `stderr`. +- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms). + +Combine values with a comma to enable multiple destinations. Prefix a +destination with `no-` to disable it. Enabling and disabling destinations +can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all +destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations. diff --git a/src/cli/args.zig b/src/cli/args.zig index 43a15ca06..bd5060d69 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -604,7 +604,7 @@ pub fn parseAutoStruct( return result; } -fn parsePackedStruct(comptime T: type, v: []const u8) !T { +pub fn parsePackedStruct(comptime T: type, v: []const u8) !T { const info = @typeInfo(T).@"struct"; comptime assert(info.layout == .@"packed"); diff --git a/src/global.zig b/src/global.zig index 8034fabe0..29eaf5f36 100644 --- a/src/global.zig +++ b/src/global.zig @@ -39,9 +39,13 @@ pub const GlobalState = struct { resources_dir: internal_os.ResourcesDir, /// Where logging should go - pub const Logging = union(enum) { - disabled: void, - stderr: void, + pub const Logging = packed struct { + /// Whether to log to stderr. For lib mode we always disable stderr + /// logging by default. Otherwise it's enabled by default. + stderr: bool = build_config.app_runtime != .none, + /// Whether to log to macOS's unified logging. Enabled by default + /// on macOS. + macos: bool = builtin.os.tag.isDarwin(), }; /// Initialize the global state. @@ -61,7 +65,7 @@ pub const GlobalState = struct { .gpa = null, .alloc = undefined, .action = null, - .logging = .{ .stderr = {} }, + .logging = .{}, .rlimits = .{}, .resources_dir = .{}, }; @@ -100,12 +104,7 @@ pub const GlobalState = struct { // If we have an action executing, we disable logging by default // since we write to stderr we don't want logs messing up our // output. - if (self.action != null) self.logging = .{ .disabled = {} }; - - // For lib mode we always disable stderr logging by default. - if (comptime build_config.app_runtime == .none) { - self.logging = .{ .disabled = {} }; - } + if (self.action != null) self.logging.stderr = false; // I don't love the env var name but I don't have it in my heart // to parse CLI args 3 times (once for actions, once for config, @@ -114,9 +113,7 @@ pub const GlobalState = struct { // easy to set. if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { defer v.deinit(self.alloc); - if (v.value.len > 0) { - self.logging = .{ .stderr = {} }; - } + self.logging = cli.args.parsePackedStruct(Logging, v.value) catch .{}; } // Setup our signal handlers before logging diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 261e0ad7d..72d602989 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -118,19 +118,17 @@ fn logFn( comptime format: []const u8, args: anytype, ) void { - // Stuff we can do before the lock - const level_txt = comptime level.asText(); - const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; - - // Lock so we are thread-safe - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - // On Mac, we use unified logging. To view this: // // sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' // - if (builtin.target.os.tag.isDarwin()) { + // macOS logging is thread safe so no need for locks/mutexes + macos: { + if (comptime !builtin.target.os.tag.isDarwin()) break :macos; + if (!state.logging.macos) break :macos; + + const prefix = if (scope == .default) "" else @tagName(scope) ++ ": "; + // Convert our levels to Mac levels const mac_level: macos.os.LogType = switch (level) { .debug => .debug, @@ -143,26 +141,35 @@ fn logFn( // but we shouldn't be logging too much. const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope)); defer logger.release(); - logger.log(std.heap.c_allocator, mac_level, format, args); + logger.log(std.heap.c_allocator, mac_level, prefix ++ format, args); } - switch (state.logging) { - .disabled => {}, + stderr: { + // don't log debug messages to stderr unless we are a debug build + if (comptime builtin.mode != .Debug and level == .debug) break :stderr; - .stderr => { - // Always try default to send to stderr - var buffer: [1024]u8 = undefined; - var stderr = std.fs.File.stderr().writer(&buffer); - const writer = &stderr.interface; - nosuspend writer.print(level_txt ++ prefix ++ format ++ "\n", args) catch return; - // TODO: Do we want to use flushless stderr in the future? - writer.flush() catch {}; - }, + // skip if we are not logging to stderr + if (!state.logging.stderr) break :stderr; + + // Lock so we are thread-safe + var buf: [64]u8 = undefined; + const stderr = std.debug.lockStderrWriter(&buf); + defer std.debug.unlockStderrWriter(); + + const level_txt = comptime level.asText(); + const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch break :stderr; + nosuspend stderr.flush() catch break :stderr; } } pub const std_options: std.Options = .{ // Our log level is always at least info in every build mode. + // + // Note, we don't lower this to debug even with conditional logging + // via GHOSTTY_LOG because our debug logs are very expensive to + // calculate and we want to make sure they're optimized out in + // builds. .log_level = switch (builtin.mode) { .Debug => .debug, else => .info,