From d22363242cf56e5415201a9ca3748310ecf0ecfe Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 25 Jan 2024 15:30:01 +0100 Subject: [PATCH] Adds Observation Granularity - introduces observation levels (same as in logging) - extends the current API to allow passing of ObservationLevel fixes gh-3442 --- .../ObservationConfiguringTests.java | 8 +- .../java/io/micrometer/observation/Level.java | 28 +++ .../observation/NoopObservation.java | 2 +- .../micrometer/observation/Observation.java | 216 +++++++++++++++++- .../observation/ObservationRegistry.java | 58 ++++- .../PassthroughNoopObservation.java | 44 ++++ .../observation/ObservationTests.java | 21 +- .../ObservationTextPublisherTests.java | 2 +- 8 files changed, 358 insertions(+), 21 deletions(-) create mode 100644 micrometer-observation/src/main/java/io/micrometer/observation/Level.java create mode 100644 micrometer-observation/src/main/java/io/micrometer/observation/PassthroughNoopObservation.java diff --git a/docs/src/test/java/io/micrometer/docs/observation/ObservationConfiguringTests.java b/docs/src/test/java/io/micrometer/docs/observation/ObservationConfiguringTests.java index 90c06d0444..5f9e17ae31 100644 --- a/docs/src/test/java/io/micrometer/docs/observation/ObservationConfiguringTests.java +++ b/docs/src/test/java/io/micrometer/docs/observation/ObservationConfiguringTests.java @@ -76,10 +76,12 @@ void observation_config_customization() { .observationHandler(new DefaultMeterObservationHandler(meterRegistry)); // Observation will be ignored because of the name - then(Observation.start("to.ignore", () -> new MyContext("don't ignore"), registry)).isSameAs(Observation.NOOP); + Observation ignoredBecauseOfName = Observation.start("to.ignore", () -> new MyContext("don't ignore"), + registry); + then(ignoredBecauseOfName.isNoop()).isTrue(); // Observation will be ignored because of the entries in MyContext - then(Observation.start("not.to.ignore", () -> new MyContext("user to ignore"), registry)) - .isSameAs(Observation.NOOP); + Observation notToIgnore = Observation.start("not.to.ignore", () -> new MyContext("user to ignore"), registry); + then(notToIgnore.isNoop()).isTrue(); // Observation will not be ignored... MyContext myContext = new MyContext("user not to ignore"); diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/Level.java b/micrometer-observation/src/main/java/io/micrometer/observation/Level.java new file mode 100644 index 0000000000..91c893811b --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/Level.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.observation; + +/** + * Observation Level. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +public enum Level { + + ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF + +} diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java b/micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java index 27e91f599b..413f555772 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java @@ -27,7 +27,7 @@ * @author Marcin Grzejszczak * @since 1.10.0 */ -final class NoopObservation implements Observation { +class NoopObservation implements Observation { private static final Context CONTEXT = new Context(); diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java index d3801305d3..11fb802a21 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java @@ -90,6 +90,60 @@ static Observation start(String name, Supplier contextSup return createNotStarted(name, contextSupplier, registry).start(); } + /** + * Create and start an {@link Observation} with the given name. All Observations of + * the same type must share the same name. + *

+ * When no registry is passed or the observation is + * {@link ObservationRegistry.ObservationConfig#observationPredicate(ObservationPredicate) + * not applicable}, a no-op observation will be returned. + * @param name name of the observation + * @param level observation level + * @param registry observation registry + * @return a started observation + * @since 1.13.0 + */ + static Observation start(String name, ObservationLevel level, @Nullable ObservationRegistry registry) { + return start(name, Context::new, level, registry); + } + + /** + * Creates and starts an {@link Observation}. When the {@link ObservationRegistry} is + * null or the no-op registry, this fast returns a no-op {@link Observation} and skips + * the creation of the {@link Observation.Context}. This check avoids unnecessary + * {@link Observation.Context} creation, which is why it takes a {@link Supplier} for + * the context rather than the context directly. If the observation is not enabled + * (see + * {@link ObservationRegistry.ObservationConfig#observationPredicate(ObservationPredicate) + * ObservationConfig#observationPredicate}), a no-op observation will also be + * returned. + * @param name name of the observation + * @param contextSupplier mutable context supplier + * @param level observation level + * @param registry observation registry + * @return started observation + * @since 1.13.0 + */ + static Observation start(String name, Supplier contextSupplier, ObservationLevel level, + @Nullable ObservationRegistry registry) { + return createNotStarted(name, contextSupplier, level, registry).start(); + } + + /** + * Creates but does not start an {@link Observation}. Remember to call + * {@link Observation#start()} when you want the measurements to start. When no + * registry is passed or observation is not applicable will return a no-op + * observation. + * @param name name of the observation + * @param level observation level + * @param registry observation registry + * @return created but not started observation + * @since 1.13.0 + */ + static Observation createNotStarted(String name, ObservationLevel level, @Nullable ObservationRegistry registry) { + return createNotStarted(name, Context::new, level, registry); + } + /** * Creates but does not start an {@link Observation}. Remember to call * {@link Observation#start()} when you want the measurements to start. When no @@ -122,13 +176,38 @@ static Observation createNotStarted(String name, @Nullable ObservationRegistry r */ static Observation createNotStarted(String name, Supplier contextSupplier, @Nullable ObservationRegistry registry) { + return createNotStarted(name, contextSupplier, null, registry); + } + + /** + * Creates but does not start an {@link Observation}. Remember to call + * {@link Observation#start()} when you want the measurements to start. When the + * {@link ObservationRegistry} is null or the no-op registry, this fast returns a + * no-op {@link Observation} and skips the creation of the + * {@link Observation.Context}. This check avoids unnecessary + * {@link Observation.Context} creation, which is why it takes a {@link Supplier} for + * the context rather than the context directly. If the observation is not enabled + * (see + * {@link ObservationRegistry.ObservationConfig#observationPredicate(ObservationPredicate) + * ObservationConfig#observationPredicate}), a no-op observation will also be + * returned. + * @param name name of the observation + * @param contextSupplier supplier for mutable context + * @param level observation level + * @param registry observation registry + * @return created but not started observation + * @since 1.13.0 + */ + static Observation createNotStarted(String name, Supplier contextSupplier, + @Nullable ObservationLevel level, @Nullable ObservationRegistry registry) { if (registry == null || registry.isNoop()) { return NOOP; } Context context = contextSupplier.get(); context.setParentFromCurrentObservation(registry); + context.setLevel(level != null ? level : null); if (!registry.observationConfig().isObservationEnabled(name, context)) { - return NOOP; + return new PassthroughNoopObservation(context.getParentObservation()); } return new SimpleObservation(name, registry, context); } @@ -178,7 +257,7 @@ static Observation createNotStarted(@Nullable ObservationCon convention = registry.observationConfig().getObservationConvention(context, defaultConvention); } if (!registry.observationConfig().isObservationEnabled(convention.getName(), context)) { - return NOOP; + return new PassthroughNoopObservation(context.getParentObservation()); } return new SimpleObservation(convention, registry, context); } @@ -316,7 +395,7 @@ static Observation createNotStarted(ObservationConvention T context = contextSupplier.get(); context.setParentFromCurrentObservation(registry); if (!registry.observationConfig().isObservationEnabled(observationConvention.getName(), context)) { - return NOOP; + return new PassthroughNoopObservation(context.getParentObservation()); } return new SimpleObservation(observationConvention, registry, context); } @@ -417,7 +496,7 @@ default Observation highCardinalityKeyValues(KeyValues keyValues) { * @return {@code true} when this is a no-op observation */ default boolean isNoop() { - return this == NOOP; + return this == NOOP || this instanceof NoopObservation; } /** @@ -923,7 +1002,10 @@ class Context implements ContextView { private Throwable error; @Nullable - private ObservationView parentObservation; + private ObservationView parentObservationView; + + @Nullable + private ObservationLevel level; private final Map lowCardinalityKeyValues = new LinkedHashMap<>(); @@ -970,7 +1052,7 @@ public void setContextualName(@Nullable String contextualName) { */ @Nullable public ObservationView getParentObservation() { - return parentObservation; + return parentObservationView; } /** @@ -978,7 +1060,7 @@ public ObservationView getParentObservation() { * @param parentObservation parent observation to set */ public void setParentObservation(@Nullable ObservationView parentObservation) { - this.parentObservation = parentObservation; + this.parentObservationView = parentObservation; } /** @@ -987,7 +1069,7 @@ public void setParentObservation(@Nullable ObservationView parentObservation) { * @param registry the {@link ObservationRegistry} in using */ void setParentFromCurrentObservation(ObservationRegistry registry) { - if (this.parentObservation == null) { + if (this.parentObservationView == null) { Observation currentObservation = registry.getCurrentObservation(); if (currentObservation != null) { setParentObservation(currentObservation); @@ -1232,12 +1314,21 @@ public KeyValues getAllKeyValues() { return getLowCardinalityKeyValues().and(getHighCardinalityKeyValues()); } + @Nullable + public ObservationLevel getLevel() { + return level; + } + + void setLevel(ObservationLevel level) { + this.level = level; + } + @Override public String toString() { return "name='" + name + '\'' + ", contextualName='" + contextualName + '\'' + ", error='" + error + '\'' + ", lowCardinalityKeyValues=" + toString(getLowCardinalityKeyValues()) + ", highCardinalityKeyValues=" + toString(getHighCardinalityKeyValues()) + ", map=" + toString(map) - + ", parentObservation=" + parentObservation; + + ", parentObservation=" + parentObservationView + ", observationLevel=" + level; } private String toString(KeyValues keyValues) { @@ -1452,6 +1543,14 @@ default T getOrDefault(Object key, Supplier defaultObjectSupplier) { @NonNull KeyValues getAllKeyValues(); + /** + * Returns the observation level. + * @return observation level + */ + default Level getObservationLevel() { + return Level.ALL; + } + } /** @@ -1487,4 +1586,103 @@ interface CheckedFunction { } + /** + * Mapping of {@link Level} to {@link Class}. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ + class ObservationLevel { + + private final Level level; + + private final Class clazz; + + public ObservationLevel(Level level, Class clazz) { + this.level = level; + this.clazz = clazz; + } + + public Level getLevel() { + return level; + } + + public Class getClazz() { + return clazz; + } + + /** + * Sets {@link Level#ALL} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel all(Class clazz) { + return new ObservationLevel(Level.ALL, clazz); + } + + /** + * Sets {@link Level#TRACE} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel trace(Class clazz) { + return new ObservationLevel(Level.TRACE, clazz); + } + + /** + * Sets {@link Level#DEBUG} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel debug(Class clazz) { + return new ObservationLevel(Level.DEBUG, clazz); + } + + /** + * Sets {@link Level#INFO} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel info(Class clazz) { + return new ObservationLevel(Level.INFO, clazz); + } + + /** + * Sets {@link Level#WARN} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel warn(Class clazz) { + return new ObservationLevel(Level.WARN, clazz); + } + + /** + * Sets {@link Level#ERROR} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel error(Class clazz) { + return new ObservationLevel(Level.ERROR, clazz); + } + + /** + * Sets {@link Level#FATAL} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel fatal(Class clazz) { + return new ObservationLevel(Level.FATAL, clazz); + } + + /** + * Sets {@link Level#OFF} for observation of the given classs. + * @param clazz class to observe + * @return observation level + */ + public static ObservationLevel off(Class clazz) { + return new ObservationLevel(Level.OFF, clazz); + } + + } + } diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java b/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java index 642fb90d62..82aa698694 100644 --- a/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java +++ b/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java @@ -17,11 +17,14 @@ import io.micrometer.common.lang.Nullable; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import io.micrometer.observation.Observation.ObservationLevel; + +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Implementations of this interface are responsible for managing state of an @@ -102,6 +105,8 @@ class ObservationConfig { private final List observationFilters = new CopyOnWriteArrayList<>(); + private final Map observationLevels = new ConcurrentHashMap<>(); + /** * Register a handler for the {@link Observation observations}. * @param handler handler to add to the current configuration @@ -149,6 +154,27 @@ public ObservationConfig observationConvention(GlobalObservationConvention ob return this; } + /** + * Sets an observation level for the given package name. + * @param packageName observation package name + * @param level observation level + * @return This configuration instance + */ + public ObservationConfig observationLevel(String packageName, Level level) { + this.observationLevels.put(packageName, level); + return this; + } + + /** + * Sets observation levels. + * @param levels observation levels (package to level mappings) + * @return This configuration instance + */ + public ObservationConfig observationLevels(Map levels) { + this.observationLevels.putAll(levels); + return this; + } + /** * Finds an {@link ObservationConvention} for the given * {@link Observation.Context}. @@ -181,6 +207,32 @@ boolean isObservationEnabled(String name, @Nullable Observation.Context context) return false; } } + if (context != null) { + ObservationLevel level = context.getLevel(); + if (level == null) { + return true; + } + Class clazz = level.getClazz(); + String classToObserveFqn = clazz.getCanonicalName(); + String classToObservePackage = clazz.getPackage().getName(); + // a.b.c - 2 + // a.b.c.D - 3 + // a.b - 1 + // we sort by string length, that means that we will find the closest + // matching first + List> sortedLevels = this.observationLevels.entrySet() + .stream() + .sorted(Collections.reverseOrder(Comparator.comparingInt(value -> value.getKey().length()))) + .collect(Collectors.toList()); + for (Entry levelEntry : sortedLevels) { + if (classToObserveFqn.equals(levelEntry.getKey()) + || classToObservePackage.contains(levelEntry.getKey())) { + // exact or partial match + // e.g. ctx has INFO (3), configured is DEBUG (2) + return level.getLevel().ordinal() >= levelEntry.getValue().ordinal(); + } + } + } return true; } diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/PassthroughNoopObservation.java b/micrometer-observation/src/main/java/io/micrometer/observation/PassthroughNoopObservation.java new file mode 100644 index 0000000000..bf82d6bcf2 --- /dev/null +++ b/micrometer-observation/src/main/java/io/micrometer/observation/PassthroughNoopObservation.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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.observation; + +import io.micrometer.common.lang.Nullable; + +/** + * No-op implementation of {@link Observation} that passes through to the parent + * observation. + * + * @author Marcin Grzejszczak + * @since 1.13.0 + */ +final class PassthroughNoopObservation extends NoopObservation { + + private final @Nullable ObservationView parentObservation; + + PassthroughNoopObservation(@Nullable ObservationView parentObservation) { + this.parentObservation = parentObservation; + } + + @Override + public ContextView getContextView() { + if (this.parentObservation != null) { + // we pass through to the parent + return this.parentObservation.getContextView(); + } + return super.getContextView(); + } + +} diff --git a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java index 7f7170ace8..d491fb9113 100644 --- a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java +++ b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java @@ -18,6 +18,7 @@ import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.Observation.ObservationLevel; import org.awaitility.Awaitility; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -71,7 +72,19 @@ void notMatchingObservationPredicateShouldResultInNoopObservation() { Observation observation = Observation.createNotStarted("foo", registry); - assertThat(observation).isSameAs(Observation.NOOP); + assertThat(observation).isInstanceOf(PassthroughNoopObservation.class); + } + + @Test + void notMatchingObservationLevelShouldResultInPassthroughObservation() { + registry.observationConfig().observationHandler(context -> true); + registry.observationConfig().observationPredicate((s, context) -> true); + registry.observationConfig().observationLevel("io.micrometer", Level.TRACE); + registry.observationConfig().observationLevel("io.micrometer.observation", Level.ERROR); + + Observation observation = Observation.createNotStarted("foo", ObservationLevel.debug(getClass()), registry); + + assertThat(observation).isInstanceOf(PassthroughNoopObservation.class); } @Test @@ -81,7 +94,7 @@ void matchingPredicateAndHandlerShouldNotResultInNoopObservation() { Observation observation = Observation.createNotStarted("foo", registry); - assertThat(observation).isNotSameAs(Observation.NOOP); + assertThat(observation).isNotInstanceOf(NoopObservation.class); } @Test @@ -91,11 +104,11 @@ void usingParentObservationToMatchPredicate() { .observationPredicate((s, context) -> !s.equals("child") || context.getParentObservation() != null); Observation childWithoutParent = Observation.createNotStarted("child", registry); - assertThat(childWithoutParent).isSameAs(Observation.NOOP); + assertThat(childWithoutParent).isInstanceOf(PassthroughNoopObservation.class); Observation childWithParent = Observation.createNotStarted("parent", registry) .observe(() -> Observation.createNotStarted("child", registry)); - assertThat(childWithParent).isNotSameAs(Observation.NOOP); + assertThat(childWithParent).isNotInstanceOf(NoopObservation.class); } @Test diff --git a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTextPublisherTests.java b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTextPublisherTests.java index 511d86fc94..86d80b465d 100644 --- a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTextPublisherTests.java +++ b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTextPublisherTests.java @@ -30,7 +30,7 @@ */ class ObservationTextPublisherTests { - private static final String CONTEXT_TOSTRING = "name='testName', contextualName='testContextualName', error='java.io.IOException: simulated', lowCardinalityKeyValues=[lcTag='foo'], highCardinalityKeyValues=[hcTag='bar'], map=[contextKey='contextValue'], parentObservation=null"; + private static final String CONTEXT_TOSTRING = "name='testName', contextualName='testContextualName', error='java.io.IOException: simulated', lowCardinalityKeyValues=[lcTag='foo'], highCardinalityKeyValues=[hcTag='bar'], map=[contextKey='contextValue'], parentObservation=null, observationLevel=null"; private final TestConsumer consumer = new TestConsumer();