diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/MeterFilter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/MeterFilter.java index adc0251f7d..aaefa5adcf 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/MeterFilter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/MeterFilter.java @@ -17,6 +17,7 @@ import io.micrometer.common.lang.Nullable; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.config.filter.TagReplacingFilter; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import java.time.Duration; @@ -120,22 +121,7 @@ public Meter.Id map(Meter.Id id) { * @return A filter that replaces tag values. */ static MeterFilter replaceTagValues(String tagKey, Function replacement, String... exceptions) { - return new MeterFilter() { - @Override - public Meter.Id map(Meter.Id id) { - List tags = stream(id.getTagsAsIterable().spliterator(), false).map(t -> { - if (!t.getKey().equals(tagKey)) - return t; - for (String exception : exceptions) { - if (t.getValue().equals(exception)) - return t; - } - return Tag.of(tagKey, replacement.apply(t.getValue())); - }).collect(toList()); - - return id.replaceTags(tags); - } - }; + return TagReplacingFilter.classicValueReplacing(tagKey, replacement, exceptions); } /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/FilterSupport.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/FilterSupport.java new file mode 100644 index 0000000000..43b5dd7ac4 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/FilterSupport.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 io.micrometer.core.instrument.config.filter; + +class FilterSupport { + + /** + * At the moment of writing, it was impossible to estimate tags count from the outside + * of class, but quite often a temporary storage (ArrayList) had to be allocated + * during processing. To avoid excessive resizes, this constant is introduced to + * preallocate space for such a list. + */ + public static final int DEFAULT_TAG_COUNT_EXPECTATION = 32; + + private FilterSupport() { + } + +} diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/NoOpFilter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/NoOpFilter.java new file mode 100644 index 0000000000..8fb045f602 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/NoOpFilter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 io.micrometer.core.instrument.config.filter; + +import io.micrometer.core.instrument.config.MeterFilter; + +/** + * A fallback for all factory methods that have received an input functionally equivalent + * to "abstain from processing". + * + * @since 1.15 + */ +public class NoOpFilter implements MeterFilter { + + private static final MeterFilter INSTANCE = new NoOpFilter(); + + private NoOpFilter() { + } + + public static MeterFilter create() { + return INSTANCE; + } + +} diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/TagReplacingFilter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/TagReplacingFilter.java new file mode 100644 index 0000000000..08061e99df --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/TagReplacingFilter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 io.micrometer.core.instrument.config.filter; + +import io.micrometer.common.lang.NonNull; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.config.MeterFilter; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; + +public class TagReplacingFilter implements MeterFilter { + + private final BiPredicate filter; + + private final BiFunction replacer; + + private final int expectedTagCount; + + TagReplacingFilter(BiPredicate filter, BiFunction replacer, + int expectedTagCount) { + this.replacer = replacer; + this.filter = filter; + this.expectedTagCount = expectedTagCount; + } + + @NonNull + @Override + public Meter.Id map(@NonNull Meter.Id id) { + Iterator iterator = id.getTagsAsIterable().iterator(); + + if (!iterator.hasNext()) { + // fast path avoiding list allocation completely + return id; + } + + List replacement = new ArrayList<>(expectedTagCount); + + boolean intercepted = false; + while (iterator.hasNext()) { + Tag tag = iterator.next(); + String key = tag.getKey(); + String value = tag.getValue(); + + if (filter.test(key, value)) { + replacement.add(replacer.apply(key, value)); + intercepted = true; + } + else { + replacement.add(tag); + } + } + + return intercepted ? id.replaceTags(replacement) : id; + } + + public static MeterFilter of(BiPredicate filter, BiFunction replacer, + int expectedSize) { + return new TagReplacingFilter(filter, replacer, expectedSize); + } + + public static MeterFilter of(BiPredicate filter, BiFunction replacer) { + return new TagReplacingFilter(filter, replacer, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION); + } + + public static MeterFilter classicValueReplacing(String key, Function replacer, + Collection exceptions, int expectedSize) { + return of(new ClassicFilter(key, new HashSet<>(exceptions)), new ValueReplacer(replacer), expectedSize); + } + + public static MeterFilter classicValueReplacing(String key, Function replacer, + Collection exceptions) { + return classicValueReplacing(key, replacer, exceptions, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION); + } + + public static MeterFilter classicValueReplacing(String key, Function replacer, + String... exceptions) { + return classicValueReplacing(key, replacer, Arrays.asList(exceptions)); + } + + private static class ClassicFilter implements BiPredicate { + + private final String matcher; + + private final Set exceptions; + + public ClassicFilter(String matcher, Set exceptions) { + this.matcher = matcher; + this.exceptions = exceptions; + } + + @Override + public boolean test(String key, String value) { + return key.equals(matcher) && !exceptions.contains(value); + } + + } + + private static class ValueReplacer implements BiFunction { + + private final Function delegate; + + public ValueReplacer(Function delegate) { + this.delegate = delegate; + } + + @Override + public Tag apply(String key, String value) { + return Tag.of(key, delegate.apply(value)); + } + + } + +} diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/package-info.java new file mode 100644 index 0000000000..c71f075c3e --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/config/filter/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 io.micrometer.core.instrument.config.filter; diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterFilterTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/config/MeterFilterTest.java similarity index 98% rename from micrometer-core/src/test/java/io/micrometer/core/instrument/MeterFilterTest.java rename to micrometer-core/src/test/java/io/micrometer/core/instrument/config/MeterFilterTest.java index a3041e0c04..eed870e736 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/MeterFilterTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/config/MeterFilterTest.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.micrometer.core.instrument; +package io.micrometer.core.instrument.config; import io.micrometer.common.lang.Nullable; import io.micrometer.core.Issue; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.config.MeterFilterReply; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.assertj.core.api.Condition; diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/config/filter/TagReplacingFilterTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/config/filter/TagReplacingFilterTest.java new file mode 100644 index 0000000000..2e61c73d94 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/config/filter/TagReplacingFilterTest.java @@ -0,0 +1,186 @@ +/* + * Copyright 2025 VMware, Inc. + * + * Licensed 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 io.micrometer.core.instrument.config.filter; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.config.MeterFilter; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class TagReplacingFilterTest { + + private static final String REPLACEMENT = "replacement"; + + static Stream classicSamples() { + return Stream.of( + // Sanity check + Arguments.of(Tags.empty(), "missing", new String[0], Tags.empty()), + + // Absence + Arguments.of(Tags.of("alfa", "v"), "missing", new String[0], Tags.of("alfa", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v"), "missing", new String[0], + Tags.of("alfa", "v", "bravo", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "missing", new String[0], + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + + // Case sensitivity + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "", new String[0], + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "Bravo", new String[0], + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "Charlie", new String[0], + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + + // Normal replacement + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[0], + Tags.of("alfa", REPLACEMENT, "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[0], + Tags.of("alfa", "v", "bravo", REPLACEMENT, "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[0], + Tags.of("alfa", "v", "bravo", "v", "charlie", REPLACEMENT)), + + // Exceptions blockout + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "v" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "v" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[] { "v" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + + // Nothing happens if exceptions don't match anything + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "miss" }, + Tags.of("alfa", REPLACEMENT, "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "miss" }, + Tags.of("alfa", "v", "bravo", REPLACEMENT, "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[] { "miss" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", REPLACEMENT)), + + // Normal behavior returns if just one of them works + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "v", "miss" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "v", "miss" }, + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", + new String[] { "v", "miss" }, Tags.of("alfa", "v", "bravo", "v", "charlie", "v"))); + } + + private static Set lookup(Tag... tags) { + return new HashSet<>(Arrays.asList(tags)); + } + + static Stream genericSamples() { + return Stream.of(Arguments.of(Tags.empty(), new HashSet<>(), Tags.empty()), + Arguments.of(Tags.of("alfa", "v"), new HashSet<>(), Tags.of("alfa", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v"), new HashSet<>(), Tags.of("alfa", "v", "bravo", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), new HashSet<>(), + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + + // Filter mismatch + Arguments.of(Tags.empty(), lookup(Tag.of("alfa", "mismatch")), Tags.empty()), + Arguments.of(Tags.of("alfa", "v"), lookup(Tag.of("alfa", "mismatch")), Tags.of("alfa", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v"), lookup(Tag.of("alfa", "mismatch")), + Tags.of("alfa", "v", "bravo", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("alfa", "mismatch")), + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("alfa", "mismatch")), + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("charlie", "mismatch")), + Tags.of("alfa", "v", "bravo", "v", "charlie", "v")), + + // Filter match + Arguments.of(Tags.empty(), + lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")), + Tags.empty()), + Arguments.of(Tags.of("alfa", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")), + Tags.of("alfa", "alfa")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")), + Tags.of("alfa", "alfa", "bravo", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")), + Tags.of("alfa", "alfa", "bravo", "v", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("bravo", "v"), Tag.of("bravo", "mismatch")), + Tags.of("alfa", "v", "bravo", "bravo", "charlie", "v")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("charlie", "v"), Tag.of("bravo", "mismatch")), + Tags.of("alfa", "v", "bravo", "v", "charlie", "charlie")), + Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), + lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch"), + Tag.of("bravo", "v"), Tag.of("charlie", "v")), + Tags.of("alfa", "alfa", "bravo", "bravo", "charlie", "charlie"))); + } + + @ParameterizedTest + @MethodSource("classicSamples") + void classic(Tags input, String key, String[] exceptions, Tags expectation) { + MeterFilter sut = TagReplacingFilter.classicValueReplacing(key, any -> REPLACEMENT, exceptions); + + Meter.Id argument = new Meter.Id("_irrelevant_", input, null, null, Meter.Type.COUNTER); + + assertThat(sut.map(argument).getTagsAsIterable()).isEqualTo(expectation); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @MethodSource("genericSamples") + void generic(Tags input, Set matching, Tags expectation) { + BiPredicate matcher = mock(BiPredicate.class); + when(matcher.test(any(), any())).then(arguments -> { + String key = arguments.getArgument(0); + String value = arguments.getArgument(1); + return matching.contains(Tag.of(key, value)); + }); + + BiFunction replacer = mock(BiFunction.class); + when(replacer.apply(any(), any())).then(arguments -> { + String key = arguments.getArgument(0); + return Tag.of(key, key); + }); + + MeterFilter sut = TagReplacingFilter.of(matcher, replacer); + + Meter.Id argument = new Meter.Id("_irrelevant_", input, null, null, Meter.Type.COUNTER); + + assertThat(sut.map(argument).getTagsAsIterable()).isEqualTo(expectation); + + for (Tag tag : input) { + verify(matcher, times(1)).test(tag.getKey(), tag.getValue()); + + int invocations = matching.contains(tag) ? 1 : 0; + verify(replacer, times(invocations)).apply(tag.getKey(), tag.getValue()); + } + } + +}