diff --git a/src/main/java/com/activeviam/apps/cfg/source/InitialCsvLoad.java b/src/main/java/com/activeviam/apps/cfg/source/InitialCsvLoad.java index d32da74..397706b 100644 --- a/src/main/java/com/activeviam/apps/cfg/source/InitialCsvLoad.java +++ b/src/main/java/com/activeviam/apps/cfg/source/InitialCsvLoad.java @@ -13,11 +13,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.event.EventListener; +import com.activeviam.apps.tracing.TracingUtil; import com.activeviam.database.api.DatabasePrinter; import com.activeviam.database.datastore.api.IDatastore; import com.activeviam.io.dlc.impl.DataLoadControllerService; import com.activeviam.io.dlc.impl.operations.request.DlcLoadRequest; +import lombok.Cleanup; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,6 +37,7 @@ void onApplicationReady() { } private void initialLoad() { + @Cleanup var span = TracingUtil.startSpan("Initial CSV Data Load"); log.info("Initial data load started..."); try { dataLoadControllerService.execute(DlcLoadRequest.builder() diff --git a/src/main/java/com/activeviam/apps/tracing/TracingConfig.java b/src/main/java/com/activeviam/apps/tracing/TracingConfig.java new file mode 100644 index 0000000..b535ea3 --- /dev/null +++ b/src/main/java/com/activeviam/apps/tracing/TracingConfig.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) ActiveViam 2025 + * ALL RIGHTS RESERVED. This material is the CONFIDENTIAL and PROPRIETARY + * property of ActiveViam Limited. Any unauthorized use, + * reproduction or transfer of this material is strictly prohibited + */ + +package com.activeviam.apps.tracing; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TracingConfig { + + @Bean + @ConditionalOnProperty(name = "spring.application.name") + public Void setupTracing(@Value("${spring.application.name}") String appName) { + TracingUtil.setDefaultTracingScopeName(appName); + return null; + } + +} diff --git a/src/main/java/com/activeviam/apps/tracing/TracingUtil.java b/src/main/java/com/activeviam/apps/tracing/TracingUtil.java new file mode 100644 index 0000000..7078685 --- /dev/null +++ b/src/main/java/com/activeviam/apps/tracing/TracingUtil.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) ActiveViam 2025 + * ALL RIGHTS RESERVED. This material is the CONFIDENTIAL and PROPRIETARY + * property of ActiveViam Limited. Any unauthorized use, + * reproduction or transfer of this material is strictly prohibited + */ + +package com.activeviam.apps.tracing; + +import java.io.Closeable; +import java.util.Arrays; +import java.util.List; + +import org.springframework.lang.Nullable; + +import com.activeviam.tech.core.internal.observability.Observability; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import lombok.Builder; +import lombok.NonNull; + +/** + * Simple tracing utility to create spans. + *

+ * Spans created here can be auto-closed when used with Lombok's {@link lombok.Cleanup} annotation: + *

+ *     {@code
+ *     myMethod() {
+ *       @Cleanup var closeableSpan = TracingUtil.startSpan();
+ *       ... method body ...
+ *       ... Span is automatically closed here ...
+ *     }
+ *     }
+ * 
+ * This span will be automatically closed when the variable goes out of scope. + * + * @author ActiveViam + */ +public class TracingUtil { + + public static String defaultTracingScopeName = "Atoti-Application"; + + public static String getDefaultTracingScopeName() { + return defaultTracingScopeName; + } + + public static void setDefaultTracingScopeName(String defaultTracingScopeName) { + TracingUtil.defaultTracingScopeName = defaultTracingScopeName; + } + + /** + * Wrapper around a span and its scope. This is so we can close the scope and end the span at the same time. + * This allows easy auto-closing of spans when used with Lombok's @Cleanup annotation. + *
+     *     {@code
+     *     @Cleanup var closeableSpan = TracingUtil.startSpan();
+     *     }
+     * 
+ */ + public record CloseableSpan(Span span, Scope scope) implements Closeable { + @Override + public void close() { + scope.close(); + span.end(); + } + } + + public static Tracer getTracer(@NonNull String tracerScopeName){ + return Observability.getEffectiveOtelInstance().getTracer(tracerScopeName); + } + + /// Starts a span with the default tracing scope name and a span name based on the caller method name. + public static TracingUtil.CloseableSpan startSpan(){ + return closeableSpanBuilder().build(); + } + + /// Starts a span with the default tracing scope name and the given span name. + public static TracingUtil.CloseableSpan startSpan(@NonNull String spanName){ + return closeableSpanBuilder() + .spanName(spanName) + .build(); + } + + /// Starts a span with the default tracing scope name, the given span name., and the given attributes. + public static TracingUtil.CloseableSpan startSpan(@Nullable String spanName, @Nullable Attributes attributes){ + return closeableSpanBuilder() + .spanName(spanName) + .attributes(attributes) + .build(); + } + + public static TracingUtil.CloseableSpanBuilder closeableSpanBuilder(){ + return new TracingUtil.CloseableSpanBuilder(); + } + + /** + * Starts a span with the given tracer scope name, span name, and attributes. + *
+ * Can use {@link #closeableSpanBuilder()} to build the span with a builder pattern. + * + * @param tracerScopeName Name of the Tracer scope. If null, uses the default tracing scope name. + * @param spanName Name of the span. If null, uses the caller method name. + * @param attributes Attributes to set on the span. If null, uses empty attributes. + * @return The started span wrapped in a CloseableSpan. + */ + @Builder + protected static TracingUtil.CloseableSpan startSpan( + @Nullable String tracerScopeName, + @Nullable String spanName, + @Nullable Attributes attributes){ + tracerScopeName = tracerScopeName == null ? getDefaultTracingScopeName() : tracerScopeName; + spanName = spanName == null ? CallerTrace.ofExternalCaller().simpleName() : spanName; + attributes = attributes == null ? Attributes.empty() : attributes; + + var span = getTracer(tracerScopeName) + .spanBuilder(spanName) + .setAllAttributes(attributes) + .startSpan(); + var scope = span.makeCurrent(); + return new TracingUtil.CloseableSpan(span, scope); + } + + /// Simple record to hold caller's stack trace information + protected record CallerTrace(String className, String methodName, StackTraceElement trace) { + + /** + * Creates a CallerTrace for the method that called a TracingUtil method. + * @return the first non-TracingUtil caller method name + */ + public static TracingUtil.CallerTrace ofExternalCaller(){ + var stackTrace = Thread.currentThread().getStackTrace(); + // Start from index 1 to skip the getStackTrace method itself + for (int i = 1; i < stackTrace.length; i++) { + var trace = stackTrace[i]; + // Skip TracingUtil class methods + if(!trace.getClassName().startsWith(TracingUtil.class.getName())){ + return TracingUtil.CallerTrace.of(trace); + } + } + throw new IllegalStateException("Could not find caller method name for stack trace: " + Arrays.stream(Thread.currentThread().getStackTrace()).map(Object::toString).toList()); + } + + public static TracingUtil.CallerTrace of(StackTraceElement trace){ + var className = List.of(trace.getClassName().split("\\.")).getLast(); + var methodName = trace.getMethodName(); + return new TracingUtil.CallerTrace(className, methodName, trace); + } + public String simpleName(){ + return className + "." + methodName; + } + } +} +