Skip to content

Getting started

This tutorial walks you through redacting your first secret-bearing string, end to end. You will sanitise a string that carries three different kinds of credential, wrap an error the same way, and — most importantly — learn where in your program redact belongs and where it does not.

Everything runs in one self-contained Go program so you can see each moving part.

Prerequisites

  • Go 1.26 or newer.
  • A new module to experiment in:
mkdir redact-tutorial && cd redact-tutorial
go mod init example.test/redact-tutorial
go get gitlab.com/phpboyscout/go/redact

Step 1 — Redact a string

redact.String takes any string and returns a sanitised copy. It is safe to call on anything: it never panics, and if nothing sensitive matches it returns the input unchanged.

Start with a string that hides three secrets at once — a credential in the URL userinfo, an api_key= query parameter, and a bare sk- provider token:

package main

import (
    "fmt"

    "gitlab.com/phpboyscout/go/redact"
)

func main() {
    raw := "request to https://admin:hunter2@api.example.com/v1?api_key=sk-abc123XYZ" +
        " failed: leaked sk-abc123def456ghi789jkl"

    fmt.Println(redact.String(raw))
}

Run it:

go run .

You will see:

request to https://<redacted>@api.example.com/v1?api_key=*** failed: leaked sk-***

Three independent rules fired, in order:

  • URL userinfoadmin:hunter2@ became <redacted>@. The scheme and host are preserved so the URL is still recognisable.
  • Credential query parameterapi_key=sk-abc123XYZ became api_key=***. The parameter name survives; its value is masked whole.
  • Provider prefix — the standalone sk-abc123def456ghi789jkl token became sk-***. The literal sk- prefix is kept for readability; everything after it is gone.

Notice how much shape survives. Redaction is not deletion — you keep enough structure to debug with, and lose only the secret.

Step 2 — Redact an error

Most secrets leak through error messages: a failed request wraps the URL it called, a config loader quotes the token it rejected. redact.Error is a nil-safe convenience wrapper — it is exactly redact.String(err.Error()), but returns "" when the error is nil, so you never have to guard the call.

package main

import (
    "errors"
    "fmt"

    "gitlab.com/phpboyscout/go/redact"
)

func main() {
    err := errors.New("dial https://svc:p4ssw0rd@db.internal:5432 refused")

    fmt.Println(redact.Error(err)) // dial https://<redacted>@db.internal:5432 refused
    fmt.Println(redact.Error(nil)) // (empty line)
}

Reach for redact.Error at the point where you hand an error to a logger, a telemetry span, or a user-facing report — not before.

Step 3 — Know where to call it

This is the part that matters most, and the part a pattern list cannot decide for you.

Call redact.String / redact.Error on the way out — anywhere a caller-supplied or environment-derived string crosses the boundary into a surface you do not fully control:

  • telemetry and tracing exporters (OTLP, vendor SDKs);
  • distributed / shipped log lines that land in an aggregator;
  • error reports sent to a third-party issue tracker or crash service;
  • metrics labels and any managed observability ingest.

Do not reach for it on host-only debug logs that never leave the machine. Those are inside your trust boundary and may genuinely need the raw content to be useful for debugging. Redacting them buys you nothing and costs you signal.

The rule of thumb: redact at the last hop before data leaves your control, once, close to the exporter. See Redact at the boundary for how to wire that up as a single choke point.

Success criterion

The program in Step 1 prints the redacted line with <redacted>@, api_key=***, and sk-***. If you see the raw secrets, check that you printed the return value of redact.String rather than rawString returns a new string and never mutates its input.

Next steps