diff --git a/src/main/java/com/example/oteldemo/config/OtelConfig.java b/src/main/java/com/example/oteldemo/config/OtelConfig.java new file mode 100644 index 00000000000..98f0c83c49b --- /dev/null +++ b/src/main/java/com/example/oteldemo/config/OtelConfig.java @@ -0,0 +1,62 @@ +package com.example.oteldemo.config; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Configuration +public class OtelConfig { + private static final Logger logger = LoggerFactory.getLogger(OtelConfig.class); + + @Value("${otel.service.name:petclinic}") + private String serviceName; + + @Value("${otel.exporter.otlp.endpoint:http://localhost:4317}") + private String endpoint; + + @Bean + public OpenTelemetry openTelemetry() { + logger.info("Initializing OpenTelemetry with endpoint: {}", endpoint); + + try { + Resource resource = Resource.getDefault() + .merge(Resource.create(Attributes.of( + ResourceAttributes.SERVICE_NAME, serviceName + ))); + + OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder() + .setEndpoint(endpoint) + .build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build()) + .setResource(resource) + .build(); + + OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .build(); + + logger.info("OpenTelemetry initialization successful"); + return openTelemetrySdk; + } catch (Exception e) { + logger.error("Failed to initialize OpenTelemetry", e); + // Fallback to no-op implementation + return OpenTelemetry.noop(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/oteldemo/health/OtelHealthIndicator.java b/src/main/java/com/example/oteldemo/health/OtelHealthIndicator.java new file mode 100644 index 00000000000..6acf66343d2 --- /dev/null +++ b/src/main/java/com/example/oteldemo/health/OtelHealthIndicator.java @@ -0,0 +1,37 @@ +package com.example.oteldemo.health; + +import io.opentelemetry.api.OpenTelemetry; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class OtelHealthIndicator implements HealthIndicator { + private final OpenTelemetry openTelemetry; + + public OtelHealthIndicator(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + @Override + public Health health() { + try { + // Check if OpenTelemetry is properly initialized + if (openTelemetry != null && openTelemetry.getTracerProvider() != null) { + return Health.up() + .withDetail("status", "Operational") + .withDetail("implementation", openTelemetry.getClass().getSimpleName()) + .build(); + } else { + return Health.down() + .withDetail("status", "Not initialized") + .build(); + } + } catch (Exception e) { + return Health.down() + .withDetail("status", "Error") + .withDetail("error", e.getMessage()) + .build(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/oteldemo/instrumentation/OtelInstrumentationUtil.java b/src/main/java/com/example/oteldemo/instrumentation/OtelInstrumentationUtil.java new file mode 100644 index 00000000000..b548e696060 --- /dev/null +++ b/src/main/java/com/example/oteldemo/instrumentation/OtelInstrumentationUtil.java @@ -0,0 +1,85 @@ +package com.example.oteldemo.instrumentation; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +@Component +public class OtelInstrumentationUtil { + private static final Logger logger = LoggerFactory.getLogger(OtelInstrumentationUtil.class); + private final Tracer tracer; + + public OtelInstrumentationUtil(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer(getClass().getName()); + } + + /** + * Traces a callable operation with custom attributes + */ + public T traceOperation(String operationName, Map attributes, Callable operation) { + Span span = createSpan(operationName, attributes); + try (Scope scope = span.makeCurrent()) { + T result = operation.call(); + span.setStatus(StatusCode.OK); + return result; + } catch (Exception e) { + span.setStatus(StatusCode.ERROR, e.getMessage()); + logger.error("Error in traced operation: {}", operationName, e); + throw new RuntimeException(e); + } finally { + span.end(); + } + } + + /** + * Traces a supplier operation + */ + public T traceOperation(String operationName, Supplier operation) { + return traceOperation(operationName, Map.of(), () -> operation.get()); + } + + /** + * Creates a new span with the given name and attributes + */ + public Span createSpan(String name, Map attributes) { + Span span = tracer.spanBuilder(name) + .setSpanKind(SpanKind.INTERNAL) + .setParent(Context.current()) + .startSpan(); + + attributes.forEach((key, value) -> span.setAttribute(key, value)); + return span; + } + + /** + * Gets the current span context + */ + public Span getCurrentSpan() { + return Span.current(); + } + + /** + * Adds an event to the current span + */ + public void addEvent(String eventName, Map attributes) { + Span currentSpan = getCurrentSpan(); + if (currentSpan != null) { + currentSpan.addEvent(eventName, attributes.entrySet().stream() + .collect(java.util.stream.Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue() + ))); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f7aef094b4f..12171027a69 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -29,3 +29,12 @@ logging.level.org.springframework=INFO # Maximum time static resources should be cached spring.web.resources.cache.cachecontrol.max-age=12h + +# OpenTelemetry Configuration +otel.service.name=${OTEL_SERVICE_NAME:petclinic} +otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} +otel.traces.sampler=${OTEL_TRACES_SAMPLER:parentbased_always_on} +otel.traces.sampler.arg=${OTEL_TRACES_SAMPLER_ARG:1.0} +otel.metric.export.interval=${OTEL_METRIC_EXPORT_INTERVAL:60000} +otel.propagators=${OTEL_PROPAGATORS:tracecontext,baggage} +otel.resource.attributes=${OTEL_RESOURCE_ATTRIBUTES:service.namespace=petclinic,service.instance.id=${random.uuid}} \ No newline at end of file