Skip to content

Commit f1cc53f

Browse files
Remove ThreadLocal usage from LogbackMetrics (#6882)
Before this change, LogbackMetrics used Counter to keep track of the number of emitted log events by level. This caused issues since there are cases where incrementing a Counter can result in emitting log events. Because of this, the MetricsTurboFilter in LogbackMetrics had a chance to get into an infinite loop since it was triggered by emitting log events and it incremented a Counter that also emitted a log event which triggered MetricsTurboFilter again... The scenario was the following: 1. Somebody emits a log event (e.g.: LOGGER.info("test");) 2. MetricsTurboFilter gets notified about it and increments a Counter 3. Incrementing the Counter triggers emitting another log event 4. MetricsTurboFilter gets notified about it and increments a Counter ... To fix this issue, a ThreadLocal was used to ignore some of the log events (the ones that were emitted by incrementing a Counter). This change removes the ThreadLocal usage and instead of using Counter, it keeps track of the number of emitted log events using a LongAdder. Then it uses this LongAdder in a FunctionCounter. It assumes that LongAdder does not emit any log events that will end up triggering MetricsTurboFilter again.
1 parent d0a39e8 commit f1cc53f

File tree

5 files changed

+41
-106
lines changed

5 files changed

+41
-106
lines changed

implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/StatsdMeterRegistry.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,6 @@ private StatsdMeterRegistry(StatsdConfig config, HierarchicalNameMapper nameMapp
148148

149149
if (config.enabled()) {
150150
this.sink = processor.sink();
151-
152-
try {
153-
Class.forName("ch.qos.logback.classic.turbo.TurboFilter", false, getClass().getClassLoader());
154-
this.sink = new LogbackMetricsSuppressingFluxSink(this.sink);
155-
}
156-
catch (ClassNotFoundException ignore) {
157-
}
158151
start();
159152
}
160153
}

implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/LogbackMetricsSuppressingFluxSink.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
/**
2727
* This is an internal class only for use within Micrometer. This suppresses logback event
2828
* metrics during Sink operations to avoid infinite loops.
29+
*
30+
* @deprecated Should not be needed anymore since {@link LogbackMetrics} records logback
31+
* events asynchronously and should not get into an infinite loop.
2932
*/
33+
@Deprecated
3034
public class LogbackMetricsSuppressingFluxSink implements FluxSink<String> {
3135

3236
private final FluxSink<String> delegate;

micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/LogbackMetrics.java

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import ch.qos.logback.classic.turbo.TurboFilter;
2323
import ch.qos.logback.core.spi.FilterReply;
2424
import io.micrometer.core.instrument.Counter;
25+
import io.micrometer.core.instrument.FunctionCounter;
2526
import io.micrometer.core.instrument.MeterRegistry;
2627
import io.micrometer.core.instrument.Tag;
2728
import io.micrometer.core.instrument.binder.BaseUnits;
@@ -32,6 +33,7 @@
3233

3334
import java.util.HashMap;
3435
import java.util.Map;
36+
import java.util.concurrent.atomic.LongAdder;
3537

3638
import static java.util.Collections.emptyList;
3739

@@ -41,12 +43,11 @@
4143
* to an appender will not affect the metrics.
4244
*
4345
* @author Jon Schneider
46+
* @author Jonatan Ivanov
4447
*/
4548
@NullMarked
4649
public class LogbackMetrics implements MeterBinder, AutoCloseable {
4750

48-
static ThreadLocal<Boolean> ignoreMetrics = new ThreadLocal<>();
49-
5051
private final Iterable<Tag> tags;
5152

5253
private final LoggerContext loggerContext;
@@ -114,20 +115,18 @@ public void bindTo(MeterRegistry registry) {
114115
}
115116

116117
/**
117-
* Used by {@link Counter#increment()} implementations that may cause a logback
118-
* logging event to occur. Attempting to instrument that implementation would cause a
119-
* {@link StackOverflowError}.
120-
* @param r Don't record metrics on logging statements that occur inside of this
121-
* runnable.
118+
* This method was used by {@link Counter#increment()} implementations that may cause
119+
* a logback logging event to occur. Attempting to instrument that implementation used
120+
* to cause a {@link StackOverflowError}.
121+
* @param r The runnable to execute, previously it also disabled recording metrics on
122+
* logging statements that occur inside of this runnable but this is not needed
123+
* anymore.
124+
* @deprecated Should not be needed anymore since {@link LogbackMetrics} records
125+
* logback events asynchronously and should not get into an infinite loop.
122126
*/
127+
@Deprecated
123128
public static void ignoreMetrics(Runnable r) {
124-
ignoreMetrics.set(true);
125-
try {
126-
r.run();
127-
}
128-
finally {
129-
ignoreMetrics.remove();
130-
}
129+
r.run();
131130
}
132131

133132
@Override
@@ -148,46 +147,46 @@ class MetricsTurboFilter extends TurboFilter {
148147

149148
private static final String METER_DESCRIPTION = "Number of log events that were enabled by the effective log level";
150149

151-
private final Counter errorCounter;
150+
private final LongAdder errorCount = new LongAdder();
152151

153-
private final Counter warnCounter;
152+
private final LongAdder warnCount = new LongAdder();
154153

155-
private final Counter infoCounter;
154+
private final LongAdder infoCount = new LongAdder();
156155

157-
private final Counter debugCounter;
156+
private final LongAdder debugCount = new LongAdder();
158157

159-
private final Counter traceCounter;
158+
private final LongAdder traceCount = new LongAdder();
160159

161160
MetricsTurboFilter(MeterRegistry registry, Iterable<Tag> tags) {
162-
errorCounter = Counter.builder(METER_NAME)
161+
FunctionCounter.builder(METER_NAME, errorCount, LongAdder::doubleValue)
163162
.tags(tags)
164163
.tags("level", "error")
165164
.description(METER_DESCRIPTION)
166165
.baseUnit(BaseUnits.EVENTS)
167166
.register(registry);
168167

169-
warnCounter = Counter.builder(METER_NAME)
168+
FunctionCounter.builder(METER_NAME, warnCount, LongAdder::doubleValue)
170169
.tags(tags)
171170
.tags("level", "warn")
172171
.description(METER_DESCRIPTION)
173172
.baseUnit(BaseUnits.EVENTS)
174173
.register(registry);
175174

176-
infoCounter = Counter.builder(METER_NAME)
175+
FunctionCounter.builder(METER_NAME, infoCount, LongAdder::doubleValue)
177176
.tags(tags)
178177
.tags("level", "info")
179178
.description(METER_DESCRIPTION)
180179
.baseUnit(BaseUnits.EVENTS)
181180
.register(registry);
182181

183-
debugCounter = Counter.builder(METER_NAME)
182+
FunctionCounter.builder(METER_NAME, debugCount, LongAdder::doubleValue)
184183
.tags(tags)
185184
.tags("level", "debug")
186185
.description(METER_DESCRIPTION)
187186
.baseUnit(BaseUnits.EVENTS)
188187
.register(registry);
189188

190-
traceCounter = Counter.builder(METER_NAME)
189+
FunctionCounter.builder(METER_NAME, traceCount, LongAdder::doubleValue)
191190
.tags(tags)
192191
.tags("level", "trace")
193192
.description(METER_DESCRIPTION)
@@ -209,32 +208,27 @@ public FilterReply decide(Marker marker, Logger logger, Level level, String form
209208
return FilterReply.NEUTRAL;
210209
}
211210

212-
Boolean ignored = LogbackMetrics.ignoreMetrics.get();
213-
if (ignored != null && ignored) {
214-
return FilterReply.NEUTRAL;
215-
}
216-
217-
LogbackMetrics.ignoreMetrics(() -> recordMetrics(level));
211+
recordMetrics(level);
218212

219213
return FilterReply.NEUTRAL;
220214
}
221215

222216
private void recordMetrics(Level level) {
223217
switch (level.toInt()) {
224218
case Level.ERROR_INT:
225-
errorCounter.increment();
219+
errorCount.increment();
226220
break;
227221
case Level.WARN_INT:
228-
warnCounter.increment();
222+
warnCount.increment();
229223
break;
230224
case Level.INFO_INT:
231-
infoCounter.increment();
225+
infoCount.increment();
232226
break;
233227
case Level.DEBUG_INT:
234-
debugCounter.increment();
228+
debugCount.increment();
235229
break;
236230
case Level.TRACE_INT:
237-
traceCounter.increment();
231+
traceCount.increment();
238232
break;
239233
}
240234

micrometer-core/src/test/java/io/micrometer/core/instrument/binder/logging/LogbackMetricsTest.java

Lines changed: 7 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@
1919
import ch.qos.logback.classic.Logger;
2020
import ch.qos.logback.classic.LoggerContext;
2121
import io.micrometer.core.Issue;
22-
import io.micrometer.core.instrument.Counter;
23-
import io.micrometer.core.instrument.Meter;
24-
import io.micrometer.core.instrument.MeterRegistry;
25-
import io.micrometer.core.instrument.MockClock;
22+
import io.micrometer.core.instrument.*;
2623
import io.micrometer.core.instrument.cumulative.CumulativeCounter;
2724
import io.micrometer.core.instrument.simple.SimpleConfig;
2825
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
@@ -32,8 +29,6 @@
3229
import org.junit.jupiter.api.Test;
3330
import org.slf4j.LoggerFactory;
3431

35-
import java.util.concurrent.atomic.AtomicInteger;
36-
3732
import static java.util.Collections.emptyList;
3833
import static org.assertj.core.api.Assertions.assertThat;
3934

@@ -62,7 +57,7 @@ void closeLogbackMetrics() {
6257

6358
@Test
6459
void logbackLevelMetrics() {
65-
assertThat(registry.get("logback.events").counter().count()).isEqualTo(0.0);
60+
assertThat(registry.get("logback.events").functionCounter().count()).isEqualTo(0.0);
6661

6762
// tag::example[]
6863
logger.setLevel(Level.INFO);
@@ -71,8 +66,8 @@ void logbackLevelMetrics() {
7166
logger.error("error");
7267
logger.debug("debug"); // shouldn't record a metric
7368

74-
assertThat(registry.get("logback.events").tags("level", "warn").counter().count()).isEqualTo(1.0);
75-
assertThat(registry.get("logback.events").tags("level", "debug").counter().count()).isEqualTo(0.0);
69+
assertThat(registry.get("logback.events").tags("level", "warn").functionCounter().count()).isEqualTo(1.0);
70+
assertThat(registry.get("logback.events").tags("level", "debug").functionCounter().count()).isEqualTo(0.0);
7671
// end::example[]
7772
}
7873

@@ -81,7 +76,7 @@ void logbackLevelMetrics() {
8176
void isLevelEnabledDoesntContributeToCounts() {
8277
logger.isErrorEnabled();
8378

84-
assertThat(registry.get("logback.events").tags("level", "error").counter().count()).isEqualTo(0.0);
79+
assertThat(registry.get("logback.events").tags("level", "error").functionCounter().count()).isEqualTo(0.0);
8580
}
8681

8782
@Issue("#411, #3623")
@@ -92,7 +87,7 @@ void ignoringLogMetricsInsideCounters() {
9287
logbackMetrics.bindTo(registry);
9388
LoggerFactory.getLogger("test").info("This should be counted once");
9489
}
95-
assertThat(registry.get("logback.events").tags("level", "info").counter().count()).isOne();
90+
assertThat(registry.get("logback.events").tags("level", "info").functionCounter().count()).isOne();
9691
}
9792

9893
@Issue("#421")
@@ -122,64 +117,13 @@ void reAddFilterToLoggerContextAfterReset() {
122117
assertThat(loggerContext.getTurboFilterList()).hasSize(1);
123118
}
124119

125-
@Issue("#2270")
126-
@Test
127-
void resetIgnoreMetricsWhenRunnableThrows() {
128-
Counter infoLogCounter = registry.get("logback.events").tag("level", "info").counter();
129-
logger.info("hi");
130-
assertThat(infoLogCounter.count()).isEqualTo(1);
131-
try {
132-
LogbackMetrics.ignoreMetrics(() -> {
133-
throw new RuntimeException();
134-
});
135-
}
136-
catch (RuntimeException ignore) {
137-
}
138-
logger.info("hi");
139-
assertThat(infoLogCounter.count()).isEqualTo(2);
140-
}
141-
142-
@Issue("#3891")
143-
@Test
144-
void threadLocalNotUsedWhenLogLevelNotEnabled() {
145-
ThreadLocal<Boolean> priorThreadLocal = LogbackMetrics.ignoreMetrics;
146-
AtomicInteger interactions = new AtomicInteger();
147-
try {
148-
LogbackMetrics.ignoreMetrics = new ThreadLocal<Boolean>() {
149-
@Override
150-
public Boolean get() {
151-
interactions.incrementAndGet();
152-
return super.get();
153-
}
154-
155-
@Override
156-
public void set(Boolean value) {
157-
interactions.incrementAndGet();
158-
super.set(value);
159-
}
160-
161-
@Override
162-
public void remove() {
163-
interactions.incrementAndGet();
164-
super.remove();
165-
}
166-
};
167-
logger.trace("trace");
168-
logger.isErrorEnabled();
169-
assertThat(interactions.get()).isZero();
170-
}
171-
finally {
172-
LogbackMetrics.ignoreMetrics = priorThreadLocal;
173-
}
174-
}
175-
176120
@Test
177121
@Issue("#4404")
178122
void slf4j2FluentApiIncrementsCounter() {
179123
org.slf4j.Logger logger = LoggerFactory.getLogger("test");
180124
logger.atWarn().setMessage("test warn log with fluent builder").log();
181125

182-
assertThat(registry.get("logback.events").tags("level", "warn").counter().count()).isEqualTo(1.0);
126+
assertThat(registry.get("logback.events").tags("level", "warn").functionCounter().count()).isEqualTo(1.0);
183127
}
184128

185129
@NullMarked

samples/micrometer-samples-jersey3/src/test/java/io/micrometer/samples/jersey3/Jersey3Test.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ void loggingMetricsAreCounted() {
7070
logbackMetrics.bindTo(registry);
7171
InternalLogger logger = InternalLoggerFactory.getInstance(Jersey3Test.class);
7272
logger.info("test");
73-
assertThat(registry.get("logback.events").tags("level", "info").counter().count()).isPositive();
73+
assertThat(registry.get("logback.events").tags("level", "info").functionCounter().count()).isPositive();
7474
}
7575
}
7676

0 commit comments

Comments
 (0)