Skip to content

Commit cfe60e1

Browse files
Fix Micrometer registry serialization error due to failed meter (#1741)
* Fix Micrometer registry serialization error due to failed meter * Setting Meter CL as context CL when querying Co-authored-by: Sylvain Juge <[email protected]>
1 parent ead3c23 commit cfe60e1

File tree

6 files changed

+160
-25
lines changed

6 files changed

+160
-25
lines changed

CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Even when matching on the main class name or on system properties,
6161
** Checks the Java version before attaching to avoid attachment on unsupported JVMs.
6262
* Cassandra instrumentation - {pull}1712[#1712]
6363
* Log correlation supports JBoss Logging - {pull}1737[#1737]
64+
* Update Byte-buddy to `1.11.0` - {pull}1769[#1769]
6465
6566
[float]
6667
===== Bug fixes
@@ -70,6 +71,7 @@ Even when matching on the main class name or on system properties,
7071
* Fix rounded number format for non-english locales - {pull}1728[#1728]
7172
* Fix `NullPointerException` on legacy Apache client instrumentation when host is `null` - {pull}1746[#1746]
7273
* Apply consistent proxy class exclusion heuristic - {pull}1738[#1738]
74+
* Fix micrometer serialization error - {pull}1741[#1741]
7375
7476
[float]
7577
===== Refactors

apm-agent-plugins/apm-grpc/apm-grpc-test-latest/src/test/java/co/elastic/apm/agent/grpc/latest/testapp/generated/HelloGrpc.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
/**
3030
*/
3131
@javax.annotation.Generated(
32-
value = "by gRPC proto compiler (version 1.36.1)",
32+
value = "by gRPC proto compiler (version 1.37.0)",
3333
comments = "Source: rpc.proto")
3434
public final class HelloGrpc {
3535

apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerMeterRegistrySerializer.java

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
import co.elastic.apm.agent.configuration.MetricsConfiguration;
2828
import co.elastic.apm.agent.report.serialize.DslJsonSerializer;
29+
import co.elastic.apm.agent.sdk.weakmap.WeakMapSupplier;
30+
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentSet;
2931
import com.dslplatform.json.DslJson;
3032
import com.dslplatform.json.JsonWriter;
3133
import com.dslplatform.json.NumberConverter;
@@ -38,6 +40,8 @@
3840
import io.micrometer.core.instrument.Meter;
3941
import io.micrometer.core.instrument.Tag;
4042
import io.micrometer.core.instrument.Timer;
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
4145

4246
import java.util.ArrayList;
4347
import java.util.HashMap;
@@ -52,16 +56,23 @@
5256
public class MicrometerMeterRegistrySerializer {
5357

5458
private static final byte NEW_LINE = (byte) '\n';
59+
60+
private static final Logger logger = LoggerFactory.getLogger(MicrometerMeterRegistrySerializer.class);
61+
5562
private final DslJson<Object> dslJson = new DslJson<>(new DslJson.Settings<>());
5663
private final StringBuilder replaceBuilder = new StringBuilder();
5764
private final MetricsConfiguration config;
58-
65+
private final WeakConcurrentSet<Meter> internallyDisabledMeters = WeakMapSupplier.createSet();
5966
private int previousSize = 0;
6067

6168
public MicrometerMeterRegistrySerializer(MetricsConfiguration config) {
6269
this.config = config;
6370
}
6471

72+
Iterable<Meter> getFailedMeters() {
73+
return internallyDisabledMeters;
74+
}
75+
6576
public JsonWriter serialize(final Map<Meter.Id, Meter> metersById, final long epochMicros) {
6677
int newSize = (int) (Math.max(previousSize, 512) * 1.25);
6778
JsonWriter jw = dslJson.newWriter(newSize);
@@ -101,31 +112,50 @@ void serializeMetricSet(List<Tag> tags, List<Meter> meters, long epochMicros, St
101112
DslJsonSerializer.writeFieldName("samples", jw);
102113
jw.writeByte(JsonWriter.OBJECT_START);
103114
boolean hasValue = false;
104-
for (int i = 0, size = meters.size(); i < size; i++) {
105-
Meter meter = meters.get(i);
106-
if (meter instanceof Timer) {
107-
Timer timer = (Timer) meter;
108-
hasValue = serializeTimer(jw, timer.getId(), timer.count(), timer.totalTime(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
109-
} else if (meter instanceof FunctionTimer) {
110-
FunctionTimer timer = (FunctionTimer) meter;
111-
hasValue = serializeTimer(jw, timer.getId(), (long) timer.count(), timer.totalTime(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
112-
} else if (meter instanceof LongTaskTimer) {
113-
LongTaskTimer timer = (LongTaskTimer) meter;
114-
hasValue = serializeTimer(jw, timer.getId(), timer.activeTasks(), timer.duration(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
115-
} else if (meter instanceof DistributionSummary) {
116-
DistributionSummary timer = (DistributionSummary) meter;
117-
hasValue = serializeDistributionSummary(jw, timer.getId(), timer.count(), timer.totalAmount(), hasValue, replaceBuilder, dedotMetricName);
118-
} else if (meter instanceof Gauge) {
119-
Gauge gauge = (Gauge) meter;
120-
hasValue = serializeValue(gauge.getId(), gauge.value(), hasValue, jw, replaceBuilder, dedotMetricName);
121115

122-
} else if (meter instanceof Counter) {
123-
Counter counter = (Counter) meter;
124-
hasValue = serializeValue(counter.getId(), counter.count(), hasValue, jw, replaceBuilder, dedotMetricName);
125-
} else if (meter instanceof FunctionCounter) {
126-
FunctionCounter counter = (FunctionCounter) meter;
127-
hasValue = serializeValue(counter.getId(), counter.count(), hasValue, jw, replaceBuilder, dedotMetricName);
116+
ClassLoader originalContextCL = Thread.currentThread().getContextClassLoader();
117+
try {
118+
for (int i = 0, size = meters.size(); i < size; i++) {
119+
Meter meter = meters.get(i);
120+
if (internallyDisabledMeters.contains(meter)) {
121+
continue;
122+
}
123+
try {
124+
// Setting the Meter CL as the context class loader during the Meter query operations
125+
Thread.currentThread().setContextClassLoader(meter.getClass().getClassLoader());
126+
if (meter instanceof Timer) {
127+
Timer timer = (Timer) meter;
128+
hasValue = serializeTimer(jw, timer.getId(), timer.count(), timer.totalTime(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
129+
} else if (meter instanceof FunctionTimer) {
130+
FunctionTimer timer = (FunctionTimer) meter;
131+
hasValue = serializeTimer(jw, timer.getId(), (long) timer.count(), timer.totalTime(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
132+
} else if (meter instanceof LongTaskTimer) {
133+
LongTaskTimer timer = (LongTaskTimer) meter;
134+
hasValue = serializeTimer(jw, timer.getId(), timer.activeTasks(), timer.duration(TimeUnit.MICROSECONDS), hasValue, replaceBuilder, dedotMetricName);
135+
} else if (meter instanceof DistributionSummary) {
136+
DistributionSummary timer = (DistributionSummary) meter;
137+
hasValue = serializeDistributionSummary(jw, timer.getId(), timer.count(), timer.totalAmount(), hasValue, replaceBuilder, dedotMetricName);
138+
} else if (meter instanceof Gauge) {
139+
Gauge gauge = (Gauge) meter;
140+
hasValue = serializeValue(gauge.getId(), gauge.value(), hasValue, jw, replaceBuilder, dedotMetricName);
141+
} else if (meter instanceof Counter) {
142+
Counter counter = (Counter) meter;
143+
hasValue = serializeValue(counter.getId(), counter.count(), hasValue, jw, replaceBuilder, dedotMetricName);
144+
} else if (meter instanceof FunctionCounter) {
145+
FunctionCounter counter = (FunctionCounter) meter;
146+
hasValue = serializeValue(counter.getId(), counter.count(), hasValue, jw, replaceBuilder, dedotMetricName);
147+
}
148+
} catch (Throwable throwable) {
149+
String meterName = meter.getId().getName();
150+
logger.warn("Failed to serialize Micrometer meter \"{}\" with tags {}. This meter will be " +
151+
"excluded from serialization going forward.", meterName, tags);
152+
logger.debug("Detailed info about failure to register Micrometer meter \"" + meterName +
153+
"\": ", throwable);
154+
internallyDisabledMeters.add(meter);
155+
}
128156
}
157+
} finally {
158+
Thread.currentThread().setContextClassLoader(originalContextCL);
129159
}
130160
jw.writeByte(JsonWriter.OBJECT_END);
131161
}

apm-agent-plugins/apm-micrometer-plugin/src/main/java/co/elastic/apm/agent/micrometer/MicrometerMetricsReporter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,8 @@ public void accept(Meter meter) {
131131
WeakConcurrentSet<MeterRegistry> getMeterRegistries() {
132132
return meterRegistries;
133133
}
134+
135+
Iterable<Meter> getFailedMeters() {
136+
return serializer.getFailedMeters();
137+
}
134138
}

apm-agent-plugins/apm-micrometer-plugin/src/test/java/co/elastic/apm/agent/micrometer/MicrometerMeterRegistrySerializerTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 - 2021 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
125
package co.elastic.apm.agent.micrometer;
226

327
import co.elastic.apm.agent.configuration.MetricsConfiguration;

apm-agent-plugins/apm-micrometer-plugin/src/test/java/co/elastic/apm/agent/micrometer/MicrometerMetricsReporterTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,81 @@ void tryToSerializeInvalidGaugeValues() {
324324
}
325325
}
326326

327+
@Test
328+
void testWorkingWithProperContextCL() {
329+
List<Tag> tags = List.of(Tag.of("foo", "bar"));
330+
meterRegistry.gauge("gauge1", tags, 42, v -> {
331+
if (Thread.currentThread().getContextClassLoader() == null) {
332+
throw new RuntimeException("Context CL cannot be null when querying this gauge");
333+
}
334+
return 42D;
335+
});
336+
JsonNode metricSet;
337+
ClassLoader originalContextCL = Thread.currentThread().getContextClassLoader();
338+
try {
339+
Thread.currentThread().setContextClassLoader(null);
340+
metricSet = getSingleMetricSet();
341+
} finally {
342+
Thread.currentThread().setContextClassLoader(originalContextCL);
343+
}
344+
assertThat(metricSet.get("metricset").get("samples").get("gauge1").get("value").doubleValue()).isEqualTo(42D);
345+
assertThat(metricsReporter.getFailedMeters()).isEmpty();
346+
}
347+
348+
@Test
349+
void tryExclusionOfFailedGauge_singleGauge() {
350+
List<Tag> tags = List.of(Tag.of("foo", "bar"));
351+
meterRegistry.gauge("gauge1", tags, 42, v -> {
352+
throw new RuntimeException("Failed to read gauge value");
353+
});
354+
assertThat(metricsReporter.getFailedMeters()).isEmpty();
355+
JsonNode metricSet = getSingleMetricSet();
356+
assertThat(metricSet.get("metricset").get("samples"))
357+
.describedAs("value of %s is not expected to be written to json", "gauge1")
358+
.isEmpty();
359+
360+
getSingleMetricSet();
361+
assertThat(metricsReporter.getFailedMeters().iterator().next().getId().getName()).isEqualTo("gauge1");
362+
}
363+
364+
@Test
365+
void tryExclusionOfFailedGauge_firstFails() {
366+
List<Tag> tags = List.of(Tag.of("foo", "bar"));
367+
meterRegistry.gauge("gauge1", tags, 42, v -> {
368+
throw new RuntimeException("Failed to read gauge value");
369+
});
370+
meterRegistry.gauge("gauge2", tags, 42, v -> 42D);
371+
assertThat(metricsReporter.getFailedMeters()).isEmpty();
372+
JsonNode metricSet = getSingleMetricSet();
373+
assertThat(metricSet.get("metricset").get("samples").get("gauge1"))
374+
.describedAs("value of %s is not expected to be written to json", "gauge1")
375+
.isNull();
376+
377+
// serialization should handle ignoring the 1st value
378+
assertThat(metricSet.get("metricset").get("samples").get("gauge2").get("value").doubleValue()).isEqualTo(42D);
379+
assertThat(metricsReporter.getFailedMeters().iterator().next().getId().getName()).isEqualTo("gauge1");
380+
}
381+
382+
@Test
383+
void tryExclusionOfFailedGauge_secondFails() {
384+
List<Tag> tags = List.of(Tag.of("foo", "bar"));
385+
meterRegistry.gauge("gauge1", tags, 42, v -> 42D);
386+
meterRegistry.gauge("gauge2", tags, 42, v -> {
387+
throw new RuntimeException("Failed to read gauge value");
388+
});
389+
assertThat(metricsReporter.getFailedMeters()).isEmpty();
390+
JsonNode metricSet = getSingleMetricSet();
391+
392+
// serialization should handle ignoring the 1st value
393+
assertThat(metricSet.get("metricset").get("samples").get("gauge1").get("value").doubleValue()).isEqualTo(42D);
394+
395+
assertThat(metricSet.get("metricset").get("samples").get("gauge2"))
396+
.describedAs("value of %s is not expected to be written to json", "gauge1")
397+
.isNull();
398+
399+
assertThat(metricsReporter.getFailedMeters().iterator().next().getId().getName()).isEqualTo("gauge2");
400+
}
401+
327402
@Test
328403
void tryToSerializeInvalidCounterValues() {
329404
for (Double invalidValue : Arrays.asList(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN)) {

0 commit comments

Comments
 (0)