/* 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.core.instrument.binder.BaseUnits 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.DevSettings 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, devSettings: DevSettings, 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 reset" } } 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.serverError } ) 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", statistics ) { it.bytesSent.get().toDouble() } .baseUnit(BaseUnits.BYTES).register(registry) val verifier = TokenVerifier( tokenKey = remoteSettings.tokenKey, isDisabled = devSettings.disableTokenValidation, ) if (devSettings.disableTokenValidation) { LOGGER.warn { "Token validation has been explicitly disabled. This should only be used for testing!" } } return timeRequest() .then(addCommonHeaders(devSettings.sendServerHeader)) .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, serverSettings, devSettings, 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 } } }