-
Notifications
You must be signed in to change notification settings - Fork 951
Introduce log maskers for AnnotatedService
#6232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6781931
fa379e9
1b930ed
7cf7a11
ecd3390
4a887c0
d48e165
ebd62c1
0dd85bb
abc2164
5c29fdd
4ca67f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/* | ||
* Copyright 2025 LY Corporation | ||
* | ||
* LY 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.logging; | ||
|
||
import java.lang.annotation.Annotation; | ||
|
||
import com.linecorp.armeria.common.annotation.Nullable; | ||
import com.linecorp.armeria.common.annotation.UnstableApi; | ||
|
||
/** | ||
* Holds information about a POJO field. | ||
* Users may use the information conveyed in this object to decide whether to mask a field | ||
* via {@link FieldMasker}. | ||
* | ||
* <p>e.g. Assume a {@link BeanFieldInfo} representing the {@code inner} field. | ||
* {@link #getFieldAnnotation(Class)} will hold information about the {@code Foo#inner} field, | ||
* and {@link #getClassAnnotation(Class)} will hold information about the {@code Inner} class. | ||
* <pre>{@code | ||
* class Foo { | ||
* public Inner inner; | ||
* } | ||
* }</pre> | ||
*/ | ||
@UnstableApi | ||
public interface BeanFieldInfo { | ||
|
||
/** | ||
* A convenience method which searches for all annotations associated with this | ||
* {@link BeanFieldInfo}. This method invokes {@link #getFieldAnnotation(Class)} | ||
* and {@link #getClassAnnotation(Class)} sequentially to find the first non-null | ||
* annotation of type {@param annotationClass}. | ||
*/ | ||
@Nullable | ||
default <T extends Annotation> T getAnnotation(Class<T> annotationClass) { | ||
final T propertyAnnotation = getFieldAnnotation(annotationClass); | ||
if (propertyAnnotation != null) { | ||
return propertyAnnotation; | ||
} | ||
return getClassAnnotation(annotationClass); | ||
} | ||
|
||
/** | ||
* The name of the field. | ||
*/ | ||
String name(); | ||
|
||
/** | ||
* Returns an annotation on the specified field. | ||
*/ | ||
@Nullable | ||
<T extends Annotation> T getFieldAnnotation(Class<T> annotationClass); | ||
|
||
/** | ||
* Returns an annotation on the class of the field. | ||
*/ | ||
@Nullable | ||
<T extends Annotation> T getClassAnnotation(Class<T> annotationClass); | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||
/* | ||||
* Copyright 2025 LY Corporation | ||||
* | ||||
* LY 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.logging; | ||||
|
||||
import static java.util.Objects.requireNonNull; | ||||
|
||||
import com.linecorp.armeria.common.annotation.UnstableApi; | ||||
|
||||
/** | ||||
* A {@link FieldMaskerSelector} implementation for masking POJO data types. | ||||
* A simple use-case may look like the following: | ||||
* <pre>{@code | ||||
* @interface Masker {} | ||||
* | ||||
* class MyPojo { | ||||
* @Masker | ||||
* public String hello = "world"; | ||||
* } | ||||
* | ||||
* BeanFieldMaskerSelector selector = | ||||
* FieldMaskerSelector.ofBean(fieldInfo -> { | ||||
* Masker maskerAnnotation = fieldInfo.getAnnotation(Masker.class); | ||||
* if (maskerAnnotation == null) { | ||||
* return FieldMasker.fallthrough(); | ||||
* } | ||||
* return FieldMasker.nullify(); | ||||
* }); | ||||
* }</pre> | ||||
*/ | ||||
@UnstableApi | ||||
@FunctionalInterface | ||||
public interface BeanFieldMaskerSelector extends FieldMaskerSelector<BeanFieldInfo> { | ||||
|
||||
/** | ||||
* Delegates {@link FieldMasker} selection to a different {@link BeanFieldMaskerSelector} | ||||
* if the current {@link BeanFieldMaskerSelector} returns {@link FieldMasker#fallthrough()}. | ||||
*/ | ||||
default BeanFieldMaskerSelector orElse(BeanFieldMaskerSelector other) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was intentional since it doesn't make sense to do a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot about this, but the type of armeria/core/src/main/java/com/linecorp/armeria/common/logging/ContentSanitizerBuilder.java Line 136 in 5c29fdd
I prefer that e.g. public interface BeanFieldMaskerSelector extends ... {
@Override
default Class<?> supportedType() {
return BeanFieldInfo.class;
}
}
ContentSanitizer.builder()
.fieldMaskerSelector((BeanFieldMaskerSelector) info -> {
acc.add(info);
return FieldMasker.fallthrough();
})
.fieldMaskerSelector(((BeanFieldMaskerSelector) info -> { I think I prefer the original version unless you have another idea There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, let's use the original version, then. |
||||
requireNonNull(other, "other"); | ||||
return beanFieldInfo -> { | ||||
final FieldMasker fieldMasker = fieldMasker(beanFieldInfo); | ||||
if (fieldMasker != FieldMasker.fallthrough()) { | ||||
return fieldMasker; | ||||
} | ||||
return other.fieldMasker(beanFieldInfo); | ||||
}; | ||||
} | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
* Copyright 2025 LY Corporation | ||
* | ||
* LY 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.logging; | ||
|
||
import java.util.function.BiFunction; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
|
||
import com.linecorp.armeria.common.RequestContext; | ||
import com.linecorp.armeria.common.annotation.UnstableApi; | ||
|
||
/** | ||
* A content sanitizer implementation which sanitizes an object before serializing it into a | ||
* log friendly format. The following illustrates a simple use-case. | ||
* <pre>{@code | ||
* LogFormatter | ||
* .builderForText() | ||
* .contentSanitizer(ContentSanitizer.builder() | ||
* .fieldMaskerSelector(...) | ||
* .buildForText()) | ||
* .build(); | ||
* }</pre> | ||
* | ||
* @see TextLogFormatterBuilder#contentSanitizer(BiFunction) | ||
* @see JsonLogFormatterBuilder#contentSanitizer(BiFunction) | ||
*/ | ||
@UnstableApi | ||
@FunctionalInterface | ||
public interface ContentSanitizer<T> extends BiFunction<RequestContext, Object, T> { | ||
|
||
/** | ||
* Returns a {@link ContentSanitizerBuilder} instance. | ||
*/ | ||
static ContentSanitizerBuilder builder() { | ||
return new ContentSanitizerBuilder(); | ||
} | ||
|
||
/** | ||
* Returns a {@link ContentSanitizer} instance which doesn't mask fields. | ||
*/ | ||
static ContentSanitizer<String> forText() { | ||
return new ContentSanitizerBuilder().buildForText(); | ||
} | ||
|
||
/** | ||
* Returns a {@link ContentSanitizer} instance which doesn't mask fields. | ||
*/ | ||
static ContentSanitizer<JsonNode> forJson() { | ||
return new ContentSanitizerBuilder().buildForJson(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/* | ||
* Copyright 2025 LY Corporation | ||
* | ||
* LY 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.logging; | ||
|
||
import static com.linecorp.armeria.internal.common.logging.MaskerAttributeKeys.REQUEST_CONTEXT_KEY; | ||
import static java.util.Objects.requireNonNull; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.ServiceLoader; | ||
import java.util.function.BiFunction; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.util.RawValue; | ||
import com.google.common.base.Preconditions; | ||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.collect.ImmutableMap; | ||
|
||
import com.linecorp.armeria.common.annotation.UnstableApi; | ||
import com.linecorp.armeria.internal.common.JacksonUtil; | ||
import com.linecorp.armeria.internal.common.logging.FieldMaskerSelectorProvider; | ||
import com.linecorp.armeria.server.annotation.AnnotatedService; | ||
|
||
/** | ||
* A {@link ContentSanitizer} builder which allows users to specify {@link FieldMaskerSelector}s | ||
* to decide whether to mask a field. | ||
*/ | ||
@UnstableApi | ||
public final class ContentSanitizerBuilder { | ||
|
||
private static final Map<Class<?>, FieldMaskerSelectorProvider<?>> customizersMap; | ||
|
||
static { | ||
final ImmutableMap.Builder<Class<?>, FieldMaskerSelectorProvider<?>> customizersMapBuilder = | ||
ImmutableMap.builder(); | ||
@SuppressWarnings("rawtypes") | ||
final ServiceLoader<FieldMaskerSelectorProvider> loader = ServiceLoader.load( | ||
FieldMaskerSelectorProvider.class, | ||
ContentSanitizerBuilder.class.getClassLoader()); | ||
for (FieldMaskerSelectorProvider<?> customizer: loader) { | ||
customizersMapBuilder.put(customizer.supportedType(), customizer); | ||
} | ||
customizersMap = customizersMapBuilder.buildKeepingLast(); | ||
} | ||
|
||
private final ImmutableList.Builder<FieldMaskerSelector<?>> maskerListBuilder = ImmutableList.builder(); | ||
|
||
ContentSanitizerBuilder() {} | ||
|
||
/** | ||
* Adds a {@link FieldMaskerSelector} which decides whether each content field should be masked. | ||
* The specified {@link FieldMaskerSelector} should be an instance of a pre-defined type corresponding to a | ||
* service type. e.g. {@link BeanFieldMaskerSelector} should be used to mask content produced by | ||
* {@link AnnotatedService}. | ||
* If multiple {@link FieldMaskerSelector}s of the same type are registered, each selector will be | ||
* sequentially queried for a {@link FieldMasker}. If all {@link FieldMaskerSelector}s return | ||
* {@link FieldMasker#fallthrough()}, no masking will occur for the field. | ||
*/ | ||
public ContentSanitizerBuilder fieldMaskerSelector(FieldMaskerSelector<?> masker) { | ||
requireNonNull(masker, "masker"); | ||
Preconditions.checkArgument(!customizersMap.containsKey(masker.getClass()), | ||
jrhee17 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Specified masker should be one of the following types: %s", | ||
customizersMap.keySet()); | ||
maskerListBuilder.add(masker); | ||
return this; | ||
} | ||
|
||
/** | ||
* Builds a {@link ContentSanitizer} which can be used with | ||
* {@link TextLogFormatterBuilder#contentSanitizer(BiFunction)}. | ||
*/ | ||
public ContentSanitizer<String> buildForText() { | ||
final ObjectMapper objectMapper = buildObjectMapper(); | ||
return (requestContext, o) -> { | ||
try { | ||
return objectMapper.writer() | ||
.withAttribute(REQUEST_CONTEXT_KEY, requestContext) | ||
.writeValueAsString(o); | ||
} catch (JsonProcessingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Builds a {@link ContentSanitizer} which can be used with | ||
* {@link JsonLogFormatterBuilder#contentSanitizer(BiFunction)}. | ||
*/ | ||
public ContentSanitizer<JsonNode> buildForJson() { | ||
final ObjectMapper objectMapper = buildObjectMapper(); | ||
return (requestContext, o) -> { | ||
try { | ||
final String ser = objectMapper.writer() | ||
.withAttribute(REQUEST_CONTEXT_KEY, requestContext) | ||
.writeValueAsString(o); | ||
return objectMapper.createObjectNode().rawValueNode(new RawValue(ser)); | ||
} catch (JsonProcessingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}; | ||
} | ||
|
||
/** | ||
* Constructs an underlying {@link ObjectMapper} used to mask or unmask content. | ||
*/ | ||
public ObjectMapper buildObjectMapper() { | ||
ikhoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
final List<FieldMaskerSelector<?>> fieldMaskerSelectors = maskerListBuilder.build(); | ||
final ObjectMapper objectMapper = JacksonUtil.newDefaultObjectMapper(); | ||
for (FieldMaskerSelectorProvider<?> customizer : customizersMap.values()) { | ||
applyProvider(customizer, objectMapper, fieldMaskerSelectors); | ||
} | ||
return objectMapper; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private static <T extends FieldMaskerSelector<?>> void applyProvider( | ||
FieldMaskerSelectorProvider<T> provider, ObjectMapper objectMapper, | ||
List<FieldMaskerSelector<?>> selectors) { | ||
final List<T> filtered = | ||
selectors.stream().filter(selector -> provider.supportedType().isInstance(selector)) | ||
.map(selector -> (T) selector) | ||
.collect(ImmutableList.toImmutableList()); | ||
provider.customize(filtered, objectMapper); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.