(Don't) Leave No Trace: Capture The Context of Significant Events With trace.FlightRecorder (New in Go 1.25)

When you need to capture information about activity in your application right before a certain event, capturing a trace seems the way to go. But collecting (and digging through) loads of trace output for inspecting a few seconds of activity isn't fun. It would be much better to record only the last few seconds of activity and drop the rest, to save space and energy.

In Go 1.25, the new type FlightRecorder makes this possible (and easy to implement). FlightRecorder puts a sliding window over the execution trace produced by the runtime. At predefined events, the application can call FlightRecorder.WriteTo() to snapshot the current window and write it to an io.Writer.

Implementation is quite straightforward. Assume you have a long-running process, such as a Web server, and you want to occasionally collect a trace output from certain events.

  • Create a FlightRecorder configuration (if the default window size settings don't suit)
  • Create a new FlightRecorder object
  • Define mechanisms to call the methods
    • Start() to start tracing
    • Stop() to stop tracing
    • WriteTo() to capture the current trace window to a file

A minimal implementation could look like this:

At startup, Create and start the flight recorder:

	cfg := trace.FlightRecorderConfig{
		MinAge: 5 * time.Second,
	}

	// Create and start the flight recorder
	fr := trace.NewFlightRecorder(cfg)
	if err := fr.Start(); err != nil {
		log.Fatalf("Failed to start flight recorder: %v", err)
	}
	defer fr.Stop()

  // ... continue with running the app

At certain interesting events, add code to call WriteTo(). Or, if you would like to trigger trace output manually, have your code react to a Unix signal like SIGUSR1 to trigger a call to WriteTo:

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGUSR1, syscall.SIGTERM, syscall.SIGINT)

	for {
		sig := <-sigChan
		switch sig {
		case syscall.SIGUSR1:
			fmt.Println("Received SIGUSR1, saving trace...")
			if err := saveTrace(fr); err != nil { // see below
				log.Printf("Failed to save trace: %v", err)
			} else {
				fmt.Println("Trace saved to 'trace.out'")
			}
		case syscall.SIGTERM, syscall.SIGINT:
			fmt.Println("Received termination signal, shutting down...")
			close(stopWorkload)
			wg.Wait()
			return
		}
	}
}

And saveTrace() would then test if the flight recorder is running, and if so, capture the current trace window to trace.out:

func saveTrace(fr *trace.FlightRecorder) error {
	if !fr.Enabled() {
		return fmt.Errorf("flight recorder is not enabled")
	}

	file, err := os.Create("trace.out")
	if err != nil {
		return fmt.Errorf("failed to create trace file: %w", err)
	}
	defer file.Close()

	n, err := fr.WriteTo(file)
	if err != nil {
		return fmt.Errorf("failed to write trace data: %w", err)
	}

	fmt.Printf("Wrote %d bytes to trace.out\n", n)
	return nil
}

That's a convenient way of tracing your code when needed without needlessly spooling out megabytes of trace information just to capture a few bytes of interest.

The full code is avaialble in the Go Playground, but note that this time, the code isn't useful inside the playground. Copy it to your machine and run it (with Go1.25rc1 or later!), then you can send a USR1 signal to the process with…

kill -s USR1 <pid>

…where <pid> is the PID the app prints out at the start, and inspect the trace with…

go tool trace trace.out

…but ensure to use Go 1.25 RC1 or later here as well, or you'd get an “unsupported trace version: go 1.25” error message.

Now, whenever you need to inspect a trace of the recent activities, fire a USR1 at the app and see what's going on inside.