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
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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.
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -80,6 +82,19 @@ private SpanExporter setSpanExporter(SpanExporter spanExporter,
return spanExporter;
}

SpanProcessor spanToMetricProcessor(SpanProcessor spanProcessor,
ConfigProperties configProperties) {
// 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<String, String> getDefaultProperties() {
Map<String, String> properties = new HashMap<>();
final Map<String, String> defaultconfig = getPropertyFileProperties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

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;

/**
* This is a consumer callback that is injected into an ObservabilityHandler
Expand Down Expand Up @@ -61,6 +63,14 @@ public void accept(String id, Map<String, Object> objectMap) {
}
}

exportSpans.forEach(span -> {
if (Configuration.isSpanToMetricsEnabled()) {
long durationNanos = span.getEndEpochNanos() - span.getStartEpochNanos();
Metrics.recordSpanDuration(span.getName(), durationNanos, span.getSpanContext());
}
});

ConfigurationDefaults.spanExporter.export(exportSpans);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
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;
import io.opentelemetry.api.trace.SpanContext;

import java.time.Duration;
import java.time.Instant;
Expand All @@ -30,11 +33,11 @@ public class Metrics {
private static final Map<String, Instant> sessionStarts = new ConcurrentHashMap<>();

private static LongHistogram sessionDurationMeasurement;

private static LongHistogram spanDurationHistogram;
private static InstantProvider instantProvider = Instant::now;

static void setInstantProvider(InstantProvider instantProvider) {
Metrics.instantProvider = instantProvider;
Metrics.instantProvider = instantProvider;
}

public static void ensureMetricsRegistered() {
Expand Down Expand Up @@ -62,6 +65,13 @@ 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();
}
}

Expand Down Expand Up @@ -111,4 +121,15 @@ private static String getSessionIdentifier(VaadinSession session) {
interface InstantProvider {
Instant get();
}
}

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);
}


}
Original file line number Diff line number Diff line change
@@ -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 latencyNanos = span.getLatencyNanos();
Metrics.recordSpanDuration(span.getName(), latencyNanos, span.getSpanContext());
}

@Override
public void onStart(Context arg0, ReadWriteSpan arg1) {

}


}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public abstract class AbstractInstrumentationTest {
private VaadinSession mockSession;
private VaadinService mockService;
private Scope sessionScope;
private MockedStatic<Configuration> ConfigurationMock;
protected MockedStatic<Configuration> ConfigurationMock;
private TraceLevel configuredTraceLevel;

public UI getMockUI() {
Expand Down Expand Up @@ -106,6 +106,8 @@ public void setupMocks() {
TraceLevel level = invocation.getArgument(0);
return configuredTraceLevel.includes(level);
});
ConfigurationMock.when(() -> Configuration.isSpanToMetricsEnabled())
.thenReturn(true);
}

@AfterEach
Expand Down
Loading