> ## 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 Java SDK traces before upload with span customizers, which run on every span the SDK exports.

<Note>
  Requires Java SDK v0.3.25 or later.
</Note>

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

A span customizer is a class 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 Java 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 class.** Implement `SpanCustomizer` and override `onSpanExport(SpanData span)`. The method receives each completed span as OpenTelemetry `SpanData` and returns the span to export. `SpanCustomizer` isn't a functional interface, so you can't use a lambda. In the method:
   * **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.** `SpanData` can't be modified, so return a copy that keeps everything the same except its attributes. The `withAttributes()` helper in the example below does this: it wraps the span in a `DelegatingSpanData` and overrides `getAttributes()` to return the new attributes. To leave a span unchanged, return it as is.
2. **Register the class.** Call `addSpanCustomizer()` on `BraintrustConfig.Builder`, then pass the config to `Braintrust.get(config)`.

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

```java RedactEmails.java expandable #skip-compile theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import dev.braintrust.Braintrust;
import dev.braintrust.config.BraintrustConfig;
import dev.braintrust.trace.SpanCustomizer;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.trace.data.DelegatingSpanData;
import io.opentelemetry.sdk.trace.data.SpanData;
import java.util.List;
import java.util.regex.Pattern;

public class RedactEmails {
    static final Pattern EMAIL = Pattern.compile("[\\w.+-]+@[\\w-]+(?:\\.[\\w-]+)+");
    static final List<AttributeKey<String>> CONTENT_KEYS = List.of(
        AttributeKey.stringKey("braintrust.input_json"),
        AttributeKey.stringKey("braintrust.output_json"),
        AttributeKey.stringKey("braintrust.metadata"));

    static SpanData withAttributes(SpanData span, Attributes attributes) {
        return new DelegatingSpanData(span) {
            @Override
            public Attributes getAttributes() {
                return attributes;
            }
        };
    }

    static class RedactEmailAddresses implements SpanCustomizer {
        @Override
        public SpanData onSpanExport(SpanData span) {
            AttributesBuilder attributes = span.getAttributes().toBuilder();
            for (AttributeKey<String> key : CONTENT_KEYS) {
                String value = span.getAttributes().get(key);
                if (value != null) {
                    attributes.put(key, EMAIL.matcher(value).replaceAll("[EMAIL]"));
                }
            }
            return withAttributes(span, attributes.build());
        }
    }

    public static void main(String[] args) {
        var config = BraintrustConfig.builder()
            .defaultProjectName("my-project") // Replace with your project name
            .addSpanCustomizer(new RedactEmailAddresses())
            .build();
        var openTelemetry = Braintrust.get(config).openTelemetryCreate();

        var span = openTelemetry.getTracer("my-app").spanBuilder("draft-reply").startSpan();
        span.setAttribute("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 [Java SDK integrations](/docs/sdks/java/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 array.
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 the `withAttributes()` helper from the previous example.

This example adds an `environment` metadata key and a `production` tag. It uses Jackson to parse the metadata, so add `com.fasterxml.jackson.core:jackson-databind` to your build's dependencies. It also reuses the imports from the previous example:

```java #skip-compile theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.ArrayList;

static class AddEnvironment implements SpanCustomizer {
    static final ObjectMapper JSON = new ObjectMapper();
    static final AttributeKey<String> METADATA = AttributeKey.stringKey("braintrust.metadata");
    static final AttributeKey<List<String>> TAGS = AttributeKey.stringArrayKey("braintrust.tags");

    @Override
    public SpanData onSpanExport(SpanData span) {
        try {
            String existing = span.getAttributes().get(METADATA);
            ObjectNode metadata = existing == null
                ? JSON.createObjectNode()
                : (ObjectNode) JSON.readTree(existing);
            metadata.put("environment", "production");
            List<String> existingTags = span.getAttributes().get(TAGS);
            List<String> tags = existingTags == null ? new ArrayList<>() : new ArrayList<>(existingTags);
            if (!tags.contains("production")) {
                tags.add("production");
            }
            return withAttributes(span, span.getAttributes().toBuilder()
                .put(METADATA, JSON.writeValueAsString(metadata))
                .put(TAGS, tags)
                .build());
        } catch (Exception e) {
            return span;
        }
    }
}
```

If `onSpanExport()` throws an exception, the SDK 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**: Throw an exception 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. **Call `addSpanCustomizer()` once for each, in the order you want them to run.** Each customizer receives the span that the previous one returned.

This example runs `RedactEmailAddresses`, then `AddEnvironment`:

```java #skip-compile theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
var config = BraintrustConfig.builder()
    .addSpanCustomizer(new RedactEmailAddresses())
    .addSpanCustomizer(new AddEnvironment())
    .build();
```

## 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 enable `BRAINTRUST_FILTER_AI_SPANS`, the SDK exports only spans with an attribute that starts 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, on the background export thread.
* **Before attachment upload**: The SDK runs customizers before it converts inline base64 data in span attributes to [attachments](/docs/instrument/attachments) and uploads them. A customizer can remove file data before it leaves your process.
* **Identity**: Customizers can change anything except the trace ID, span ID, and parent span ID, and must not return `null`.

<Warning>
  Customizers fail closed. The SDK drops the whole export batch, and logs `Failed to customize spans for export`, when a customizer:

  * Throws an exception
  * Returns `null`, which drops the batch, not just that span
  * Changes a span's trace, span, or parent span ID

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

## Limitations

* **Code only**: You register customizers in code, on `BraintrustConfig`. There's no environment variable or system property for them.
* **Java agent**: The [Java agent](/docs/sdks/java/install-and-instrument) configures itself only from `BRAINTRUST_*` environment variables, and no environment variable registers a customizer. To use customizers with the agent, also set up the SDK in code with `Braintrust.get(config)` before your application makes any instrumented calls. The agent's spans then run through your customizers too.
* **One config per process**: The first call to `Braintrust.get()` or `Braintrust.get(config)` in a process sets the SDK's configuration. Later calls return that same instance and ignore the config you pass, so register every customizer in the first call.

## Next steps

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