Skip to content

Commit

Permalink
Merge pull request #1816 from embrace-io/embrace-hosted-attachments
Browse files Browse the repository at this point in the history
Support embrace hosted attachments
  • Loading branch information
fractalwrench authored Jan 17, 2025
2 parents 2c782cc + 244ace1 commit f996835
Show file tree
Hide file tree
Showing 33 changed files with 427 additions and 187 deletions.
2 changes: 1 addition & 1 deletion embrace-android-api/api/embrace-android-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/LogsA
public abstract fun logInfo (Ljava/lang/String;)V
public abstract fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;)V
public abstract fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;)V
public abstract fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;J)V
public abstract fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;)V
public abstract fun logMessage (Ljava/lang/String;Lio/embrace/android/embracesdk/Severity;Ljava/util/Map;[B)V
public abstract fun logPushNotification (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;)V
public abstract fun logWarning (Ljava/lang/String;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,13 @@ public interface LogsApi {
* @param properties the properties to attach to the log message
* @param attachmentId a UUID that identifies the attachment
* @param attachmentUrl a URL that gives the location of the attachment
* @param attachmentSize the size of the attachment in bytes
*/
public fun logMessage(
message: String,
severity: Severity,
properties: Map<String, Any>?,
attachmentId: String,
attachmentUrl: String,
attachmentSize: Long,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ internal class LogModuleImpl(
EmbraceLogService(
essentialServiceModule.logWriter,
configModule.configService,
essentialServiceModule.sessionPropertiesService
essentialServiceModule.sessionPropertiesService,
deliveryModule.payloadStore,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import io.embrace.android.embracesdk.internal.arch.schema.TelemetryAttributes
import io.embrace.android.embracesdk.internal.capture.session.SessionPropertiesService
import io.embrace.android.embracesdk.internal.config.ConfigService
import io.embrace.android.embracesdk.internal.config.behavior.REDACTED_LABEL
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment
import io.embrace.android.embracesdk.internal.opentelemetry.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.spans.toOtelSeverity
import io.embrace.android.embracesdk.internal.utils.PropertyUtils.normalizeProperties
import io.embrace.android.embracesdk.internal.utils.Uuid
Expand All @@ -26,6 +29,7 @@ class EmbraceLogService(
private val logWriter: LogWriter,
private val configService: ConfigService,
private val sessionPropertiesService: SessionPropertiesService,
private val payloadStore: PayloadStore?,
) : LogService {

private val behavior = configService.logMessageBehavior
Expand All @@ -41,6 +45,7 @@ class EmbraceLogService(
logExceptionType: LogExceptionType,
properties: Map<String, Any>?,
customLogAttrs: Map<AttributeKey<String>, String>,
logAttachment: Attachment.EmbraceHosted?,
) {
val redactedProperties = redactSensitiveProperties(normalizeProperties(properties))
val attrs = createTelemetryAttributes(redactedProperties, customLogAttrs)
Expand All @@ -53,6 +58,12 @@ class EmbraceLogService(
if (logExceptionType != LogExceptionType.NONE) {
attrs.setAttribute(embExceptionHandling, logExceptionType.value)
}

logAttachment?.let {
val envelope = Envelope(data = Pair(it.id, it.bytes))
payloadStore?.storeAttachment(envelope)
}

addLogEventData(
message = message,
severity = severity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.embrace.android.embracesdk.internal.logs

import io.embrace.android.embracesdk.LogExceptionType
import io.embrace.android.embracesdk.Severity
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment
import io.embrace.android.embracesdk.internal.session.MemoryCleanerListener
import io.opentelemetry.api.common.AttributeKey

Expand All @@ -12,18 +13,14 @@ interface LogService : MemoryCleanerListener {

/**
* Creates a remote log.
*
* @param message the message to log
* @param severity the log severity
* @param logExceptionType whether the log is a handled exception, unhandled, or non an exception
* @param properties custom properties to send as part of the event
*/
fun log(
message: String,
severity: Severity,
logExceptionType: LogExceptionType,
properties: Map<String, Any>? = null,
customLogAttrs: Map<AttributeKey<String>, String> = emptyMap(),
logAttachment: Attachment.EmbraceHosted? = null,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import java.util.UUID
/**
* Holds attributes that describe an attachment to a log record.
*/
sealed class Attachment(
val size: Long,
val id: String,
) {
sealed class Attachment(val id: String) {

internal companion object {
private const val LIMIT_MB = 1 * 1024 * 1024
Expand All @@ -26,11 +23,9 @@ sealed class Attachment(
abstract val attributes: Map<EmbraceAttributeKey, String>

protected fun constructAttributes(
size: Long,
id: String,
errorCode: AttachmentErrorCode? = null
errorCode: AttachmentErrorCode? = null,
): Map<EmbraceAttributeKey, String> = mapOf(
embAttachmentSize to size.toString(),
embAttachmentId to id,
embAttachmentErrorCode to errorCode?.name
).toNonNullMap()
Expand All @@ -40,20 +35,22 @@ sealed class Attachment(
*/
class EmbraceHosted(
val bytes: ByteArray,
counter: () -> Boolean
counter: () -> Boolean,
) : Attachment(
bytes.size.toLong(),
UUID.randomUUID().toString()
) {

private val size: Long = bytes.size.toLong()

private val errorCode: AttachmentErrorCode? = when {
!counter() -> OVER_MAX_ATTACHMENTS
size > LIMIT_MB -> ATTACHMENT_TOO_LARGE
else -> null
}

override val attributes: Map<EmbraceAttributeKey, String> =
constructAttributes(size, id, errorCode)
override val attributes: Map<EmbraceAttributeKey, String> = constructAttributes(id, errorCode).plus(
embAttachmentSize to size.toString(),
)

fun shouldAttemptUpload(): Boolean = errorCode == null
}
Expand All @@ -62,24 +59,21 @@ sealed class Attachment(
* An attachment that is uploaded to a user-supplied backend.
*/
class UserHosted(
size: Long,
id: String,
val url: String,
counter: () -> Boolean,
) : Attachment(size, id) {
) : Attachment(id) {

private val errorCode: AttachmentErrorCode? = when {
!counter() -> OVER_MAX_ATTACHMENTS
size < 0 -> UNKNOWN
url.isEmpty() -> UNKNOWN
isNotUuid() -> UNKNOWN
else -> null
}

override val attributes: Map<EmbraceAttributeKey, String> =
constructAttributes(size, id, errorCode).plus(
embAttachmentUrl to url
)
override val attributes: Map<EmbraceAttributeKey, String> = constructAttributes(id, errorCode).plus(
embAttachmentUrl to url
)

private fun isNotUuid(): Boolean = try {
UUID.fromString(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package io.embrace.android.embracesdk.internal.logs.attachments
*/
enum class AttachmentErrorCode {
ATTACHMENT_TOO_LARGE,
UNSUCCESSFUL_UPLOAD,
OVER_MAX_ATTACHMENTS,
UNKNOWN
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ class AttachmentService(private val limit: Int = 5) : MemoryCleanerListener {
fun createAttachment(
attachmentId: String,
attachmentUrl: String,
attachmentSize: Long,
): UserHosted = UserHosted(
attachmentSize,
attachmentId,
attachmentUrl,
::incrementAndCheckAttachmentLimit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ internal class PayloadResurrectionServiceImpl(
inputStream = GZIPInputStream(
cacheStorageService.loadPayloadAsStream(cachedCrashEnvelopeMetadata)
),
type = SupportedEnvelopeType.CRASH.serializedType
type = checkNotNull(SupportedEnvelopeType.CRASH.serializedType)
).also {
cacheStorageService.delete(cachedCrashEnvelopeMetadata)
}
Expand Down Expand Up @@ -157,7 +157,7 @@ internal class PayloadResurrectionServiceImpl(
SupportedEnvelopeType.SESSION -> {
val deadSession = serializer.fromJson<Envelope<SessionPayload>>(
inputStream = GZIPInputStream(cacheStorageService.loadPayloadAsStream(this)),
type = envelopeType.serializedType
type = checkNotNull(envelopeType.serializedType)
)

val sessionId = deadSession.getSessionId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface PayloadStore : CrashTeardownHandler {
/**
* Stores a log attachment.
*/
fun storeAttachment(envelope: Envelope<ByteArray>)
fun storeAttachment(envelope: Envelope<Pair<String, ByteArray>>)

/**
* Stores an empty payload-type-less crash envelope for future use. One one cached version of this should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class V1PayloadStore(
}
}

override fun storeAttachment(envelope: Envelope<ByteArray>) {
override fun storeAttachment(envelope: Envelope<Pair<String, ByteArray>>) {
// ignored - v1 doesn't support attachments
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ internal class V2PayloadStore(
intakeService.take(envelope, createMetadata(type, payloadType = payloadType))
}

override fun storeAttachment(envelope: Envelope<ByteArray>) {
override fun storeAttachment(envelope: Envelope<Pair<String, ByteArray>>) {
intakeService.take(
envelope,
createMetadata(
type = SupportedEnvelopeType.ATTACHMENT,
payloadType = PayloadType.ATTACHMENT
)
)
}

override fun cacheEmptyCrashEnvelope(envelope: Envelope<LogPayload>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.embrace.android.embracesdk.LogExceptionType
import io.embrace.android.embracesdk.Severity
import io.embrace.android.embracesdk.fakes.FakeConfigService
import io.embrace.android.embracesdk.fakes.FakeLogWriter
import io.embrace.android.embracesdk.fakes.FakePayloadStore
import io.embrace.android.embracesdk.fakes.FakeSessionPropertiesService
import io.embrace.android.embracesdk.fakes.behavior.FakeLogMessageBehavior
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
Expand All @@ -14,6 +15,7 @@ import io.embrace.android.embracesdk.internal.config.behavior.REDACTED_LABEL
import io.embrace.android.embracesdk.internal.config.behavior.SensitiveKeysBehaviorImpl
import io.embrace.android.embracesdk.internal.config.remote.RemoteConfig
import io.embrace.android.embracesdk.internal.config.remote.SessionRemoteConfig
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment
import io.embrace.android.embracesdk.internal.payload.AppFramework
import io.opentelemetry.semconv.incubating.LogIncubatingAttributes
import org.junit.Assert.assertEquals
Expand All @@ -28,6 +30,7 @@ internal class EmbraceLogServiceTest {
private lateinit var fakeLogWriter: FakeLogWriter
private lateinit var fakeSessionPropertiesService: FakeSessionPropertiesService
private lateinit var fakeConfigService: FakeConfigService
private lateinit var payloadStore: FakePayloadStore

@Before
fun setUp() {
Expand All @@ -38,14 +41,15 @@ internal class EmbraceLogServiceTest {
)
fakeSessionPropertiesService = FakeSessionPropertiesService()
fakeLogWriter = FakeLogWriter()

payloadStore = FakePayloadStore()
logService = createEmbraceLogService()
}

private fun createEmbraceLogService() = EmbraceLogService(
logWriter = fakeLogWriter,
configService = fakeConfigService,
sessionPropertiesService = fakeSessionPropertiesService,
payloadStore = payloadStore,
)

@Test
Expand Down Expand Up @@ -237,4 +241,22 @@ internal class EmbraceLogServiceTest {
// then the correct number of error logs is returned
assertEquals(5, logService.getErrorLogsCount())
}

@Test
fun `log with attachment`() {
val bytes = ByteArray(2)
val msg = "message"
logService.log(
message = msg,
severity = Severity.INFO,
logExceptionType = LogExceptionType.NONE,
logAttachment = Attachment.EmbraceHosted(bytes) { true },
)

// then the sensitive key is redacted
val log = fakeLogWriter.logEvents.single()
assertEquals(msg, log.message)
val attachment = payloadStore.storedAttachments.single()
assertEquals(bytes, attachment.data.second)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,57 +73,34 @@ internal class AttachmentTest {

@Test
fun `create user hosted attachment`() {
val attachment = UserHosted(SIZE, ID, URL, counter)
val attachment = UserHosted(ID, URL, counter)
attachment.assertUserHostedAttributesMatch()
}

@Test
fun `user hosted attachment empty size`() {
val size: Long = 0
val attachment = UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size)
}

@Test
fun `user hosted attachment invalid size`() {
val size: Long = -1
val attachment = UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment invalid url`() {
val url = ""
val attachment = UserHosted(SIZE, ID, url, counter)
val attachment = UserHosted(ID, url, counter)
attachment.assertUserHostedAttributesMatch(url = url, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment invalid ID`() {
val id = "my-id"
val attachment = UserHosted(SIZE, id, URL, counter)
val attachment = UserHosted(id, URL, counter)
attachment.assertUserHostedAttributesMatch(id = id, errorCode = UNKNOWN)
}

@Test
fun `user hosted attachment has no max size constraints`() {
val size = 5000000L // 50MiB
val attachment = UserHosted(size, ID, URL, counter)
attachment.assertUserHostedAttributesMatch(size = size)
}

@Test
fun `user hosted attachment exceeds session limit`() {
var limit = true
val smallCounter: () -> Boolean = { limit }
val attachment = UserHosted(SIZE, ID, URL, smallCounter)
val attachment = UserHosted(ID, URL, smallCounter)
attachment.assertUserHostedAttributesMatch()

val size = -1L
limit = false
val limitedAttachment = UserHosted(size, ID, URL, smallCounter)
val limitedAttachment = UserHosted(ID, URL, smallCounter)
limitedAttachment.assertUserHostedAttributesMatch(
size = size,
errorCode = OVER_MAX_ATTACHMENTS
)
}
Expand All @@ -140,12 +117,10 @@ internal class AttachmentTest {
}

private fun UserHosted.assertUserHostedAttributesMatch(
size: Long = SIZE,
url: String = URL,
id: String = ID,
errorCode: AttachmentErrorCode? = null,
) {
assertEquals(size, checkNotNull(attributes[embAttachmentSize]).toLong())
assertEquals(id, checkNotNull(attributes[embAttachmentId]))
assertEquals(errorCode?.toString(), attributes[embAttachmentErrorCode])
assertEquals(url, checkNotNull(attributes[embAttachmentUrl]))
Expand Down
Loading

0 comments on commit f996835

Please sign in to comment.