Skip to content
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

Adds Observation Granularity #4613

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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 @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

FULL, DETAILED, BASIC, OFF

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,60 @@ static <T extends Context> Observation start(String name, Supplier<T> 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.
* <p>
* 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 <T extends Context> Observation start(String name, Supplier<T> contextSupplier, ObservationLevel level,
@Nullable ObservationRegistry registry) {
return createNotStarted(name, contextSupplier, level, registry).start();
}

/**
* Creates but <b>does not start</b> 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 <b>does not start</b> an {@link Observation}. Remember to call
* {@link Observation#start()} when you want the measurements to start. When no
Expand Down Expand Up @@ -122,13 +176,41 @@ static Observation createNotStarted(String name, @Nullable ObservationRegistry r
*/
static <T extends Context> Observation createNotStarted(String name, Supplier<T> contextSupplier,
@Nullable ObservationRegistry registry) {
return createNotStarted(name, contextSupplier, null, registry);
}

/**
* Creates but <b>does not start</b> 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 <T extends Context> Observation createNotStarted(String name, Supplier<T> contextSupplier,
@Nullable ObservationLevel level, @Nullable ObservationRegistry registry) {
if (registry == null || registry.isNoop()) {
return NOOP;
}
Context context = contextSupplier.get();
if (context.getName() == null) {
context.setName(name);
}
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);
}
Expand Down Expand Up @@ -178,7 +260,7 @@ static <T extends Context> 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);
}
Expand Down Expand Up @@ -316,7 +398,7 @@ static <T extends Context> Observation createNotStarted(ObservationConvention<T>
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);
}
Expand Down Expand Up @@ -417,7 +499,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;
}

/**
Expand Down Expand Up @@ -923,7 +1005,10 @@ class Context implements ContextView {
private Throwable error;

@Nullable
private ObservationView parentObservation;
private ObservationView parentObservationView;

@Nullable
private ObservationLevel level;

private final Map<String, KeyValue> lowCardinalityKeyValues = new LinkedHashMap<>();

Expand Down Expand Up @@ -970,15 +1055,15 @@ public void setContextualName(@Nullable String contextualName) {
*/
@Nullable
public ObservationView getParentObservation() {
return parentObservation;
return parentObservationView;
}

/**
* Sets the parent {@link ObservationView}.
* @param parentObservation parent observation to set
*/
public void setParentObservation(@Nullable ObservationView parentObservation) {
this.parentObservation = parentObservation;
this.parentObservationView = parentObservation;
}

/**
Expand All @@ -987,7 +1072,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);
Expand Down Expand Up @@ -1232,12 +1317,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) {
Expand Down Expand Up @@ -1452,6 +1546,14 @@ default <T> T getOrDefault(Object key, Supplier<T> defaultObjectSupplier) {
@NonNull
KeyValues getAllKeyValues();

/**
* Returns the observation level.
* @return observation level
*/
default Level getObservationLevel() {
return Level.FULL;
}

}

/**
Expand Down Expand Up @@ -1487,4 +1589,56 @@ interface CheckedFunction<T, R, E extends Throwable> {

}

/**
* Mapping of {@link Level} to {@link Class}.
*
* @author Marcin Grzejszczak
* @since 1.13.0
*/
class ObservationLevel {

private final Level level;

public ObservationLevel(Level level) {
this.level = level;
}

public Level getLevel() {
return level;
}

/**
* Sets {@link Level#FULL} for observation of the given classs.
* @return observation level
*/
public static ObservationLevel full() {
return new ObservationLevel(Level.FULL);
}

/**
* Sets {@link Level#BASIC} for observation of the given classs.
* @return observation level
*/
public static ObservationLevel basic() {
return new ObservationLevel(Level.BASIC);
}

/**
* Sets {@link Level#DETAILED} for observation of the given classs.
* @return observation level
*/
public static ObservationLevel detailed() {
return new ObservationLevel(Level.DETAILED);
}

/**
* Sets {@link Level#OFF} for observation of the given classs.
* @return observation level
*/
public static ObservationLevel off() {
return new ObservationLevel(Level.OFF);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

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;

Expand Down Expand Up @@ -102,6 +104,8 @@ class ObservationConfig {

private final List<ObservationFilter> observationFilters = new CopyOnWriteArrayList<>();

private final Map<String, Level> observationLevels = new ConcurrentHashMap<>();

/**
* Register a handler for the {@link Observation observations}.
* @param handler handler to add to the current configuration
Expand Down Expand Up @@ -149,6 +153,27 @@ public ObservationConfig observationConvention(GlobalObservationConvention<?> ob
return this;
}

/**
* Sets an observation level for the given observation name.
* @param observationName observation name
* @param level observation level
* @return This configuration instance
*/
public ObservationConfig observationLevel(String observationName, Level level) {
this.observationLevels.put(observationName, level);
return this;
}

/**
* Sets observation levels.
* @param levels observation levels (observation name to level mappings)
* @return This configuration instance
*/
public ObservationConfig observationLevels(Map<String, Level> levels) {
this.observationLevels.putAll(levels);
return this;
}

/**
* Finds an {@link ObservationConvention} for the given
* {@link Observation.Context}.
Expand Down Expand Up @@ -181,6 +206,20 @@ boolean isObservationEnabled(String name, @Nullable Observation.Context context)
return false;
}
}
if (context != null) {
ObservationLevel level = context.getLevel();
if (level == null) {
return true;
}
String observationName = context.getName();
for (Entry<String, Level> levelEntry : this.observationLevels.entrySet()) {
if (levelEntry.getKey().equalsIgnoreCase(observationName)) {
// exact or partial match
// e.g. ctx has INFO (3), configured is DEBUG (2)
return level.getLevel().ordinal() >= levelEntry.getValue().ordinal();
}
}
}
return true;
}

Expand Down
Loading