commit
86dccf0624
|
|
@ -60,3 +60,10 @@
|
|||
* layout
|
||||
* context
|
||||
* format
|
||||
|
||||
## logger-spring
|
||||
|
||||
Встраивание системы логирования в `spring`
|
||||
|
||||
* autologging
|
||||
* serializer
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
id("maven-publish")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":logger"))
|
||||
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
implementation(project(":common-spring"))
|
||||
|
||||
implementation("org.springframework.boot:spring-boot")
|
||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring
|
||||
|
||||
import org.springframework.context.annotation.Import
|
||||
import ru.touchin.logger.spring.configurations.SpringLoggerConfiguration
|
||||
|
||||
@Import(value = [SpringLoggerConfiguration::class])
|
||||
annotation class EnableSpringLogger
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.logger.spring.annotations
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class AutoLogging(
|
||||
val tags: Array<String>,
|
||||
val preventError: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package ru.touchin.logger.spring.annotations
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER)
|
||||
annotation class LogValue(
|
||||
val prefix: String = "",
|
||||
val expand: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package ru.touchin.logger.spring.aspects
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint
|
||||
import org.aspectj.lang.annotation.Around
|
||||
import org.aspectj.lang.annotation.Aspect
|
||||
import org.aspectj.lang.reflect.MethodSignature
|
||||
import ru.touchin.logger.spring.annotations.AutoLogging
|
||||
import ru.touchin.logger.spring.annotations.LogValue
|
||||
import ru.touchin.logger.builder.LogDataItem
|
||||
import ru.touchin.logger.factory.LogBuilderFactory
|
||||
import ru.touchin.logger.dto.LogDuration
|
||||
import ru.touchin.logger.dto.LogError
|
||||
import ru.touchin.logger.dto.LogValueField
|
||||
import ru.touchin.logger.spring.serializers.LogValueFieldSerializer
|
||||
import java.lang.reflect.AnnotatedElement
|
||||
import java.lang.reflect.Method
|
||||
|
||||
@Aspect
|
||||
class LogAspect(
|
||||
private val logBuilderFactory: LogBuilderFactory<*>,
|
||||
private val logValueFieldSerializer: LogValueFieldSerializer,
|
||||
) {
|
||||
|
||||
@Around("@annotation(autoLoggingAnnotation)")
|
||||
fun logInvocation(pjp: ProceedingJoinPoint, autoLoggingAnnotation: AutoLogging): Any? {
|
||||
val duration = LogDuration()
|
||||
|
||||
val actionResult = runCatching(pjp::proceed)
|
||||
|
||||
try {
|
||||
val method = pjp.method()
|
||||
|
||||
logBuilderFactory.create(method.declaringClass)
|
||||
.setMethod(method.name)
|
||||
.setDuration(duration)
|
||||
.addTags(*autoLoggingAnnotation.tags)
|
||||
.addData(*getDataItems(pjp, actionResult).toTypedArray())
|
||||
.setError(actionResult.exceptionOrNull())
|
||||
.build()
|
||||
.log()
|
||||
} catch (error: Exception) {
|
||||
try {
|
||||
logBuilderFactory.create(this::class.java)
|
||||
.addTags(LogError.ERROR_TAG, LogError.ERROR_FATAL_TAG)
|
||||
.setError(error)
|
||||
.build()
|
||||
.error()
|
||||
} catch (logError: Throwable) {
|
||||
error.printStackTrace()
|
||||
logError.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
return if (autoLoggingAnnotation.preventError) {
|
||||
actionResult.getOrNull()
|
||||
} else {
|
||||
actionResult.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDataItems(pjp: ProceedingJoinPoint, result: Result<Any?>): List<LogDataItem> {
|
||||
return getArgumentsField(pjp) + getResultFields(pjp.method(), result)
|
||||
}
|
||||
|
||||
private fun getArgumentsField(pjp: ProceedingJoinPoint): List<LogDataItem> {
|
||||
return pjp.method().parameters
|
||||
.zip(pjp.args)
|
||||
.filter { (parameter, value) ->
|
||||
parameter.hasLogValueAnnotation() && value != null
|
||||
}
|
||||
.flatMap { (parameter, value) ->
|
||||
logValueFieldSerializer.invoke(
|
||||
LogValueField(
|
||||
name = parameter.name,
|
||||
value = value,
|
||||
prefix = parameter.getLogValuePrefix(),
|
||||
expand = parameter.getLogValueExpand(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResultFields(method: Method, result: Result<Any?>): List<LogDataItem> {
|
||||
if (result.isFailure || !method.hasLogValueAnnotation()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val returnValue = result.getOrNull()
|
||||
?: return emptyList()
|
||||
|
||||
return logValueFieldSerializer.invoke(
|
||||
LogValueField (
|
||||
name = null,
|
||||
value = returnValue,
|
||||
prefix = method.getLogValuePrefix(),
|
||||
expand = method.getLogValueExpand(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun AnnotatedElement.hasLogValueAnnotation() = this.isAnnotationPresent(LogValue::class.java)
|
||||
|
||||
private fun AnnotatedElement.getLogValuePrefix(): String? {
|
||||
val prefix = this.getAnnotation(LogValue::class.java).prefix.trim()
|
||||
|
||||
if (prefix.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return prefix
|
||||
}
|
||||
|
||||
private fun AnnotatedElement.getLogValueExpand() = this.getAnnotation(LogValue::class.java).expand
|
||||
|
||||
private fun ProceedingJoinPoint.method(): Method = (signature as MethodSignature).method
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package ru.touchin.logger.spring.configurations
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.context.annotation.Scope
|
||||
import ru.touchin.logger.spring.aspects.LogAspect
|
||||
import ru.touchin.logger.factory.LogBuilderFactory
|
||||
import ru.touchin.logger.creator.LogCreator
|
||||
import ru.touchin.logger.creator.JsonLogCreatorImpl
|
||||
import ru.touchin.logger.factory.LogBuilderFactoryImpl
|
||||
import ru.touchin.logger.creator.SimpleLogCreatorImpl
|
||||
import ru.touchin.logger.dto.LogData
|
||||
import ru.touchin.logger.spring.serializers.LogValueFieldSerializer
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("ru.touchin.logger.spring.serializers", "ru.touchin.logger.spring.listeners")
|
||||
class SpringLoggerConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@Profile("json-log")
|
||||
fun jsonLogCreator(): LogCreator<LogData> {
|
||||
return JsonLogCreatorImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun localLogCreator(): LogCreator<LogData> {
|
||||
return SimpleLogCreatorImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
fun logBuilderFactory(logCreator: LogCreator<LogData>): LogBuilderFactory<LogData> {
|
||||
return LogBuilderFactoryImpl(logCreator)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun logAspect(
|
||||
logBuilderFactory: LogBuilderFactory<LogData>,
|
||||
logValueFieldSerializer: LogValueFieldSerializer
|
||||
): LogAspect {
|
||||
return LogAspect(logBuilderFactory, logValueFieldSerializer)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.listeners
|
||||
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent
|
||||
import org.springframework.context.event.ContextClosedEvent
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.logger.factory.LogBuilderFactory
|
||||
import ru.touchin.logger.dto.LogData
|
||||
import ru.touchin.logger.dto.LogDuration
|
||||
|
||||
private const val APPLICATION = "application"
|
||||
|
||||
@Component
|
||||
class ApplicationLifeCycleEventListener(
|
||||
private val logBuilderFactory: LogBuilderFactory<LogData>
|
||||
) {
|
||||
lateinit var duration: LogDuration
|
||||
|
||||
@EventListener(ApplicationStartedEvent::class)
|
||||
fun onApplicationStart() {
|
||||
duration = LogDuration()
|
||||
|
||||
logBuilderFactory.create(this::class.java)
|
||||
.addTags(APPLICATION, "started")
|
||||
.build()
|
||||
.log()
|
||||
}
|
||||
|
||||
@EventListener(ContextClosedEvent::class)
|
||||
fun onApplicationStopped() {
|
||||
logBuilderFactory.create(this::class.java)
|
||||
.addTags(APPLICATION, "stopped")
|
||||
.setDuration(duration)
|
||||
.build()
|
||||
.log()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package ru.touchin.logger.spring.serializers
|
||||
|
||||
import ru.touchin.logger.builder.LogDataItem
|
||||
import ru.touchin.logger.dto.LogValueField
|
||||
|
||||
interface LogValueFieldSerializer {
|
||||
|
||||
operator fun invoke(field: LogValueField): List<LogDataItem>
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.logger.builder.LogDataItem
|
||||
import ru.touchin.logger.dto.LogValueField
|
||||
import ru.touchin.logger.spring.serializers.resolvers.LogValueResolver
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
|
||||
@Component
|
||||
class LogValueFieldSerializerImpl(
|
||||
resolversList: List<LogValueResolver<*>>
|
||||
) : LogValueFieldSerializer {
|
||||
|
||||
private val resolvers = resolversList.asSequence()
|
||||
|
||||
private fun resolveFieldName(field: LogValueField, resolvedValue: ResolvedValue<*>): String {
|
||||
val prefix = field.prefix
|
||||
|
||||
if (prefix != null) {
|
||||
return prefix
|
||||
}
|
||||
|
||||
var suffix = ""
|
||||
|
||||
if (resolvedValue.typeName.isNotBlank()) {
|
||||
suffix = "_${resolvedValue.typeName}"
|
||||
}
|
||||
|
||||
return (field.name ?: DEFAULT_RETURN_FIELD_NAME) + suffix
|
||||
}
|
||||
|
||||
private fun serialize(field: LogValueField): LogDataItem? {
|
||||
val resolvedValue = resolvers
|
||||
.mapNotNull { it.invoke(field.value) }
|
||||
.firstOrNull()
|
||||
|
||||
if (resolvedValue?.value == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return resolveFieldName(field, resolvedValue) to resolvedValue.value
|
||||
}
|
||||
|
||||
private fun expand(field: LogValueField): List<LogValueField> {
|
||||
if (!field.expand) {
|
||||
return listOf(field)
|
||||
}
|
||||
|
||||
return field.value::class.declaredMemberProperties
|
||||
.mapNotNull { property ->
|
||||
property.getter.call(field.value)
|
||||
?.let { propertyValue ->
|
||||
LogValueField(
|
||||
name = property.name,
|
||||
value = propertyValue,
|
||||
prefix = field.prefix?.let { "$it.${property.name}" }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun invoke(field: LogValueField): List<LogDataItem> {
|
||||
return expand(field).mapNotNull(this::serialize)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_RETURN_FIELD_NAME = "result"
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class BooleanLogValueResolverImpl : LogValueResolver<Boolean> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<Boolean>? {
|
||||
if (value !is Boolean) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value,
|
||||
typeName = "boolean"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
import java.time.temporal.Temporal
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class DateLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is Temporal) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value.toString(),
|
||||
typeName = "date",
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
import java.io.File
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class FileLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is File) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class FunctionLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is Function<*>) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue.SKIP_VALUE
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
interface LogValueResolver<out T: Any> {
|
||||
|
||||
operator fun invoke(value: Any): ResolvedValue<T>?
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class NumberLogValueResolverImpl : LogValueResolver<Number> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<Number>? {
|
||||
if (value !is Number) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value,
|
||||
typeName = "number"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.LOW)
|
||||
@Component
|
||||
class ObjectLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String> {
|
||||
return ResolvedValue(
|
||||
value = objectMapper.writeValueAsString(value)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class StringLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is String) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
import java.util.*
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class UUIDLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is UUID) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class UnitLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is Unit) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue.SKIP_VALUE
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.Ordered
|
||||
import ru.touchin.logger.spring.serializers.resolvers.dto.ResolvedValue
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
@Order(Ordered.NORMAL)
|
||||
@Component
|
||||
class UrlLogValueResolverImpl : LogValueResolver<String> {
|
||||
|
||||
override operator fun invoke(value: Any): ResolvedValue<String>? {
|
||||
if (value !is URL && value !is URI) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ResolvedValue(
|
||||
value = value.toString(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.logger.spring.serializers.resolvers.dto
|
||||
|
||||
class ResolvedValue<out T: Any>(
|
||||
val value: T?,
|
||||
val typeName: String = ""
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val SKIP_VALUE = ResolvedValue(value = null)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package ru.touchin.logger
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class TestApplication
|
||||
|
|
@ -0,0 +1,432 @@
|
|||
package ru.touchin.logger.serializers.impl
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import ru.touchin.logger.dto.LogValueField
|
||||
import ru.touchin.logger.spring.serializers.LogValueFieldSerializer
|
||||
import java.io.File
|
||||
import java.math.BigDecimal
|
||||
import java.net.URI
|
||||
import java.time.LocalDate
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
|
||||
@SpringBootTest
|
||||
internal class LogValueFieldResolverImplTest {
|
||||
private val birthDate = "2020-05-15"
|
||||
|
||||
@Autowired
|
||||
private lateinit var logValueFieldSerializer: LogValueFieldSerializer
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
|
||||
@Suppress("unused")
|
||||
class Pet(
|
||||
val nickname: String,
|
||||
val weight: Double,
|
||||
val hasTail: Boolean,
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
class User(
|
||||
val name: String,
|
||||
val age: Int?,
|
||||
val birthDate: LocalDate = LocalDate.parse("2020-05-12"),
|
||||
val pet: Pet? = null
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("Префикс должен быть приоритетнее имени")
|
||||
fun shouldUsePrefixInsteadName() {
|
||||
val value = 25
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "myAge",
|
||||
value = value,
|
||||
prefix = "prefixName",
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals(logValueField.prefix, field.first)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если имя не указано, то название параметра должно быть result")
|
||||
fun shouldBeFieldNameResultIfNameOmit() {
|
||||
val value = "john"
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = null,
|
||||
value = value,
|
||||
prefix = null,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("result", field.first)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Для примитивных аргументов должен добавляться суффикс с типом")
|
||||
fun shouldBeSuffixTypeForPrimitiveArguments() {
|
||||
val value = 25
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "age",
|
||||
value = value,
|
||||
prefix = null,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertTrue(field.first == "age_number")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Для примитивных значение в return должен добавляться префикс")
|
||||
fun shouldBeSuffixTypeForPrimitiveResult() {
|
||||
val value = true
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = null,
|
||||
value = value,
|
||||
prefix = null,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("result_boolean", field.first)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Объект должен быть строкой, если не указан expand")
|
||||
fun shouldSerializeObjectValue() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "user",
|
||||
value = value,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("user", field.first)
|
||||
|
||||
assertTrue(field.second is String)
|
||||
assertEquals(objectMapper.writeValueAsString(value), field.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Объекты со значением Unit должны отфильтровываться")
|
||||
fun shouldOmitUnit() {
|
||||
val logValueField = LogValueField(
|
||||
name = "null",
|
||||
value = Unit,
|
||||
prefix = "prefixName",
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Объекты со значением Function должны отфильтровываться")
|
||||
fun shouldOmitFunction() {
|
||||
val logValueField = LogValueField(
|
||||
name = "null",
|
||||
value = {},
|
||||
prefix = "prefixName",
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertTrue(result.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если указан expand для объекта, то его поля поднимаются на один уровень с аргументами")
|
||||
fun shouldExpandObjectValues() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "user",
|
||||
value = value,
|
||||
expand = true
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField).sortedBy { it.first }
|
||||
assertEquals(3, result.size)
|
||||
|
||||
|
||||
val (ageItem, birthDateItem, nameItem) = result
|
||||
|
||||
assertEquals("age_number", ageItem.first)
|
||||
assertEquals(value.age, ageItem.second)
|
||||
|
||||
assertEquals("birthDate_date", birthDateItem.first)
|
||||
assertEquals(birthDate, birthDateItem.second)
|
||||
|
||||
assertEquals("name", nameItem.first)
|
||||
assertEquals(value.name, nameItem.second)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если expand объекта есть поле-объект, то это поле сериализуется в строку")
|
||||
fun shouldSerializeObjectIfParentExpand() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate),
|
||||
pet = Pet("Rex", 56.13, false)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "user",
|
||||
value = value,
|
||||
expand = true
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField).sortedBy { it.first }
|
||||
assertEquals(4, result.size)
|
||||
|
||||
val (_, _, _, petItem) = result
|
||||
|
||||
assertEquals("pet", petItem.first)
|
||||
assertTrue(petItem.second is String)
|
||||
assertEquals(objectMapper.writeValueAsString(value.pet), petItem.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если указан expand с префиксом, то названия полей формируется как `prefix.field`")
|
||||
fun shouldExpandObjectWithPrefix() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate),
|
||||
pet = Pet("Rex", 56.13, false)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "object",
|
||||
value = value,
|
||||
prefix = "user",
|
||||
expand = true
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer
|
||||
.invoke(logValueField)
|
||||
.map { it.first }
|
||||
.sorted()
|
||||
|
||||
assertEquals(4, result.size)
|
||||
assertEquals(listOf("user.age", "user.birthDate", "user.name", "user.pet"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если указан expand для return, то его поля попадают в список")
|
||||
fun shouldExpandReturnValues() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = null,
|
||||
value = value,
|
||||
expand = true
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField).sortedBy { it.first }
|
||||
assertEquals(3, result.size)
|
||||
|
||||
val (ageItem, birthDateItem, nameItem) = result
|
||||
|
||||
assertEquals("age_number", ageItem.first)
|
||||
assertEquals(value.age, ageItem.second)
|
||||
|
||||
assertEquals("birthDate_date", birthDateItem.first)
|
||||
assertEquals(birthDate, birthDateItem.second)
|
||||
|
||||
assertEquals("name", nameItem.first)
|
||||
assertEquals(value.name, nameItem.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Если указан expand для return и префикс, то его поля попадают в список с названием `prefix.field`")
|
||||
fun shouldExpandReturnWithPrefix() {
|
||||
val value = User(
|
||||
name = "john",
|
||||
age = 15,
|
||||
birthDate = LocalDate.parse(birthDate),
|
||||
pet = Pet("Rex", 56.13, false)
|
||||
)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = null,
|
||||
value = value,
|
||||
prefix = "user",
|
||||
expand = true
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer
|
||||
.invoke(logValueField)
|
||||
.map { it.first }
|
||||
.sorted()
|
||||
|
||||
assertEquals(4, result.size)
|
||||
assertEquals(listOf("user.age", "user.birthDate", "user.name", "user.pet"), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Даты должны сериализоваться в строку")
|
||||
fun shouldBeDateString() {
|
||||
val date = "2020-12-21T12:09:12Z"
|
||||
val value = ZonedDateTime.parse(date)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "birthdate",
|
||||
value = value,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("birthdate_date", field.first)
|
||||
assertEquals(date, field.second)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("URI должны сериализоваться в строку")
|
||||
fun shouldBeUriString() {
|
||||
val uri = URI.create("http://example.com/test")
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "url",
|
||||
value = uri,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("url", field.first)
|
||||
assertEquals(field.second, uri.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("URL должны сериализоваться в строку")
|
||||
fun shouldBeUrlString() {
|
||||
val url = URI.create("http://example.com/test").toURL()
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "url",
|
||||
value = url,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("url", field.first)
|
||||
assertEquals(field.second, url.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Files должны сериализоваться в строку")
|
||||
fun shouldFilePath() {
|
||||
val path = "/tmp/filename"
|
||||
val file = File(path)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "file",
|
||||
value = file,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("file", field.first)
|
||||
assertEquals(field.second, path)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("UUID должны сериализоваться в строку")
|
||||
fun shouldBeUUIDString() {
|
||||
val uuidString = "f1036cc5-2d86-4dde-b27c-820fb11f1974"
|
||||
val uuid = UUID.fromString(uuidString)
|
||||
|
||||
val logValueField = LogValueField(
|
||||
name = "uuid",
|
||||
value = uuid,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField)
|
||||
assertEquals(1, result.size)
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("uuid", field.first)
|
||||
assertEquals(field.second, uuidString)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Number не должен сериализоваться в строку")
|
||||
fun shouldBeInt() {
|
||||
val b: Byte = 128.toByte()
|
||||
val s: Short = 256
|
||||
val i = 512
|
||||
val l = 1024L
|
||||
val f = 1.2f
|
||||
val d = 12.13
|
||||
val bd: BigDecimal = BigDecimal.valueOf(12.10)
|
||||
|
||||
val values = listOf(
|
||||
b, s, i, l, f, d, bd
|
||||
)
|
||||
|
||||
values.forEach { value ->
|
||||
val logValueField = LogValueField(
|
||||
name = null,
|
||||
value = value,
|
||||
)
|
||||
|
||||
val result = logValueFieldSerializer.invoke(logValueField).also {
|
||||
assertEquals(1, it.size)
|
||||
}
|
||||
|
||||
val field = result.first()
|
||||
assertEquals("result_number", field.first)
|
||||
assertEquals(value, field.second)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -26,3 +26,4 @@ include("common-spring-web")
|
|||
include("common-spring-test")
|
||||
include("common-spring-test-jpa")
|
||||
include("logger")
|
||||
include("logger-spring")
|
||||
|
|
|
|||
Loading…
Reference in New Issue