Skip to Content
Course content

159: Configuring Log Output Destinations

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're setting up a security camera system for a warehouse. By default, the cameras are wired to a monitor in the security office. If something happens, the guard sees it in real-time. That's great for immediate awareness, but it's useless for evidence because the feed isn't being saved. To fix this, you don't replace the cameras; you just change where the cable goes. You plug the feed into a DVR (Digital Video Recorder) to save it to a hard drive, or maybe you split the signal so it goes to both the monitor and the DVR.

In Go, the log package works exactly the same way. By default, every log.Println call is "wired" to os.Stderr (the standard error stream). It's like that monitor in the security office—it's visible in your terminal, but it vanishes the moment you close the session. To keep a permanent record, we need to redirect that output stream to something that persists, like a file.

Redirecting the Global Logger

The simplest way to change where your logs go is using log.SetOutput. This function takes any type that implements the io.Writer interface. Since a file is an io.Writer, we can open a file and tell the logger to send everything there.

package main

import (
	"log"
	"os"
)

func main() {
	// Open a file for writing. 
	// O_APPEND adds to the end, O_CREATE makes it if it doesn't exist.
	file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// This is where we "switch the cable"
	log.SetOutput(file)

	log.Println("This message will go straight to app.log, not the console!")
}

I'll be honest: using the global logger like this is fine for tiny scripts, but in a professional codebase, it's a bit of a blunt instrument. If you have different parts of your app—say, a database handler and an HTTP server—you probably don't want them all shouting into the same file without a way to distinguish them.

Creating Dedicated Loggers

Instead of hijacking the global logger, I prefer creating specific logger instances using log.New. This allows you to define exactly where the output goes, what the prefix is (like "[DATABASE]" or "[API]"), and which flags (like timestamps) are included.

package main

import (
	"log"
	"os"
)

func main() {
	file, _ := os.OpenFile("server.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

	// Create a custom logger: output destination, prefix, and flags
	serverLog := log.New(file, "SERVER: ", log.Ldate|log.Ltime|log.Lshortfile)
	
	serverLog.Println("Server started on port 8080")
}

By doing this, you can have one logger writing to a file for long-term auditing and another writing to os.Stdout for real-time debugging, all within the same program.

The Best of Both Worlds with MultiWriter

There are times when you want the "monitor" and the "DVR" simultaneously. You want to see the logs in your terminal while you're developing, but you also want them saved to a file so you can analyze them later if the app crashes at 3 AM.

Go provides a handy utility in the io package called MultiWriter. It essentially acts as a "Y-splitter" for your data stream. Anything written to the MultiWriter is duplicated to every destination you provide.

package main

import (
	"io"
	"log"
	"os"
)

func main() {
	file, _ := os.OpenFile("combined.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

	// Split the output between the file and the standard output (console)
	multi := io.MultiWriter(os.Stdout, file)

	log.SetOutput(multi)

	log.Println("I am appearing in both the terminal and the file!")
}

One quick tip: always remember to handle your file closing. Using defer file.Close() is the standard way to ensure you don't leave file handles hanging open, which could lead to memory leaks or locked files in some operating systems.




📋 Practical Task

Build a Dual-Stream Audit Logger for User Registrations

You are building a user registration system. For security reasons, you need to maintain a permanent audit trail of every registration in a file called audit.log, but for operational monitoring, you also need to see these events in the console in real-time.

Requirements:

  • Create a program that simulates a user registration process.
  • Configure the log package to use an io.MultiWriter that sends output to both os.Stdout and a file named audit.log.
  • The log output should include a custom prefix "[AUDIT] ".
  • Log a message whenever a fake user "registers" (e.g., log.Printf("User %s registered successfully", username)).
  • Ensure the file is opened with the correct flags to append data rather than overwriting it.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.