diff --git a/common/src/main/java/com/genexus/opentelemetry/OpenTelemetryHelper.java b/common/src/main/java/com/genexus/opentelemetry/OpenTelemetryHelper.java
index a7a12d796..bc37b378f 100644
--- a/common/src/main/java/com/genexus/opentelemetry/OpenTelemetryHelper.java
+++ b/common/src/main/java/com/genexus/opentelemetry/OpenTelemetryHelper.java
@@ -2,15 +2,31 @@
import io.opentelemetry.api.trace.Span;
+/**
+ * Helper class for OpenTelemetry operations.
+ * Provides utility methods for working with OpenTelemetry.
+ */
public class OpenTelemetryHelper {
+ /**
+ * Records an exception on the specified span
+ *
+ * @param span the span to record the exception on
+ * @param exc the exception to record
+ */
public static void recordException(Span span, Throwable exc) {
if (span != null && exc != null) {
span.recordException(exc);
}
}
+ /**
+ * Records an exception on the current active span
+ *
+ * @param exc the exception to record
+ * @throws NullPointerException if exc is null
+ */
public static void recordException(Throwable exc) {
recordException(Span.current(), exc);
}
-}
+}
\ No newline at end of file
diff --git a/gxobservability/README.md b/gxobservability/README.md
new file mode 100644
index 000000000..100936097
--- /dev/null
+++ b/gxobservability/README.md
@@ -0,0 +1,100 @@
+# GeneXus Observability
+
+A Java library for integrating OpenTelemetry tracing capabilities into GeneXus applications.
+
+## Overview
+
+The GeneXus Observability module provides a wrapper around the OpenTelemetry Java SDK, allowing GeneXus applications to easily implement distributed tracing. It simplifies the process of creating spans, managing trace context, and propagating trace information across service boundaries.
+
+## Features
+
+- Creation and management of spans with different configurations
+- Context propagation across service boundaries
+- Ability to add and retrieve baggage items (metadata associated with a trace)
+- Support for different span types (internal, server, client, producer, consumer)
+- Ability to set span attributes, status, and record exceptions
+- Automatic configuration from environment variables
+
+## Dependencies
+
+This module depends on the following OpenTelemetry components:
+
+- opentelemetry-api
+- opentelemetry-sdk-trace
+- opentelemetry-exporter-otlp
+- opentelemetry-sdk
+- opentelemetry-semconv
+- opentelemetry-extension-annotations
+- opentelemetry-sdk-extension-autoconfigure
+
+## Main Components
+
+### OtelTracer
+
+The `OtelTracer` class is the main entry point for creating spans. It provides methods to:
+
+- Create spans with different configurations
+- Specify parent context
+- Link to other spans
+- Define span types
+
+### OtelSpan
+
+The `OtelSpan` class wraps an OpenTelemetry Span and provides methods to:
+
+- Set span attributes
+- Record exceptions
+- Set span status
+- Add and retrieve baggage items
+- End spans
+
+### GXSpanContext and GXTraceContext
+
+These classes provide wrappers around OpenTelemetry's SpanContext and Context objects, making it easier to handle trace propagation between services.
+
+## Configuration
+
+The module can be configured using environment variables:
+
+- `OTEL_SERVICE_NAME`: Defines the service name for traces
+- `OTEL_SERVICE_VERSION`: Defines the service version
+- `OTEL_RESOURCE_ATTRIBUTES`: Defines additional resource attributes
+- `JAVA_INSTRUMENTATION_SCOPE_NAME`: Override for the instrumentation scope name
+- `JAVA_INSTRUMENTATION_SCOPE_VERSION`: Override for the instrumentation scope version
+
+## Usage Examples
+
+### Creating a Simple Span
+
+```java
+OtelTracer tracer = new OtelTracer();
+OtelSpan span = tracer.createSpan("MyOperation");
+try {
+ // Perform your operation here
+ span.setStringAttribute("attribute.key", "value");
+} catch (Exception e) {
+ span.recordException(e.getMessage());
+ span.setStatus((byte)2, "Operation failed"); // Set status to ERROR
+} finally {
+ span.endSpan();
+}
+```
+
+### Creating a Span with Context Propagation
+
+```java
+OtelTracer tracer = new OtelTracer();
+OtelSpan parentSpan = tracer.createSpan("ParentOperation");
+GXTraceContext context = parentSpan.getGXTraceContext();
+
+// Use the context in a child operation
+OtelSpan childSpan = tracer.createSpan("ChildOperation", context, (byte)2); // CLIENT span
+childSpan.setStringAttribute("request.id", "12345");
+childSpan.endSpan();
+
+parentSpan.endSpan();
+```
+
+## Integration with GeneXus
+
+This module is designed to be easily integrated with GeneXus applications, providing tracing capabilities that can be used both in Java-based backend services and in code generated by GeneXus.
\ No newline at end of file
diff --git a/gxobservability/pom.xml b/gxobservability/pom.xml
index 9b48bd2b8..d395152d2 100644
--- a/gxobservability/pom.xml
+++ b/gxobservability/pom.xml
@@ -11,24 +11,29 @@
gxobservability
- GeneXus Observability
+ GeneXus Observability
+
io.opentelemetry
opentelemetry-api
+ ${io.opentelemetry.version}
io.opentelemetry
opentelemetry-sdk-trace
+ ${io.opentelemetry.version}
io.opentelemetry
opentelemetry-exporter-otlp
+ ${io.opentelemetry.version}
io.opentelemetry
opentelemetry-sdk
+ ${io.opentelemetry.version}
io.opentelemetry
@@ -38,24 +43,52 @@
io.opentelemetry
opentelemetry-extension-annotations
+ 1.18.0
io.opentelemetry
opentelemetry-sdk-extension-autoconfigure
- 1.36.0
+ ${io.opentelemetry.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.8.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.8.2
+ test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ 1.8.2
+ test
+
+
+ org.mockito
+ mockito-core
+ 4.5.1
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ 4.5.1
+ test
+
+
+ io.opentelemetry
+ opentelemetry-sdk-testing
+ ${io.opentelemetry.version}
+ test
-
-
-
- io.opentelemetry
- opentelemetry-bom
- 1.23.0
- pom
- import
-
-
-
gxobservability
diff --git a/gxobservability/src/main/java/com/genexus/opentelemetry/ExtractAttributeValueHelper.java b/gxobservability/src/main/java/com/genexus/opentelemetry/ExtractAttributeValueHelper.java
new file mode 100644
index 000000000..9d3ea96b4
--- /dev/null
+++ b/gxobservability/src/main/java/com/genexus/opentelemetry/ExtractAttributeValueHelper.java
@@ -0,0 +1,31 @@
+package com.genexus.opentelemetry;
+
+/**
+ * Helper class to provide a testable version of the extractAttributeValue method from OtelTracer.
+ * This is used to support unit testing of the private method.
+ */
+public class ExtractAttributeValueHelper {
+
+ /**
+ * Extracts an attribute value from a resource attributes string
+ *
+ * @param resourceAttributes the resource attributes string
+ * @param attributeName the name of the attribute to extract
+ * @return the attribute value, or null if not found
+ */
+ public static String extractAttributeValue(String resourceAttributes, String attributeName) {
+ if (resourceAttributes == null || attributeName == null) {
+ return null;
+ }
+
+ // Simple parsing for key-value pairs
+ String[] pairs = resourceAttributes.split(",");
+ for (String pair : pairs) {
+ String[] keyValue = pair.split("=");
+ if (keyValue.length == 2 && keyValue[0].equals(attributeName)) {
+ return keyValue[1];
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/gxobservability/src/main/java/com/genexus/opentelemetry/GXSpanContext.java b/gxobservability/src/main/java/com/genexus/opentelemetry/GXSpanContext.java
index 07c68f93b..6101002d2 100644
--- a/gxobservability/src/main/java/com/genexus/opentelemetry/GXSpanContext.java
+++ b/gxobservability/src/main/java/com/genexus/opentelemetry/GXSpanContext.java
@@ -1,33 +1,55 @@
package com.genexus.opentelemetry;
import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.trace.StatusCode;
-import io.opentelemetry.context.Context;
-public class GXSpanContext
-{
- private io.opentelemetry.api.trace.SpanContext _spanContext;
+import io.opentelemetry.api.trace.SpanContext;
- public io.opentelemetry.api.trace.SpanContext getSpanContext()
- {
- return _spanContext;
+/**
+ * Wrapper class for OpenTelemetry SpanContext.
+ * Provides access to trace and span identifiers.
+ */
+public class GXSpanContext {
+ private final SpanContext spanContext;
+
+ /**
+ * Returns the underlying OpenTelemetry SpanContext
+ *
+ * @return the underlying SpanContext instance
+ */
+ public SpanContext getSpanContext() {
+ return spanContext;
}
- public GXSpanContext(io.opentelemetry.api.trace.SpanContext spanContext)
- {
- this._spanContext = spanContext;
+
+ /**
+ * Creates a GXSpanContext from an existing SpanContext
+ *
+ * @param spanContext the OpenTelemetry SpanContext to wrap
+ */
+ public GXSpanContext(SpanContext spanContext) {
+ this.spanContext = spanContext;
}
- public GXSpanContext()
- {
- _spanContext = Span.current().getSpanContext();
+
+ /**
+ * Creates a GXSpanContext from the current active span
+ */
+ public GXSpanContext() {
+ spanContext = Span.current().getSpanContext();
}
- public String traceId()
- {
- return _spanContext.getTraceId();
+
+ /**
+ * Gets the trace ID of the current span context
+ *
+ * @return the trace ID as a hexadecimal string
+ */
+ public String traceId() {
+ return spanContext.getTraceId();
}
- public String spanId()
- {
- return _spanContext.getSpanId();
+
+ /**
+ * Gets the span ID of the current span context
+ *
+ * @return the span ID as a hexadecimal string
+ */
+ public String spanId() {
+ return spanContext.getSpanId();
}
}
\ No newline at end of file
diff --git a/gxobservability/src/main/java/com/genexus/opentelemetry/GXTraceContext.java b/gxobservability/src/main/java/com/genexus/opentelemetry/GXTraceContext.java
index 6367b2938..e7b12a110 100644
--- a/gxobservability/src/main/java/com/genexus/opentelemetry/GXTraceContext.java
+++ b/gxobservability/src/main/java/com/genexus/opentelemetry/GXTraceContext.java
@@ -1,21 +1,29 @@
package com.genexus.opentelemetry;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
-public class GXTraceContext
-{
- private Context context;
- public GXTraceContext(io.opentelemetry.context.Context context)
- {
+/**
+ * Wrapper class for OpenTelemetry Context.
+ * Provides access to trace context for propagation between processes.
+ */
+public class GXTraceContext {
+ private final Context context;
+
+ /**
+ * Creates a GXTraceContext from an existing OpenTelemetry Context
+ *
+ * @param context the OpenTelemetry Context to wrap
+ */
+ public GXTraceContext(Context context) {
this.context = context;
}
- public Context getTraceContext()
- {
+
+ /**
+ * Returns the underlying OpenTelemetry Context
+ *
+ * @return the underlying Context instance
+ */
+ public Context getTraceContext() {
return this.context;
}
}
\ No newline at end of file
diff --git a/gxobservability/src/main/java/com/genexus/opentelemetry/OtelSpan.java b/gxobservability/src/main/java/com/genexus/opentelemetry/OtelSpan.java
index 9e1c2d928..9ea2ed957 100644
--- a/gxobservability/src/main/java/com/genexus/opentelemetry/OtelSpan.java
+++ b/gxobservability/src/main/java/com/genexus/opentelemetry/OtelSpan.java
@@ -2,164 +2,301 @@
import java.util.concurrent.atomic.AtomicReference;
import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.api.GlobalOpenTelemetry;
-import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.extension.annotations.SpanAttribute;
-import io.opentelemetry.extension.annotations.WithSpan;
-import io.opentelemetry.api.common.AttributeKey;
-import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.baggage.Baggage;
-import io.opentelemetry.api.baggage.BaggageBuilder;
+
+/**
+ * Wrapper class for OpenTelemetry Span.
+ * Provides methods to manage spans, attributes, and context propagation.
+ */
public class OtelSpan {
- private Span span;
- public enum SpanStatusCode
- {
+ private final Span span;
+
+ /**
+ * Enum representing the different status codes a span can have
+ */
+ public enum SpanStatusCode {
UNSET,
OK,
ERROR
}
- public OtelSpan(Span span)
- {
- this.span=span;
+
+ /**
+ * Creates an OtelSpan with an existing OpenTelemetry Span
+ *
+ * @param span the OpenTelemetry Span to wrap
+ */
+ public OtelSpan(Span span) {
+ this.span = span;
+ }
+
+ /**
+ * Creates an empty OtelSpan with no underlying span
+ */
+ public OtelSpan() {
+ this.span = null;
}
- public OtelSpan()
- {}
- //region EO Properties
- public String getTraceId()
- {
- if (span != null)
+
+ //region Properties
+
+ /**
+ * Gets the trace ID of the current span
+ *
+ * @return the trace ID as a string, or empty string if no span exists
+ */
+ public String getTraceId() {
+ if (span != null && span.getSpanContext() != null) {
return span.getSpanContext().getTraceId();
+ }
return "";
}
- public String getSpanId()
- {
- if (span != null)
+
+ /**
+ * Gets the span ID of the current span
+ *
+ * @return the span ID as a string, or empty string if no span exists
+ */
+ public String getSpanId() {
+ if (span != null && span.getSpanContext() != null) {
return span.getSpanContext().getSpanId();
+ }
return "";
}
- public Boolean isRecording()
- {
- if (span != null)
- return span.isRecording();
- return false;
+
+ /**
+ * Checks if the current span is recording events
+ *
+ * @return true if recording, false otherwise
+ */
+ public Boolean isRecording() {
+ return span != null && span.isRecording();
}
+
+ /**
+ * Gets the span context of the current span
+ *
+ * @return a GXSpanContext wrapping the current span context, or null if no span exists
+ */
public GXSpanContext getSpanContext() {
- return new GXSpanContext(getSpanContext(span));
+ if (span != null && span.getSpanContext() != null) {
+ return new GXSpanContext(span.getSpanContext());
+ }
+ return null;
}
//endregion
- //region EO Methods
- public void endSpan()
- {
- if (span!=null)
+
+ //region Methods
+
+ /**
+ * Ends the current span
+ */
+ public void endSpan() {
+ if (span != null) {
span.end();
+ }
+ }
+
+ /**
+ * Adds a baggage item to the current context
+ *
+ * @param key the baggage item key
+ * @param value the baggage item value
+ * @return a new GXTraceContext containing the added baggage
+ */
+ public GXTraceContext addBaggage(String key, String value) {
+ Context context = addBaggageReturnContext(key, value);
+ return new GXTraceContext(context);
+ }
+
+ /**
+ * Gets a baggage item from the specified trace context
+ *
+ * @param key the baggage item key
+ * @param gxTraceContext the trace context to get the baggage from
+ * @return the baggage item value, or empty string if not found
+ */
+ public String getBaggageItem(String key, GXTraceContext gxTraceContext) {
+ return getBaggageItemInContext(gxTraceContext.getTraceContext(), key);
}
- public GXTraceContext addBaggage(String key, String value)
- {
- return new GXTraceContext(addBaggageReturnContext(key, value));
- }
- public String getBaggaeItem(String key,GXTraceContext gxTraceContext)
- {
- return getBaggaeItemInContext(gxTraceContext.getTraceContext(),key);
- }
- public GXTraceContext getGXTraceContext()
- {
- return new GXTraceContext(getContext());
- }
- public void recordException(String message)
- {
- recordException(span,new Throwable(message));
- }
- public void setStringAttribute(String key, String value)
- {
- if (span != null)
- span.setAttribute(key,value);
- }
- public void setBooleanAttribute(String key, boolean value)
- {
- if (span != null)
- span.setAttribute(key,value);
- }
- public void setDoubleAttribute(String key, double value)
- {
- if (span != null)
- span.setAttribute(key,value);
- }
- public void setLongAttribute(String key, long value)
- {
- if (span != null)
- span.setAttribute(key,value);
- }
- public void setStatus(Byte spanStatusCodeByte)
- {
- StatusCode statusCode = toStatusCode(spanStatusCodeByte);
- if (span != null)
- span.setStatus(statusCode);
- }
- public void setStatus(Byte spanStatusCodeByte, String message)
- {
- StatusCode statusCode = toStatusCode(spanStatusCodeByte);
- if (span != null)
- span.setStatus(statusCode, message);
+
+ /**
+ * Gets the current trace context
+ *
+ * @return a new GXTraceContext containing the current context
+ */
+ public GXTraceContext getGXTraceContext() {
+ Context context = getContext();
+ if (context != null) {
+ return new GXTraceContext(context);
+ }
+ return new GXTraceContext(Context.current());
+ }
+
+ /**
+ * Records an exception with the given message
+ *
+ * @param message the exception message
+ */
+ public void recordException(String message) {
+ if (span != null && message != null) {
+ recordException(span, new Throwable(message));
+ }
+ }
+
+ /**
+ * Sets a string attribute on the current span
+ *
+ * @param key the attribute key
+ * @param value the attribute value
+ */
+ public void setStringAttribute(String key, String value) {
+ if (span != null && key != null && value != null) {
+ span.setAttribute(key, value);
+ }
+ }
+
+ /**
+ * Sets a boolean attribute on the current span
+ *
+ * @param key the attribute key
+ * @param value the attribute value
+ */
+ public void setBooleanAttribute(String key, boolean value) {
+ if (span != null && key != null) {
+ span.setAttribute(key, value);
+ }
+ }
+
+ /**
+ * Sets a double attribute on the current span
+ *
+ * @param key the attribute key
+ * @param value the attribute value
+ */
+ public void setDoubleAttribute(String key, double value) {
+ if (span != null && key != null) {
+ span.setAttribute(key, value);
+ }
+ }
+
+ /**
+ * Sets a long attribute on the current span
+ *
+ * @param key the attribute key
+ * @param value the attribute value
+ */
+ public void setLongAttribute(String key, long value) {
+ if (span != null && key != null) {
+ span.setAttribute(key, value);
+ }
+ }
+
+ /**
+ * Sets the status of the current span
+ *
+ * @param spanStatusCodeByte the status code as a byte
+ */
+ public void setStatus(Byte spanStatusCodeByte) {
+ if (span != null && spanStatusCodeByte != null) {
+ StatusCode statusCode = toStatusCode(spanStatusCodeByte);
+ if (statusCode != null) {
+ span.setStatus(statusCode);
+ }
+ }
+ }
+
+ /**
+ * Sets the status of the current span with a description
+ *
+ * @param spanStatusCodeByte the status code as a byte
+ * @param message the status description
+ */
+ public void setStatus(Byte spanStatusCodeByte, String message) {
+ if (span != null && spanStatusCodeByte != null && message != null) {
+ StatusCode statusCode = toStatusCode(spanStatusCodeByte);
+ if (statusCode != null) {
+ span.setStatus(statusCode, message);
+ }
+ }
}
//endregion
-
+
//region Private methods
-
- private String getBaggaeItemInContext(Context context, String key)
- {
+
+ /**
+ * Gets a baggage item from the specified context
+ *
+ * @param context the context to get the baggage from
+ * @param key the baggage item key
+ * @return the baggage item value, or empty string if not found
+ */
+ private String getBaggageItemInContext(Context context, String key) {
+ if (context == null || key == null) {
+ return "";
+ }
+
AtomicReference value = new AtomicReference<>("");
Baggage.fromContext(context).asMap().forEach((k, v) -> {
if (k.equals(key)) {
value.set(v.getValue());
}
});
- if (value != null)
- return value.get();
- return "";
+ return value.get();
}
- private Context addBaggageReturnContext(String key, String value)
- {
- Baggage baggage = Baggage.current().toBuilder().put(key,value).build();
- return baggage.storeInContext(getContext());
+
+ /**
+ * Adds a baggage item to the current context
+ *
+ * @param key the baggage item key
+ * @param value the baggage item value
+ * @return the new context with the added baggage
+ */
+ private Context addBaggageReturnContext(String key, String value) {
+ if (key == null || value == null) {
+ return Context.current();
+ }
+
+ Baggage baggage = Baggage.current().toBuilder().put(key, value).build();
+ Context context = getContext();
+ return baggage.storeInContext(context != null ? context : Context.current());
}
- private Context getContext()
- {
- if (span != null)
+
+ /**
+ * Gets the current context with the span
+ *
+ * @return the current context with the span, or null if no span exists
+ */
+ private Context getContext() {
+ if (span != null) {
return Context.current().with(span);
- return null;
- }
- private Context getContextCurrentSpan()
- {
+ }
return Context.current();
}
-
+
+ /**
+ * Records an exception on the specified span
+ *
+ * @param span the span to record the exception on
+ * @param exc the exception to record
+ */
private static void recordException(Span span, Throwable exc) {
if (span != null && exc != null) {
span.recordException(exc);
}
}
- private io.opentelemetry.api.trace.SpanContext getSpanContext(Span span)
- {
- if (span != null)
- return span.getSpanContext();
- return null;
- }
- private boolean isRecording(Span span)
- {
- if (span != null)
- return span.isRecording();
- return false;
- }
- private Span current()
- {
- return Span.current();
-
- }
- private static StatusCode toStatusCode (Byte spanStatusCode){
+
+ /**
+ * Converts a byte value to a StatusCode
+ *
+ * @param spanStatusCode the status code as a byte
+ * @return the corresponding StatusCode, or null if invalid
+ */
+ private static StatusCode toStatusCode(Byte spanStatusCode) {
+ if (spanStatusCode == null) {
+ return null;
+ }
+
switch (spanStatusCode) {
case 0:
return StatusCode.UNSET;
@@ -167,10 +304,22 @@ private static StatusCode toStatusCode (Byte spanStatusCode){
return StatusCode.OK;
case 2:
return StatusCode.ERROR;
+ default:
+ return null;
}
- return null;
}
- private static SpanStatusCode fromStatusCode (StatusCode statusCode){
+
+ /**
+ * Converts a StatusCode to a SpanStatusCode
+ *
+ * @param statusCode the StatusCode to convert
+ * @return the corresponding SpanStatusCode, or null if invalid
+ */
+ private static SpanStatusCode fromStatusCode(StatusCode statusCode) {
+ if (statusCode == null) {
+ return null;
+ }
+
switch (statusCode) {
case UNSET:
return SpanStatusCode.UNSET;
@@ -178,9 +327,9 @@ private static SpanStatusCode fromStatusCode (StatusCode statusCode){
return SpanStatusCode.OK;
case ERROR:
return SpanStatusCode.ERROR;
+ default:
+ return null;
}
- return null;
}
//endregion
-
}
\ No newline at end of file
diff --git a/gxobservability/src/main/java/com/genexus/opentelemetry/OtelTracer.java b/gxobservability/src/main/java/com/genexus/opentelemetry/OtelTracer.java
index cd7442a96..6f5840c9f 100644
--- a/gxobservability/src/main/java/com/genexus/opentelemetry/OtelTracer.java
+++ b/gxobservability/src/main/java/com/genexus/opentelemetry/OtelTracer.java
@@ -5,202 +5,343 @@
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.context.Context;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.extension.annotations.SpanAttribute;
-import io.opentelemetry.extension.annotations.WithSpan;
-import io.opentelemetry.sdk.resources.Resource;
-import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
+
import java.util.Iterator;
-import java.util.regex.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Main class for creating and managing OpenTelemetry spans.
+ * Provides methods to create spans with different configurations.
+ */
public class OtelTracer {
private static final String OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME";
private static final String OTEL_SERVICE_VERSION = "OTEL_SERVICE_VERSION";
private static final String OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES";
private static final String JAVA_INSTRUMENTATION_SCOPE_NAME = "JAVA_INSTRUMENTATION_SCOPE_NAME";
private static final String JAVA_INSTRUMENTATION_SCOPE_VERSION = "JAVA_INSTRUMENTATION_SCOPE_VERSION";
- private static StringPair instrumentationScope=getInstrumentationScope();
- private static Tracer tracer=getTracer(instrumentationScope);
+
+ private static final StringPair instrumentationScope = getInstrumentationScope();
+ private static final Tracer tracer = getTracer(instrumentationScope);
+
+ /**
+ * Helper class to store name and version information
+ */
static class StringPair {
final String name;
final String version;
StringPair(String name, String version) {
- this.name = name;
- this.version = version;
+ this.name = name != null ? name : "";
+ this.version = version != null ? version : "";
}
}
- public enum SpanType
- {
+
+ /**
+ * Enum representing the different types of spans
+ */
+ public enum SpanType {
INTERNAL,
SERVER,
CLIENT,
PRODUCER,
CONSUMER
}
- public OtelSpan createSpan(String displayName)
- {
- Span otelspan= createAndStartSpan(displayName);
+
+ /**
+ * Creates a new span with the specified name
+ *
+ * @param displayName the name of the span
+ * @return a new OtelSpan instance
+ */
+ public OtelSpan createSpan(String displayName) {
+ Span otelspan = createAndStartSpan(displayName);
return new OtelSpan(otelspan);
}
- public OtelSpan createSpan(String displayName, Byte spanTypeByte)
- {
- io.opentelemetry.api.trace.SpanKind spanKind = toSpanKind(spanTypeByte);
+
+ /**
+ * Creates a new span with the specified name and type
+ *
+ * @param displayName the name of the span
+ * @param spanTypeByte the type of the span as a byte
+ * @return a new OtelSpan instance
+ */
+ public OtelSpan createSpan(String displayName, Byte spanTypeByte) {
+ SpanKind spanKind = toSpanKind(spanTypeByte);
Span otelspan = createAndStartSpan(displayName, spanKind);
return new OtelSpan(otelspan);
}
- public OtelSpan createSpan(String displayName, GXTraceContext gxTraceContext, Byte spanTypeByte)
- {
- io.opentelemetry.api.trace.SpanKind spanKind = toSpanKind(spanTypeByte);
- Span otelspan = createAndStartSpan(displayName,spanKind,gxTraceContext.getTraceContext());
+
+ /**
+ * Creates a new span with the specified name, type, and parent context
+ *
+ * @param displayName the name of the span
+ * @param gxTraceContext the parent context
+ * @param spanTypeByte the type of the span as a byte
+ * @return a new OtelSpan instance
+ */
+ public OtelSpan createSpan(String displayName, GXTraceContext gxTraceContext, Byte spanTypeByte) {
+ if (gxTraceContext == null) {
+ return createSpan(displayName, spanTypeByte);
+ }
+
+ SpanKind spanKind = toSpanKind(spanTypeByte);
+ Span otelspan = createAndStartSpan(displayName, spanKind, gxTraceContext.getTraceContext());
return new OtelSpan(otelspan);
}
- public OtelSpan createSpan(String displayName, GXTraceContext gxTraceContext, Byte spanTypeByte, Iterator gxSpanContextIterator)
- {
- io.opentelemetry.api.trace.SpanKind spanKind = toSpanKind(spanTypeByte);
- Span otelspan = createAndStartSpan(displayName,spanKind,gxTraceContext.getTraceContext(),gxSpanContextIterator);
+
+ /**
+ * Creates a new span with the specified name, type, parent context, and linked spans
+ *
+ * @param displayName the name of the span
+ * @param gxTraceContext the parent context
+ * @param spanTypeByte the type of the span as a byte
+ * @param gxSpanContextIterator an iterator of span contexts to link
+ * @return a new OtelSpan instance
+ */
+ public OtelSpan createSpan(String displayName, GXTraceContext gxTraceContext, Byte spanTypeByte, Iterator gxSpanContextIterator) {
+ if (gxTraceContext == null || gxSpanContextIterator == null) {
+ return createSpan(displayName, gxTraceContext, spanTypeByte);
+ }
+
+ SpanKind spanKind = toSpanKind(spanTypeByte);
+ Span otelspan = createAndStartSpan(
+ displayName,
+ spanKind,
+ gxTraceContext.getTraceContext(),
+ gxSpanContextIterator
+ );
return new OtelSpan(otelspan);
}
- public static OtelSpan getCurrentSpan()
- {
+
+ /**
+ * Gets the current active span
+ *
+ * @return an OtelSpan wrapping the current span
+ */
+ public static OtelSpan getCurrentSpan() {
return new OtelSpan(Span.current());
}
+
//region Private methods
- private static StringPair getInstrumentationScope()
- {
- String name="GeneXus.Tracing";
- String version="";
+
+ /**
+ * Gets the instrumentation scope information from environment variables
+ *
+ * @return a StringPair containing the scope name and version
+ */
+ private static StringPair getInstrumentationScope() {
+ String name = "GeneXus.Tracing";
+ String version = "";
String javaInstrumentationScopeName = System.getenv(JAVA_INSTRUMENTATION_SCOPE_NAME);
String javaInstrumentationScopeVersion = System.getenv(JAVA_INSTRUMENTATION_SCOPE_VERSION);
- if (javaInstrumentationScopeName!=null && !javaInstrumentationScopeName.trim().isEmpty())
- {
+ if (javaInstrumentationScopeName != null && !javaInstrumentationScopeName.trim().isEmpty()) {
name = javaInstrumentationScopeName;
- if (javaInstrumentationScopeVersion!=null && !javaInstrumentationScopeVersion.trim().isEmpty()) {
+ if (javaInstrumentationScopeVersion != null && !javaInstrumentationScopeVersion.trim().isEmpty()) {
version = javaInstrumentationScopeVersion;
}
- }
- else
- {
+ } else {
String serviceName = System.getenv(OTEL_SERVICE_NAME);
+ String resourceAttributes = System.getenv(OTEL_RESOURCE_ATTRIBUTES);
- if (serviceName==null || serviceName.trim().isEmpty()) {
- String pattern = "(?:\\b\\w+\\b=\\w+)(?:,(?:\\b\\w+\\b=\\w+))*";
- Pattern regex = Pattern.compile(pattern);
- Matcher matcher = regex.matcher(OTEL_RESOURCE_ATTRIBUTES);
-
- while (matcher.find()) {
- String[] keyValue = matcher.group().split("=");
- if (keyValue[0].equals("service.name")) {
- serviceName = keyValue[1];
- break;
- }
- }
+ if ((serviceName == null || serviceName.trim().isEmpty()) && resourceAttributes != null) {
+ serviceName = extractAttributeValue(resourceAttributes, "service.name");
}
String serviceVersion = System.getenv(OTEL_SERVICE_VERSION);
- if (serviceVersion==null || serviceVersion.trim().isEmpty()) {
- String pattern = "(?:\\b\\w+\\b=\\w+)(?:,(?:\\b\\w+\\b=\\w+))*";
- Pattern regex = Pattern.compile(pattern);
- Matcher matcher = regex.matcher(OTEL_RESOURCE_ATTRIBUTES);
-
- while (matcher.find()) {
-
- String[] keyValue = matcher.group().split("=");
-
- if (keyValue[0].equals("service.version")) {
- serviceVersion = keyValue[1];
- break;
- }
- }
+ if ((serviceVersion == null || serviceVersion.trim().isEmpty()) && resourceAttributes != null) {
+ serviceVersion = extractAttributeValue(resourceAttributes, "service.version");
}
- if (serviceName!=null && !serviceName.trim().isEmpty())
+ if (serviceName != null && !serviceName.trim().isEmpty()) {
name = serviceName;
- if (serviceVersion!=null && !serviceVersion.trim().isEmpty())
+ }
+ if (serviceVersion != null && !serviceVersion.trim().isEmpty()) {
version = serviceVersion;
-
+ }
}
- return new StringPair(name,version);
+ return new StringPair(name, version);
}
+
+ /**
+ * Extracts an attribute value from a resource attributes string
+ *
+ * @param resourceAttributes the resource attributes string
+ * @param attributeName the name of the attribute to extract
+ * @return the attribute value, or null if not found
+ */
+ private static String extractAttributeValue(String resourceAttributes, String attributeName) {
+ if (resourceAttributes == null || attributeName == null) {
+ return null;
+ }
+
+ String pattern = "(\\w[\\w.\\-]*?)=([^,]+)";
+ Pattern regex = Pattern.compile(pattern);
+ Matcher matcher = regex.matcher(resourceAttributes);
- private static Tracer getTracer(StringPair instrumentationScope)
- {
- OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
- if (openTelemetry != null)
- return openTelemetry.getTracer(instrumentationScope.name,instrumentationScope.version);
+ while (matcher.find()) {
+ String[] keyValue = matcher.group().split("=");
+ if (keyValue.length == 2 && keyValue[0].equals(attributeName)) {
+ return keyValue[1];
+ }
+ }
return null;
}
- private static Span createAndStartSpan(String displayName)
- {
- if (tracer != null) {
- if (!displayName.isEmpty())
- return tracer.spanBuilder(displayName).startSpan();
+ /**
+ * Gets a Tracer instance from the GlobalOpenTelemetry
+ *
+ * @param instrumentationScope the instrumentation scope information
+ * @return a Tracer instance, or null if not available
+ */
+ private static Tracer getTracer(StringPair instrumentationScope) {
+ if (instrumentationScope == null) {
+ return null;
}
- return null;
- }
- private static Span createAndStartSpan(String displayName, io.opentelemetry.api.trace.SpanKind spanKind)
- {
- if (tracer != null) {
- if (!displayName.isEmpty() && spanKind != null)
- return tracer.spanBuilder(displayName).setSpanKind(spanKind).startSpan();
- else if (spanKind == null) {
- return tracer.spanBuilder(displayName).startSpan();
+
+ try {
+ OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
+ if (openTelemetry != null) {
+ return openTelemetry.getTracer(
+ instrumentationScope.name,
+ instrumentationScope.version
+ );
}
+ } catch (IllegalStateException e) {
+ // OpenTelemetry not properly initialized
}
return null;
}
- private static Span createAndStartSpan(String displayName, io.opentelemetry.api.trace.SpanKind spanKind, Context context)
- {
- if (tracer != null) {
- if (!displayName.isEmpty() && spanKind != null && context != null)
- return tracer.spanBuilder(displayName).setSpanKind(spanKind).setParent(context).startSpan();
- else {
- if (!displayName.isEmpty() && spanKind != null && context == null)
- return tracer.spanBuilder(displayName).setSpanKind(spanKind).setNoParent().startSpan();
- }
+
+ /**
+ * Creates and starts a span with the specified name
+ *
+ * @param displayName the name of the span
+ * @return the created span, or null if creation failed
+ */
+ private static Span createAndStartSpan(String displayName) {
+ if (tracer != null && displayName != null && !displayName.isEmpty()) {
+ return tracer.spanBuilder(displayName).startSpan();
}
return null;
}
- private static Span createAndStartSpan(String displayName, io.opentelemetry.api.trace.SpanKind spanKind, Context context, Iterator gxSpanContexts)
- {
- if (tracer != null) {
- SpanBuilder spanBuilder;
- if (!displayName.isEmpty() && spanKind != null && context != null) {
- spanBuilder = tracer.spanBuilder(displayName).setSpanKind(spanKind).setParent(context);
- } else {
- if (!displayName.isEmpty() && spanKind != null && context == null)
- spanBuilder = tracer.spanBuilder(displayName).setSpanKind(spanKind).setNoParent();
- else
- return null;
- }
- while (gxSpanContexts.hasNext()) {
- if (spanBuilder != null)
- spanBuilder.addLink(gxSpanContexts.next().getSpanContext());
+
+ /**
+ * Creates and starts a span with the specified name and kind
+ *
+ * @param displayName the name of the span
+ * @param spanKind the kind of the span
+ * @return the created span, or null if creation failed
+ */
+ private static Span createAndStartSpan(String displayName, SpanKind spanKind) {
+ if (tracer == null || displayName == null || displayName.isEmpty()) {
+ return null;
+ }
+
+ SpanBuilder builder = tracer.spanBuilder(displayName);
+ if (spanKind != null) {
+ builder.setSpanKind(spanKind);
+ }
+ return builder.startSpan();
+ }
+
+ /**
+ * Creates and starts a span with the specified name, kind, and parent context
+ *
+ * @param displayName the name of the span
+ * @param spanKind the kind of the span
+ * @param context the parent context
+ * @return the created span, or null if creation failed
+ */
+ private static Span createAndStartSpan(String displayName, SpanKind spanKind, Context context) {
+ if (tracer == null || displayName == null || displayName.isEmpty()) {
+ return null;
+ }
+
+ SpanBuilder builder = tracer.spanBuilder(displayName);
+ if (spanKind != null) {
+ builder.setSpanKind(spanKind);
+ }
+
+ if (context != null) {
+ builder.setParent(context);
+ } else {
+ builder.setNoParent();
+ }
+
+ return builder.startSpan();
+ }
+
+ /**
+ * Creates and starts a span with the specified name, kind, parent context, and linked spans
+ *
+ * @param displayName the name of the span
+ * @param spanKind the kind of the span
+ * @param context the parent context
+ * @param gxSpanContexts an iterator of span contexts to link
+ * @return the created span, or null if creation failed
+ */
+ private static Span createAndStartSpan(
+ String displayName,
+ SpanKind spanKind,
+ Context context,
+ Iterator gxSpanContexts
+ ) {
+ if (tracer == null || displayName == null || displayName.isEmpty() || gxSpanContexts == null) {
+ return null;
+ }
+
+ SpanBuilder spanBuilder = tracer.spanBuilder(displayName);
+
+ if (spanKind != null) {
+ spanBuilder.setSpanKind(spanKind);
+ }
+
+ if (context != null) {
+ spanBuilder.setParent(context);
+ } else {
+ spanBuilder.setNoParent();
+ }
+
+ while (gxSpanContexts.hasNext()) {
+ GXSpanContext spanContext = gxSpanContexts.next();
+ if (spanContext != null && spanContext.getSpanContext() != null) {
+ spanBuilder.addLink(spanContext.getSpanContext());
}
- return spanBuilder.startSpan();
}
- return null;
+
+ return spanBuilder.startSpan();
}
- private static io.opentelemetry.api.trace.SpanKind toSpanKind (Byte spanTypeByte){
+ /**
+ * Converts a byte value to a SpanKind
+ *
+ * @param spanTypeByte the span type as a byte
+ * @return the corresponding SpanKind, or null if invalid
+ */
+ private static SpanKind toSpanKind(Byte spanTypeByte) {
+ if (spanTypeByte == null) {
+ return null;
+ }
+
switch (spanTypeByte) {
case 0:
- return io.opentelemetry.api.trace.SpanKind.INTERNAL;
+ return SpanKind.INTERNAL;
case 1:
- return io.opentelemetry.api.trace.SpanKind.SERVER;
+ return SpanKind.SERVER;
case 2:
- return io.opentelemetry.api.trace.SpanKind.CLIENT;
+ return SpanKind.CLIENT;
case 3:
- return io.opentelemetry.api.trace.SpanKind.PRODUCER;
+ return SpanKind.PRODUCER;
case 4:
- return io.opentelemetry.api.trace.SpanKind.CONSUMER;
+ return SpanKind.CONSUMER;
+ default:
+ return null;
}
- return null;
}
//endregion
-
}
\ No newline at end of file
diff --git a/gxobservability/src/test/java/com/genexus/opentelemetry/ExtractAttributeValueTest.java b/gxobservability/src/test/java/com/genexus/opentelemetry/ExtractAttributeValueTest.java
new file mode 100644
index 000000000..cf9d934f4
--- /dev/null
+++ b/gxobservability/src/test/java/com/genexus/opentelemetry/ExtractAttributeValueTest.java
@@ -0,0 +1,38 @@
+package com.genexus.opentelemetry;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ExtractAttributeValueTest {
+
+ @Test
+ void testExtractAttributeValue() {
+ // Test case 1: Test with null parameters
+ String attributeValue = ExtractAttributeValueHelper.extractAttributeValue(null, "service.name");
+ assertNull(attributeValue, "Should return null for null resource attributes");
+
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue("service.name=test", null);
+ assertNull(attributeValue, "Should return null for null attribute name");
+
+ // Test case 2: Empty resource attributes
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue("", "service.name");
+ assertNull(attributeValue, "Should return null for empty resource attributes");
+
+ // Test case 3: Simple key-value pair
+ String simpleAttributes = "service.name=test-service";
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue(simpleAttributes, "service.name");
+ assertEquals("test-service", attributeValue, "Should extract simple attribute correctly");
+
+ // Test case 4: Multiple key-value pairs
+ String multiAttributes = "service.name=my-service,service.version=1.0.0";
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue(multiAttributes, "service.name");
+ assertEquals("my-service", attributeValue, "Should extract first attribute correctly");
+
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue(multiAttributes, "service.version");
+ assertEquals("1.0.0", attributeValue, "Should extract second attribute correctly");
+
+ // Test case 5: Non-existent attribute
+ attributeValue = ExtractAttributeValueHelper.extractAttributeValue(multiAttributes, "non.existent");
+ assertNull(attributeValue, "Should return null for non-existent attribute");
+ }
+}
\ No newline at end of file
diff --git a/gxobservability/src/test/java/com/genexus/opentelemetry/GXSpanContextTest.java b/gxobservability/src/test/java/com/genexus/opentelemetry/GXSpanContextTest.java
new file mode 100644
index 000000000..17ca1c11b
--- /dev/null
+++ b/gxobservability/src/test/java/com/genexus/opentelemetry/GXSpanContextTest.java
@@ -0,0 +1,83 @@
+package com.genexus.opentelemetry;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanId;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.samplers.Sampler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class GXSpanContextTest {
+
+ private SdkTracerProvider tracerProvider;
+ private io.opentelemetry.api.trace.Tracer tracer;
+
+ @BeforeEach
+ void setUp() {
+ // Create a tracer provider for testing
+ tracerProvider = SdkTracerProvider.builder()
+ .setSampler(Sampler.alwaysOn())
+ .build();
+
+ // Get a tracer from the provider
+ tracer = tracerProvider.get("GXSpanContextTest");
+ }
+
+ @Test
+ void testConstructorWithSpanContext() {
+ // Create a valid SpanContext
+ String traceId = TraceId.fromLongs(1234, 5678);
+ String spanId = SpanId.fromLong(4321);
+ TraceFlags traceFlags = TraceFlags.getSampled();
+ TraceState traceState = TraceState.getDefault();
+
+ SpanContext spanContext = SpanContext.create(traceId, spanId, traceFlags, traceState);
+
+ // Create GXSpanContext with the SpanContext
+ GXSpanContext gxSpanContext = new GXSpanContext(spanContext);
+
+ // Verify GXSpanContext has the correct values
+ assertEquals(traceId, gxSpanContext.traceId());
+ assertEquals(spanId, gxSpanContext.spanId());
+ assertEquals(spanContext, gxSpanContext.getSpanContext());
+ }
+
+ @Test
+ void testDefaultConstructor() {
+ // Create a span with known values
+ String expectedTraceId = TraceId.fromLongs(8765, 4321);
+ String expectedSpanId = SpanId.fromLong(9876);
+ TraceFlags traceFlags = TraceFlags.getSampled();
+ TraceState traceState = TraceState.getDefault();
+
+ // Create a span context with our expected values
+ SpanContext expectedSpanContext = SpanContext.create(
+ expectedTraceId,
+ expectedSpanId,
+ traceFlags,
+ traceState
+ );
+
+ // Create a span with this context
+ Span span = Span.wrap(expectedSpanContext);
+
+ // Make this the current span in the context
+ try (Scope scope = Context.current().with(span).makeCurrent()) {
+ // Create GXSpanContext with the default constructor - should use the current span
+ GXSpanContext gxSpanContext = new GXSpanContext();
+
+ // Verify GXSpanContext has the same values as our test span
+ assertNotNull(gxSpanContext);
+ assertNotNull(gxSpanContext.getSpanContext());
+ assertEquals(expectedTraceId, gxSpanContext.traceId());
+ assertEquals(expectedSpanId, gxSpanContext.spanId());
+ }
+ }
+}
\ No newline at end of file
diff --git a/gxobservability/src/test/java/com/genexus/opentelemetry/GXTraceContextTest.java b/gxobservability/src/test/java/com/genexus/opentelemetry/GXTraceContextTest.java
new file mode 100644
index 000000000..91c7cc5a8
--- /dev/null
+++ b/gxobservability/src/test/java/com/genexus/opentelemetry/GXTraceContextTest.java
@@ -0,0 +1,20 @@
+package com.genexus.opentelemetry;
+
+import io.opentelemetry.context.Context;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class GXTraceContextTest {
+
+ @Test
+ void testConstructorWithContext() {
+ // Create a root context
+ Context context = Context.root();
+
+ // Create GXTraceContext with the context
+ GXTraceContext gxTraceContext = new GXTraceContext(context);
+
+ // Verify GXTraceContext has the correct context
+ assertEquals(context, gxTraceContext.getTraceContext());
+ }
+}
\ No newline at end of file
diff --git a/gxobservability/src/test/java/com/genexus/opentelemetry/OtelSpanTest.java b/gxobservability/src/test/java/com/genexus/opentelemetry/OtelSpanTest.java
new file mode 100644
index 000000000..7e0203540
--- /dev/null
+++ b/gxobservability/src/test/java/com/genexus/opentelemetry/OtelSpanTest.java
@@ -0,0 +1,136 @@
+package com.genexus.opentelemetry;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanId;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.TraceState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class OtelSpanTest {
+
+ private Span mockSpan;
+ private SpanContext mockSpanContext;
+ private OtelSpan otelSpan;
+ private final String traceId = TraceId.fromLongs(1234, 5678);
+ private final String spanId = SpanId.fromLong(4321);
+
+ @BeforeEach
+ void setUp() {
+ // Create mock span context
+ mockSpanContext = SpanContext.create(
+ traceId,
+ spanId,
+ TraceFlags.getSampled(),
+ TraceState.getDefault()
+ );
+
+ // Create mock span
+ mockSpan = Mockito.mock(Span.class);
+ when(mockSpan.getSpanContext()).thenReturn(mockSpanContext);
+ when(mockSpan.isRecording()).thenReturn(true);
+
+ // Create OtelSpan with mock span
+ otelSpan = new OtelSpan(mockSpan);
+ }
+
+ @Test
+ void testGetTraceId() {
+ assertEquals(traceId, otelSpan.getTraceId());
+
+ // Test with null span
+ OtelSpan nullSpan = new OtelSpan(null);
+ assertEquals("", nullSpan.getTraceId());
+ }
+
+ @Test
+ void testGetSpanId() {
+ assertEquals(spanId, otelSpan.getSpanId());
+
+ // Test with null span
+ OtelSpan nullSpan = new OtelSpan(null);
+ assertEquals("", nullSpan.getSpanId());
+ }
+
+ @Test
+ void testIsRecording() {
+ assertTrue(otelSpan.isRecording());
+
+ // Test with null span
+ OtelSpan nullSpan = new OtelSpan(null);
+ assertFalse(nullSpan.isRecording());
+ }
+
+ @Test
+ void testSetStringAttribute() {
+ String key = "string.key";
+ String value = "string value";
+
+ otelSpan.setStringAttribute(key, value);
+ verify(mockSpan, times(1)).setAttribute(key, value);
+
+ // Test with null key or value (should not call setAttribute)
+ reset(mockSpan);
+ otelSpan.setStringAttribute(null, value);
+ otelSpan.setStringAttribute(key, null);
+ verify(mockSpan, never()).setAttribute(anyString(), anyString());
+ }
+
+ @Test
+ void testSetBooleanAttribute() {
+ String key = "boolean.key";
+ boolean value = true;
+
+ otelSpan.setBooleanAttribute(key, value);
+ verify(mockSpan, times(1)).setAttribute(key, value);
+
+ // Test with null key (should not call setAttribute)
+ reset(mockSpan);
+ otelSpan.setBooleanAttribute(null, value);
+ verify(mockSpan, never()).setAttribute(anyString(), anyBoolean());
+ }
+
+ @Test
+ void testSetStatus() {
+ Byte errorStatus = 2; // ERROR
+
+ otelSpan.setStatus(errorStatus);
+ verify(mockSpan, times(1)).setStatus(io.opentelemetry.api.trace.StatusCode.ERROR);
+
+ // Test with null status code (should not call setStatus)
+ reset(mockSpan);
+ otelSpan.setStatus(null);
+ verify(mockSpan, never()).setStatus(any(io.opentelemetry.api.trace.StatusCode.class));
+ }
+
+ @Test
+ void testSetStatusWithMessage() {
+ Byte okStatus = 1; // OK
+ String message = "Status message";
+
+ otelSpan.setStatus(okStatus, message);
+ verify(mockSpan, times(1)).setStatus(io.opentelemetry.api.trace.StatusCode.OK, message);
+
+ // Test with null inputs (should not call setStatus)
+ reset(mockSpan);
+ otelSpan.setStatus(null, message);
+ otelSpan.setStatus(okStatus, null);
+ verify(mockSpan, never()).setStatus(any(io.opentelemetry.api.trace.StatusCode.class), anyString());
+ }
+
+ @Test
+ void testEndSpan() {
+ otelSpan.endSpan();
+ verify(mockSpan, times(1)).end();
+
+ // Test with null span (should not throw exception)
+ OtelSpan nullSpan = new OtelSpan(null);
+ nullSpan.endSpan(); // Should not throw exception
+ }
+}
\ No newline at end of file
diff --git a/gxobservability/src/test/java/com/genexus/opentelemetry/OtelTracerTest.java b/gxobservability/src/test/java/com/genexus/opentelemetry/OtelTracerTest.java
new file mode 100644
index 000000000..ba18b78bf
--- /dev/null
+++ b/gxobservability/src/test/java/com/genexus/opentelemetry/OtelTracerTest.java
@@ -0,0 +1,193 @@
+package com.genexus.opentelemetry;
+
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanId;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceId;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.context.Context;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.lang.reflect.Method;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class OtelTracerTest {
+
+ // These tests can't properly test the static initialization of OtelTracer
+ // because GlobalOpenTelemetry is hard to mock. We'll focus on testing the public methods.
+
+ @Test
+ void testCreateSpan() {
+ // This is a basic test of the public API
+ // Real functionality is difficult to test without integration tests
+ OtelTracer tracer = new OtelTracer();
+ OtelSpan span = tracer.createSpan("test-span");
+
+ // We can't make strong assertions because of the static initialization
+ // Just verify no exceptions are thrown
+ assertNotNull(span);
+ }
+
+ @Test
+ void testCreateSpanWithType() {
+ OtelTracer tracer = new OtelTracer();
+ Byte spanType = 1; // SERVER
+ OtelSpan span = tracer.createSpan("test-span-with-type", spanType);
+
+ assertNotNull(span);
+ }
+
+ @Test
+ void testCreateSpanWithContext() {
+ OtelTracer tracer = new OtelTracer();
+ Byte spanType = 2; // CLIENT
+
+ // Create a mock context
+ Context context = Context.root();
+ GXTraceContext gxContext = new GXTraceContext(context);
+
+ OtelSpan span = tracer.createSpan("test-span-with-context", gxContext, spanType);
+
+ assertNotNull(span);
+ }
+
+ @Test
+ void testCreateSpanWithLinkedSpans() {
+ OtelTracer tracer = new OtelTracer();
+ Byte spanType = 3; // PRODUCER
+
+ // Create a mock context
+ Context context = Context.root();
+ GXTraceContext gxContext = new GXTraceContext(context);
+
+ // Create a mock span context
+ String traceId = TraceId.fromLongs(1234, 5678);
+ String spanId = SpanId.fromLong(4321);
+ SpanContext spanContext = SpanContext.create(
+ traceId,
+ spanId,
+ TraceFlags.getSampled(),
+ TraceState.getDefault()
+ );
+
+ GXSpanContext gxSpanContext = new GXSpanContext(spanContext);
+ List linkedSpans = new ArrayList<>();
+ linkedSpans.add(gxSpanContext);
+
+ OtelSpan span = tracer.createSpan(
+ "test-span-with-links",
+ gxContext,
+ spanType,
+ linkedSpans.iterator()
+ );
+
+ assertNotNull(span);
+ }
+
+ @Test
+ void testGetCurrentSpan() {
+ OtelSpan span = OtelTracer.getCurrentSpan();
+ assertNotNull(span);
+ }
+
+ @Test
+ void testCreateSpanWithNullContext() {
+ OtelTracer tracer = new OtelTracer();
+ Byte spanType = 2; // CLIENT
+
+ // Pass null context - should not throw exception
+ OtelSpan span = tracer.createSpan("test-span-with-null-context", null, spanType);
+
+ assertNotNull(span);
+ }
+
+ @Test
+ void testCreateSpanWithNullIterator() {
+ OtelTracer tracer = new OtelTracer();
+ Byte spanType = 3; // PRODUCER
+
+ Context context = Context.root();
+ GXTraceContext gxContext = new GXTraceContext(context);
+
+ // Pass null iterator - should fall back to simpler method
+ OtelSpan span = tracer.createSpan(
+ "test-span-with-null-iterator",
+ gxContext,
+ spanType,
+ null
+ );
+
+ assertNotNull(span);
+ }
+
+ @Test
+ void testExtractAttributeValue() throws Exception {
+ // Get access to the private method using reflection
+ Method extractAttributeValueMethod = OtelTracer.class.getDeclaredMethod(
+ "extractAttributeValue",
+ String.class,
+ String.class
+ );
+ extractAttributeValueMethod.setAccessible(true);
+
+ // Test case 1: Test with null parameters
+ String attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ null,
+ "service.name"
+ );
+ assertNull(attributeValue, "Should return null for null resource attributes");
+
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ "service.name=test",
+ null
+ );
+ assertNull(attributeValue, "Should return null for null attribute name");
+
+ // Test case 2: Empty resource attributes
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ "",
+ "service.name"
+ );
+ assertNull(attributeValue, "Should return null for empty resource attributes");
+
+ // Test case 3: Simple key-value pair
+ String simpleAttributes = "service.name=test-service";
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ simpleAttributes,
+ "service.name"
+ );
+ assertEquals("test-service", attributeValue, "Should extract simple attribute correctly");
+
+ // Test case 4: Multiple key-value pairs
+ String multiAttributes = "service.name=my-service,service.version=1.0.0";
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ multiAttributes,
+ "service.name"
+ );
+ assertEquals("my-service", attributeValue, "Should extract first attribute correctly");
+
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ multiAttributes,
+ "service.version"
+ );
+ assertEquals("1.0.0", attributeValue, "Should extract second attribute correctly");
+
+ // Test case 5: Non-existent attribute
+ attributeValue = (String) extractAttributeValueMethod.invoke(
+ null,
+ multiAttributes,
+ "non.existent"
+ );
+ assertNull(attributeValue, "Should return null for non-existent attribute");
+ }
+}
\ No newline at end of file