diff --git a/core/build.gradle b/core/build.gradle index 1a09b8467061..eb894d016ac2 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -281,3 +281,7 @@ class PublicSuffixesTask extends DefaultTask { } task publicSuffixes(type: PublicSuffixesTask) + +test { + testLogging.showStandardStreams = true +} diff --git a/core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java new file mode 100644 index 000000000000..d15469be7b79 --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import java.util.function.BiFunction; + +import com.google.common.collect.ImmutableSet; + +import com.linecorp.armeria.common.annotation.Nullable; + +/** + * A skeletal builder implementation for {@link HeadersSanitizer}. + */ +abstract class AbstractHeadersSanitizerBuilder { + + @Nullable + private BiFunction headersSanitizer; + + private Set headersMask = ImmutableSet.of(); + + /** + * Sets the {@link BiFunction} to use to sanitize headers before logging. It is common to have the + * {@link BiFunction} that removes sensitive headers, like Cookie, before logging. + */ + public AbstractHeadersSanitizerBuilder headersSanitizer( + BiFunction headersSanitizer) { + this.headersSanitizer = requireNonNull(headersSanitizer, "headersSanitizer"); + return this; + } + + /** + * Returns the {@link BiFunction} to use to sanitize headers before logging. + */ + @Nullable + final BiFunction headersSanitizer() { + return headersSanitizer; + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + public AbstractHeadersSanitizerBuilder headersMask(String... headers) { + headersMask = ImmutableSet.copyOf(requireNonNull(headers, "headers")); + return this; + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + public AbstractHeadersSanitizerBuilder headersMask(Iterable headers) { + headersMask = ImmutableSet.copyOf(requireNonNull(headers, "headers")); + return this; + } + + /** + * Returns the {@link Set} to use to mask headers before logging. + */ + final Set headersMask() { + return headersMask; + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java new file mode 100644 index 000000000000..e909c403f0de --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import java.util.function.BiFunction; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A sanitizer that sanitizes {@link HttpHeaders}. + */ +public interface HeadersSanitizer extends BiFunction { + + /** + * Returns the default text {@link HeadersSanitizer}. + */ + static HeadersSanitizer ofText() { + return TextHeadersSanitizer.INSTANCE; + } + + /** + * Returns a newly created {@link TextHeadersSanitizerBuilder}. + */ + static TextHeadersSanitizerBuilder builderForText() { + return new TextHeadersSanitizerBuilder(); + } + + /** + * Returns the default json {@link HeadersSanitizer}. + */ + static HeadersSanitizer ofJson() { + return JsonHeadersSanitizer.INSTANCE; + } + + /** + * Returns a newly created {@link JsonHeadersSanitizerBuilder}. + */ + static JsonHeadersSanitizerBuilder builderForJson() { + return new JsonHeadersSanitizerBuilder(); + } + + /** + * Returns the sanitized {@link HttpHeaders}. + */ + T sanitizeHeaders(RequestContext requestContext, HttpHeaders headers); +} diff --git a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java new file mode 100644 index 000000000000..e826c1fc3df2 --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java @@ -0,0 +1,61 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import java.util.Set; +import java.util.function.BiFunction; + +import com.fasterxml.jackson.databind.JsonNode; + +import com.linecorp.armeria.common.annotation.Nullable; + +/** + * A sanitizer that sanitizes {@link HttpHeaders} and returns {@link JsonNode}. + */ +public final class JsonHeadersSanitizer implements HeadersSanitizer { + + static final HeadersSanitizer INSTANCE = new JsonHeadersSanitizerBuilder().build(); + + private static final String MASK = "****"; + + private final BiFunction headersSanitizer; + + private final Set headersMask; + + JsonHeadersSanitizer( + BiFunction + headersSanitizer, + Set headersMask) { + this.headersSanitizer = headersSanitizer; + this.headersMask = headersMask; + } + + @Override + public JsonNode sanitizeHeaders(RequestContext ctx, HttpHeaders headers) { + final HttpHeadersBuilder builder = headers.toBuilder(); + headers.forEach( + (name, value) -> builder.set(name, headersMask.contains(name.toString()) ? MASK : value)); + + return headersSanitizer.apply(ctx, builder.build()); + } + + @Override + public JsonNode apply(RequestContext requestContext, HttpHeaders headers) { + return null; + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java new file mode 100644 index 000000000000..4a20594730ec --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static java.util.Objects.requireNonNull; + +import java.util.Set; +import java.util.function.BiFunction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.internal.common.JacksonUtil; + +/** + * A builder implementation for {@link JsonHeadersSanitizer}. + */ +public final class JsonHeadersSanitizerBuilder extends AbstractHeadersSanitizerBuilder { + @Nullable + private ObjectMapper objectMapper; + + /** + * Sets the {@link ObjectMapper} that will be used to convert an object into a JSON format message. + */ + public JsonHeadersSanitizerBuilder objectMapper(ObjectMapper objectMapper) { + this.objectMapper = requireNonNull(objectMapper, "objectMapper"); + return this; + } + + /** + * Sets the {@link BiFunction} to use to sanitize headers before logging. + */ + @Override + public JsonHeadersSanitizerBuilder headersSanitizer( + BiFunction + headersSanitizer) { + return (JsonHeadersSanitizerBuilder) super.headersSanitizer(headersSanitizer); + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + @Override + public JsonHeadersSanitizerBuilder headersMask(String... headers) { + return (JsonHeadersSanitizerBuilder) super.headersMask(headers); + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + @Override + public JsonHeadersSanitizerBuilder headersMask(Iterable headers) { + return (JsonHeadersSanitizerBuilder) super.headersMask(headers); + } + + /** + * Returns a newly-created JSON {@link HeadersSanitizer} based on the properties of this builder. + */ + public JsonHeadersSanitizer build() { + final ObjectMapper objectMapper = this.objectMapper != null ? + this.objectMapper : JacksonUtil.newDefaultObjectMapper(); + + return new JsonHeadersSanitizer(firstNonNull(headersSanitizer(), defaultSanitizer(objectMapper)), + headersMask()); + } + + private static BiFunction + defaultSanitizer(ObjectMapper objectMapper) { + return (requestContext, obj) -> objectMapper.valueToTree(obj); + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java new file mode 100644 index 000000000000..14f1351be79b --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import java.util.Set; +import java.util.function.BiFunction; + +import com.linecorp.armeria.common.annotation.Nullable; + +/** + * A sanitizer that sanitizes {@link HttpHeaders} and returns {@link String}. + */ +public final class TextHeadersSanitizer implements HeadersSanitizer { + + static final HeadersSanitizer INSTANCE = new TextHeadersSanitizerBuilder().build(); + + private static final String MASK = "****"; + + private final BiFunction headersSanitizer; + + private final Set headersMask; + + TextHeadersSanitizer( + BiFunction + headersSanitizer, + Set headersMask) { + this.headersSanitizer = headersSanitizer; + this.headersMask = headersMask; + } + + @Override + public String apply(RequestContext ctx, HttpHeaders headers) { + final HttpHeadersBuilder builder = headers.toBuilder(); + headers.forEach( + (name, value) -> builder.set(name, headersMask.contains(name.toString()) ? MASK : value) + ); + return headersSanitizer.apply(ctx, builder.build()); + } + + @Override + public String sanitizeHeaders(RequestContext requestContext, HttpHeaders headers) { + return null; + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java new file mode 100644 index 000000000000..bce02e5ef51b --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.linecorp.armeria.common; + +import static com.google.common.base.MoreObjects.firstNonNull; + +import java.util.Set; +import java.util.function.BiFunction; + +/** + * A builder implementation for {@link TextHeadersSanitizer}. + */ +public final class TextHeadersSanitizerBuilder extends AbstractHeadersSanitizerBuilder { + + /** + * Sets the {@link BiFunction} to use to sanitize headers before logging. + */ + @Override + public TextHeadersSanitizerBuilder headersSanitizer( + BiFunction headersSanitizer) { + return (TextHeadersSanitizerBuilder) super.headersSanitizer(headersSanitizer); + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + @Override + public TextHeadersSanitizerBuilder headersMask(String... headers) { + return (TextHeadersSanitizerBuilder) super.headersMask(headers); + } + + /** + * Sets the {@link Set} to use to mask headers before logging. + */ + @Override + public TextHeadersSanitizerBuilder headersMask(Iterable headers) { + return (TextHeadersSanitizerBuilder) super.headersMask(headers); + } + + /** + * Returns a newly-created text {@link HeadersSanitizer} based on the properties of this builder. + */ + public TextHeadersSanitizer build() { + return new TextHeadersSanitizer(firstNonNull(headersSanitizer(), defaultSanitizer()), + headersMask()); + } + + private static BiFunction defaultSanitizer() { + return (requestContext, object) -> object.toString(); + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java index 7286334c24a4..c701d333087a 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java @@ -30,6 +30,7 @@ import com.google.common.base.MoreObjects; import com.linecorp.armeria.common.HttpHeaders; +import com.linecorp.armeria.common.JsonHeadersSanitizer; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; @@ -46,11 +47,9 @@ final class JsonLogFormatter implements LogFormatter { static final LogFormatter DEFAULT_INSTANCE = new JsonLogFormatterBuilder().build(); - private final BiFunction - requestHeadersSanitizer; + private final JsonHeadersSanitizer requestHeadersSanitizer; - private final BiFunction - responseHeadersSanitizer; + private final JsonHeadersSanitizer responseHeadersSanitizer; private final BiFunction requestTrailersSanitizer; @@ -67,10 +66,8 @@ final class JsonLogFormatter implements LogFormatter { private final ObjectMapper objectMapper; JsonLogFormatter( - BiFunction requestHeadersSanitizer, - BiFunction responseHeadersSanitizer, + JsonHeadersSanitizer requestHeadersSanitizer, + JsonHeadersSanitizer responseHeadersSanitizer, BiFunction requestTrailersSanitizer, BiFunction defaultContentSanitizer = defaultSanitizer(objectMapper); + + final JsonHeadersSanitizerBuilder requestHeadersSanitizerBuilder = HeadersSanitizer.builderForJson(); + if (requestHeadersSanitizer() != null) { + requestHeadersSanitizerBuilder.headersSanitizer(requestHeadersSanitizer()); + } + + final JsonHeadersSanitizerBuilder responseHeadersSanitizerBuilder = HeadersSanitizer.builderForJson(); + if (responseHeadersSanitizer() != null) { + responseHeadersSanitizerBuilder.headersSanitizer(responseHeadersSanitizer()); + } + return new JsonLogFormatter( - firstNonNull(requestHeadersSanitizer(), defaultHeadersSanitizer), - firstNonNull(responseHeadersSanitizer(), defaultHeadersSanitizer), + requestHeadersSanitizerBuilder.build(), + responseHeadersSanitizerBuilder.build(), firstNonNull(requestTrailersSanitizer(), defaultHeadersSanitizer), firstNonNull(responseTrailersSanitizer(), defaultHeadersSanitizer), firstNonNull(requestContentSanitizer(), defaultContentSanitizer), diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java index be19207c8a3f..4f233c9b5b65 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java @@ -26,6 +26,7 @@ import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.SerializationFormat; +import com.linecorp.armeria.common.TextHeadersSanitizer; import com.linecorp.armeria.common.annotation.Nullable; import com.linecorp.armeria.common.annotation.UnstableApi; import com.linecorp.armeria.common.util.TextFormatter; @@ -39,11 +40,9 @@ final class TextLogFormatter implements LogFormatter { static final LogFormatter DEFAULT_INSTANCE = new TextLogFormatterBuilder().build(); - private final BiFunction requestHeadersSanitizer; + private final TextHeadersSanitizer requestHeadersSanitizer; - private final BiFunction responseHeadersSanitizer; + private final TextHeadersSanitizer responseHeadersSanitizer; private final BiFunction requestTrailersSanitizer; @@ -60,10 +59,8 @@ final class TextLogFormatter implements LogFormatter { private final boolean includeContext; TextLogFormatter( - BiFunction requestHeadersSanitizer, - BiFunction responseHeadersSanitizer, + TextHeadersSanitizer requestHeadersSanitizer, + TextHeadersSanitizer responseHeadersSanitizer, BiFunction requestTrailersSanitizer, BiFunction objectMapper.valueToTree( + headers.toBuilder() + .set("Cookie", "****") + .build())) + .headersSanitizer( + (ctx, headers) -> objectMapper.valueToTree( + headers.toBuilder() + .set("Authorization", "****") + .build())) + .build(); + final HttpRequest req = HttpRequest.of(RequestHeaders.of(HttpMethod.GET, "/hello", + "Cookie", "Armeria=awesome", + "Authorization", "authorization", + "Cache-Control", "no-cache")); + + final ServiceRequestContext ctx = ServiceRequestContext.of(req); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.endRequest(); + final String requestLog = logFormatter.formatRequest(log); + + final Matcher matcher1 = Pattern.compile("\"cookie\":\"(.*?)\"").matcher(requestLog); + assertThat(matcher1.find()).isTrue(); + assertThat(matcher1.group(1)).isEqualTo("****"); + + final Matcher matcher2 = Pattern.compile("\"authorization\":\"(.*?)\"").matcher(requestLog); + assertThat(matcher2.find()).isTrue(); + assertThat(matcher2.group(1)).isEqualTo("****"); + + final Matcher matcher3 = Pattern.compile("\"cache-control\":\"(.*?)\"").matcher(requestLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); + } + + @Test + void maskResponseHeaders() { + final LogFormatter logFormatter = LogFormatter.builderForJson() + .responseHeadersSanitizer( + (ctx, headers) -> objectMapper.valueToTree( + headers.toBuilder() + .set("Content-Type", "****") + .build())) + .headersSanitizer( + (ctx, headers) -> objectMapper.valueToTree( + headers.toBuilder() + .set("Authorization", "****") + .build())) + .build(); + final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/hello")); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.responseHeaders(ResponseHeaders.of(HttpStatus.OK, + "Content-Type", "text/html", + "Set-Cookie", "Armeria=awesome", + "Cache-Control", "no-cache")); + log.endResponse(); + final String responseLog = logFormatter.formatResponse(log); + final Matcher matcher1 = Pattern.compile("\"content-type\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher1.find()).isTrue(); + assertThat(matcher1.group(1)).isEqualTo("****"); + + final Matcher matcher2 = Pattern.compile("\"set-cookie\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher2.find()).isTrue(); + assertThat(matcher2.group(1)).isEqualTo("****"); + + final Matcher matcher3 = Pattern.compile("\"cache-control\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); + } } diff --git a/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java b/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java index 0dbc36122b49..8d12e956619c 100644 --- a/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java +++ b/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java @@ -18,11 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.RequestHeaders; +import com.linecorp.armeria.common.ResponseHeaders; import com.linecorp.armeria.server.ServiceRequestContext; class TextLogFormatterTest { @@ -60,4 +67,78 @@ void formatResponse(boolean containContext) { assertThat(responseLog).matches(regex); } } + + @Test + void maskRequestHeaders() { + final LogFormatter logFormatter = LogFormatter.builderForText() + .requestHeadersSanitizer( + (ctx, headers) -> headers.toBuilder() + .set("Cookie", "****") + .build() + .toString()) + .headersSanitizer( + (ctx, headers) -> headers.toBuilder() + .set("Authorization", + "****") + .build() + .toString()) + .build(); + final HttpRequest req = HttpRequest.of(RequestHeaders.of(HttpMethod.GET, "/hello", + "Cookie", "Armeria=awesome", + "Authorization", "authorization", + "Cache-Control", "no-cache")); + + final ServiceRequestContext ctx = ServiceRequestContext.of(req); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.endRequest(); + final String requestLog = logFormatter.formatRequest(log); + final Matcher matcher1 = Pattern.compile("cookie=(.*?)[,\\]]").matcher(requestLog); + assertThat(matcher1.find()).isTrue(); + assertThat(matcher1.group(1)).isEqualTo("****"); + + final Matcher matcher2 = Pattern.compile("authorization=(.*?)[,\\]]").matcher(requestLog); + assertThat(matcher2.find()).isTrue(); + assertThat(matcher2.group(1)).isEqualTo("****"); + + final Matcher matcher3 = Pattern.compile("cache-control=(.*?)[,\\]]").matcher(requestLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); + } + + @Test + void maskResponseHeaders() { + final LogFormatter logFormatter = LogFormatter.builderForText() + .responseHeadersSanitizer( + (ctx, headers) -> headers.toBuilder() + .set("Content-Type", + "****") + .build() + .toString()) + .headersSanitizer( + (ctx, headers) -> headers.toBuilder() + .set("Set-Cookie", + "****") + .build() + .toString()) + .build(); + final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/hello")); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.responseHeaders(ResponseHeaders.of(HttpStatus.OK, + "Content-Type", "text/html", + "Set-Cookie", "Armeria=awesome", + "Cache-Control", "no-cache")); + log.endResponse(); + final String responseLog = logFormatter.formatResponse(log); + final Matcher matcher1 = Pattern.compile("content-type=(.*?)[,\\]]").matcher(responseLog); + assertThat(matcher1.find()).isTrue(); + assertThat(matcher1.group(1)).isEqualTo("****"); + + final Matcher matcher2 = Pattern.compile("set-cookie=(.*?)[,\\]]").matcher(responseLog); + assertThat(matcher2.find()).isTrue(); + assertThat(matcher2.group(1)).isEqualTo("****"); + + final Matcher matcher3 = Pattern.compile("cache-control=(.*?)[,\\]]").matcher(responseLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); + } }