CoRequestLoggingAutoConfiguration.kt

package eu.inqudium.limesium.reactive.logging

import eu.inqudium.limesium.common.CorrelationIdGenerator
import eu.inqudium.limesium.common.HeaderValueMasker
import eu.inqudium.limesium.common.NanoTimeSource
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
import kotlinx.coroutines.slf4j.MDCContext
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Conditional
import org.springframework.context.annotation.ConfigurationCondition
import org.springframework.web.server.CoWebFilter

/**
 * Registers the COROUTINE variant ([CoRequestLoggingWebFilter]) when the coroutine libraries are present
 * - `kotlinx-coroutines-reactor` (for [CoWebFilter]) and `kotlinx-coroutines-slf4j` (for [MDCContext]),
 * both optional dependencies of this module; their presence is the whole opt-in, so the
 * `endpoint-logging.*` namespace stays identical across the variants - and, apart from the
 * reactive-only `variant` key, across the twins.
 *
 * Ordered BEFORE [RequestLoggingAutoConfiguration]: whichever variant registers first claims the
 * [EndpointLoggingFilter] slot, and the other backs off via `@ConditionalOnMissingBean` - exactly one
 * filter is ever active. The time-source, id-generator and header-masker defaults still come from the
 * main auto-configuration (bean creation is independent of registration order).
 *
 * The classpath-based choice can be overridden explicitly: `endpoint-logging.variant=reactor` makes
 * this configuration back off although the libraries are present (see [NotForcedToReactor]);
 * `endpoint-logging.variant=coroutine` is enforced by the Reactor auto-configuration, which refuses to
 * register its fallback when the coroutine variant was demanded but its libraries are missing.
 */
@AutoConfiguration(before = [RequestLoggingAutoConfiguration::class])
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnProperty(prefix = "endpoint-logging", name = ["enabled"], havingValue = "true", matchIfMissing = true)
@ConditionalOnClass(CoWebFilter::class, MDCContext::class)
@Conditional(CoRequestLoggingAutoConfiguration.NotForcedToReactor::class)
@EnableConfigurationProperties(RequestLoggingProperties::class)
class CoRequestLoggingAutoConfiguration {
    /** Matches unless `endpoint-logging.variant=reactor` is configured - the explicit opt-out of this variant. */
    class NotForcedToReactor : NoneNestedConditions(ConfigurationCondition.ConfigurationPhase.PARSE_CONFIGURATION) {
        /**
         * The nested POSITIVE condition [NotForcedToReactor] inverts - framework plumbing, not API;
         * Spring reads member conditions from class metadata, so `private` suffices.
         */
        @ConditionalOnProperty(prefix = "endpoint-logging", name = ["variant"], havingValue = "reactor")
        private class ForcedToReactor
    }

    @Bean
    @ConditionalOnMissingBean(EndpointLoggingFilter::class)
    fun coRequestLoggingWebFilter(
        properties: RequestLoggingProperties,
        nanoTime: NanoTimeSource,
        correlationIds: CorrelationIdGenerator,
        masker: HeaderValueMasker,
        meterRegistry: ObjectProvider<MeterRegistry>,
    ): CoRequestLoggingWebFilter = CoRequestLoggingWebFilter(properties, nanoTime, correlationIds, meterRegistry.getIfAvailable { SimpleMeterRegistry() }, masker)
}