diff --git a/README.md b/README.md index 7ab0137..de54ddc 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,10 @@ * layout * context * format + +## logger-spring + +Встраивание системы логирования в `spring` + +* autologging +* serializer diff --git a/logger-spring/build.gradle.kts b/logger-spring/build.gradle.kts new file mode 100644 index 0000000..0da95c0 --- /dev/null +++ b/logger-spring/build.gradle.kts @@ -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") +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/EnableSpringLogger.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/EnableSpringLogger.kt new file mode 100644 index 0000000..37df043 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/EnableSpringLogger.kt @@ -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 diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/AutoLogging.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/AutoLogging.kt new file mode 100644 index 0000000..b0954de --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/AutoLogging.kt @@ -0,0 +1,7 @@ +package ru.touchin.logger.spring.annotations + +@Target(AnnotationTarget.FUNCTION) +annotation class AutoLogging( + val tags: Array, + val preventError: Boolean = false, +) diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/LogValue.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/LogValue.kt new file mode 100644 index 0000000..2b9c4a2 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/annotations/LogValue.kt @@ -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, +) diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/aspects/LogAspect.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/aspects/LogAspect.kt new file mode 100644 index 0000000..b9f1397 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/aspects/LogAspect.kt @@ -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): List { + return getArgumentsField(pjp) + getResultFields(pjp.method(), result) + } + + private fun getArgumentsField(pjp: ProceedingJoinPoint): List { + 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): List { + 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 + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/configurations/SpringLoggerConfiguration.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/configurations/SpringLoggerConfiguration.kt new file mode 100644 index 0000000..a59736e --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/configurations/SpringLoggerConfiguration.kt @@ -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 { + return JsonLogCreatorImpl() + } + + @Bean + fun localLogCreator(): LogCreator { + return SimpleLogCreatorImpl() + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + fun logBuilderFactory(logCreator: LogCreator): LogBuilderFactory { + return LogBuilderFactoryImpl(logCreator) + } + + @Bean + fun logAspect( + logBuilderFactory: LogBuilderFactory, + logValueFieldSerializer: LogValueFieldSerializer + ): LogAspect { + return LogAspect(logBuilderFactory, logValueFieldSerializer) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/listeners/ApplicationLifeCycleEventListener.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/listeners/ApplicationLifeCycleEventListener.kt new file mode 100644 index 0000000..b318367 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/listeners/ApplicationLifeCycleEventListener.kt @@ -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 +) { + 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() + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializer.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializer.kt new file mode 100644 index 0000000..14385d5 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializer.kt @@ -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 + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializerImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializerImpl.kt new file mode 100644 index 0000000..05cf8b1 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/LogValueFieldSerializerImpl.kt @@ -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> +) : 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 { + 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 { + return expand(field).mapNotNull(this::serialize) + } + + companion object { + private const val DEFAULT_RETURN_FIELD_NAME = "result" + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/BooleanLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/BooleanLogValueResolverImpl.kt new file mode 100644 index 0000000..5b6e66e --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/BooleanLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is Boolean) { + return null + } + + return ResolvedValue( + value = value, + typeName = "boolean" + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/DateLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/DateLogValueResolverImpl.kt new file mode 100644 index 0000000..2a98caf --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/DateLogValueResolverImpl.kt @@ -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 { + + override fun invoke(value: Any): ResolvedValue? { + if (value !is Temporal) { + return null + } + + return ResolvedValue( + value = value.toString(), + typeName = "date", + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FileLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FileLogValueResolverImpl.kt new file mode 100644 index 0000000..0d654f7 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FileLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is File) { + return null + } + + return ResolvedValue( + value = value.toString(), + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FunctionLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FunctionLogValueResolverImpl.kt new file mode 100644 index 0000000..6ceeb37 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/FunctionLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is Function<*>) { + return null + } + + return ResolvedValue.SKIP_VALUE + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/LogValueResolver.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/LogValueResolver.kt new file mode 100644 index 0000000..25afc1d --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/LogValueResolver.kt @@ -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 { + + operator fun invoke(value: Any): ResolvedValue? + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/NumberLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/NumberLogValueResolverImpl.kt new file mode 100644 index 0000000..cd697ca --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/NumberLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is Number) { + return null + } + + return ResolvedValue( + value = value, + typeName = "number" + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/ObjectLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/ObjectLogValueResolverImpl.kt new file mode 100644 index 0000000..fe14ca2 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/ObjectLogValueResolverImpl.kt @@ -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 { + + private val objectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + + override operator fun invoke(value: Any): ResolvedValue { + return ResolvedValue( + value = objectMapper.writeValueAsString(value) + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/StringLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/StringLogValueResolverImpl.kt new file mode 100644 index 0000000..e569a5f --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/StringLogValueResolverImpl.kt @@ -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 { + + override fun invoke(value: Any): ResolvedValue? { + if (value !is String) { + return null + } + + return ResolvedValue( + value = value + ) + } +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UUIDLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UUIDLogValueResolverImpl.kt new file mode 100644 index 0000000..b0ce839 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UUIDLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is UUID) { + return null + } + + return ResolvedValue( + value = value.toString(), + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UnitLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UnitLogValueResolverImpl.kt new file mode 100644 index 0000000..e990e8d --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UnitLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is Unit) { + return null + } + + return ResolvedValue.SKIP_VALUE + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UrlLogValueResolverImpl.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UrlLogValueResolverImpl.kt new file mode 100644 index 0000000..8e66c5e --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/UrlLogValueResolverImpl.kt @@ -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 { + + override operator fun invoke(value: Any): ResolvedValue? { + if (value !is URL && value !is URI) { + return null + } + + return ResolvedValue( + value = value.toString(), + ) + } + +} diff --git a/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/dto/ResolvedValue.kt b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/dto/ResolvedValue.kt new file mode 100644 index 0000000..6717b78 --- /dev/null +++ b/logger-spring/src/main/kotlin/ru/touchin/logger/spring/serializers/resolvers/dto/ResolvedValue.kt @@ -0,0 +1,13 @@ +@file:Suppress("unused") +package ru.touchin.logger.spring.serializers.resolvers.dto + +class ResolvedValue( + val value: T?, + val typeName: String = "" +) { + + companion object { + val SKIP_VALUE = ResolvedValue(value = null) + } + +} diff --git a/logger-spring/src/test/kotlin/ru/touchin/logger/TestApplication.kt b/logger-spring/src/test/kotlin/ru/touchin/logger/TestApplication.kt new file mode 100644 index 0000000..2f9ccdc --- /dev/null +++ b/logger-spring/src/test/kotlin/ru/touchin/logger/TestApplication.kt @@ -0,0 +1,6 @@ +package ru.touchin.logger + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class TestApplication diff --git a/logger-spring/src/test/kotlin/ru/touchin/logger/serializers/impl/LogValueFieldResolverImplTest.kt b/logger-spring/src/test/kotlin/ru/touchin/logger/serializers/impl/LogValueFieldResolverImplTest.kt new file mode 100644 index 0000000..ce6c10c --- /dev/null +++ b/logger-spring/src/test/kotlin/ru/touchin/logger/serializers/impl/LogValueFieldResolverImplTest.kt @@ -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) + } + + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8d1f6ad..6871629 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,3 +26,4 @@ include("common-spring-web") include("common-spring-test") include("common-spring-test-jpa") include("logger") +include("logger-spring")