module 5 lesson 1
This commit is contained in:
@ -1,13 +1,36 @@
|
||||
[versions]
|
||||
kotlin = "2.1.0"
|
||||
kotlinx-coroutines = "1.9.0"
|
||||
kotlinx-datetime = "0.6.1"
|
||||
kotlinx-datetime = "0.6.2"
|
||||
kotlinx-serialization = "1.6.3"
|
||||
okhhtp = "4.12.0"
|
||||
jackson-module = "2.18.2"
|
||||
|
||||
# Http
|
||||
okhttp = "4.12.0"
|
||||
|
||||
# Serialization
|
||||
jackson = "2.18.2"
|
||||
|
||||
# Spec generator
|
||||
openapi-generator = "7.11.0"
|
||||
|
||||
# Logging
|
||||
slf4j = "2.0.9"
|
||||
logback-classic = "1.4.11"
|
||||
openapi-generator = "7.3.0"
|
||||
logback = "1.5.3"
|
||||
logback-appender = "1.8.8"
|
||||
kermit = "2.0.3"
|
||||
logstash = "7.4"
|
||||
fluentd = "0.3.4"
|
||||
|
||||
# Framework
|
||||
ktor = "3.0.3"
|
||||
|
||||
#Testing
|
||||
testcontainers = "1.19.7"
|
||||
kotest = "6.0.0.M2"
|
||||
mockito = "5.2.1"
|
||||
|
||||
# Docker
|
||||
muschko = "9.4.0"
|
||||
|
||||
# BASE
|
||||
jvm-compiler = "17"
|
||||
@ -20,17 +43,69 @@ java-gradle-plugin = { id = "java-gradle-plugin" }
|
||||
openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" }
|
||||
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
# Ktor
|
||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||
|
||||
# Docker
|
||||
muschko-remote = { id = "com.bmuschko.docker-remote-api", version.ref = "muschko" }
|
||||
muschko-java = { id = "com.bmuschko.docker-java-application", version.ref = "muschko" }
|
||||
|
||||
[bundles]
|
||||
kotest = ["kotest-junit5", "kotest-core", "kotest-property"]
|
||||
|
||||
[libraries]
|
||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||
kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||
kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||
kotlin-jackson-module = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module" }
|
||||
kotlin-jackson-datatype = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson-module" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhhtp" }
|
||||
|
||||
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
|
||||
jackson-datatype = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
|
||||
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
|
||||
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
|
||||
# Ktor
|
||||
ktor-network = { module = "io.ktor:ktor-network", version.ref = "ktor" }
|
||||
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||
ktor-serialization-jackson = { module = "io.ktor:ktor-serialization-jackson", version.ref = "ktor" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-client-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
|
||||
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
|
||||
ktor-server-headers-response = { module = "io.ktor:ktor-server-auto-head-response", version.ref = "ktor" }
|
||||
ktor-server-headers-caching = { module = "io.ktor:ktor-server-caching-headers", version.ref = "ktor" }
|
||||
ktor-server-headers-default = { module = "io.ktor:ktor-server-default-headers", version.ref = "ktor" }
|
||||
ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
|
||||
ktor-server-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" }
|
||||
ktor-server-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
|
||||
ktor-server-calllogging = { module = "io.ktor:ktor-server-call-logging", version.ref = "ktor" }
|
||||
ktor-server-websocket = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
|
||||
ktor-server-test = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
|
||||
|
||||
# Message Queues
|
||||
rabbitmq-client = { module = "com.rabbitmq:amqp-client", version = "5.20.0" }
|
||||
kafka-client = { module = "org.apache.kafka:kafka-clients", version = "3.7.0" }
|
||||
|
||||
# Testing
|
||||
kotest-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" }
|
||||
kotest-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" }
|
||||
kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
|
||||
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito" }
|
||||
|
||||
testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
|
||||
testcontainers-rabbitmq = { module = "org.testcontainers:rabbitmq", version.ref = "testcontainers" }
|
||||
|
||||
# Logging
|
||||
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" }
|
||||
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||
logback-appenders = { module = "com.sndyuk:logback-more-appenders", version.ref = "logback-appender" }
|
||||
logback-logstash = { module = "net.logstash.logback:logstash-logback-encoder", version.ref = "logstash" }
|
||||
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
|
||||
logger-fluentd = { module = "org.fluentd:fluent-logger", version.ref = "fluentd" }
|
||||
|
||||
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
plugin-binaryCompatibilityValidator = "org.jetbrains.kotlinx:binary-compatibility-validator:0.13.2"
|
||||
@ -14,7 +14,7 @@ dependencies {
|
||||
|
||||
// Homework Hard
|
||||
implementation(libs.okhttp) // http client
|
||||
implementation(libs.kotlin.jackson.module) // from string to object
|
||||
implementation(libs.jackson.kotlin) // from string to object
|
||||
|
||||
testImplementation(kotlin("test"))
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ subprojects {
|
||||
ext {
|
||||
val specDir = layout.projectDirectory.dir("../specs")
|
||||
set("spec-v1", specDir.file("specs-v1.yaml").toString())
|
||||
set("spec-log-v1", specDir.file("specs-log-v1.yaml").toString())
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
58
ok-messenger-be/ok-messenger-api-log-v1/build.gradle.kts
Normal file
58
ok-messenger-be/ok-messenger-api-log-v1/build.gradle.kts
Normal file
@ -0,0 +1,58 @@
|
||||
plugins {
|
||||
id("build-jvm")
|
||||
alias(libs.plugins.openapi.generator)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDir(layout.buildDirectory.dir("generate-resources/main/src/main/kotlin"))
|
||||
}
|
||||
}
|
||||
|
||||
openApiGenerate {
|
||||
val openapiGroup = "${rootProject.group}.api.log.v1"
|
||||
generatorName.set("kotlin")
|
||||
packageName.set(openapiGroup)
|
||||
apiPackage.set("$openapiGroup.api")
|
||||
modelPackage.set("$openapiGroup.models")
|
||||
inputSpec.set(rootProject.ext["spec-log-v1"] as String)
|
||||
|
||||
/**
|
||||
* Use only models
|
||||
* Doc: https://openapi-generator.tech/docs/globals
|
||||
*/
|
||||
globalProperties.apply {
|
||||
put("models", "")
|
||||
put("modelDocs", "false")
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional parameters from
|
||||
* https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin.md
|
||||
*/
|
||||
configOptions.set(
|
||||
mapOf(
|
||||
"dateLibrary" to "string",
|
||||
"enumPropertyNaming" to "UPPERCASE",
|
||||
"serializationLibrary" to "jackson",
|
||||
"collectionType" to "list"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.kotlin.datetime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.jackson.kotlin)
|
||||
implementation(libs.jackson.datatype)
|
||||
implementation(project(":ok-messenger-common"))
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
}
|
||||
|
||||
tasks {
|
||||
filter { it.name.startsWith("compile") }.forEach {
|
||||
it.dependsOn(openApiGenerate)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package ru.otus.messenger.api.log.v1.mapper
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import ru.otus.messenger.api.log.v1.models.*
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.*
|
||||
|
||||
fun MessengerContext.toLog(logId: String) = CommonLogModel(
|
||||
messageTime = Clock.System.now().toString(),
|
||||
logId = logId,
|
||||
source = "ok-messenger",
|
||||
chat = toChatLog(),
|
||||
errors = errors.map { it.toLog() },
|
||||
)
|
||||
|
||||
private fun MessengerContext.toChatLog(): ChatLogModel? {
|
||||
val emptyReport = MessengerChat()
|
||||
return ChatLogModel(
|
||||
requestId = requestId.takeIf { it != RequestId.NONE }?.asString(),
|
||||
requestChat = chatRequest.takeIf { it != emptyReport }?.toLog(),
|
||||
requestSearch = chatFilterRequest.takeIf { it != ChatSearchFilter.NONE }?.toLog(),
|
||||
responseChat = chatResponse.takeIf { it != emptyReport }?.toLog(),
|
||||
responseChats = chatsResponse.takeIf { it.isNotEmpty() }?.filter { it != emptyReport }?.map { it.toLog() },
|
||||
).takeIf { it != ChatLogModel() }
|
||||
}
|
||||
|
||||
private fun ChatSearchFilter.toLog() = ChatSearchLog(
|
||||
searchFields = searchFields.joinToString("\t") { it.toString() },
|
||||
)
|
||||
|
||||
private fun ChatError.toLog() = ErrorLogModel(
|
||||
message = message.takeIf { it.isNotBlank() },
|
||||
field = field.takeIf { it.isNotBlank() },
|
||||
code = code.takeIf { it.isNotBlank() },
|
||||
level = level.name,
|
||||
)
|
||||
|
||||
private fun MessengerChat.toLog() = ChatLog(
|
||||
chatId = id.takeIf { it != ChatId.NONE }?.asString(),
|
||||
title = title.takeIf { it.isNotBlank() },
|
||||
description = description.takeIf { it.isNotBlank() },
|
||||
type = type.takeIf { it != ChatType.NONE }.toString(),
|
||||
mode = mode.takeIf { it != ChatMode.NONE }.toString(),
|
||||
ownerId = ownerId.takeIf { it != ChatOwnerId.NONE }?.asString(),
|
||||
participants = participants.takeIf { it.isNotEmpty() }?.map { it.asString() }?.toSet(),
|
||||
metadata = metadata.takeIf { it != ChatMetadata.NONE }?.asString()
|
||||
)
|
||||
@ -5,10 +5,10 @@ import kotlinx.serialization.json.jsonObject
|
||||
import ru.otus.messenger.api.v1.mappers.exceptions.UnknownRequestClass
|
||||
import ru.otus.messenger.api.v1.models.*
|
||||
import ru.otus.messenger.common.models.*
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.stubs.Stubs
|
||||
|
||||
fun ChatContext.fromTransport(request: IRequest) = when (request) {
|
||||
fun MessengerContext.fromTransport(request: IRequest) = when (request) {
|
||||
is ChatCreateRequest -> fromTransport(request)
|
||||
is ChatReadRequest -> fromTransport(request)
|
||||
is ChatDeleteRequest -> fromTransport(request)
|
||||
@ -40,35 +40,35 @@ private fun Debug?.transportToStubCase(): Stubs = when (this?.stub) {
|
||||
null -> Stubs.NONE
|
||||
}
|
||||
|
||||
fun ChatContext.fromTransport(request: ChatCreateRequest) {
|
||||
fun MessengerContext.fromTransport(request: ChatCreateRequest) {
|
||||
command = ChatCommand.CREATE
|
||||
chatRequest = request.chat?.toInternal() ?: MessengerChat()
|
||||
workMode = request.debug.transportToWorkMode()
|
||||
stubCase = request.debug.transportToStubCase()
|
||||
}
|
||||
|
||||
fun ChatContext.fromTransport(request: ChatReadRequest) {
|
||||
fun MessengerContext.fromTransport(request: ChatReadRequest) {
|
||||
command = ChatCommand.READ
|
||||
chatRequest = request.chatId.toChatId().toInternal()
|
||||
workMode = request.debug.transportToWorkMode()
|
||||
stubCase = request.debug.transportToStubCase()
|
||||
}
|
||||
|
||||
fun ChatContext.fromTransport(request: ChatDeleteRequest) {
|
||||
fun MessengerContext.fromTransport(request: ChatDeleteRequest) {
|
||||
command = ChatCommand.DELETE
|
||||
chatRequest = request.chatId.toChatId().toInternal()
|
||||
workMode = request.debug.transportToWorkMode()
|
||||
stubCase = request.debug.transportToStubCase()
|
||||
}
|
||||
|
||||
fun ChatContext.fromTransport(request: ChatUpdateRequest) {
|
||||
fun MessengerContext.fromTransport(request: ChatUpdateRequest) {
|
||||
command = ChatCommand.UPDATE
|
||||
chatRequest = request.chat?.toInternal() ?: MessengerChat()
|
||||
workMode = request.debug.transportToWorkMode()
|
||||
stubCase = request.debug.transportToStubCase()
|
||||
}
|
||||
|
||||
fun ChatContext.fromTransport(request: ChatSearchRequest) {
|
||||
fun MessengerContext.fromTransport(request: ChatSearchRequest) {
|
||||
command = ChatCommand.SEARCH
|
||||
chatRequest = request.criteria?.toInternal() ?: MessengerChat()
|
||||
workMode = request.debug.transportToWorkMode()
|
||||
|
||||
@ -3,44 +3,56 @@ package ru.otus.messenger.api.v1.mappers
|
||||
import kotlinx.datetime.Instant
|
||||
import ru.otus.messenger.api.v1.models.*
|
||||
import ru.otus.messenger.common.models.*
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.NONE
|
||||
import ru.otus.messenger.common.exceptions.UnknownChatCommand
|
||||
|
||||
|
||||
fun ChatContext.toTransportChat(): IResponse = when (val cmd = command) {
|
||||
fun MessengerContext.toTransportChat(): IResponse = when (val cmd = command) {
|
||||
ChatCommand.CREATE -> toTransportCreate()
|
||||
ChatCommand.READ -> toTransportRead()
|
||||
ChatCommand.DELETE -> toTransportDelete()
|
||||
ChatCommand.SEARCH -> toTransportSearch()
|
||||
ChatCommand.UPDATE -> toTransportUpdate()
|
||||
ChatCommand.INIT -> toTransportInit()
|
||||
ChatCommand.FINISH -> toTransportFinish()
|
||||
ChatCommand.NONE -> throw UnknownChatCommand(cmd)
|
||||
}
|
||||
|
||||
fun ChatContext.toTransportCreate() = ChatCreateResponse(
|
||||
fun MessengerContext.toTransportInit() = ChatInitResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
)
|
||||
|
||||
fun MessengerContext.toTransportFinish() = ChatInitResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
)
|
||||
|
||||
fun MessengerContext.toTransportCreate() = ChatCreateResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
chat = chatResponse.toTransportChat()
|
||||
)
|
||||
|
||||
fun ChatContext.toTransportRead() = ChatReadResponse(
|
||||
fun MessengerContext.toTransportRead() = ChatReadResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
chat = chatResponse.toTransportChat()
|
||||
)
|
||||
|
||||
fun ChatContext.toTransportDelete() = ChatDeleteResponse(
|
||||
fun MessengerContext.toTransportDelete() = ChatDeleteResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
)
|
||||
|
||||
fun ChatContext.toTransportSearch() = ChatSearchResponse(
|
||||
fun MessengerContext.toTransportSearch() = ChatSearchResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
chats = chatsResponse.toTransportChats()
|
||||
)
|
||||
|
||||
fun ChatContext.toTransportUpdate() = ChatUpdateResponse(
|
||||
fun MessengerContext.toTransportUpdate() = ChatUpdateResponse(
|
||||
result = state.toResult(),
|
||||
errors = errors.toTransportErrors(),
|
||||
chat = chatResponse.toTransportChat()
|
||||
|
||||
@ -10,7 +10,7 @@ import ru.otus.messenger.api.v1.models.ChatCreateResponse
|
||||
import ru.otus.messenger.api.v1.models.Debug
|
||||
import ru.otus.messenger.api.v1.models.DebugMode
|
||||
import ru.otus.messenger.api.v1.models.DebugStubs
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
@ -39,7 +39,7 @@ class MapperCreateTest {
|
||||
)
|
||||
)
|
||||
|
||||
val context = ChatContext()
|
||||
val context = MessengerContext()
|
||||
context.fromTransport(req)
|
||||
|
||||
assertEquals(Stubs.SUCCESS, context.stubCase)
|
||||
@ -51,7 +51,7 @@ class MapperCreateTest {
|
||||
|
||||
@Test
|
||||
fun toTransport() {
|
||||
val context = ChatContext(
|
||||
val context = MessengerContext(
|
||||
requestId = RequestId(UUID.randomUUID().toString()),
|
||||
command = ChatCommand.CREATE,
|
||||
state = ChatState.RUNNING,
|
||||
|
||||
@ -9,7 +9,7 @@ import ru.otus.messenger.api.v1.models.Debug
|
||||
import ru.otus.messenger.api.v1.models.DebugMode
|
||||
import ru.otus.messenger.api.v1.models.DebugStubs
|
||||
import ru.otus.messenger.api.v1.models.ResponseResult
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
@ -30,7 +30,7 @@ class MapperDeleteTest {
|
||||
chatId = "chat-id",
|
||||
)
|
||||
|
||||
val context = ChatContext()
|
||||
val context = MessengerContext()
|
||||
context.fromTransport(req)
|
||||
|
||||
assertEquals(Stubs.SUCCESS, context.stubCase)
|
||||
@ -43,7 +43,7 @@ class MapperDeleteTest {
|
||||
|
||||
@Test
|
||||
fun toTransport() {
|
||||
val context = ChatContext(
|
||||
val context = MessengerContext(
|
||||
requestId = RequestId(UUID.randomUUID().toString()),
|
||||
command = ChatCommand.DELETE,
|
||||
state = ChatState.RUNNING,
|
||||
|
||||
@ -9,7 +9,7 @@ import ru.otus.messenger.api.v1.models.ChatReadResponse
|
||||
import ru.otus.messenger.api.v1.models.Debug
|
||||
import ru.otus.messenger.api.v1.models.DebugMode
|
||||
import ru.otus.messenger.api.v1.models.DebugStubs
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
@ -32,7 +32,7 @@ class MapperReadTest {
|
||||
chatId = "chat-id",
|
||||
)
|
||||
|
||||
val context = ChatContext()
|
||||
val context = MessengerContext()
|
||||
context.fromTransport(req)
|
||||
|
||||
assertEquals(Stubs.SUCCESS, context.stubCase)
|
||||
@ -45,7 +45,7 @@ class MapperReadTest {
|
||||
|
||||
@Test
|
||||
fun toTransport() {
|
||||
val context = ChatContext(
|
||||
val context = MessengerContext(
|
||||
requestId = RequestId(UUID.randomUUID().toString()),
|
||||
command = ChatCommand.READ,
|
||||
state = ChatState.RUNNING,
|
||||
|
||||
@ -10,7 +10,7 @@ import ru.otus.messenger.api.v1.models.ChatSearchResponse
|
||||
import ru.otus.messenger.api.v1.models.Debug
|
||||
import ru.otus.messenger.api.v1.models.DebugMode
|
||||
import ru.otus.messenger.api.v1.models.DebugStubs
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
@ -38,7 +38,7 @@ class MapperSearchTest {
|
||||
)
|
||||
)
|
||||
|
||||
val context = ChatContext()
|
||||
val context = MessengerContext()
|
||||
context.fromTransport(req)
|
||||
|
||||
assertEquals(Stubs.SUCCESS, context.stubCase)
|
||||
@ -50,7 +50,7 @@ class MapperSearchTest {
|
||||
|
||||
@Test
|
||||
fun toTransport() {
|
||||
val context = ChatContext(
|
||||
val context = MessengerContext(
|
||||
requestId = RequestId(UUID.randomUUID().toString()),
|
||||
command = ChatCommand.SEARCH,
|
||||
state = ChatState.RUNNING,
|
||||
|
||||
@ -10,7 +10,7 @@ import ru.otus.messenger.api.v1.models.ChatUpdateResponse
|
||||
import ru.otus.messenger.api.v1.models.Debug
|
||||
import ru.otus.messenger.api.v1.models.DebugMode
|
||||
import ru.otus.messenger.api.v1.models.DebugStubs
|
||||
import ru.otus.messenger.common.ChatContext
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
@ -44,7 +44,7 @@ class MapperUpdateTest {
|
||||
)
|
||||
)
|
||||
|
||||
val context = ChatContext()
|
||||
val context = MessengerContext()
|
||||
context.fromTransport(req)
|
||||
|
||||
assertEquals(Stubs.SUCCESS, context.stubCase)
|
||||
@ -57,7 +57,7 @@ class MapperUpdateTest {
|
||||
|
||||
@Test
|
||||
fun toTransport() {
|
||||
val context = ChatContext(
|
||||
val context = MessengerContext(
|
||||
requestId = RequestId(UUID.randomUUID().toString()),
|
||||
command = ChatCommand.UPDATE,
|
||||
state = ChatState.RUNNING,
|
||||
|
||||
@ -44,8 +44,9 @@ openApiGenerate {
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.kotlin.jackson.module)
|
||||
implementation(libs.kotlin.jackson.datatype)
|
||||
implementation(libs.kotlin.datetime)
|
||||
implementation(libs.jackson.kotlin)
|
||||
implementation(libs.jackson.datatype)
|
||||
testImplementation(kotlin("test-junit"))
|
||||
}
|
||||
|
||||
|
||||
24
ok-messenger-be/ok-messenger-app/README.md
Normal file
24
ok-messenger-be/ok-messenger-app/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Модуль ok-messenger-app
|
||||
|
||||
Ktor server application
|
||||
|
||||
## Building & Running
|
||||
|
||||
To build or run the project, use one of the following tasks:
|
||||
|
||||
| Task | Description |
|
||||
|-------------------------------|----------------------------------------------------------------------|
|
||||
| `./gradlew test` | Run the tests |
|
||||
| `./gradlew build` | Build everything |
|
||||
| `buildFatJar` | Build an executable JAR of the server with all dependencies included |
|
||||
| `buildImage` | Build the docker image to use with the fat JAR |
|
||||
| `publishImageToLocalRegistry` | Publish the docker image locally |
|
||||
| `run` | Run the server |
|
||||
| `runDocker` | Run using the local docker image |
|
||||
|
||||
If the server starts successfully, you'll see the following output:
|
||||
|
||||
```
|
||||
2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds.
|
||||
2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080
|
||||
```
|
||||
55
ok-messenger-be/ok-messenger-app/build.gradle.kts
Normal file
55
ok-messenger-be/ok-messenger-app/build.gradle.kts
Normal file
@ -0,0 +1,55 @@
|
||||
plugins {
|
||||
id("build-jvm")
|
||||
alias(libs.plugins.ktor)
|
||||
alias(libs.plugins.muschko.remote)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("io.ktor.server.cio.EngineMain")
|
||||
}
|
||||
|
||||
ktor {
|
||||
docker {
|
||||
localImageName.set(project.name)
|
||||
imageTag.set(project.version.toString())
|
||||
jreVersion.set(JavaVersion.VERSION_21)
|
||||
}
|
||||
}
|
||||
|
||||
jib {
|
||||
container.mainClass = application.mainClass.get()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.ktor.server.cors)
|
||||
implementation(libs.ktor.server.yaml)
|
||||
implementation(libs.ktor.server.negotiation)
|
||||
implementation(libs.ktor.server.headers.default)
|
||||
implementation(libs.ktor.server.headers.response)
|
||||
implementation(libs.ktor.server.headers.caching)
|
||||
implementation(libs.ktor.serialization.jackson)
|
||||
implementation(libs.ktor.server.calllogging)
|
||||
implementation(libs.ktor.server.websocket)
|
||||
|
||||
// transport models
|
||||
implementation(project(":ok-messenger-common"))
|
||||
implementation(project(":ok-messenger-api-v1"))
|
||||
implementation(project(":ok-messenger-api-v1-mappers"))
|
||||
|
||||
// logic
|
||||
implementation(project(":ok-messenger-biz"))
|
||||
|
||||
// stubs
|
||||
implementation(project(":ok-messenger-stubs"))
|
||||
|
||||
// logging
|
||||
implementation(project(":ok-messenger-api-log-v1"))
|
||||
implementation("ru.otus.messenger.libs:ok-messenger-lib-logging")
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
testImplementation(libs.ktor.server.test)
|
||||
testImplementation(libs.ktor.client.negotiation)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.netty.EngineMain
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import ru.otus.messenger.app.plugins.initAppSettings
|
||||
|
||||
fun main(args: Array<String>) = EngineMain.main(args)
|
||||
|
||||
fun Application.module(
|
||||
appSettings: MessengerAppSettings = initAppSettings(),
|
||||
) {
|
||||
install(CORS) {
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowMethod(HttpMethod.Patch)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
allowHeader("MyCustomHeader")
|
||||
allowCredentials = true
|
||||
/* TODO
|
||||
Это временное решение, оно опасно.
|
||||
В боевом приложении здесь должны быть конкретные настройки
|
||||
*/
|
||||
anyHost()
|
||||
}
|
||||
configureHTTP()
|
||||
configureSerialization()
|
||||
|
||||
configureRouting(appSettings)
|
||||
}
|
||||
10
ok-messenger-be/ok-messenger-app/src/main/kotlin/HTTP.kt
Normal file
10
ok-messenger-be/ok-messenger-app/src/main/kotlin/HTTP.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cachingheaders.*
|
||||
import io.ktor.server.plugins.defaultheaders.*
|
||||
|
||||
fun Application.configureHTTP() {
|
||||
install(CachingHeaders)
|
||||
install(DefaultHeaders)
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import org.slf4j.event.Level
|
||||
|
||||
fun Application.configureMonitoring() {
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
}
|
||||
}
|
||||
29
ok-messenger-be/ok-messenger-app/src/main/kotlin/Routing.kt
Normal file
29
ok-messenger-be/ok-messenger-app/src/main/kotlin/Routing.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.autohead.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import ru.otus.messenger.app.v1.v1Chat
|
||||
import ru.otus.messenger.app.v1.wsHandlerV1
|
||||
|
||||
fun Application.configureRouting(appSettings: MessengerAppSettings) {
|
||||
install(AutoHeadResponse)
|
||||
install(WebSockets)
|
||||
|
||||
routing {
|
||||
get("/") {
|
||||
call.respondText("Hello World!")
|
||||
}
|
||||
|
||||
route("v1") {
|
||||
v1Chat(appSettings)
|
||||
|
||||
webSocket("/ws") {
|
||||
wsHandlerV1(appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.serialization.jackson.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import ru.otus.messenger.api.v1.apiV1Mapper
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
jackson {
|
||||
setConfig(apiV1Mapper.serializationConfig)
|
||||
setConfig(apiV1Mapper.deserializationConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package ru.otus.messenger.app.base
|
||||
|
||||
import ru.otus.messenger.common.ws.IMessengerWsSession
|
||||
import ru.otus.messenger.common.ws.IMessengerWsSessionRepo
|
||||
|
||||
class KtorWsSessionRepo(): IMessengerWsSessionRepo {
|
||||
private val sessions: MutableSet<IMessengerWsSession> = mutableSetOf()
|
||||
override fun add(session: IMessengerWsSession) {
|
||||
sessions.add(session)
|
||||
}
|
||||
|
||||
override fun clearAll() {
|
||||
sessions.clear()
|
||||
}
|
||||
|
||||
override fun remove(session: IMessengerWsSession) {
|
||||
sessions.remove(session)
|
||||
}
|
||||
|
||||
override suspend fun <T> sendAll(obj: T) {
|
||||
sessions.forEach { it.send(obj) }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package ru.otus.messenger.app.base
|
||||
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import ru.otus.messenger.api.v1.apiV1ResponseSerialize
|
||||
import ru.otus.messenger.api.v1.models.IResponse
|
||||
import ru.otus.messenger.common.ws.IMessengerWsSession
|
||||
|
||||
data class KtorWsSessionV1(
|
||||
private val session: WebSocketSession
|
||||
) : IMessengerWsSession {
|
||||
override suspend fun <T> send(obj: T) {
|
||||
require(obj is IResponse)
|
||||
session.send(Frame.Text(apiV1ResponseSerialize(obj)))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package ru.otus.messenger.app.common
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import ru.otus.messenger.api.log.v1.mapper.toLog
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.helpers.asMessengerError
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import ru.otus.messenger.common.models.ChatState
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
suspend inline fun <T> MessengerAppSettings.controllerHelper(
|
||||
crossinline getRequest: suspend MessengerContext.() -> Unit,
|
||||
crossinline toResponse: suspend MessengerContext.() -> T,
|
||||
clazz: KClass<*>,
|
||||
logId: String,
|
||||
): T {
|
||||
val logger = corSettings.loggerProvider.logger(clazz)
|
||||
val ctx = MessengerContext(
|
||||
timeStart = Clock.System.now(),
|
||||
)
|
||||
return try {
|
||||
ctx.getRequest()
|
||||
logger.info(
|
||||
msg = "Request $logId started for ${clazz.simpleName}",
|
||||
marker = "BIZ",
|
||||
data = ctx.toLog(logId)
|
||||
)
|
||||
processor.exec(ctx)
|
||||
logger.info(
|
||||
msg = "Request $logId processed for ${clazz.simpleName}",
|
||||
marker = "BIZ",
|
||||
data = ctx.toLog(logId)
|
||||
)
|
||||
ctx.toResponse()
|
||||
} catch (e: Throwable) {
|
||||
logger.error(
|
||||
msg = "Request $logId failed for ${clazz.simpleName}",
|
||||
marker = "BIZ",
|
||||
data = ctx.toLog(logId),
|
||||
e = e,
|
||||
)
|
||||
ctx.state = ChatState.FAILING
|
||||
ctx.errors.add(e.asMessengerError())
|
||||
processor.exec(ctx)
|
||||
if (ctx.command == ChatCommand.NONE) {
|
||||
ctx.command = ChatCommand.READ
|
||||
}
|
||||
ctx.toResponse()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package ru.otus.messenger.app.common
|
||||
|
||||
import ru.otus.messenger.biz.MessengerProcessor
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
|
||||
interface MessengerAppSettings {
|
||||
val processor: MessengerProcessor
|
||||
val corSettings: MessengerCorSettings
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package ru.otus.messenger.app.common
|
||||
|
||||
import ru.otus.messenger.biz.MessengerProcessor
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
|
||||
data class MessengerAppSettingsData(
|
||||
val appUrls: List<String> = emptyList(),
|
||||
override val corSettings: MessengerCorSettings = MessengerCorSettings(),
|
||||
override val processor: MessengerProcessor = MessengerProcessor(corSettings),
|
||||
): MessengerAppSettings
|
||||
@ -0,0 +1,7 @@
|
||||
package ru.otus.messenger.app.plugins
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import ru.otus.messenger.logging.common.LoggerProvider
|
||||
import ru.otus.messenger.logging.loggerLogback
|
||||
|
||||
fun Application.getLoggerProviderConf(): LoggerProvider = LoggerProvider { loggerLogback(it) }
|
||||
@ -0,0 +1,18 @@
|
||||
package ru.otus.messenger.app.plugins
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import ru.otus.messenger.app.common.MessengerAppSettingsData
|
||||
import ru.otus.messenger.biz.MessengerProcessor
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
|
||||
fun Application.initAppSettings(): MessengerAppSettings {
|
||||
val corSettings = MessengerCorSettings(
|
||||
loggerProvider = getLoggerProviderConf(),
|
||||
)
|
||||
return MessengerAppSettingsData(
|
||||
appUrls = environment.config.propertyOrNull("ktor.urls")?.getList() ?: emptyList(),
|
||||
corSettings = corSettings,
|
||||
processor = MessengerProcessor(corSettings),
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package ru.otus.messenger.app.v1
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import ru.otus.messenger.api.v1.models.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val clCreate: KClass<*> = ApplicationCall::createChat::class
|
||||
suspend fun ApplicationCall.createChat(appSettings: MessengerAppSettings) =
|
||||
processV1<ChatCreateRequest, ChatCreateResponse>(appSettings, clCreate,"create")
|
||||
|
||||
val clRead: KClass<*> = ApplicationCall::readChat::class
|
||||
suspend fun ApplicationCall.readChat(appSettings: MessengerAppSettings) =
|
||||
processV1<ChatReadRequest, ChatReadResponse>(appSettings, clRead, "read")
|
||||
|
||||
val clDelete: KClass<*> = ApplicationCall::deleteChat::class
|
||||
suspend fun ApplicationCall.deleteChat(appSettings: MessengerAppSettings) =
|
||||
processV1<ChatDeleteRequest, ChatDeleteResponse>(appSettings, clDelete, "delete")
|
||||
|
||||
val clSearch: KClass<*> = ApplicationCall::searchChat::class
|
||||
suspend fun ApplicationCall.searchChat(appSettings: MessengerAppSettings) =
|
||||
processV1<ChatSearchRequest, ChatSearchResponse>(appSettings, clSearch, "search")
|
||||
@ -0,0 +1,25 @@
|
||||
package ru.otus.messenger.app.v1
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import ru.otus.messenger.api.v1.models.IRequest
|
||||
import ru.otus.messenger.api.v1.models.IResponse
|
||||
import ru.otus.messenger.app.common.controllerHelper
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import ru.otus.messenger.api.v1.mappers.fromTransport
|
||||
import ru.otus.messenger.api.v1.mappers.toTransportChat
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
suspend inline fun <reified Q : IRequest, @Suppress("unused") reified R : IResponse> ApplicationCall.processV1(
|
||||
appSettings: MessengerAppSettings,
|
||||
clazz: KClass<*>,
|
||||
logId: String,
|
||||
) = appSettings.controllerHelper(
|
||||
{
|
||||
fromTransport(receive<Q>())
|
||||
},
|
||||
{ respond(toTransportChat()) },
|
||||
clazz,
|
||||
logId,
|
||||
)
|
||||
@ -0,0 +1,21 @@
|
||||
package ru.otus.messenger.app.v1
|
||||
|
||||
import io.ktor.server.routing.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
|
||||
fun Route.v1Chat(appSettings: MessengerAppSettings) {
|
||||
route("chat") {
|
||||
post("create") {
|
||||
call.createChat(appSettings)
|
||||
}
|
||||
post("read") {
|
||||
call.readChat(appSettings)
|
||||
}
|
||||
post("delete") {
|
||||
call.deleteChat(appSettings)
|
||||
}
|
||||
post("search") {
|
||||
call.searchChat(appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package ru.otus.messenger.app.v1
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import ru.otus.messenger.api.v1.apiV1Mapper
|
||||
import ru.otus.messenger.api.v1.mappers.fromTransport
|
||||
import ru.otus.messenger.api.v1.mappers.toTransportInit
|
||||
import ru.otus.messenger.api.v1.mappers.toTransportChat
|
||||
import ru.otus.messenger.api.v1.models.IRequest
|
||||
import ru.otus.messenger.app.base.KtorWsSessionV1
|
||||
import ru.otus.messenger.app.common.MessengerAppSettings
|
||||
import ru.otus.messenger.app.common.controllerHelper
|
||||
import ru.otus.messenger.common.models.ChatCommand
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
private val clWsV1: KClass<*> = WebSocketSession::wsHandlerV1::class
|
||||
suspend fun WebSocketSession.wsHandlerV1(appSettings: MessengerAppSettings) = with(KtorWsSessionV1(this)) {
|
||||
val sessions = appSettings.corSettings.wsSessions
|
||||
sessions.add(this)
|
||||
|
||||
// Handle init request
|
||||
appSettings.controllerHelper(
|
||||
{
|
||||
command = ChatCommand.INIT
|
||||
wsSession = this@with
|
||||
},
|
||||
{ outgoing.send(Frame.Text(apiV1Mapper.writeValueAsString(toTransportInit()))) },
|
||||
clWsV1,
|
||||
"wsV1-init"
|
||||
)
|
||||
|
||||
// Handle flow
|
||||
incoming.receiveAsFlow().mapNotNull {
|
||||
val frame = it as? Frame.Text ?: return@mapNotNull
|
||||
// Handle without flow destruction
|
||||
try {
|
||||
appSettings.controllerHelper(
|
||||
{
|
||||
fromTransport(apiV1Mapper.readValue<IRequest>(frame.readText()))
|
||||
wsSession = this@with
|
||||
},
|
||||
{
|
||||
val result = apiV1Mapper.writeValueAsString(toTransportChat())
|
||||
// If change request, response is sent to everyone
|
||||
outgoing.send(Frame.Text(result))
|
||||
},
|
||||
clWsV1,
|
||||
"wsV1-handle"
|
||||
)
|
||||
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
sessions.remove(this@with)
|
||||
} finally {
|
||||
// Handle finish request
|
||||
appSettings.controllerHelper(
|
||||
{
|
||||
command = ChatCommand.FINISH
|
||||
wsSession = this@with
|
||||
},
|
||||
{ },
|
||||
clWsV1,
|
||||
"wsV1-finish"
|
||||
)
|
||||
sessions.remove(this@with)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
ktor:
|
||||
development: true
|
||||
deployment:
|
||||
port: 8080
|
||||
watch:
|
||||
- classes
|
||||
- resources
|
||||
application:
|
||||
modules:
|
||||
- ru.otus.messenger.app.ApplicationKt.module
|
||||
122
ok-messenger-be/ok-messenger-app/src/main/resources/logback.xml
Normal file
122
ok-messenger-be/ok-messenger-app/src/main/resources/logback.xml
Normal file
@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE configuration>
|
||||
<configuration scan="true" scanPeriod="30 seconds" debug="false">
|
||||
|
||||
<property name="LOGS_FB_HOSTS" value="${LOGS_FB_HOSTS:-127.0.0.1}"/>
|
||||
<property name="LOGS_FB_PORT" value="${LOGS_FB_PORT:-24224}"/>
|
||||
<property name="SERVICE_NAME" value="${SERVICE_NAME:-ok_messenger}"/>
|
||||
<property name="LOG_OTUS_LEVEL" value="${LOG_OTUS_LEVEL:-info}"/>
|
||||
<property name="LOG_COMMON_LEVEL" value="${LOG_COMMON_LEVEL:-error}"/>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level[%marker] %logger{36} - %msg%n%mdc%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_FB_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
|
||||
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_FB_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
<appender name="fluentd" class="ch.qos.logback.more.appenders.DataFluentAppender">
|
||||
<tag>app.logs</tag>
|
||||
<label>normal</label>
|
||||
<remoteHost>${LOGS_FB_HOSTS}</remoteHost>
|
||||
<port>${LOGS_FB_PORT}</port>
|
||||
<maxQueueSize>20</maxQueueSize>
|
||||
|
||||
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||
<providers>
|
||||
<timestamp/>
|
||||
<version/>
|
||||
<pattern>
|
||||
<pattern>
|
||||
{
|
||||
"component": "${SERVICE_NAME}",
|
||||
"container-id": "${HOSTNAME}"
|
||||
}
|
||||
</pattern>
|
||||
</pattern>
|
||||
<message/>
|
||||
<loggerName/>
|
||||
<threadName/>
|
||||
<logLevel/>
|
||||
<logstashMarkers/>
|
||||
<callerData/>
|
||||
<stackTrace/>
|
||||
<context/>
|
||||
<mdc/>
|
||||
<logstashMarkers/>
|
||||
<arguments/>
|
||||
<tags/>
|
||||
</providers>
|
||||
</encoder>
|
||||
</appender>
|
||||
<!-- </then>-->
|
||||
<!-- </if>-->
|
||||
|
||||
<!-- <!– For ELK-Stack: Kafka log's host –>-->
|
||||
<!-- <property name="LOGS_KAFKA_HOSTS" value="${BOOTSTRAP_SERVERS:-localhost:9094}"/>-->
|
||||
<!-- <!– For ELK-Stack: Kafka log's topic –>-->
|
||||
<!-- <property name="LOGS_KAFKA_TOPIC" value="${LOGS_KAFKA_TOPIC:-ok-mkpl-logs}"/>-->
|
||||
<!-- <appender name="asyncMyLogKafka"-->
|
||||
<!-- class="net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender">-->
|
||||
<!-- <if condition='!property("LOGS_KAFKA_HOSTS").equals("LOGS_KAFKA_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_KAFKA_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
<!-- <appender name="kafkaVerboseAppender"-->
|
||||
<!-- class="com.github.danielwegener.logback.kafka.KafkaAppender">-->
|
||||
<!-- <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">-->
|
||||
<!-- <providers>-->
|
||||
<!-- <timestamp/>-->
|
||||
<!-- <version/>-->
|
||||
<!-- <pattern>-->
|
||||
<!-- <pattern>-->
|
||||
<!-- {-->
|
||||
<!-- "component": "${SERVICE_NAME}",-->
|
||||
<!-- "container-id": "${HOSTNAME}"-->
|
||||
<!-- }-->
|
||||
<!-- </pattern>-->
|
||||
<!-- </pattern>-->
|
||||
<!-- <message/>-->
|
||||
<!-- <loggerName/>-->
|
||||
<!-- <threadName/>-->
|
||||
<!-- <logLevel/>-->
|
||||
<!-- <logstashMarkers/>-->
|
||||
<!-- <callerData/>-->
|
||||
<!-- <stackTrace/>-->
|
||||
<!-- <context/>-->
|
||||
<!-- <mdc/>-->
|
||||
<!-- <logstashMarkers/>-->
|
||||
<!-- <arguments/>-->
|
||||
<!-- <tags/>-->
|
||||
<!-- </providers>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- <topic>${LOGS_KAFKA_TOPIC}</topic>-->
|
||||
<!-- <deliveryStrategy-->
|
||||
<!-- class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy"/>-->
|
||||
<!-- <producerConfig>bootstrap.servers=${LOGS_KAFKA_HOSTS}</producerConfig>-->
|
||||
<!-- </appender>-->
|
||||
<!-- </then>-->
|
||||
<!-- </if>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<logger name="ru.otus" level="${LOG_OTUS_LEVEL}" additivity="false">
|
||||
<appender-ref ref="fluentd"/>
|
||||
<!-- <appender-ref ref="asyncMyLogKafka"/>-->
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</logger>
|
||||
|
||||
<logger name="Application" level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</logger>
|
||||
|
||||
<root level="${LOG_COMMON_LEVEL}">
|
||||
<appender-ref ref="fluentd"/>
|
||||
<!-- <appender-ref ref="asyncMyLogKafka"/>-->
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@ -0,0 +1,21 @@
|
||||
package ru.otus.messenger.app
|
||||
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlin.test.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ApplicationTest {
|
||||
|
||||
@Test
|
||||
fun testRoot() = testApplication {
|
||||
application {
|
||||
module()
|
||||
}
|
||||
client.get("/").apply {
|
||||
assertEquals(HttpStatusCode.Companion.OK, status)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package ru.otus.messenger.app.common
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import ru.otus.messenger.api.v1.mappers.fromTransport
|
||||
import ru.otus.messenger.api.v1.mappers.toTransportChat
|
||||
import ru.otus.messenger.api.v1.models.*
|
||||
import ru.otus.messenger.biz.MessengerProcessor
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ControllerTest {
|
||||
private val request = ChatCreateRequest(
|
||||
chat = ChatCreateRequestAllOfChat(
|
||||
title = "New chat title",
|
||||
description = "New chat description",
|
||||
type = ChatCreateRequestAllOfChat.Type.GROUP,
|
||||
mode = ChatCreateRequestAllOfChat.Mode.PERSONAL,
|
||||
ownerId = UUID.randomUUID().toString(),
|
||||
participants = setOf(),
|
||||
metadata = """
|
||||
{
|
||||
"organization": "BlancLabs",
|
||||
"sampleName": "B26",
|
||||
"analyte": "DNA"
|
||||
}
|
||||
""".trimIndent()
|
||||
),
|
||||
debug = Debug(mode = DebugMode.STUB, stub = DebugStubs.SUCCESS)
|
||||
)
|
||||
|
||||
private val appSettings: MessengerAppSettings = object : MessengerAppSettings {
|
||||
override val corSettings: MessengerCorSettings = MessengerCorSettings()
|
||||
override val processor: MessengerProcessor = MessengerProcessor(corSettings)
|
||||
}
|
||||
|
||||
class TestApplicationCall(private val request: IRequest) {
|
||||
var response: IResponse? = null
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : IRequest> receive(): T = request as T
|
||||
fun respond(response: IResponse) {
|
||||
this.response = response
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TestApplicationCall.createReport(appSettings: MessengerAppSettings) {
|
||||
val response = appSettings.controllerHelper(
|
||||
{ fromTransport(receive<ChatCreateRequest>()) },
|
||||
{ toTransportChat() },
|
||||
ControllerTest::class,
|
||||
"controller-v1-test"
|
||||
)
|
||||
respond(response)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ktorHelperTest() = runTest {
|
||||
val testApp = TestApplicationCall(request).apply { createReport(appSettings) }
|
||||
val response = testApp.response as ChatCreateResponse
|
||||
assertEquals(ResponseResult.SUCCESS, response.result)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
package ru.otus.messenger.app.stub
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
package ru.otus.messenger.app.websocket
|
||||
|
||||
import io.ktor.client.plugins.websocket.*
|
||||
import io.ktor.serialization.jackson.*
|
||||
import io.ktor.server.testing.*
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import ru.otus.messenger.api.v1.models.*
|
||||
import ru.otus.messenger.app.common.MessengerAppSettingsData
|
||||
import ru.otus.messenger.app.module
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class V1WebsocketStubTest {
|
||||
|
||||
@Test
|
||||
fun createStub() {
|
||||
val request = ChatCreateRequest(
|
||||
chat = ChatCreateRequestAllOfChat(
|
||||
title = "New chat title",
|
||||
description = "New chat description",
|
||||
type = ChatCreateRequestAllOfChat.Type.CHANNEL,
|
||||
mode = ChatCreateRequestAllOfChat.Mode.WORK,
|
||||
ownerId = UUID.randomUUID().toString(),
|
||||
participants = setOf(UUID.randomUUID().toString()),
|
||||
metadata = """
|
||||
{
|
||||
"organization": "BlancLabs",
|
||||
"sampleName": "B26",
|
||||
"analyte": "DNA"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
debug = Debug(
|
||||
mode = DebugMode.STUB,
|
||||
stub = DebugStubs.SUCCESS
|
||||
)
|
||||
)
|
||||
|
||||
testMethod<IResponse>(request) {
|
||||
assertEquals(ResponseResult.SUCCESS, it.result)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readStub() {
|
||||
val request = ChatReadRequest(
|
||||
chatId = UUID.randomUUID().toString(),
|
||||
debug = Debug(
|
||||
mode = DebugMode.STUB,
|
||||
stub = DebugStubs.SUCCESS
|
||||
)
|
||||
)
|
||||
|
||||
testMethod<IResponse>(request) {
|
||||
assertEquals(ResponseResult.SUCCESS, it.result)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteStub() {
|
||||
val request = ChatDeleteRequest(
|
||||
chatId = UUID.randomUUID().toString(),
|
||||
debug = Debug(
|
||||
mode = DebugMode.STUB,
|
||||
stub = DebugStubs.SUCCESS
|
||||
)
|
||||
)
|
||||
|
||||
testMethod<IResponse>(request) {
|
||||
assertEquals(ResponseResult.SUCCESS, it.result)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchStub() {
|
||||
val request = ChatSearchRequest(
|
||||
criteria = ChatSearchRequestAllOfCriteria(
|
||||
title = "Chat search title",
|
||||
type = ChatSearchRequestAllOfCriteria.Type.CHANNEL,
|
||||
mode = ChatSearchRequestAllOfCriteria.Mode.WORK,
|
||||
),
|
||||
debug = Debug(
|
||||
mode = DebugMode.STUB,
|
||||
stub = DebugStubs.SUCCESS
|
||||
)
|
||||
)
|
||||
|
||||
testMethod<IResponse>(request) {
|
||||
assertEquals(ResponseResult.SUCCESS, it.result)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> testMethod(
|
||||
request: IRequest,
|
||||
crossinline assertBlock: (T) -> Unit
|
||||
) = testApplication {
|
||||
application { module(MessengerAppSettingsData(corSettings = MessengerCorSettings())) }
|
||||
val client = createClient {
|
||||
install(WebSockets) {
|
||||
contentConverter = JacksonWebsocketContentConverter()
|
||||
}
|
||||
}
|
||||
|
||||
client.webSocket("/v1/ws") {
|
||||
withTimeout(3000) {
|
||||
val response = receiveDeserialized<IResponse>() as T
|
||||
assertIs<ChatInitResponse>(response)
|
||||
}
|
||||
sendSerialized(request)
|
||||
withTimeout(3000) {
|
||||
val response = receiveDeserialized<IResponse>() as T
|
||||
assertBlock(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
ok-messenger-be/ok-messenger-biz/build.gradle.kts
Normal file
11
ok-messenger-be/ok-messenger-biz/build.gradle.kts
Normal file
@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
id("build-jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(project(":ok-messenger-common"))
|
||||
implementation(project(":ok-messenger-stubs"))
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package ru.otus.messenger.biz
|
||||
|
||||
import ru.otus.messenger.common.MessengerContext
|
||||
import ru.otus.messenger.common.MessengerCorSettings
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
import ru.otus.messenger.common.models.ChatState
|
||||
import ru.otus.messenger.common.models.ChatType
|
||||
import ru.otus.messenger.stubs.MessengerChatStub
|
||||
|
||||
@Suppress("unused", "RedundantSuspendModifier")
|
||||
class MessengerProcessor(val corSettings: MessengerCorSettings) {
|
||||
suspend fun exec(ctx: MessengerContext) {
|
||||
ctx.chatResponse = MessengerChatStub.get()
|
||||
ctx.chatsResponse = MessengerChatStub.prepareSearchList("New chat", ChatType.GROUP, ChatMode.PERSONAL).toMutableList()
|
||||
ctx.state = ChatState.RUNNING
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.kotlin.datetime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
api("ru.otus.messenger.libs:ok-messenger-lib-logging")
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
}
|
||||
@ -3,19 +3,21 @@ package ru.otus.messenger.common
|
||||
import kotlinx.datetime.Instant
|
||||
import ru.otus.messenger.common.models.*
|
||||
import ru.otus.messenger.common.stubs.Stubs
|
||||
import ru.otus.messenger.common.ws.IMessengerWsSession
|
||||
|
||||
data class ChatContext(
|
||||
data class MessengerContext(
|
||||
var command: ChatCommand = ChatCommand.NONE,
|
||||
var state: ChatState = ChatState.NONE,
|
||||
val errors: MutableList<ChatError> = mutableListOf(),
|
||||
|
||||
var workMode: WorkMode = WorkMode.PROD,
|
||||
var stubCase: Stubs = Stubs.NONE,
|
||||
var wsSession: IMessengerWsSession = IMessengerWsSession.NONE,
|
||||
|
||||
var requestId: RequestId = RequestId.NONE,
|
||||
var timeStart: Instant = Instant.NONE,
|
||||
var chatRequest: MessengerChat = MessengerChat(),
|
||||
var chatFilterRequest: ChatSearchFilter = ChatSearchFilter(),
|
||||
var chatFilterRequest: ChatSearchFilter = ChatSearchFilter.NONE,
|
||||
|
||||
var chatResponse: MessengerChat = MessengerChat(),
|
||||
var chatsResponse: MutableList<MessengerChat> = mutableListOf(),
|
||||
@ -0,0 +1,13 @@
|
||||
package ru.otus.messenger.common
|
||||
|
||||
import ru.otus.messenger.common.ws.IMessengerWsSessionRepo
|
||||
import ru.otus.messenger.logging.common.LoggerProvider
|
||||
|
||||
data class MessengerCorSettings(
|
||||
val loggerProvider: LoggerProvider = LoggerProvider(),
|
||||
val wsSessions: IMessengerWsSessionRepo = IMessengerWsSessionRepo.NONE,
|
||||
) {
|
||||
companion object {
|
||||
val NONE = MessengerCorSettings()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package ru.otus.messenger.common.helpers
|
||||
|
||||
import ru.otus.messenger.common.models.ChatError
|
||||
|
||||
fun Throwable.asMessengerError(
|
||||
code: String = "unknown",
|
||||
group: String = "exceptions",
|
||||
message: String = this.message ?: "",
|
||||
) = ChatError(
|
||||
code = code,
|
||||
group = group,
|
||||
field = "",
|
||||
message = message,
|
||||
exception = this,
|
||||
)
|
||||
@ -7,4 +7,6 @@ enum class ChatCommand {
|
||||
DELETE,
|
||||
SEARCH,
|
||||
UPDATE,
|
||||
INIT,
|
||||
FINISH,
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
package ru.otus.messenger.common.models
|
||||
|
||||
import ru.otus.messenger.logging.common.LogLevel
|
||||
|
||||
data class ChatError(
|
||||
val code: String = "",
|
||||
val group: String = "",
|
||||
val field: String = "",
|
||||
val message: String = "",
|
||||
val level: LogLevel = LogLevel.ERROR,
|
||||
val exception: Throwable? = null,
|
||||
)
|
||||
@ -1,7 +1,30 @@
|
||||
package ru.otus.messenger.common.models
|
||||
|
||||
data class ChatSearchFilter(
|
||||
var searchString: String = "",
|
||||
var searchFields: List<SearchField> = emptyList(),
|
||||
var ownerId: ChatOwnerId = ChatOwnerId.NONE,
|
||||
var type: ChatType = ChatType.NONE,
|
||||
)
|
||||
var mode: ChatMode = ChatMode.NONE,
|
||||
) {
|
||||
interface SearchField {
|
||||
val fieldName: String
|
||||
val action: SearchAction
|
||||
}
|
||||
|
||||
enum class SearchAction {
|
||||
CONTAINS,
|
||||
EQUALS,
|
||||
MORE,
|
||||
LESS
|
||||
}
|
||||
|
||||
data class StringSearchField(
|
||||
override val fieldName: String,
|
||||
override val action: SearchAction = SearchAction.CONTAINS,
|
||||
val stringValue: String,
|
||||
) : SearchField
|
||||
|
||||
companion object {
|
||||
val NONE = ChatSearchFilter()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package ru.otus.messenger.common.ws
|
||||
|
||||
interface IMessengerWsSession {
|
||||
suspend fun <T> send(obj: T)
|
||||
companion object {
|
||||
val NONE = object : IMessengerWsSession {
|
||||
override suspend fun <T> send(obj: T) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package ru.otus.messenger.common.ws
|
||||
|
||||
interface IMessengerWsSessionRepo {
|
||||
fun add(session: IMessengerWsSession)
|
||||
fun clearAll()
|
||||
fun remove(session: IMessengerWsSession)
|
||||
suspend fun <K> sendAll(obj: K)
|
||||
|
||||
companion object {
|
||||
val NONE = object : IMessengerWsSessionRepo {
|
||||
override fun add(session: IMessengerWsSession) {}
|
||||
override fun clearAll() {}
|
||||
override fun remove(session: IMessengerWsSession) {}
|
||||
override suspend fun <K> sendAll(obj: K) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
ok-messenger-be/ok-messenger-stubs/build.gradle.kts
Normal file
12
ok-messenger-be/ok-messenger-stubs/build.gradle.kts
Normal file
@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id("build-jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.kotlin.datetime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(project(":ok-messenger-common"))
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package ru.otus.messenger.stubs
|
||||
|
||||
import ru.otus.messenger.common.models.ChatMode
|
||||
import ru.otus.messenger.common.models.ChatType
|
||||
import ru.otus.messenger.common.models.MessengerChat
|
||||
import ru.otus.messenger.stubs.MessengerChatStubSample.CHAT_SAMPLE_1
|
||||
import ru.otus.messenger.stubs.MessengerChatStubSample.CHAT_SAMPLE_2
|
||||
|
||||
object MessengerChatStub {
|
||||
fun get(): MessengerChat = CHAT_SAMPLE_1.copy()
|
||||
|
||||
fun prepareResult(block: MessengerChat.() -> Unit): MessengerChat = get().apply(block)
|
||||
|
||||
fun prepareSearchList(
|
||||
chatTitle: String,
|
||||
chatType: ChatType,
|
||||
chatMode: ChatMode,
|
||||
) = listOf(
|
||||
CHAT_SAMPLE_1,
|
||||
CHAT_SAMPLE_2
|
||||
).filter { it.title == chatTitle && it.type == chatType && it.mode == chatMode }
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package ru.otus.messenger.stubs
|
||||
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import ru.otus.messenger.common.models.*
|
||||
|
||||
object MessengerChatStubSample {
|
||||
val chatId = UUID.randomUUID().toString()
|
||||
val chatOwnerId = UUID.randomUUID().toString()
|
||||
val participants = MutableList(3) { ChatUserId(UUID.randomUUID().toString()) }
|
||||
private val timestamp = Instant.parse("2025-01-31T01:30:00.000-05:00")
|
||||
|
||||
val CHAT_SAMPLE_1: MessengerChat
|
||||
get() = MessengerChat(
|
||||
id = ChatId(chatId),
|
||||
title = "New chat",
|
||||
description = "New chat description",
|
||||
type = ChatType.GROUP,
|
||||
mode = ChatMode.PERSONAL,
|
||||
ownerId = ChatOwnerId(chatOwnerId),
|
||||
participants = (participants + ChatUserId(chatOwnerId)).toMutableSet(),
|
||||
createdAt = timestamp,
|
||||
updatedAt = timestamp.plus(10.hours),
|
||||
isArchived = ChatArchiveFlag(false),
|
||||
metadata = ChatMetadata(
|
||||
buildJsonObject {
|
||||
put("sampleId", "uuid4")
|
||||
put("testParam", "test")
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val CHAT_SAMPLE_2: MessengerChat
|
||||
get() = MessengerChat(
|
||||
id = ChatId(chatId),
|
||||
title = "New chat",
|
||||
description = "New chat description",
|
||||
type = ChatType.GROUP,
|
||||
mode = ChatMode.WORK,
|
||||
ownerId = ChatOwnerId(chatOwnerId),
|
||||
participants = (participants + ChatUserId(chatOwnerId)).toMutableSet(),
|
||||
createdAt = timestamp.plus(100.hours),
|
||||
updatedAt = timestamp.plus(101.hours),
|
||||
isArchived = ChatArchiveFlag(false),
|
||||
metadata = ChatMetadata(
|
||||
buildJsonObject {
|
||||
put("sampleId", "uuid5")
|
||||
put("testParam", "test")
|
||||
put("organization", "BlancLabs")
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -23,6 +23,10 @@ rootProject.name = "ok-messenger-be"
|
||||
|
||||
//enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
include(":ok-messenger-api-log-v1")
|
||||
include(":ok-messenger-api-v1")
|
||||
include(":ok-messenger-api-v1-mappers")
|
||||
include(":ok-messenger-common")
|
||||
include(":ok-messenger-stubs")
|
||||
include(":ok-messenger-app")
|
||||
include(":ok-messenger-biz")
|
||||
|
||||
31
ok-messenger-libs/build.gradle.kts
Normal file
31
ok-messenger-libs/build.gradle.kts
Normal file
@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
alias(libs.plugins.jvm) apply false
|
||||
}
|
||||
|
||||
group = "ru.otus.messenger.libs"
|
||||
version = "0.0.1"
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
group = rootProject.group
|
||||
version = rootProject.version
|
||||
}
|
||||
|
||||
ext {
|
||||
val specDir = layout.projectDirectory.dir("../specs")
|
||||
set("spec-v1", specDir.file("specs-v1.yaml").toString())
|
||||
}
|
||||
|
||||
tasks {
|
||||
arrayOf("build", "clean", "check").forEach {tsk ->
|
||||
create(tsk) {
|
||||
group = "build"
|
||||
dependsOn(subprojects.map { it.getTasksByName(tsk,false)})
|
||||
}
|
||||
}
|
||||
}
|
||||
3
ok-messenger-libs/gradle.properties
Normal file
3
ok-messenger-libs/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
||||
kotlin.code.style=official
|
||||
kotlin.native.ignoreDisabledTargets=true
|
||||
#kotlin.native.cacheKind.linuxX64=none
|
||||
16
ok-messenger-libs/ok-messenger-lib-logging/build.gradle.kts
Normal file
16
ok-messenger-libs/ok-messenger-lib-logging/build.gradle.kts
Normal file
@ -0,0 +1,16 @@
|
||||
plugins {
|
||||
id("build-jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.kotlin.coroutines)
|
||||
implementation(libs.kotlin.datetime)
|
||||
implementation(libs.logback.classic)
|
||||
implementation(libs.logback.logstash)
|
||||
api(libs.logback.appenders)
|
||||
api(libs.logger.fluentd)
|
||||
|
||||
testImplementation(kotlin("test-junit"))
|
||||
testImplementation(libs.kotlin.coroutines.test)
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package ru.otus.messenger.logging
|
||||
|
||||
import org.slf4j.Marker
|
||||
|
||||
/**
|
||||
* Реализация SLF4J маркера логов для маркировки различных типов логов
|
||||
*/
|
||||
class DefaultMarker(
|
||||
private val name: String,
|
||||
private val submarkers: List<Marker> = emptyList()
|
||||
): Marker {
|
||||
override fun getName(): String = name
|
||||
|
||||
override fun add(reference: Marker) {}
|
||||
|
||||
override fun remove(reference: Marker): Boolean = false
|
||||
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("hasReferences()"))
|
||||
override fun hasChildren(): Boolean = hasReferences()
|
||||
|
||||
override fun hasReferences(): Boolean = submarkers.isNotEmpty()
|
||||
|
||||
override fun iterator(): Iterator<Marker> = submarkers.iterator()
|
||||
|
||||
override fun contains(other: Marker): Boolean = submarkers.contains(other)
|
||||
|
||||
override fun contains(name: String): Boolean = submarkers.any { it.name == name }
|
||||
|
||||
override fun toString(): String = arrayOf(name, *submarkers.toTypedArray()).joinToString(",")
|
||||
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package ru.otus.messenger.logging
|
||||
|
||||
import ch.qos.logback.classic.Logger
|
||||
import net.logstash.logback.argument.StructuredArguments
|
||||
import org.slf4j.Marker
|
||||
import org.slf4j.event.KeyValuePair
|
||||
import org.slf4j.event.Level
|
||||
import org.slf4j.event.LoggingEvent
|
||||
import ru.otus.messenger.logging.common.ILogWrapper
|
||||
import ru.otus.messenger.logging.common.LogLevel
|
||||
import java.time.Instant
|
||||
|
||||
class LogWrapperLogback(
|
||||
/**
|
||||
* Экземпляр логера (Logback)
|
||||
*/
|
||||
val logger: Logger,
|
||||
/**
|
||||
* Идентификатор логера. Пробрасывается в Logback и замещает loggerClass. Также используется в сообщения
|
||||
* логера о входе и выходе из функции.
|
||||
*/
|
||||
override val loggerId: String = logger.name,
|
||||
) : ILogWrapper {
|
||||
/**
|
||||
* Основная функция для логирования
|
||||
*/
|
||||
private fun log(
|
||||
msg: String = "",
|
||||
level: Level = Level.TRACE,
|
||||
marker: Marker = DefaultMarker("DEV"),
|
||||
e: Throwable? = null,
|
||||
data: Any? = null,
|
||||
objs: Map<String, Any>? = null,
|
||||
) {
|
||||
logger.log(object : LoggingEvent {
|
||||
override fun getThrowable() = e
|
||||
override fun getTimeStamp(): Long = Instant.now().toEpochMilli()
|
||||
override fun getThreadName(): String = Thread.currentThread().name
|
||||
override fun getMessage(): String = msg
|
||||
override fun getArguments(): MutableList<Any> = argumentArray.toMutableList()
|
||||
override fun getArgumentArray(): Array<out Any> = data
|
||||
?.let { d ->
|
||||
listOfNotNull(
|
||||
objs?.map { StructuredArguments.keyValue(it.key, it.value) }?.toTypedArray(),
|
||||
StructuredArguments.keyValue("data", d),
|
||||
).toTypedArray()
|
||||
}
|
||||
?: objs?.mapNotNull { StructuredArguments.keyValue(it.key, it.value) }?.toTypedArray()
|
||||
?: emptyArray()
|
||||
|
||||
override fun getMarkers(): MutableList<Marker> = mutableListOf(marker)
|
||||
override fun getKeyValuePairs(): MutableList<KeyValuePair> = objs
|
||||
?.mapNotNull {
|
||||
it.let { KeyValuePair(it.key, it.value) }
|
||||
}
|
||||
?.toMutableList()
|
||||
?: mutableListOf()
|
||||
|
||||
override fun getLevel(): Level = level
|
||||
override fun getLoggerName(): String = logger.name
|
||||
})
|
||||
}
|
||||
|
||||
override fun log(
|
||||
msg: String,
|
||||
level: LogLevel,
|
||||
marker: String,
|
||||
e: Throwable?,
|
||||
data: Any?,
|
||||
objs: Map<String, Any>?,
|
||||
) = log(
|
||||
msg = msg,
|
||||
level = level.toSlf(),
|
||||
marker = DefaultMarker(marker),
|
||||
e = e,
|
||||
data = data,
|
||||
objs = objs,
|
||||
)
|
||||
|
||||
private fun LogLevel.toSlf() = when (this) {
|
||||
LogLevel.ERROR -> Level.ERROR
|
||||
LogLevel.WARN -> Level.WARN
|
||||
LogLevel.INFO -> Level.INFO
|
||||
LogLevel.DEBUG -> Level.DEBUG
|
||||
LogLevel.TRACE -> Level.TRACE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package ru.otus.messenger.logging
|
||||
|
||||
import ch.qos.logback.classic.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import ru.otus.messenger.logging.common.ILogWrapper
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Generate internal MpLogContext logger
|
||||
*
|
||||
* @param logger Logback instance from [LoggerFactory.getLogger()]
|
||||
*/
|
||||
fun loggerLogback(logger: Logger): ILogWrapper = LogWrapperLogback(
|
||||
logger = logger,
|
||||
loggerId = logger.name,
|
||||
)
|
||||
|
||||
fun loggerLogback(clazz: KClass<*>): ILogWrapper = loggerLogback(LoggerFactory.getLogger(clazz.java) as Logger)
|
||||
|
||||
@Suppress("unused")
|
||||
fun loggerLogback(loggerId: String): ILogWrapper = loggerLogback(LoggerFactory.getLogger(loggerId) as Logger)
|
||||
@ -0,0 +1,113 @@
|
||||
package ru.otus.messenger.logging.common
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Suppress("unused")
|
||||
interface ILogWrapper: AutoCloseable {
|
||||
val loggerId: String
|
||||
|
||||
fun log(
|
||||
msg: String = "",
|
||||
level: LogLevel = LogLevel.TRACE,
|
||||
marker: String = "DEV",
|
||||
e: Throwable? = null,
|
||||
data: Any? = null,
|
||||
objs: Map<String, Any>? = null,
|
||||
)
|
||||
|
||||
fun error(
|
||||
msg: String = "",
|
||||
marker: String = "DEV",
|
||||
e: Throwable? = null,
|
||||
data: Any? = null,
|
||||
objs: Map<String, Any>? = null,
|
||||
) = log(msg, LogLevel.ERROR, marker, e, data, objs)
|
||||
|
||||
fun info(
|
||||
msg: String = "",
|
||||
marker: String = "DEV",
|
||||
data: Any? = null,
|
||||
objs: Map<String, Any>? = null,
|
||||
) = log(msg, LogLevel.INFO, marker, null, data, objs)
|
||||
|
||||
fun debug(
|
||||
msg: String = "",
|
||||
marker: String = "DEV",
|
||||
data: Any? = null,
|
||||
objs: Map<String, Any>? = null,
|
||||
) = log(msg, LogLevel.DEBUG, marker, null, data, objs)
|
||||
|
||||
/**
|
||||
* Функция обертка для выполнения прикладного кода с логированием перед выполнением и после
|
||||
*/
|
||||
suspend fun <T> doWithLogging(
|
||||
id: String = "",
|
||||
level: LogLevel = LogLevel.INFO,
|
||||
block: suspend () -> T,
|
||||
): T = try {
|
||||
log("Started $loggerId $id", level)
|
||||
val (res, diffTime) = measureTimedValue { block() }
|
||||
|
||||
log(
|
||||
msg = "Finished $loggerId $id",
|
||||
level = level,
|
||||
objs = mapOf("metricHandleTime" to diffTime.toIsoString())
|
||||
)
|
||||
res
|
||||
} catch (e: Throwable) {
|
||||
log(
|
||||
msg = "Failed $loggerId $id",
|
||||
level = LogLevel.ERROR,
|
||||
e = e
|
||||
)
|
||||
throw e
|
||||
}
|
||||
|
||||
/**
|
||||
* Функция обертка для выполнения прикладного кода с логированием ошибки
|
||||
*/
|
||||
suspend fun <T> doWithErrorLogging(
|
||||
id: String = "",
|
||||
throwRequired: Boolean = true,
|
||||
block: suspend () -> T,
|
||||
): T? = try {
|
||||
val result = block()
|
||||
result
|
||||
} catch (e: Throwable) {
|
||||
log(
|
||||
msg = "Failed $loggerId $id",
|
||||
level = LogLevel.ERROR,
|
||||
e = e
|
||||
)
|
||||
if (throwRequired) throw e else null
|
||||
}
|
||||
|
||||
override fun close() {}
|
||||
|
||||
companion object {
|
||||
val DEFAULT = object: ILogWrapper {
|
||||
override val loggerId: String = "NONE"
|
||||
|
||||
override fun log(
|
||||
msg: String,
|
||||
level: LogLevel,
|
||||
marker: String,
|
||||
e: Throwable?,
|
||||
data: Any?,
|
||||
objs: Map<String, Any>?,
|
||||
) {
|
||||
val markerString = marker
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let { " ($it)" }
|
||||
val args = listOfNotNull(
|
||||
"${Clock.System.now()} [${level.name}]$markerString: $msg",
|
||||
e?.let { "${it.message ?: "Unknown reason"}:\n${it.stackTraceToString()}" },
|
||||
data.toString(),
|
||||
objs.toString(),
|
||||
)
|
||||
println(args.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package ru.otus.messenger.logging.common
|
||||
|
||||
enum class LogLevel(
|
||||
private val levelInt: Int,
|
||||
private val levelStr: String,
|
||||
) {
|
||||
ERROR(40, "ERROR"),
|
||||
WARN(30, "WARN"),
|
||||
INFO(20, "INFO"),
|
||||
DEBUG(10, "DEBUG"),
|
||||
TRACE(0, "TRACE");
|
||||
|
||||
@Suppress("unused")
|
||||
fun toInt(): Int {
|
||||
return levelInt
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of this Level.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return levelStr
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package ru.otus.messenger.logging.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
|
||||
/**
|
||||
* Инициализирует выбранный логер
|
||||
*
|
||||
* ```kotlin
|
||||
* // Обычно логер вызывается вот так
|
||||
* val logger = LoggerFactory.getLogger(this::class.java)
|
||||
* // Мы создаем экземпляр логер-провайдера вот так
|
||||
* val loggerProvider = MkpLoggerProvider { clazz -> mpLoggerLogback(clazz) }
|
||||
*
|
||||
* // В дальнейшем будем использовать этот экземпляр вот так:
|
||||
* val logger = loggerProvider.logger(this::class)
|
||||
* logger.info("My log")
|
||||
* ```
|
||||
*/
|
||||
class LoggerProvider(
|
||||
private val provider: (String) -> ILogWrapper = { ILogWrapper.DEFAULT }
|
||||
) {
|
||||
/**
|
||||
* Инициализирует и возвращает экземпляр логера
|
||||
*/
|
||||
fun logger(loggerId: String): ILogWrapper = provider(loggerId)
|
||||
|
||||
/**
|
||||
* Инициализирует и возвращает экземпляр логера
|
||||
*/
|
||||
fun logger(clazz: KClass<*>): ILogWrapper = provider(clazz.qualifiedName ?: clazz.simpleName ?: "(unknown)")
|
||||
|
||||
/**
|
||||
* Инициализирует и возвращает экземпляр логера
|
||||
*/
|
||||
fun logger(function: KFunction<*>): ILogWrapper = provider(function.name)
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE configuration>
|
||||
<configuration scan="true" scanPeriod="30 seconds" debug="false">
|
||||
|
||||
<property name="LOGS_FB_HOSTS" value="${LOGS_FB_HOSTS:-127.0.0.1}"/>
|
||||
<property name="LOGS_FB_PORT" value="${LOGS_FB_PORT:-24224}"/>
|
||||
<property name="SERVICE_NAME" value="${SERVICE_NAME:-ok_messenger}"/>
|
||||
<property name="LOG_OTUS_LEVEL" value="${LOG_OTUS_LEVEL:-info}"/>
|
||||
<property name="LOG_COMMON_LEVEL" value="${LOG_COMMON_LEVEL:-error}"/>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level[%marker] %logger{36} - %msg%n%mdc%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_FB_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
|
||||
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_FB_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
<appender name="fluentd" class="ch.qos.logback.more.appenders.DataFluentAppender">
|
||||
<tag>app.logs</tag>
|
||||
<label>normal</label>
|
||||
<remoteHost>${LOGS_FB_HOSTS}</remoteHost>
|
||||
<port>${LOGS_FB_PORT}</port>
|
||||
<maxQueueSize>20</maxQueueSize>
|
||||
|
||||
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||
<providers>
|
||||
<timestamp/>
|
||||
<version/>
|
||||
<pattern>
|
||||
<pattern>
|
||||
{
|
||||
"component": "${SERVICE_NAME}",
|
||||
"container-id": "${HOSTNAME}"
|
||||
}
|
||||
</pattern>
|
||||
</pattern>
|
||||
<message/>
|
||||
<loggerName/>
|
||||
<threadName/>
|
||||
<logLevel/>
|
||||
<logstashMarkers/>
|
||||
<callerData/>
|
||||
<stackTrace/>
|
||||
<context/>
|
||||
<mdc/>
|
||||
<arguments/>
|
||||
<tags/>
|
||||
</providers>
|
||||
</encoder>
|
||||
</appender>
|
||||
<!-- </then>-->
|
||||
<!-- </if>-->
|
||||
|
||||
<!-- <!– For ELK-Stack: Kafka log's host –>-->
|
||||
<!-- <property name="LOGS_KAFKA_HOSTS" value="${BOOTSTRAP_SERVERS:-localhost:9094}"/>-->
|
||||
<!-- <!– For ELK-Stack: Kafka log's topic –>-->
|
||||
<!-- <property name="LOGS_KAFKA_TOPIC" value="${LOGS_KAFKA_TOPIC:-ok-mkpl-logs}"/>-->
|
||||
<!-- <appender name="asyncMyLogKafka"-->
|
||||
<!-- class="net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender">-->
|
||||
<!-- <if condition='!property("LOGS_KAFKA_HOSTS").equals("LOGS_KAFKA_HOSTS_IS_UNDEFINED")-->
|
||||
<!-- && !property("LOGS_KAFKA_HOSTS").isEmpty()'>-->
|
||||
<!-- <then>-->
|
||||
<!-- <appender name="kafkaVerboseAppender"-->
|
||||
<!-- class="com.github.danielwegener.logback.kafka.KafkaAppender">-->
|
||||
<!-- <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">-->
|
||||
<!-- <providers>-->
|
||||
<!-- <timestamp/>-->
|
||||
<!-- <version/>-->
|
||||
<!-- <pattern>-->
|
||||
<!-- <pattern>-->
|
||||
<!-- {-->
|
||||
<!-- "component": "${SERVICE_NAME}",-->
|
||||
<!-- "container-id": "${HOSTNAME}"-->
|
||||
<!-- }-->
|
||||
<!-- </pattern>-->
|
||||
<!-- </pattern>-->
|
||||
<!-- <message/>-->
|
||||
<!-- <loggerName/>-->
|
||||
<!-- <threadName/>-->
|
||||
<!-- <logLevel/>-->
|
||||
<!-- <logstashMarkers/>-->
|
||||
<!-- <callerData/>-->
|
||||
<!-- <stackTrace/>-->
|
||||
<!-- <context/>-->
|
||||
<!-- <mdc/>-->
|
||||
<!-- <logstashMarkers/>-->
|
||||
<!-- <arguments/>-->
|
||||
<!-- <tags/>-->
|
||||
<!-- </providers>-->
|
||||
<!-- </encoder>-->
|
||||
<!-- <topic>${LOGS_KAFKA_TOPIC}</topic>-->
|
||||
<!-- <deliveryStrategy-->
|
||||
<!-- class="com.github.danielwegener.logback.kafka.delivery.AsynchronousDeliveryStrategy"/>-->
|
||||
<!-- <producerConfig>bootstrap.servers=${LOGS_KAFKA_HOSTS}</producerConfig>-->
|
||||
<!-- </appender>-->
|
||||
<!-- </then>-->
|
||||
<!-- </if>-->
|
||||
<!-- </appender>-->
|
||||
|
||||
<logger name="ru.otus" level="${LOG_OTUS_LEVEL}" additivity="false">
|
||||
<appender-ref ref="fluentd"/>
|
||||
<!-- <appender-ref ref="asyncMyLogKafka"/>-->
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</logger>
|
||||
|
||||
<logger name="Application" level="INFO">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</logger>
|
||||
|
||||
<root level="${LOG_COMMON_LEVEL}">
|
||||
<appender-ref ref="fluentd"/>
|
||||
<!-- <appender-ref ref="asyncMyLogKafka"/>-->
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@ -0,0 +1,64 @@
|
||||
package ru.otus.messenger.logging
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.PrintStream
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LoggerTest {
|
||||
private val logId = "test-logger"
|
||||
private val logger = LoggerFactory.getLogger("xx")
|
||||
data class Xx(val x: String = "sdf")
|
||||
|
||||
|
||||
@Test
|
||||
fun slf4jTest() {
|
||||
@Suppress("LoggingPlaceholderCountMatchesArgumentCount")
|
||||
logger.info("ggg {} {} {}", 1, "sdf", Xx(), Xx("234234"))
|
||||
// ------------это objs - ! ! !
|
||||
// -----------------------------------------это data- !
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logger init`() {
|
||||
val output = invokeLogger {
|
||||
println("Some action")
|
||||
}
|
||||
|
||||
assertTrue(Regex("Started .* $logId.*").containsMatchIn(output.toString()))
|
||||
assertTrue(output.toString().contains("Some action"))
|
||||
assertTrue(Regex("Finished .* $logId.*").containsMatchIn(output.toString()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logger fails`() {
|
||||
val output = invokeLogger {
|
||||
throw RuntimeException("Some action")
|
||||
}
|
||||
|
||||
assertTrue(Regex("Started .* $logId.*").containsMatchIn(output.toString()))
|
||||
assertTrue(Regex("Failed .* $logId.*").containsMatchIn(output.toString()))
|
||||
}
|
||||
|
||||
private fun invokeLogger(block: suspend () -> Unit): ByteArrayOutputStream {
|
||||
val outputStreamCaptor = outputStreamCaptor()
|
||||
|
||||
try {
|
||||
runBlocking {
|
||||
val logger = loggerLogback(this::class)
|
||||
logger.doWithLogging(logId, block = block)
|
||||
}
|
||||
} catch (ignore: RuntimeException) {
|
||||
}
|
||||
|
||||
return outputStreamCaptor
|
||||
}
|
||||
|
||||
private fun outputStreamCaptor(): ByteArrayOutputStream {
|
||||
return ByteArrayOutputStream().apply {
|
||||
System.setOut(PrintStream(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level[%marker] %logger{36} - %msg%n%mdc%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<root level="debug">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
</configuration>
|
||||
26
ok-messenger-libs/settings.gradle.kts
Normal file
26
ok-messenger-libs/settings.gradle.kts
Normal file
@ -0,0 +1,26 @@
|
||||
rootProject.name = "ok-messenger-libs"
|
||||
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginManagement {
|
||||
includeBuild("../build-plugin")
|
||||
plugins {
|
||||
id("build-jvm") apply false
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
|
||||
}
|
||||
|
||||
include(":ok-messenger-lib-logging")
|
||||
@ -11,3 +11,5 @@ includeBuild("build-plugin")
|
||||
includeBuild("ok-lessons")
|
||||
|
||||
includeBuild("ok-messenger-be")
|
||||
|
||||
includeBuild("ok-messenger-libs")
|
||||
|
||||
110
specs/specs-log-v1.yaml
Normal file
110
specs/specs-log-v1.yaml
Normal file
@ -0,0 +1,110 @@
|
||||
openapi: 3.0.4
|
||||
info:
|
||||
title: Messenger log models
|
||||
description: Models for logging services
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
version: 1.0.0
|
||||
|
||||
paths: {}
|
||||
|
||||
components:
|
||||
schemas:
|
||||
CommonLogModel:
|
||||
title: Общая модель лога
|
||||
description: Общая модель лога для всех микросервисов системы
|
||||
type: object
|
||||
properties:
|
||||
messageTime:
|
||||
type: string
|
||||
logId:
|
||||
type: string
|
||||
source:
|
||||
type: string
|
||||
chat:
|
||||
$ref: '#/components/schemas/ChatLogModel'
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ErrorLogModel'
|
||||
|
||||
ChatLogOperation:
|
||||
type: string
|
||||
enum:
|
||||
- create
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
- search
|
||||
- init
|
||||
- finish
|
||||
|
||||
ChatLogModel:
|
||||
title: Модель лога для микросервиса Messenger
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
operation:
|
||||
$ref: '#/components/schemas/ChatLogOperation'
|
||||
requestChat:
|
||||
$ref: '#/components/schemas/ChatLog'
|
||||
requestSearch:
|
||||
$ref: '#/components/schemas/ChatSearchLog'
|
||||
responseChat:
|
||||
$ref: '#/components/schemas/ChatLog'
|
||||
responseChats:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ChatLog'
|
||||
|
||||
ErrorLogModel:
|
||||
title: Модель лога для ошибки
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
field:
|
||||
type: string
|
||||
code:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
|
||||
ChatLog:
|
||||
title: chat log model
|
||||
type: object
|
||||
properties:
|
||||
chatId:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
mode:
|
||||
type: string
|
||||
owner_id:
|
||||
type: string
|
||||
participants:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
metadata:
|
||||
type: string
|
||||
format: json
|
||||
|
||||
ChatSearchLog:
|
||||
title: log model for filter request
|
||||
properties:
|
||||
searchFields:
|
||||
type: string
|
||||
|
||||
ChatUpdateLog:
|
||||
title: log model for update request
|
||||
properties:
|
||||
updateFieldName:
|
||||
type: string
|
||||
@ -167,6 +167,8 @@ components:
|
||||
search: '#/components/schemas/ChatSearchResponse'
|
||||
update: '#/components/schemas/ChatUpdateResponse'
|
||||
delete: '#/components/schemas/ChatDeleteResponse'
|
||||
init: '#/components/schemas/ChatInitResponse'
|
||||
finish: '#/components/schemas/ChatFinishResponse'
|
||||
|
||||
# Основная схема объекта Chat
|
||||
Chat:
|
||||
@ -370,6 +372,14 @@ components:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/IResponse'
|
||||
|
||||
ChatInitResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/IResponse'
|
||||
|
||||
ChatFinishResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/IResponse'
|
||||
|
||||
# STUBS ======================
|
||||
DebugMode:
|
||||
type: string
|
||||
|
||||
Reference in New Issue
Block a user