TracingConfiguration.kt

package org.openapitools.configuration

import io.micrometer.observation.ObservationPredicate
import io.micrometer.observation.ObservationRegistry
import io.micrometer.tracing.handler.DefaultTracingObservationHandler
import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler
import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler
import io.micrometer.tracing.otel.bridge.OtelBaggageManager
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext
import io.micrometer.tracing.otel.bridge.OtelPropagator
import io.micrometer.tracing.otel.bridge.OtelTracer
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
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.sdk.trace.samplers.Sampler
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.micrometer.observation.autoconfigure.ObservationRegistryCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.server.observation.ServerRequestObservationContext

// Spring Boot 4.x removed tracing autoconfiguration; this class replaces it.
@Configuration(proxyBeanMethods = false)
class TracingConfiguration(
	@Value("\${spring.application.name:spring-api}")
	private val serviceName: String,
	@Value("\${management.otlp.tracing.endpoint:http://alloy.monitoring.svc.cluster.local:4318/v1/traces}")
	private val otlpEndpoint: String,
	@Value("\${management.tracing.sampling.probability:0.1}")
	private val samplingProbability: Double,
) {
	@Bean
	fun otlpHttpSpanExporter(): OtlpHttpSpanExporter = OtlpHttpSpanExporter.builder().setEndpoint(otlpEndpoint).build()

	@Bean
	fun sdkTracerProvider(spanExporter: OtlpHttpSpanExporter): SdkTracerProvider {
		val resource =
			Resource.getDefault().merge(
				Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), serviceName)),
			)
		return SdkTracerProvider
			.builder()
			.setResource(resource)
			.setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(samplingProbability)))
			.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
			.build()
	}

	@Bean
	fun openTelemetrySdk(tracerProvider: SdkTracerProvider): OpenTelemetrySdk =
		OpenTelemetrySdk
			.builder()
			.setTracerProvider(tracerProvider)
			.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
			.build()

	@Bean
	fun otelCurrentTraceContext(): OtelCurrentTraceContext = OtelCurrentTraceContext()

	@Bean
	fun otelBaggageManager(currentTraceContext: OtelCurrentTraceContext): OtelBaggageManager =
		OtelBaggageManager(currentTraceContext, emptyList(), emptyList())

	@Bean
	fun otelTracer(
		openTelemetry: OpenTelemetrySdk,
		currentTraceContext: OtelCurrentTraceContext,
		baggageManager: OtelBaggageManager,
	): OtelTracer =
		OtelTracer(
			openTelemetry.getTracer("io.micrometer.tracing.otel"),
			currentTraceContext,
			OtelTracer.EventPublisher { },
			baggageManager,
		)

	@Bean
	fun otelPropagator(openTelemetry: OpenTelemetrySdk): OtelPropagator =
		OtelPropagator(openTelemetry.propagators, openTelemetry.getTracer("io.micrometer.tracing.otel"))

	@Bean
	fun noNoisyObservations(): ObservationPredicate =
		ObservationPredicate { name, context ->
			if (name.startsWith("spring.security")) return@ObservationPredicate false
			if (name == "http.server.requests" && context is ServerRequestObservationContext) {
				context.carrier?.requestURI?.startsWith("/actuator/") != true
			} else {
				true
			}
		}

	@Bean
	fun tracingObservationRegistryCustomizer(
		tracer: OtelTracer,
		propagator: OtelPropagator,
		noNoisyObservations: ObservationPredicate,
	): ObservationRegistryCustomizer<ObservationRegistry> =
		ObservationRegistryCustomizer { registry ->
			registry
				.observationConfig()
				.observationPredicate(noNoisyObservations)
				.observationHandler(
					io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler(
						PropagatingReceiverTracingObservationHandler(tracer, propagator),
						PropagatingSenderTracingObservationHandler(tracer, propagator),
						DefaultTracingObservationHandler(tracer),
					),
				)
		}
}