feat(api): publish business metrics

This commit is contained in:
Gauthier Roebroeck 2022-02-10 16:21:23 +08:00
parent a8340e816b
commit 78174db6fb
15 changed files with 180 additions and 0 deletions

View file

@ -1,5 +1,6 @@
package org.gotson.komga.application.tasks
import io.micrometer.core.instrument.MeterRegistry
import mu.KotlinLogging
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
@ -16,10 +17,13 @@ import org.gotson.komga.domain.service.SeriesMetadataLifecycle
import org.gotson.komga.infrastructure.jms.QUEUE_FACTORY
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
import org.gotson.komga.infrastructure.search.SearchIndexLifecycle
import org.gotson.komga.interfaces.scheduler.METER_TASKS_EXECUTION
import org.gotson.komga.interfaces.scheduler.METER_TASKS_FAILURE
import org.springframework.jms.annotation.JmsListener
import org.springframework.stereotype.Service
import java.nio.file.Paths
import kotlin.time.measureTime
import kotlin.time.toJavaDuration
private val logger = KotlinLogging.logger {}
@ -39,6 +43,7 @@ class TaskHandler(
private val bookConverter: BookConverter,
private val bookPageEditor: BookPageEditor,
private val searchIndexLifecycle: SearchIndexLifecycle,
private val meterRegistry: MeterRegistry,
) {
@JmsListener(destination = QUEUE_TASKS, containerFactory = QUEUE_FACTORY, concurrency = "#{@komgaProperties.taskConsumers}-#{@komgaProperties.taskConsumersMax}")
@ -151,9 +156,11 @@ class TaskHandler(
}
}.also {
logger.info { "Task $task executed in $it" }
meterRegistry.timer(METER_TASKS_EXECUTION, "type", task.javaClass.simpleName).record(it.toJavaDuration())
}
} catch (e: Exception) {
logger.error(e) { "Task $task execution failed" }
meterRegistry.counter(METER_TASKS_FAILURE, "type", task.javaClass.simpleName).increment()
}
}
}

View file

@ -8,6 +8,7 @@ sealed class DomainEvent : Serializable {
data class LibraryAdded(val library: Library) : DomainEvent()
data class LibraryUpdated(val library: Library) : DomainEvent()
data class LibraryDeleted(val library: Library) : DomainEvent()
data class LibraryScanned(val library: Library) : DomainEvent()
data class SeriesAdded(val series: Series) : DomainEvent()
data class SeriesUpdated(val series: Series) : DomainEvent()

View file

@ -5,6 +5,7 @@ import org.gotson.komga.domain.model.BookSearch
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import java.math.BigDecimal
import java.net.URL
interface BookRepository {
@ -43,4 +44,7 @@ interface BookRepository {
fun deleteAll()
fun count(): Long
fun countGroupedByLibraryName(): Map<String, Int>
fun getFilesizeGroupedByLibraryName(): Map<String, BigDecimal>
}

View file

@ -42,4 +42,6 @@ interface ReadListRepository {
fun deleteAll()
fun existsByName(name: String): Boolean
fun count(): Long
}

View file

@ -42,4 +42,6 @@ interface SeriesCollectionRepository {
fun deleteAll()
fun existsByName(name: String): Boolean
fun count(): Long
}

View file

@ -26,4 +26,5 @@ interface SeriesRepository {
fun deleteAll()
fun count(): Long
fun countGroupedByLibraryName(): Map<String, Int>
}

View file

@ -11,4 +11,6 @@ interface SidecarRepository {
fun deleteByLibraryIdAndUrls(libraryId: String, urls: Collection<URL>)
fun deleteByLibraryId(libraryId: String)
fun countGroupedByLibraryName(): Map<String, Int>
}

View file

@ -231,6 +231,8 @@ class LibraryContentLifecycle(
if (library.emptyTrashAfterScan) emptyTrash(library)
else cleanupEmptySets()
}.also { logger.info { "Library updated in $it" } }
eventPublisher.publishEvent(DomainEvent.LibraryScanned(library))
}
/**

View file

@ -16,6 +16,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.net.URL
import java.time.LocalDateTime
import java.time.ZoneId
@ -29,6 +30,7 @@ class BookDao(
private val m = Tables.MEDIA
private val d = Tables.BOOK_METADATA
private val r = Tables.READ_PROGRESS
private val l = Tables.LIBRARY
private val sorts = mapOf(
"createdDate" to b.CREATED_DATE,
@ -314,6 +316,20 @@ class BookDao(
override fun count(): Long = dsl.fetchCount(b).toLong()
override fun countGroupedByLibraryName(): Map<String, Int> =
dsl.select(l.NAME, DSL.count(b.ID))
.from(l)
.leftJoin(b).on(l.ID.eq(b.LIBRARY_ID))
.groupBy(l.NAME)
.fetchMap(l.NAME, DSL.count(b.ID))
override fun getFilesizeGroupedByLibraryName(): Map<String, BigDecimal> =
dsl.select(l.NAME, DSL.sum(b.FILE_SIZE))
.from(l)
.leftJoin(b).on(l.ID.eq(b.LIBRARY_ID))
.groupBy(l.NAME)
.fetchMap(l.NAME, DSL.sum(b.FILE_SIZE))
private fun BookSearch.toCondition(): Condition {
var c: Condition = DSL.trueCondition()

View file

@ -247,6 +247,8 @@ class ReadListDao(
.where(rl.NAME.equalIgnoreCase(name)),
)
override fun count(): Long = dsl.fetchCount(rl).toLong()
private fun ReadlistRecord.toDomain(bookIds: SortedMap<Int, String>) =
ReadList(
name = name,

View file

@ -247,6 +247,8 @@ class SeriesCollectionDao(
.where(c.NAME.equalIgnoreCase(name)),
)
override fun count(): Long = dsl.fetchCount(c).toLong()
private fun CollectionRecord.toDomain(seriesIds: List<String>) =
SeriesCollection(
name = name,

View file

@ -24,6 +24,7 @@ class SeriesDao(
private val s = Tables.SERIES
private val d = Tables.SERIES_METADATA
private val cs = Tables.COLLECTION_SERIES
private val l = Tables.LIBRARY
override fun findAll(): Collection<Series> =
dsl.selectFrom(s)
@ -136,6 +137,13 @@ class SeriesDao(
override fun count(): Long = dsl.fetchCount(s).toLong()
override fun countGroupedByLibraryName(): Map<String, Int> =
dsl.select(l.NAME, DSL.count(s.ID))
.from(l)
.leftJoin(s).on(l.ID.eq(s.LIBRARY_ID))
.groupBy(l.NAME)
.fetchMap(l.NAME, DSL.count(s.ID))
private fun SeriesSearch.toCondition(): Condition {
var c: Condition = DSL.trueCondition()

View file

@ -6,6 +6,7 @@ import org.gotson.komga.domain.persistence.SidecarRepository
import org.gotson.komga.jooq.Tables
import org.gotson.komga.jooq.tables.records.SidecarRecord
import org.jooq.DSLContext
import org.jooq.impl.DSL
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
@ -17,6 +18,7 @@ class SidecarDao(
@Value("#{@komgaProperties.database.batchChunkSize}") private val batchSize: Int,
) : SidecarRepository {
private val sc = Tables.SIDECAR
private val l = Tables.LIBRARY
override fun findAll(): Collection<SidecarStored> =
dsl.selectFrom(sc).fetch().map { it.toDomain() }
@ -52,6 +54,13 @@ class SidecarDao(
.execute()
}
override fun countGroupedByLibraryName(): Map<String, Int> =
dsl.select(l.NAME, DSL.count(sc.URL))
.from(l)
.leftJoin(sc).on(l.ID.eq(sc.LIBRARY_ID))
.groupBy(l.NAME)
.fetchMap(l.NAME, DSL.count(sc.URL))
private fun SidecarRecord.toDomain() =
SidecarStored(
url = URL(url),

View file

@ -0,0 +1,121 @@
package org.gotson.komga.interfaces.scheduler
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Gauge
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.MultiGauge
import io.micrometer.core.instrument.MultiGauge.Row
import io.micrometer.core.instrument.Tags
import io.micrometer.core.instrument.Timer
import mu.KotlinLogging
import org.gotson.komga.domain.model.DomainEvent
import org.gotson.komga.domain.persistence.BookRepository
import org.gotson.komga.domain.persistence.LibraryRepository
import org.gotson.komga.domain.persistence.ReadListRepository
import org.gotson.komga.domain.persistence.SeriesCollectionRepository
import org.gotson.komga.domain.persistence.SeriesRepository
import org.gotson.komga.domain.persistence.SidecarRepository
import org.gotson.komga.infrastructure.jms.TOPIC_EVENTS
import org.gotson.komga.infrastructure.jms.TOPIC_FACTORY
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.jms.annotation.JmsListener
import org.springframework.stereotype.Component
import java.util.concurrent.atomic.AtomicLong
private val logger = KotlinLogging.logger {}
private const val LIBRARIES = "libraries"
private const val SERIES = "series"
private const val BOOKS = "books"
private const val BOOKS_FILESIZE = "books.filesize"
private const val COLLECTIONS = "collections"
private const val READLISTS = "readlists"
private const val SIDECARS = "sidecars"
const val METER_TASKS_EXECUTION = "komga.tasks.execution"
const val METER_TASKS_FAILURE = "komga.tasks.failure"
@Component
class MetricsPublisherController(
private val libraryRepository: LibraryRepository,
private val bookRepository: BookRepository,
private val seriesRepository: SeriesRepository,
private val collectionRepository: SeriesCollectionRepository,
private val readListRepository: ReadListRepository,
private val sidecarRepository: SidecarRepository,
private val meterRegistry: MeterRegistry,
) {
init {
Timer.builder(METER_TASKS_EXECUTION)
.description("Task execution time")
.register(meterRegistry)
Counter.builder(METER_TASKS_FAILURE)
.description("Count of failed tasks")
.register(meterRegistry)
}
private final val entitiesMultiTag = listOf(SERIES, BOOKS, BOOKS_FILESIZE, SIDECARS)
private final val entitiesNoTags = listOf(LIBRARIES, COLLECTIONS, READLISTS)
private final val allEntities = entitiesMultiTag + entitiesNoTags
val multiGauges = entitiesMultiTag.associateWith { entity ->
MultiGauge.builder("komga.$entity")
.description("The number of $entity")
.baseUnit("count")
.register(meterRegistry)
}
val noTagGauges = entitiesNoTags.associateWith { entity ->
AtomicLong(0).also { value ->
Gauge.builder("komga.$entity", value) { value.get().toDouble() }
.description("The number of $entity")
.baseUnit("count")
.register(meterRegistry)
}
}
val bookFileSizeGauge = MultiGauge.builder("komga.$BOOKS_FILESIZE")
.description("The cumulated filesize of books")
.baseUnit("bytes")
.register(meterRegistry)
@JmsListener(destination = TOPIC_EVENTS, containerFactory = TOPIC_FACTORY)
private fun pushMetricsOnEvent(event: DomainEvent) {
when (event) {
is DomainEvent.LibraryScanned -> entitiesMultiTag.forEach { pushMetricsCount(it) }
is DomainEvent.LibraryAdded -> noTagGauges[LIBRARIES]?.incrementAndGet()
is DomainEvent.LibraryDeleted -> {
noTagGauges[LIBRARIES]?.decrementAndGet()
entitiesMultiTag.forEach { pushMetricsCount(it) }
}
is DomainEvent.CollectionAdded -> noTagGauges[COLLECTIONS]?.incrementAndGet()
is DomainEvent.CollectionDeleted -> noTagGauges[COLLECTIONS]?.decrementAndGet()
is DomainEvent.ReadListAdded -> noTagGauges[READLISTS]?.incrementAndGet()
is DomainEvent.ReadListDeleted -> noTagGauges[READLISTS]?.decrementAndGet()
else -> Unit
}
}
@EventListener(ApplicationReadyEvent::class)
fun pushAllMetrics() {
allEntities.forEach { pushMetricsCount(it) }
}
private fun pushMetricsCount(entity: String) {
when (entity) {
LIBRARIES -> noTagGauges[LIBRARIES]?.set(libraryRepository.count())
COLLECTIONS -> noTagGauges[COLLECTIONS]?.set(collectionRepository.count())
READLISTS -> noTagGauges[READLISTS]?.set(readListRepository.count())
SERIES -> multiGauges[SERIES]?.register(seriesRepository.countGroupedByLibraryName().map { Row.of(Tags.of("library", it.key), it.value) })
BOOKS -> multiGauges[BOOKS]?.register(bookRepository.countGroupedByLibraryName().map { Row.of(Tags.of("library", it.key), it.value) })
BOOKS_FILESIZE -> bookFileSizeGauge.register(bookRepository.getFilesizeGroupedByLibraryName().map { Row.of(Tags.of("library", it.key), it.value) })
SIDECARS -> multiGauges[SIDECARS]?.register(sidecarRepository.countGroupedByLibraryName().map { Row.of(Tags.of("library", it.key), it.value) })
}
}
}

View file

@ -77,6 +77,7 @@ class SseController(
is DomainEvent.LibraryAdded -> emitSse("LibraryAdded", LibrarySseDto(event.library.id))
is DomainEvent.LibraryUpdated -> emitSse("LibraryChanged", LibrarySseDto(event.library.id))
is DomainEvent.LibraryDeleted -> emitSse("LibraryDeleted", LibrarySseDto(event.library.id))
is DomainEvent.LibraryScanned -> Unit
is DomainEvent.SeriesAdded -> emitSse("SeriesAdded", SeriesSseDto(event.series.id, event.series.libraryId))
is DomainEvent.SeriesUpdated -> emitSse("SeriesChanged", SeriesSseDto(event.series.id, event.series.libraryId))