From 47fb563efeefb2e9ef80db123a8b820633092778 Mon Sep 17 00:00:00 2001 From: teaho2015 Date: Sun, 12 Jul 2026 23:35:09 +0800 Subject: [PATCH] add: apollo opentelemetry metric. --- .../pom.xml | 57 ++++++ ...nTelemetryApolloClientMetricsExporter.java | 184 ++++++++++++++++++ ...ernal.exporter.ApolloClientMetricsExporter | 1 + ...emetryApolloClientMetricsExporterTest.java | 162 +++++++++++++++ ...emetryApolloClientMetricsExporterTest.java | 114 +++++++++++ apollo-plugin/pom.xml | 1 + 6 files changed, 519 insertions(+) create mode 100644 apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml create mode 100644 apollo-plugin/apollo-plugin-client-opentelemetry/src/main/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporter.java create mode 100644 apollo-plugin/apollo-plugin-client-opentelemetry/src/main/resources/META-INF/services/com.ctrip.framework.apollo.monitor.internal.exporter.ApolloClientMetricsExporter create mode 100644 apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/OpenTelemetryApolloClientMetricsExporterTest.java create mode 100644 apollo-plugin/apollo-plugin-client-opentelemetry/src/test/java/com/ctrip/framework/apollo/monitor/internal/exporter/impl/SimpleOpenTelemetryApolloClientMetricsExporterTest.java 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..b6ee8eda --- /dev/null +++ b/apollo-plugin/apollo-plugin-client-opentelemetry/pom.xml @@ -0,0 +1,57 @@ + + + + 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.35.0 + + + + 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..62dcc1cc --- /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,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 counterMap; + private Map gaugeMap; + private Map> gaugeValueMap; + private Map 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 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 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); + } + + private ObservableDoubleGauge createGauge(String name, Map 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 valueRef = gaugeValueMap.get(gaugeKey); + if (valueRef != null) { + Double value = valueRef.get(); + if (value != null) { + measurement.record(value, attributes); + } + } + }); + } + + private Attributes getOrCreateAttributes(Map 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 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 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); + } +} \ No newline at end of file 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/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..2e7e8c64 --- /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,162 @@ +/* + * 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.LongCounter counter = mock(io.opentelemetry.api.metrics.LongCounter.class); + + when(meter.counterBuilder(name)).thenReturn(counterBuilder); + when(counterBuilder.setDescription("Apollo counter metrics")).thenReturn(counterBuilder); + when(counterBuilder.setUnit("1")).thenReturn(counterBuilder); + when(counterBuilder.build()).thenReturn(counter); + + // This will create the counter on first call + exporter.registerOrUpdateCounterSample(name, tags, 1.0); + + // Verify counter was created + verify(meter).counterBuilder(name); + verify(counterBuilder).build(); + } + + @Test + public void testRegisterOrUpdateGaugeSample() { + String name = "test_gauge"; + Map tags = new HashMap<>(); + tags.put("namespace", "application"); + tags.put("cluster", "default"); + + // Mock gauge builder + io.opentelemetry.api.metrics.DoubleGaugeBuilder gaugeBuilder = mock(io.opentelemetry.api.metrics.DoubleGaugeBuilder.class); + io.opentelemetry.api.metrics.ObservableDoubleGauge gauge = mock(io.opentelemetry.api.metrics.ObservableDoubleGauge.class); + + when(meter.gaugeBuilder(name)).thenReturn(gaugeBuilder); + when(gaugeBuilder.setDescription("Apollo gauge metrics")).thenReturn(gaugeBuilder); + when(gaugeBuilder.setUnit("1")).thenReturn(gaugeBuilder); + when(gaugeBuilder.buildWithCallback(any())).thenReturn(gauge); + + // This will create the gauge on first call + exporter.registerOrUpdateGaugeSample(name, tags, 3.14); + + // Verify gauge was created + verify(meter).gaugeBuilder(name); + verify(gaugeBuilder).buildWithCallback(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.LongCounter counter = mock(io.opentelemetry.api.metrics.LongCounter.class); + when(meter.counterBuilder("test_counter")).thenReturn(counterBuilder); + when(counterBuilder.setDescription(anyString())).thenReturn(counterBuilder); + when(counterBuilder.setUnit(anyString())).thenReturn(counterBuilder); + when(counterBuilder.build()).thenReturn(counter); + + // Mock gauge creation + io.opentelemetry.api.metrics.DoubleGaugeBuilder gaugeBuilder = mock(io.opentelemetry.api.metrics.DoubleGaugeBuilder.class); + io.opentelemetry.api.metrics.ObservableDoubleGauge gauge = mock(io.opentelemetry.api.metrics.ObservableDoubleGauge.class); + when(meter.gaugeBuilder("test_gauge")).thenReturn(gaugeBuilder); + when(gaugeBuilder.setDescription(anyString())).thenReturn(gaugeBuilder); + when(gaugeBuilder.setUnit(anyString())).thenReturn(gaugeBuilder); + when(gaugeBuilder.buildWithCallback(any())).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")); + } +} \ 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/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..671df0a8 --- /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 + } + } +} \ No newline at end of file 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