Skip to content

Commit

Permalink
#169 Add authority and consumer metrics (#173)
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman authored Jun 21, 2024
1 parent e018952 commit 972248f
Show file tree
Hide file tree
Showing 24 changed files with 1,625 additions and 196 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Configuration
import ru.vityaman.lms.botalka.app.spring.api.http.message.toMessage
import ru.vityaman.lms.botalka.app.spring.api.http.message.toModel
import ru.vityaman.lms.botalka.app.spring.api.http.server.WorkspaceEventMessage
import ru.vityaman.lms.botalka.app.spring.monitoring.MetricsFactory
import ru.vityaman.lms.botalka.app.spring.storage.MainR2dbcConfig
import ru.vityaman.lms.botalka.commons.Consumer
import ru.vityaman.lms.botalka.commons.Runnable
Expand All @@ -21,6 +22,7 @@ import ru.vityaman.lms.botalka.core.external.telegram.TelegramBot
import ru.vityaman.lms.botalka.core.external.telegram.TelegramChat
import ru.vityaman.lms.botalka.core.external.telegram.TelegramConsumer
import ru.vityaman.lms.botalka.core.logging.Slf4jLog
import ru.vityaman.lms.botalka.core.logic.metered.MeteredConsumer
import ru.vityaman.lms.botalka.core.model.Workspace
import ru.vityaman.lms.botalka.core.tx.TxEnv
import ru.vityaman.lms.botalka.storage.jooq.JooqDatabase
Expand Down Expand Up @@ -68,12 +70,16 @@ class SpringWorkspaceEventConfig {
@Bean
@Qualifier(BeanName.KAFKA_CONSUMER)
fun consumer(
metrics: MetricsFactory,
@Value("\${broker.bootstrap-servers}")
bootstrapServers: String,
@Qualifier(BeanName.WORKSPACE_TOPIC)
topic: KafkaTopic<Workspace.Event.Id, Workspace.Event>,
) = BasicKafkaProducer(BasicKafkaProducer.Config(bootstrapServers), topic)
.asConsumerWithKey(Workspace.Event::id)
) = MeteredConsumer(
metrics.consumer("kafka-workspace-event").status(),
BasicKafkaProducer(BasicKafkaProducer.Config(bootstrapServers), topic)
.asConsumerWithKey(Workspace.Event::id),
)

@Bean
@Qualifier(BeanName.EVENTS)
Expand Down Expand Up @@ -102,35 +108,39 @@ class SpringWorkspaceEventConfig {
@Bean
@Qualifier(BeanName.TELEGRAM_CONSUMER)
fun telegramConsumer(
metrics: MetricsFactory,
telegram: TelegramBot,
@Value("\${external.service.telegram.admin-chat-id}")
adminChatId: Long,
) = TelegramConsumer<Workspace.Event>(
telegram,
TelegramChat(adminChatId),
) {
buildString {
append("New workspace event!\n")
append("From user with id ${it.producer}\n")
append("EventId: ${it.id}\n")
append("Kind: ")
append(
when (it) {
is Workspace.Feedback -> "Feedback"
is Workspace.Submission -> "Submission"
is Workspace.Comment -> "Comment"
},
)
append("\n")
append(
when (it) {
is Workspace.Feedback -> it.comment
is Workspace.Submission -> it.note
is Workspace.Comment -> it.text
},
)
}
}
) = MeteredConsumer(
metrics.consumer("telegram-workspace-event").status(),
TelegramConsumer<Workspace.Event>(
telegram,
TelegramChat(adminChatId),
) {
buildString {
append("New workspace event!\n")
append("From user with id ${it.producer}\n")
append("EventId: ${it.id}\n")
append("Kind: ")
append(
when (it) {
is Workspace.Feedback -> "Feedback"
is Workspace.Submission -> "Submission"
is Workspace.Comment -> "Comment"
},
)
append("\n")
append(
when (it) {
is Workspace.Feedback -> it.comment
is Workspace.Submission -> it.note
is Workspace.Comment -> it.text
},
)
}
},
)

@Bean
@Qualifier(BeanName.EVENT_CONSUMING_ACTOR)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import ru.vityaman.lms.botalka.app.spring.event.KafkaTopicConfig
import ru.vityaman.lms.botalka.app.spring.monitoring.MetricsFactory
import ru.vityaman.lms.botalka.commons.Consumer
import ru.vityaman.lms.botalka.core.logic.metered.MeteredConsumer
import ru.vityaman.lms.botalka.core.model.Homework
import ru.vityaman.lms.botalka.storage.kafka.BasicKafkaProducer
import ru.vityaman.lms.botalka.storage.kafka.KafkaTopic

@Component
@Qualifier(SpringConfig.BeanName.KAFKA_CONSUMER)
class SpringKafkaConsumer(
metrics: MetricsFactory,

@Value("\${broker.bootstrap-servers}")
bootstrapServers: String,

@Qualifier(KafkaTopicConfig.BeanName.HOMEWORK_TOPIC)
topic: KafkaTopic<Homework.Id, Homework>,
) : Consumer<Homework> by
BasicKafkaProducer(BasicKafkaProducer.Config(bootstrapServers), topic)
.asConsumerWithKey(Homework::id)
MeteredConsumer(
metrics.consumer("kafka-homework").status(),
BasicKafkaProducer(BasicKafkaProducer.Config(bootstrapServers), topic)
.asConsumerWithKey(Homework::id),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,34 @@ package ru.vityaman.lms.botalka.app.spring.event.homework
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import ru.vityaman.lms.botalka.app.spring.monitoring.MetricsFactory
import ru.vityaman.lms.botalka.commons.Consumer
import ru.vityaman.lms.botalka.core.external.telegram.TelegramBot
import ru.vityaman.lms.botalka.core.external.telegram.TelegramChat
import ru.vityaman.lms.botalka.core.external.telegram.TelegramConsumer
import ru.vityaman.lms.botalka.core.logic.metered.MeteredConsumer
import ru.vityaman.lms.botalka.core.model.Homework

@Component
@Qualifier(SpringConfig.BeanName.TELEGRAM_CONSUMER)
class SpringTelegramConsumer(
metrics: MetricsFactory,
telegram: TelegramBot,

@Value("\${external.service.telegram.admin-chat-id}")
adminChatId: Long,
) : Consumer<Homework> by
TelegramConsumer(telegram, TelegramChat(adminChatId), {
buildString {
append("Published homework '${it.title.text}'!\n")
append("\n")
append("${it.description}\n")
append("\n")
append("MaxScore: ${it.maxScore.value}\n")
append("Deadline: ${it.deadlineMoment}\n")
append("Id: ${it.id.number}\n")
}
})
MeteredConsumer(
metrics.consumer("telegram-homework").status(),
TelegramConsumer(telegram, TelegramChat(adminChatId), {
buildString {
append("Published homework '${it.title.text}'!\n")
append("\n")
append("${it.description}\n")
append("\n")
append("MaxScore: ${it.maxScore.value}\n")
append("Deadline: ${it.deadlineMoment}\n")
append("Id: ${it.id.number}\n")
}
}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,38 @@ package ru.vityaman.lms.botalka.app.spring.monitoring
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.stereotype.Component
import ru.vityaman.lms.botalka.core.logic.metered.StatusCount

@Component
class MetricsFactory(private val registry: MeterRegistry) {
fun consumer(name: String) = Consumer(name)
fun service(name: String) = Service(name)

inner class Consumer(private val name: String) {
@Suppress("LabeledExpression")
fun status() = counter(
"lms_consumer_accept",
"consumer" to this@Consumer.name,
).let { status(it) }
}

inner class Service(private val name: String) {
fun method(name: String) = Method(name)

inner class Method(private val name: String) {
@Suppress("LabeledExpression")
fun status(): StatusCount {
val count = Counter
.builder("lms_service_method_call")
.tag("service", this@Service.name)
.tag("method", this@Method.name)
.withRegistry(registry)
fun status() = counter(
"lms_service_method_call",
"service" to this@Service.name,
"method" to this@Method.name,
).let { status(it) }
}
}

return StatusCount(
successes = MicrometerCount(
count.withTag("status", "success"),
),
failures = MicrometerCount(
count.withTag("status", "failure"),
),
)
}
fun counter(name: String, vararg tags: Pair<String, String>) = run {
var counter = Counter.builder(name)
for ((key, value) in tags) {
counter = counter.tag(key, value)
}
counter.withRegistry(registry)
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package ru.vityaman.lms.botalka.app.spring.monitoring

import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Meter
import ru.vityaman.lms.botalka.core.monitoring.Count
import ru.vityaman.lms.botalka.core.monitoring.StatusCount

class MicrometerCount(
private val origin: Counter,
) : Count {
override fun add(amount: Int) =
origin.increment(amount.toDouble())
}

fun status(provider: Meter.MeterProvider<Counter>) = StatusCount(
successes = MicrometerCount(provider.withTag("status", "success")),
failures = MicrometerCount(provider.withTag("status", "failure")),
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,59 @@
package ru.vityaman.lms.botalka.app.spring.security

import org.springframework.stereotype.Component
import ru.vityaman.lms.botalka.app.spring.monitoring.MetricsFactory
import ru.vityaman.lms.botalka.app.spring.monitoring.MicrometerCount
import ru.vityaman.lms.botalka.app.spring.monitoring.status
import ru.vityaman.lms.botalka.commons.ExhaustiveMap
import ru.vityaman.lms.botalka.core.model.User
import ru.vityaman.lms.botalka.core.security.persmission.Authority
import ru.vityaman.lms.botalka.core.security.persmission.BasicAuthority
import ru.vityaman.lms.botalka.core.security.persmission.MeteredAuthority
import ru.vityaman.lms.botalka.core.security.persmission.Permission.Kind.GetEvents
import ru.vityaman.lms.botalka.core.security.persmission.Permission.Kind.PostHomework
import ru.vityaman.lms.botalka.core.security.persmission.Permission.Kind.ResolvePromotion
import ru.vityaman.lms.botalka.core.security.persmission.Permission.Kind.SendFeedback
import ru.vityaman.lms.botalka.core.security.persmission.Permission.Kind.SendSubmission

@Component
class SpringAuthority : Authority by BasicAuthority()
class SpringAuthority(
metrics: MetricsFactory,
) : Authority by
MeteredAuthority(
run {
MeteredAuthority.Metrics(
role = { kind: String ->
MicrometerCount(
metrics
.counter("lms_authorization_user_role")
.withTag("kind", kind),
)
}.let {
ExhaustiveMap.from {
when (it) {
User.Role.ADMIN -> it("admin")
User.Role.TEACHER -> it("teacher")
User.Role.STUDENT -> it("student")
}
}
},
permission = { kind: String ->
metrics.counter(
"lms_authorization_permission",
"kind" to kind,
).let { cnt -> status(cnt) }
}.let {
ExhaustiveMap.from {
when (it) {
ResolvePromotion -> it("resolve-promotion")
GetEvents -> it("get-events")
SendFeedback -> it("send-feedback")
SendSubmission -> it("send-submission")
PostHomework -> it("post-homework")
}
}
},
)
},
BasicAuthority(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.vityaman.lms.botalka.commons

import java.util.EnumMap
import kotlin.enums.enumEntries

@OptIn(ExperimentalStdlibApi::class)
interface ExhaustiveMap<K, V> {
operator fun get(key: K): V

companion object {
inline fun <reified K : Enum<K>, V> from(
vararg pairs: Pair<K, V>,
): ExhaustiveMap<K, V> = object : ExhaustiveMap<K, V> {
private val origin = pairs
.associateTo(EnumMap(K::class.java)) { (l, r) -> l to r }

init {
val keys = pairs.map { it.first }
require(keys.distinct().size == keys.size)
require(enumEntries<K>().size == keys.size)
}

override operator fun get(key: K): V = origin[key]!!
}

inline fun <reified K : Enum<K>, V> from(
mapping: (K) -> V,
): ExhaustiveMap<K, V> {
val entries = enumEntries<K>()
return Array(entries.size) {
entries[it] to mapping(entries[it])
}.let { from(pairs = it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package ru.vityaman.lms.botalka.core.logging
interface Log {
fun info(message: String)
fun warn(message: String)
fun debug(message: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import org.slf4j.LoggerFactory
class Slf4jLog(name: String) : Log {
private val origin = LoggerFactory.getLogger(name)

override fun info(message: String) {
override fun info(message: String) =
origin.info(message)
}

override fun warn(message: String) {
override fun warn(message: String) =
origin.warn(message)
}

override fun debug(message: String) =
origin.debug(message)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ class LoggingAuthService<T>(
) : AuthService<T> {
override suspend fun signUp(draft: User.Draft, credentials: T): AuthUser =
runCatching { origin.signUp(draft, credentials) }
.onSuccess { log.info("User with id ${it.user.id} signed up") }
.onFailure { log.warn("Failed to sign up") }
.onSuccess { log.debug("User with id ${it.user.id} signed up") }
.onFailure { log.debug("Failed to sign up") }
.getOrThrow()

override suspend fun signIn(credentials: T): AccessToken =
runCatching { origin.signIn(credentials) }
.onSuccess { log.info("Signed in") }
.onFailure { log.warn("Failed to sign in") }
.onSuccess { log.debug("Signed in") }
.onFailure { log.debug("Failed to sign in") }
.getOrThrow()
}
Loading

0 comments on commit 972248f

Please sign in to comment.