Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#169 Add authority and consumer metrics #173

Merged
merged 4 commits into from
Jun 21, 2024
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
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
Loading