From 1ba88c3d86abd5f36c0fbffca35e4cf8f23020dc Mon Sep 17 00:00:00 2001 From: joecqupt Date: Fri, 13 Nov 2026 07:28:55 +0000 Subject: [PATCH] Cache UTF-8 encoded OTLP metric metadata --- .../otlp/metrics/MetricMarshaler.java | 25 ++++++-- .../otlp/metrics/MetricMarshalerTest.java | 63 +++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshalerTest.java diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshaler.java index 6b91a407c5f..d654208dc09 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshaler.java @@ -5,6 +5,7 @@ package io.opentelemetry.exporter.internal.otlp.metrics; +import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; import io.opentelemetry.exporter.internal.marshal.Marshaler; import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; @@ -15,6 +16,10 @@ import java.io.IOException; final class MetricMarshaler extends MarshalerWithSize { + + private static final WeakConcurrentMap UTF8_CACHE = + new WeakConcurrentMap.WithInlinedExpunction<>(); + private final byte[] nameUtf8; private final byte[] descriptionUtf8; private final byte[] unitUtf8; @@ -23,10 +28,9 @@ final class MetricMarshaler extends MarshalerWithSize { private final ProtoFieldInfo dataField; static Marshaler create(MetricData metric) { - // TODO(anuraaga): Cache these as they should be effectively singleton. - byte[] name = MarshalerUtil.toBytes(metric.getName()); - byte[] description = MarshalerUtil.toBytes(metric.getDescription()); - byte[] unit = MarshalerUtil.toBytes(metric.getUnit()); + byte[] name = toBytes(metric.getName()); + byte[] description = toBytes(metric.getDescription()); + byte[] unit = toBytes(metric.getUnit()); Marshaler dataMarshaler = null; ProtoFieldInfo dataField = null; @@ -68,6 +72,19 @@ static Marshaler create(MetricData metric) { return new MetricMarshaler(name, description, unit, dataMarshaler, dataField); } + private static byte[] toBytes(String value) { + if (value.isEmpty()) { + return MarshalerUtil.toBytes(value); + } + byte[] cached = UTF8_CACHE.get(value); + if (cached == null) { + // Concurrent misses may encode the same string more than once, which is harmless. + cached = MarshalerUtil.toBytes(value); + UTF8_CACHE.put(value, cached); + } + return cached; + } + private MetricMarshaler( byte[] nameUtf8, byte[] descriptionUtf8, diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshalerTest.java new file mode 100644 index 00000000000..307d011dddd --- /dev/null +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/metrics/MetricMarshalerTest.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.opentelemetry.exporter.internal.marshal.ProtoFieldInfo; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.proto.metrics.v1.internal.Metric; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableGaugeData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableMetricData; +import io.opentelemetry.sdk.resources.Resource; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class MetricMarshalerTest { + + @Test + void reusesEncodedMetadata() throws IOException { + String name = "request.duration"; + String description = "Request duration"; + String unit = "ms"; + Serializer serializer = mock(Serializer.class); + + for (int i = 0; i < 2; i++) { + MetricData metric = + ImmutableMetricData.createLongGauge( + Resource.empty(), + InstrumentationScopeInfo.empty(), + name, + description, + unit, + ImmutableGaugeData.create(Collections.emptyList())); + ((MetricMarshaler) MetricMarshaler.create(metric)).writeTo(serializer); + } + + assertReusedBytes(serializer, Metric.NAME, name); + assertReusedBytes(serializer, Metric.DESCRIPTION, description); + assertReusedBytes(serializer, Metric.UNIT, unit); + } + + private static void assertReusedBytes(Serializer serializer, ProtoFieldInfo field, String value) + throws IOException { + ArgumentCaptor bytes = ArgumentCaptor.forClass(byte[].class); + verify(serializer, times(2)).serializeString(eq(field), bytes.capture()); + byte[] first = bytes.getAllValues().get(0); + byte[] second = bytes.getAllValues().get(1); + assertThat(first).isEqualTo(value.getBytes(StandardCharsets.UTF_8)); + assertThat(second).isSameAs(first); + } +}