Skip to content

Commit

Permalink
Refactor logging and tracing implementations
Browse files Browse the repository at this point in the history
Introduce a `TracingEvent` interface and `SimpleConsoleTracingAppender` for tracing support, while refactoring `LoggingEvent` into an interface and renaming ConsoleAppender to `SimpleConsoleLoggingAppender`. This update also includes making the `Appender` interface generic, restructuring `Logger` and `RootLogger` for better logging and tracing separation, and revising tests to incorporate tracing functionality.
  • Loading branch information
smyrgeorge committed Oct 19, 2024
1 parent d4f2921 commit afc090b
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 97 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.smyrgeorge.log4k

interface Appender {
interface Appender<T> {
val name: String
fun append(event: LoggingEvent)
fun append(event: T)
}
57 changes: 34 additions & 23 deletions log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/Logger.kt
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
package io.github.smyrgeorge.log4k

import kotlinx.datetime.Clock
import kotlin.reflect.KClass

@Suppress("unused", "MemberVisibilityCanBePrivate", "ConvertSecondaryConstructorToPrimary")
abstract class Logger {
val name: String
@Suppress("unused", "MemberVisibilityCanBePrivate")
abstract class Logger(
val name: String,
private var level: Level
private var levelBeforeMute: Level

constructor(name: String, level: Level) {
this.name = name
this.level = level
this.levelBeforeMute = level
}
) {
private var levelBeforeMute: Level = level

private fun log(level: Level, msg: String, args: Array<out Any?>) =
log(level, msg, null, args)

private fun log(level: Level, msg: String, throwable: Throwable?, args: Array<out Any?>) {
if (!level.shouldLog()) return
val event = LoggingEvent(
level = level,
message = msg,
logger = name,
arguments = args,
timestamp = Clock.System.now(),
thread = null,
throwable = throwable
)

val event = toLoggingEvent(level, msg, throwable, args)
RootLogger.log(event)
}

abstract fun toLoggingEvent(level: Level, msg: String, throwable: Throwable?, args: Array<out Any?>): LoggingEvent

private fun Level.shouldLog(): Boolean =
ordinal >= level.ordinal

Expand All @@ -50,6 +37,30 @@ abstract class Logger {
levelBeforeMute = level
}

fun span(name: String, parent: String? = null): TracingEvent.Span =
TracingEvent.Span(RootLogger.Tracing.id(), name, level, parent, this)

inline fun <T> span(name: String, parent: String? = null, f: (TracingEvent.Span) -> T): T {
val span = span(name, parent)
return try {
f(span)
} finally {
span.end()
}
}

fun span(id: String, name: String, parent: String? = null): TracingEvent.Span =
TracingEvent.Span(id, name, level, parent, this)

inline fun <T> span(id: String, name: String, parent: String? = null, f: (TracingEvent.Span) -> T): T {
val span = span(id, name, parent)
return try {
f(span)
} finally {
span.end()
}
}

fun trace(f: () -> String): Unit = if (Level.TRACE.shouldLog()) trace(f()) else Unit
fun trace(msg: String, vararg args: Any?): Unit = log(Level.TRACE, msg, args)
fun debug(f: () -> String): Unit = if (Level.DEBUG.shouldLog()) debug(f()) else Unit
Expand All @@ -64,7 +75,7 @@ abstract class Logger {
fun error(msg: String?, t: Throwable, vararg args: Any?): Unit = log(Level.ERROR, msg ?: "", t, args)

companion object {
fun of(name: String): Logger = RootLogger.factory.getLogger(name)
fun of(clazz: KClass<*>): Logger = RootLogger.factory.getLogger(clazz)
fun of(name: String): Logger = RootLogger.Logging.factory.get(name)
fun of(clazz: KClass<*>): Logger = RootLogger.Logging.factory.get(clazz)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ package io.github.smyrgeorge.log4k
import io.github.smyrgeorge.log4k.impl.extensions.toName
import kotlin.reflect.KClass

@Suppress("unused")
interface LoggerFactory {
fun getLogger(clazz: KClass<*>): Logger = getLogger(clazz.toName())
fun getLogger(name: String): Logger
fun create(name: String): Logger
fun get(clazz: KClass<*>): Logger = get(clazz.toName())
fun get(name: String): Logger {
val existing = RootLogger.Logging.loggers.get(name)
if (existing != null) return existing
return create(name).also {
RootLogger.Logging.loggers.register(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package io.github.smyrgeorge.log4k

import kotlinx.datetime.Instant

class LoggingEvent(
var id: Long = 0,
val level: Level,
val logger: String,
val message: String,
val arguments: Array<out Any?>,
val timestamp: Instant,
val thread: String? = null,
val throwable: Throwable? = null,
)
interface LoggingEvent {
var id: Long
val level: Level
val timestamp: Instant
val logger: String
val message: String
val arguments: Array<out Any?>
val thread: String?
val throwable: Throwable?
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,81 @@
package io.github.smyrgeorge.log4k

import io.github.smyrgeorge.log4k.impl.SimpleLoggerFactory
import io.github.smyrgeorge.log4k.impl.appenders.ConsoleAppender
import io.github.smyrgeorge.log4k.impl.appenders.SimpleConsoleLoggingAppender
import io.github.smyrgeorge.log4k.impl.extensions.forEachParallel
import io.github.smyrgeorge.log4k.registry.AppenderRegistry
import io.github.smyrgeorge.log4k.registry.LoggerRegistry
import io.github.smyrgeorge.log4k.impl.registry.AppenderRegistry
import io.github.smyrgeorge.log4k.impl.registry.LoggerRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext

@Suppress("MemberVisibilityCanBePrivate")
object RootLogger {
private val events: Channel<LoggingEvent> =
val level: Level = Level.INFO

private val logs: Channel<LoggingEvent> =
Channel(capacity = Channel.UNLIMITED)

val level: Level = Level.INFO
val factory = SimpleLoggerFactory()
val loggers = LoggerRegistry()
val appenders = AppenderRegistry()
private val traces: Channel<TracingEvent> =
Channel(capacity = Channel.UNLIMITED)

object Logging {
private var idx: Long = 0
fun id(): Long = ++idx
val factory = SimpleLoggerFactory()
val loggers = LoggerRegistry()
val appenders = AppenderRegistry<LoggingEvent>()
fun register(appender: Appender<LoggingEvent>) = appenders.register(appender)
}

object Tracing {
private var idx: Long = 0
var prefix: String = "span"
fun id(): String = runBlocking { "$prefix-${Clock.System.now().epochSeconds}-${idx()}" }
private val mutex = Mutex()
private suspend fun idx(): Long = mutex.withLock { ++idx }
val appenders = AppenderRegistry<TracingEvent>()
fun register(appender: Appender<TracingEvent>) = appenders.register(appender)
}

init {
register(ConsoleAppender())
Logging.register(SimpleConsoleLoggingAppender())

// Start consuming the logging queue.
// Start consuming the Logging queue.
LoggerScope.launch(Dispatchers.IO) {
events.consumeEach { event ->
event.id = nextIdx()
appenders.all().forEachParallel { it.append(event) }
logs.consumeEach { event ->
event.id = Logging.id()
Logging.appenders.all().forEachParallel { it.append(event) }
}
}
}

fun log(event: LoggingEvent) = runBlocking { events.send(event) }
fun register(appender: Appender) = appenders.register(appender)
// Start consuming the Tracing queue.
TracerScope.launch(Dispatchers.IO) {
traces.consumeEach { event ->
Tracing.appenders.all().forEachParallel { it.append(event) }
}
}
}

private var idx: Long = 0
private fun nextIdx(): Long = ++idx
fun log(event: LoggingEvent) = runBlocking { logs.send(event) }
fun trace(event: TracingEvent) = runBlocking { traces.send(event) }

private object LoggerScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

private object TracerScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.github.smyrgeorge.log4k

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

interface TracingEvent {
val id: String
val level: Level
val timestamp: Instant
val tracer: String
val thread: String?

class Span(
val id: String,
val name: String,
val level: Level,
parent: String? = null,
val logger: Logger,
) {
private val mutex = Mutex()
private var idx: Int = 0
private var closed: Boolean = false
private fun idx(): Int = ++idx

init {
val event = Start(
id = id,
name = name,
level = level,
tracer = logger.name,
parent = parent,
)
RootLogger.trace(event)
}

fun event(msg: String, vararg args: Any?): Unit = withLock {
// If already ended, return.
if (closed) return@withLock
val event = Event(
id = "$id-${idx()}",
spanId = id,
level = level,
tracer = logger.name,
message = msg,
arguments = args,
timestamp = Clock.System.now()
)
RootLogger.trace(event)
}

fun end(): Unit = withLock {
// If already ended, return.
if (closed) return@withLock
closed = true

val event = End(
id = id,
level = level,
tracer = logger.name,
)
RootLogger.trace(event)
}

private fun withLock(f: () -> Unit) = runBlocking { mutex.withLock { f() } }

class Event(
override val id: String,
val spanId: String,
override val level: Level,
override val tracer: String,
val message: String,
val arguments: Array<out Any?>,
override val timestamp: Instant = Clock.System.now(),
override val thread: String? = null
) : TracingEvent

class Start(
override var id: String,
val name: String,
override val level: Level,
override val tracer: String,
val parent: String?,
override val timestamp: Instant = Clock.System.now(),
override val thread: String? = null,
) : TracingEvent

class End(
override var id: String,
override val level: Level,
override val tracer: String,
override val timestamp: Instant = Clock.System.now(),
override val thread: String? = null,
) : TracingEvent
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package io.github.smyrgeorge.log4k.appenders

import io.github.smyrgeorge.log4k.LoggingEvent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.chunked

@Suppress("unused")
abstract class BatchAppender(private val size: Int) : FlowAppender<List<LoggingEvent>>() {
abstract class BatchAppender<T>(private val size: Int) : FlowAppender<List<T>, T>() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun setup(flow: Flow<LoggingEvent>): Flow<List<LoggingEvent>> =
override fun setup(flow: Flow<T>): Flow<List<T>> =
flow.chunked(size)
}
Loading

0 comments on commit afc090b

Please sign in to comment.