diff --git a/build.gradle.kts b/build.gradle.kts index 695043b..1bca65c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,10 @@ dependencies { implementation("ch.qos.logback", "logback-classic", "1.5.19") implementation("io.github.microutils", "kotlin-logging", "2.0.6") + // metrics + implementation("io.ktor", "ktor-server-metrics-micrometer", ktorVersion) + implementation("io.micrometer", "micrometer-registry-prometheus", "1.16.0") + // DI implementation("org.kodein.di", "kodein-di-framework-ktor-server-jvm", "7.28.0") diff --git a/helm/pollapp/templates/servicemonitor.yaml b/helm/pollapp/templates/servicemonitor.yaml new file mode 100644 index 0000000..8345552 --- /dev/null +++ b/helm/pollapp/templates/servicemonitor.yaml @@ -0,0 +1,13 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "pollapp.fullname" . }} + labels: + {{- include "pollapp.labels" . | nindent 4 }} +spec: + endpoints: + - port: http + path: /metrics + selector: + matchLabels: + {{- include "pollapp.selectorLabels" . | nindent 6 }} diff --git a/src/main/kotlin/com/wire/apps/polls/services/MessagesHandlingService.kt b/src/main/kotlin/com/wire/apps/polls/services/MessagesHandlingService.kt index 4329d2a..e58728a 100644 --- a/src/main/kotlin/com/wire/apps/polls/services/MessagesHandlingService.kt +++ b/src/main/kotlin/com/wire/apps/polls/services/MessagesHandlingService.kt @@ -4,6 +4,7 @@ import com.wire.apps.polls.dto.PollAction import com.wire.apps.polls.dto.PollAction.VoteAction import com.wire.apps.polls.dto.PollAction.ShowResultsAction import com.wire.apps.polls.dto.UsersInput +import com.wire.apps.polls.setup.metrics.UsageMetrics import com.wire.sdk.model.QualifiedId import com.wire.sdk.service.WireApplicationManager import mu.KLogging @@ -13,7 +14,8 @@ import mu.KLogging */ class MessagesHandlingService( private val pollService: PollService, - private val userCommunicationService: UserCommunicationService + private val userCommunicationService: UserCommunicationService, + private val usageMetrics: UsageMetrics ) { private companion object : KLogging() @@ -73,11 +75,14 @@ class MessagesHandlingService( } // send version when asked trimmed == "/poll help" -> { + usageMetrics.onHelpCommand() userCommunicationService.sendHelp(manager, conversationId) } - // poll request - trimmed.startsWith("/poll") -> + // poll creation request + trimmed.startsWith("/poll") -> { + usageMetrics.onCreatePollCommand() pollService.createPoll(manager, usersInput) + } // Easter egg, good app is good trimmed == "good app" -> { userCommunicationService.goodApp(manager, conversationId) diff --git a/src/main/kotlin/com/wire/apps/polls/setup/DependencyInjection.kt b/src/main/kotlin/com/wire/apps/polls/setup/DependencyInjection.kt index 01da4a1..b535fbc 100644 --- a/src/main/kotlin/com/wire/apps/polls/setup/DependencyInjection.kt +++ b/src/main/kotlin/com/wire/apps/polls/setup/DependencyInjection.kt @@ -11,7 +11,10 @@ import com.wire.apps.polls.services.PollService import com.wire.apps.polls.services.ProxySenderService import com.wire.apps.polls.services.StatsFormattingService import com.wire.apps.polls.services.UserCommunicationService +import com.wire.apps.polls.setup.metrics.UsageMetrics import com.wire.apps.polls.utils.createLogger +import io.micrometer.prometheusmetrics.PrometheusConfig +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import mu.KLogger import org.kodein.di.DI import org.kodein.di.bind @@ -47,10 +50,18 @@ fun DI.MainBuilder.configureContainer() { bind() with singleton { PollActionMapper(instance()) } bind() with - singleton { MessagesHandlingService(instance(), instance()) } + singleton { MessagesHandlingService(instance(), instance(), instance()) } bind() with singleton { StatsFormattingService(instance()) } + bind() with singleton { + PrometheusMeterRegistry(PrometheusConfig.DEFAULT) + } + + bind() with singleton { + UsageMetrics(instance()) + } + bind("routing-logger") with singleton { createLogger("Routing") } bind("install-logger") with singleton { createLogger("KtorStartup") } } diff --git a/src/main/kotlin/com/wire/apps/polls/setup/KtorInstallation.kt b/src/main/kotlin/com/wire/apps/polls/setup/KtorInstallation.kt index 14b3a6a..1991afc 100644 --- a/src/main/kotlin/com/wire/apps/polls/setup/KtorInstallation.kt +++ b/src/main/kotlin/com/wire/apps/polls/setup/KtorInstallation.kt @@ -6,8 +6,12 @@ import com.wire.apps.polls.setup.conf.DatabaseConfiguration import com.wire.apps.polls.utils.createLogger import io.ktor.http.HttpStatusCode import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.metrics.micrometer.MicrometerMetrics +import io.ktor.server.response.respond import io.ktor.server.routing.get import io.ktor.server.routing.routing +import io.micrometer.prometheusmetrics.PrometheusMeterRegistry import org.flywaydb.core.Flyway import org.kodein.di.instance import org.kodein.di.ktor.closestDI @@ -22,18 +26,40 @@ fun Application.init() { // now kodein is running and can be used installationLogger.debug { "DI container started." } - // connect to the database connectDatabase() + setupHealthEndpoint() + setupEventRouting() + setupMetrics() +} - // register routing +fun Application.setupHealthEndpoint() { routing { get("/health") { call.response.status(HttpStatusCode.OK) } + } +} + +fun Application.setupEventRouting() { + routing { events() } } +fun Application.setupMetrics() { + val prometheusRegistry by closestDI().instance() + + install(MicrometerMetrics) { + registry = prometheusRegistry + } + + routing { + get("/metrics") { + call.respond(prometheusRegistry.scrape()) + } + } +} + /** * Connect app to the database. */ diff --git a/src/main/kotlin/com/wire/apps/polls/setup/metrics/UsageMetrics.kt b/src/main/kotlin/com/wire/apps/polls/setup/metrics/UsageMetrics.kt new file mode 100644 index 0000000..af9f5ef --- /dev/null +++ b/src/main/kotlin/com/wire/apps/polls/setup/metrics/UsageMetrics.kt @@ -0,0 +1,26 @@ +package com.wire.apps.polls.setup.metrics + +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.MeterRegistry + +class UsageMetrics( + registry: MeterRegistry +) { + private val helpCommandCounter: Counter = Counter + .builder("pollapp_help_commands_total") + .description("Number of Poll help commands received") + .register(registry) + + private val createPollCommandCounter: Counter = Counter + .builder("pollapp_create_poll_commands_total") + .description("Number of Poll creation commands received") + .register(registry) + + fun onHelpCommand() { + helpCommandCounter.increment() + } + + fun onCreatePollCommand() { + createPollCommandCounter.increment() + } +} diff --git a/src/test/kotlin/com/wire/apps/polls/services/MessagesHandlingServiceTest.kt b/src/test/kotlin/com/wire/apps/polls/services/MessagesHandlingServiceTest.kt index d5ecef1..a8f9278 100644 --- a/src/test/kotlin/com/wire/apps/polls/services/MessagesHandlingServiceTest.kt +++ b/src/test/kotlin/com/wire/apps/polls/services/MessagesHandlingServiceTest.kt @@ -1,6 +1,7 @@ package com.wire.apps.polls.services import com.wire.apps.polls.setup.configureContainer +import com.wire.apps.polls.setup.metrics.UsageMetrics import com.wire.sdk.service.WireApplicationManager import io.mockk.Called import io.mockk.coVerify @@ -22,10 +23,12 @@ class MessagesHandlingServiceTest { val userCommunicationService = mockk(relaxed = true) val pollService = mockk(relaxed = true) val manager = mockk() + val usageMetrics = mockk(relaxed = true) val testModule = DI.Module("testModule") { bind(overrides = true) with singleton { userCommunicationService } bind(overrides = true) with singleton { pollService } + bind(overrides = true) with singleton { usageMetrics } } val di = DI { @@ -50,6 +53,7 @@ class MessagesHandlingServiceTest { messagesHandlingService.handleUserCommand(manager, usersInput) // assert + coVerify(exactly = 1) { usageMetrics.onCreatePollCommand() } coVerify(exactly = 1) { pollService.createPoll(any(), any()) } verify { userCommunicationService wasNot Called } } @@ -66,6 +70,7 @@ class MessagesHandlingServiceTest { // assert coVerify(exactly = 1) { userCommunicationService.sendVersion(any(), any()) } verify { pollService wasNot Called } + verify { usageMetrics wasNot Called } } @Test @@ -78,6 +83,7 @@ class MessagesHandlingServiceTest { messagesHandlingService.handleUserCommand(manager, usersInput) // assert + coVerify(exactly = 1) { usageMetrics.onHelpCommand() } coVerify(exactly = 1) { userCommunicationService.sendHelp(any(), any()) } verify { pollService wasNot Called } } @@ -94,6 +100,7 @@ class MessagesHandlingServiceTest { // assert coVerify(exactly = 1) { userCommunicationService.goodApp(any(), any()) } verify { pollService wasNot Called } + verify { usageMetrics wasNot Called } } @Test @@ -108,6 +115,7 @@ class MessagesHandlingServiceTest { // assert verify { pollService wasNot Called } verify { userCommunicationService wasNot Called } + verify { usageMetrics wasNot Called } } @ParameterizedTest @@ -129,6 +137,7 @@ class MessagesHandlingServiceTest { // assert coVerify { pollService.createPoll(any(), any()) } + coVerify(exactly = 1) { usageMetrics.onCreatePollCommand() } } @Test @@ -142,6 +151,7 @@ class MessagesHandlingServiceTest { // assert coVerify(exactly = 1) { userCommunicationService.sendHelp(any(), any()) } + coVerify(exactly = 1) { usageMetrics.onHelpCommand() } verify { pollService wasNot Called } } @@ -156,6 +166,7 @@ class MessagesHandlingServiceTest { // assert coVerify(exactly = 1) { userCommunicationService.sendHelp(any(), any()) } + coVerify(exactly = 1) { usageMetrics.onHelpCommand() } verify { pollService wasNot Called } } }