From d538bb519fde8c761b6b55fefa587b79199133c1 Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Wed, 23 Jul 2025 11:47:54 +0700 Subject: [PATCH 1/7] feat: add span duration metrics and processor for observability - Introduced SpanToMetricProcessor to record span durations. - Enhanced Metrics class with span and document load duration histograms. - Updated ConfigurationDefaults to include span processor customization. - Modified ObjectMapExporter to log span durations using the new metrics. --- .../extension/conf/ConfigurationDefaults.java | 11 ++++-- .../client/ObjectMapExporter.java | 11 ++++++ .../com/vaadin/extension/metrics/Metrics.java | 34 +++++++++++++++++-- .../metrics/SpanToMetricProcessor.java | 32 +++++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java index 2912eaf3..d6585b44 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java @@ -12,12 +12,13 @@ import static java.util.Collections.emptyMap; import com.vaadin.extension.Constants; - +import com.vaadin.extension.metrics.SpanToMetricProcessor; import com.google.auto.service.AutoService; import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.File; @@ -70,7 +71,8 @@ public int order() { public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration .addSpanExporterCustomizer(this::setSpanExporter) - .addPropertiesSupplier(this::getDefaultProperties); + .addPropertiesSupplier(this::getDefaultProperties) + .addSpanProcessorCustomizer(this::spanToMetricProcessor); } private SpanExporter setSpanExporter(SpanExporter spanExporter, @@ -80,6 +82,11 @@ private SpanExporter setSpanExporter(SpanExporter spanExporter, return spanExporter; } + private SpanProcessor spanToMetricProcessor(SpanProcessor spanProcessor, + ConfigProperties configProperties) { + return SpanProcessor.composite(spanProcessor, new SpanToMetricProcessor()); + } + private Map getDefaultProperties() { Map properties = new HashMap<>(); final Map defaultconfig = getPropertyFileProperties(); diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java index f19c7dc1..2f1649e7 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java @@ -18,6 +18,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; import com.vaadin.extension.conf.ConfigurationDefaults; +import com.vaadin.extension.metrics.Metrics; /** * This is a consumer callback that is injected into an ObservabilityHandler @@ -61,6 +62,16 @@ public void accept(String id, Map objectMap) { } } + exportSpans.forEach(span -> { + // if (span.getName().contains("documentLoad")) { + Long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); + Long durationMs = durationNanos / 1000000; + System.out.println("Span name: " + span.getName()); + Metrics.recordSpanDuration(span.getName(), durationMs, span.getTraceId()); + // } + }); + ConfigurationDefaults.spanExporter.export(exportSpans); + } } diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java index 9ab74598..e947d40b 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java @@ -13,6 +13,8 @@ import com.vaadin.flow.server.VaadinSession; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.Meter; @@ -30,11 +32,12 @@ public class Metrics { private static final Map sessionStarts = new ConcurrentHashMap<>(); private static LongHistogram sessionDurationMeasurement; - + private static LongHistogram spanDurationHistogram; + private static LongHistogram documentLoadHistogram; private static InstantProvider instantProvider = Instant::now; static void setInstantProvider(InstantProvider instantProvider) { - Metrics.instantProvider = instantProvider; + Metrics.instantProvider = instantProvider; } public static void ensureMetricsRegistered() { @@ -62,6 +65,20 @@ public static void ensureMetricsRegistered() { .histogramBuilder("vaadin.session.duration") .setDescription("Duration of sessions").setUnit("seconds") .ofLongs().build(); + + spanDurationHistogram = meter + .histogramBuilder("vaadin.span.duration") + .setDescription("Duration of spans in milliseconds") + .setUnit("ms") + .ofLongs() + .build(); + + documentLoadHistogram = meter + .histogramBuilder("vaadin.document.load.duration") + .setDescription("Duration of document loads in milliseconds") + .setUnit("ms") + .ofLongs() + .build(); } } @@ -111,4 +128,15 @@ private static String getSessionIdentifier(VaadinSession session) { interface InstantProvider { Instant get(); } -} + + public static void recordSpanDuration(String spanName, long durationMs, String traceId) { + Metrics.ensureMetricsRegistered(); + Attributes attributes = Attributes.of( + AttributeKey.stringKey("span.name"), spanName + ); + if (spanName.contains("documentLoad")) { + documentLoadHistogram.record(durationMs, attributes); + } + spanDurationHistogram.record(durationMs, attributes); + } +} \ No newline at end of file diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java new file mode 100644 index 00000000..80e5ca72 --- /dev/null +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java @@ -0,0 +1,32 @@ +package com.vaadin.extension.metrics; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +public class SpanToMetricProcessor implements SpanProcessor { + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) { + Long latencyMs = span.getLatencyNanos() / 1000000; + Metrics.recordSpanDuration(span.getName(), latencyMs, span.getSpanContext().getTraceId()); + } + + @Override + public void onStart(Context arg0, ReadWriteSpan arg1) { + + } + + +} From 68f8e8dbb9bc37f84de39b4c3cd0a7ed92e014d1 Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Wed, 23 Jul 2025 13:25:32 +0700 Subject: [PATCH 2/7] remove verbose logging --- .../client/ObjectMapExporter.java | 3 +-- .../com/vaadin/extension/metrics/Metrics.java | 20 +++++++++++++++---- .../metrics/SpanToMetricProcessor.java | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java index 2f1649e7..bef91331 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java @@ -66,8 +66,7 @@ public void accept(String id, Map objectMap) { // if (span.getName().contains("documentLoad")) { Long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); Long durationMs = durationNanos / 1000000; - System.out.println("Span name: " + span.getName()); - Metrics.recordSpanDuration(span.getName(), durationMs, span.getTraceId()); + Metrics.recordSpanDuration(span.getName(), durationMs, span.getSpanContext()); // } }); diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java index e947d40b..7f1ecde2 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java @@ -17,6 +17,8 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; import java.time.Duration; import java.time.Instant; @@ -129,14 +131,24 @@ interface InstantProvider { Instant get(); } - public static void recordSpanDuration(String spanName, long durationMs, String traceId) { - Metrics.ensureMetricsRegistered(); + public static void recordSpanDuration(String spanName, long durationMs, SpanContext spanContext) { Attributes attributes = Attributes.of( AttributeKey.stringKey("span.name"), spanName ); + + // Create a Context with span information for exemplars + Context contextWithSpan = Context.current(); + if (spanContext != null && spanContext.isValid()) { + contextWithSpan = Context.current().with( + io.opentelemetry.api.trace.Span.wrap(spanContext) + ); + } + if (spanName.contains("documentLoad")) { - documentLoadHistogram.record(durationMs, attributes); + documentLoadHistogram.record(durationMs, attributes, contextWithSpan); } - spanDurationHistogram.record(durationMs, attributes); + spanDurationHistogram.record(durationMs, attributes, contextWithSpan); } + + } \ No newline at end of file diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java index 80e5ca72..5b619591 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java @@ -20,7 +20,7 @@ public boolean isStartRequired() { @Override public void onEnd(ReadableSpan span) { Long latencyMs = span.getLatencyNanos() / 1000000; - Metrics.recordSpanDuration(span.getName(), latencyMs, span.getSpanContext().getTraceId()); + Metrics.recordSpanDuration(span.getName(), latencyMs, span.getSpanContext()); } @Override From e42a91d3578b0f005c0d464b72adeff6595bfa82 Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Fri, 25 Jul 2025 17:45:59 +0700 Subject: [PATCH 3/7] simplify metrics --- .../java/com/vaadin/extension/metrics/Metrics.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java index 7f1ecde2..7640f14f 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java @@ -35,7 +35,6 @@ public class Metrics { private static LongHistogram sessionDurationMeasurement; private static LongHistogram spanDurationHistogram; - private static LongHistogram documentLoadHistogram; private static InstantProvider instantProvider = Instant::now; static void setInstantProvider(InstantProvider instantProvider) { @@ -74,13 +73,6 @@ public static void ensureMetricsRegistered() { .setUnit("ms") .ofLongs() .build(); - - documentLoadHistogram = meter - .histogramBuilder("vaadin.document.load.duration") - .setDescription("Duration of document loads in milliseconds") - .setUnit("ms") - .ofLongs() - .build(); } } @@ -132,6 +124,7 @@ interface InstantProvider { } public static void recordSpanDuration(String spanName, long durationMs, SpanContext spanContext) { + Metrics.ensureMetricsRegistered(); Attributes attributes = Attributes.of( AttributeKey.stringKey("span.name"), spanName ); @@ -143,10 +136,6 @@ public static void recordSpanDuration(String spanName, long durationMs, SpanCont io.opentelemetry.api.trace.Span.wrap(spanContext) ); } - - if (spanName.contains("documentLoad")) { - documentLoadHistogram.record(durationMs, attributes, contextWithSpan); - } spanDurationHistogram.record(durationMs, attributes, contextWithSpan); } From e6c915ca8654968781123d9e4382d6635e0726bf Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Tue, 29 Jul 2025 13:10:49 +0700 Subject: [PATCH 4/7] use document load metric --- .../client/ObjectMapExporter.java | 2 -- .../com/vaadin/extension/metrics/Metrics.java | 20 +++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java index bef91331..43c1c7d1 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java @@ -63,11 +63,9 @@ public void accept(String id, Map objectMap) { } exportSpans.forEach(span -> { - // if (span.getName().contains("documentLoad")) { Long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); Long durationMs = durationNanos / 1000000; Metrics.recordSpanDuration(span.getName(), durationMs, span.getSpanContext()); - // } }); ConfigurationDefaults.spanExporter.export(exportSpans); diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java index 7640f14f..911c8a4b 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java @@ -35,6 +35,7 @@ public class Metrics { private static LongHistogram sessionDurationMeasurement; private static LongHistogram spanDurationHistogram; + private static LongHistogram documentLoadHistogram; private static InstantProvider instantProvider = Instant::now; static void setInstantProvider(InstantProvider instantProvider) { @@ -73,6 +74,13 @@ public static void ensureMetricsRegistered() { .setUnit("ms") .ofLongs() .build(); + + documentLoadHistogram = meter + .histogramBuilder("vaadin.document.load") + .setDescription("Duration of document load in milliseconds") + .setUnit("ms") + .ofLongs() + .build(); } } @@ -128,15 +136,11 @@ public static void recordSpanDuration(String spanName, long durationMs, SpanCont Attributes attributes = Attributes.of( AttributeKey.stringKey("span.name"), spanName ); - - // Create a Context with span information for exemplars - Context contextWithSpan = Context.current(); - if (spanContext != null && spanContext.isValid()) { - contextWithSpan = Context.current().with( - io.opentelemetry.api.trace.Span.wrap(spanContext) - ); + spanDurationHistogram.record(durationMs, attributes); + + if (spanName.contains("documentLoad") || spanName.contains("navigate") || spanName.contains("Navigate")) { + documentLoadHistogram.record(durationMs, attributes); } - spanDurationHistogram.record(durationMs, attributes, contextWithSpan); } From b3cb6a9425c77aceffa6286faa238826ad5e438e Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Tue, 5 Aug 2025 14:53:14 +0700 Subject: [PATCH 5/7] tests --- .../client/ObjectMapExporter.java | 5 +- .../com/vaadin/extension/metrics/Metrics.java | 16 +----- .../metrics/SpanToMetricProcessor.java | 4 +- .../vaadin/extension/metrics/MetricsTest.java | 57 +++++++++++++++++++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java index 43c1c7d1..9618b6a6 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java @@ -63,9 +63,8 @@ public void accept(String id, Map objectMap) { } exportSpans.forEach(span -> { - Long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); - Long durationMs = durationNanos / 1000000; - Metrics.recordSpanDuration(span.getName(), durationMs, span.getSpanContext()); + long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); + Metrics.recordSpanDuration(span.getName(), durationNanos, span.getSpanContext()); }); ConfigurationDefaults.spanExporter.export(exportSpans); diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java index 911c8a4b..cbd9b1f4 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/Metrics.java @@ -18,7 +18,6 @@ import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.context.Context; import java.time.Duration; import java.time.Instant; @@ -35,7 +34,6 @@ public class Metrics { private static LongHistogram sessionDurationMeasurement; private static LongHistogram spanDurationHistogram; - private static LongHistogram documentLoadHistogram; private static InstantProvider instantProvider = Instant::now; static void setInstantProvider(InstantProvider instantProvider) { @@ -74,13 +72,6 @@ public static void ensureMetricsRegistered() { .setUnit("ms") .ofLongs() .build(); - - documentLoadHistogram = meter - .histogramBuilder("vaadin.document.load") - .setDescription("Duration of document load in milliseconds") - .setUnit("ms") - .ofLongs() - .build(); } } @@ -131,16 +122,13 @@ interface InstantProvider { Instant get(); } - public static void recordSpanDuration(String spanName, long durationMs, SpanContext spanContext) { + public static void recordSpanDuration(String spanName, long durationNanos, SpanContext spanContext) { + long durationMs = durationNanos / 1000000; Metrics.ensureMetricsRegistered(); Attributes attributes = Attributes.of( AttributeKey.stringKey("span.name"), spanName ); spanDurationHistogram.record(durationMs, attributes); - - if (spanName.contains("documentLoad") || spanName.contains("navigate") || spanName.contains("Navigate")) { - documentLoadHistogram.record(durationMs, attributes); - } } diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java index 5b619591..97717301 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/metrics/SpanToMetricProcessor.java @@ -19,8 +19,8 @@ public boolean isStartRequired() { @Override public void onEnd(ReadableSpan span) { - Long latencyMs = span.getLatencyNanos() / 1000000; - Metrics.recordSpanDuration(span.getName(), latencyMs, span.getSpanContext()); + long latencyNanos = span.getLatencyNanos(); + Metrics.recordSpanDuration(span.getName(), latencyNanos, span.getSpanContext()); } @Override diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java index cbca3971..391c5a4d 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java @@ -1,11 +1,15 @@ package com.vaadin.extension.metrics; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.vaadin.extension.instrumentation.AbstractInstrumentationTest; import com.vaadin.extension.instrumentation.server.VaadinSessionInstrumentation; import com.vaadin.flow.server.VaadinSession; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.sdk.metrics.data.HistogramPointData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -83,10 +87,63 @@ public void sessionDuration() { assertEquals(1500, metricValue.getSum(), 0); } + @Test + public void recordSpanDuration() { + SpanContext spanContext = createMockSpanContext(); + String spanName = "test-span"; + long durationNanos = 150_000_000; // 150ms in nanoseconds + + Metrics.recordSpanDuration(spanName, durationNanos, spanContext); + + HistogramPointData metricValue = getLastHistogramMetricValue("vaadin.span.duration"); + assertEquals(150, metricValue.getSum(), 0); // Should be 150ms + assertEquals(1, metricValue.getCount()); + + // Verify span name attribute is recorded + assertTrue(metricValue.getAttributes().asMap().containsKey(io.opentelemetry.api.common.AttributeKey.stringKey("span.name"))); + assertEquals(spanName, metricValue.getAttributes().get(io.opentelemetry.api.common.AttributeKey.stringKey("span.name"))); + } + + @Test + public void recordSpanDurationVariousSpanNames() { + SpanContext spanContext = createMockSpanContext(); + + // Test recording spans with different names (including ones that used to trigger document load) + String[] spanNames = { + "regular-operation", + "documentLoad", + "navigate-to-page", + "Navigate-Home" + }; + + // Record spans and verify basic functionality + for (String spanName : spanNames) { + Metrics.recordSpanDuration(spanName, 100_000_000, spanContext); // 100ms in nanoseconds + } + + // Verify basic functionality - that spans are recorded in the histogram + HistogramPointData spanMetric = getLastHistogramMetricValue("vaadin.span.duration"); + assertTrue(spanMetric.getCount() >= 1, "Should have recorded at least 1 span"); + assertTrue(spanMetric.getSum() >= 100, "Should have recorded at least 100ms total"); + + // Verify that span name attribute key exists (value will be from the last recorded span) + assertTrue(spanMetric.getAttributes().asMap().containsKey(io.opentelemetry.api.common.AttributeKey.stringKey("span.name")), + "Span name attribute should be present"); + } + private static VaadinSession mockSession(String sessionId) { VaadinSession session = Mockito.mock(VaadinSession.class); Mockito.when(session.getPushId()).thenReturn(sessionId); return session; } + + private static SpanContext createMockSpanContext() { + return SpanContext.create( + "12345678901234567890123456789012", // traceId (32 hex chars) + "1234567890123456", // spanId (16 hex chars) + TraceFlags.getSampled(), + TraceState.getDefault() + ); + } } From 727761c092ce7390f0f731747da40a8b25366602 Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Tue, 5 Aug 2025 15:06:22 +0700 Subject: [PATCH 6/7] span to metrics only if configured --- .../java/com/vaadin/extension/Constants.java | 1 + .../com/vaadin/extension/conf/Configuration.java | 16 ++++++++++++++++ .../extension/conf/ConfigurationDefaults.java | 10 +++++++++- .../client/ObjectMapExporter.java | 3 +++ .../AbstractInstrumentationTest.java | 2 ++ .../vaadin/extension/metrics/MetricsTest.java | 1 + 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/Constants.java b/observability-kit-agent/src/main/java/com/vaadin/extension/Constants.java index 76f1b89d..750e4335 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/Constants.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/Constants.java @@ -15,6 +15,7 @@ */ public class Constants { public static final String CONFIG_TRACE_LEVEL = "otel.instrumentation.vaadin.trace-level"; + public static final String CONFIG_SPAN_TO_METRICS_ENABLED = "otel.instrumentation.vaadin.span-to-metrics.enabled"; // Vaadin attribute names public static final String SESSION_ID = "vaadin.session.id"; diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/Configuration.java b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/Configuration.java index 5b680349..93d767ac 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/Configuration.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/Configuration.java @@ -9,6 +9,7 @@ */ public class Configuration { public static final TraceLevel TRACE_LEVEL = determineTraceLevel(); + public static final boolean SPAN_TO_METRICS_ENABLED = determineSpanToMetricsEnabled(); private static TraceLevel determineTraceLevel() { String traceLevelString = AgentInstrumentationConfig.get().getString( @@ -20,6 +21,11 @@ private static TraceLevel determineTraceLevel() { } } + private static boolean determineSpanToMetricsEnabled() { + return AgentInstrumentationConfig.get().getBoolean( + Constants.CONFIG_SPAN_TO_METRICS_ENABLED, false); + } + /** * Checks whether a trace level is enabled. Can be used by instrumentations * to check whether some detail should be added to a trace or not. @@ -31,4 +37,14 @@ private static TraceLevel determineTraceLevel() { public static boolean isEnabled(TraceLevel traceLevel) { return TRACE_LEVEL.includes(traceLevel); } + + /** + * Checks whether span-to-metrics recording is enabled. When enabled, + * span duration data will be recorded as metrics. + * + * @return true if span-to-metrics is enabled, false if not + */ + public static boolean isSpanToMetricsEnabled() { + return SPAN_TO_METRICS_ENABLED; + } } diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java index d6585b44..2fb551bc 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java @@ -84,7 +84,15 @@ private SpanExporter setSpanExporter(SpanExporter spanExporter, private SpanProcessor spanToMetricProcessor(SpanProcessor spanProcessor, ConfigProperties configProperties) { - return SpanProcessor.composite(spanProcessor, new SpanToMetricProcessor()); + // Only add SpanToMetricProcessor if explicitly enabled + boolean spanToMetricsEnabled = configProperties.getBoolean( + Constants.CONFIG_SPAN_TO_METRICS_ENABLED, false); + + if (spanToMetricsEnabled) { + return SpanProcessor.composite(spanProcessor, new SpanToMetricProcessor()); + } else { + return spanProcessor; + } } private Map getDefaultProperties() { diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java index 9618b6a6..f190c3e7 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/instrumentation/client/ObjectMapExporter.java @@ -17,6 +17,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; +import com.vaadin.extension.conf.Configuration; import com.vaadin.extension.conf.ConfigurationDefaults; import com.vaadin.extension.metrics.Metrics; @@ -63,8 +64,10 @@ public void accept(String id, Map objectMap) { } exportSpans.forEach(span -> { + if (Configuration.isSpanToMetricsEnabled()) { long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos(); Metrics.recordSpanDuration(span.getName(), durationNanos, span.getSpanContext()); + } }); ConfigurationDefaults.spanExporter.export(exportSpans); diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java index a8cee418..d09ad105 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java @@ -106,6 +106,8 @@ public void setupMocks() { TraceLevel level = invocation.getArgument(0); return configuredTraceLevel.includes(level); }); + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(true); } @AfterEach diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java index 391c5a4d..0e0afdf4 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.vaadin.extension.conf.Configuration; import com.vaadin.extension.instrumentation.AbstractInstrumentationTest; import com.vaadin.extension.instrumentation.server.VaadinSessionInstrumentation; import com.vaadin.flow.server.VaadinSession; From 5a4a0c8b314064a9bf01940e3c1d31805d98c499 Mon Sep 17 00:00:00 2001 From: Oliver BN Date: Wed, 6 Aug 2025 10:43:54 +0700 Subject: [PATCH 7/7] test config --- .../extension/conf/ConfigurationDefaults.java | 2 +- .../conf/ConfigurationDefaultsTest.java | 64 ++++++++++ .../AbstractInstrumentationTest.java | 2 +- .../client/ObjectMapExporterTest.java | 117 ++++++++++++++++++ .../vaadin/extension/metrics/MetricsTest.java | 27 ++++ 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 observability-kit-agent/src/test/java/com/vaadin/extension/conf/ConfigurationDefaultsTest.java diff --git a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java index 2fb551bc..89ce0a72 100644 --- a/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java +++ b/observability-kit-agent/src/main/java/com/vaadin/extension/conf/ConfigurationDefaults.java @@ -82,7 +82,7 @@ private SpanExporter setSpanExporter(SpanExporter spanExporter, return spanExporter; } - private SpanProcessor spanToMetricProcessor(SpanProcessor spanProcessor, + SpanProcessor spanToMetricProcessor(SpanProcessor spanProcessor, ConfigProperties configProperties) { // Only add SpanToMetricProcessor if explicitly enabled boolean spanToMetricsEnabled = configProperties.getBoolean( diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/conf/ConfigurationDefaultsTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/conf/ConfigurationDefaultsTest.java new file mode 100644 index 00000000..a0807b94 --- /dev/null +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/conf/ConfigurationDefaultsTest.java @@ -0,0 +1,64 @@ +package com.vaadin.extension.conf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.vaadin.extension.Constants; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.trace.SpanProcessor; +import org.junit.jupiter.api.Test; + +class ConfigurationDefaultsTest { + + @Test + public void spanToMetricProcessor_whenEnabled_addsSpanToMetricProcessor() { + ConfigurationDefaults configDefaults = new ConfigurationDefaults(); + ConfigProperties configProperties = mock(ConfigProperties.class); + SpanProcessor originalProcessor = mock(SpanProcessor.class); + + // Mock configuration to return true for span-to-metrics enabled + when(configProperties.getBoolean(Constants.CONFIG_SPAN_TO_METRICS_ENABLED, false)) + .thenReturn(true); + + SpanProcessor result = configDefaults.spanToMetricProcessor(originalProcessor, configProperties); + + // Should return a composite processor (different from the original) + assertNotSame(originalProcessor, result, "Should return composite processor when enabled"); + } + + @Test + public void spanToMetricProcessor_whenDisabled_returnsOriginalProcessor() { + ConfigurationDefaults configDefaults = new ConfigurationDefaults(); + ConfigProperties configProperties = mock(ConfigProperties.class); + SpanProcessor originalProcessor = mock(SpanProcessor.class); + + // Mock configuration to return false for span-to-metrics enabled (default) + when(configProperties.getBoolean(Constants.CONFIG_SPAN_TO_METRICS_ENABLED, false)) + .thenReturn(false); + + SpanProcessor result = configDefaults.spanToMetricProcessor(originalProcessor, configProperties); + + // Should return the same original processor + assertSame(originalProcessor, result, "Should return original processor when disabled"); + } + + @Test + public void spanToMetricProcessor_whenNotConfigured_defaultsToDisabled() { + ConfigurationDefaults configDefaults = new ConfigurationDefaults(); + ConfigProperties configProperties = mock(ConfigProperties.class); + SpanProcessor originalProcessor = mock(SpanProcessor.class); + + // Mock configuration to use default value (false) when not explicitly set + when(configProperties.getBoolean(Constants.CONFIG_SPAN_TO_METRICS_ENABLED, false)) + .thenReturn(false); // This simulates the default case + + SpanProcessor result = configDefaults.spanToMetricProcessor(originalProcessor, configProperties); + + // Should return the same original processor (disabled by default) + assertSame(originalProcessor, result, "Should default to disabled when not configured"); + } +} \ No newline at end of file diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java index d09ad105..6ee0be0a 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/AbstractInstrumentationTest.java @@ -47,7 +47,7 @@ public abstract class AbstractInstrumentationTest { private VaadinSession mockSession; private VaadinService mockService; private Scope sessionScope; - private MockedStatic ConfigurationMock; + protected MockedStatic ConfigurationMock; private TraceLevel configuredTraceLevel; public UI getMockUI() { diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/client/ObjectMapExporterTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/client/ObjectMapExporterTest.java index 07e1d2b2..50b838c8 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/client/ObjectMapExporterTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/instrumentation/client/ObjectMapExporterTest.java @@ -3,6 +3,8 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; +import com.vaadin.extension.conf.Configuration; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; import io.opentelemetry.sdk.trace.data.SpanData; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -253,4 +255,119 @@ public void accept_emptyJson_exceptionIsThrown() { fail(e); } } + + @Test + public void spanToMetricsRespectedWhenDisabled() { + try { + // First, enable span-to-metrics and create a baseline metric + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(true); + + String jsonString = """ + { + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": {"stringValue": "test_service"} + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "test-scope" + }, + "spans": [ + { + "traceId": "12345678901234567890123456789012", + "spanId": "1234567890123456", + "name": "baseline-span", + "kind": 1, + "startTimeUnixNano": 1674542404352000000, + "endTimeUnixNano": 1674542405301000200, + "attributes": [] + } + ] + } + ] + } + ] + } + """; + + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map objectMap = objectMapper.readValue(jsonString, Map.class); + + // Process the spans with enabled configuration to create the metric + new ObjectMapExporter().accept("foo", objectMap); + + // Now get initial metrics state (after creating the metric) + HistogramPointData initialSpanMetric = getLastHistogramMetricValue("vaadin.span.duration"); + long initialSpanCount = initialSpanMetric.getCount(); + double initialSpanSum = initialSpanMetric.getSum(); + + // Now disable span-to-metrics + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(false); + + String jsonString2 = """ + { + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": {"stringValue": "test_service"} + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "test-scope" + }, + "spans": [ + { + "traceId": "12345678901234567890123456789013", + "spanId": "1234567890123457", + "name": "test-span-disabled", + "kind": 1, + "startTimeUnixNano": 1674542406352000000, + "endTimeUnixNano": 1674542407301000200, + "attributes": [] + } + ] + } + ] + } + ] + } + """; + + @SuppressWarnings("unchecked") + Map objectMap2 = objectMapper.readValue(jsonString2, Map.class); + + // Process the spans with disabled configuration + new ObjectMapExporter().accept("foo", objectMap2); + + // Verify no new span metrics were recorded + HistogramPointData finalSpanMetric = getLastHistogramMetricValue("vaadin.span.duration"); + assertEquals(initialSpanCount, finalSpanMetric.getCount(), + "Span count should not increase when span-to-metrics is disabled"); + assertEquals(initialSpanSum, finalSpanMetric.getSum(), 0, + "Span sum should not increase when span-to-metrics is disabled"); + + } catch (Exception e) { + fail(e); + } finally { + // Re-enable for other tests + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(true); + } + } } diff --git a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java index 0e0afdf4..7f0abf0f 100644 --- a/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java +++ b/observability-kit-agent/src/test/java/com/vaadin/extension/metrics/MetricsTest.java @@ -1,13 +1,17 @@ package com.vaadin.extension.metrics; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.vaadin.extension.conf.Configuration; import com.vaadin.extension.instrumentation.AbstractInstrumentationTest; import com.vaadin.extension.instrumentation.server.VaadinSessionInstrumentation; +import com.vaadin.extension.metrics.SpanToMetricProcessor; import com.vaadin.flow.server.VaadinSession; +import io.opentelemetry.sdk.trace.ReadableSpan; + import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; @@ -105,6 +109,29 @@ public void recordSpanDuration() { assertEquals(spanName, metricValue.getAttributes().get(io.opentelemetry.api.common.AttributeKey.stringKey("span.name"))); } + @Test + public void spanToMetricsConfigurationRespected() { + // Test that the configuration mock works as expected + + // Disable span-to-metrics + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(false); + + assertFalse(Configuration.isSpanToMetricsEnabled(), + "Configuration should reflect disabled state"); + + // Enable span-to-metrics + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(true); + + assertTrue(Configuration.isSpanToMetricsEnabled(), + "Configuration should reflect enabled state"); + + // Reset to enabled for other tests + ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled()) + .thenReturn(true); + } + @Test public void recordSpanDurationVariousSpanNames() { SpanContext spanContext = createMockSpanContext();