From 8103c38c8b71a732142456e439e7bd1f3ef4a35f Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Tue, 10 Feb 2026 20:02:45 +0100 Subject: [PATCH 01/10] fix: Update currentTraceId function to use currentCoroutineContext for trace ID retrieval --- logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt index 2144de2..a3ca497 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt @@ -5,6 +5,7 @@ package dev.logtide.sdk import kotlinx.coroutines.CopyableThreadContextElement import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.currentCoroutineContext import kotlin.coroutines.CoroutineContext /** @@ -92,6 +93,6 @@ class TraceIdElement( * ``` */ suspend fun currentTraceId(): String? { - return kotlin.coroutines.coroutineContext[TraceIdElement]?.traceId + return currentCoroutineContext()[TraceIdElement]?.traceId ?: threadLocalTraceId.get() } From b73307704cdf7a62c1b464559dd87e2c1f117e35 Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Tue, 10 Feb 2026 20:06:25 +0100 Subject: [PATCH 02/10] refactor: Move serializers to dev.logtide.sdk.serializers package and update imports --- .../sdk/models/AggregatedStatsOptions.kt | 1 + .../kotlin/dev/logtide/sdk/models/LogEntry.kt | 1 + .../dev/logtide/sdk/models/QueryOptions.kt | 1 + .../AnyValueSerializer.kt | 29 ++++--------------- .../InstantSerializer.kt | 4 +-- 5 files changed, 11 insertions(+), 25 deletions(-) rename logtide-core/src/main/kotlin/dev/logtide/sdk/{models => serializers}/AnyValueSerializer.kt (75%) rename logtide-core/src/main/kotlin/dev/logtide/sdk/{models => serializers}/InstantSerializer.kt (88%) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AggregatedStatsOptions.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AggregatedStatsOptions.kt index 5f45e54..09957b6 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AggregatedStatsOptions.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AggregatedStatsOptions.kt @@ -1,5 +1,6 @@ package dev.logtide.sdk.models +import dev.logtide.sdk.serializers.InstantSerializer import kotlinx.serialization.Serializable import java.time.Instant diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogEntry.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogEntry.kt index 8fc2e3f..91f9eb3 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogEntry.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogEntry.kt @@ -1,6 +1,7 @@ package dev.logtide.sdk.models import dev.logtide.sdk.enums.LogLevel +import dev.logtide.sdk.serializers.AnyValueSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.Instant diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/QueryOptions.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/QueryOptions.kt index cb43ff2..f0aab99 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/QueryOptions.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/QueryOptions.kt @@ -1,6 +1,7 @@ package dev.logtide.sdk.models import dev.logtide.sdk.enums.LogLevel +import dev.logtide.sdk.serializers.InstantSerializer import kotlinx.serialization.Serializable import java.time.Instant diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AnyValueSerializer.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/AnyValueSerializer.kt similarity index 75% rename from logtide-core/src/main/kotlin/dev/logtide/sdk/models/AnyValueSerializer.kt rename to logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/AnyValueSerializer.kt index f482f60..2162fff 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/AnyValueSerializer.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/AnyValueSerializer.kt @@ -1,4 +1,4 @@ -package dev.logtide.sdk.models +package dev.logtide.sdk.serializers import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind @@ -12,34 +12,14 @@ import kotlinx.serialization.json.* * Custom serializer for Any type in metadata maps * Handles primitive types, lists, and nested maps */ -object AnyValueSerializer : KSerializer { +internal object AnyValueSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AnyValue", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Any) { val jsonEncoder = encoder as? JsonEncoder ?: throw IllegalStateException("This serializer can only be used with Json") - - val element = when (value) { - is Number -> JsonPrimitive(value) - is String -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - is Map<*, *> -> buildJsonObject { - value.forEach { (k, v) -> - if (k is String && v != null) { - put(k, serializeValue(v)) - } - } - } - is List<*> -> buildJsonArray { - value.forEach { item -> - if (item != null) { - add(serializeValue(item)) - } - } - } - else -> JsonPrimitive(value.toString()) - } + val element = serializeValue(value) jsonEncoder.encodeJsonElement(element) } @@ -62,6 +42,7 @@ object AnyValueSerializer : KSerializer { } } } + is List<*> -> buildJsonArray { value.forEach { item -> if (item != null) { @@ -69,6 +50,7 @@ object AnyValueSerializer : KSerializer { } } } + else -> JsonPrimitive(value.toString()) } @@ -82,6 +64,7 @@ object AnyValueSerializer : KSerializer { else -> element.content } } + is JsonObject -> element.mapValues { deserializeJsonElement(it.value) } is JsonArray -> element.map { deserializeJsonElement(it) } } diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/InstantSerializer.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/InstantSerializer.kt similarity index 88% rename from logtide-core/src/main/kotlin/dev/logtide/sdk/models/InstantSerializer.kt rename to logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/InstantSerializer.kt index 0ae260d..8d3f7cd 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/InstantSerializer.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/serializers/InstantSerializer.kt @@ -1,4 +1,4 @@ -package dev.logtide.sdk.models +package dev.logtide.sdk.serializers import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind @@ -11,7 +11,7 @@ import java.time.Instant /** * Custom serializer for java.time.Instant */ -object InstantSerializer : KSerializer { +internal object InstantSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) From 6e0dda3d9569f3571257d6203fcb61c1d6995e50 Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Tue, 10 Feb 2026 20:43:29 +0100 Subject: [PATCH 03/10] feat: Refactor flush mechanism to use coroutines and add metadataOrErrorToMap utility function --- .../kotlin/dev/logtide/sdk/LogTideClient.kt | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index c2af63a..b9cfb2c 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -23,8 +23,6 @@ import okhttp3.sse.EventSources import org.slf4j.LoggerFactory import java.io.IOException import java.util.* -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.math.pow @@ -67,10 +65,7 @@ class LogTideClient(private val options: LogTideClientOptions) { // Trace ID context (uses shared ThreadLocal from TraceIdContext for coroutine compatibility) internal val traceIdContext: ThreadLocal get() = threadLocalTraceId - // Periodic flush timer - private val flushExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor { r -> - thread(start = false, name = "LogTide-Flush-Timer", isDaemon = true) { r.run() } - } + private lateinit var flushJob: Job // Coroutine scope for async operations private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -81,12 +76,14 @@ class LogTideClient(private val options: LogTideClientOptions) { init { // Setup periodic flush - flushExecutor.scheduleAtFixedRate( - { runBlocking { flush() } }, - options.flushInterval.inWholeMilliseconds, - options.flushInterval.inWholeMilliseconds, - TimeUnit.MILLISECONDS - ) + suspend { + flushJob = scope.launch { + while (isActive) { + delay(options.flushInterval) + flush() + } + } + } // Register shutdown hook for graceful cleanup Runtime.getRuntime().addShutdownHook(thread(start = false) { @@ -227,6 +224,15 @@ class LogTideClient(private val options: LogTideClientOptions) { } } + fun metadataOrErrorToMap(metadataOrError: Any?): Map? { + return when (metadataOrError) { + is Throwable -> mapOf("error" to serializeError(metadataOrError)) + is Map<*, *> -> @Suppress("UNCHECKED_CAST") (metadataOrError as Map) + null -> null + else -> mapOf("data" to metadataOrError) + } + } + /** * Log debug message */ @@ -253,12 +259,7 @@ class LogTideClient(private val options: LogTideClientOptions) { * Can accept either metadata map or Throwable */ fun error(service: String, message: String, metadataOrError: Any? = null) { - val metadata = when (metadataOrError) { - is Throwable -> mapOf("error" to serializeError(metadataOrError)) - is Map<*, *> -> @Suppress("UNCHECKED_CAST") (metadataOrError as Map) - null -> null - else -> mapOf("data" to metadataOrError) - } + val metadata = metadataOrErrorToMap(metadataOrError) log(LogEntry(service, LogLevel.ERROR, message, metadata = metadata)) } @@ -267,12 +268,7 @@ class LogTideClient(private val options: LogTideClientOptions) { * Can accept either metadata map or Throwable */ fun critical(service: String, message: String, metadataOrError: Any? = null) { - val metadata = when (metadataOrError) { - is Throwable -> mapOf("error" to serializeError(metadataOrError)) - is Map<*, *> -> @Suppress("UNCHECKED_CAST") (metadataOrError as Map) - null -> null - else -> mapOf("data" to metadataOrError) - } + val metadata = metadataOrErrorToMap(metadataOrError) log(LogEntry(service, LogLevel.CRITICAL, message, metadata = metadata)) } @@ -537,11 +533,10 @@ class LogTideClient(private val options: LogTideClientOptions) { } flush() - flushExecutor.shutdown() - withContext(Dispatchers.IO) { - flushExecutor.awaitTermination(5, TimeUnit.SECONDS) + runCatching { + scope.cancel() } - scope.cancel() + httpClient.dispatcher.executorService.shutdown() httpClient.connectionPool.evictAll() } @@ -564,11 +559,32 @@ class LogTideClient(private val options: LogTideClientOptions) { } } + /** + * @see StructuredException Interface + */ private fun serializeError(error: Throwable): Map { + fun serializeStacktrace(stacktrace: String): List> { + return stacktrace.lines().map { line -> + val regex = """^\s*at\s+(\S+)\s+\(([^:]+):(\d+)\)$""".toRegex() + val match = regex.find(line) + if (match != null) { + mapOf( + "function" to match.groupValues[1], + "file" to match.groupValues[2], + "line" to match.groupValues[3].toIntOrNull() + ) + } else { + mapOf("raw" to line.trim()) + } + } + } + return mapOf( - "name" to error::class.simpleName, + "type" to error::class.simpleName, "message" to error.message, - "stack" to error.stackTraceToString() + "stacktrace" to error.stackTraceToString(), + "language" to "kotlin", + "cause" to error.cause?.let { serializeError(it) }, ) } From 6401acf1810585cca5c9b98abb221098d831799f Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 11 Feb 2026 20:36:36 +0100 Subject: [PATCH 04/10] fix: Remove unused flushJob variable and simplify flush mechanism initialization --- .../src/main/kotlin/dev/logtide/sdk/LogTideClient.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index b9cfb2c..f65277f 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -65,8 +65,6 @@ class LogTideClient(private val options: LogTideClientOptions) { // Trace ID context (uses shared ThreadLocal from TraceIdContext for coroutine compatibility) internal val traceIdContext: ThreadLocal get() = threadLocalTraceId - private lateinit var flushJob: Job - // Coroutine scope for async operations private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -76,12 +74,10 @@ class LogTideClient(private val options: LogTideClientOptions) { init { // Setup periodic flush - suspend { - flushJob = scope.launch { - while (isActive) { - delay(options.flushInterval) - flush() - } + scope.launch { + while (isActive) { + delay(options.flushInterval) + flush() } } From 66bc4eb42503bab03a182ebef2a49b6ac592540d Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 11 Feb 2026 20:57:05 +0100 Subject: [PATCH 05/10] fix: Trace ID handling in coroutine and children scopes --- .../kotlin/dev/logtide/sdk/LogTideClient.kt | 9 ++++-- .../kotlin/dev/logtide/sdk/TraceIdContext.kt | 4 +-- .../dev/logtide/sdk/LogTideClientTest.kt | 28 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index f65277f..7d223e9 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -25,6 +25,7 @@ import java.io.IOException import java.util.* import java.util.concurrent.TimeUnit import kotlin.concurrent.thread +import kotlin.coroutines.coroutineContext import kotlin.math.pow /** @@ -151,17 +152,19 @@ class LogTideClient(private val options: LogTideClientOptions) { * } * ``` */ - suspend fun withTraceIdSuspend(traceId: String, block: suspend () -> T): T { + suspend fun withTraceIdSuspend(traceId: String, block: suspend CoroutineScope.() -> T): T { val normalizedTraceId = normalizeTraceId(traceId) ?: UUID.randomUUID().toString() return withContext(TraceIdElement(normalizedTraceId)) { - block() + coroutineScope { + block() + } } } /** * Execute suspend function with a new auto-generated trace ID (coroutine-safe) */ - suspend fun withNewTraceIdSuspend(block: suspend () -> T): T { + suspend fun withNewTraceIdSuspend(block: suspend CoroutineScope.() -> T): T { return withTraceIdSuspend(UUID.randomUUID().toString(), block) } diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt index a3ca497..197d91c 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt @@ -35,7 +35,7 @@ internal val threadLocalTraceId = ThreadLocal() * ``` */ @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) -class TraceIdElement( +data class TraceIdElement( val traceId: String ) : CopyableThreadContextElement { @@ -66,7 +66,7 @@ class TraceIdElement( * Returns a copy of this element for the child coroutine. */ override fun copyForChild(): CopyableThreadContextElement { - return TraceIdElement(traceId) + return copy() } /** diff --git a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt index f8d1405..3248775 100644 --- a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt +++ b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt @@ -5,12 +5,14 @@ import dev.logtide.sdk.exceptions.BufferFullException import dev.logtide.sdk.models.LogEntry import dev.logtide.sdk.models.LogTideClientOptions import kotlinx.coroutines.* -import org.junit.jupiter.api.Test import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import kotlin.test.* import kotlin.time.Duration.Companion.seconds +private val FLUSH_INTERVAL = 10.seconds + /** * Unit tests for LogTideClient */ @@ -25,7 +27,7 @@ class LogTideClientTest { apiUrl = "http://localhost:8080", apiKey = "test_key", batchSize = 10, - flushInterval = 10.seconds, + flushInterval = FLUSH_INTERVAL, maxBufferSize = 100, enableMetrics = true, debug = false @@ -42,7 +44,7 @@ class LogTideClientTest { try { client.close() } catch (e: Exception) { - // Ignore errors during cleanup + // Ignore other errors during cleanup } } } @@ -52,7 +54,7 @@ class LogTideClientTest { client.info("test-service", "Test message") val metrics = client.getMetrics() // Buffer should have 1 log (not sent yet) - assertTrue(metrics.logsSent == 0L) + assertEquals(metrics.logsSent, 0L) } @Test @@ -140,7 +142,7 @@ class LogTideClientTest { } @Test - fun `should propagate trace ID to child coroutines`() = runBlocking { + fun `should propagate trace ID to child coroutines`(): Unit = runBlocking { val validTraceId = "550e8400-e29b-41d4-a716-446655440004" client.withTraceIdSuspend(validTraceId) { @@ -230,7 +232,7 @@ class LogTideClientTest { @Test fun `should track metrics`() { val metrics = client.getMetrics() - + assertEquals(0L, metrics.logsSent) assertEquals(0L, metrics.logsDropped) assertEquals(0L, metrics.errors) @@ -242,9 +244,9 @@ class LogTideClientTest { @Test fun `should reset metrics`() { client.info("test", "Message") - + client.resetMetrics() - + val metrics = client.getMetrics() assertEquals(0L, metrics.logsSent) assertEquals(0L, metrics.logsDropped) @@ -253,10 +255,10 @@ class LogTideClientTest { @Test fun `should handle error serialization`() { val exception = RuntimeException("Test error") - + // Should not throw client.error("test-service", "Error occurred", exception) - + // Check metrics val metrics = client.getMetrics() assertTrue(metrics.errors == 0L) // No send errors yet @@ -269,7 +271,7 @@ class LogTideClientTest { client.warn("test", "Warn message") client.error("test", "Error message") client.critical("test", "Critical message") - + // All should be buffered val metrics = client.getMetrics() assertEquals(0L, metrics.logsSent) // Not flushed yet @@ -283,9 +285,9 @@ class LogTideClientTest { message = "Custom message", metadata = mapOf("custom" to "metadata") ) - + client.log(entry) - + // Should be buffered val metrics = client.getMetrics() assertEquals(0L, metrics.logsSent) From 2e0eb1f640f29c3d20a9143c07e4e8c07265549d Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 11 Feb 2026 21:01:23 +0100 Subject: [PATCH 06/10] refactor: Clean up exception handling in tests and update coroutine API usage --- .../src/main/kotlin/dev/logtide/sdk/LogTideClient.kt | 1 - .../src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt | 2 +- .../src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index 7d223e9..c6fa90e 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -25,7 +25,6 @@ import java.io.IOException import java.util.* import java.util.concurrent.TimeUnit import kotlin.concurrent.thread -import kotlin.coroutines.coroutineContext import kotlin.math.pow /** diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt index 197d91c..2c20ee3 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/TraceIdContext.kt @@ -1,4 +1,4 @@ -@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class, kotlinx.coroutines.DelicateCoroutinesApi::class) +@file:OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) package dev.logtide.sdk diff --git a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt index 3248775..513a21a 100644 --- a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt +++ b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt @@ -43,7 +43,7 @@ class LogTideClientTest { runBlocking { try { client.close() - } catch (e: Exception) { + } catch (_: Exception) { // Ignore other errors during cleanup } } @@ -73,7 +73,7 @@ class LogTideClientTest { runBlocking { try { clientWithMetadata.close() - } catch (e: Exception) { + } catch (_: Exception) { // Expected - no real server } } @@ -223,7 +223,7 @@ class LogTideClientTest { runBlocking { try { smallBufferClient.close() - } catch (e: Exception) { + } catch (_: Exception) { // Expected } } From 658dd9aef96891ebce4e56de55e70793e13b15b0 Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 11 Feb 2026 22:17:37 +0100 Subject: [PATCH 07/10] feat: Enhance trace ID handling with validation allowing IDs that aren't UUIDs --- .../main/kotlin/dev/logtide/sdk/LogTideClient.kt | 14 +++++++------- .../kotlin/dev/logtide/sdk/LogTideClientTest.kt | 6 +----- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index c6fa90e..1fd2412 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -34,6 +34,10 @@ import kotlin.math.pow * retry logic, circuit breaker, and query capabilities. */ class LogTideClient(private val options: LogTideClientOptions) { + companion object { + private const val MAX_TRACEID_LENGTH = 250 + } + private val logger = LoggerFactory.getLogger(this::class.java) private val httpClient: OkHttpClient = OkHttpClient.Builder() @@ -152,7 +156,8 @@ class LogTideClient(private val options: LogTideClientOptions) { * ``` */ suspend fun withTraceIdSuspend(traceId: String, block: suspend CoroutineScope.() -> T): T { - val normalizedTraceId = normalizeTraceId(traceId) ?: UUID.randomUUID().toString() + val normalizedTraceId = + normalizeTraceId(traceId) ?: throw IllegalArgumentException("Invalid trace ID: $traceId") return withContext(TraceIdElement(normalizedTraceId)) { coroutineScope { block() @@ -544,12 +549,7 @@ class LogTideClient(private val options: LogTideClientOptions) { internal fun normalizeTraceId(traceId: String?): String? { if (traceId == null) return null - val uuidRegex = - "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$".toRegex(RegexOption.IGNORE_CASE) - - return if (uuidRegex.matches(traceId)) { - traceId - } else { + return traceId.ifBlank { if (options.debug) { logger.error("Invalid trace ID '$traceId', generating new UUID") } diff --git a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt index 513a21a..8d9f4d0 100644 --- a/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt +++ b/logtide-core/src/test/kotlin/dev/logtide/sdk/LogTideClientTest.kt @@ -109,7 +109,6 @@ class LogTideClientTest { client.withNewTraceId { val traceId = client.getTraceId() assertNotNull(traceId) - assertTrue(traceId.matches(Regex("[0-9a-f-]{36}"))) } } @@ -165,11 +164,10 @@ class LogTideClientTest { } @Test - fun `should generate new suspend trace ID`() = runBlocking { + fun `should generate new suspend trace ID`(): Unit = runBlocking { client.withNewTraceIdSuspend { val traceId = client.getTraceIdSuspend() assertNotNull(traceId) - assertTrue(traceId.matches(Regex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))) } } @@ -196,8 +194,6 @@ class LogTideClientTest { client.setTraceId("invalid-trace-id") val traceId = client.getTraceId() assertNotNull(traceId) - // Should be a valid UUID - assertTrue(traceId.matches(Regex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"))) } @Test From aeb253b5896c91344c0daf139b4bd97a374f5581 Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 18 Mar 2026 19:12:16 +0100 Subject: [PATCH 08/10] fix: Apply Kotlin JVM plugin in build.gradle.kts --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 52820aa..972ac47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ val projectGroup: String by project val projectVersion: String by project plugins { - alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.maven.publish) apply false } From afcd397340c62d160b17cb09bd11634bc6aaccdd Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 18 Mar 2026 20:30:50 +0100 Subject: [PATCH 09/10] feat: Update LogTide plugin configuration to use client options and increment project version to 0.8.3 --- README.md | 15 ++++--- examples/middleware/ktor/KtorExample.kt | 26 +++++++------ gradle.properties | 2 +- .../kotlin/dev/logtide/sdk/LogTideClient.kt | 37 ++++++++++++------ .../sdk/models/LogTideClientOptions.kt | 26 ++++++------- logtide-ktor/src/main/kotlin/LogTidePlugin.kt | 39 +++++++++---------- 6 files changed, 81 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index d2e1cbb..90b48bc 100644 --- a/README.md +++ b/README.md @@ -346,9 +346,10 @@ import dev.logtide.sdk.middleware.LogTidePlugin import io.ktor.server.application.* fun Application.module() { + val apiUrl = "http://localhost:8080" + val apiKey = "lp_your_api_key_here" + install(LogTidePlugin) { - apiUrl = "http://localhost:8080" - apiKey = "lp_your_api_key_here" serviceName = "ktor-app" // Optional configuration @@ -357,10 +358,12 @@ fun Application.module() { skipPaths = setOf("/metrics", "/internal") // Client options - batchSize = 100 - flushInterval = kotlin.time.Duration.parse("5s") - enableMetrics = true - globalMetadata = mapOf("env" to "production") + logtideClientOptions(apiUrl, apiKey) { + batchSize = 100 + flushInterval = kotlin.time.Duration.parse("5s") + enableMetrics = true + globalMetadata = mapOf("env" to "production") + } // Enable request/response logging logRequests = true // Log incoming requests, e.g., method, path, headers diff --git a/examples/middleware/ktor/KtorExample.kt b/examples/middleware/ktor/KtorExample.kt index 01816f9..ab88c01 100644 --- a/examples/middleware/ktor/KtorExample.kt +++ b/examples/middleware/ktor/KtorExample.kt @@ -19,9 +19,10 @@ import io.ktor.http.* fun main() { embeddedServer(Netty, port = 8080) { // Install LogTide plugin + val apiUrl = "http://localhost:8080" + val apiKey = "lp_your_api_key_here" + install(LogTidePlugin) { - apiUrl = "http://localhost:8080" - apiKey = "lp_your_api_key_here" serviceName = "ktor-app" // Optional configuration @@ -31,16 +32,17 @@ fun main() { skipHealthCheck = true skipPaths = setOf("/metrics", "/internal") - // LogTide client options - batchSize = 100 - flushInterval = kotlin.time.Duration.parse("5s") - maxBufferSize = 10000 - enableMetrics = true - debug = false - globalMetadata = mapOf( - "env" to "production", - "version" to "1.0.0" - ) + logtideClientOptions(apiUrl, apiKey) { + batchSize = 100 + flushInterval = kotlin.time.Duration.parse("5s") + maxBufferSize = 10000 + enableMetrics = true + debug = false + globalMetadata = mapOf( + "env" to "production", + "version" to "1.0.0" + ) + } } routing { diff --git a/gradle.properties b/gradle.properties index d585e4a..bc103cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,6 @@ org.gradle.configuration.cache=true org.gradle.caching=true # Project Properties projectGroup=io.github.logtide-dev -projectVersion=0.5.0 +projectVersion=0.8.3 # Kotlin kotlinJvmTarget=17 \ No newline at end of file diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index 1fd2412..cef6880 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -199,10 +199,13 @@ class LogTideClient(private val options: LogTideClientOptions) { // Apply trace ID context if (finalEntry.traceId == null) { val contextTraceId = traceIdContext.get() - if (contextTraceId != null) { - finalEntry = finalEntry.copy(traceId = contextTraceId) + finalEntry = if (contextTraceId != null) { + finalEntry.copy(traceId = contextTraceId) } else if (options.autoTraceId) { - finalEntry = finalEntry.copy(traceId = UUID.randomUUID().toString()) + finalEntry.copy(traceId = UUID.randomUUID().toString()) + } else { + logger.error("Missing trace ID for log, log will be dropped. Consider enabling autoTraceId in client options.") + return } } @@ -386,7 +389,11 @@ class LogTideClient(private val options: LogTideClientOptions) { httpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { - throw IOException("HTTP ${response.code}: ${response.message}") + if (options.debug) { + logger.error(response.body?.charStream()?.readText()) + logger.error(payload.toString()) + } + throw IOException("Failed to send logs: HTTP ${response.code} - ${response.message}") } } } @@ -577,13 +584,21 @@ class LogTideClient(private val options: LogTideClientOptions) { } } - return mapOf( - "type" to error::class.simpleName, - "message" to error.message, - "stacktrace" to error.stackTraceToString(), - "language" to "kotlin", - "cause" to error.cause?.let { serializeError(it) }, - ) + val visited = Collections.newSetFromMap(IdentityHashMap()) + + fun serializeRecursive(current: Throwable): Map { + visited.add(current) + + return mapOf( + "type" to current::class.simpleName, + "message" to current.message, + "stacktrace" to serializeStacktrace(current.stackTraceToString()), + "language" to "kotlin", + "cause" to current.cause?.takeIf { visited.add(it) }?.let { serializeRecursive(it) }, + ) + } + + return serializeRecursive(error) } private fun updateLatency(latency: Double) { diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt index 1e666b3..9416141 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt @@ -8,19 +8,19 @@ import kotlin.time.Duration.Companion.seconds * Configuration options for LogTide client */ data class LogTideClientOptions( - val apiUrl: String, - val apiKey: String, - val batchSize: Int = 100, - val flushInterval: Duration = 5.seconds, - val maxBufferSize: Int = 10000, - val maxRetries: Int = 3, - val retryDelay: Duration = 1.seconds, - val circuitBreakerThreshold: Int = 5, - val circuitBreakerReset: Duration = 30.seconds, - val enableMetrics: Boolean = true, - val debug: Boolean = false, - val globalMetadata: Map = emptyMap(), - val autoTraceId: Boolean = false + var apiUrl: String, + var apiKey: String, + var batchSize: Int = 100, + var flushInterval: Duration = 5.seconds, + var maxBufferSize: Int = 10000, + var maxRetries: Int = 3, + var retryDelay: Duration = 1.seconds, + var circuitBreakerThreshold: Int = 5, + var circuitBreakerReset: Duration = 30.seconds, + var enableMetrics: Boolean = true, + var debug: Boolean = false, + var globalMetadata: Map = emptyMap(), + var autoTraceId: Boolean = false ) { init { require(apiUrl.isNotBlank()) { "apiUrl cannot be blank" } diff --git a/logtide-ktor/src/main/kotlin/LogTidePlugin.kt b/logtide-ktor/src/main/kotlin/LogTidePlugin.kt index 5d5ca65..91a9e1a 100644 --- a/logtide-ktor/src/main/kotlin/LogTidePlugin.kt +++ b/logtide-ktor/src/main/kotlin/LogTidePlugin.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import java.util.* +import kotlin.apply /** * AttributeKey to access the LogTide client instance @@ -55,20 +56,22 @@ private val TraceIdAttributeKey = AttributeKey("LogTideTraceId") * ``` */ class LogTidePluginConfig { - var apiUrl: String = "" - var apiKey: String = "" var serviceName: String = "ktor-app" var logErrors: Boolean = true var skipHealthCheck: Boolean = true var skipPaths: Set = emptySet() + internal lateinit var clientOptions: LogTideClientOptions + // Forward all LogTideClientOptions - var batchSize: Int = 100 - var flushInterval: kotlin.time.Duration = kotlin.time.Duration.parse("5s") - var maxBufferSize: Int = 10000 - var enableMetrics: Boolean = true - var debug: Boolean = false - var globalMetadata: Map = emptyMap() + fun logtideClientOptions(apiUrl: String, apiKey: String, block: LogTideClientOptions.() -> Unit) { + if (!::clientOptions.isInitialized) { + clientOptions = LogTideClientOptions(apiUrl, apiKey) + } + clientOptions = clientOptions.apply { + block() + } + } /** * Whether to log incoming requests' metadata @@ -135,16 +138,10 @@ class LogTidePluginConfig { // Whether to use the default interceptor to propagate trace IDs in call context var useDefaultInterceptor: Boolean = true - internal fun toClientOptions() = LogTideClientOptions( - apiUrl = apiUrl, - apiKey = apiKey, - batchSize = batchSize, - flushInterval = flushInterval, - maxBufferSize = maxBufferSize, - enableMetrics = enableMetrics, - debug = debug, - globalMetadata = globalMetadata - ) + internal fun toClientOptions(): LogTideClientOptions { + if (!::clientOptions.isInitialized) throw IllegalStateException("Client options have not been provided. Call logtideClientOptions in the plugin configuration block to set them up.") + return this.clientOptions.copy() + } } val LogTidePlugin = createApplicationPlugin( @@ -156,9 +153,9 @@ val LogTidePlugin = createApplicationPlugin( // Log plugin installation application.log.info("LogTide Plugin Initialized") application.log.info(" Service Name: ${config.serviceName}") - application.log.info(" API URL: ${config.apiUrl}") - application.log.info(" Batch Size: ${config.batchSize}") - application.log.info(" Flush Interval: ${config.flushInterval}") + application.log.info(" API URL: ${config.clientOptions.apiUrl}") + application.log.info(" Batch Size: ${config.clientOptions.batchSize}") + application.log.info(" Flush Interval: ${config.clientOptions.flushInterval}") application.log.info(" Log Requests: ${config.logRequests}") application.log.info(" Log Responses: ${config.logResponses}") application.log.info(" Log Errors: ${config.logErrors}") From 67f499d816722c2dd896b5f79f0e1aab63c831f4 Mon Sep 17 00:00:00 2001 From: EmanueleIannuzzi Date: Wed, 18 Mar 2026 21:04:01 +0100 Subject: [PATCH 10/10] refactor: Remove autoTraceId option from LogTideClientOptions and related code --- README.md | 4 - examples/advanced/Advanced.kt | 1 - .../kotlin/dev/logtide/sdk/LogTideClient.kt | 9 +- .../sdk/models/LogTideClientOptions.kt | 8 +- .../sdk/models/LogTideClientOptionsTest.kt | 6 +- logtide-ktor/src/main/kotlin/LogTidePlugin.kt | 7 +- .../src/test/kotlin/LogTidePluginTest.kt | 116 +++++++++++------- 7 files changed, 86 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 90b48bc..0aa61ed 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ runBlocking { | `enableMetrics` | `Boolean` | `true` | Track internal metrics | | `debug` | `Boolean` | `false` | Enable debug logging to console | | `globalMetadata` | `Map` | `emptyMap()` | Metadata added to all logs | -| `autoTraceId` | `Boolean` | `false` | Auto-generate trace IDs for logs | ### Example: Full Configuration @@ -152,9 +151,6 @@ val client = LogTideClient( "version" to "1.0.0", "hostname" to System.getenv("HOSTNAME") ), - - // Auto trace IDs - autoTraceId = false ) ) ``` diff --git a/examples/advanced/Advanced.kt b/examples/advanced/Advanced.kt index d8e9e57..1f2ab1f 100644 --- a/examples/advanced/Advanced.kt +++ b/examples/advanced/Advanced.kt @@ -33,7 +33,6 @@ fun main() = runBlocking { "version" to "1.0.0", "region" to "eu-west-1" ), - autoTraceId = false ) ) diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt index cef6880..404e5f5 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/LogTideClient.kt @@ -199,13 +199,14 @@ class LogTideClient(private val options: LogTideClientOptions) { // Apply trace ID context if (finalEntry.traceId == null) { val contextTraceId = traceIdContext.get() + val metadataTraceId = entry.metadata?.get("traceId")?.toString() finalEntry = if (contextTraceId != null) { finalEntry.copy(traceId = contextTraceId) - } else if (options.autoTraceId) { - finalEntry.copy(traceId = UUID.randomUUID().toString()) + } else if (metadataTraceId != null) { + finalEntry.copy(traceId = metadataTraceId) } else { - logger.error("Missing trace ID for log, log will be dropped. Consider enabling autoTraceId in client options.") - return + logger.warn("No trace ID provided for log. Generating one automatically. Consider providing a meaningful one to help debugging.") + finalEntry.copy(traceId = UUID.randomUUID().toString()) } } diff --git a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt index 9416141..4311f3d 100644 --- a/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt +++ b/logtide-core/src/main/kotlin/dev/logtide/sdk/models/LogTideClientOptions.kt @@ -1,7 +1,6 @@ package dev.logtide.sdk.models import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds /** @@ -19,8 +18,7 @@ data class LogTideClientOptions( var circuitBreakerReset: Duration = 30.seconds, var enableMetrics: Boolean = true, var debug: Boolean = false, - var globalMetadata: Map = emptyMap(), - var autoTraceId: Boolean = false + var globalMetadata: Map = emptyMap() ) { init { require(apiUrl.isNotBlank()) { "apiUrl cannot be blank" } @@ -57,7 +55,6 @@ class LogTideClientOptionsBuilder { var enableMetrics: Boolean = true var debug: Boolean = false var globalMetadata: Map = emptyMap() - var autoTraceId: Boolean = false fun build(): LogTideClientOptions = LogTideClientOptions( apiUrl = apiUrl, @@ -71,8 +68,7 @@ class LogTideClientOptionsBuilder { circuitBreakerReset = circuitBreakerReset, enableMetrics = enableMetrics, debug = debug, - globalMetadata = globalMetadata, - autoTraceId = autoTraceId + globalMetadata = globalMetadata ) } diff --git a/logtide-core/src/test/kotlin/dev/logtide/sdk/models/LogTideClientOptionsTest.kt b/logtide-core/src/test/kotlin/dev/logtide/sdk/models/LogTideClientOptionsTest.kt index be2461d..cfe8681 100644 --- a/logtide-core/src/test/kotlin/dev/logtide/sdk/models/LogTideClientOptionsTest.kt +++ b/logtide-core/src/test/kotlin/dev/logtide/sdk/models/LogTideClientOptionsTest.kt @@ -29,7 +29,6 @@ class LogTideClientOptionsTest { assertTrue(options.enableMetrics) assertFalse(options.debug) assertTrue(options.globalMetadata.isEmpty()) - assertFalse(options.autoTraceId) } @Test @@ -47,8 +46,7 @@ class LogTideClientOptionsTest { circuitBreakerReset = 60.seconds, enableMetrics = false, debug = true, - globalMetadata = metadata, - autoTraceId = true + globalMetadata = metadata ) assertEquals("https://api.logtide.dev", options.apiUrl) @@ -63,7 +61,6 @@ class LogTideClientOptionsTest { assertFalse(options.enableMetrics) assertTrue(options.debug) assertEquals(metadata, options.globalMetadata) - assertTrue(options.autoTraceId) } @Test @@ -255,7 +252,6 @@ class LogTideClientOptionsTest { assertTrue(builder.enableMetrics) assertFalse(builder.debug) assertTrue(builder.globalMetadata.isEmpty()) - assertFalse(builder.autoTraceId) } // ==================== Data Class Tests ==================== diff --git a/logtide-ktor/src/main/kotlin/LogTidePlugin.kt b/logtide-ktor/src/main/kotlin/LogTidePlugin.kt index 91a9e1a..8002e49 100644 --- a/logtide-ktor/src/main/kotlin/LogTidePlugin.kt +++ b/logtide-ktor/src/main/kotlin/LogTidePlugin.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import java.util.* -import kotlin.apply /** * AttributeKey to access the LogTide client instance @@ -64,7 +63,11 @@ class LogTidePluginConfig { internal lateinit var clientOptions: LogTideClientOptions // Forward all LogTideClientOptions - fun logtideClientOptions(apiUrl: String, apiKey: String, block: LogTideClientOptions.() -> Unit) { + fun logtideClientOptions( + apiUrl: String, + apiKey: String, + block: LogTideClientOptions.() -> Unit = { this.apiUrl = apiUrl; this.apiKey = apiKey } + ) { if (!::clientOptions.isInitialized) { clientOptions = LogTideClientOptions(apiUrl, apiKey) } diff --git a/logtide-ktor/src/test/kotlin/LogTidePluginTest.kt b/logtide-ktor/src/test/kotlin/LogTidePluginTest.kt index feff3e6..eea08cb 100644 --- a/logtide-ktor/src/test/kotlin/LogTidePluginTest.kt +++ b/logtide-ktor/src/test/kotlin/LogTidePluginTest.kt @@ -27,9 +27,13 @@ class LogTidePluginTest { private lateinit var mockServer: MockWebServer internal fun Application.installLogtideDefault(mockServer: MockWebServer) { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" + logtideClientOptions(apiUrl, apiKey) { + batchSize = 1 + flushInterval = 1.seconds + } serviceName = "test-service" logRequests = false logResponses = false @@ -92,14 +96,17 @@ class LogTidePluginTest { mockServer.enqueue(MockResponse().setResponseCode(200)) mockServer.enqueue(MockResponse().setResponseCode(200)) + val apiUrl = mockServer.url("/api/v1/ingest").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/api/v1/ingest").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = true logResponses = false - batchSize = 1 // Flush immediately + logtideClientOptions(apiUrl, apiKey) { + batchSize = 1 // Flush immediately} + } } routing { @@ -187,14 +194,16 @@ class LogTidePluginTest { var contextTraceId: String? = null val headerTraceId = "550e8400-e29b-41d4-a716-446655440001" + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = false logResponses = false useDefaultInterceptor = true + logtideClientOptions(apiUrl, apiKey) } routing { @@ -218,16 +227,18 @@ class LogTidePluginTest { fun `plugin should use custom trace ID extractor`() = testApplication { var extractedTraceId: String? = null + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = false logResponses = false extractTraceIdFromCall = { call -> call.request.headers["Custom-Trace-Header"] } + logtideClientOptions(apiUrl, apiKey) } routing { @@ -252,14 +263,16 @@ class LogTidePluginTest { @Test fun `plugin should skip health check path by default`() = testApplication { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = true logResponses = true skipHealthCheck = true + logtideClientOptions(apiUrl, apiKey) } routing { @@ -277,14 +290,16 @@ class LogTidePluginTest { @Test fun `plugin should skip custom paths`() = testApplication { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = true logResponses = true skipPaths = setOf("/metrics", "/status") + logtideClientOptions(apiUrl, apiKey) } routing { @@ -308,15 +323,18 @@ class LogTidePluginTest { mockServer.enqueue(MockResponse().setResponseCode(200)) mockServer.enqueue(MockResponse().setResponseCode(200)) + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/api/v1/ingest").toString() - apiKey = "test_key" serviceName = "test-service" skipHealthCheck = false logRequests = true logResponses = true - batchSize = 1 + logtideClientOptions(apiUrl, apiKey) { + batchSize = 1 + } } routing { @@ -339,10 +357,11 @@ class LogTidePluginTest { @Test fun `plugin should use custom request metadata extractor`() = testApplication { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = true logResponses = false @@ -352,6 +371,7 @@ class LogTidePluginTest { "traceId" to traceId ) } + logtideClientOptions(apiUrl, apiKey) } } @@ -361,10 +381,11 @@ class LogTidePluginTest { @Test fun `plugin should use custom response metadata extractor`() = testApplication { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = false logResponses = true @@ -376,6 +397,7 @@ class LogTidePluginTest { "processingTime" to (duration ?: 0L) ) } + logtideClientOptions(apiUrl, apiKey) } } @@ -389,14 +411,16 @@ class LogTidePluginTest { var contextTraceId: String? = "not-set" val headerTraceId = "550e8400-e29b-41d4-a716-446655440002" + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = false logResponses = false useDefaultInterceptor = false + logtideClientOptions(apiUrl, apiKey) } routing { @@ -426,14 +450,17 @@ class LogTidePluginTest { mockServer.enqueue(MockResponse().setResponseCode(200)) mockServer.enqueue(MockResponse().setResponseCode(200)) + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + application { install(LogTidePlugin) { - apiUrl = mockServer.url("/api/v1/ingest").toString() - apiKey = "test_key" serviceName = "test-service" logRequests = false logResponses = true - batchSize = 1 + logtideClientOptions(apiUrl, apiKey) { + batchSize = 1 + } } routing { @@ -479,20 +506,23 @@ class LogTidePluginTest { @Test fun `plugin config should convert to client options`() { + val apiUrl = mockServer.url("/").toString() + val apiKey = "test_key" + val config = LogTidePluginConfig().apply { - apiUrl = "http://localhost:8080" - apiKey = "test_key" - batchSize = 50 - flushInterval = 10.seconds - maxBufferSize = 5000 - enableMetrics = false - debug = true - globalMetadata = mapOf("env" to "test") + logtideClientOptions(apiUrl, apiKey) { + batchSize = 50 + flushInterval = 10.seconds + maxBufferSize = 5000 + enableMetrics = false + debug = true + globalMetadata = mapOf("env" to "test") + } } val options = config.toClientOptions() - assertEquals("http://localhost:8080", options.apiUrl) + assertEquals(apiUrl, options.apiUrl) assertEquals("test_key", options.apiKey) assertEquals(50, options.batchSize) assertEquals(10.seconds, options.flushInterval) @@ -504,20 +534,20 @@ class LogTidePluginTest { @Test fun `plugin config should have correct defaults`() { - val config = LogTidePluginConfig() + val config = LogTidePluginConfig().apply { + logtideClientOptions("no-url", "no-key") + } - assertEquals("", config.apiUrl) - assertEquals("", config.apiKey) assertEquals("ktor-app", config.serviceName) assertTrue(config.logErrors) assertTrue(config.skipHealthCheck) assertTrue(config.skipPaths.isEmpty()) - assertEquals(100, config.batchSize) - assertEquals(5.seconds, config.flushInterval) - assertEquals(10000, config.maxBufferSize) - assertTrue(config.enableMetrics) - assertFalse(config.debug) - assertTrue(config.globalMetadata.isEmpty()) + assertEquals(100, config.clientOptions.batchSize) + assertEquals(5.seconds, config.clientOptions.flushInterval) + assertEquals(10000, config.clientOptions.maxBufferSize) + assertTrue(config.clientOptions.enableMetrics) + assertFalse(config.clientOptions.debug) + assertTrue(config.clientOptions.globalMetadata.isEmpty()) assertTrue(config.logRequests) assertTrue(config.logResponses) assertTrue(config.useDefaultInterceptor)