Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
13 changes: 13 additions & 0 deletions helm/pollapp/templates/servicemonitor.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,10 +50,18 @@ fun DI.MainBuilder.configureContainer() {
bind<PollActionMapper>() with singleton { PollActionMapper(instance()) }

bind<MessagesHandlingService>() with
singleton { MessagesHandlingService(instance(), instance()) }
singleton { MessagesHandlingService(instance(), instance(), instance()) }

bind<StatsFormattingService>() with singleton { StatsFormattingService(instance()) }

bind<PrometheusMeterRegistry>() with singleton {
PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
}

bind<UsageMetrics>() with singleton {
UsageMetrics(instance())
}

bind<KLogger>("routing-logger") with singleton { createLogger("Routing") }
bind<KLogger>("install-logger") with singleton { createLogger("KtorStartup") }
}
30 changes: 28 additions & 2 deletions src/main/kotlin/com/wire/apps/polls/setup/KtorInstallation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PrometheusMeterRegistry>()

install(MicrometerMetrics) {
registry = prometheusRegistry
}

routing {
get("/metrics") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good, apart from the business metrics you have defined, does the /metrics now export JVM or server data?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this automatically emits JVM and system metrics (https://micrometer.io/docs/ref/jvm) and also http metrics.

Some more info:

image

Also: https://chatgpt.com/s/t_6926c4491864819181ee50f4b42ec07d

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metrics_on_local_test.txt

This is how they look.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. If it's easy then as a test could you try to setup the full micrometer dashboard, just for this service? Not building it by hand, there should be some pre-made things in Grafana https://grafana.com/grafana/dashboards/4701-jvm-micrometer/

call.respond(prometheusRegistry.scrape())
}
}
}

/**
* Connect app to the database.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add a counter for buttons being clicked ? Differentiate between a Delete button and clicking on an option

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spoonman01
For now, I am adding the very basic custom metrics of basic actions user take.

Regarding voting action: I thought about it but then I was not convinced about the benefit of it. Because people will vote they can change the vote and this means another voting action. So the metric will not show sth very clear. --> Maybe later we can find more meaningful reason for that. Wdyt?

Regarding delete poll: At the moment we don't have Delete functionality, but actually I can add the metric now, so it can be used when the feature implemented.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📣 Custom metric for "delete poll" is added although we don't have the functionality yet. Maybe this is not a good proactice to add unneeded things but since we are introducing the custom metrics first time in this app, I just wanted to make it ready from now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right it's total votes and not by person, still better than nothing I think.
I will leave the decision to you

Copy link
Copy Markdown
Author

@bbaarriiss bbaarriiss Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not adding a metric for voting for now. Because it will not tell us anything meaningful. We can send metrics with the information of the pool itself (such as poll-id) but then it will be out of the purpose of metrics. Metrics are for general overview not for every instance of polls created.

Also just to keep here updated: I reverted the "delete poll" as we agreed as a team.

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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,10 +23,12 @@ class MessagesHandlingServiceTest {
val userCommunicationService = mockk<UserCommunicationService>(relaxed = true)
val pollService = mockk<PollService>(relaxed = true)
val manager = mockk<WireApplicationManager>()
val usageMetrics = mockk<UsageMetrics>(relaxed = true)

val testModule = DI.Module("testModule") {
bind<UserCommunicationService>(overrides = true) with singleton { userCommunicationService }
bind<PollService>(overrides = true) with singleton { pollService }
bind<UsageMetrics>(overrides = true) with singleton { usageMetrics }
}

val di = DI {
Expand All @@ -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 }
}
Expand All @@ -66,6 +70,7 @@ class MessagesHandlingServiceTest {
// assert
coVerify(exactly = 1) { userCommunicationService.sendVersion(any(), any()) }
verify { pollService wasNot Called }
verify { usageMetrics wasNot Called }
}

@Test
Expand All @@ -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 }
}
Expand All @@ -94,6 +100,7 @@ class MessagesHandlingServiceTest {
// assert
coVerify(exactly = 1) { userCommunicationService.goodApp(any(), any()) }
verify { pollService wasNot Called }
verify { usageMetrics wasNot Called }
}

@Test
Expand All @@ -108,6 +115,7 @@ class MessagesHandlingServiceTest {
// assert
verify { pollService wasNot Called }
verify { userCommunicationService wasNot Called }
verify { usageMetrics wasNot Called }
}

@ParameterizedTest
Expand All @@ -129,6 +137,7 @@ class MessagesHandlingServiceTest {

// assert
coVerify { pollService.createPoll(any(), any()) }
coVerify(exactly = 1) { usageMetrics.onCreatePollCommand() }
}

@Test
Expand All @@ -142,6 +151,7 @@ class MessagesHandlingServiceTest {

// assert
coVerify(exactly = 1) { userCommunicationService.sendHelp(any(), any()) }
coVerify(exactly = 1) { usageMetrics.onHelpCommand() }
verify { pollService wasNot Called }
}

Expand All @@ -156,6 +166,7 @@ class MessagesHandlingServiceTest {

// assert
coVerify(exactly = 1) { userCommunicationService.sendHelp(any(), any()) }
coVerify(exactly = 1) { usageMetrics.onHelpCommand() }
verify { pollService wasNot Called }
}
}