CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-cloud-google-com--go

Google Cloud Client Libraries for Go providing documentation, authentication patterns, and utility packages for civil time types and HTTP/gRPC recording/replay functionality

Overview
Eval results
Files

rpcreplay.mddocs/

gRPC Replay Package

The rpcreplay package supports the capture and replay of gRPC calls. Its main goal is to improve testing by allowing you to record gRPC interactions with a real service, then replay them in tests as an "automatic mock" that is fast and flake-free.

Status: This package is EXPERIMENTAL and subject to change without notice.

Import

import "cloud.google.com/go/rpcreplay"

Overview

The rpcreplay package allows you to:

  1. Record gRPC calls between your application and Google API services to a file
  2. Replay those recorded calls in tests without contacting real services

This approach provides several benefits for testing:

  • Fast test execution (no network calls)
  • Deterministic test results (same responses every time)
  • Ability to test against specific gRPC responses
  • No need for credentials during replay
  • Tests can run offline

The package works by providing gRPC dial options that intercept calls. During recording, calls are forwarded to the real service and logged. During replay, calls are matched against the log and recorded responses are returned.

Recording Workflow

To record gRPC calls:

  1. Create a Recorder with NewRecorder or NewRecorderWriter
  2. Pass the recorder's DialOptions() to grpc.Dial
  3. Use the connection to make gRPC calls (which are recorded)
  4. Close the recorder to save the recording file

Replay Workflow

To replay recorded calls:

  1. Create a Replayer with NewReplayer or NewReplayerReader
  2. Pass the replayer's DialOptions() to grpc.Dial, or use Connection() for a fake connection
  3. Make the same gRPC calls (which return recorded responses)
  4. Close the replayer

Recorder Type

Type Definition

type Recorder struct {
    BeforeFunc func(string, proto.Message) error
    // Has unexported fields.
}

A Recorder records gRPC RPCs for later playback.

Fields:

  • BeforeFunc: Optional callback function that can inspect and modify requests and responses before they are written to the replay file. It does not modify messages sent to the actual service. Called with the method name and the message. If it returns an error, that error is returned to the client. Only executed for unary RPCs; streaming RPCs are not supported.

Creating a Recorder

func NewRecorder(filename string, initial []byte) (*Recorder, error)

Creates a recorder that writes to filename. The file will also store initial state for retrieval during replay. You must call Close() on the Recorder to ensure all data is written.

Parameters:

  • filename: Path to the file where interactions will be recorded
  • initial: Optional initial state (e.g., random seed, timestamp) to be saved for replay

Returns: A new Recorder instance or an error if the file cannot be created.

Example:

import (
    "context"
    "log"
    "time"

    "cloud.google.com/go/rpcreplay"
    "google.golang.org/api/option"
    "google.golang.org/grpc"
)

func recordGRPCCalls() {
    // Serialize initial state
    timeNow := time.Now()
    initialBytes, _ := timeNow.MarshalBinary()

    // Create recorder
    rec, err := rpcreplay.NewRecorder("recordings/test.rpclog", initialBytes)
    if err != nil {
        log.Fatal(err)
    }
    defer rec.Close()

    // Optional: Set up callback
    rec.BeforeFunc = func(method string, msg proto.Message) error {
        log.Printf("Recording: %s", method)
        return nil
    }

    // Create gRPC connection with recording enabled
    conn, err := grpc.Dial("api.example.com:443",
        rec.DialOptions()...,
        grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")))
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // Use connection to make gRPC calls
    // These calls will be recorded
}
func NewRecorderWriter(w io.Writer, initial []byte) (*Recorder, error)

Creates a recorder that writes to an arbitrary io.Writer. The initial bytes will also be written to w for retrieval during replay. You must call Close() on the Recorder to ensure all data is written.

Parameters:

  • w: Writer where interactions will be recorded
  • initial: Optional initial state to be saved for replay

Returns: A new Recorder instance or an error.

Example:

var buf bytes.Buffer
rec, err := rpcreplay.NewRecorderWriter(&buf, []byte("initial-state"))
if err != nil {
    log.Fatal(err)
}
defer rec.Close()

Recorder Methods

DialOptions

func (r *Recorder) DialOptions() []grpc.DialOption

Returns the options that must be passed to grpc.Dial to enable recording. These options install interceptors that capture unary and streaming RPCs.

Returns: Slice of gRPC dial options for recording.

Example:

rec, _ := rpcreplay.NewRecorder("test.rpclog", nil)
defer rec.Close()

// Combine recorder options with other dial options
conn, err := grpc.Dial(serverAddress,
    append(rec.DialOptions(),
        grpc.WithTransportCredentials(creds),
        grpc.WithBlock())...)

Close

func (r *Recorder) Close() error

Saves any unwritten information to the recording file or writer. You must call this method to ensure all recorded data is persisted.

Returns: An error if the close operation fails.

Example:

rec, _ := rpcreplay.NewRecorder("test.rpclog", nil)
defer func() {
    if err := rec.Close(); err != nil {
        log.Printf("Failed to close recorder: %v", err)
    }
}()

Replayer Type

Type Definition

type Replayer struct {
    BeforeFunc func(string, proto.Message) error
    // Has unexported fields.
}

A Replayer replays a set of RPCs saved by a Recorder.

Fields:

  • BeforeFunc: Optional callback function that can inspect and modify requests before they are matched against responses from the replay file. Called with the method name and the message. If it returns an error, that error is returned to the client. Only executed for unary RPCs; streaming RPCs are not supported.

Creating a Replayer

func NewReplayer(filename string) (*Replayer, error)

Creates a Replayer that reads from filename.

Parameters:

  • filename: Path to the file containing recorded interactions

Returns: A new Replayer instance or an error if the file cannot be read.

Example:

func replayGRPCCalls() {
    rep, err := rpcreplay.NewReplayer("recordings/test.rpclog")
    if err != nil {
        log.Fatal(err)
    }
    defer rep.Close()

    // Retrieve initial state
    var timeNow time.Time
    if err := timeNow.UnmarshalBinary(rep.Initial()); err != nil {
        log.Fatal(err)
    }

    // Get connection for replay
    conn, err := rep.Connection()
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // Use connection to make the same gRPC calls
    // These will return recorded responses
}
func NewReplayerReader(r io.Reader) (*Replayer, error)

Creates a Replayer that reads from an io.Reader.

Parameters:

  • r: Reader containing recorded interactions

Returns: A new Replayer instance or an error.

Example:

data, _ := os.ReadFile("test.rpclog")
rep, err := rpcreplay.NewReplayerReader(bytes.NewReader(data))

Replayer Methods

Connection

func (rep *Replayer) Connection() (*grpc.ClientConn, error)

Returns a fake gRPC connection suitable for replaying. This is more convenient than calling grpc.Dial with DialOptions() since a real connection is not necessary for replay.

Returns: A gRPC client connection configured for replay, or an error.

Example:

rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()

conn, err := rep.Connection()
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

// Use conn to create service clients
client := pb.NewMyServiceClient(conn)

DialOptions

func (rep *Replayer) DialOptions() []grpc.DialOption

Returns the options that must be passed to grpc.Dial to enable replaying. Use this if you need to combine replay with real dialing for some reason; otherwise, prefer Connection().

Returns: Slice of gRPC dial options for replaying.

Example:

rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()

conn, err := grpc.Dial(serverAddress, rep.DialOptions()...)

Initial

func (rep *Replayer) Initial() []byte

Returns the initial state saved by the Recorder. Use this to restore random seeds, timestamps, or other values that were used during recording.

Returns: The initial state bytes.

Example:

rep, _ := rpcreplay.NewReplayer("test.rpclog")

// Restore timestamp from initial state
var recordedTime time.Time
if err := recordedTime.UnmarshalBinary(rep.Initial()); err != nil {
    log.Fatal(err)
}

SetLogFunc

func (rep *Replayer) SetLogFunc(f func(format string, v ...interface{}))

Sets a function to be used for debug logging. The function should be safe to call from multiple goroutines. Use this to debug replay issues.

Parameters:

  • f: Logging function with Printf-style signature

Example:

rep, _ := rpcreplay.NewReplayer("test.rpclog")

// Enable debug logging
rep.SetLogFunc(func(format string, v ...interface{}) {
    log.Printf("[REPLAY] "+format, v...)
})

Close

func (rep *Replayer) Close() error

Closes the Replayer and releases associated resources.

Returns: An error if the close operation fails.

Example:

rep, _ := rpcreplay.NewReplayer("test.rpclog")
defer rep.Close()

Package-Level Functions

Fprint

func Fprint(w io.Writer, filename string) error

Reads the entries from filename and writes them to w in human-readable form. Intended for debugging replay files.

Parameters:

  • w: Writer where human-readable output will be written
  • filename: Path to the replay file

Returns: An error if reading or writing fails.

Example:

// Print replay file contents to stdout
err := rpcreplay.Fprint(os.Stdout, "test.rpclog")
if err != nil {
    log.Fatal(err)
}

FprintReader

func FprintReader(w io.Writer, r io.Reader) error

Reads the entries from r and writes them to w in human-readable form. Intended for debugging replay data.

Parameters:

  • w: Writer where human-readable output will be written
  • r: Reader containing replay data

Returns: An error if reading or writing fails.

Example:

data, _ := os.ReadFile("test.rpclog")
err := rpcreplay.FprintReader(os.Stdout, bytes.NewReader(data))

Initial State

Tests often use random or time-sensitive values to create unique resources or for other purposes. These values must be consistent between recording and replay. The initial state mechanism allows you to save and restore such values.

Recording Initial State

// Create some initial state
timeNow := time.Now()
randomSeed := rand.Int63()

// Serialize it
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, timeNow.Unix())
binary.Write(&buf, binary.LittleEndian, randomSeed)

// Pass to recorder
rec, _ := rpcreplay.NewRecorder("test.rpclog", buf.Bytes())

Restoring Initial State

// Get initial state
rep, _ := rpcreplay.NewReplayer("test.rpclog")
initialBytes := rep.Initial()

// Deserialize it
buf := bytes.NewReader(initialBytes)
var timeUnix int64
var randomSeed int64
binary.Read(buf, binary.LittleEndian, &timeUnix)
binary.Read(buf, binary.LittleEndian, &randomSeed)

timeNow := time.Unix(timeUnix, 0)
rand.Seed(randomSeed)

Callbacks (BeforeFunc)

Both Recorders and Replayers support BeforeFunc callbacks for inspecting and modifying messages:

  • Recorder.BeforeFunc: Runs before messages are written to the replay file (does not affect actual RPC)
  • Replayer.BeforeFunc: Runs before request messages are matched against the replay file

Common uses include:

  • Customized logging
  • Scrubbing sensitive data before writing to replay file
  • Modifying requests to match different test scenarios

Important: If you modify requests in the recorder's BeforeFunc, you must apply the same modifications in the replayer's BeforeFunc, or matching will fail.

Example: Scrubbing Sensitive Data

import "google.golang.org/protobuf/proto"

// Define a function to scrub messages
func scrubSensitive(method string, msg proto.Message) error {
    // Type switch on message types
    switch m := msg.(type) {
    case *pb.CreateUserRequest:
        // Clear sensitive field
        m.Password = "REDACTED"
    case *pb.AuthRequest:
        m.ApiKey = "REDACTED"
    }
    return nil
}

// Apply to both recorder and replayer
rec.BeforeFunc = scrubSensitive
rep.BeforeFunc = scrubSensitive

Nondeterminism and Matching

Unary RPCs

The replayer matches unary RPC requests by method name and request message contents. Nondeterministic call ordering is generally not a problem as long as:

  • Different requests produce different responses
  • Your application logic doesn't depend on the specific ordering of identical requests

Streaming RPCs

Streaming RPCs are matched only by method name (no message content matching). Two streams with the same method name that are started concurrently may replay in the wrong order. This is a known limitation.

Causality Violations

The replayer delivers responses immediately without waiting for other RPCs. This can violate causality in some scenarios. For example, in a Pub/Sub application where one goroutine publishes and another subscribes, the Subscribe call might return its recorded response before the Publish call begins.

Replay Limitations

  • Streaming RPCs: Send/Recv messages are replayed in recorded order; no content matching is performed
  • Stream metadata: Headers and trailers are not currently recorded or replayed
  • CloseSend: The result of CloseSend is not recorded

Complete Example: Testing with Record/Replay

package myservice_test

import (
    "context"
    "flag"
    "testing"
    "time"

    "cloud.google.com/go/rpcreplay"
    pb "example.com/myservice/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

var (
    record = flag.Bool("record", false, "record gRPC interactions")
)

func TestGRPCService(t *testing.T) {
    ctx := context.Background()
    var conn *grpc.ClientConn
    var err error

    if *record {
        // Recording mode
        timeNow := time.Now()
        initialBytes, _ := timeNow.MarshalBinary()

        rec, err := rpcreplay.NewRecorder("testdata/service.rpclog", initialBytes)
        if err != nil {
            t.Fatal(err)
        }
        defer rec.Close()

        // Optional: Set up callback for scrubbing
        rec.BeforeFunc = func(method string, msg proto.Message) error {
            t.Logf("Recording: %s", method)
            // Scrub sensitive data here
            return nil
        }

        // Dial with recording enabled
        creds, _ := credentials.NewClientTLSFromFile("cert.pem", "")
        conn, err = grpc.Dial("api.example.com:443",
            append(rec.DialOptions(),
                grpc.WithTransportCredentials(creds))...)
        if err != nil {
            t.Fatal(err)
        }
        defer conn.Close()
    } else {
        // Replay mode
        rep, err := rpcreplay.NewReplayer("testdata/service.rpclog")
        if err != nil {
            t.Fatal(err)
        }
        defer rep.Close()

        // Restore initial state
        var timeNow time.Time
        if err := timeNow.UnmarshalBinary(rep.Initial()); err != nil {
            t.Fatal(err)
        }

        // Optional: Set up callback (must match recorder's transformations)
        rep.BeforeFunc = func(method string, msg proto.Message) error {
            t.Logf("Replaying: %s", method)
            return nil
        }

        // Enable debug logging
        rep.SetLogFunc(t.Logf)

        // Get fake connection
        conn, err = rep.Connection()
        if err != nil {
            t.Fatal(err)
        }
        defer conn.Close()
    }

    // Create service client
    client := pb.NewMyServiceClient(conn)

    // Make gRPC calls (recorded or replayed)
    resp, err := client.GetResource(ctx, &pb.GetResourceRequest{
        Id: "test-resource-123",
    })
    if err != nil {
        t.Fatal(err)
    }

    // Verify response
    if resp.Name == "" {
        t.Error("Expected non-empty name")
    }
}

To use this test:

# Record interactions (requires credentials and network)
go test -record -v

# Replay interactions (no credentials or network needed)
go test -v

Debugging Replay Issues

When replay fails to match requests, use these techniques:

Enable Debug Logging

rep.SetLogFunc(func(format string, v ...interface{}) {
    log.Printf("[REPLAY DEBUG] "+format, v...)
})

Inspect Replay File

// Print replay file in human-readable format
err := rpcreplay.Fprint(os.Stdout, "test.rpclog")

Check BeforeFunc Consistency

Ensure that any modifications made in the recorder's BeforeFunc are also made in the replayer's BeforeFunc.

Verify Request Messages

Make sure the requests during replay exactly match those during recording, including all fields.

Best Practices

  1. Store recordings in version control: Commit replay files for consistent test behavior across environments

  2. Use initial state for time-dependent tests: Save timestamps, random seeds, or other values that affect behavior

  3. Scrub sensitive data: Use BeforeFunc to redact passwords, API keys, and other secrets before writing to replay files

  4. Keep BeforeFunc consistent: Apply the same transformations during both recording and replay

  5. Enable debug logging during development: Use SetLogFunc to troubleshoot matching issues

  6. Inspect replay files: Use Fprint to examine recorded interactions in human-readable form

  7. Re-record when APIs change: Update recordings when the gRPC service changes to avoid stale test data

  8. Test both modes periodically: Run tests in recording mode occasionally to ensure your application still works with the real service

  9. Be aware of streaming limitations: Avoid concurrent streams with the same method name

  10. Handle causality carefully: Be aware that replay doesn't preserve timing relationships between RPCs

Install with Tessl CLI

npx tessl i tessl/golang-cloud-google-com--go@0.123.0

docs

civil.md

httpreplay.md

index.md

rpcreplay.md

tile.json