{"id":2090,"date":"2026-02-01T15:55:03","date_gmt":"2026-02-01T02:55:03","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2090"},"modified":"2026-02-01T15:55:03","modified_gmt":"2026-02-01T02:55:03","slug":"custom-log-masking-in-apache-camel","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2090","title":{"rendered":"Custom Log Masking in Apache Camel"},"content":{"rendered":"<p>Apache Camel's built-in masking can sometimes truncate after the first match. This article shows how to implement a <strong>custom <code>MaskingFormatter<\/code><\/strong> that processes <strong>entire strings<\/strong> and masks <strong>all<\/strong> sensitive fields in JSON, query strings, and mixed formats without truncation.<\/p>\n<h2>Why Custom Masking?<\/h2>\n<p>The custom formatter:<\/p>\n<ul>\n<li>Uses <code>SensitiveUtils.getSensitiveKeys()<\/code> (~100 built-in keywords like <code>password<\/code>, <code>secret<\/code>, <code>token<\/code>)<\/li>\n<li>Adds your custom keywords (<code>userId<\/code>, <code>ssn<\/code>, <code>creditCard<\/code>)<\/li>\n<li>Handles <strong>both<\/strong> JSON (<code>&quot;key&quot;: &quot;value&quot;<\/code>) and query strings (<code>key=value<\/code>)<\/li>\n<li>Uses <code>Matcher.appendReplacement()<\/code> loop to mask <strong>every<\/strong> occurrence<\/li>\n<li><strong>Never truncates<\/strong> - processes complete input<\/li>\n<\/ul>\n<h2>Example<\/h2>\n<pre><code class=\"language-java\">import org.apache.camel.builder.RouteBuilder;\nimport org.apache.camel.impl.DefaultCamelContext;\nimport org.apache.camel.spi.MaskingFormatter;\nimport org.apache.camel.support.SimpleRegistry;\nimport org.apache.camel.util.SensitiveUtils;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n\/**\n * Custom formatter that masks ALL sensitive fields without truncating.\n * Processes entire string and masks all occurrences of sensitive keywords.\n *\/\nstatic class NonTruncatingMaskingFormatter implements MaskingFormatter {\n    private final Pattern sensitivePattern;\n\n    public NonTruncatingMaskingFormatter() {\n        \/\/ Use SensitiveUtils for ~100 built-in keywords + custom ones\n        var sensitiveKeys = new java.util.HashSet&lt;&gt;(SensitiveUtils.getSensitiveKeys());\n        sensitiveKeys.add(&quot;userId&quot;);\n        sensitiveKeys.add(&quot;ssn&quot;);\n        sensitiveKeys.add(&quot;creditCard&quot;);\n\n        String keywordGroup = String.join(&quot;|&quot;, sensitiveKeys);\n\n        \/\/ Matches JSON: &quot;keyword&quot;: &quot;value&quot; AND query: keyword=value\n        this.sensitivePattern = Pattern.compile(\n                &quot;(?i)(?:([\\&quot;&#039;])(&quot; + keywordGroup + &quot;)\\\\1\\\\s*:\\\\s*[\\&quot;&#039;]([^\\&quot;&#039;]+)[\\&quot;&#039;]&quot; +  \/\/ JSON\n                        &quot;|(&quot; + keywordGroup + &quot;)\\\\s*=\\\\s*([^&amp;\\\\s,}]+))&quot;,  \/\/ Query string\n                Pattern.CASE_INSENSITIVE\n        );\n    }\n\n    @Override\n    public String format(String source) {\n        if (source == null || source.isEmpty()) {\n            return source;\n        }\n\n        Matcher matcher = sensitivePattern.matcher(source);\n        StringBuilder result = new StringBuilder();\n\n        while (matcher.find()) {\n            String replacement;\n            if (matcher.group(1) != null) {\n                \/\/ JSON format: &quot;key&quot;: &quot;value&quot; -&gt; &quot;key&quot;: &quot;xxxxx&quot;\n                String quote = matcher.group(1);\n                String keyword = matcher.group(2);\n                replacement = quote + keyword + quote + &quot;: \\&quot;xxxxx\\&quot;&quot;;\n            } else {\n                \/\/ Query string: key=value -&gt; key=xxxxx\n                String keyword = matcher.group(4);\n                replacement = keyword + &quot;=xxxxx&quot;;\n            }\n            matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));\n        }\n        matcher.appendTail(result);\n\n        return result.toString();\n    }\n}\n\nstatic class MyRoutes extends RouteBuilder {\n    @Override\n    public void configure() {\n        from(&quot;direct:test&quot;)\n                .to(&quot;log:maskedLogger?showAll=true&amp;multiline=true&quot;)\n                .log(&quot;*** FULL BODY (custom masking): ${body}&quot;);\n    }\n}\n\nvoid main() throws Exception {\n    SimpleRegistry registry = new SimpleRegistry();\n\n    \/\/ Register custom formatter\n    NonTruncatingMaskingFormatter formatter = new NonTruncatingMaskingFormatter();\n    registry.bind(MaskingFormatter.CUSTOM_LOG_MASK_REF, formatter);\n\n    DefaultCamelContext context = new DefaultCamelContext(registry);\n    context.setLogMask(true);  \/\/ Enable globally\n    context.addRoutes(new MyRoutes());\n    context.start();\n\n    \/\/ Query string payload - ALL fields masked\n    String queryPayload = &quot;userId=12345&amp;password=pass123&amp;apiKey=abc-xyz-123&amp;ssn=123-45-6789&amp;creditCard=4111-1111-1111-1111&amp;token=jwt.abc.def&amp;anotherPassword=secret456&quot;;\n    context.createProducerTemplate().sendBody(&quot;direct:test&quot;, queryPayload);\n\n    \/\/ Complex nested JSON - ALL fields masked\n    String jsonPayload = &quot;&quot;&quot;\n        {\n            &quot;userId&quot;: &quot;user123&quot;,\n            &quot;username&quot;: &quot;john.doe&quot;,\n            &quot;password&quot;: &quot;secretPass456&quot;,\n            &quot;email&quot;: &quot;john@example.com&quot;,\n            &quot;apiKey&quot;: &quot;sk-live-abc123xyz&quot;,\n            &quot;ssn&quot;: &quot;987-65-4321&quot;,\n            &quot;creditCard&quot;: &quot;5555-4444-3333-2222&quot;,\n            &quot;token&quot;: &quot;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9&quot;,\n            &quot;profile&quot;: {\n                &quot;accessToken&quot;: &quot;ghp_xxxxxxxxxxxx&quot;,\n                &quot;refreshToken&quot;: &quot;refresh_abc123&quot;\n            }\n        }\n        &quot;&quot;&quot;;\n    context.createProducerTemplate().sendBody(&quot;direct:test&quot;, jsonPayload);\n\n    Thread.sleep(2000);\n    context.stop();\n}<\/code><\/pre>\n<h2>Expected Output<\/h2>\n<p><strong>Query string (ALL 7 fields masked):<\/strong><\/p>\n<pre><code>*** FULL BODY (custom masking): userId=xxxxx&amp;password=xxxxx&amp;apiKey=xxxxx&amp;ssn=xxxxx&amp;creditCard=xxxxx&amp;token=xxxxx&amp;anotherPassword=xxxxx<\/code><\/pre>\n<p><strong>JSON (ALL 10+ fields masked):<\/strong><\/p>\n<pre><code>*** FULL BODY (custom masking): {\n    &quot;userId&quot;: &quot;xxxxx&quot;,\n    &quot;username&quot;: &quot;xxxxx&quot;,\n    &quot;password&quot;: &quot;xxxxx&quot;,\n    &quot;email&quot;: &quot;john@example.com&quot;,\n    &quot;apiKey&quot;: &quot;xxxxx&quot;,\n    &quot;ssn&quot;: &quot;xxxxx&quot;,\n    &quot;creditCard&quot;: &quot;xxxxx&quot;,\n    &quot;token&quot;: &quot;xxxxx&quot;,\n    &quot;profile&quot;: {\n        &quot;accessToken&quot;: &quot;xxxxx&quot;,\n        &quot;refreshToken&quot;: &quot;xxxxx&quot;\n    }\n}<\/code><\/pre>\n<h2>Key Advantages<\/h2>\n<table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>Built-in<\/th>\n<th>Custom<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Truncation<\/td>\n<td>\u274c Sometimes<\/td>\n<td>\u2705 Never<\/td>\n<\/tr>\n<tr>\n<td>Custom keywords<\/td>\n<td>Limited<\/td>\n<td>\u2705 Full control<\/td>\n<\/tr>\n<tr>\n<td>Nested objects<\/td>\n<td>\u274c Limited<\/td>\n<td>\u2705 Complete scan<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Usage Patterns<\/h2>\n<p><strong>Route-level:<\/strong><\/p>\n<pre><code class=\"language-java\">from(&quot;direct:secure&quot;)\n    .logMask()  \/\/ Uses your custom formatter\n    .log(&quot;${body}&quot;);<\/code><\/pre>\n<p><strong>Endpoint-level:<\/strong><\/p>\n<pre><code class=\"language-java\">.to(&quot;log:secure?logMask=true&quot;)<\/code><\/pre>\n<p><strong>Global:<\/strong> <code>context.setLogMask(true)<\/code> (as shown)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Apache Camel&#8217;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 like password, secret, token) Adds your [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[92],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2090"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2090"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2090\/revisions"}],"predecessor-version":[{"id":2091,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2090\/revisions\/2091"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2090"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2090"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2090"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}