RequestLoggingAutoConfiguration.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.context.ContextRegistry
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
import org.springframework.beans.factory.InitializingBean
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.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment

/**
 * Registers the [RequestLoggingWebFilter] in a REACTIVE (WebFlux) Spring Boot application - drop the
 * module on the classpath and every exchange is logged; `endpoint-logging.enabled=false` removes it
 * again. The property namespace matches limesium-servlet-logging's key for key, plus the reactive-only
 * `endpoint-logging.variant` selector; the two auto-configurations can never clash, as each is
 * conditional on its own web-application type.
 *
 * Every bean backs off to a host-provided one. The meter registry arrives as an [ObjectProvider] and is
 * CONSUMED, never exported - a logging library must not define the host's `MeterRegistry`; without one
 * (no actuator) a private [SimpleMeterRegistry] absorbs the counts and the module works unchanged. WebFlux picks the
 * `WebFilter` bean up automatically and orders it via its [org.springframework.core.Ordered] contract.
 */
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnProperty(prefix = "endpoint-logging", name = ["enabled"], havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(RequestLoggingProperties::class)
class RequestLoggingAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    fun requestLoggingNanoTimeSource(): NanoTimeSource = NanoTimeSource.SYSTEM

    @Bean
    @ConditionalOnMissingBean
    fun requestLoggingCorrelationIdGenerator(): CorrelationIdGenerator = CorrelationIdGenerator.DEFAULT

    /** How masked header values render - a host pins a keyed or fixed masker; both variants and both twins take the same bean. */
    @Bean
    @ConditionalOnMissingBean
    fun requestLoggingHeaderValueMasker(properties: RequestLoggingProperties): HeaderValueMasker = HeaderValueMasker.forKey(properties.maskingKey.value)

    /**
     * The Reactor variant, registered only when NO [EndpointLoggingFilter] exists yet: the coroutine
     * auto-configuration runs BEFORE this one and claims the slot when the coroutine libraries are
     * present, and a host-defined bean of either variant backs both off.
     *
     * `endpoint-logging.variant=coroutine` reaching this method means the coroutine variant was demanded
     * but did not register (its libraries are missing): that fails the context start with a message
     * naming the missing libraries, instead of silently serving the other variant.
     */
    @Bean
    @ConditionalOnMissingBean(EndpointLoggingFilter::class)
    fun requestLoggingWebFilter(
        properties: RequestLoggingProperties,
        nanoTime: NanoTimeSource,
        correlationIds: CorrelationIdGenerator,
        masker: HeaderValueMasker,
        meterRegistry: ObjectProvider<MeterRegistry>,
    ): RequestLoggingWebFilter {
        check(properties.variant != Variant.COROUTINE) {
            "endpoint-logging.variant=coroutine requires kotlinx-coroutines-reactor and kotlinx-coroutines-slf4j " +
                "on the classpath; neither a coroutine filter nor those libraries were found"
        }
        return RequestLoggingWebFilter(properties, nanoTime, correlationIds, meterRegistry.getIfAvailable { SimpleMeterRegistry() }, masker)
    }

    /**
     * Handler-MDC parity with the servlet twin: with `io.micrometer:context-propagation` on the classpath
     * (an optional dependency - no extra `endpoint-logging.*` key) the `endpoint_*` accessors are
     * registered and the propagation-mode prerequisite is validated at startup - see
     * [EndpointMdcContextPropagation] for the mechanism and the prerequisite.
     */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(ContextRegistry::class)
    class MdcContextPropagationConfiguration {
        /**
         * Runs only while the REACTOR variant owns the filter slot (the module's own or a host-defined
         * [RequestLoggingWebFilter]): the accessors read the Reactor context THAT variant writes, and the
         * propagation-mode warning is about that variant's handler MDC. With the coroutine variant
         * active (handler MDC natively via `MDCContext`) or a host filter of another type, nothing here
         * applies and a startup warning would be false noise. Resolved at initialization time rather than by
         * `@ConditionalOnBean`, whose evaluation order against the sibling bean methods is not guaranteed.
         * Resolved over ALL filter beans (`stream()`), never through `getIfAvailable()`: that call throws
         * `NoUniqueBeanDefinitionException` for two host-defined filters - a constellation the module
         * otherwise permits - and would fail the context start from a logging library.
         */
        @Bean
        fun endpointMdcContextPropagationInitializer(
            environment: Environment,
            activeFilter: ObjectProvider<EndpointLoggingFilter>,
        ): InitializingBean =
            InitializingBean {
                if (activeFilter.stream().anyMatch { it is RequestLoggingWebFilter }) {
                    EndpointMdcContextPropagation.registerAccessors()
                    EndpointMdcContextPropagation.warnUnlessAutomaticPropagation(
                        environment.getProperty(EndpointMdcContextPropagation.PROPAGATION_MODE_PROPERTY),
                    )
                }
            }
    }
}