diff --git a/core/benchmark-tests.txt b/core/benchmark-tests.txt new file mode 100644 index 0000000..fe9eff5 --- /dev/null +++ b/core/benchmark-tests.txt @@ -0,0 +1,4 @@ +Benchmark Mode Cnt Score Error Units +ScopeImplBenchmark.recordingBenchmark thrpt 10 74.819 ± 4.573 ops/ms +ScopeImplBenchmark.recordingWithSanitizingDashBenchmark thrpt 10 49.256 ± 9.474 ops/ms +ScopeImplBenchmark.scopeReportingBenchmark thrpt 10 39066.800 ± 262.844 ops/ms diff --git a/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java b/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java index 5844536..3c916c3 100644 --- a/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java +++ b/core/src/jmh/java/com/uber/m3/tally/ScopeImplBenchmark.java @@ -20,23 +20,18 @@ package com.uber.m3.tally; +import com.uber.m3.tally.sanitizers.ScopeSanitizerBuilder; +import com.uber.m3.tally.sanitizers.ValidCharacters; import com.uber.m3.util.Duration; import com.uber.m3.util.ImmutableMap; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Setup; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.*; import java.util.Random; import java.util.concurrent.TimeUnit; @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MILLISECONDS) -@Fork(value = 2, jvmArgsAppend = { "-server", "-XX:+UseG1GC" }) +@Fork(value = 2, jvmArgsAppend = {"-server", "-XX:+UseG1GC"}) public class ScopeImplBenchmark { private static final DurationBuckets EXPONENTIAL_BUCKETS = DurationBuckets.linear(Duration.ofMillis(1), Duration.ofMillis(10), 128); @@ -67,28 +62,56 @@ public class ScopeImplBenchmark { @Benchmark public void scopeReportingBenchmark(BenchmarkState state) { - state.scope.reportLoopIteration(); + state.reportingBenchmarkScope.reportLoopIteration(); + } + + @Benchmark + public void recordingWithSanitizingDashBenchmark(BenchmarkState state) { + state.recordTestMetrics(state.sanitizingBenchmarkScope); + } + + @Benchmark + public void recordingBenchmark(BenchmarkState state) { + state.recordTestMetrics(state.recordingBenchmarkScope); } @State(org.openjdk.jmh.annotations.Scope.Benchmark) public static class BenchmarkState { - private ScopeImpl scope; + private ScopeImpl reportingBenchmarkScope; + private ScopeImpl sanitizingBenchmarkScope; + private ScopeImpl recordingBenchmarkScope; @Setup public void setup() { - this.scope = - (ScopeImpl) new RootScopeBuilder() - .reporter(new TestStatsReporter()) - .tags( - ImmutableMap.of( - "service", "some-service", - "application", "some-application", - "instance", "some-instance" - ) - ) - .reportEvery(Duration.MAX_VALUE); + final ScopeBuilder scopeBuilder = new RootScopeBuilder() + .reporter(new TestStatsReporter()) + .tags( + ImmutableMap.of( + "service", "some-service", + "application", "some-application", + "instance", "some-instance" + ) + ); + + this.reportingBenchmarkScope = + (ScopeImpl) scopeBuilder.reportEvery(Duration.MAX_VALUE); + + this.recordTestMetrics(this.reportingBenchmarkScope); + + this.recordingBenchmarkScope = (ScopeImpl) scopeBuilder.reportEvery(Duration.MAX_VALUE); + this.sanitizingBenchmarkScope = + (ScopeImpl) scopeBuilder.sanitizer( + new ScopeSanitizerBuilder() + .withNameValidCharacters(ValidCharacters.of(null, ValidCharacters.UNDERSCORE_CHARACTERS)) + .withTagKeyValidCharacters(ValidCharacters.of(null, ValidCharacters.UNDERSCORE_CHARACTERS)) + .withTagValueValidCharacters(ValidCharacters.of(null, ValidCharacters.UNDERSCORE_CHARACTERS)) + .build() + ) + .reportEvery(Duration.MAX_VALUE); + } + public void recordTestMetrics(final ScopeImpl scope) { for (String counterName : COUNTER_NAMES) { scope.counter(counterName).inc(1); } @@ -112,7 +135,9 @@ public void setup() { @TearDown public void teardown() { - scope.close(); + reportingBenchmarkScope.close(); + recordingBenchmarkScope.close(); + sanitizingBenchmarkScope.close(); } } diff --git a/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java b/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java index 08cdd3f..f617d9a 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeBuilder.java @@ -20,6 +20,8 @@ package com.uber.m3.tally; +import com.uber.m3.tally.sanitizers.NoopSanitizer; +import com.uber.m3.tally.sanitizers.ScopeSanitizer; import com.uber.m3.util.Duration; import com.uber.m3.util.ImmutableMap; @@ -55,6 +57,7 @@ public class ScopeBuilder { protected String separator = DEFAULT_SEPARATOR; protected ImmutableMap tags; protected Buckets defaultBuckets = DEFAULT_SCOPE_BUCKETS; + protected ScopeSanitizer sanitizer = new NoopSanitizer(); private ScheduledExecutorService scheduler; private ScopeImpl.Registry registry; @@ -128,6 +131,17 @@ public ScopeBuilder defaultBuckets(Buckets defaultBuckets) { return this; } + /** + * Update the sanitizer. + * + * @param sanitizer value to update to + * @return Builder with new param updated + */ + public ScopeBuilder sanitizer(ScopeSanitizer sanitizer) { + this.sanitizer = sanitizer; + return this; + } + // Private build method - clients should rely on `reportEvery` to create root scopes, and // a root scope's `tagged` and `subScope` functions to create subscopes. ScopeImpl build() { diff --git a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java index da0a6d8..abb855b 100644 --- a/core/src/main/java/com/uber/m3/tally/ScopeImpl.java +++ b/core/src/main/java/com/uber/m3/tally/ScopeImpl.java @@ -20,6 +20,7 @@ package com.uber.m3.tally; +import com.uber.m3.tally.sanitizers.ScopeSanitizer; import com.uber.m3.util.ImmutableMap; import javax.annotation.Nullable; @@ -41,6 +42,7 @@ class ScopeImpl implements Scope { private String separator; private ImmutableMap tags; private Buckets defaultBuckets; + private ScopeSanitizer sanitizer; private ScheduledExecutorService scheduler; private Registry registry; @@ -57,6 +59,7 @@ class ScopeImpl implements Scope { ScopeImpl(ScheduledExecutorService scheduler, Registry registry, ScopeBuilder builder) { this.scheduler = scheduler; this.registry = registry; + this.sanitizer = builder.sanitizer; this.reporter = builder.reporter; this.prefix = builder.prefix; @@ -67,33 +70,37 @@ class ScopeImpl implements Scope { @Override public Counter counter(String name) { - return counters.computeIfAbsent(name, ignored -> + final String finalName = sanitizer.sanitizeName(name); + return counters.computeIfAbsent(finalName, ignored -> // NOTE: This will called at most once - new CounterImpl(this, fullyQualifiedName(name)) + new CounterImpl(this, fullyQualifiedName(finalName)) ); } @Override public Gauge gauge(String name) { - return gauges.computeIfAbsent(name, ignored -> + final String finalName = sanitizer.sanitizeName(name); + return gauges.computeIfAbsent(finalName, ignored -> // NOTE: This will called at most once - new GaugeImpl(this, fullyQualifiedName(name))); + new GaugeImpl(this, fullyQualifiedName(finalName))); } @Override public Timer timer(String name) { + final String finalName = sanitizer.sanitizeName(name); // Timers report directly to the {@code StatsReporter}, and therefore not added to reporting queue // i.e. they are not buffered - return timers.computeIfAbsent(name, ignored -> new TimerImpl(fullyQualifiedName(name), tags, reporter)); + return timers.computeIfAbsent(finalName, ignored -> new TimerImpl(fullyQualifiedName(finalName), tags, reporter)); } @Override public Histogram histogram(String name, @Nullable Buckets buckets) { - return histograms.computeIfAbsent(name, ignored -> + final String finalName = sanitizer.sanitizeName(name); + return histograms.computeIfAbsent(finalName, ignored -> // NOTE: This will called at most once new HistogramImpl( this, - fullyQualifiedName(name), + fullyQualifiedName(finalName), tags, Optional.ofNullable(buckets) .orElse(defaultBuckets) @@ -103,12 +110,13 @@ public Histogram histogram(String name, @Nullable Buckets buckets) { @Override public Scope tagged(Map tags) { - return subScopeHelper(prefix, tags); + return subScopeHelper(prefix, sanitizeTags(tags)); } @Override public Scope subScope(String name) { - return subScopeHelper(fullyQualifiedName(name), null); + final String finalName = sanitizer.sanitizeName(name); + return subScopeHelper(fullyQualifiedName(finalName), null); } @Override @@ -136,12 +144,35 @@ public void close() { } } + private Map sanitizeTags(Map tags) { + if (tags == null) { + return null; + } + + boolean hasChange = false; + for (Map.Entry kv : tags.entrySet()) { + if (!sanitizer.sanitizeTagKey(kv.getKey()).equals(kv.getKey()) || !sanitizer.sanitizeTagValue(kv.getValue()).equals(kv.getValue())) { + hasChange = true; + break; + } + } + if (!hasChange) { + return tags; + } + + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + tags.forEach((key, value) -> builder.put(sanitizer.sanitizeTagKey(key), sanitizer.sanitizeTagValue(value))); + + return builder.build(); + } + void addToReportingQueue(T metric) { reportingQueue.add(metric); } /** * Reports using the specified reporter. + * * @param reporter the reporter to report */ void report(StatsReporter reporter) { @@ -193,6 +224,7 @@ String fullyQualifiedName(String name) { /** * Returns a {@link Snapshot} of this {@link Scope}. + * * @return a {@link Snapshot} of this {@link Scope} */ public Snapshot snapshot() { @@ -205,12 +237,12 @@ public Snapshot snapshot() { String id = keyForPrefixedStringMap(name, tags); snap.counters().put( - id, - new CounterSnapshotImpl( - name, - tags, - counter.getValue().snapshot() - ) + id, + new CounterSnapshotImpl( + name, + tags, + counter.getValue().snapshot() + ) ); } @@ -220,12 +252,12 @@ public Snapshot snapshot() { String id = keyForPrefixedStringMap(name, tags); snap.gauges().put( - id, - new GaugeSnapshotImpl( - name, - tags, - gauge.getValue().snapshot() - ) + id, + new GaugeSnapshotImpl( + name, + tags, + gauge.getValue().snapshot() + ) ); } @@ -235,12 +267,12 @@ public Snapshot snapshot() { String id = keyForPrefixedStringMap(name, tags); snap.timers().put( - id, - new TimerSnapshotImpl( - name, - tags, - timer.getValue().snapshot() - ) + id, + new TimerSnapshotImpl( + name, + tags, + timer.getValue().snapshot() + ) ); } @@ -250,13 +282,13 @@ public Snapshot snapshot() { String id = keyForPrefixedStringMap(name, tags); snap.histograms().put( - id, - new HistogramSnapshotImpl( - name, - tags, - histogram.getValue().snapshotValues(), - histogram.getValue().snapshotDurations() - ) + id, + new HistogramSnapshotImpl( + name, + tags, + histogram.getValue().snapshotValues(), + histogram.getValue().snapshotDurations() + ) ); } } @@ -283,12 +315,13 @@ private Scope subScopeHelper(String prefix, Map tags) { return registry.subscopes.computeIfAbsent( key, (k) -> new ScopeBuilder(scheduler, registry) - .reporter(reporter) - .prefix(prefix) - .separator(separator) - .tags(mergedTags) - .defaultBuckets(defaultBuckets) - .build() + .reporter(reporter) + .prefix(prefix) + .separator(separator) + .tags(mergedTags) + .sanitizer(sanitizer) + .defaultBuckets(defaultBuckets) + .build() ); } diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/CharRange.java b/core/src/main/java/com/uber/m3/tally/sanitizers/CharRange.java new file mode 100644 index 0000000..0dd1ffd --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/CharRange.java @@ -0,0 +1,54 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +/** + * CharRange is a range of characters (inclusive on both ends). + */ +public class CharRange { + + private final char low; + private final char high; + + private CharRange(char low, char high) { + if (low > high) { + throw new IllegalArgumentException("invalid CharRange"); + } + this.low = low; + this.high = high; + } + + public static CharRange of(char low, char high) { + return new CharRange(low, high); + } + + char low() { + return low; + } + + char high() { + return high; + } + + public boolean isWithinRange(char ch) { + return (ch >= low) && (ch <= high); + } +} diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/NoopSanitizer.java b/core/src/main/java/com/uber/m3/tally/sanitizers/NoopSanitizer.java new file mode 100644 index 0000000..ca026aa --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/NoopSanitizer.java @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +/** + * NoopSanitizer will do nothing + */ +public class NoopSanitizer implements ScopeSanitizer { + + @Override + public String sanitizeName(String name) { + return name; + } + + @Override + public String sanitizeTagKey(String key) { + return key; + } + + @Override + public String sanitizeTagValue(String value) { + return value; + } +} diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/SanitizerImpl.java b/core/src/main/java/com/uber/m3/tally/sanitizers/SanitizerImpl.java new file mode 100644 index 0000000..240c296 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/SanitizerImpl.java @@ -0,0 +1,72 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +import java.util.function.Function; + +/** + * SanitizerImpl sanitizes the provided input based on the function executed. + */ +class SanitizerImpl implements ScopeSanitizer { + + private final Function nameSanitizer; + private final Function tagKeySanitizer; + private final Function tagValueSanitizer; + + SanitizerImpl(Function nameSanitizer, Function tagKeySanitizer, Function tagValueSanitizer) { + this.nameSanitizer = nameSanitizer; + this.tagKeySanitizer = tagKeySanitizer; + this.tagValueSanitizer = tagValueSanitizer; + } + + /** + * Name sanitizes the provided 'name' string. + * + * @param name the name string + * @return the sanitized name + */ + @Override + public String sanitizeName(String name) { + return this.nameSanitizer.apply(name); + } + + /** + * Key sanitizes the provided 'key' string. + * + * @param key the key string + * @return the sanitized key + */ + @Override + public String sanitizeTagKey(String key) { + return this.tagKeySanitizer.apply(key); + } + + /** + * Value sanitizes the provided 'value' string. + * + * @param value the value string + * @return the sanitized value + */ + @Override + public String sanitizeTagValue(String value) { + return this.tagValueSanitizer.apply(value); + } +} diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizer.java b/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizer.java new file mode 100644 index 0000000..dd0d4e8 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizer.java @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +/** + * SanitizerImpl sanitizes scope properties: name, key and value + * for counter/timer/histogram/gauge/tags/etc + */ +public interface ScopeSanitizer { + + /** + * Name sanitizes the provided 'name' of counter/timer/histogram/gauge/etc + * @param name the name string + * @return the sanitized name + */ + String sanitizeName(String name); + + /** + * Key sanitizes the provided 'key' of a tag + * @param key the key string + * @return the sanitized key + */ + String sanitizeTagKey(String key); + + /** + * Value sanitizes the provided 'value' of a tag + * @param value the value string + * @return the sanitized value + */ + String sanitizeTagValue(String value); +} diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizerBuilder.java b/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizerBuilder.java new file mode 100644 index 0000000..b0fdd5f --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/ScopeSanitizerBuilder.java @@ -0,0 +1,118 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +import java.util.function.Function; + +/** + * The SanitizerBuilder returns a Sanitizer for the name, key and value. By + * default, the name, key and value sanitize functions returns all the input + * untouched. Custom name, key or value Sanitize functions or ValidCharacters + * can be provided to override their default behaviour. + */ +public class ScopeSanitizerBuilder { + + private Function nameSanitizer; + private Function tagKeySanitizer; + private Function tagValueSanitizer; + + private char replacementChar = ValidCharacters.DEFAULT_REPLACEMENT_CHARACTER; + private ValidCharacters nameValidCharacters; + private ValidCharacters tagKeyValidCharacters; + private ValidCharacters tagValueValidCharacters; + + public ScopeSanitizerBuilder withNameSanitizer(Function nameSanitizer) { + if (nameValidCharacters != null) { + throw new IllegalArgumentException("only one of them can be provided: nameValidCharacters, nameSanitizer"); + } + this.nameSanitizer = nameSanitizer; + return this; + } + + public ScopeSanitizerBuilder withTagKeySanitizer(Function tagKeySanitizer) { + if (tagKeyValidCharacters != null) { + throw new IllegalArgumentException("only one of them can be provided: tagKeyValidCharacters, tagKeySanitizer"); + } + this.tagKeySanitizer = tagKeySanitizer; + return this; + } + + public ScopeSanitizerBuilder withTagValueSanitizer(Function tagValueSanitizer) { + if (tagValueValidCharacters != null) { + throw new IllegalArgumentException("only one of them can be provided: tagValueValidCharacters, tagValueSanitizer"); + } + this.tagValueSanitizer = tagValueSanitizer; + return this; + } + + public ScopeSanitizerBuilder withReplacementCharacter(char replacementChar) { + this.replacementChar = replacementChar; + return this; + } + + public ScopeSanitizerBuilder withNameValidCharacters(ValidCharacters validCharacters) { + if (this.nameSanitizer != null) { + throw new IllegalArgumentException("only one of them can be provided: nameValidCharacters, nameSanitizer"); + } + this.nameValidCharacters = validCharacters; + return this; + } + + public ScopeSanitizerBuilder withTagKeyValidCharacters(ValidCharacters validCharacters) { + if (this.tagKeySanitizer != null) { + throw new IllegalArgumentException("only one of them can be provided: tagKeyValidCharacters, tagKeySanitizer"); + } + this.tagKeyValidCharacters = validCharacters; + return this; + } + + public ScopeSanitizerBuilder withTagValueValidCharacters(ValidCharacters validCharacters) { + if (this.tagValueSanitizer != null) { + throw new IllegalArgumentException("only one of them can be provided: tagValueValidCharacters, tagValueSanitizer"); + } + this.tagValueValidCharacters = validCharacters; + return this; + } + + public ScopeSanitizer build() { + if (nameSanitizer == null) { + nameSanitizer = Function.identity(); + } + if (nameValidCharacters != null) { + nameSanitizer = nameValidCharacters.sanitizeStringFunc(replacementChar); + } + + if (tagKeySanitizer == null) { + tagKeySanitizer = Function.identity(); + } + if (tagKeyValidCharacters != null) { + tagKeySanitizer = tagKeyValidCharacters.sanitizeStringFunc(replacementChar); + } + + if (tagValueSanitizer == null) { + tagValueSanitizer = Function.identity(); + } + if (tagValueValidCharacters != null) { + tagValueSanitizer = tagValueValidCharacters.sanitizeStringFunc(replacementChar); + } + return new SanitizerImpl(nameSanitizer, tagKeySanitizer, tagValueSanitizer); + } +} diff --git a/core/src/main/java/com/uber/m3/tally/sanitizers/ValidCharacters.java b/core/src/main/java/com/uber/m3/tally/sanitizers/ValidCharacters.java new file mode 100644 index 0000000..b22cfd7 --- /dev/null +++ b/core/src/main/java/com/uber/m3/tally/sanitizers/ValidCharacters.java @@ -0,0 +1,128 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +/** + * ValidCharacters is a collection of valid characters. + */ +public class ValidCharacters { + + /** + * DEFAULT_REPLACEMENT_CHARACTER is the default character used for replacements. + */ + public static char DEFAULT_REPLACEMENT_CHARACTER = '_'; + + /** + * ALPHANUMERIC_RANGE is the range of alphanumeric characters. + */ + public static final Set ALPHANUMERIC_RANGE = + new HashSet<>(Arrays.asList( + CharRange.of('a', 'z'), + CharRange.of('A', 'Z'), + CharRange.of('0', '9'))); + + /** + * UNDERSCORE_CHARACTERS contains the underscore character. + */ + public static final Set UNDERSCORE_CHARACTERS = Collections.singleton('_'); + + /** + * UNDERSCORE_DASH_CHARACTERS contains the underscore and dash characters. + */ + public static final Set UNDERSCORE_DASH_CHARACTERS = new HashSet<>(Arrays.asList('_', '-')); + + /** + * UNDERSCORE_DASH_DOT_CHARACTERS contains the underscore, dash and dot characters. + */ + public static final Set UNDERSCORE_DASH_DOT_CHARACTERS = new HashSet<>(Arrays.asList('_', '-', '.')); + + private final Set validRanges; + private final Set validCharacters; + + private ValidCharacters(Set validRanges, Set validCharacters) { + this.validRanges = (validRanges != null) ? validRanges : Collections.emptySet(); + this.validCharacters = (validCharacters != null) ? validCharacters : Collections.emptySet(); + } + + /** + * returns an instance of ValidCharacters + * + * @param ranges + * @param characters + * @return + */ + public static ValidCharacters of(Set ranges, Set characters) { + return new ValidCharacters(ranges, characters); + } + + /** + * returns if a char is valid + * a char is valid if it's within range of any range of validRanges or within validCharacters + * + * @param ch + * @return + */ + private boolean isValid(char ch) { + return validRanges.stream().anyMatch(range -> (range.isWithinRange(ch))) + || validCharacters.contains(ch); + } + + Function sanitizeStringFunc(char replaceChar) { + return input -> { + StringBuilder output = null; + + for (int i = 0; i < input.length(); i++) { + char currChar = input.charAt(i); + + // first check if the provided character is valid + boolean isCurrValid = isValid(currChar); + + // if it's valid, we can optimize allocations by avoiding copying + if (isCurrValid) { + if (output != null) { + output.append(currChar); + } + continue; + } + + // the character is invalid, and the buffer has not been initialized + // so we initialize the buffer and back-fill. + if (output == null) { + output = new StringBuilder(input.length()); + output.append(input, 0, i); + } + + // write the replacement character + output.append(replaceChar); + } + + // return input un-touched if the buffer has not been initialized + // otherwise, return the newly constructed buffer string + return (output == null) ? input : output.toString(); + }; + } +} diff --git a/core/src/test/java/com/uber/m3/tally/sanitizers/CharRangeTest.java b/core/src/test/java/com/uber/m3/tally/sanitizers/CharRangeTest.java new file mode 100644 index 0000000..09ade2a --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/sanitizers/CharRangeTest.java @@ -0,0 +1,55 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class CharRangeTest { + + @Test + public void sanitizeRange() { + CharRange range = CharRange.of('a', 'z'); + assertNotNull(range); + assertEquals('a', range.low()); + assertEquals('z', range.high()); + assertTrue(range.isWithinRange('a')); + assertTrue(range.isWithinRange('z')); + assertTrue(range.isWithinRange('b')); + } + + @Test + public void sanitizeRangeSameLowHigh() { + CharRange range = CharRange.of('a', 'a'); + assertNotNull(range); + assertEquals('a', range.low()); + assertEquals('a', range.high()); + assertTrue(range.isWithinRange('a')); + } + + @Test(expected = IllegalArgumentException.class) + public void sanitizeRangeWrongLowHigh() { + CharRange range = CharRange.of('b', 'a'); + } +} diff --git a/core/src/test/java/com/uber/m3/tally/sanitizers/ScopeSanitizerTest.java b/core/src/test/java/com/uber/m3/tally/sanitizers/ScopeSanitizerTest.java new file mode 100644 index 0000000..6712ebc --- /dev/null +++ b/core/src/test/java/com/uber/m3/tally/sanitizers/ScopeSanitizerTest.java @@ -0,0 +1,154 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.sanitizers; + +import org.junit.Test; + +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; + +public class ScopeSanitizerTest { + + private static final String NAME = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String KEY = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String VALUE = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String SANITIZED_NAME_1 = "sanitized-name"; + private static final String SANITIZED_KEY_1 = "sanitized-key"; + private static final String SANITIZED_VALUE_1 = "sanitized-value"; + private static final String SANITIZED_NAME_2 = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890___"; + private static final String SANITIZED_KEY_2 = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-_"; + private static final String SANITIZED_VALUE_2 = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String SANITIZED_NAME_3 = "@@@@@@@@@@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_@@"; + private static final String SANITIZED_KEY_3 = "@@@@@@@@@@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-@"; + private static final String SANITIZED_VALUE_3 = "@@@@@@@@@@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final char REPLACEMENT_CHAR = '@'; + + @Test + public void builderNoopSanitizer() { + ScopeSanitizer sanitizer = new ScopeSanitizerBuilder().build(); + assertEquals(NAME, sanitizer.sanitizeName(NAME)); + assertEquals(KEY, sanitizer.sanitizeTagKey(KEY)); + assertEquals(VALUE, sanitizer.sanitizeTagValue(VALUE)); + } + + @Test + public void noopSanitizer() { + ScopeSanitizer sanitizer = new NoopSanitizer(); + assertEquals(NAME, sanitizer.sanitizeName(NAME)); + assertEquals(KEY, sanitizer.sanitizeTagKey(KEY)); + assertEquals(VALUE, sanitizer.sanitizeTagValue(VALUE)); + } + + @Test + public void withSanitizers() { + ScopeSanitizer sanitizer = + new ScopeSanitizerBuilder() + .withNameSanitizer(value -> SANITIZED_NAME_1) + .withTagKeySanitizer(value -> SANITIZED_KEY_1) + .withTagValueSanitizer(value -> SANITIZED_VALUE_1) + .build(); + assertEquals(SANITIZED_NAME_1, sanitizer.sanitizeName(NAME)); + assertEquals(SANITIZED_KEY_1, sanitizer.sanitizeTagKey(KEY)); + assertEquals(SANITIZED_VALUE_1, sanitizer.sanitizeTagValue(VALUE)); + } + + @Test + public void withValidCharactersAndDefaultRepChar() { + ScopeSanitizer sanitizer = + new ScopeSanitizerBuilder() + .withNameValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_CHARACTERS)) + .withTagKeyValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagValueValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_DOT_CHARACTERS)) + .build(); + assertEquals(SANITIZED_NAME_2, sanitizer.sanitizeName(NAME)); + assertEquals(SANITIZED_KEY_2, sanitizer.sanitizeTagKey(KEY)); + assertEquals(SANITIZED_VALUE_2, sanitizer.sanitizeTagValue(VALUE)); + } + + @Test + public void withValidCharactersAndRepChar() { + ScopeSanitizer sanitizer = + new ScopeSanitizerBuilder() + .withReplacementCharacter(REPLACEMENT_CHAR) + .withNameValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_CHARACTERS)) + .withTagKeyValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagValueValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_DOT_CHARACTERS)) + .build(); + assertEquals(SANITIZED_NAME_3, sanitizer.sanitizeName(NAME)); + assertEquals(SANITIZED_KEY_3, sanitizer.sanitizeTagKey(KEY)); + assertEquals(SANITIZED_VALUE_3, sanitizer.sanitizeTagValue(VALUE)); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidBuilderConflictNameSanitizer() { + new ScopeSanitizerBuilder() + .withReplacementCharacter(REPLACEMENT_CHAR) + .withNameValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_CHARACTERS)) + .withNameSanitizer(Function.identity()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidBuilderConflictTagKeySanitizer() { + new ScopeSanitizerBuilder() + .withReplacementCharacter(REPLACEMENT_CHAR) + .withTagKeyValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagKeySanitizer(Function.identity()) + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidBuilderConflictTagValueSanitizer() { + new ScopeSanitizerBuilder() + .withReplacementCharacter(REPLACEMENT_CHAR) + .withTagValueValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagValueSanitizer(Function.identity()) + .build(); + } +} diff --git a/m3/src/main/java/com/uber/m3/tally/m3/M3Sanitizer.java b/m3/src/main/java/com/uber/m3/tally/m3/M3Sanitizer.java new file mode 100644 index 0000000..57e15e6 --- /dev/null +++ b/m3/src/main/java/com/uber/m3/tally/m3/M3Sanitizer.java @@ -0,0 +1,51 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.m3; + +import com.uber.m3.tally.sanitizers.ScopeSanitizer; +import com.uber.m3.tally.sanitizers.ScopeSanitizerBuilder; +import com.uber.m3.tally.sanitizers.ValidCharacters; + +public class M3Sanitizer { + + /** + * Creates the default M3 sanitizer. + * + * @return default M3 sanitizer + */ + public static ScopeSanitizer create() { + return new ScopeSanitizerBuilder() + .withReplacementCharacter(ValidCharacters.DEFAULT_REPLACEMENT_CHARACTER) + .withNameValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagKeyValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .withTagValueValidCharacters( + ValidCharacters.of( + ValidCharacters.ALPHANUMERIC_RANGE, + ValidCharacters.UNDERSCORE_DASH_CHARACTERS)) + .build(); + } +} diff --git a/m3/src/test/java/com/uber/m3/tally/m3/M3SanitizerTest.java b/m3/src/test/java/com/uber/m3/tally/m3/M3SanitizerTest.java new file mode 100644 index 0000000..b54d8d8 --- /dev/null +++ b/m3/src/test/java/com/uber/m3/tally/m3/M3SanitizerTest.java @@ -0,0 +1,46 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package com.uber.m3.tally.m3; + +import com.uber.m3.tally.sanitizers.ScopeSanitizer; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class M3SanitizerTest { + + private static final String NAME = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String KEY = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String VALUE = "!@#$%^&*()abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-."; + private static final String SANITIZED_NAME = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-_"; + private static final String SANITIZED_KEY = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-_"; + private static final String SANITIZED_VALUE = "__________abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-_"; + + @Test + public void m3Sanitizer() { + ScopeSanitizer sanitizer = M3Sanitizer.create(); + assertNotNull(sanitizer); + assertEquals(SANITIZED_NAME, sanitizer.sanitizeName(NAME)); + assertEquals(SANITIZED_KEY, sanitizer.sanitizeTagKey(KEY)); + assertEquals(SANITIZED_VALUE, sanitizer.sanitizeTagValue(VALUE)); + } +}