Skip to content

Add TagReplacingFilter #6183

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,22 +121,7 @@ public Meter.Id map(Meter.Id id) {
* @return A filter that replaces tag values.
*/
static MeterFilter replaceTagValues(String tagKey, Function<String, String> replacement, String... exceptions) {
return new MeterFilter() {
@Override
public Meter.Id map(Meter.Id id) {
List<Tag> 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);
}
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What were the problems / what was solved:

  • Stream was quite allocateful for a handful of elements. We're usually OK for that in writing our applications, but for critical paths we'd like to avoid that
  • for (String exception : exceptions) was adding O(n²) complexity
  • toList() creates a new zero-sized list. This results in (usually) a number of unnecessary resizes.
  • Finally, we'd like to abstain from recreating meter if there were no actual replacements. This doesn't do really much, but just a pleasant addition + even if we've allocated a new list, there is no record in GC card table pointing at it.

return TagReplacingFilter.classicValueReplacing(tagKey, replacement, exceptions);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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<String, String> filter;

private final BiFunction<String, String, Tag> replacer;

private final int expectedTagCount;

TagReplacingFilter(BiPredicate<String, String> filter, BiFunction<String, String, Tag> replacer,
int expectedTagCount) {
this.replacer = replacer;
this.filter = filter;
this.expectedTagCount = expectedTagCount;
}

@NonNull
@Override
public Meter.Id map(@NonNull Meter.Id id) {
Iterator<Tag> iterator = id.getTagsAsIterable().iterator();

if (!iterator.hasNext()) {
// fast path avoiding list allocation completely
return id;
}

List<Tag> replacement = new ArrayList<>(expectedTagCount);

boolean intercepted = false;
while (iterator.hasNext()) {
Tag tag = iterator.next();
String key = tag.getKey();
String value = tag.getValue();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of hacky move that can be hindering performance instead of helping it. The idea is simple - since predicate and replacing function aren't really interested in the tag itself, only in the key and value, let's extract them once, store in registers, and prevent double memory access. This sounds good, but in reality it might end up with two additional stack writes (which are relatively cheap and wouldn't make that much difference): if for any reason compiler can't fully inline the predicate and function, it will have to store the registers on the stack, and instead of one tag reference it will be forced to store one tag and two string references. However, this still saves us one indirection.

TBH this is solved simply by running a pair of benchmarks, but i'm all out of energy, and there are a couple more PRs coming


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<String, String> filter, BiFunction<String, String, Tag> replacer,
int expectedSize) {
return new TagReplacingFilter(filter, replacer, expectedSize);
}

public static MeterFilter of(BiPredicate<String, String> filter, BiFunction<String, String, Tag> replacer) {
return new TagReplacingFilter(filter, replacer, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION);
}

public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
Collection<String> exceptions, int expectedSize) {
return of(new ClassicFilter(key, new HashSet<>(exceptions)), new ValueReplacer(replacer), expectedSize);
}

public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
Collection<String> exceptions) {
return classicValueReplacing(key, replacer, exceptions, FilterSupport.DEFAULT_TAG_COUNT_EXPECTATION);
}

public static MeterFilter classicValueReplacing(String key, Function<String, String> replacer,
String... exceptions) {
return classicValueReplacing(key, replacer, Arrays.asList(exceptions));
}

private static class ClassicFilter implements BiPredicate<String, String> {

private final String matcher;

private final Set<String> exceptions;

public ClassicFilter(String matcher, Set<String> 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<String, String, Tag> {

private final Function<String, String> delegate;

public ValueReplacer(Function<String, String> delegate) {
this.delegate = delegate;
}

@Override
public Tag apply(String key, String value) {
return Tag.of(key, delegate.apply(value));
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading