Skip to content

Commit

Permalink
logback integration (#5)
Browse files Browse the repository at this point in the history
* logback integration

* make exception not null for protocol

* use HubAdapter

* fix test

* reflect review
  • Loading branch information
davin111 authored Apr 2, 2023
1 parent 9cd4783 commit 8413001
Show file tree
Hide file tree
Showing 20 changed files with 285 additions and 79 deletions.
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
rootProject.name = "truffle-kotlin"

include("truffle-core")
include("truffle-logback")
include("truffle-spring-boot-starter")
18 changes: 18 additions & 0 deletions truffle-core/src/main/kotlin/Hub.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.wafflestudio.truffle.sdk.core

import com.wafflestudio.truffle.sdk.core.protocol.TruffleEvent
import org.springframework.web.reactive.function.client.WebClient

internal class Hub(
truffleOptions: TruffleOptions,
webClientBuilder: WebClient.Builder? = null,
) : IHub {
private val client: TruffleClient = DefaultTruffleClient(
apiKey = truffleOptions.apiKey,
webClientBuilder = webClientBuilder ?: WebClient.builder(),
)

override fun captureEvent(truffleEvent: TruffleEvent) {
client.sendEvent(truffleEvent)
}
}
7 changes: 7 additions & 0 deletions truffle-core/src/main/kotlin/IHub.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.wafflestudio.truffle.sdk.core

import com.wafflestudio.truffle.sdk.core.protocol.TruffleEvent

interface IHub {
fun captureEvent(truffleEvent: TruffleEvent)
}
40 changes: 40 additions & 0 deletions truffle-core/src/main/kotlin/Truffle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.wafflestudio.truffle.sdk.core

import com.wafflestudio.truffle.sdk.core.protocol.TruffleEvent
import org.springframework.web.reactive.function.client.WebClient

object Truffle {
private lateinit var hub: IHub

internal object HubAdapter : IHub {
override fun captureEvent(truffleEvent: TruffleEvent) {
Truffle.captureEvent(truffleEvent)
}
}

// for modules without access WebClient.Builder
fun init(truffleOptions: TruffleOptions): IHub {
return init(truffleOptions, null)
}

@Synchronized fun init(truffleOptions: TruffleOptions, webClientBuilder: WebClient.Builder? = null): IHub {
if (::hub.isInitialized) {
return hub
}

validateConfig(truffleOptions)

this.hub = Hub(truffleOptions, webClientBuilder)
return HubAdapter
}

private fun captureEvent(truffleEvent: TruffleEvent) {
hub.captureEvent(truffleEvent)
}

private fun validateConfig(truffleOptions: TruffleOptions) {
if (truffleOptions.apiKey.isBlank()) {
throw IllegalArgumentException("Truffle API key is blank")
}
}
}
31 changes: 8 additions & 23 deletions truffle-core/src/main/kotlin/TruffleClient.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.wafflestudio.truffle.sdk.core

import com.wafflestudio.truffle.sdk.core.protocol.TruffleApp
import com.wafflestudio.truffle.sdk.core.protocol.TruffleEvent
import com.wafflestudio.truffle.sdk.core.protocol.TruffleException
import com.wafflestudio.truffle.sdk.core.protocol.TruffleRuntime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
Expand All @@ -16,25 +13,21 @@ import org.springframework.web.reactive.function.client.bodyToMono
import java.time.Duration
import java.util.concurrent.Executors

interface TruffleClient {
fun sendEvent(ex: Throwable)
internal interface TruffleClient {
fun sendEvent(truffleEvent: TruffleEvent)
}

class DefaultTruffleClient(
name: String,
phase: String,
internal class DefaultTruffleClient(
apiKey: String,
webClientBuilder: WebClient.Builder,
) : TruffleClient {
private val events = MutableSharedFlow<TruffleEvent>(extraBufferCapacity = 10)

private val logger = LoggerFactory.getLogger(javaClass)

private val truffleApp = TruffleApp(name, phase)
private val truffleRuntime = TruffleRuntime(name = "Java", version = System.getProperty("java.version"))

init {
val coroutineScope = CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
val coroutineScope = CoroutineScope(
Executors.newSingleThreadExecutor { r -> Thread(r, "truffle-client") }.asCoroutineDispatcher()
)
val webClient = webClientBuilder
.baseUrl("https://truffle-api.wafflestudio.com")
.defaultHeader("x-api-key", apiKey)
Expand All @@ -58,15 +51,7 @@ class DefaultTruffleClient(
}
}

override fun sendEvent(ex: Throwable) {
if (truffleApp.phase == "local" || truffleApp.phase == "test") return

events.tryEmit(
TruffleEvent(
app = truffleApp,
runtime = truffleRuntime,
exception = TruffleException(ex),
)
)
override fun sendEvent(truffleEvent: TruffleEvent) {
events.tryEmit(truffleEvent)
}
}
17 changes: 17 additions & 0 deletions truffle-core/src/main/kotlin/TruffleOptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.wafflestudio.truffle.sdk.core

import ch.qos.logback.classic.Level

open class TruffleOptions {
/**
* Truffle 서버에서 애플리케이션의 요청이 유효한지 검증하는 데에 사용하는 API key.
*
* 외부에 공개되지 않도록 주의해 관리해야 합니다.
*/
open lateinit var apiKey: String

/**
* truffle logback 사용 시 이벤트를 전송할 최소 로그 레벨.
*/
open var minimumLevel: Level = Level.ERROR
}
6 changes: 0 additions & 6 deletions truffle-core/src/main/kotlin/protocol/TruffleApp.kt

This file was deleted.

10 changes: 10 additions & 0 deletions truffle-core/src/main/kotlin/protocol/TruffleAppInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.wafflestudio.truffle.sdk.core.protocol

object TruffleAppInfo {
val runtime = TruffleRuntime()

data class TruffleRuntime(
val name: String = "Java",
val version: String = System.getProperty("java.version")
)
}
4 changes: 2 additions & 2 deletions truffle-core/src/main/kotlin/protocol/TruffleEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.wafflestudio.truffle.sdk.core.protocol

data class TruffleEvent(
val version: String = TruffleVersion.V1,
val app: TruffleApp,
val runtime: TruffleRuntime,
val runtime: TruffleAppInfo.TruffleRuntime = TruffleAppInfo.runtime,
val level: TruffleLevel,
val exception: TruffleException,
)
23 changes: 23 additions & 0 deletions truffle-core/src/main/kotlin/protocol/TruffleLevel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.wafflestudio.truffle.sdk.core.protocol

import ch.qos.logback.classic.Level

enum class TruffleLevel {
DEBUG,
INFO,
WARNING,
ERROR,
FATAL,
;

companion object {
fun from(level: Level): TruffleLevel {
return when {
level.isGreaterOrEqual(Level.ERROR) -> ERROR
level.isGreaterOrEqual(Level.WARN) -> WARNING
level.isGreaterOrEqual(Level.INFO) -> INFO
else -> DEBUG
}
}
}
}
6 changes: 0 additions & 6 deletions truffle-core/src/main/kotlin/protocol/TruffleRuntime.kt

This file was deleted.

5 changes: 5 additions & 0 deletions truffle-logback/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dependencies {
compileOnly("ch.qos.logback:logback-classic")

implementation(project(":truffle-core"))
}
53 changes: 53 additions & 0 deletions truffle-logback/src/main/kotlin/appender/TruffleAppender.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.wafflestudio.truffle.sdk.logback

import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.classic.spi.ThrowableProxy
import ch.qos.logback.core.UnsynchronizedAppenderBase
import com.wafflestudio.truffle.sdk.core.IHub
import com.wafflestudio.truffle.sdk.core.Truffle
import com.wafflestudio.truffle.sdk.core.TruffleOptions
import com.wafflestudio.truffle.sdk.core.protocol.TruffleEvent
import com.wafflestudio.truffle.sdk.core.protocol.TruffleException
import com.wafflestudio.truffle.sdk.core.protocol.TruffleLevel

class TruffleAppender : UnsynchronizedAppenderBase<ILoggingEvent>() {
lateinit var options: TruffleOptions
private lateinit var hub: IHub

override fun start() {
hub = Truffle.init(options)
super.start()
}

override fun append(eventObject: ILoggingEvent) {
if (eventObject.level.isGreaterOrEqual(options.minimumLevel) &&
!eventObject.loggerName.startsWith("com.wafflestudio.truffle.sdk")
) {
val truffleEvent = createEvent(eventObject)
hub.captureEvent(truffleEvent)
}
}

private fun createEvent(eventObject: ILoggingEvent): TruffleEvent {
val exception = eventObject.throwableProxy?.let {
TruffleException((it as ThrowableProxy).throwable)
} ?: TruffleException(
className = eventObject.loggerName,
message = eventObject.formattedMessage,
elements = eventObject.callerData.map {
TruffleException.Element(
className = it.className,
methodName = it.methodName,
fileName = it.fileName ?: "",
lineNumber = it.lineNumber,
isInAppInclude = true, // FIXME
)
},
)

return TruffleEvent(
level = TruffleLevel.from(eventObject.level),
exception = exception,
)
}
}
1 change: 1 addition & 0 deletions truffle-spring-boot-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

implementation(project(":truffle-core"))
compileOnly(project(":truffle-logback"))
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.wafflestudio.truffle.sdk

import com.wafflestudio.truffle.sdk.core.DefaultTruffleClient
import com.wafflestudio.truffle.sdk.core.TruffleClient
import ch.qos.logback.classic.LoggerContext
import com.wafflestudio.truffle.sdk.core.IHub
import com.wafflestudio.truffle.sdk.core.Truffle
import com.wafflestudio.truffle.sdk.logback.TruffleAppender
import com.wafflestudio.truffle.sdk.reactive.TruffleWebExceptionHandler
import com.wafflestudio.truffle.sdk.servlet.TruffleHandlerExceptionResolver
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
Expand All @@ -13,32 +17,39 @@ import org.springframework.web.server.WebExceptionHandler
import org.springframework.web.servlet.HandlerExceptionResolver

@EnableConfigurationProperties(TruffleProperties::class)
@ConditionalOnProperty(value = ["truffle.enabled"], havingValue = "true")
@Configuration
class TruffleAutoConfiguration {
@Bean
fun truffleHub(properties: TruffleProperties, webClientBuilder: WebClient.Builder): IHub {
return Truffle.init(properties, webClientBuilder)
}

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@Configuration
class TruffleServletConfiguration {
@Bean
fun truffleHandlerExceptionResolver(truffleClient: TruffleClient): HandlerExceptionResolver {
return TruffleHandlerExceptionResolver(truffleClient)
fun truffleHandlerExceptionResolver(truffleHub: IHub): HandlerExceptionResolver {
return TruffleHandlerExceptionResolver(truffleHub)
}
}

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Configuration
class TruffleReactiveConfiguration {
@Bean
fun truffleWebExceptionHandler(truffleClient: TruffleClient): WebExceptionHandler {
return TruffleWebExceptionHandler(truffleClient)
fun truffleWebExceptionHandler(truffleHub: IHub): WebExceptionHandler {
return TruffleWebExceptionHandler(truffleHub)
}
}

@Bean
fun truffleClient(properties: TruffleProperties, webClientBuilder: WebClient.Builder): TruffleClient =
DefaultTruffleClient(
name = properties.name,
phase = properties.phase,
apiKey = properties.apiKey,
webClientBuilder = webClientBuilder,
)
@ConditionalOnClass(value = [LoggerContext::class, TruffleAppender::class])
@ConditionalOnProperty(value = ["truffle.logback.enabled"], havingValue = "true", matchIfMissing = true)
@Configuration
class TruffleLogbackConfiguration {
@Bean
fun truffleLogbackInitializer(truffleProperties: TruffleProperties): TruffleLogbackInitializer {
return TruffleLogbackInitializer(truffleProperties)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.wafflestudio.truffle.sdk

import ch.qos.logback.classic.LoggerContext
import com.wafflestudio.truffle.sdk.logback.TruffleAppender
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEvent
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.context.event.GenericApplicationListener
import org.springframework.core.ResolvableType
import ch.qos.logback.classic.Logger as LogbackLogger

class TruffleLogbackInitializer(
private val truffleProperties: TruffleProperties,
) : GenericApplicationListener {
override fun supportsEventType(eventType: ResolvableType): Boolean {
return eventType.rawClass?.let { ContextRefreshedEvent::class.java.isAssignableFrom(it) } ?: false
}

override fun onApplicationEvent(event: ApplicationEvent) {
val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as LogbackLogger

if (!isTruffleAppenderRegistered(rootLogger)) {
val truffleAppender = TruffleAppender()
truffleAppender.name = "TRUFFLE_APPENDER"
truffleAppender.context = LoggerFactory.getILoggerFactory() as LoggerContext
truffleAppender.options = truffleProperties

truffleAppender.start()
rootLogger.addAppender(truffleAppender)
}
}

private fun isTruffleAppenderRegistered(logger: LogbackLogger): Boolean {
val iterator = logger.iteratorForAppenders()
while (iterator.hasNext()) {
if (iterator.next() is TruffleAppender) {
return true
}
}
return false
}
}
Loading

0 comments on commit 8413001

Please sign in to comment.