Skip to content

Commit

Permalink
Add OpenTelemetry support and enhance tracing functionalities
Browse files Browse the repository at this point in the history
Introduce OpenTelemetry constants and attributes to enrich telemetry data. Refactor tracing logic and improve span handling, including exception recording and attribute setting. Update documentation and adjust examples to reflect these changes.
  • Loading branch information
smyrgeorge committed Oct 21, 2024
1 parent 942270d commit 01e2682
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 62 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
```
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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()
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> span(
name: String,
Expand All @@ -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()
}
}

Expand Down
150 changes: 100 additions & 50 deletions log4k/src/commonMain/kotlin/io/github/smyrgeorge/log4k/TracingEvent.kt
Original file line number Diff line number Diff line change
@@ -1,71 +1,91 @@
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<String, Any?>,
val events: MutableList<Event>,
var status: Status,
) : TracingEvent {
var status: Status = Status.UNSET
var start: Instant? = null
var end: Instant? = null
val events: MutableList<Event> = 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<String, Any?> = 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<String, Any?> = 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()
)
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<String, Any?> = 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<String, Any?>) -> Unit) {
val attributes: MutableMap<String, Any?> = 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)
}

Expand All @@ -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<String, Any?>,
)

// 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<String, Any?>,
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(),
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SimpleConsoleLoggingAppender : Appender<LoggingEvent> {
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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()

Expand Down

0 comments on commit 01e2682

Please sign in to comment.