diff --git a/README.md b/README.md index bd23465..7ab0137 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,11 @@ ## common-spring-test-jpa Утилиты для тестирования репозиториев + +## logger + +Основные компоненты логирования: + +* layout +* context +* format diff --git a/logger/build.gradle.kts b/logger/build.gradle.kts new file mode 100644 index 0000000..9b63655 --- /dev/null +++ b/logger/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("kotlin") + id("maven-publish") +} + +dependencies { + api("com.fasterxml.jackson.module:jackson-module-kotlin") + + api(project(":common")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("ch.qos.logback.contrib:logback-jackson") + implementation("ch.qos.logback.contrib:logback-json-classic") +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilder.kt b/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilder.kt new file mode 100644 index 0000000..091285c --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilder.kt @@ -0,0 +1,22 @@ +package ru.touchin.logger.builder + +import ru.touchin.logger.context.LogExecutionContextData +import ru.touchin.logger.dto.LogData +import ru.touchin.logger.dto.LogDuration +import ru.touchin.logger.dto.LogLevel +import ru.touchin.logger.log.Log + +typealias LogDataItem = Pair + +interface LogBuilder { + + fun addTags(vararg tags: String): LogBuilder + fun setError(error: Throwable?): LogBuilder + fun setDuration(duration: LogDuration): LogBuilder + fun addData(vararg items: LogDataItem): LogBuilder + fun setMethod(method: String): LogBuilder + fun setContext(context: LogExecutionContextData): LogBuilder + fun build(): Log + fun isEnabled(logLevel: LogLevel): Boolean + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilderImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilderImpl.kt new file mode 100644 index 0000000..13f9de7 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/builder/LogBuilderImpl.kt @@ -0,0 +1,57 @@ +package ru.touchin.logger.builder + +import ru.touchin.logger.context.LogExecutionContextData +import ru.touchin.logger.context.LoggerExecutionContext +import ru.touchin.logger.log.Log +import ru.touchin.logger.dto.LogLevel +import ru.touchin.logger.dto.LogData +import ru.touchin.logger.dto.LogDuration +import ru.touchin.logger.dto.LogError + +class LogBuilderImpl( + val createLog: (LogData) -> Log +) : LogBuilder { + + private val logData: LogData = LogData() + + override fun addTags(vararg tags: String): LogBuilder = also { + logData.tags.addAll(tags) + } + + override fun setError(error: Throwable?): LogBuilder = also { + logData.error = error + + if (error != null) { + addTags(LogError.ERROR_TAG) + } + } + + override fun setDuration(duration: LogDuration): LogBuilder = also { + logData.duration = duration.getDurationInMs() + } + + override fun addData(vararg items: LogDataItem): LogBuilder = also { + logData.data.putAll(items) + } + + override fun setMethod(method: String): LogBuilder = also { + logData.method = method + } + + override fun setContext(context: LogExecutionContextData): LogBuilder = also { + logData.ctx = LoggerExecutionContext.current.get() + } + + override fun build(): Log { + if (logData.ctx == null) { + logData.ctx = LoggerExecutionContext.current.get() + } + + return createLog(logData) + } + + override fun isEnabled(logLevel: LogLevel): Boolean { + return createLog(LogData()).isEnabled(logLevel) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/context/DefaultContextFields.kt b/logger/src/main/kotlin/ru/touchin/logger/context/DefaultContextFields.kt new file mode 100644 index 0000000..b204267 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/context/DefaultContextFields.kt @@ -0,0 +1,12 @@ +package ru.touchin.logger.context + +@Suppress("unused", "EnumEntryName") +enum class DefaultContextFields { + id, + host, + path, + httpMethod, + ipAddress, + appCode, + deviceId, +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/context/LoggerExecutionContext.kt b/logger/src/main/kotlin/ru/touchin/logger/context/LoggerExecutionContext.kt new file mode 100644 index 0000000..744c162 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/context/LoggerExecutionContext.kt @@ -0,0 +1,14 @@ +@file:Suppress("unused") +package ru.touchin.logger.context + +import ru.touchin.common.context.ExecutionContext + +typealias LogExecutionContextData = Map + +object LoggerExecutionContext { + + val current = ExecutionContext( + init = { emptyMap() } + ) + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/creator/JsonLogCreatorImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/creator/JsonLogCreatorImpl.kt new file mode 100644 index 0000000..f3c98ce --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/creator/JsonLogCreatorImpl.kt @@ -0,0 +1,13 @@ +package ru.touchin.logger.creator + +import ru.touchin.logger.log.Log +import ru.touchin.logger.log.JsonLogImpl +import ru.touchin.logger.dto.LogData + +class JsonLogCreatorImpl : LogCreator { + + override fun create(clazz: Class<*>): Log { + return JsonLogImpl(clazz) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/creator/LogCreator.kt b/logger/src/main/kotlin/ru/touchin/logger/creator/LogCreator.kt new file mode 100644 index 0000000..72da80d --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/creator/LogCreator.kt @@ -0,0 +1,9 @@ +package ru.touchin.logger.creator + +import ru.touchin.logger.log.Log + +interface LogCreator { + + fun create(clazz: Class<*>): Log + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/creator/SimpleLogCreatorImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/creator/SimpleLogCreatorImpl.kt new file mode 100644 index 0000000..8537563 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/creator/SimpleLogCreatorImpl.kt @@ -0,0 +1,13 @@ +package ru.touchin.logger.creator + +import ru.touchin.logger.log.Log +import ru.touchin.logger.log.SimpleLogImpl +import ru.touchin.logger.dto.LogData + +class SimpleLogCreatorImpl : LogCreator { + + override fun create(clazz: Class<*>): Log { + return SimpleLogImpl(clazz) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/dto/LogData.kt b/logger/src/main/kotlin/ru/touchin/logger/dto/LogData.kt new file mode 100644 index 0000000..f273c06 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/dto/LogData.kt @@ -0,0 +1,19 @@ +package ru.touchin.logger.dto + +import ru.touchin.logger.context.LogExecutionContextData + +class LogData { + + val tags = mutableSetOf() + + var error: Throwable? = null + + var duration: Long? = null + + val data = mutableMapOf() + + var method: String? = null + + var ctx: LogExecutionContextData? = null + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/dto/LogDuration.kt b/logger/src/main/kotlin/ru/touchin/logger/dto/LogDuration.kt new file mode 100644 index 0000000..6c8ffd3 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/dto/LogDuration.kt @@ -0,0 +1,19 @@ +package ru.touchin.logger.dto + +import java.time.Duration + +class LogDuration { + + private val startTime = Duration.ofNanos(System.nanoTime()) + private var duration: Duration? = null + + fun getDurationInMs(): Long { + if (this.duration == null) { + this.duration = Duration.ofNanos(System.nanoTime()) + .minus(startTime) + } + + return this.duration!!.toMillis() + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/dto/LogError.kt b/logger/src/main/kotlin/ru/touchin/logger/dto/LogError.kt new file mode 100644 index 0000000..83d0c1d --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/dto/LogError.kt @@ -0,0 +1,24 @@ +@file:Suppress("unused") +package ru.touchin.logger.dto + +data class LogError( + val message: String?, + val trace: String? +) { + + companion object { + const val ERROR_TAG = "error" + const val ERROR_FATAL_TAG = "errorFatal" + const val ERROR_BASE_TAG = "errorBase" + + fun Throwable.stackTraceAsString() = this.stackTrace?.fold("") { a, b -> + "$a$b\n" + } + + fun Throwable.toLogError() = LogError( + message = this.message, + trace = this.stackTraceAsString() + ) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/dto/LogLevel.kt b/logger/src/main/kotlin/ru/touchin/logger/dto/LogLevel.kt new file mode 100644 index 0000000..9d333c5 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/dto/LogLevel.kt @@ -0,0 +1,5 @@ +package ru.touchin.logger.dto + +enum class LogLevel { + Trace, Info, Error +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/dto/LogValueField.kt b/logger/src/main/kotlin/ru/touchin/logger/dto/LogValueField.kt new file mode 100644 index 0000000..6e0cb5c --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/dto/LogValueField.kt @@ -0,0 +1,8 @@ +package ru.touchin.logger.dto + +class LogValueField( + val name: String?, + val value: Any, + val prefix: String? = null, + val expand: Boolean = false, +) diff --git a/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactory.kt b/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactory.kt new file mode 100644 index 0000000..8a1fb3d --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactory.kt @@ -0,0 +1,9 @@ +package ru.touchin.logger.factory + +import ru.touchin.logger.builder.LogBuilder + +interface LogBuilderFactory { + + fun create(clazz: Class<*>): LogBuilder + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactoryImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactoryImpl.kt new file mode 100644 index 0000000..83e2b3a --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/factory/LogBuilderFactoryImpl.kt @@ -0,0 +1,23 @@ +package ru.touchin.logger.factory + +import ru.touchin.logger.builder.LogBuilder +import ru.touchin.logger.builder.LogBuilderImpl +import ru.touchin.logger.creator.LogCreator +import ru.touchin.logger.dto.LogData + +class LogBuilderFactoryImpl( + private val logCreator: LogCreator +) : LogBuilderFactory { + + override fun create(clazz: Class<*>): LogBuilder { + return LogBuilderImpl( + createLog = { logData -> + logCreator.create(clazz) + .also { log -> + log.logData = logData + } + } + ) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/layouts/JsonLayout.kt b/logger/src/main/kotlin/ru/touchin/logger/layouts/JsonLayout.kt new file mode 100644 index 0000000..b27e935 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/layouts/JsonLayout.kt @@ -0,0 +1,19 @@ +package ru.touchin.logger.layouts + +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.contrib.json.classic.JsonLayout as ContribJsonLayout + +private const val MESSAGE_TYPE_ATTR_NAME = "messageType" + +@Suppress("unused", "MemberVisibilityCanBePrivate") +class JsonLayout : ContribJsonLayout() { + + var messageType: String? = null + + override fun addCustomDataToJsonMap(map: MutableMap?, event: ILoggingEvent?) { + messageType?.let { + map?.put(MESSAGE_TYPE_ATTR_NAME, it) + } + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/log/AbstractLog.kt b/logger/src/main/kotlin/ru/touchin/logger/log/AbstractLog.kt new file mode 100644 index 0000000..9870c8e --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/log/AbstractLog.kt @@ -0,0 +1,61 @@ +package ru.touchin.logger.log + +import org.slf4j.LoggerFactory +import ru.touchin.logger.dto.LogLevel +import ru.touchin.logger.dto.LogData + +abstract class AbstractLog(clazz: Class<*>) : Log { + + class LogMessage ( + val message: String, + val error: Throwable? = null + ) + + private val logger = LoggerFactory.getLogger(clazz) + ?: throw IllegalStateException("unable to retrieve Logger") + + override var logData: LogData = LogData() + + protected abstract fun getMessage(): LogMessage + + override fun trace() { + if (logger.isTraceEnabled) { + val logMessage = getMessage() + + logger.trace(logMessage.message, logMessage.error) + } + } + + override fun info() { + if (logger.isInfoEnabled) { + val logMessage = getMessage() + + logger.info(logMessage.message, logMessage.error) + } + } + + override fun error() { + if (logger.isErrorEnabled) { + val logMessage = getMessage() + + logger.error(logMessage.message, logMessage.error) + } + } + + override fun log() { + if (logData.error == null) { + info() + } else { + error() + } + } + + override fun isEnabled(level: LogLevel): Boolean { + return when(level) { + LogLevel.Trace -> logger.isTraceEnabled + LogLevel.Info -> logger.isInfoEnabled + LogLevel.Error -> logger.isErrorEnabled + } + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/log/JsonLogImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/log/JsonLogImpl.kt new file mode 100644 index 0000000..b0b43c1 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/log/JsonLogImpl.kt @@ -0,0 +1,52 @@ +package ru.touchin.logger.log + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import ru.touchin.logger.dto.LogError +import ru.touchin.logger.dto.LogError.Companion.stackTraceAsString +import ru.touchin.logger.dto.LogError.Companion.toLogError + +open class JsonLogImpl(clazz: Class<*>) : AbstractLog(clazz) { + + companion object { + val objectMapper: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + } + + private fun toJson(): JsonNode { + val result = objectMapper.convertValue(logData, ObjectNode::class.java) + + logData.error + ?.toLogError() + ?.let { + objectMapper.convertValue(it, JsonNode::class.java) + } + ?.let { + result.remove("error") + result.set("error", it) + } + + return result + } + + override fun getMessage(): LogMessage { + val message = runCatching { + objectMapper.writeValueAsString(toJson()) + }.getOrElse { throwable -> + """ + { + "tags": ["logParse", "${LogError.ERROR_TAG}", "${LogError.ERROR_FATAL_TAG}"], + "error": { + "message": "${throwable.message}", + "trace": "${throwable.stackTraceAsString()}" + } + } + """.trimIndent() + } + + return LogMessage(message) + } + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/log/Log.kt b/logger/src/main/kotlin/ru/touchin/logger/log/Log.kt new file mode 100644 index 0000000..26f9405 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/log/Log.kt @@ -0,0 +1,20 @@ +package ru.touchin.logger.log + +import ru.touchin.logger.dto.LogLevel + +interface Log { + + var logData: T + + fun trace() + fun info() + fun error() + + /*** + * Use loglevel error is logData has error and use info level otherwise + */ + fun log() + + fun isEnabled(level: LogLevel): Boolean + +} diff --git a/logger/src/main/kotlin/ru/touchin/logger/log/SimpleLogImpl.kt b/logger/src/main/kotlin/ru/touchin/logger/log/SimpleLogImpl.kt new file mode 100644 index 0000000..adabfb1 --- /dev/null +++ b/logger/src/main/kotlin/ru/touchin/logger/log/SimpleLogImpl.kt @@ -0,0 +1,20 @@ +package ru.touchin.logger.log + +class SimpleLogImpl(clazz: Class<*>): AbstractLog(clazz) { + + override fun getMessage(): LogMessage { + val builder = StringBuilder() + + builder.append("\n\ttags: ${logData.tags.joinToString(",")}") + + if (logData.duration != null) { + builder.append("\n\tduration: ${logData.duration}ms") + } + + return LogMessage( + message = builder.toString(), + error = logData.error + ) + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c2c85e..8d1f6ad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,3 +25,4 @@ include("common-spring-jpa") include("common-spring-web") include("common-spring-test") include("common-spring-test-jpa") +include("logger")