/* Mangadex@Home Copyright (c) 2020, MangaDex Network This file is part of MangaDex@Home. MangaDex@Home is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. MangaDex@Home is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this MangaDex@Home. If not, see . */ package mdnet.server import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry import io.github.resilience4j.micrometer.tagged.TaggedCircuitBreakerMetrics import io.micrometer.core.instrument.FunctionCounter import io.micrometer.prometheus.PrometheusMeterRegistry import mdnet.cache.ImageStorage import mdnet.data.Statistics import mdnet.logging.info import mdnet.logging.warn import mdnet.metrics.GeoIpMetricsFilterBuilder import mdnet.metrics.PostTransactionLabeler import mdnet.netty.Netty import mdnet.settings.MetricsSettings import mdnet.settings.RemoteSettings import mdnet.settings.ServerSettings import org.http4k.core.* import org.http4k.filter.* import org.http4k.routing.* import org.http4k.server.Http4kServer import org.http4k.server.asServer import org.slf4j.LoggerFactory import java.time.Duration fun getServer( storage: ImageStorage, remoteSettings: RemoteSettings, serverSettings: ServerSettings, metricsSettings: MetricsSettings, statistics: Statistics, registry: PrometheusMeterRegistry, client: HttpHandler ): Http4kServer { val circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults() TaggedCircuitBreakerMetrics .ofCircuitBreakerRegistry(circuitBreakerRegistry) .bindTo(registry) val circuitBreaker = circuitBreakerRegistry.circuitBreaker( "upstream", CircuitBreakerConfig.custom() .slidingWindow(50, 20, CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) .permittedNumberOfCallsInHalfOpenState(10) .slowCallDurationThreshold(Duration.ofSeconds(20)) .waitDurationInOpenState(Duration.ofSeconds(20)) .build() ) circuitBreaker.eventPublisher.onFailureRateExceeded { LOGGER.warn { "Circuit breaker has exceeded failure rate" } } circuitBreaker.eventPublisher.onSlowCallRateExceeded { LOGGER.warn { "Circuit breaker has exceeded slow call rate" } } circuitBreaker.eventPublisher.onReset { LOGGER.warn { "Circuit breaker has rest" } } circuitBreaker.eventPublisher.onStateTransition { LOGGER.warn { "Circuit breaker has moved from ${it.stateTransition.fromState} to ${it.stateTransition.toState}" } } val circuited = ResilienceFilters.CircuitBreak( circuitBreaker, isError = { r: Response -> !r.status.successful } ) val upstream = ClientFilters.MicrometerMetrics.RequestTimer(registry) .then(ClientFilters.SetBaseUriFrom(remoteSettings.imageServer)) .then(circuited) .then(client) val imageServer = ImageServer( storage = storage, upstream = upstream, registry = registry ) FunctionCounter.builder( "client_sent_bytes", statistics, { it.bytesSent.get().toDouble() } ).register(registry) val verifier = TokenVerifier( tokenKey = remoteSettings.tokenKey, ) return timeRequest() .then(addCommonHeaders()) .then(catchAllHideDetails()) .then( routes( "/{token}/data/{chapterHash}/{fileName}" bind Method.GET to verifier.then( imageServer.handler( dataSaver = false, ) ), "/{token}/data-saver/{chapterHash}/{fileName}" bind Method.GET to verifier.then( imageServer.handler( dataSaver = true, ) ), "/data/{chapterHash}/{fileName}" bind Method.GET to verifier.then( imageServer.handler( dataSaver = false, ) ), "/data-saver/{chapterHash}/{fileName}" bind Method.GET to verifier.then( imageServer.handler( dataSaver = true, ) ), "/prometheus" bind Method.GET to { Response(Status.OK).body(registry.scrape()) }, ).withFilter( ServerFilters.MicrometerMetrics.RequestTimer(registry, labeler = PostTransactionLabeler()) ).withFilter( GeoIpMetricsFilterBuilder(metricsSettings.enableGeoip, metricsSettings.geoipLicenseKey, registry).build() ) ) .asServer(Netty(remoteSettings.tls!!, serverSettings, statistics)) } private val LOGGER = LoggerFactory.getLogger(ImageServer::class.java) fun timeRequest(): Filter { return Filter { next: HttpHandler -> { request: Request -> val cleanedUri = request.uri.path.replaceBefore("/data", "/{token}") LOGGER.info { "Request for $cleanedUri received" } val start = System.currentTimeMillis() val response = next(request) val latency = System.currentTimeMillis() - start LOGGER.info { "Request for $cleanedUri completed (TTFB) in ${latency}ms" } response } } }