logging: document GHOSTTY_LOG and make it more flexible

pull/8815/head
Jeffrey C. Ollie 2025-09-21 00:21:14 -05:00 committed by Mitchell Hashimoto
parent 6d2beed1b0
commit f8c03bb6f6
9 changed files with 124 additions and 43 deletions

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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");

View File

@ -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: <https://github.com/ghostty-org/ghostty/issues>

View File

@ -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.

View File

@ -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");

View File

@ -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

View File

@ -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,