Architecture of lazygit's Logging and Debugging System: A Deep Dive
lazygit employs a dual-layer logging architecture built on logrus that separates structured JSON file diagnostics (via NewDevelopmentLogger) from human-readable UI command logs, with both systems accessible through a shared *logrus.Entry stored in the Common struct.
The lazygit terminal UI client implements a sophisticated yet lightweight logging and debugging architecture designed to support both end-user troubleshooting and developer diagnostics. Built on top of the logrus structured logging library, the system orchestrates file-based debug logs, environment-driven configuration, and real-time log tailing through a centralized dependency injection pattern. Understanding this architecture is essential for contributors extending the codebase or debugging complex Git operations.
High-Level Architecture Overview
The lazygit logging system operates through distinct architectural layers that handle CLI parsing, logger instantiation, and output distribution.
Entry Point and Configuration
The logging lifecycle begins in pkg/app/entry_point.go (lines 95-98), where the application parses the --debug (-d) flag and checks for the legacy DEBUG=TRUE environment variable. When activated, this triggers the instantiation of a development logger rather than the production null logger.
Logger Factory and Common Struct
The pkg/logs/logs.go file serves as the central factory for logger instances. The NewDevelopmentLogger function creates a JSON-writing file logger, while NewProductionLogger returns a discarded logger for normal operation. The resulting *logrus.Entry is stored in common.Common.Log and injected throughout the application via the newLogger() function in pkg/app/app.go (lines 82-91).
UI Command Log vs File Logger
lazygit maintains two independent logging mechanisms. The structured file logger captures diagnostic JSON output for debugging purposes, while the UI command log displays human-readable action summaries in the Extras panel. The UI implementation in pkg/gui/command_log_panel.go uses LogAction and LogCommand for display-only output that never persists to disk.
Core Components and Implementation Details
Flag and Environment Variable Handling
In pkg/app/entry_point.go, the application evaluates debug mode through both modern CLI flags and backward-compatible environment variables:
flaggy.Bool(&debug, "d", "debug", "Run in debug mode with logging...")
if os.Getenv("DEBUG") == "TRUE" {
debug = true
}
Log File Resolution
The pkg/config/app_config.go file (lines 713-718) defines the LogPath() function, which determines the log destination through environment variable or default path resolution:
func LogPath() (string, error) {
if os.Getenv("LAZYGIT_LOG_PATH") != "" {
return os.Getenv("LAZYGIT_LOG_PATH"), nil
}
return stateFilePath("development.log")
}
Development vs Production Loggers
The pkg/logs/logs.go file contains the core logging logic. The NewDevelopmentLogger function configures logrus with a JSON formatter and file output:
func NewDevelopmentLogger(logPath string) *logrus.Entry {
logger := logrus.New()
logger.SetLevel(getLogLevel())
file, err := os.OpenFile(logPath,
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil { log.Fatalf("Unable to log to log file: %v", err) }
logger.SetOutput(file)
return formatted(logger) // Attaches JSONFormatter
}
The getLogLevel() function (lines 57-64) reads the LOG_LEVEL environment variable, defaulting to DebugLevel when parsing fails or the variable is unset.
Wiring the Logger into the Application
In pkg/app/app.go (lines 82-91), the newLogger() function instantiates the appropriate logger based on the debug configuration:
func newLogger(cfg config.AppConfigurer) *logrus.Entry {
if cfg.GetDebug() {
logPath, err := config.LogPath()
if err != nil { log.Fatal(err) }
return logs.NewDevelopmentLogger(logPath)
}
return logs.NewProductionLogger()
}
Real-time Log Viewing
The pkg/logs/tail/tail.go file implements the lazygit --logs functionality. The TailLogs function streams the JSON log file and pretty-prints it using the humanlog library:
func TailLogs(logFilePath string) {
fmt.Printf("Tailing log file %s\n\n", logFilePath)
tailLogsForPlatform(logFilePath, humanlog.DefaultOptions)
}
Practical Usage Examples
Enable Debug Logging with Custom Path
Run lazygit with debug mode and specify a custom log file location:
LAZYGIT_LOG_PATH=/tmp/lg-debug.log LOG_LEVEL=debug lazygit --debug
This writes structured JSON logs to /tmp/lg-debug.log with debug-level verbosity.
View Logs in Real-Time
In a separate terminal, tail the log file with human-readable formatting:
lazygit --logs
Or view the raw JSON directly:
tail -f ~/.config/jesseduffield/lazygit/development.log
Add Logging to Custom Components
When extending lazygit, access the logger through the Common struct:
package myfeature
import "github.com/jesseduffield/lazygit/pkg/common"
func DoSomething(c *common.Common) {
c.Log.Infof("myfeature.DoSomething started")
// ... logic ...
c.Log.Errorf("myfeature.DoSomething failed: %v", err)
}
Summary
- Debug activation occurs via the
--debugflag orDEBUG=TRUEenvironment variable, triggeringNewDevelopmentLoggerinpkg/logs/logs.go. - Log file location defaults to
development.login the config directory, overrideable viaLAZYGIT_LOG_PATHas resolved byLogPath()inpkg/config/app_config.go. - Log level control uses the
LOG_LEVELenvironment variable, parsed bygetLogLevel()to configure logrus thresholds. - Shared access is provided through
common.Common.Log, a*logrus.Entryinjected into all subsystems viapkg/app/app.go(lines 82-91). - Dual logging system separates structured JSON diagnostics (file) from human-readable action summaries (UI Extras panel via
pkg/gui/command_log_panel.go). - Log viewing via
lazygit --logsusesTailLogsinpkg/logs/tail/tail.gowith humanlog formatting for readable output.
Frequently Asked Questions
How do I enable debug logging in lazygit?
Enable debug logging by running lazygit --debug or setting the environment variable DEBUG=TRUE before launching. This instantiates the development logger via NewDevelopmentLogger in pkg/logs/logs.go and begins writing structured JSON logs to the configured log file path.
Where are lazygit log files stored?
By default, lazygit stores log files in the user configuration directory at ~/.config/jesseduffield/lazygit/development.log. Override this location by setting the LAZYGIT_LOG_PATH environment variable, which is checked by the LogPath() function in pkg/config/app_config.go (lines 713-718).
What is the difference between the file logger and the UI command log?
The file logger captures detailed, structured JSON output for debugging purposes and writes to a file via logrus in pkg/logs/logs.go. The UI command log displays simplified, human-readable action strings in the Extras panel using LogAction and LogCommand in pkg/gui/command_log_panel.go, and does not persist to disk or use the JSON formatter.
How can I change the log level in lazygit?
Set the LOG_LEVEL environment variable to debug, info, warn, or error before starting lazygit. The getLogLevel() function in pkg/logs/logs.go (lines 57-64) parses this variable and configures the logrus logger accordingly, defaulting to DebugLevel if the variable is unset or contains an invalid value.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →