> ## Documentation Index
> Fetch the complete documentation index at: https://braintrust.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Redact sensitive data

> Remove sensitive data from Go SDK traces before upload with span customizers, which run on every span the SDK exports.

<Note>
  Requires Go SDK v0.16.0 or later.
</Note>

Span customizers let you change span data inside your application before the Go SDK exports it to Braintrust, for example to redact sensitive values such as email addresses or credentials, or to add tags and metadata.

A span customizer is a function that the SDK calls on each span after the span ends. The SDK runs customizers on every span it exports, including spans from integrations and spans you create with the OpenTelemetry API.

<Note>
  The Go SDK has no masking function, so span customizers are how you remove sensitive data before it leaves your application. To redact data after Braintrust receives it instead, see [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
</Note>

## Register a customizer

To create a customizer and register it with the SDK:

1. **Write the function.** It receives each completed span as an OpenTelemetry `sdktrace.ReadOnlySpan` and returns the span to export. In the function:
   * **Read the attributes you want to change.** Integrations store the span's content as JSON strings in `braintrust.input_json` (input), `braintrust.output_json` (output), and `braintrust.metadata` (metadata). Braintrust parses these into the span's fields.
   * **Return a replacement span with the new attributes.** `ReadOnlySpan` can't be modified, so wrap the span in a struct that keeps everything the same except its attributes. The `customizedSpan` type in the example below does this: it embeds the original span and overrides `Attributes()` to return the new list. To leave a span unchanged, return it as is.
2. **Register the function.** Wrap it in a `config.SpanCustomizer` and pass it to `braintrust.New` with the `braintrust.WithSpanCustomizers` option.

This example replaces email addresses in the input, output, and metadata of every span:

```go main.go expandable theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
package main

import (
	"context"
	"log"
	"regexp"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"

	"github.com/braintrustdata/braintrust-sdk-go"
	"github.com/braintrustdata/braintrust-sdk-go/config"
)

var email = regexp.MustCompile(`[\w.+-]+@[\w-]+(?:\.[\w-]+)+`)

// customizedSpan exports a span with replacement attributes.
type customizedSpan struct {
	sdktrace.ReadOnlySpan
	attrs []attribute.KeyValue
}

func (s customizedSpan) Attributes() []attribute.KeyValue { return s.attrs }

func redactEmails(span sdktrace.ReadOnlySpan) (sdktrace.ReadOnlySpan, error) {
	attrs := make([]attribute.KeyValue, 0, len(span.Attributes()))
	for _, attr := range span.Attributes() {
		switch attr.Key {
		case "braintrust.input_json", "braintrust.output_json", "braintrust.metadata":
			redacted := email.ReplaceAllString(attr.Value.AsString(), "[EMAIL]")
			attr = attribute.String(string(attr.Key), redacted)
		}
		attrs = append(attrs, attr)
	}
	return customizedSpan{ReadOnlySpan: span, attrs: attrs}, nil
}

func main() {
	ctx := context.Background()
	tp := sdktrace.NewTracerProvider()
	defer tp.Shutdown(ctx)
	otel.SetTracerProvider(tp)

	_, err := braintrust.New(tp,
		braintrust.WithProject("my-project"), // Replace with your project name
		braintrust.WithSpanCustomizers(config.SpanCustomizer{OnSpanExport: redactEmails}),
	)
	if err != nil {
		log.Fatal(err)
	}

	_, span := otel.Tracer("my-app").Start(ctx, "draft-reply")
	span.SetAttributes(attribute.String("braintrust.input_json", `{"to":"jane@example.com"}`))
	span.End()
}
```

In Braintrust, the `draft-reply` span's input shows `{"to": "[EMAIL]"}`. The customizer redacts spans from [Go SDK integrations](/docs/sdks/go/sdk-integrations) the same way.

## Add tags and metadata

To add metadata or tags to a span without losing the values the integration recorded:

1. **Read the existing values.** `braintrust.metadata` holds the span's whole metadata object as a JSON string, and `braintrust.tags` holds its tags as a string slice.
2. **Merge in your values.** Add your keys to the parsed metadata and your tags to the existing list. Replacing either attribute drops what was there.
3. **Return a replacement span** with the merged attributes, using `customizedSpan` from the previous example.

This example adds an `environment` metadata key and a `production` tag:

```go #skip-compile theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import (
	"encoding/json"
	"slices"
)

func addEnvironment(span sdktrace.ReadOnlySpan) (sdktrace.ReadOnlySpan, error) {
	metadata := map[string]any{}
	var tags []string
	attrs := make([]attribute.KeyValue, 0, len(span.Attributes())+2)
	for _, attr := range span.Attributes() {
		switch attr.Key {
		case "braintrust.metadata":
			if err := json.Unmarshal([]byte(attr.Value.AsString()), &metadata); err != nil {
				return span, nil
			}
		case "braintrust.tags":
			tags = attr.Value.AsStringSlice()
		default:
			attrs = append(attrs, attr)
		}
	}
	metadata["environment"] = "production"
	encoded, err := json.Marshal(metadata)
	if err != nil {
		return span, nil
	}
	if !slices.Contains(tags, "production") {
		tags = append(tags, "production")
	}
	attrs = append(attrs,
		attribute.String("braintrust.metadata", string(encoded)),
		attribute.StringSlice("braintrust.tags", tags),
	)
	return customizedSpan{ReadOnlySpan: span, attrs: attrs}, nil
}
```

Returning an error drops the whole export batch, as described in [How customizers run](#how-customizers-run). Choose the behavior that fits each customizer:

* **Customizers that add data**, like this one: Return the span unchanged when you can't process it. This example does that when the existing metadata isn't valid JSON.
* **Customizers that redact data**: Return an error when you can't redact a span, so the SDK drops the batch instead of sending unredacted data.

## Chain customizers

To run more than one customizer:

1. **Write each customizer separately**, so each one handles one change and you can test and reuse it on its own.
2. **Pass them to `WithSpanCustomizers` in the order you want them to run.** Each customizer receives the span that the previous one returned. If you pass `WithSpanCustomizers` more than once, the SDK appends to the list.

This example runs `redactEmails`, then `addEnvironment`:

```go #skip-compile theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
_, err := braintrust.New(tp,
	braintrust.WithSpanCustomizers(
		config.SpanCustomizer{OnSpanExport: redactEmails},
		config.SpanCustomizer{OnSpanExport: addEnvironment},
	),
)
```

## How customizers run

* **Every exported span**: The SDK runs customizers on every span it exports, including spans you create with the OpenTelemetry API. If you set `braintrust.WithFilterAISpans(true)`, the SDK exports only root spans and spans whose name or attributes start with `gen_ai.`, `braintrust.`, `llm.`, `ai.`, or `traceloop.`. Customizers don't run on the spans the filter drops.
* **Once per completed span**: The SDK runs each customizer once per span, after the span ends, and can call it from a background goroutine. Make customizers safe for concurrent use, and don't hold on to or change the `ReadOnlySpan` values they receive or return.
* **Identity**: Customizers can change anything except the trace ID, span ID, parent trace ID, and parent span ID.

<Warning>
  Customizers fail closed. The SDK drops the whole export batch, and logs `span customization failed; batch not exported`, when a customizer:

  * Returns an error
  * Panics (the SDK recovers the panic, so your application keeps running)
  * Returns `nil`, which drops the batch, not just that span
  * Changes a span's trace, span, or parent IDs

  A batch is the group of spans the SDK exports together, so it can include spans from other traces.
</Warning>

## Limitations

* **Attachments**: The SDK starts uploading [attachments](/docs/instrument/attachments) before customizers run. A customizer sees only the attachment reference, so it can't keep the file out of Braintrust.
* **Console output**: Spans that `braintrust.WithEnableTraceConsoleLog(true)` prints aren't customized.
* **Code only**: You register customizers in code. There's no environment variable for them.

## Next steps

* See all redaction options, including ingestion redaction, in [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
* See [`WithSpanCustomizers` and `config.SpanCustomizer`](/docs/sdks/go/api-reference#span-customizers) in the API reference.
* Set up [Go SDK integrations](/docs/sdks/go/sdk-integrations) for your AI libraries.
