Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
47fb563
add: apollo opentelemetry metric.
teaho2015 Jul 12, 2026
2614592
Merge branch 'apolloconfig:main' into main
teaho2015 Jul 12, 2026
3ed4f29
add: apollo opentelemetry metric.
teaho2015 Aug 18, 2026
7f5016c
feat: apollo opentelemetry metrics.
teaho2015 Jul 12, 2026
6f90d56
* Remove unused imports (#111)
arrow1991 Oct 24, 2025
d50cd48
* Remove redundant local variables (#112)
arrow1991 Nov 27, 2025
ed3ee0f
Support Spring Boot 4.0 bootstrap context package relocation (#115)
Copilot Dec 7, 2025
2ac6b31
* Refactor: string checks with idiomatic alternatives
arrow1991 Dec 29, 2025
75385a5
retry ut on error
nobodyiam Jan 1, 2026
f4879c5
docs: add AGENTS.md for contribution workflow
nobodyiam Feb 8, 2026
21057a7
test: overhaul automated compatibility coverage (#123)
nobodyiam Feb 18, 2026
9347c2e
fix: deduplicate config listeners by identity (#121)
nobodyiam Feb 19, 2026
8e81919
ci: externalize release workflow helper scripts (#128)
nobodyiam Feb 19, 2026
bfa8b6e
chore: bump version to 2.5.0
nobodyiam Feb 19, 2026
e610057
ci: migrate release publishing to central plugin
nobodyiam Feb 19, 2026
2223b01
chore: bump version to 2.6.0-SNAPSHOT
nobodyiam Feb 19, 2026
c9cd490
ci: simplify release workflow publish command
nobodyiam Feb 19, 2026
5afe313
ci: add mergify merge queue config
nobodyiam Feb 19, 2026
9ad5f55
fix: handle nested jar class path fallback
nobodyiam Apr 10, 2026
d079c41
ci(mergify): upgrade configuration to current format
mergify[bot] Jun 9, 2026
f0ef963
test: fix flaky SpringAnnotationCompatibilityTest
Shawyeok Jun 11, 2026
426cf8e
fix: ConfigService.getConfig(appId, namespace) returns wrong app's co…
Shawyeok Jun 21, 2026
88da143
Merge remote-tracking branch 'origin/main'
teaho2015 Aug 18, 2026
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
57 changes: 57 additions & 0 deletions apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 Apollo Authors
~
~ 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
~
~ http://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.
~
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>apollo-plugin</artifactId>
<groupId>com.ctrip.framework.apollo</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>apollo-plugin-client-opentelemetry</artifactId>
<name>Apollo Plugin OpenTelemetry</name>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.35.0</version>
</dependency>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright 2026 Apollo Authors
*
* 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
*
* http://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 com.ctrip.framework.apollo.monitor.internal.exporter.impl;

import com.ctrip.framework.apollo.core.utils.DeferredLoggerFactory;
import com.ctrip.framework.apollo.monitor.internal.exporter.AbstractApolloClientMetricsExporter;
import com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter;
import com.google.common.collect.Maps;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.DoubleGaugeBuilder;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongCounterBuilder;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableDoubleGauge;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;

/**
* OpenTelemetry implementation of Apollo client metrics exporter.
* Only uses OpenTelemetry API layer (no SDK dependency).
*
* @author teaho2015@gmail.com
*/
public class OpenTelemetryApolloClientMetricsExporter extends
AbstractApolloClientMetricsExporter implements ApolloClientMetricsExporter {

private static final String OPENTELEMETRY = "opentelemetry";
private static final String METER_NAME = "apollo-client";
private static final String COUNTER_UNIT = "1";
private static final String GAUGE_UNIT = "1";

private final Logger logger = DeferredLoggerFactory.getLogger(
OpenTelemetryApolloClientMetricsExporter.class);

private Meter meter;
private Map<String, LongCounter> counterMap;
private Map<String, ObservableDoubleGauge> gaugeMap;
private Map<String, AtomicReference<Double>> gaugeValueMap;
private Map<String, Attributes> attributesCache;

@Override
public void doInit() {
// Get meter from global OpenTelemetry
meter = GlobalOpenTelemetry.get().getMeter(METER_NAME);
// Initialize maps
counterMap = new ConcurrentHashMap<>();
gaugeMap = new ConcurrentHashMap<>();
gaugeValueMap = new ConcurrentHashMap<>();
attributesCache = new ConcurrentHashMap<>();
logger.info("OpenTelemetry metrics exporter initialized with meter: {}", METER_NAME);
}

@Override
public boolean isSupport(String form) {
return OPENTELEMETRY.equals(form);
}

@Override
public void registerOrUpdateCounterSample(String name, Map<String, String> tags,
double incrValue) {
try {
if (meter == null) {
logger.warn("OpenTelemetry meter not initialized, skipping counter registration for '{}'", name);
return;
}

LongCounter counter = counterMap.computeIfAbsent(name, this::createCounter);

Attributes attributes = getOrCreateAttributes(tags);
counter.add((long) incrValue, attributes);

logger.debug("Updated OpenTelemetry counter '{}' with value: {}, tags: {}",
name, incrValue, tags);
} catch (Exception e) {
logger.error("Failed to register or update OpenTelemetry counter '{}'", name, e);
}
}

private LongCounter createCounter(String name) {
LongCounterBuilder builder = meter.counterBuilder(name)
.setDescription("Apollo counter metrics")
.setUnit(COUNTER_UNIT);

// Build the counter
return builder.build();
}

@Override
public void registerOrUpdateGaugeSample(String name, Map<String, String> tags, double value) {
if (meter == null) {
logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'", name);
return;
}

// Store the gauge value
String gaugeKey = getGaugeKey(name, tags);
gaugeValueMap.put(gaugeKey, new AtomicReference<>(value));

// Register gauge if not already registered
gaugeMap.computeIfAbsent(gaugeKey, key -> createGauge(name, tags, gaugeKey));

logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}",
name, value, tags);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

registerOrUpdateGaugeSample lacks error handling and will NPE on null tags.

Two issues:

  1. Inconsistent error handling: registerOrUpdateCounterSample wraps all logic in try-catch, but registerOrUpdateGaugeSample does not. If meter.gaugeBuilder(name) or buildWithCallback throws, the exception propagates unhandled to the caller, potentially crashing the monitoring thread.

  2. NPE on null tags: getGaugeKey(name, tags)createCacheKey(tags) calls tags.entrySet() without a null check. When the meter is initialized and tags is null, this throws NPE. Note that getOrCreateAttributes handles null tags, but it's called inside createGaugeafter getGaugeKey has already thrown.

🐛 Proposed fix: add try-catch and null-guard in createCacheKey
 `@Override`
 public void registerOrUpdateGaugeSample(String name, Map<String, String> tags, double value) {
-    if (meter == null) {
-        logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'", name);
-        return;
-    }
-
-    // Store the gauge value
-    String gaugeKey = getGaugeKey(name, tags);
-    gaugeValueMap.put(gaugeKey, new AtomicReference<>(value));
-
-    // Register gauge if not already registered
-    gaugeMap.computeIfAbsent(gaugeKey, key -> createGauge(name, tags, gaugeKey));
-
-    logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}",
-      name, value, tags);
+    try {
+        if (meter == null) {
+            logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'", name);
+            return;
+        }
+
+        // Store the gauge value
+        String gaugeKey = getGaugeKey(name, tags);
+        gaugeValueMap.computeIfAbsent(gaugeKey, k -> new AtomicReference<>()).set(value);
+
+        // Register gauge if not already registered
+        gaugeMap.computeIfAbsent(gaugeKey, key -> createGauge(name, tags, gaugeKey));
+
+        logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}",
+          name, value, tags);
+    } catch (Exception e) {
+        logger.error("Failed to register or update OpenTelemetry gauge '{}'", name, e);
+    }
 }

And add a null guard in createCacheKey:

 private String createCacheKey(Map<String, String> tags) {
+    if (tags == null || tags.isEmpty()) {
+        return "";
+    }
     // Sort keys to ensure consistent cache key
     return tags.entrySet().stream()
         .sorted(Map.Entry.comparingByKey())
         .map(entry -> entry.getKey() + "=" + entry.getValue())
         .reduce((a, b) -> a + ";" + b)
         .orElse("");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public void registerOrUpdateGaugeSample(String name, Map<String, String> tags, double value) {
if (meter == null) {
logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'", name);
return;
}
// Store the gauge value
String gaugeKey = getGaugeKey(name, tags);
gaugeValueMap.put(gaugeKey, new AtomicReference<>(value));
// Register gauge if not already registered
gaugeMap.computeIfAbsent(gaugeKey, key -> createGauge(name, tags, gaugeKey));
logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}",
name, value, tags);
}
`@Override`
public void registerOrUpdateGaugeSample(String name, Map<String, String> tags, double value) {
try {
if (meter == null) {
logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'", name);
return;
}
// Store the gauge value
String gaugeKey = getGaugeKey(name, tags);
gaugeValueMap.computeIfAbsent(gaugeKey, k -> new AtomicReference<>()).set(value);
// Register gauge if not already registered
gaugeMap.computeIfAbsent(gaugeKey, key -> createGauge(name, tags, gaugeKey));
logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}",
name, value, tags);
} catch (Exception e) {
logger.error("Failed to register or update OpenTelemetry gauge '{}'", name, e);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apollo-plugin/apollo-plugin-client-opentelemetry/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporter.java`
around lines 106 - 122, Update registerOrUpdateGaugeSample to wrap gauge value
storage and registration in the same try-catch pattern used by
registerOrUpdateCounterSample, logging failures without propagating exceptions.
Also update createCacheKey to handle null tags before iterating entrySet,
preserving the existing key generation for non-null tags.


private ObservableDoubleGauge createGauge(String name, Map<String, String> tags, String gaugeKey) {
Attributes attributes = getOrCreateAttributes(tags);

DoubleGaugeBuilder gaugeBuilder = meter.gaugeBuilder(name)
.setDescription("Apollo gauge metrics")
.setUnit(GAUGE_UNIT);

// Register callback for gauge value
return gaugeBuilder.buildWithCallback(measurement -> {
AtomicReference<Double> valueRef = gaugeValueMap.get(gaugeKey);
if (valueRef != null) {
Double value = valueRef.get();
if (value != null) {
measurement.record(value, attributes);
}
}
});
}

private Attributes getOrCreateAttributes(Map<String, String> tags) {
if (tags == null || tags.isEmpty()) {
return Attributes.empty();
}

// Create cache key from sorted tag entries for consistency
String cacheKey = createCacheKey(tags);

return attributesCache.computeIfAbsent(cacheKey, key -> {
AttributesBuilder builder = Attributes.builder();
tags.forEach(builder::put);
return builder.build();
});
}

private String createCacheKey(Map<String, String> tags) {
// Sort keys to ensure consistent cache key
return tags.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.reduce((a, b) -> a + ";" + b)
.orElse("");
}

private String getGaugeKey(String name, Map<String, String> tags) {
return name + ":" + createCacheKey(tags);
}

@Override
public String response() {
// Return simple status information since we're only using API layer
int counterCount = counterMap != null ? counterMap.size() : 0;
int gaugeCount = gaugeMap != null ? gaugeMap.size() : 0;
int attributesCount = attributesCache != null ? attributesCache.size() : 0;

String meterStatus = (meter != null) ? METER_NAME : "not initialized";

return String.format(
"OpenTelemetry metrics exporter status - Counters: %d, Gauges: %d, Cached attributes: %d, Meter: %s",
counterCount, gaugeCount, attributesCount, meterStatus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.ctrip.framework.apollo.monitor.internal.exporter.impl.OpenTelemetryApolloClientMetricsExporter
Loading
Loading