diff --git a/README.md b/README.md index 85dd026f..4f76ad10 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ RootLogger.Tracing.register(SimpleConsoleTracingAppender()) // Create the span and then start it. val span: TracingEvent.Span = trace.span("test").start() span.event(name = "test-event") -span.tracer // Close the span manually. span.end() ``` @@ -113,6 +112,8 @@ Similarly to the logging API, we also support a more kotlin style API: val parent: TracingEvent.Span = trace.span(id = "PARENT_SPAN_ID", traceId = "TRACE_ID", name = "parent") // Starts immediately the span. trace.span("test", parent) { + // Set span attributes. + it.attributes["key"] = "value" // Send events that are related to the current span. it.event(name = "event-1", level = Level.DEBUG) it.debug(name = "event-1") // Same as event(name = "event-1", level = Level.DEBUG) @@ -175,6 +176,9 @@ repeat(10) { // Starts immediately the span. trace.span("test") { + log.info(it, "this is a test with span") // The log will contain the span id. + // Set span attributes. + it.attributes["key"] = "value" // Send events that are related to the current span. it.event(name = "event-1", level = Level.DEBUG) // Include attributes in the event. @@ -188,7 +192,6 @@ trace.span("test") { // Create the span and then start it. val span: TracingEvent.Span = trace.span("test").start() span.event("this is a test event") -span.tracer // Close the span manually. span.end() ``` \ No newline at end of file diff --git a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/Tracer.kt b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/Tracer.kt index a7782e06..690dd603 100644 --- a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/Tracer.kt +++ b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/Tracer.kt @@ -21,10 +21,10 @@ abstract class Tracer( } fun span(id: String, traceId: String, name: String): TracingEvent.Span = - TracingEvent.Span(id = id, level = level, tracer = this, name = name, traceId = traceId) + TracingEvent.Span.of(id = id, level = level, tracer = this, name = name, traceId = traceId, isRemote = true) fun span(name: String, parent: TracingEvent.Span? = null): TracingEvent.Span = - TracingEvent.Span(id = RootLogger.Tracing.id(), level = level, tracer = this, name = name, parent = parent) + TracingEvent.Span.of(id = RootLogger.Tracing.id(), level = level, tracer = this, name = name, parent = parent) inline fun span( name: String, @@ -33,12 +33,11 @@ abstract class Tracer( ): T { val span = span(name, parent).start() return try { - f(span) + f(span).also { span.end() } } catch (e: Throwable) { + span.exception(e, true) span.end(e) throw e - } finally { - span.end() } } diff --git a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/TracingEvent.kt b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/TracingEvent.kt index d81ef7f5..ec6d1e28 100644 --- a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/TracingEvent.kt +++ b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/TracingEvent.kt @@ -1,57 +1,46 @@ package io.github.smyrgeorge.log4k -import io.github.smyrgeorge.log4k.impl.extensions.withLockBlocking -import kotlinx.coroutines.sync.Mutex +import io.github.smyrgeorge.log4k.impl.OpenTelemetry import kotlinx.datetime.Clock import kotlinx.datetime.Instant -@Suppress("MemberVisibilityCanBePrivate") +@Suppress("MemberVisibilityCanBePrivate", "unused") interface TracingEvent { - val id: String - val name: String - // https://opentelemetry.io/docs/specs/otel/trace/api/#span - @Suppress("unused") - class Span( - override val id: String, - override val name: String, + data class Span( + val name: String, val level: Level, - val tracer: Tracer, - val parent: Span? = null, - val traceId: String = parent?.traceId ?: id, + val context: Context, + val parent: Span?, + var start: Instant?, + var end: Instant?, + val attributes: MutableMap, + val events: MutableList, + var status: Status, ) : TracingEvent { - var status: Status = Status.UNSET - var start: Instant? = null - var end: Instant? = null - val events: MutableList = mutableListOf() - var error: Throwable? = null private fun shouldStart(): Boolean = - level.ordinal >= tracer.level.ordinal + !context.isRemote && level.ordinal >= context.tracer.level.ordinal private fun shouldLogEvent(level: Level): Boolean = - level.ordinal >= level.ordinal + !context.isRemote && level.ordinal >= level.ordinal - private val mutex = Mutex() - private var idx: Int = 0 private var started: Boolean = false private var closed: Boolean = false - private fun idx(): Int = ++idx - fun start(): Span = mutex.withLockBlocking { - if (!shouldStart()) return@withLockBlocking this - if (started) return@withLockBlocking this + fun start(): Span { + if (!shouldStart()) return this + if (started) return this start = Clock.System.now() started = true - this + return this } - fun event(name: String, level: Level, attrs: Map = emptyMap()): Unit = mutex.withLockBlocking { - if (!shouldStart()) return@withLockBlocking - if (!shouldLogEvent(level)) return@withLockBlocking - if (!started || closed) return@withLockBlocking + fun event(name: String, level: Level, attrs: Map = emptyMap()) { + if (!shouldStart()) return + if (!shouldLogEvent(level)) return + if (!started || closed) return val event = Event( - id = "$id-${idx()}", name = name, attributes = attrs, timestamp = Clock.System.now() @@ -59,13 +48,44 @@ interface TracingEvent { events.add(event) } - fun end(error: Throwable? = null): Unit = mutex.withLockBlocking { - if (!shouldStart()) return@withLockBlocking - if (closed || !started) return@withLockBlocking + /** + * https://opentelemetry.io/docs/specs/otel/trace/exceptions/ + * Records an exception event with the given attributes. + * + * @param error The throwable error to be recorded. + * @param escaped A boolean indicating if the exception was propagated. + * @param attrs A map of additional attributes to associate with the exception event. + */ + fun exception(error: Throwable, escaped: Boolean, attrs: Map = emptyMap()) { + val event = Event( + name = OpenTelemetry.EXCEPTION, + timestamp = Clock.System.now(), + attributes = attrs + mapOf( + OpenTelemetry.EXCEPTION_TYPE to error::class.qualifiedName, + OpenTelemetry.EXCEPTION_ESCAPED to escaped, + OpenTelemetry.EXCEPTION_MESSAGE to error.message, + OpenTelemetry.EXCEPTION_STACKTRACE to error.stackTraceToString(), + ) + ) + events.add(event) + } + + fun exception(error: Throwable, escaped: Boolean, f: (MutableMap) -> Unit) { + val attributes: MutableMap = mutableMapOf() + f(attributes) + exception(error, escaped, attributes) + } + + fun end(error: Throwable? = null) { + if (!shouldStart()) return + if (closed || !started) return end = Clock.System.now() closed = true - this.error = error - status = error?.let { Status.ERROR } ?: Status.OK + status = Status( + code = error?.let { Status.Code.ERROR } ?: Status.Code.OK, + error = error, + description = error?.message, + ) RootLogger.trace(this) } @@ -91,22 +111,52 @@ interface TracingEvent { } } - enum class Status { UNSET, OK, ERROR } + // https://opentelemetry.io/docs/specs/otel/trace/api/#spancontext + data class Context( + val traceId: String, + val spanId: String, + val isRemote: Boolean, // Indicates whether the Span was received from somewhere else or locally generated. + val tracer: Tracer, // Information about the local [Tracer]. + ) { + data class Tracer(val name: String, val level: Level) + } - override fun toString(): String { - return "Span(id='$id', traceId='$traceId', name='$name', parent=$parent, status=$status, start=$start, end=$end, error=${error?.message}, events=$events)" + // https://opentelemetry.io/docs/specs/otel/trace/api/#add-events + data class Event( + val name: String, + val timestamp: Instant, + val attributes: Map, + ) + + // https://opentelemetry.io/docs/specs/otel/trace/api/#set-status + data class Status( + val code: Code = Code.UNSET, + val error: Throwable? = null, + val description: String? = null, + ) { + enum class Code { UNSET, OK, ERROR } } - } - // https://opentelemetry.io/docs/specs/otel/trace/api/#add-events - class Event( - override val id: String, - override val name: String, - val attributes: Map, - val timestamp: Instant, - ) : TracingEvent { - override fun toString(): String { - return "Event(id='$id', name='$name', attributes=$attributes, timestamp=$timestamp)" + companion object { + fun of( + id: String, + name: String, + level: Level, + tracer: Tracer, + parent: Span? = null, + isRemote: Boolean = false, + traceId: String = parent?.context?.traceId ?: id, + ) = Span( + name = name, + level = level, + context = Context(traceId, id, isRemote, Context.Tracer(tracer.name, tracer.level)), + parent = parent, + start = null, + end = null, + attributes = mutableMapOf(), + events = mutableListOf(), + status = Status(), + ) } } } \ No newline at end of file diff --git a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/OpenTelemetry.kt b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/OpenTelemetry.kt new file mode 100644 index 00000000..196b0745 --- /dev/null +++ b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/OpenTelemetry.kt @@ -0,0 +1,56 @@ +package io.github.smyrgeorge.log4k.impl + +@Suppress("unused") +object OpenTelemetry { + // Exception Attributes + const val EXCEPTION = "exception" + const val EXCEPTION_MESSAGE = "exception.message" // The exception message or error message + const val EXCEPTION_STACKTRACE = "exception.stacktrace" // Full stack trace of the exception + const val EXCEPTION_TYPE = "exception.type" // Type or class of the exception (e.g., IOException, NullPointerException) + const val EXCEPTION_ESCAPED = "exception.escaped" // Indicates if the exception was caught (false) or escaped the span (true) + const val EXCEPTION_CAUGHT = "exception.caught" // Boolean indicating whether the exception was caught in the current span + const val EXCEPTION_CAUSE_TYPE = "exception.cause.type" // Type or class of the root cause exception (if there is a cause chain) + const val EXCEPTION_CAUSE_MESSAGE = "exception.cause.message" // The message from the root cause exception + const val EXCEPTION_CAUSE_STACKTRACE = "exception.cause.stacktrace" // Stack trace of the root cause exception + const val EXCEPTION_HANDLED_AT = "exception.handled_at" // The location (file, method, or class) where the exception was handled + const val EXCEPTION_TIMESTAMP = "exception.timestamp" // The time when the exception occurred + + // HTTP Attributes + const val HTTP_METHOD = "http.method" // HTTP method (e.g., GET, POST, PUT) + const val HTTP_URL = "http.url" // Full URL of the HTTP request + const val HTTP_TARGET = "http.target" // The target of the HTTP request (e.g., path in the URL) + const val HTTP_HOST = "http.host" // Host header value (e.g., www.example.com) + const val HTTP_SCHEME = "http.scheme" // URL scheme (http or https) + const val HTTP_STATUS_CODE = "http.status_code" // HTTP response status code (e.g., 200, 404) + const val HTTP_FLAVOR = "http.flavor" // HTTP protocol version (e.g., 1.1, 2.0) + const val HTTP_USER_AGENT = "http.user_agent" // Value of the User-Agent header in the request + const val HTTP_SERVER_NAME = "http.server_name" // The server name as defined by the host or service + const val HTTP_ROUTE = "http.route" // Matched route (for server spans) + const val HTTP_CLIENT_IP = "http.client_ip" // Client's IP address + const val HTTP_REQUEST_CONTENT_LENGTH = "http.request_content_length" // Size of the request body in bytes + const val HTTP_RESPONSE_CONTENT_LENGTH = "http.response_content_length" // Size of the response body in bytes + const val HTTP_ERROR_MESSAGE = "http.error_message" // Error message when an HTTP error occurs + + // SQL Attributes + const val DB_SYSTEM = "db.system" // The type of the database (e.g., mysql, postgresql, sqlite) + const val DB_CONNECTION_STRING = "db.connection_string" // Connection string used to connect to the database + const val DB_USER = "db.user" // The database username + const val DB_NAME = "db.name" // The name of the database being accessed + const val DB_STATEMENT = "db.statement" // The actual SQL query or command executed + const val DB_OPERATION = "db.operation" // Type of operation (e.g., SELECT, INSERT, UPDATE) + const val DB_SQL_TABLE = "db.sql.table" // The table involved in the query (if applicable) + const val DB_SQL_ROW_COUNT = "db.sql.row_count" // Number of rows returned or affected by the query + const val DB_STATEMENT_ERROR = "db.statement.error" // Error message if the query fails + const val DB_DRIVER_NAME = "db.driver.name" // Name of the database driver (e.g., org.postgresql.Driver) + const val DB_DRIVER_VERSION = "db.driver.version" // Version of the database driver + const val DB_CONNECTION_ID = "db.connection.id" // Identifier for the database connection + + // Thread Attributes + const val THREAD_ID = "thread.id" // The system ID of the thread (native thread ID) + const val THREAD_NAME = "thread.name" // The name of the thread (as set by the application or JVM) + const val THREAD_STATE = "thread.state" // The current state of the thread (e.g., RUNNABLE, BLOCKED, WAITING) + const val THREAD_PRIORITY = "thread.priority" // The priority of the thread (if applicable) + const val THREAD_CPU_TIME = "thread.cpu_time" // The CPU time consumed by the thread (in nanoseconds, if available) + const val THREAD_BLOCKED_TIME = "thread.blocked_time" // Time spent blocked (waiting for a monitor or lock) + const val THREAD_WAITING_TIME = "thread.waiting_time" // Time spent waiting (in WAITING or TIMED_WAITING state) +} \ No newline at end of file diff --git a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/appenders/SimpleConsoleLoggingAppender.kt b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/appenders/SimpleConsoleLoggingAppender.kt index ade14a33..df95796c 100644 --- a/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/appenders/SimpleConsoleLoggingAppender.kt +++ b/log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/impl/appenders/SimpleConsoleLoggingAppender.kt @@ -15,7 +15,7 @@ class SimpleConsoleLoggingAppender : Appender { private fun LoggingEvent.format(): String = buildString { append(id) append(' ') - append(span?.id?.let { " [$it] " } ?: " ") + append(span?.context?.spanId?.let { " [$it] " } ?: " ") append(timestamp) append(' ') append(level.name.padEnd(5)) diff --git a/log4k/src/commonTest/kotlin/io/github/smyrgeorge/log4k/MainTests.kt b/log4k/src/commonTest/kotlin/io/github/smyrgeorge/log4k/MainTests.kt index 3117b979..d51f3f49 100644 --- a/log4k/src/commonTest/kotlin/io/github/smyrgeorge/log4k/MainTests.kt +++ b/log4k/src/commonTest/kotlin/io/github/smyrgeorge/log4k/MainTests.kt @@ -46,15 +46,15 @@ class MainTests { delay(500) } - delay(1000) - RootLogger.Tracing.register(SimpleConsoleTracingAppender()) // Create the parent span. // NOTICE: we do not start it, since it's already started. val parent: TracingEvent.Span = trace.span(id = "ID_EXAMPLE", traceId = "TRACE_ID_EXAMPLE", name = "parent") // Starts immediately the span. trace.span("test", parent) { - log.info(it, "this is a test with span") + log.info(it, "this is a test with span") // The log will contain the span id. + // Set span attributes. + it.attributes["key"] = "value" // Send events that are related to the current span. it.event(name = "event-1", level = Level.DEBUG) it.debug(name = "event-1") // Same as event(name = "event-1", level = Level.DEBUG) @@ -69,7 +69,6 @@ class MainTests { // Create the span and then start it. val span: TracingEvent.Span = trace.span("test").start() span.event("this is a test event") - span.tracer // Close the span manually. span.end()