Skip to content

Commit 0fbcad3

Browse files
committed
Refactor json logger
1 parent f427ac9 commit 0fbcad3

File tree

5 files changed

+120
-21
lines changed

5 files changed

+120
-21
lines changed

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.github.trueangle.knative.lambda.runtime
22

3+
import kotlinx.serialization.Serializable
4+
import kotlin.experimental.ExperimentalNativeApi
5+
6+
internal fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)
7+
38
internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildString {
49
append("An exception occurred:\n")
510
message?.let {
@@ -11,4 +16,16 @@ internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildStr
1116
}
1217
}
1318

14-
internal fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)
19+
@OptIn(ExperimentalNativeApi::class)
20+
internal fun Throwable.asSerialObject(): ThrowableDto = ThrowableDto(
21+
message = message,
22+
cause = cause?.asSerialObject(),
23+
stackTrace = getStackTrace().toList()
24+
)
25+
26+
@Serializable
27+
internal data class ThrowableDto(
28+
val message: String?,
29+
val cause: ThrowableDto?,
30+
val stackTrace: List<String>,
31+
)

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.github.trueangle.knative.lambda.runtime.log.debug
1414
import io.github.trueangle.knative.lambda.runtime.log.error
1515
import io.github.trueangle.knative.lambda.runtime.log.fatal
1616
import io.github.trueangle.knative.lambda.runtime.log.info
17+
import io.github.trueangle.knative.lambda.runtime.log.trace
1718
import io.github.trueangle.knative.lambda.runtime.log.warn
1819
import io.ktor.client.HttpClient
1920
import io.ktor.client.engine.HttpClientEngine
@@ -141,18 +142,21 @@ internal class Runner(
141142
handler(channel)
142143
} catch (e: Exception) {
143144
log.warn("Exception occurred on streaming: " + e.message)
145+
log.error(e)
144146

145-
channel.writeStringUtf8(e.toTrailer())
147+
channel.writeMidstreamError(e)
146148
}
147149
}
148-
149-
private fun Throwable.toTrailer(): String =
150-
"Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: ${stackTraceToString().encodeBase64()}\r\n"
151150
}
152151

153152
inline fun <T, R> T.bufferedResponse(context: Context, block: T.() -> R): R = try {
154153
block()
155154
} catch (e: Exception) {
156155
throw e.asHandlerError(context)
157156
}
158-
}
157+
}
158+
159+
suspend fun ByteWriteChannel.writeMidstreamError(e: Throwable) = writeStringUtf8(e.toTrailer())
160+
161+
internal fun Throwable.toTrailer(): String =
162+
"Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: ${stackTraceToString().encodeBase64()}\r\n"

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ 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.github.trueangle.knative.lambda.runtime.prettyPrint
5+
import io.github.trueangle.knative.lambda.runtime.asSerialObject
66
import io.ktor.util.reflect.TypeInfo
77
import kotlinx.datetime.Clock
88
import kotlinx.serialization.SerializationException
@@ -18,17 +18,29 @@ internal class JsonLogFormatter(
1818

1919
override fun <T> format(logLevel: LogLevel, message: T?, messageType: TypeInfo): String {
2020
val json = try {
21-
val messageSerializer = serializer(messageType.reifiedType)
22-
val dtoSerializer = LogMessageDto.serializer(messageSerializer)
23-
json.encodeToString(
24-
dtoSerializer,
25-
LogMessageDto(
26-
timestamp = clock.now().toString(),
27-
message = if (message is Throwable) message.prettyPrint() else message,
28-
level = logLevel,
29-
awsRequestId = requestContext?.awsRequestId
21+
if (message is Throwable) {
22+
json.encodeToString(
23+
LogMessageDto(
24+
timestamp = clock.now().toString(),
25+
message = message.asSerialObject(),
26+
level = logLevel,
27+
awsRequestId = requestContext?.awsRequestId
28+
)
3029
)
31-
)
30+
} else {
31+
val messageSerializer = serializer(messageType.reifiedType)
32+
val dtoSerializer = LogMessageDto.serializer(messageSerializer)
33+
34+
json.encodeToString(
35+
dtoSerializer,
36+
LogMessageDto(
37+
timestamp = clock.now().toString(),
38+
message = message,
39+
level = logLevel,
40+
awsRequestId = requestContext?.awsRequestId
41+
)
42+
)
43+
}
3244
} catch (e: SerializationException) {
3345
json.encodeToString(
3446
LogMessageDto(

lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/JsonLogFormatterTest.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import io.github.trueangle.knative.lambda.runtime.log.LogLevel
99
import io.ktor.util.reflect.typeInfo
1010
import kotlinx.datetime.Clock
1111
import kotlinx.serialization.ExperimentalSerializationApi
12-
import kotlinx.serialization.Serializable
1312
import kotlinx.serialization.encodeToString
1413
import kotlinx.serialization.json.Json
1514
import kotlin.test.Test
@@ -100,4 +99,23 @@ class JsonLogFormatterTest {
10099

101100
assertEquals(expected, actual)
102101
}
102+
103+
@Test
104+
fun `GIVEN throwable message WHEN format THEN json`() {
105+
val message = RuntimeException("Runtime exception occurred")
106+
107+
every { clock.now() } returns (timestamp)
108+
109+
val expected = Json.encodeToString(
110+
LogMessageDto(
111+
timestamp = timestamp.toString(),
112+
message = message.asSerialObject(),
113+
level = LogLevel.INFO,
114+
awsRequestId = requestId
115+
)
116+
)
117+
val actual = formatter.format(LogLevel.INFO, message, typeInfo<RuntimeException>())
118+
119+
assertEquals(expected, actual)
120+
}
103121
}

lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntimeTest.kt

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import io.github.trueangle.knative.lambda.runtime.api.LambdaClient
2020
import io.github.trueangle.knative.lambda.runtime.api.LambdaClientImpl
2121
import io.github.trueangle.knative.lambda.runtime.events.apigateway.APIGatewayRequest
2222
import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler
23+
import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler
2324
import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger
25+
import io.github.trueangle.knative.lambda.runtime.log.Log
2426
import io.github.trueangle.knative.lambda.runtime.log.LogLevel.ERROR
2527
import io.github.trueangle.knative.lambda.runtime.log.LogLevel.FATAL
2628
import io.ktor.client.engine.HttpClientEngine
@@ -29,15 +31,24 @@ import io.ktor.client.engine.mock.MockRequestHandleScope
2931
import io.ktor.client.engine.mock.respond
3032
import io.ktor.client.engine.mock.respondBadRequest
3133
import io.ktor.client.engine.mock.respondError
34+
import io.ktor.client.engine.mock.respondOk
35+
import io.ktor.client.request.HttpRequestData
3236
import io.ktor.http.HttpHeaders
3337
import io.ktor.http.HttpStatusCode
38+
import io.ktor.http.content.ChannelWriterContent
39+
import io.ktor.http.content.OutgoingContent
3440
import io.ktor.http.headers
3541
import io.ktor.http.headersOf
3642
import io.ktor.util.reflect.typeInfo
3743
import io.ktor.utils.io.ByteReadChannel
44+
import io.ktor.utils.io.ByteWriteChannel
45+
import io.ktor.utils.io.copyTo
3846
import kotlinx.cinterop.ExperimentalForeignApi
3947
import kotlinx.cinterop.toKString
4048
import kotlinx.coroutines.test.runTest
49+
import kotlinx.io.Buffer
50+
import kotlinx.io.RawSource
51+
import kotlinx.io.Source
4152
import kotlinx.serialization.json.Json
4253
import platform.posix.getenv
4354
import platform.posix.setenv
@@ -49,7 +60,7 @@ import kotlin.test.assertTrue
4960
internal const val RESOURCES_PATH = "src/nativeTest/resources"
5061

5162
class LambdaRuntimeTest {
52-
private val log = mock<LambdaLogger>()
63+
private val log = spy<LambdaLogger>(Log)
5364
private val context = Context(
5465
awsRequestId = "156cb537-e2d4-11e8-9b34-d36013741fb9",
5566
deadlineTimeInMs = 1542409706888L,
@@ -203,7 +214,7 @@ class LambdaRuntimeTest {
203214
}
204215

205216
@Test
206-
fun `GIVEN EventBodyParseException WHEN retrieveNextEvent THEN report error AND continue working`() = runTest {
217+
fun `GIVEN EventBodyParseException WHEN retrieveNextEvent THEN report error AND skip event`() = runTest {
207218
val event = NonSerialObject("")
208219
val lambdaRunner = createRunner(MockEngine { request ->
209220
val path = request.url.encodedPath
@@ -227,7 +238,7 @@ class LambdaRuntimeTest {
227238
}
228239

229240
@Test
230-
fun `GIVEN Handler exception WHEN handleRequest THEN report error AND continue working`() = runTest {
241+
fun `GIVEN Handler exception WHEN handleRequest THEN report error AND skip event`() = runTest {
231242
val lambdaRunner = createRunner(MockEngine { request ->
232243
val path = request.url.encodedPath
233244
when {
@@ -249,6 +260,43 @@ class LambdaRuntimeTest {
249260
verify(not) { lambdaRunner.env.terminate() }
250261
}
251262

263+
@Test
264+
fun `GIVEN Handler exception WHEN streamingResponse THEN consume error`() = runTest {
265+
val lambdaRunner = createRunner(MockEngine { request ->
266+
val path = request.url.encodedPath
267+
when {
268+
path.contains("invocation/next") -> respondNextEventSuccess("")
269+
path.contains("${context.awsRequestId}/response") -> {
270+
println(request.body.contentLength)
271+
println(request.body.status)
272+
respond("", HttpStatusCode.Accepted)
273+
}
274+
275+
else -> respondBadRequest()
276+
}
277+
})
278+
val client = lambdaRunner.client
279+
280+
val handler = object : LambdaStreamHandler<String, ByteWriteChannel> {
281+
override suspend fun handleRequest(input: String, output: ByteWriteChannel, context: Context) {
282+
output.writeMidstreamError(RuntimeException())
283+
ByteReadChannel("").copyTo(output)
284+
}
285+
}
286+
287+
lambdaRunner.run(singleEventMode = true) { handler }
288+
289+
//assertTrue(request.body is ByteWriteChannel)
290+
291+
//val condition = body?.trailers()?.contains(RuntimeException().toTrailer())
292+
293+
// assertTrue(condition == true)
294+
295+
verifySuspend { client.streamResponse(any(), any()) }
296+
verify(not) { log.log(FATAL, any<Any>(), any()) }
297+
verify(not) { lambdaRunner.env.terminate() }
298+
}
299+
252300
@OptIn(ExperimentalForeignApi::class)
253301
private fun mockEnvironment() {
254302
if (getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().isNullOrEmpty()) {

0 commit comments

Comments
 (0)