We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Filesystem Logging

Logging directly to the filesystem is less common these days, but it still comes up, and it's worth understanding the fundamentals. We already set up file logging, so let's review the design decisions.

Our Approach

Linko logs to a file when the LINKO_LOG_FILE environment variable is set. This involves three parts:

  1. Creating the logger that writes to the filesystem.
  2. Sending logs to both the filesystem logger and the console logger.
  3. Ensuring that logs to the filesystem are flushed when the application shuts down.

Another approach is a single logger writing to two outputs: console and file. But then both destinations must receive identical output, and you still have to flush the file correctly. We want flexibility: color in the console, no color in the file, and often JSON in the file but text in the console.

Multiple Loggers

Our initializeLogger function optionally sends logs to a file when LINKO_LOG_FILE is set:

type closeFunc func() error

func initializeLogger(logFile string) (*slog.Logger, closeFunc, error) {
	var (
		handlers []slog.Handler
		closers  []closeFunc
	)

	replaceAttr := func(groups []string, a slog.Attr) slog.Attr { /* ... */ }

	// First initialize the console logger
	handlers = append(handlers, tint.NewTextHandler(os.Stderr, &tint.Options{
		ReplaceAttr: replaceAttr,
		NoColor:     !(isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())),
	}))

	if logFile != "" {
		file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0x666)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to open log file: %w", err)
		}
		bufferedFile := bufio.NewWriter(file)
		handlers = append(handlers, slog.NewJSONHandler(bufferedFile, &slog.HandlerOptions{
			ReplaceAttr: replaceAttr,
		}))
		closers = append(closers, func() error {
			if err := bufferedFile.Flush(); err != nil {
				return fmt.Errorf("failed to flush log file: %w", err)
			}
			if err := file.Close(); err != nil {
				return fmt.Errorf("failed to close log file: %w", err)
			}
			return nil
		})
	}

	close := func() error {
		var errs []error
		for _, closer := range closers {
			errs = append(errs, closer())
		}
		return errors.Join(errs...)
	}
	return slog.New(slog.NewMultiHandler(handlers...)), close, nil
}

This structure makes future logger targets easy to add: append handlers and closers.