diff --git a/CHANGES.md b/CHANGES.md
index c18c8d75..c7c1753e 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -8,6 +8,7 @@ Apollo Java 2.6.0
* [Fix Apollo client local cache fallback for Spring Boot 3 executable JARs](https://github.com/apolloconfig/apollo-java/pull/136)
* [Support `extendInfo`/`parentAppId` in OpenAPI `getNamespace(s)` client calls](https://github.com/apolloconfig/apollo-java/pull/144)
+* [Export Apollo client metrics through OpenTelemetry](https://github.com/apolloconfig/apollo-java/pull/149)
------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/6?closed=1)
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml b/apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml
new file mode 100644
index 00000000..5a67228d
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml
@@ -0,0 +1,63 @@
+
+
+
+ 4.0.0
+
+ apollo-plugin
+ com.ctrip.framework.apollo
+ ${revision}
+ ../pom.xml
+
+
+ apollo-plugin-client-opentelemetry
+ Apollo Plugin OpenTelemetry
+ jar
+
+
+
+ com.ctrip.framework.apollo
+ apollo-client
+ provided
+
+
+ io.opentelemetry
+ opentelemetry-api
+ 1.62.0
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+ 1.62.0
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-inline
+ test
+
+
+
+
\ No newline at end of file
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporter.java b/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporter.java
new file mode 100644
index 00000000..8c7d97f0
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporter.java
@@ -0,0 +1,125 @@
+/*
+ * 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 io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.metrics.*;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.slf4j.Logger;
+
+/**
+ * OpenTelemetry implementation of Apollo client metrics exporter.
+ * Only uses OpenTelemetry API layer (no SDK dependency).
+ *
+ * @author leon.he@walmart.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 counterMap;
+ private Map gaugeMap;
+
+ @Override
+ public void doInit() {
+ // Get meter from global OpenTelemetry
+ meter = GlobalOpenTelemetry.get().getMeter(METER_NAME);
+ // Initialize maps
+ counterMap = new ConcurrentHashMap<>();
+ gaugeMap = 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 tags,
+ double incrValue) {
+ if (meter == null) {
+ logger.warn("OpenTelemetry meter not initialized, skipping counter registration for '{}'",
+ name);
+ return;
+ }
+ DoubleCounter counter = counterMap.computeIfAbsent(name, key -> meter.counterBuilder(name)
+ .setDescription("Apollo counter metrics").setUnit(COUNTER_UNIT).ofDoubles().build());
+ Attributes attributes = getOrCreateAttributes(tags);
+ counter.add(incrValue, attributes);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Updated OpenTelemetry counter '{}' with value: {}, tags: {}", name, incrValue,
+ tags);
+ }
+ }
+
+ @Override
+ public void registerOrUpdateGaugeSample(String name, Map tags, double value) {
+ if (meter == null) {
+ logger.warn("OpenTelemetry meter not initialized, skipping gauge registration for '{}'",
+ name);
+ return;
+ }
+ Attributes attributes = getOrCreateAttributes(tags);
+ // Register gauge if not already registered
+ DoubleGauge gauge = gaugeMap.computeIfAbsent(name, key -> meter.gaugeBuilder(name)
+ .setDescription("Apollo gauge metrics").setUnit(GAUGE_UNIT).build());
+ gauge.set(value, attributes);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Updated OpenTelemetry gauge '{}' with value: {}, tags: {}", name, value, tags);
+ }
+ }
+
+ private Attributes getOrCreateAttributes(Map tags) {
+ if (tags == null || tags.isEmpty()) {
+ return Attributes.empty();
+ }
+ AttributesBuilder builder = Attributes.builder();
+ tags.forEach(builder::put);
+ return builder.build();
+ }
+
+
+ @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;
+
+ String meterStatus = (meter != null) ? METER_NAME : "not initialized";
+ return String.format(
+ "OpenTelemetry metrics exporter status - Counters: %d, Gauges: %d, Meter: %s", counterCount,
+ gaugeCount, meterStatus);
+ }
+}
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/resources/META-INF/services/com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter b/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/resources/META-INF/services/com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter
new file mode 100644
index 00000000..8e064550
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/src/main/resources/META-INF/services/com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter
@@ -0,0 +1 @@
+com.ctrip.framework.apollo.monitor.internal.exporter.impl.OpenTelemetryApolloClientMetricsExporter
\ No newline at end of file
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterIntegrationTest.java b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterIntegrationTest.java
new file mode 100644
index 00000000..2a3ebc08
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterIntegrationTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ *
+ */
+package com.ctrip.framework.apollo.monitor.internal.exporter.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Integration tests using a real OpenTelemetry SDK to verify exported metric values.
+ */
+public class OpenTelemetryApolloClientMetricsExporterIntegrationTest {
+
+ private SdkMeterProvider meterProvider;
+ private InMemoryMetricReader reader;
+ private OpenTelemetryApolloClientMetricsExporter exporter;
+
+ @Before
+ public void setUp() {
+ reader = InMemoryMetricReader.create();
+ meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build();
+ OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build();
+ GlobalOpenTelemetry.set(sdk);
+
+ exporter = new OpenTelemetryApolloClientMetricsExporter();
+ exporter.doInit();
+ }
+
+ @After
+ public void tearDown() {
+ GlobalOpenTelemetry.resetForTest();
+ meterProvider.close();
+ }
+
+ private double latestGaugeValue(String metricName, String tagValue) {
+ for (MetricData md : reader.collectAllMetrics()) {
+ if (!metricName.equals(md.getName())) {
+ continue;
+ }
+ return md.getDoubleGaugeData().getPoints().stream()
+ .filter(p -> tagValue.equals(p.getAttributes()
+ .get(io.opentelemetry.api.common.AttributeKey.stringKey("thread_pool_name"))))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("No gauge point found for metric '" + metricName
+ + "' with tag value '" + tagValue + "', all metrics: " + reader.collectAllMetrics()))
+ .getValue();
+ }
+ throw new AssertionError("Metric not found: " + metricName);
+ }
+
+ private double latestCounterValue(String metricName) {
+ for (MetricData md : reader.collectAllMetrics()) {
+ if (metricName.equals(md.getName())) {
+ return md.getDoubleSumData().getPoints().stream().findFirst()
+ .orElseThrow(() -> new AssertionError("No counter point for " + metricName)).getValue();
+ }
+ }
+ throw new AssertionError("Metric not found: " + metricName);
+ }
+
+ @Test
+ public void gaugeShouldReflectLatestValueAfterUpdates() {
+ Map tags = new HashMap<>();
+ tags.put("thread_pool_name", "ConfigService");
+
+ exporter.registerOrUpdateGaugeSample("apollo_client_thread_pool_queue_size", tags, 5.0);
+ // force collection so subsequent update is a new delta
+ reader.collectAllMetrics();
+ exporter.registerOrUpdateGaugeSample("apollo_client_thread_pool_queue_size", tags, 10.0);
+ reader.collectAllMetrics();
+ exporter.registerOrUpdateGaugeSample("apollo_client_thread_pool_queue_size", tags, 3.0);
+
+ assertEquals(3.0, latestGaugeValue("apollo_client_thread_pool_queue_size", "ConfigService"),
+ 0.0001);
+ }
+
+ @Test
+ public void gaugeWithDifferentTagsShouldRecordSeparateSeries() {
+ Map tags1 = new HashMap<>();
+ tags1.put("thread_pool_name", "ConfigService");
+ Map tags2 = new HashMap<>();
+ tags2.put("thread_pool_name", "RemoteConfigLongPollService");
+
+ exporter.registerOrUpdateGaugeSample("apollo_client_thread_pool_active_task_count", tags1, 1.0);
+ exporter.registerOrUpdateGaugeSample("apollo_client_thread_pool_active_task_count", tags2, 7.0);
+
+ assertEquals(1.0,
+ latestGaugeValue("apollo_client_thread_pool_active_task_count", "ConfigService"), 0.0001);
+ assertEquals(7.0, latestGaugeValue("apollo_client_thread_pool_active_task_count",
+ "RemoteConfigLongPollService"), 0.0001);
+ }
+
+ @Test
+ public void counterShouldAccumulateAcrossUpdates() {
+ Map tags = new HashMap<>();
+ tags.put("namespace", "application");
+
+ reader.collectAllMetrics();
+ exporter.registerOrUpdateCounterSample("apollo_client_exception_num", tags, 2.0);
+ reader.collectAllMetrics();
+ exporter.registerOrUpdateCounterSample("apollo_client_exception_num", tags, 3.0);
+
+ assertEquals(5.0, latestCounterValue("apollo_client_exception_num"), 0.0001);
+ }
+}
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterTest.java b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterTest.java
new file mode 100644
index 00000000..cbe28c86
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ *
+ */
+package com.ctrip.framework.apollo.monitor.internal.exporter.impl;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.metrics.Meter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class OpenTelemetryApolloClientMetricsExporterTest {
+
+ private OpenTelemetryApolloClientMetricsExporter exporter;
+ private MockedStatic globalOpenTelemetryMock;
+ private OpenTelemetry openTelemetry;
+ private Meter meter;
+
+ @Before
+ public void setUp() {
+ // Mock GlobalOpenTelemetry
+ globalOpenTelemetryMock = mockStatic(GlobalOpenTelemetry.class);
+ openTelemetry = mock(OpenTelemetry.class);
+ meter = mock(Meter.class);
+
+ when(GlobalOpenTelemetry.get()).thenReturn(openTelemetry);
+ when(openTelemetry.getMeter("apollo-client")).thenReturn(meter);
+
+ exporter = new OpenTelemetryApolloClientMetricsExporter();
+ exporter.doInit();
+ }
+
+ @After
+ public void tearDown() {
+ if (globalOpenTelemetryMock != null) {
+ globalOpenTelemetryMock.close();
+ }
+ }
+
+ @Test
+ public void testIsSupport() {
+ assertTrue(exporter.isSupport("opentelemetry"));
+ assertFalse(exporter.isSupport("prometheus"));
+ assertFalse(exporter.isSupport("other"));
+ }
+
+ @Test
+ public void testDoInit() {
+ // Verify that meter was obtained from GlobalOpenTelemetry
+ globalOpenTelemetryMock.verify(() -> GlobalOpenTelemetry.get());
+ verify(openTelemetry).getMeter("apollo-client");
+ }
+
+ @Test
+ public void testRegisterOrUpdateCounterSample() {
+ String name = "test_counter";
+ Map tags = new HashMap<>();
+ tags.put("namespace", "application");
+ tags.put("cluster", "default");
+
+ // Mock counter builder
+ io.opentelemetry.api.metrics.LongCounterBuilder counterBuilder =
+ mock(io.opentelemetry.api.metrics.LongCounterBuilder.class);
+ io.opentelemetry.api.metrics.DoubleCounterBuilder doubleCounterBuilder =
+ mock(io.opentelemetry.api.metrics.DoubleCounterBuilder.class);
+ io.opentelemetry.api.metrics.DoubleCounter counter =
+ mock(io.opentelemetry.api.metrics.DoubleCounter.class);
+
+ when(meter.counterBuilder(name)).thenReturn(counterBuilder);
+ when(counterBuilder.setDescription("Apollo counter metrics")).thenReturn(counterBuilder);
+ when(counterBuilder.setUnit("1")).thenReturn(counterBuilder);
+ when(counterBuilder.ofDoubles()).thenReturn(doubleCounterBuilder);
+ when(doubleCounterBuilder.build()).thenReturn(counter);
+
+ // This will create the counter on first call
+ exporter.registerOrUpdateCounterSample(name, tags, 1.0);
+
+ // Verify counter was created and value recorded
+ verify(meter).counterBuilder(name);
+ verify(counter).add(eq(1.0), any());
+ }
+
+ @Test
+ public void testRegisterOrUpdateGaugeSample() {
+ String name = "test_gauge";
+ Map tags = new HashMap<>();
+ tags.put("namespace", "application");
+ tags.put("cluster", "default");
+
+ // Mock gauge builder (synchronous double gauge)
+ io.opentelemetry.api.metrics.DoubleGaugeBuilder gaugeBuilder =
+ mock(io.opentelemetry.api.metrics.DoubleGaugeBuilder.class);
+ io.opentelemetry.api.metrics.DoubleGauge gauge =
+ mock(io.opentelemetry.api.metrics.DoubleGauge.class);
+
+ when(meter.gaugeBuilder(name)).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.setDescription("Apollo gauge metrics")).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.setUnit("1")).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.build()).thenReturn(gauge);
+
+ // This will create the gauge on first call
+ exporter.registerOrUpdateGaugeSample(name, tags, 3.14);
+
+ // Verify gauge was created and value set
+ verify(meter).gaugeBuilder(name);
+ verify(gauge).set(eq(3.14), any());
+ }
+
+ @Test
+ public void testResponse() {
+ String response = exporter.response();
+ assertNotNull(response);
+ assertTrue(response.contains("OpenTelemetry metrics exporter status"));
+ assertTrue(response.contains("Counters: 0"));
+ assertTrue(response.contains("Gauges: 0"));
+ assertTrue(response.contains("Meter: apollo-client"));
+ }
+
+ @Test
+ public void testResponseWithMetrics() {
+ // Register some metrics first
+ Map tags = new HashMap<>();
+ tags.put("test", "value");
+
+ // Mock counter creation
+ io.opentelemetry.api.metrics.LongCounterBuilder counterBuilder =
+ mock(io.opentelemetry.api.metrics.LongCounterBuilder.class);
+ io.opentelemetry.api.metrics.DoubleCounterBuilder doubleCounterBuilder =
+ mock(io.opentelemetry.api.metrics.DoubleCounterBuilder.class);
+ io.opentelemetry.api.metrics.DoubleCounter counter =
+ mock(io.opentelemetry.api.metrics.DoubleCounter.class);
+ when(meter.counterBuilder("test_counter")).thenReturn(counterBuilder);
+ when(counterBuilder.setDescription(anyString())).thenReturn(counterBuilder);
+ when(counterBuilder.setUnit(anyString())).thenReturn(counterBuilder);
+ when(counterBuilder.ofDoubles()).thenReturn(doubleCounterBuilder);
+ when(doubleCounterBuilder.build()).thenReturn(counter);
+
+ // Mock gauge creation (synchronous double gauge)
+ io.opentelemetry.api.metrics.DoubleGaugeBuilder gaugeBuilder =
+ mock(io.opentelemetry.api.metrics.DoubleGaugeBuilder.class);
+ io.opentelemetry.api.metrics.DoubleGauge gauge =
+ mock(io.opentelemetry.api.metrics.DoubleGauge.class);
+ when(meter.gaugeBuilder("test_gauge")).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.setDescription(anyString())).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.setUnit(anyString())).thenReturn(gaugeBuilder);
+ when(gaugeBuilder.build()).thenReturn(gauge);
+
+ exporter.registerOrUpdateCounterSample("test_counter", tags, 1.0);
+ exporter.registerOrUpdateGaugeSample("test_gauge", tags, 2.0);
+
+ String response = exporter.response();
+ assertTrue(response.contains("Counters: 1"));
+ assertTrue(response.contains("Gauges: 1"));
+ }
+}
diff --git a/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/SimpleOpenTelemetryApolloClientMetricsExporterTest.java b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/SimpleOpenTelemetryApolloClientMetricsExporterTest.java
new file mode 100644
index 00000000..3a715d4c
--- /dev/null
+++ b/apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/SimpleOpenTelemetryApolloClientMetricsExporterTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ *
+ */
+package com.ctrip.framework.apollo.monitor.internal.exporter.impl;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SimpleOpenTelemetryApolloClientMetricsExporterTest {
+
+ private OpenTelemetryApolloClientMetricsExporter exporter;
+
+ @Before
+ public void setUp() {
+ exporter = new OpenTelemetryApolloClientMetricsExporter();
+ // Note: We don't call doInit() because it requires GlobalOpenTelemetry
+ // In a real test environment, you would need to setup OpenTelemetry first
+ }
+
+ @Test
+ public void testIsSupport() {
+ assertTrue(exporter.isSupport("opentelemetry"));
+ assertFalse(exporter.isSupport("prometheus"));
+ assertFalse(exporter.isSupport("other"));
+ assertFalse(exporter.isSupport(null));
+ }
+
+ @Test
+ public void testResponseWithoutInit() {
+ // Even without init, response should return a status message
+ String response = exporter.response();
+ assertNotNull(response);
+ assertTrue(response.contains("OpenTelemetry metrics exporter status"));
+ }
+
+ @Test
+ public void testRegisterOrUpdateCounterSampleWithoutInit() {
+ // This should not throw exception even without init
+ String name = "test_counter";
+ Map tags = new HashMap<>();
+ tags.put("namespace", "application");
+
+ try {
+ exporter.registerOrUpdateCounterSample(name, tags, 1.0);
+ // If we get here, it means no exception was thrown
+ assertTrue(true);
+ } catch (Exception e) {
+ // It's okay if it throws exception when not initialized
+ // This depends on OpenTelemetry API behavior
+ }
+ }
+
+ @Test
+ public void testRegisterOrUpdateGaugeSampleWithoutInit() {
+ // This should not throw exception even without init
+ String name = "test_gauge";
+ Map tags = new HashMap<>();
+ tags.put("namespace", "application");
+
+ try {
+ exporter.registerOrUpdateGaugeSample(name, tags, 3.14);
+ // If we get here, it means no exception was thrown
+ assertTrue(true);
+ } catch (Exception e) {
+ // It's okay if it throws exception when not initialized
+ // This depends on OpenTelemetry API behavior
+ }
+ }
+
+ @Test
+ public void testEmptyTags() {
+ String name = "test_metric";
+ Map emptyTags = new HashMap<>();
+
+ try {
+ exporter.registerOrUpdateCounterSample(name, emptyTags, 1.0);
+ exporter.registerOrUpdateGaugeSample(name, emptyTags, 2.0);
+ assertTrue(true);
+ } catch (Exception e) {
+ // Acceptable if it throws exception
+ }
+ }
+
+ @Test
+ public void testNullTags() {
+ String name = "test_metric";
+
+ try {
+ exporter.registerOrUpdateCounterSample(name, null, 1.0);
+ exporter.registerOrUpdateGaugeSample(name, null, 2.0);
+ assertTrue(true);
+ } catch (Exception e) {
+ // Acceptable if it throws exception for null tags
+ }
+ }
+}
diff --git a/apollo-plugin/pom.xml b/apollo-plugin/pom.xml
index f0d1b00f..b97e09f4 100644
--- a/apollo-plugin/pom.xml
+++ b/apollo-plugin/pom.xml
@@ -32,6 +32,7 @@
apollo-plugin-log4j2
apollo-plugin-client-prometheus
+ apollo-plugin-client-opentelemetry