Skip to content

Commit 8364f76

Browse files
committed
Add TagReplacingFilter
Signed-off-by: etki <[email protected]>
1 parent 14cc5b0 commit 8364f76

File tree

3 files changed

+318
-16
lines changed

3 files changed

+318
-16
lines changed

micrometer-core/src/main/java/io/micrometer/core/instrument/config/MeterFilter.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import io.micrometer.common.lang.Nullable;
1919
import io.micrometer.core.instrument.*;
20+
import io.micrometer.core.instrument.config.filter.TagReplacingFilter;
2021
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
2122

2223
import java.time.Duration;
@@ -120,22 +121,7 @@ public Meter.Id map(Meter.Id id) {
120121
* @return A filter that replaces tag values.
121122
*/
122123
static MeterFilter replaceTagValues(String tagKey, Function<String, String> replacement, String... exceptions) {
123-
return new MeterFilter() {
124-
@Override
125-
public Meter.Id map(Meter.Id id) {
126-
List<Tag> tags = stream(id.getTagsAsIterable().spliterator(), false).map(t -> {
127-
if (!t.getKey().equals(tagKey))
128-
return t;
129-
for (String exception : exceptions) {
130-
if (t.getValue().equals(exception))
131-
return t;
132-
}
133-
return Tag.of(tagKey, replacement.apply(t.getValue()));
134-
}).collect(toList());
135-
136-
return id.replaceTags(tags);
137-
}
138-
};
124+
return TagReplacingFilter.classicValueReplacing(tagKey, replacement, exceptions);
139125
}
140126

141127
/**
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.config.filter;
17+
18+
import io.micrometer.common.lang.NonNull;
19+
import io.micrometer.core.instrument.Meter;
20+
import io.micrometer.core.instrument.Tag;
21+
import io.micrometer.core.instrument.config.MeterFilter;
22+
23+
import java.util.*;
24+
import java.util.function.BiFunction;
25+
import java.util.function.BiPredicate;
26+
import java.util.function.Function;
27+
28+
public class TagReplacingFilter implements MeterFilter {
29+
30+
private final BiPredicate<String, String> filter;
31+
32+
private final BiFunction<String, String, Tag> replacer;
33+
34+
private final int expectedTagCount;
35+
36+
TagReplacingFilter(BiPredicate<String, String> filter, BiFunction<String, String, Tag> replacer,
37+
int expectedTagCount) {
38+
this.replacer = replacer;
39+
this.filter = filter;
40+
this.expectedTagCount = expectedTagCount;
41+
}
42+
43+
@NonNull
44+
@Override
45+
public Meter.Id map(@NonNull Meter.Id id) {
46+
Iterator<Tag> iterator = id.getTagsAsIterable().iterator();
47+
48+
if (!iterator.hasNext()) {
49+
// fast path avoiding list allocation completely
50+
return id;
51+
}
52+
53+
List<Tag> replacement = new ArrayList<>(expectedTagCount);
54+
55+
boolean intercepted = false;
56+
while (iterator.hasNext()) {
57+
Tag tag = iterator.next();
58+
String key = tag.getKey();
59+
String value = tag.getValue();
60+
61+
if (filter.test(key, value)) {
62+
replacement.add(replacer.apply(key, value));
63+
intercepted = true;
64+
}
65+
else {
66+
replacement.add(tag);
67+
}
68+
}
69+
70+
return intercepted ? id.replaceTags(replacement) : id;
71+
}
72+
73+
public static MeterFilter of(BiPredicate<String, String> filter, BiFunction<String, String, Tag> replacer,
74+
int expectedSize) {
75+
return new TagReplacingFilter(filter, replacer, expectedSize);
76+
}
77+
78+
public static MeterFilter of(BiPredicate<String, String> filter, BiFunction<String, String, Tag> replacer) {
79+
return new TagReplacingFilter(filter, replacer, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION);
80+
}
81+
82+
public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
83+
Collection<String> exceptions, int expectedSize) {
84+
return of(new ClassicFilter(key, new HashSet<>(exceptions)), new ValueReplacer(replacer), expectedSize);
85+
}
86+
87+
public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
88+
Collection<String> exceptions) {
89+
return classicValueReplacing(key, replacer, exceptions, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION);
90+
}
91+
92+
public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
93+
String... exceptions) {
94+
return classicValueReplacing(key, replacer, Arrays.asList(exceptions));
95+
}
96+
97+
private static class ClassicFilter implements BiPredicate<String, String> {
98+
99+
private final String matcher;
100+
101+
private final Set<String> exceptions;
102+
103+
public ClassicFilter(String matcher, Set<String> exceptions) {
104+
this.matcher = matcher;
105+
this.exceptions = exceptions;
106+
}
107+
108+
@Override
109+
public boolean test(String key, String value) {
110+
return key.equals(matcher) && !exceptions.contains(value);
111+
}
112+
113+
}
114+
115+
private static class ValueReplacer implements BiFunction<String, String, Tag> {
116+
117+
private final Function<String, String> delegate;
118+
119+
public ValueReplacer(Function<String, String> delegate) {
120+
this.delegate = delegate;
121+
}
122+
123+
@Override
124+
public Tag apply(String key, String value) {
125+
return Tag.of(key, delegate.apply(value));
126+
}
127+
128+
}
129+
130+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.core.instrument.config.filter;
17+
18+
import io.micrometer.core.instrument.Meter;
19+
import io.micrometer.core.instrument.Tag;
20+
import io.micrometer.core.instrument.Tags;
21+
import io.micrometer.core.instrument.config.MeterFilter;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
26+
import java.util.Arrays;
27+
import java.util.HashSet;
28+
import java.util.Set;
29+
import java.util.function.BiFunction;
30+
import java.util.function.BiPredicate;
31+
import java.util.stream.Stream;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.mockito.Mockito.any;
35+
import static org.mockito.Mockito.mock;
36+
import static org.mockito.Mockito.times;
37+
import static org.mockito.Mockito.verify;
38+
import static org.mockito.Mockito.when;
39+
40+
class TagReplacingFilterTest {
41+
42+
private static final String REPLACEMENT = "replacement";
43+
44+
static Stream<Arguments> classicSamples() {
45+
return Stream.of(
46+
// Sanity check
47+
Arguments.of(Tags.empty(), "missing", new String[0], Tags.empty()),
48+
49+
// Absence
50+
Arguments.of(Tags.of("alfa", "v"), "missing", new String[0], Tags.of("alfa", "v")),
51+
Arguments.of(Tags.of("alfa", "v", "bravo", "v"), "missing", new String[0],
52+
Tags.of("alfa", "v", "bravo", "v")),
53+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "missing", new String[0],
54+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
55+
56+
// Case sensitivity
57+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "", new String[0],
58+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
59+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "Bravo", new String[0],
60+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
61+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "Charlie", new String[0],
62+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
63+
64+
// Normal replacement
65+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[0],
66+
Tags.of("alfa", REPLACEMENT, "bravo", "v", "charlie", "v")),
67+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[0],
68+
Tags.of("alfa", "v", "bravo", REPLACEMENT, "charlie", "v")),
69+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[0],
70+
Tags.of("alfa", "v", "bravo", "v", "charlie", REPLACEMENT)),
71+
72+
// Exceptions blockout
73+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "v" },
74+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
75+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "v" },
76+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
77+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[] { "v" },
78+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
79+
80+
// Nothing happens if exceptions don't match anything
81+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "miss" },
82+
Tags.of("alfa", REPLACEMENT, "bravo", "v", "charlie", "v")),
83+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "miss" },
84+
Tags.of("alfa", "v", "bravo", REPLACEMENT, "charlie", "v")),
85+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie", new String[] { "miss" },
86+
Tags.of("alfa", "v", "bravo", "v", "charlie", REPLACEMENT)),
87+
88+
// Normal behavior returns if just one of them works
89+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "alfa", new String[] { "v", "miss" },
90+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
91+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "bravo", new String[] { "v", "miss" },
92+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
93+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), "charlie",
94+
new String[] { "v", "miss" }, Tags.of("alfa", "v", "bravo", "v", "charlie", "v")));
95+
}
96+
97+
private static Set<Tag> lookup(Tag... tags) {
98+
return new HashSet<>(Arrays.asList(tags));
99+
}
100+
101+
static Stream<Arguments> genericSamples() {
102+
return Stream.of(Arguments.of(Tags.empty(), new HashSet<>(), Tags.empty()),
103+
Arguments.of(Tags.of("alfa", "v"), new HashSet<>(), Tags.of("alfa", "v")),
104+
Arguments.of(Tags.of("alfa", "v", "bravo", "v"), new HashSet<>(), Tags.of("alfa", "v", "bravo", "v")),
105+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), new HashSet<>(),
106+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
107+
108+
// Filter mismatch
109+
Arguments.of(Tags.empty(), lookup(Tag.of("alfa", "mismatch")), Tags.empty()),
110+
Arguments.of(Tags.of("alfa", "v"), lookup(Tag.of("alfa", "mismatch")), Tags.of("alfa", "v")),
111+
Arguments.of(Tags.of("alfa", "v", "bravo", "v"), lookup(Tag.of("alfa", "mismatch")),
112+
Tags.of("alfa", "v", "bravo", "v")),
113+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("alfa", "mismatch")),
114+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
115+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("alfa", "mismatch")),
116+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
117+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"), lookup(Tag.of("charlie", "mismatch")),
118+
Tags.of("alfa", "v", "bravo", "v", "charlie", "v")),
119+
120+
// Filter match
121+
Arguments.of(Tags.empty(),
122+
lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")),
123+
Tags.empty()),
124+
Arguments.of(Tags.of("alfa", "v"),
125+
lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")),
126+
Tags.of("alfa", "alfa")),
127+
Arguments.of(Tags.of("alfa", "v", "bravo", "v"),
128+
lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")),
129+
Tags.of("alfa", "alfa", "bravo", "v")),
130+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"),
131+
lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch")),
132+
Tags.of("alfa", "alfa", "bravo", "v", "charlie", "v")),
133+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"),
134+
lookup(Tag.of("alfa", "mismatch"), Tag.of("bravo", "v"), Tag.of("bravo", "mismatch")),
135+
Tags.of("alfa", "v", "bravo", "bravo", "charlie", "v")),
136+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"),
137+
lookup(Tag.of("alfa", "mismatch"), Tag.of("charlie", "v"), Tag.of("bravo", "mismatch")),
138+
Tags.of("alfa", "v", "bravo", "v", "charlie", "charlie")),
139+
Arguments.of(Tags.of("alfa", "v", "bravo", "v", "charlie", "v"),
140+
lookup(Tag.of("alfa", "mismatch"), Tag.of("alfa", "v"), Tag.of("bravo", "mismatch"),
141+
Tag.of("bravo", "v"), Tag.of("charlie", "v")),
142+
Tags.of("alfa", "alfa", "bravo", "bravo", "charlie", "charlie")));
143+
}
144+
145+
@ParameterizedTest
146+
@MethodSource("classicSamples")
147+
void classic(Tags input, String key, String[] exceptions, Tags expectation) {
148+
MeterFilter sut = TagReplacingFilter.classicValueReplacing(key, any -> REPLACEMENT, exceptions);
149+
150+
Meter.Id argument = new Meter.Id("_irrelevant_", input, null, null, Meter.Type.COUNTER);
151+
152+
assertThat(sut.map(argument).getTagsAsIterable()).isEqualTo(expectation);
153+
}
154+
155+
@SuppressWarnings("unchecked")
156+
@ParameterizedTest
157+
@MethodSource("genericSamples")
158+
void generic(Tags input, Set<Tag> matching, Tags expectation) {
159+
BiPredicate<String, String> matcher = mock(BiPredicate.class);
160+
when(matcher.test(any(), any())).then(arguments -> {
161+
String key = arguments.getArgument(0);
162+
String value = arguments.getArgument(1);
163+
return matching.contains(Tag.of(key, value));
164+
});
165+
166+
BiFunction<String, String, Tag> replacer = mock(BiFunction.class);
167+
when(replacer.apply(any(), any())).then(arguments -> {
168+
String key = arguments.getArgument(0);
169+
return Tag.of(key, key);
170+
});
171+
172+
MeterFilter sut = TagReplacingFilter.of(matcher, replacer);
173+
174+
Meter.Id argument = new Meter.Id("_irrelevant_", input, null, null, Meter.Type.COUNTER);
175+
176+
assertThat(sut.map(argument).getTagsAsIterable()).isEqualTo(expectation);
177+
178+
for (Tag tag : input) {
179+
verify(matcher, times(1)).test(tag.getKey(), tag.getValue());
180+
181+
int invocations = matching.contains(tag) ? 1 : 0;
182+
verify(replacer, times(invocations)).apply(tag.getKey(), tag.getValue());
183+
}
184+
}
185+
186+
}

0 commit comments

Comments
 (0)