Merge pull request #8 from TouchInstinct/logger

add logger
This commit is contained in:
Alexander Buntakov 2021-06-07 19:23:38 +03:00 committed by GitHub
commit 5c154e034b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 443 additions and 0 deletions

View File

@ -52,3 +52,11 @@
## common-spring-test-jpa
Утилиты для тестирования репозиториев
## logger
Основные компоненты логирования:
* layout
* context
* format

15
logger/build.gradle.kts Normal file
View File

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

View File

@ -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<String, Any>
interface LogBuilder<T> {
fun addTags(vararg tags: String): LogBuilder<T>
fun setError(error: Throwable?): LogBuilder<T>
fun setDuration(duration: LogDuration): LogBuilder<T>
fun addData(vararg items: LogDataItem): LogBuilder<T>
fun setMethod(method: String): LogBuilder<T>
fun setContext(context: LogExecutionContextData): LogBuilder<LogData>
fun build(): Log<T>
fun isEnabled(logLevel: LogLevel): Boolean
}

View File

@ -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<LogData>
) : LogBuilder<LogData> {
private val logData: LogData = LogData()
override fun addTags(vararg tags: String): LogBuilder<LogData> = also {
logData.tags.addAll(tags)
}
override fun setError(error: Throwable?): LogBuilder<LogData> = also {
logData.error = error
if (error != null) {
addTags(LogError.ERROR_TAG)
}
}
override fun setDuration(duration: LogDuration): LogBuilder<LogData> = also {
logData.duration = duration.getDurationInMs()
}
override fun addData(vararg items: LogDataItem): LogBuilder<LogData> = also {
logData.data.putAll(items)
}
override fun setMethod(method: String): LogBuilder<LogData> = also {
logData.method = method
}
override fun setContext(context: LogExecutionContextData): LogBuilder<LogData> = also {
logData.ctx = LoggerExecutionContext.current.get()
}
override fun build(): Log<LogData> {
if (logData.ctx == null) {
logData.ctx = LoggerExecutionContext.current.get()
}
return createLog(logData)
}
override fun isEnabled(logLevel: LogLevel): Boolean {
return createLog(LogData()).isEnabled(logLevel)
}
}

View File

@ -0,0 +1,12 @@
package ru.touchin.logger.context
@Suppress("unused", "EnumEntryName")
enum class DefaultContextFields {
id,
host,
path,
httpMethod,
ipAddress,
appCode,
deviceId,
}

View File

@ -0,0 +1,14 @@
@file:Suppress("unused")
package ru.touchin.logger.context
import ru.touchin.common.context.ExecutionContext
typealias LogExecutionContextData = Map<String, Any>
object LoggerExecutionContext {
val current = ExecutionContext<LogExecutionContextData>(
init = { emptyMap() }
)
}

View File

@ -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<LogData> {
override fun create(clazz: Class<*>): Log<LogData> {
return JsonLogImpl(clazz)
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.logger.creator
import ru.touchin.logger.log.Log
interface LogCreator<T> {
fun create(clazz: Class<*>): Log<T>
}

View File

@ -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<LogData> {
override fun create(clazz: Class<*>): Log<LogData> {
return SimpleLogImpl(clazz)
}
}

View File

@ -0,0 +1,19 @@
package ru.touchin.logger.dto
import ru.touchin.logger.context.LogExecutionContextData
class LogData {
val tags = mutableSetOf<String>()
var error: Throwable? = null
var duration: Long? = null
val data = mutableMapOf<String, Any>()
var method: String? = null
var ctx: LogExecutionContextData? = null
}

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package ru.touchin.logger.dto
enum class LogLevel {
Trace, Info, Error
}

View File

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

View File

@ -0,0 +1,9 @@
package ru.touchin.logger.factory
import ru.touchin.logger.builder.LogBuilder
interface LogBuilderFactory<T> {
fun create(clazz: Class<*>): LogBuilder<T>
}

View File

@ -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<LogData>
) : LogBuilderFactory<LogData> {
override fun create(clazz: Class<*>): LogBuilder<LogData> {
return LogBuilderImpl(
createLog = { logData ->
logCreator.create(clazz)
.also { log ->
log.logData = logData
}
}
)
}
}

View File

@ -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<String, Any>?, event: ILoggingEvent?) {
messageType?.let {
map?.put(MESSAGE_TYPE_ATTR_NAME, it)
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,20 @@
package ru.touchin.logger.log
import ru.touchin.logger.dto.LogLevel
interface Log<T> {
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
}

View File

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

View File

@ -25,3 +25,4 @@ include("common-spring-jpa")
include("common-spring-web")
include("common-spring-test")
include("common-spring-test-jpa")
include("logger")