Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.embrace.android.embracesdk.internal.arch.destination.SessionSpanWriter
import io.embrace.android.embracesdk.internal.config.ConfigService
import io.embrace.android.embracesdk.internal.config.behavior.REDACTED_LABEL
import io.embrace.android.embracesdk.internal.prefs.PreferencesService
import io.embrace.android.embracesdk.internal.utils.PropertyUtils

internal class SessionPropertiesServiceImpl(
preferencesService: PreferencesService,
Expand All @@ -18,7 +19,7 @@ internal class SessionPropertiesServiceImpl(
if (!isValidKey(originalKey)) {
return false
}
val sanitizedKey = enforceLength(originalKey, SESSION_PROPERTY_KEY_LIMIT)
val sanitizedKey = PropertyUtils.truncate(originalKey, SESSION_PROPERTY_KEY_LIMIT)

if (!isValidValue(originalValue)) {
return false
Expand All @@ -27,7 +28,7 @@ internal class SessionPropertiesServiceImpl(
val sanitizedValue = if (configService.sensitiveKeysBehavior.isSensitiveKey(sanitizedKey)) {
REDACTED_LABEL
} else {
enforceLength(originalValue, SESSION_PROPERTY_VALUE_LIMIT)
PropertyUtils.truncate(originalValue, SESSION_PROPERTY_VALUE_LIMIT)
}

val added = props.add(sanitizedKey, sanitizedValue, permanent)
Expand All @@ -41,7 +42,7 @@ internal class SessionPropertiesServiceImpl(
if (!isValidKey(originalKey)) {
return false
}
val sanitizedKey = enforceLength(originalKey, SESSION_PROPERTY_KEY_LIMIT)
val sanitizedKey = PropertyUtils.truncate(originalKey, SESSION_PROPERTY_KEY_LIMIT)

val removed = props.remove(sanitizedKey)
if (removed) {
Expand All @@ -68,14 +69,6 @@ internal class SessionPropertiesServiceImpl(

private fun isValidValue(key: String?): Boolean = key != null

private fun enforceLength(value: String, maxLength: Int): String {
if (value.length <= maxLength) {
return value
}
val endChars = "..."
return value.substring(0, maxLength - endChars.length) + endChars
}

private companion object {
/**
* The maximum number of characters of a session property key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.embrace.android.embracesdk.internal.config.instrumented.schema.Session
import io.embrace.android.embracesdk.internal.config.remote.RemoteConfig
import io.embrace.android.embracesdk.internal.gating.SessionGatingKeys
import java.util.Locale
import kotlin.math.min

/**
* Provides the behavior that functionality relating to sessions should follow.
Expand All @@ -15,7 +16,8 @@ class SessionBehaviorImpl(
) : SessionBehavior {

companion object {
const val SESSION_PROPERTY_LIMIT: Int = 10
const val SESSION_PROPERTY_LIMIT: Int = 100
const val SESSION_PROPERTY_MAX_LIMIT: Int = 200
}

override val local: SessionConfig = local.session
Expand All @@ -32,7 +34,7 @@ class SessionBehaviorImpl(

override fun isSessionControlEnabled(): Boolean = remote?.sessionConfig?.isEnabled ?: false

override fun getMaxSessionProperties(): Int = remote?.maxSessionProperties ?: SESSION_PROPERTY_LIMIT
override fun getMaxSessionProperties(): Int = min(remote?.maxSessionProperties ?: SESSION_PROPERTY_LIMIT, SESSION_PROPERTY_MAX_LIMIT)

override fun shouldGateInfoLog(): Boolean = shouldGateFeature(SessionGatingKeys.LOGS_INFO)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.embrace.android.embracesdk.internal.logs

import android.os.Parcelable
import io.embrace.android.embracesdk.LogExceptionType
import io.embrace.android.embracesdk.Severity
import io.embrace.android.embracesdk.internal.arch.destination.LogWriter
Expand All @@ -16,9 +17,10 @@ import io.embrace.android.embracesdk.internal.otel.attrs.embExceptionHandling
import io.embrace.android.embracesdk.internal.payload.AppFramework
import io.embrace.android.embracesdk.internal.payload.Envelope
import io.embrace.android.embracesdk.internal.session.orchestrator.PayloadStore
import io.embrace.android.embracesdk.internal.utils.PropertyUtils.sanitizeProperties
import io.embrace.android.embracesdk.internal.utils.PropertyUtils.truncate
import io.embrace.android.embracesdk.internal.utils.Uuid
import io.opentelemetry.semconv.incubating.LogIncubatingAttributes
import java.io.Serializable

/**
* Creates log records to be sent using the Open Telemetry Logs data model.
Expand Down Expand Up @@ -161,11 +163,48 @@ class EmbraceLogService(
}
}

private fun sanitizeProperties(
properties: Map<String, Any>?,
bypassPropertyLimit: Boolean = false,
): Map<String, Any> {
return if (properties == null) {
emptyMap()
} else {
runCatching {
if (bypassPropertyLimit) {
properties.entries.associate {
Pair(it.key, checkIfSerializable(it.value))
}
} else {
properties.entries.take(MAX_PROPERTY_COUNT).associate {
Pair(
first = truncate(it.key, MAX_PROPERTY_KEY_LENGTH),
second = truncate(checkIfSerializable(it.value).toString(), MAX_PROPERTY_VALUE_LENGTH)
)
}
}
}.getOrDefault(emptyMap())
}
}

private fun checkIfSerializable(value: Any): Any {
if (!(value is Parcelable || value is Serializable)) {
return "not serializable"
}
return value
}

private companion object {

/**
* The default limit of Unity log messages that can be sent.
*/
private const val LOG_MESSAGE_UNITY_MAXIMUM_ALLOWED_LENGTH = 16384

private const val MAX_PROPERTY_COUNT: Int = 100

private const val MAX_PROPERTY_KEY_LENGTH = 128

private const val MAX_PROPERTY_VALUE_LENGTH = 1024
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,14 @@
package io.embrace.android.embracesdk.internal.utils

import android.os.Parcelable
import java.io.Serializable

/**
* Utility to for sanitizing user-supplied properties.
*/
object PropertyUtils {

const val MAX_PROPERTY_SIZE: Int = 10
private const val END_CHARS = "..."

fun sanitizeProperties(properties: Map<String, Any>?, bypassPropertyLimit: Boolean = false): Map<String, Any> {
return if (properties == null) {
emptyMap()
} else {
runCatching {
if (bypassPropertyLimit) {
properties.entries
} else {
properties.entries.take(MAX_PROPERTY_SIZE)
}.associate { Pair(it.key, checkIfSerializable(it.value)) }
}.getOrDefault(emptyMap())
fun truncate(value: String, maxLength: Int): String {
if (value.length <= maxLength) {
return value
}
}

private fun checkIfSerializable(value: Any): Any {
if (!(value is Parcelable || value is Serializable)) {
return "not serializable"
}
return value
return "${value.take(maxLength - 3)}$END_CHARS"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal class SessionBehaviorImplImplTest {
assertNull(getSessionComponents())
assertFalse(isGatingFeatureEnabled())
assertFalse(isSessionControlEnabled())
assertEquals(10, getMaxSessionProperties())
assertEquals(100, getMaxSessionProperties())
}
}

Expand Down Expand Up @@ -58,6 +58,13 @@ internal class SessionBehaviorImplImplTest {
assertEquals(setOf("crashes", "errors"), behavior.getFullSessionEvents())
}

@Test
fun `remote session properties limit is capped to 200`() {
with(createSessionBehavior(remoteCfg = RemoteConfig(maxSessionProperties = 1000))) {
assertEquals(200, getMaxSessionProperties())
}
}

private fun buildGatingConfig(events: Set<String>) = RemoteConfig(
sessionConfig = SessionRemoteConfig(
fullSessionEvents = events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,66 @@ internal class EmbraceLogServiceTest {
val attachment = payloadStore.storedAttachments.single()
assertEquals(bytes, attachment.data.second)
}

@Test
fun `log properties truncated properly`() {
logService.log(
message = "message",
severity = Severity.INFO,
logExceptionType = LogExceptionType.NONE,
properties = tooBigProperties
)

// then the message is not ellipsized
val logProps = fakeLogWriter.logEvents.single().schemaType.attributes().filter { it.key.startsWith("test") }
assertEquals(truncatedProps, logProps)
}

@Test
fun `unserializable log values turned into error string`() {
logService.log(
message = "message",
severity = Severity.INFO,
logExceptionType = LogExceptionType.NONE,
properties = mapOf("badvalue" to UnSerializableClass())
)
assertEquals("not serializable", fakeLogWriter.logEvents.single().schemaType.attributes()["badvalue"])
}

@Test
fun `log properties unchanged if embrace not in use`() {
fakeConfigService = FakeConfigService(onlyUsingOtelExporters = true)
logService = createEmbraceLogService()
logService.log(
message = "message",
severity = Severity.INFO,
logExceptionType = LogExceptionType.NONE,
properties = tooBigProperties
)

// then the message is not ellipsized
val logProps = fakeLogWriter.logEvents.single().schemaType.attributes().filter { it.key.startsWith("test") }
assertEquals(tooBigProperties, logProps)
}

private class UnSerializableClass

companion object {
private val twoHundredXs = "x".repeat(200)
private val twoThousandXs = twoHundredXs.repeat(10)

val tooBigProperties = mutableMapOf<String, String>().apply {
repeat(150) {
this["test$it$twoHundredXs"] = twoThousandXs
}
}

val truncatedProps = mutableMapOf<String, String>().apply {
val expectedValue = twoThousandXs.take(1021) + "..."
repeat(100) {
val key = "test$it$twoHundredXs".take(125) + "..."
this[key] = expectedValue
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,72 +1,19 @@
package io.embrace.android.embracesdk.internal.utils

import io.embrace.android.embracesdk.internal.utils.PropertyUtils.sanitizeProperties
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

internal class PropertyUtilsTest {

@Test
fun testEmptyCase() {
assertEquals(emptyMap<String, String>(), sanitizeProperties(null))
assertEquals(emptyMap<String, String>(), sanitizeProperties(emptyMap()))
fun `no truncation`() {
val value = "a".repeat(100)
assertEquals(value, PropertyUtils.truncate(value, 100))
}

@Test
fun testPropertyLimitExceeded() {
val input = (0..20).associateBy { "$it" }
val expected = (0..9).associateBy { "$it" }
assertEquals(expected, sanitizeProperties(input as Map<String, Any>?))
fun `basic truncation`() {
val value = "a".repeat(110)
assertEquals(value.take(97) + "...", PropertyUtils.truncate(value, 100))
}

@Test
fun testSerializableValue() {
val obj = SerializableClass()
assertEquals(obj, sanitizeProperties(mapOf("a" to obj))["a"])
}

@Test
fun testUnserializableValue() {
assertEquals("not serializable", sanitizeProperties(mapOf("a" to UnSerializableClass()))["a"])
}

@Test
fun `bypass limits`() {
val input = (0..20).associateBy { "$it" }
assertEquals(input.size, sanitizeProperties(input, true).size)
}

@Test
fun testPropertiesNormalization() {
val sourceMap: MutableMap<String, Any> = HashMap()
sourceMap[""] = "Empty key"
sourceMap["EmptyValue"] = ""
sourceMap["NullValue"] = ""
for (i in 1..9) {
sourceMap["Key$i"] = "Value$i"
}
val resultMap = sanitizeProperties(sourceMap)
assertTrue(
"Unexpected normalized map size.",
resultMap.size <= PropertyUtils.MAX_PROPERTY_SIZE
)
resultMap.entries.stream()
.peek { (key): Map.Entry<String, Any> ->
assertNotNull(
"Unexpected normalized map key: NULL.",
key
)
}
.peek { (_, value): Map.Entry<String, Any> ->
assertNotNull(
"Unexpected normalized map value: NULL.",
value
)
}
}

private class SerializableClass : java.io.Serializable
private class UnSerializableClass
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ class OtelSdkWrapper(
addLogRecordProcessor(
DefaultLogRecordProcessor(configuration.logRecordExporter)
)
logLimits {
attributeCountLimit = limits.getMaxTotalAttributeCount()
}
},
tracerProvider = {
resource(configuration.resourceAction)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ internal class SpanServiceImplTest {
val completedSpans = spanSink.completedSpans()
assertEquals(1, completedSpans.size)
val attrs = completedSpans[0].attributes.filterNot { it.key.startsWith("emb.") }
assertEquals(49, attrs.size)
assertEquals(99, attrs.size)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ interface OtelLimitsConfig {
fun getMaxNameLength(): Int = 50
fun getMaxCustomEventCount(): Int = 10
fun getMaxSystemEventCount(): Int = 11000
fun getMaxCustomAttributeCount(): Int = 50
fun getMaxCustomAttributeCount(): Int = 100
fun getMaxSystemAttributeCount(): Int = 300
fun getMaxCustomLinkCount(): Int = 10
fun getMaxSystemLinkCount(): Int = 100
fun getMaxInternalAttributeKeyLength(): Int = 1000
fun getMaxInternalAttributeValueLength(): Int = 2000
fun getMaxCustomAttributeKeyLength(): Int = 50
fun getMaxCustomAttributeValueLength(): Int = 500
fun getMaxCustomAttributeKeyLength(): Int = 128
fun getMaxCustomAttributeValueLength(): Int = 1024
fun getExceptionEventName(): String = "exception"
}
Loading
Loading