module 5 lesson 1

This commit is contained in:
Александр Веденёв
2025-03-17 22:26:48 +07:00
parent 34636aaf60
commit d0bc6821d2
68 changed files with 1945 additions and 45 deletions

View File

@ -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"

View File

@ -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"))
}

View File

@ -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 {

View 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)
}
}

View File

@ -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()
)

View File

@ -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()

View File

@ -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()

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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"))
}

View 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
```

View 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)
}

View File

@ -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)
}

View 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)
}

View File

@ -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
}
}

View 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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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) }
}
}

View File

@ -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)))
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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) }

View File

@ -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),
)
}

View File

@ -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")

View File

@ -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,
)

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -0,0 +1,10 @@
ktor:
development: true
deployment:
port: 8080
watch:
- classes
- resources
application:
modules:
- ru.otus.messenger.app.ApplicationKt.module

View 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")-->
<!-- &amp;&amp; !property("LOGS_FB_HOSTS").isEmpty()'>-->
<!-- <then>-->
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
<!-- &amp;&amp; !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>-->
<!-- &lt;!&ndash; For ELK-Stack: Kafka log's host &ndash;&gt;-->
<!-- <property name="LOGS_KAFKA_HOSTS" value="${BOOTSTRAP_SERVERS:-localhost:9094}"/>-->
<!-- &lt;!&ndash; For ELK-Stack: Kafka log's topic &ndash;&gt;-->
<!-- <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")-->
<!-- &amp;&amp; !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>

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1,2 @@
package ru.otus.messenger.app.stub

View File

@ -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)
}
}
}
}

View 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"))
}

View File

@ -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
}
}

View File

@ -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"))
}

View File

@ -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(),

View File

@ -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()
}
}

View File

@ -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,
)

View File

@ -7,4 +7,6 @@ enum class ChatCommand {
DELETE,
SEARCH,
UPDATE,
INIT,
FINISH,
}

View File

@ -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,
)

View File

@ -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()
}
}

View File

@ -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) {
}
}
}
}

View File

@ -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) {}
}
}
}

View 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"))
}

View File

@ -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 }
}

View File

@ -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")
}
)
)
}

View File

@ -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")

View 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)})
}
}
}

View File

@ -0,0 +1,3 @@
kotlin.code.style=official
kotlin.native.ignoreDisabledTargets=true
#kotlin.native.cacheKind.linuxX64=none

View 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)
}

View File

@ -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(",")
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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"))
}
}
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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")-->
<!-- &amp;&amp; !property("LOGS_FB_HOSTS").isEmpty()'>-->
<!-- <then>-->
<!-- <if condition='!property("LOGS_FB_HOSTS").equals("LOGS_FB_HOSTS_IS_UNDEFINED")-->
<!-- &amp;&amp; !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>-->
<!-- &lt;!&ndash; For ELK-Stack: Kafka log's host &ndash;&gt;-->
<!-- <property name="LOGS_KAFKA_HOSTS" value="${BOOTSTRAP_SERVERS:-localhost:9094}"/>-->
<!-- &lt;!&ndash; For ELK-Stack: Kafka log's topic &ndash;&gt;-->
<!-- <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")-->
<!-- &amp;&amp; !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>

View File

@ -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))
}
}
}

View File

@ -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>

View 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")

View File

@ -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
View 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

View File

@ -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