Skip to content

Commit 39821d9

Browse files
Improve no-op behavior of the Observation API (#6700)
There are four cases when an Observation can be no-op: 1. The ObservationRegistry is null 2. The ObservationRegistry is ObservationRegistry.NOOP 3. The ObservationRegistry does not have any handlers 4. An Observation is disabled by an ObservationPredicate In the first three cases, no Observations are created. In the last case, some Observations can be created but some can be ignored. In this scenario, we need to support scoping (context-propagation) but in the first three we don't. This change makes the NoopObservation truly no-op to cover the first three cases and introduces NoopButScopeHandlingObservation to cover the last one. This should result in better performance in cases where JIT can eliminate the code because of NoopObservation being truly no-op.
1 parent e52190c commit 39821d9

File tree

10 files changed

+135
-100
lines changed

10 files changed

+135
-100
lines changed

benchmarks/benchmarks-core/src/jmh/java/io/micrometer/benchmark/core/ObservationBenchmark.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
*/
1616
package io.micrometer.benchmark.core;
1717

18-
import java.util.concurrent.TimeUnit;
19-
2018
import io.micrometer.common.KeyValue;
2119
import io.micrometer.common.KeyValues;
2220
import io.micrometer.core.instrument.LongTaskTimer;
@@ -34,13 +32,17 @@
3432
import org.openjdk.jmh.runner.RunnerException;
3533
import org.openjdk.jmh.runner.options.OptionsBuilder;
3634

35+
import java.util.concurrent.TimeUnit;
36+
3737
@Fork(1)
3838
@Threads(4)
3939
@State(Scope.Benchmark)
4040
@BenchmarkMode(Mode.AverageTime)
4141
@OutputTimeUnit(TimeUnit.NANOSECONDS)
4242
public class ObservationBenchmark {
4343

44+
private static final Exception error = new IllegalStateException("error");
45+
4446
SimpleMeterRegistry meterRegistry;
4547

4648
ObservationRegistry observationRegistry;
@@ -181,13 +183,15 @@ public ObservationOrTimerCompatibleInstrumentation<Observation.Context> observat
181183
return instrumentation;
182184
}
183185

186+
// This should not measure anything, JIT should figure out that the registry is noop
184187
@Benchmark
185188
public Observation noopObservation() {
186-
// This might not measure anything if JIT figures it out that the registry is
187-
// always noop
188189
Observation observation = Observation.createNotStarted("test.obs", noopRegistry)
189190
.lowCardinalityKeyValue("abc", "123")
190191
.start();
192+
try (Observation.Scope ignored = observation.openScope()) {
193+
observation.error(error);
194+
}
191195
observation.stop();
192196

193197
return observation;

docs/src/test/java/io/micrometer/docs/observation/ObservationConfiguringTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,9 @@ void observation_config_customization() {
7676
.observationHandler(new DefaultMeterObservationHandler(meterRegistry));
7777

7878
// Observation will be ignored because of the name
79-
then(Observation.start("to.ignore", () -> new MyContext("don't ignore"), registry)).isSameAs(Observation.NOOP);
79+
then(Observation.start("to.ignore", () -> new MyContext("don't ignore"), registry).isNoop()).isTrue();
8080
// Observation will be ignored because of the entries in MyContext
81-
then(Observation.start("not.to.ignore", () -> new MyContext("user to ignore"), registry))
82-
.isSameAs(Observation.NOOP);
81+
then(Observation.start("not.to.ignore", () -> new MyContext("user to ignore"), registry).isNoop()).isTrue();
8382

8483
// Observation will not be ignored...
8584
MyContext myContext = new MyContext("user not to ignore");
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025 VMware, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micrometer.observation;
17+
18+
/**
19+
* No-op implementation of {@link Observation} except scope opening so that we can disable
20+
* the instrumentation for certain Observations but keep context-propagation working.
21+
*
22+
* @author Jonatan Ivanov
23+
* @author Tommy Ludwig
24+
* @author Marcin Grzejszczak
25+
*/
26+
final class NoopButScopeHandlingObservation extends NoopObservation {
27+
28+
static final Observation INSTANCE = new NoopButScopeHandlingObservation();
29+
30+
@Override
31+
public Scope openScope() {
32+
return new SimpleObservation.SimpleScope(NoopObservationRegistry.FOR_SCOPES, this);
33+
}
34+
35+
}

micrometer-observation/src/main/java/io/micrometer/observation/NoopObservation.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020

2121
/**
2222
* No-op implementation of {@link Observation} so that we can disable the instrumentation
23-
* logic.
23+
* logic entirely.
2424
*
2525
* @author Jonatan Ivanov
2626
* @author Tommy Ludwig
2727
* @author Marcin Grzejszczak
28-
* @since 1.10.0
2928
*/
30-
final class NoopObservation implements Observation {
29+
class NoopObservation implements Observation {
30+
31+
static final Observation INSTANCE = new NoopObservation();
3132

3233
private static final Context CONTEXT = new Context();
3334

@@ -92,7 +93,7 @@ public void stop() {
9293

9394
@Override
9495
public Scope openScope() {
95-
return new SimpleObservation.SimpleScope(NoopObservationRegistry.FOR_SCOPES, this);
96+
return NoopScope.INSTANCE;
9697
}
9798

9899
/**
@@ -111,7 +112,7 @@ private NoopScope() {
111112

112113
@Override
113114
public Observation getCurrentObservation() {
114-
return Observation.NOOP;
115+
return NoopObservation.INSTANCE;
115116
}
116117

117118
@Override

micrometer-observation/src/main/java/io/micrometer/observation/NoopObservationConvention.java

Lines changed: 0 additions & 64 deletions
This file was deleted.

micrometer-observation/src/main/java/io/micrometer/observation/Observation.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@
5050
public interface Observation extends ObservationView {
5151

5252
/**
53-
* No-op observation.
53+
* No-op observation. Do not use it to check if an Observation is no-op, use
54+
* {@code observation.isNoop()} instead.
5455
*/
55-
Observation NOOP = new NoopObservation();
56+
Observation NOOP = NoopObservation.INSTANCE;
5657

5758
/**
5859
* Create and start an {@link Observation} with the given name. All Observations of
@@ -122,12 +123,12 @@ static Observation createNotStarted(String name, @Nullable ObservationRegistry r
122123
static <T extends Context> Observation createNotStarted(String name, Supplier<T> contextSupplier,
123124
@Nullable ObservationRegistry registry) {
124125
if (registry == null || registry.isNoop()) {
125-
return NOOP;
126+
return NoopObservation.INSTANCE;
126127
}
127128
Context context = contextSupplier.get();
128129
context.setParentFromCurrentObservation(registry);
129130
if (!registry.observationConfig().isObservationEnabled(name, context)) {
130-
return NOOP;
131+
return NoopButScopeHandlingObservation.INSTANCE;
131132
}
132133
return new SimpleObservation(name, registry, context);
133134
}
@@ -165,7 +166,7 @@ static <T extends Context> Observation createNotStarted(@Nullable ObservationCon
165166
ObservationConvention<T> defaultConvention, Supplier<T> contextSupplier,
166167
@Nullable ObservationRegistry registry) {
167168
if (registry == null || registry.isNoop()) {
168-
return Observation.NOOP;
169+
return NoopObservation.INSTANCE;
169170
}
170171
ObservationConvention<T> convention;
171172
T context = contextSupplier.get();
@@ -177,7 +178,7 @@ static <T extends Context> Observation createNotStarted(@Nullable ObservationCon
177178
convention = registry.observationConfig().getObservationConvention(context, defaultConvention);
178179
}
179180
if (!registry.observationConfig().isObservationEnabled(convention.getName(), context)) {
180-
return NOOP;
181+
return NoopButScopeHandlingObservation.INSTANCE;
181182
}
182183
return new SimpleObservation(convention, registry, context);
183184
}
@@ -309,13 +310,13 @@ static Observation createNotStarted(ObservationConvention<Context> observationCo
309310
*/
310311
static <T extends Context> Observation createNotStarted(ObservationConvention<T> observationConvention,
311312
Supplier<T> contextSupplier, ObservationRegistry registry) {
312-
if (registry == null || registry.isNoop() || observationConvention == NoopObservationConvention.INSTANCE) {
313-
return NOOP;
313+
if (registry == null || registry.isNoop()) {
314+
return NoopObservation.INSTANCE;
314315
}
315316
T context = contextSupplier.get();
316317
context.setParentFromCurrentObservation(registry);
317318
if (!registry.observationConfig().isObservationEnabled(observationConvention.getName(), context)) {
318-
return NOOP;
319+
return NoopButScopeHandlingObservation.INSTANCE;
319320
}
320321
return new SimpleObservation(observationConvention, registry, context);
321322
}
@@ -416,7 +417,7 @@ default Observation highCardinalityKeyValues(KeyValues keyValues) {
416417
* @return {@code true} when this is a no-op observation
417418
*/
418419
default boolean isNoop() {
419-
return this == NOOP;
420+
return this == NoopObservation.INSTANCE || this == NoopButScopeHandlingObservation.INSTANCE;
420421
}
421422

422423
/**
@@ -622,7 +623,7 @@ default <T, E extends Throwable> CheckedCallable<T, E> wrapChecked(CheckedCallab
622623
* <li>Stops the {@code Observation}</li>
623624
* </ul>
624625
*
625-
* NOTE: When the {@link ObservationRegistry} is a noop, this function receives a
626+
* NOTE: When the {@link ObservationRegistry} is a no-op, this function receives a
626627
* default {@link Context} instance which is not the one that has been passed at
627628
* {@link Observation} creation.
628629
* @param function the {@link Function} to call
@@ -663,7 +664,7 @@ default <T, E extends Throwable> CheckedCallable<T, E> wrapChecked(CheckedCallab
663664
* <li>Stops the {@code Observation}</li>
664665
* </ul>
665666
*
666-
* NOTE: When the {@link ObservationRegistry} is a noop, this function receives a
667+
* NOTE: When the {@link ObservationRegistry} is a no-op, this function receives a
667668
* default {@link Context} instance which is not the one that has been passed at
668669
* {@link Observation} creation.
669670
* @param function the {@link CheckedFunction} to call

micrometer-observation/src/main/java/io/micrometer/observation/ObservationPredicate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
/**
2121
* A predicate to define whether {@link Observation observation} should be created or a
22-
* {@link NoopObservation} instead.
22+
* no-op Observation instead.
2323
*
2424
* @author Jonatan Ivanov
2525
* @author Tommy Ludwig

micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public ObservationConfig observationHandler(ObservationHandler<?> handler) {
112112

113113
/**
114114
* Register a predicate to define whether {@link Observation observation} should
115-
* be created or a {@link NoopObservation} instead.
115+
* be created or a no-op Observation instead.
116116
* @param predicate predicate
117117
* @return This configuration instance
118118
*/
@@ -167,8 +167,8 @@ <T extends Observation.Context> ObservationConvention<T> getObservationConventio
167167
}
168168

169169
/**
170-
* Check to assert whether {@link Observation} should be created or
171-
* {@link NoopObservation} instead.
170+
* Check to assert whether {@link Observation} should be created or a no-op
171+
* Observation instead.
172172
* @param name observation technical name
173173
* @param context context
174174
* @return {@code true} when observation is enabled

micrometer-observation/src/test/java/io/micrometer/observation/NoopObservationRegistryTests.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ class NoopObservationRegistryTests {
2323

2424
@Test
2525
@SuppressWarnings("NullAway")
26-
void should_respect_scopes() {
27-
ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
26+
void shouldRespectScopesIfDisabledByPredicate() {
27+
ObservationRegistry observationRegistry = ObservationRegistry.create();
28+
observationRegistry.observationConfig().getObservationHandlers().add(new ObservationTextPublisher());
29+
observationRegistry.observationConfig().observationPredicate((name, context) -> false);
2830
then(observationRegistry.getCurrentObservation()).isNull();
2931

3032
Observation observation = Observation.start("foo", observationRegistry);
3133
then(observation.isNoop()).isTrue();
34+
then(observation).isSameAs(NoopButScopeHandlingObservation.INSTANCE);
3235
then(observationRegistry.getCurrentObservation()).isNull();
3336

3437
try (Observation.Scope scope = observation.openScope()) {
@@ -45,4 +48,60 @@ void should_respect_scopes() {
4548
then(observationRegistry.getCurrentObservation()).isNull();
4649
}
4750

51+
@Test
52+
@SuppressWarnings("NullAway")
53+
void shouldNotRespectScopesIfNullRegistryIsUsed() {
54+
Observation observation = Observation.start("foo", null);
55+
then(observation.isNoop()).isTrue();
56+
then(observation).isSameAs(NoopObservation.INSTANCE);
57+
}
58+
59+
@Test
60+
@SuppressWarnings("NullAway")
61+
void shouldNotRespectScopesIfNoopRegistryIsUsed() {
62+
ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
63+
then(observationRegistry.getCurrentObservation()).isNull();
64+
65+
Observation observation = Observation.start("foo", observationRegistry);
66+
then(observation.isNoop()).isTrue();
67+
then(observation).isSameAs(NoopObservation.INSTANCE);
68+
then(observationRegistry.getCurrentObservation()).isNull();
69+
70+
try (Observation.Scope ignored = observation.openScope()) {
71+
then(observationRegistry.getCurrentObservationScope()).isSameAs(Observation.Scope.NOOP);
72+
then(observationRegistry.getCurrentObservation()).isNull();
73+
try (Observation.Scope ignored2 = observation.openScope()) {
74+
then(observationRegistry.getCurrentObservationScope()).isSameAs(Observation.Scope.NOOP);
75+
then(observationRegistry.getCurrentObservation()).isNull();
76+
}
77+
then(observationRegistry.getCurrentObservation()).isNull();
78+
}
79+
80+
then(observationRegistry.getCurrentObservation()).isNull();
81+
}
82+
83+
@Test
84+
@SuppressWarnings("NullAway")
85+
void shouldNotRespectScopesIfNoHandlersAreRegistered() {
86+
ObservationRegistry observationRegistry = ObservationRegistry.create();
87+
then(observationRegistry.getCurrentObservation()).isNull();
88+
89+
Observation observation = Observation.start("foo", observationRegistry);
90+
then(observation.isNoop()).isTrue();
91+
then(observation).isSameAs(NoopObservation.INSTANCE);
92+
then(observationRegistry.getCurrentObservation()).isNull();
93+
94+
try (Observation.Scope ignored = observation.openScope()) {
95+
then(observationRegistry.getCurrentObservationScope()).isSameAs(Observation.Scope.NOOP);
96+
then(observationRegistry.getCurrentObservation()).isNull();
97+
try (Observation.Scope ignored2 = observation.openScope()) {
98+
then(observationRegistry.getCurrentObservationScope()).isSameAs(Observation.Scope.NOOP);
99+
then(observationRegistry.getCurrentObservation()).isNull();
100+
}
101+
then(observationRegistry.getCurrentObservation()).isNull();
102+
}
103+
104+
then(observationRegistry.getCurrentObservation()).isNull();
105+
}
106+
48107
}

0 commit comments

Comments
 (0)