Apache Camel's built-in masking can sometimes truncate after the first match. This article shows how to implement a custom MaskingFormatter that processes entire strings and masks all sensitive fields in JSON, query strings, and mixed formats without truncation.
Why Custom Masking?
The custom formatter:
- Uses
SensitiveUtils.getSensitiveKeys()(~100 built-in keywords likepassword,secret,token) - Adds your custom keywords (
userId,ssn,creditCard) - Handles both JSON (
"key": "value") and query strings (key=value) - Uses
Matcher.appendReplacement()loop to mask every occurrence - Never truncates - processes complete input
Example
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.spi.MaskingFormatter;
import org.apache.camel.support.SimpleRegistry;
import org.apache.camel.util.SensitiveUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Custom formatter that masks ALL sensitive fields without truncating.
* Processes entire string and masks all occurrences of sensitive keywords.
*/
static class NonTruncatingMaskingFormatter implements MaskingFormatter {
private final Pattern sensitivePattern;
public NonTruncatingMaskingFormatter() {
// Use SensitiveUtils for ~100 built-in keywords + custom ones
var sensitiveKeys = new java.util.HashSet<>(SensitiveUtils.getSensitiveKeys());
sensitiveKeys.add("userId");
sensitiveKeys.add("ssn");
sensitiveKeys.add("creditCard");
String keywordGroup = String.join("|", sensitiveKeys);
// Matches JSON: "keyword": "value" AND query: keyword=value
this.sensitivePattern = Pattern.compile(
"(?i)(?:([\"'])(" + keywordGroup + ")\\1\\s*:\\s*[\"']([^\"']+)[\"']" + // JSON
"|(" + keywordGroup + ")\\s*=\\s*([^&\\s,}]+))", // Query string
Pattern.CASE_INSENSITIVE
);
}
@Override
public String format(String source) {
if (source == null || source.isEmpty()) {
return source;
}
Matcher matcher = sensitivePattern.matcher(source);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String replacement;
if (matcher.group(1) != null) {
// JSON format: "key": "value" -> "key": "xxxxx"
String quote = matcher.group(1);
String keyword = matcher.group(2);
replacement = quote + keyword + quote + ": \"xxxxx\"";
} else {
// Query string: key=value -> key=xxxxx
String keyword = matcher.group(4);
replacement = keyword + "=xxxxx";
}
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
}
static class MyRoutes extends RouteBuilder {
@Override
public void configure() {
from("direct:test")
.to("log:maskedLogger?showAll=true&multiline=true")
.log("*** FULL BODY (custom masking): ${body}");
}
}
void main() throws Exception {
SimpleRegistry registry = new SimpleRegistry();
// Register custom formatter
NonTruncatingMaskingFormatter formatter = new NonTruncatingMaskingFormatter();
registry.bind(MaskingFormatter.CUSTOM_LOG_MASK_REF, formatter);
DefaultCamelContext context = new DefaultCamelContext(registry);
context.setLogMask(true); // Enable globally
context.addRoutes(new MyRoutes());
context.start();
// Query string payload - ALL fields masked
String queryPayload = "userId=12345&password=pass123&apiKey=abc-xyz-123&ssn=123-45-6789&creditCard=4111-1111-1111-1111&token=jwt.abc.def&anotherPassword=secret456";
context.createProducerTemplate().sendBody("direct:test", queryPayload);
// Complex nested JSON - ALL fields masked
String jsonPayload = """
{
"userId": "user123",
"username": "john.doe",
"password": "secretPass456",
"email": "john@example.com",
"apiKey": "sk-live-abc123xyz",
"ssn": "987-65-4321",
"creditCard": "5555-4444-3333-2222",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"profile": {
"accessToken": "ghp_xxxxxxxxxxxx",
"refreshToken": "refresh_abc123"
}
}
""";
context.createProducerTemplate().sendBody("direct:test", jsonPayload);
Thread.sleep(2000);
context.stop();
}
Expected Output
Query string (ALL 7 fields masked):
*** FULL BODY (custom masking): userId=xxxxx&password=xxxxx&apiKey=xxxxx&ssn=xxxxx&creditCard=xxxxx&token=xxxxx&anotherPassword=xxxxx
JSON (ALL 10+ fields masked):
*** FULL BODY (custom masking): {
"userId": "xxxxx",
"username": "xxxxx",
"password": "xxxxx",
"email": "john@example.com",
"apiKey": "xxxxx",
"ssn": "xxxxx",
"creditCard": "xxxxx",
"token": "xxxxx",
"profile": {
"accessToken": "xxxxx",
"refreshToken": "xxxxx"
}
}
Key Advantages
| Feature | Built-in | Custom |
|---|---|---|
| Truncation | ❌ Sometimes | ✅ Never |
| Custom keywords | Limited | ✅ Full control |
| Nested objects | ❌ Limited | ✅ Complete scan |
Usage Patterns
Route-level:
from("direct:secure")
.logMask() // Uses your custom formatter
.log("${body}");
Endpoint-level:
.to("log:secure?logMask=true")
Global: context.setLogMask(true) (as shown)
Leave a Reply