3
3
4
4
package dev.hermannm.devlog
5
5
6
+ import kotlin.reflect.KType
7
+ import kotlin.reflect.typeOf
6
8
import kotlinx.serialization.ExperimentalSerializationApi
7
9
import kotlinx.serialization.SerializationStrategy
8
10
import kotlinx.serialization.json.Json
@@ -15,6 +17,7 @@ import kotlinx.serialization.json.JsonUnquotedLiteral
15
17
import kotlinx.serialization.json.booleanOrNull
16
18
import kotlinx.serialization.json.doubleOrNull
17
19
import kotlinx.serialization.json.longOrNull
20
+ import kotlinx.serialization.serializer
18
21
19
22
/* *
20
23
* A log field is a key-value pair for adding structured data to logs.
@@ -43,7 +46,6 @@ import kotlinx.serialization.json.longOrNull
43
46
* 3. Logging context fields (from [withLoggingContext])
44
47
*/
45
48
public class LogField
46
- @PublishedApi
47
49
internal constructor (
48
50
@kotlin.jvm.JvmField internal val key: String ,
49
51
@kotlin.jvm.JvmField internal val value: String ,
@@ -94,11 +96,55 @@ internal constructor(
94
96
* - `java.math.BigDecimal`
95
97
*/
96
98
public inline fun <reified ValueT > field (key : String , value : ValueT ): LogField {
97
- return encodeFieldValue(
98
- value,
99
- onJson = { jsonValue -> LogField (key, jsonValue, isJson = true ) },
100
- onString = { stringValue -> LogField (key, stringValue, isJson = false ) },
101
- )
99
+ return try {
100
+ createLogFieldOfType(key, value, valueType = typeOf<ValueT >())
101
+ } catch (_: Exception ) {
102
+ // Falls back to `toString()` if serialization fails
103
+ createStringLogField(key, value)
104
+ }
105
+ }
106
+
107
+ /* *
108
+ * We split this out from [field] to reduce the amount of inlined code (more inlined code -> bigger
109
+ * code size -> possibly worse performance, and also more internal API that must be exposed with
110
+ * `@PublishedApi`).
111
+ */
112
+ @PublishedApi
113
+ internal fun createLogFieldOfType (
114
+ key : String ,
115
+ value : Any? ,
116
+ valueType : KType ,
117
+ ): LogField {
118
+ return when {
119
+ /* * See [JSON_NULL_VALUE] for why we handle nulls like this. */
120
+ value == null -> {
121
+ LogField (key, JSON_NULL_VALUE , isJson = true )
122
+ }
123
+ // Special case for String, to avoid redundant serialization
124
+ value is String -> {
125
+ LogField (key, value, isJson = false )
126
+ }
127
+ // Special case for common types that kotlinx.serialization doesn't handle by default
128
+ fieldValueShouldUseToString(value) -> {
129
+ LogField (key, value.toString(), isJson = false )
130
+ }
131
+ // Try to serialize with kotlinx.serialization - if it fails, we fall back to toString below
132
+ else -> {
133
+ val serializer = LOG_FIELD_JSON_FORMAT .serializersModule.serializer(valueType)
134
+ val serializedValue = LOG_FIELD_JSON_FORMAT .encodeToString(serializer, value)
135
+ LogField (key, serializedValue, isJson = true )
136
+ }
137
+ }
138
+ }
139
+
140
+ /* * `toString()` fallback for the log field value when [createLogFieldOfType] fails. */
141
+ @PublishedApi
142
+ internal fun createStringLogField (key : String , value : Any? ): LogField {
143
+ return if (value == null ) {
144
+ LogField (key, JSON_NULL_VALUE , isJson = true )
145
+ } else {
146
+ LogField (key, value.toString(), isJson = false )
147
+ }
102
148
}
103
149
104
150
/* *
@@ -134,70 +180,16 @@ public fun <ValueT : Any> field(
134
180
value : ValueT ? ,
135
181
serializer : SerializationStrategy <ValueT >,
136
182
): LogField {
137
- return encodeFieldValueWithSerializer(
138
- value,
139
- serializer,
140
- onJson = { jsonValue -> LogField (key, jsonValue, isJson = true ) },
141
- onString = { stringValue -> LogField (key, stringValue, isJson = false ) },
142
- )
143
- }
144
-
145
- /* *
146
- * Encodes the given value to JSON, calling [onJson] with the result. If we failed to encode to
147
- * JSON, or the value was already a string (or one of the types with special handling as explained
148
- * on [field]'s docstring), we fall back to its `toString` representation and call [onString].
149
- *
150
- * We take callbacks for the different results here instead of returning a return value. This is
151
- * because we use this in both [field] and [LogBuilder.field], and they want to do different things
152
- * with the encoded value:
153
- * - [field] constructs a [LogField] with it
154
- * - [LogBuilder.field] passes the value to [LogEvent.addStringField] or [LogEvent.addJsonField]
155
- *
156
- * If we used a return value here, we would have to wrap it in an object to convey whether it was
157
- * encoded to JSON or just a plain string, which requires an allocation. By instead taking callbacks
158
- * and making the function `inline`, we pay no extra cost.
159
- */
160
- @PublishedApi
161
- internal inline fun <reified ValueT , ReturnT > encodeFieldValue (
162
- value : ValueT ,
163
- crossinline onJson : (String ) -> ReturnT ,
164
- crossinline onString : (String ) -> ReturnT ,
165
- ): ReturnT {
166
- try {
167
- return when {
168
- value == null -> onJson(JSON_NULL_VALUE )
169
- // Special case for String, to avoid redundant serialization
170
- value is String -> onString(value)
171
- // Special case for common types that kotlinx.serialization doesn't handle by default
172
- fieldValueShouldUseToString(value) -> onString(value.toString())
173
- // Try to serialize with kotlinx.serialization - if it fails, we fall back to toString below
174
- else -> onJson(LOG_FIELD_JSON_FORMAT .encodeToString(value))
175
- }
176
- } catch (_: Exception ) {
177
- // We don't want to ever throw an exception from constructing a log field, which may happen if
178
- // serialization fails, for example. So in these cases we fall back to toString()
179
- return onString(value.toString())
180
- }
181
- }
182
-
183
- /* * Same as [encodeFieldValue], but takes a user-provided serializer for serializing the value. */
184
- internal inline fun <ValueT : Any , ReturnT > encodeFieldValueWithSerializer (
185
- value : ValueT ? ,
186
- serializer : SerializationStrategy <ValueT >,
187
- crossinline onJson : (String ) -> ReturnT ,
188
- crossinline onString : (String ) -> ReturnT ,
189
- ): ReturnT {
190
- try {
191
- return when {
192
- // Handle nulls here, so users don't have to deal with passing a null-handling serializer
193
- value == null -> onJson(JSON_NULL_VALUE )
194
- // Try to serialize with kotlinx.serialization - if it fails, we fall back to toString below
195
- else -> onJson(LOG_FIELD_JSON_FORMAT .encodeToString(serializer, value))
183
+ return try {
184
+ if (value == null ) {
185
+ LogField (key, JSON_NULL_VALUE , isJson = true )
186
+ } else {
187
+ val serializedValue = LOG_FIELD_JSON_FORMAT .encodeToString(serializer, value)
188
+ LogField (key, serializedValue, isJson = true )
196
189
}
197
190
} catch (_: Exception ) {
198
- // We don't want to ever throw an exception from constructing a log field, which may happen if
199
- // serialization fails, for example. So in these cases we fall back to toString().
200
- return onString(value.toString())
191
+ // Falls back to `toString()` if serialization fails
192
+ createStringLogField(key, value)
201
193
}
202
194
}
203
195
@@ -324,8 +316,16 @@ public fun rawJson(json: String, validJson: Boolean = false): JsonElement {
324
316
* Validates that the given raw JSON string is valid JSON, calling [onValidJson] if it is, or
325
317
* [onInvalidJson] if it's not.
326
318
*
327
- * We take lambdas here instead of returning a value, for the same reason as [encodeFieldValue]. We
328
- * use this in both [rawJsonField] and [LogBuilder.rawJsonField].
319
+ * We take callbacks for the different results here instead of returning a return value. This is
320
+ * because we use this in both [rawJsonField] and [LogBuilder.rawJsonField], and they want to do
321
+ * different things with the encoded value:
322
+ * - [rawJsonField] constructs a [LogField] with it
323
+ * - [LogBuilder.rawJsonField] passes the value to [LogEvent.addStringField] or
324
+ * [LogEvent.addJsonField]
325
+ *
326
+ * If we used a return value here, we would have to wrap it in an object to convey whether it was
327
+ * encoded to JSON or just a plain string, which requires an allocation. By instead taking callbacks
328
+ * and making the function `inline`, we pay no extra cost.
329
329
*/
330
330
internal inline fun <ReturnT > validateRawJson (
331
331
json : String ,
@@ -405,7 +405,7 @@ internal fun isValidJson(jsonElement: JsonElement): Boolean {
405
405
* We make this an expect-actual function, so that implementations can use platform-specific types
406
406
* (such as Java standard library classes on the JVM).
407
407
*/
408
- @PublishedApi internal expect fun fieldValueShouldUseToString (value : Any ): Boolean
408
+ internal expect fun fieldValueShouldUseToString (value : Any ): Boolean
409
409
410
410
/* *
411
411
* SLF4J supports null values in `KeyValuePair`s, and it's up to the logger implementation for how
@@ -414,9 +414,8 @@ internal fun isValidJson(jsonElement: JsonElement): Boolean {
414
414
* field was omitted due to some error. So in this library, we instead use a JSON `null` as the
415
415
* value for null log fields.
416
416
*/
417
- @PublishedApi internal const val JSON_NULL_VALUE : String = " null"
417
+ internal const val JSON_NULL_VALUE : String = " null"
418
418
419
- @PublishedApi
420
419
@kotlin.jvm.JvmField
421
420
internal val LOG_FIELD_JSON_FORMAT : Json = Json {
422
421
encodeDefaults = true
0 commit comments