Skip to Content
Course content

93: Working with the log Package

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

A few years ago, I had a colleague who relied entirely on fmt.Println for debugging a background worker. It worked great on his local machine. But once it hit production, we encountered a race condition that only triggered once every few thousand requests. He spent three days trying to correlate the "something went wrong" printouts with the server's system logs, but since fmt doesn't provide timestamps by default, he was basically guessing when the errors actually occurred. That's the moment you realize that "printing" isn't "logging."

The standard log package in Go is your first line of defense against this kind of frustration. While it's simple, it provides the essential metadata—like the date and time—that transforms a random string of text into a useful diagnostic record. More importantly, it writes to standard error (stderr) by default, which is the convention for logs in most cloud and container environments.

Moving Beyond Simple Printing

When you use log.Println or log.Printf, you're getting more than just a line of text. By default, the logger prefixes every entry with the current date and time. This seems trivial until you're staring at a log file with 10,000 lines trying to find exactly when a database connection timed out.

package main

import (
    "log"
)

func main() {
    log.Println("Starting the server on port 8080...")
    
    // Simulating an error
    err := doSomething()
    if err != nil {
        log.Printf("Critical error encountered: %v", err)
    }
}

func doSomething() error {
    return someError // imagine a real error here
}

I usually recommend log.Printf for most production scenarios because it allows you to inject variables into your log messages cleanly. It's a small habit, but it makes your logs much more readable than concatenating strings with plus signs.

Adding Context with Custom Flags

The default timestamp is a start, but when your project grows to dozens of files, knowing when something happened isn't enough—you need to know where it happened. This is where log flags come in. I almost always add log.Lshortfile to my projects; it appends the filename and line number to every log entry.

package main

import (
    "log"
)

func main() {
    // Ldate: the date in the local time zone: 2009/01/23
    // Ltime: the time in the local time zone: 01:23:23
    // Lshortfile: final file name element and line number: main.go:23
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)

    log.Println("This log now tells me exactly which line of code produced it.")
}

Be careful not to overdo it. Adding too much metadata to every single line can bloat your logs and make them harder to read for humans. Use Lshortfile for errors and warnings, but maybe stick to basic timestamps for general info logs.

Persisting Logs to a File

Writing to the console is great for development, but in a real-world app, you want those logs stored somewhere permanent. The log package allows you to redirect the output to any object that implements the io.Writer interface, which includes files.

package main

import (
    "log"
    "os"
)

func main() {
    // Open a file for writing. 
    // O_APPEND keeps old logs, O_CREATE makes the file if it's missing, 
    // O_WRONLY opens it for writing only.
    file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // Set the output of the standard logger to the file
    log.SetOutput(file)

    log.Println("This message will be saved to app.log instead of the console.")
}

One quick tip: notice the use of log.Fatal in the error check. log.Fatal is a powerful tool—it logs the message and then immediately calls os.Exit(1). Use it sparingly, typically only during the application startup phase. If you call it inside a request handler in a web server, you'll crash the entire server just because one request failed, which is definitely not what you want.




📋 Practical Task

Implementing a Persistent Application Audit Log

Build a small Go program that simulates a user authentication system. Your task is to implement a logging system that records security events to a local file.

  • Create a file named auth.log to store the logs.
  • Configure the log package to include the date, time, and the short file name/line number (Lshortfile).
  • Create a function called logEvent(event string, userID int) that writes a formatted message to the log file (e.g., "User 123: Login Successful").
  • In your main function, simulate three different events: a successful login, a failed password attempt, and a password reset.
  • Ensure that if the log file cannot be opened, the program uses log.Fatal to exit immediately.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.