Skip to content

Commit 21f6622

Browse files
committed
Refactor and Json log formatter tests
1 parent a827fd7 commit 21f6622

File tree

12 files changed

+195
-76
lines changed

12 files changed

+195
-76
lines changed

gradle/libs.versions.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ serialization = "1.6.3"
77
logback = "1.5.6"
88
io = "0.5.1"
99
date-time = "0.6.0"
10+
ksp = "2.0.10-1.0.24"
11+
mockative = "2.2.2"
12+
allopen = "2.0.20"
13+
mokkery = "2.3.0"
1014

1115
[libraries]
1216
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -25,9 +29,14 @@ ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
2529
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
2630
ktor-content-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
2731
ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
32+
mockative = { module = "io.mockative:mockative", version.ref = "mockative" }
33+
mockative-processor = { module = "io.mockative:mockative-processor", version.ref = "mockative" }
2834

2935
[plugins]
3036
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
3137
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
3238
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
33-
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
39+
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
40+
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
41+
allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "allopen" }
42+
mokkery = { id = "dev.mokkery", version.ref = "mokkery" }

lambda-events/build.gradle.kts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ plugins {
55

66
kotlin {
77
val isArm64 = System.getProperty("os.arch") == "aarch64"
8-
val nativeTarget = if (isArm64) linuxArm64() else linuxX64()
8+
val hostOs = System.getProperty("os.name")
9+
val isMingwX64 = hostOs.startsWith("Windows")
10+
val nativeTarget = when {
11+
hostOs == "Mac OS X" -> if(isArm64) macosArm64() else macosX64()
12+
hostOs == "Linux" -> if (isArm64) linuxArm64() else linuxX64()
13+
isMingwX64 -> mingwX64("native")
14+
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
15+
}
916

1017
sourceSets {
1118
commonMain.dependencies {

lambda-runtime/build.gradle.kts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,43 @@
11
plugins {
22
alias(libs.plugins.kotlin.multiplatform)
33
alias(libs.plugins.kotlin.serialization)
4+
alias(libs.plugins.mokkery)
5+
alias(libs.plugins.allopen)
46
}
57

68
kotlin {
79
val isArm64 = System.getProperty("os.arch") == "aarch64"
8-
val nativeTarget = if (isArm64) linuxArm64() else linuxX64()
10+
val hostOs = System.getProperty("os.name")
11+
val isMingwX64 = hostOs.startsWith("Windows")
12+
val nativeTarget = when {
13+
hostOs == "Mac OS X" -> if (isArm64) macosArm64() else macosX64()
14+
hostOs == "Linux" -> if (isArm64) linuxArm64() else linuxX64()
15+
isMingwX64 -> mingwX64("native")
16+
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
17+
}
918

1019
sourceSets {
1120
commonMain.dependencies {
1221
implementation(libs.ktor.client.core)
1322
implementation(libs.kotlin.serialization.json)
1423
implementation(libs.kotlin.io.core)
1524
implementation(libs.kotlin.date.time)
16-
//implementation(libs.ktor.client.cio)
1725
implementation(libs.ktor.client.curl)
1826
implementation(libs.ktor.client.logging)
1927
implementation(libs.ktor.content.negotiation)
2028
implementation(libs.ktor.content.json)
2129
}
2230

23-
nativeMain.dependencies {}
24-
25-
commonTest.dependencies {
31+
nativeTest.dependencies {
2632
implementation(libs.kotlin.test)
2733
}
2834
}
2935
}
36+
37+
fun isTestingTask(name: String) = name.endsWith("Test")
38+
val isTesting = gradle.startParameter.taskNames.any(::isTestingTask)
39+
40+
if (isTesting) allOpen {
41+
annotation("kotlin.Metadata")
42+
}
43+

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,15 @@ package io.github.trueangle.knative.lambda.runtime
33
import io.github.trueangle.knative.lambda.runtime.LambdaEnvironmentException.NonRecoverableStateException
44
import io.github.trueangle.knative.lambda.runtime.api.Context
55
import io.github.trueangle.knative.lambda.runtime.api.LambdaClient
6-
import io.github.trueangle.knative.lambda.runtime.api.dto.LogMessageDto
76
import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler
87
import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler
98
import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler
109
import io.github.trueangle.knative.lambda.runtime.log.KtorLogger
1110
import io.github.trueangle.knative.lambda.runtime.log.Log
12-
import io.github.trueangle.knative.lambda.runtime.log.LogLevel
1311
import io.ktor.client.HttpClient
1412
import io.ktor.client.engine.curl.Curl
1513
import io.ktor.client.plugins.HttpTimeout
1614
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
17-
import io.ktor.client.plugins.logging.Logger
1815
import io.ktor.client.plugins.logging.Logging
1916
import io.ktor.http.content.OutgoingContent.WriteChannelContent
2017
import io.ktor.serialization.kotlinx.json.json
@@ -23,20 +20,17 @@ import io.ktor.util.reflect.typeInfo
2320
import io.ktor.utils.io.ByteWriteChannel
2421
import io.ktor.utils.io.writeStringUtf8
2522
import kotlinx.coroutines.runBlocking
23+
import kotlinx.serialization.ExperimentalSerializationApi
2624
import kotlinx.serialization.json.Json
27-
import kotlinx.serialization.modules.SerializersModule
2825
import kotlin.system.exitProcess
29-
import kotlin.time.TimeSource
30-
import io.ktor.client.plugins.logging.LogLevel as KtorLogLevel
3126

3227
object LambdaRuntime {
28+
@OptIn(ExperimentalSerializationApi::class)
29+
internal val json = Json { explicitNulls = false }
30+
3331
private val httpClient = HttpClient(Curl) {
3432
install(HttpTimeout)
35-
install(ContentNegotiation) {
36-
json(Json {
37-
explicitNulls = false
38-
})
39-
}
33+
install(ContentNegotiation) { json(json) }
4034
install(Logging) {
4135
val kLogger = KtorLogger()
4236
level = kLogger.getLevel()

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ internal data class LogMessageDto<T>(
1313
@SerialName("timestamp")
1414
val timestamp: String,
1515
@SerialName("message")
16+
@Contextual
1617
val message: T?,
1718
@SerialName("level")
1819
val level: LogLevel,
1920
@SerialName("AWSRequestId")
20-
val awsRequestId: String? = null
21+
val awsRequestId: String?
2122
)

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,36 @@ package io.github.trueangle.knative.lambda.runtime.log
22

33
import io.github.trueangle.knative.lambda.runtime.api.Context
44
import io.github.trueangle.knative.lambda.runtime.api.dto.LogMessageDto
5+
import io.ktor.util.reflect.TypeInfo
56
import kotlinx.datetime.Clock
6-
import kotlinx.serialization.InternalSerializationApi
77
import kotlinx.serialization.SerializationException
88
import kotlinx.serialization.encodeToString
99
import kotlinx.serialization.json.Json
1010
import kotlinx.serialization.serializer
1111

12-
@OptIn(InternalSerializationApi::class)
13-
internal class JsonLogFormatter : LogFormatter {
12+
internal class JsonLogFormatter(
13+
private val json: Json,
14+
private val clock: Clock = Clock.System,
15+
) : LogFormatter {
1416
private var requestContext: Context? = null
1517

16-
override fun format(logLevel: LogLevel, message: Any?): String {
18+
override fun <T> format(logLevel: LogLevel, message: T?, messageType: TypeInfo): String {
1719
val json = try {
18-
Json.encodeToString(
20+
val messageSerializer = serializer(messageType.reifiedType)
21+
val dtoSerializer = LogMessageDto.serializer(messageSerializer)
22+
json.encodeToString(
23+
dtoSerializer,
1924
LogMessageDto(
20-
timestamp = Clock.System.now().toString(),
21-
message = if (message is Throwable) message.prettyPrint() else message?.let {
22-
it::class.serializer()
23-
},
25+
timestamp = clock.now().toString(),
26+
message = if (message is Throwable) message.prettyPrint() else message,
2427
level = logLevel,
2528
awsRequestId = requestContext?.awsRequestId
2629
)
2730
)
2831
} catch (e: SerializationException) {
29-
Log.warn("Log serialisation error: ${e.message}")
30-
31-
Json.encodeToString(
32+
json.encodeToString(
3233
LogMessageDto(
33-
timestamp = Clock.System.now().toString(),
34+
timestamp = clock.now().toString(),
3435
message = message?.toString(),
3536
level = logLevel,
3637
awsRequestId = requestContext?.awsRequestId
Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,64 @@
11
package io.github.trueangle.knative.lambda.runtime.log
22

33
import io.github.trueangle.knative.lambda.runtime.LambdaEnvironment
4+
import io.github.trueangle.knative.lambda.runtime.LambdaRuntime
45
import io.github.trueangle.knative.lambda.runtime.api.Context
6+
import io.github.trueangle.knative.lambda.runtime.log.Log.write
7+
import io.ktor.util.reflect.TypeInfo
8+
import io.ktor.util.reflect.typeInfo
59

610
internal interface LogWriter {
711
fun write(level: LogLevel, message: Any?)
812
}
913

1014
internal interface LogFormatter {
11-
fun format(logLevel: LogLevel, message: Any?): Any?
12-
fun onContextAvailable(context: Context) {}
15+
fun <T> format(logLevel: LogLevel, message: T?, messageType: TypeInfo): String?
16+
fun onContextAvailable(context: Context) = Unit
1317
}
1418

1519
object Log {
1620
@PublishedApi
1721
internal val currentLogLevel = LogLevel.fromEnv()
1822
private val writer = StdoutLogWriter()
1923
private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT == "JSON") {
20-
JsonLogFormatter()
24+
JsonLogFormatter(LambdaRuntime.json)
2125
} else {
2226
TextLogFormatter()
2327
}
2428

25-
fun trace(message: Any?) {
26-
write(LogLevel.TRACE, message)
29+
inline fun <reified T> trace(message: T?) {
30+
write(LogLevel.TRACE, message, typeInfo<T>())
2731
}
2832

29-
fun debug(message: Any?) {
30-
write(LogLevel.DEBUG, message)
33+
inline fun <reified T> debug(message: T?) {
34+
write(LogLevel.DEBUG, message, typeInfo<T>())
3135
}
3236

33-
fun info(message: Any?) {
34-
write(LogLevel.INFO, message)
37+
inline fun <reified T> info(message: T?) {
38+
write(LogLevel.INFO, message, typeInfo<T>())
3539
}
3640

37-
fun warn(message: Any?) {
38-
write(LogLevel.WARN, message)
41+
inline fun <reified T> warn(message: T?) {
42+
write(LogLevel.WARN, message, typeInfo<T>())
3943
}
4044

41-
fun error(message: Any?) {
42-
write(LogLevel.ERROR, message)
45+
inline fun <reified T> error(message: T?) {
46+
write(LogLevel.ERROR, message, typeInfo<T>())
4347
}
4448

45-
fun fatal(message: Any?) {
46-
write(LogLevel.FATAL, message)
49+
inline fun <reified T> fatal(message: T?) {
50+
write(LogLevel.FATAL, message, typeInfo<T>())
4751
}
4852

4953
@PublishedApi
5054
internal fun setContext(context: Context) {
5155
logFormatter.onContextAvailable(context)
5256
}
5357

54-
private fun write(level: LogLevel, message: Any?) {
58+
@PublishedApi
59+
internal fun write(level: LogLevel, message: Any?, typeInfo: TypeInfo) {
5560
if (level >= currentLogLevel) {
56-
writer.write(level, logFormatter.format(level, message))
61+
writer.write(level, logFormatter.format(level, message, typeInfo))
5762
}
5863
}
5964
}

lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/TextLogFormatter.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.github.trueangle.knative.lambda.runtime.log
22

3+
import io.ktor.util.reflect.TypeInfo
4+
35
internal class TextLogFormatter : LogFormatter {
4-
override fun format(logLevel: LogLevel, message: Any?): String? = message?.let {
6+
override fun <T> format(logLevel: LogLevel, message: T?, typeInfo: TypeInfo) = message?.let {
57
buildString {
68
append("[${logLevel.toString().uppercase()}] | ")
79
append(if (message is Throwable) message.prettyPrint() else message.toString())
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.github.trueangle.knative.lambda.runtime
2+
3+
import dev.mokkery.answering.returns
4+
import dev.mokkery.every
5+
import dev.mokkery.mock
6+
import io.github.trueangle.knative.lambda.runtime.api.dto.LogMessageDto
7+
import io.github.trueangle.knative.lambda.runtime.log.JsonLogFormatter
8+
import io.github.trueangle.knative.lambda.runtime.log.LogLevel
9+
import io.ktor.util.reflect.typeInfo
10+
import kotlinx.datetime.Clock
11+
import kotlinx.serialization.ExperimentalSerializationApi
12+
import kotlinx.serialization.Serializable
13+
import kotlinx.serialization.encodeToString
14+
import kotlinx.serialization.json.Json
15+
import kotlin.test.Test
16+
import kotlin.test.assertEquals
17+
18+
class JsonLogFormatterTest {
19+
private val clock = mock<Clock>()
20+
private val requestId = "awsRequestId"
21+
private val timestamp = Clock.System.now()
22+
23+
@OptIn(ExperimentalSerializationApi::class)
24+
private val formatter = JsonLogFormatter(clock = clock, json = Json { explicitNulls = true }).apply {
25+
onContextAvailable(mockContext(requestId))
26+
}
27+
28+
@Test
29+
fun `GIVEN message of object WHEN format THEN json`() {
30+
val message = SampleObject("Hello world")
31+
32+
every { clock.now() } returns (timestamp)
33+
34+
val expected = Json.encodeToString(
35+
LogMessageDto(
36+
timestamp = timestamp.toString(),
37+
message = message,
38+
level = LogLevel.INFO,
39+
awsRequestId = requestId
40+
)
41+
)
42+
val actual = formatter.format(LogLevel.INFO, message, typeInfo<SampleObject>())
43+
44+
assertEquals(expected, actual)
45+
}
46+
47+
@Test
48+
fun `GIVEN message of primitive WHEN format THEN json`() {
49+
val message = "Hello world"
50+
51+
every { clock.now() } returns (timestamp)
52+
53+
val expected = Json.encodeToString(
54+
LogMessageDto(
55+
timestamp = timestamp.toString(),
56+
message = message,
57+
level = LogLevel.INFO,
58+
awsRequestId = requestId
59+
)
60+
)
61+
val actual = formatter.format(LogLevel.INFO, message, typeInfo<String>())
62+
63+
assertEquals(expected, actual)
64+
}
65+
66+
@Test
67+
fun `GIVEN non-serializable message object WHEN format THEN json`() {
68+
val message = NoSerialObject("Hello world")
69+
70+
every { clock.now() } returns (timestamp)
71+
72+
val expected = Json.encodeToString(
73+
LogMessageDto(
74+
timestamp = timestamp.toString(),
75+
message = message.toString(),
76+
level = LogLevel.INFO,
77+
awsRequestId = requestId
78+
)
79+
)
80+
val actual = formatter.format(LogLevel.INFO, message, typeInfo<NoSerialObject>())
81+
82+
assertEquals(expected, actual)
83+
}
84+
85+
@Serializable
86+
private data class SampleObject(val hello: String)
87+
88+
private data class NoSerialObject(val hello: String)
89+
}

0 commit comments

Comments
 (0)