Skip to content
Open
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
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 on lines +40 to +43

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of io.opentelemetry:opentelemetry-api, and are there any security advisories for version 1.35.0?

💡 Result:

As of July 12, 2026, the latest stable version of io.opentelemetry:opentelemetry-api is 1.63.0 [1][2]. Regarding version 1.35.0, it is affected by a security advisory: GHSA-rcgg-9c38-7xpx: Unbounded Memory Allocation in W3C Baggage Propagation. This vulnerability, identified as CVE-2026-45292, affects versions up to and including 1.61.0 [3]. Because version 1.35.0 is included in this range, it is considered vulnerable to this issue [3][4]. The vulnerability involves the lack of limits on W3C Baggage header size and entry counts, which can lead to excessive memory allocation and CPU consumption [3]. This was addressed in version 1.62.0 [3].

Citations:


Update opentelemetry-api to a fixed release1.35.0 is affected by GHSA-rcgg-9c38-7xpx (unbounded memory allocation in W3C baggage propagation); upgrade to at least 1.62.0 (latest stable is 1.63.0). If this dependency is shared elsewhere, move the version into the parent dependencyManagement section.

🤖 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/pom.xml` around lines 40 -
43, Update the opentelemetry-api dependency version from 1.35.0 to at least
1.62.0, preferably the stable 1.63.0 release; if the dependency version is
shared across modules, define it in the parent dependencyManagement and reuse it
here.

<!-- 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);
}
Comment on lines +106 to +122

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