Data sanitization
Overview
Data sanitization is a runtime-level governance capability built into Akka. Rather than relying on application code to remember to scrub PII at every integration point, you declare your sanitization policy once and the runtime enforces it across logs, agent model inputs, and tool outputs.
This matters because protecting personally identifiable information is simultaneously a legal obligation and a trust imperative. Regulations such as GDPR, CCPA, and HIPAA mandate careful handling of personal data. The EU AI Act goes further: when AI systems process personal data, organizations must be able to explain what data was used and how it was protected. Runtime-level PII scrubbing gives you a verifiable, auditable answer: sensitive information is masked before it reaches a model, which satisfies both the right to erasure and the right to explanation.
By shifting sanitization from application logic into the runtime, you gain:
-
Consistency: the same policy covers every code path an entry applies to, with nothing to remember at the call site.
-
Auditability: governance and infosec teams can inspect a single configuration to verify what categories of PII are masked.
-
Reduced attack surface: in the event of a breach, masked data limits exposure.
Sanitization is disabled by default. You enable it selectively through configuration, choosing exactly which categories of sensitive data to mask.
A sanitizer masks at one or more application points:
-
logs: text written to logs -
model-call: text passed to the model in each model call of an agent, tool results included -
tool-result: only the output of local tools and MCP tools, which amodel-callentry masks as well
Each sanitizer can name the points it masks at, and the agents it masks for. See Configuring sanitizers.
Text matched by a sanitizer is replaced by a mask of * containing the same number of characters as the original matched string.
For example, with a credit card sanitizer enabled, the following text:
I'm having problems using my credit card 5204 46025 0000 006
Will be masked to:
I'm having problems using my credit card *******************
Before being written in logs or passed to agent models.
Masked text in session memory
Session memory holds the text that the runtime sent to the model. The runtime stores a user message and a tool result masked, and stores the model output as the model produced it. The runtime sends these messages to the model as they are. A sanitizer that you add or change later does not mask them. When application code, a compaction or an earlier Akka SDK version stored a user message or a tool result, the runtime masks it with the current sanitizers before it sends it. The runtime does not mask an AI message from session memory.
The ledger records the text that the runtime sent in each model call.
Ad hoc sanitization
You can also mask text programmatically where it makes sense for a specific business case, for example before sending text to a third-party API or before passing it to an entity to store. To do this, inject an akka.javasdk.SanitizerClient into a component and call sanitize with the name of a configured sanitizer and the text.
A sanitizer scoped to an agent or to an application point masks the text you pass here as well. The name selects which sanitizer runs, and nothing else does. For a sanitizer that only application code uses, define apply-at = ["client"], so that the runtime does not apply it anywhere on its own.
@HttpEndpoint("/example-with-ad-hoc-sanitization")
@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL))
public class SanitizingEndpoint {
private final SanitizerClient sanitizerClient;
public SanitizingEndpoint(SanitizerClient sanitizerClient) {
this.sanitizerClient = sanitizerClient;
}
@Get("/somepath/{id}")
public String returnSanitizedData(String id) {
// String data from another component or a third party library/API
String someText = loadText();
return sanitizerClient.sanitize("customer-ids", someText);
}
Configuring sanitizers
You configure sanitizers as named entries of akka.javasdk.sanitization.sanitizers. The name is how application code reaches the sanitizer, and how it is reported. Each entry defines exactly one of:
-
pattern: a Java regular expression matching the text to mask -
predefined: a built-in group of patterns, see Predefined groups -
class: an implementation class, see Sanitizer implementations
Each entry can also state where and for which agents it masks:
-
apply-at: the application points to mask at, any ofmodel-call,tool-result,logsandclient. -
agents: the component ids of the agents to mask for. Use*for every agent. -
agent-roles: the roles of the agents to mask for. Use*for every agent that has a role. -
enabled: set it tofalseto drop the entry. A dropped entry masks nothing and constructs nothing. -
control-id: the id of the control that the entry implements, see control ids.
An entry that defines neither agents nor agent-roles masks for every agent. An entry that defines both masks for an agent that matches either one.
An entry that defines no apply-at, or defines *, masks at:
-
model-call,tool-resultandlogsfor apatternorpredefinedentry. -
every point its class can mask at for a
classentry, see Sanitizer implementations. -
model-callandtool-resultfor an entry withagentsoragent-roles.
The logs and client application points are global and cannot be combined with agents or agent-roles, but you can define the sanitizer twice with different names to enable it for all logs and for specific agents. An entry with client masks nothing on its own, and application code calls it by name, see Ad hoc sanitization.
akka.javasdk.sanitization.sanitizers {
"credit-card" {
predefined = CREDIT_CARD
}
"customer-ids" {
pattern = "S\\d{10}"
apply-at = ["logs"]
}
"account-numbers" {
class = "com.example.sanitizer.AccountSanitizer"
apply-at = ["model-call", "tool-result"]
agent-roles = ["customer-facing"]
account-prefix = "ACC-"
}
"session-tokens" {
class = "com.example.sanitizer.TokenLogSanitizer"
key = "token"
}
}
The customer-ids entry above masks a hypothetical customer ID in the form S0123456789, so that text like:
Customer S0847362951 reported an issue with their order
Is written to logs as:
Customer *********** reported an issue with their order
Any other key of an entry is specific configuration for that sanitizer. An implementation reads it from the SanitizerContext constructor parameter, see Sanitizer implementations.
Predefined groups
A small set of common sanitizers is built into the Akka runtime. You select one with predefined:
| Name | Description |
|---|---|
|
email addresses |
|
International and national phone numbers |
|
VISA, Mastercard, American Express, Diners, Discover, JCB, and generic credit card numbers |
|
international bank account numbers |
|
ipv4 and ipv6 network addresses |
Sanitizer implementations
When a regular expression cannot express what to mask, implement a class and configure it with class. The interface of the class decides where it can mask:
-
akka.javasdk.TextSanitizermasks the text of agents, atmodel-callandtool-result. -
akka.javasdk.LogSanitizermasks log messages, atlogs.
A class can implement both. Return the text unchanged when there is nothing to mask.
import akka.javasdk.SanitizerContext;
import akka.javasdk.TextSanitizer;
public class AccountSanitizer implements TextSanitizer {
private final String prefix;
public AccountSanitizer(SanitizerContext context) {
this.prefix = context.config().getString("account-prefix");
}
@Override
public String sanitize(String text) {
StringBuilder masked = new StringBuilder();
for (String word : text.split(" ", -1)) {
if (!masked.isEmpty()) masked.append(" ");
masked.append(word.startsWith(prefix) ? "*".repeat(word.length()) : word);
}
return masked.toString();
}
}
The constructor of a TextSanitizer can take an akka.javasdk.SanitizerContext, which gives access to the entry name and its configuration, and any other injectable type, such as an akka.javasdk.agent.ClassifierClient to mask what a configured classifier detects.
import akka.javasdk.LogSanitizer;
import akka.javasdk.SanitizerContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TokenLogSanitizer implements LogSanitizer {
private final String key;
private final Pattern keyValue;
public TokenLogSanitizer(SanitizerContext context) {
this.key = context.config().getString("key");
this.keyValue = Pattern.compile(Pattern.quote(key) + "=(\\S+)");
}
@Override
public String sanitize(String message) {
// keeps the key and masks the value, for example token=****
return keyValue
.matcher(message)
.replaceAll(m -> Matcher.quoteReplacement(key + "=" + "*".repeat(m.group(1).length())));
}
}
The runtime calls a LogSanitizer for every log line of the service, including its own. Its constructor can optionally take an akka.javasdk.SanitizerContext, but no other dependency injection. Keep it a fast function of the message and its configuration, and do not log from it. The SDK constructs a class that implements both interfaces as a LogSanitizer.
Performance considerations
An entry that masks at the logs application point is applied to every log entry. In high-throughput applications, numerous sanitization rules or complex regular expressions may impact performance. You should monitor application performance and optimize regex patterns if necessary.
Testing sanitization
In tests you can access the sanitizer client from the getSanitizerClient method in TestKit or TestKitSupport, and mask with a configured sanitizer by name to assert that expected texts are masked given your service sanitizer configuration.
See also
-
Guardrails: input and output guardrails that complement data sanitization.