Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
01286eb
poc: file attachment api
fractalwrench Jan 13, 2025
0bf1add
file attachment api
fractalwrench Jan 13, 2025
2ecb573
Merge pull request #1814 from embrace-io/user-hosted-attachment
fractalwrench Jan 14, 2025
dba3ebd
hosted attachment work
fractalwrench Jan 14, 2025
2c782cc
Merge pull request #1817 from embrace-io/hosted-attachment-parts
fractalwrench Jan 15, 2025
76f1340
wip: support embrace hosted attachments
fractalwrench Jan 14, 2025
19e6abc
api: remove size parameter from user hosted log messages
fractalwrench Jan 16, 2025
606090a
test: add integration tests for file attachments
fractalwrench Jan 16, 2025
62f5d66
Merge pull request #1822 from embrace-io/extra-test-cases
fractalwrench Jan 17, 2025
244ace1
Merge pull request #1821 from embrace-io/tweak-api
fractalwrench Jan 17, 2025
f996835
Merge pull request #1816 from embrace-io/embrace-hosted-attachments
fractalwrench Jan 17, 2025
eb9e17e
tweak file attachment implementation
fractalwrench Jan 17, 2025
0cf563c
Merge pull request #1823 from embrace-io/testing-tweaks
fractalwrench Jan 17, 2025
e67f90f
Merge branch 'main' into file-attachment-api
fractalwrench Jan 27, 2025
9095060
Merge branch 'main' into file-attachment-api
fractalwrench Jan 29, 2025
934b58a
Merge branch 'main' into file-attachment-api
fractalwrench Jan 29, 2025
27060c1
testing tweaks
fractalwrench Jan 16, 2025
94eb41d
Merge pull request #1828 from embrace-io/file-attachment-example
fractalwrench Jan 29, 2025
64544c2
Merge branch 'main' into file-attachment-api
fractalwrench Jan 29, 2025
ad53a57
remove attachment example temporarily
fractalwrench Jan 29, 2025
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
2 changes: 2 additions & 0 deletions embrace-android-api/api/embrace-android-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ 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;)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 @@ -34,6 +34,42 @@ public interface LogsApi {
properties: Map<String, Any>?,
)

/**
* Remotely logs a message at the given severity level with an attachment. These log messages will appear
* as part of the session timeline, and can be used to describe what was happening at a particular
* time within the app.
*
* @param message the message to remotely log
* @param severity the severity level of the log message
* @param properties the properties to attach to the log message
* @param attachment an attachment to include with the log message. This must be < 1MB in size.
*/
public fun logMessage(
message: String,
severity: Severity,
properties: Map<String, Any>?,
attachment: ByteArray,
)

/**
* Remotely logs a message at the given severity level with an attachment that has been persisted on a 3rd party
* hosting solution. These log messages will appear as part of the session timeline, and can be used to
* describe what was happening at a particular time within the app.
*
* @param message the message to remotely log
* @param severity the severity level of the log message
* @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
*/
public fun logMessage(
message: String,
severity: Severity,
properties: Map<String, Any>?,
attachmentId: String,
attachmentUrl: String,
)

/**
* Remotely logs a message at INFO level. These log messages will appear as part of the session
* timeline, and can be used to describe what was happening at a particular time within the app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.logs.LogOrchestrator
import io.embrace.android.embracesdk.internal.logs.LogService
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentService
import io.embrace.android.embracesdk.internal.network.logging.NetworkCaptureDataSource
import io.embrace.android.embracesdk.internal.network.logging.NetworkCaptureService
import io.embrace.android.embracesdk.internal.network.logging.NetworkLoggingService
Expand All @@ -15,4 +16,5 @@ interface LogModule {
val networkLoggingService: NetworkLoggingService
val logService: LogService
val logOrchestrator: LogOrchestrator
val attachmentService: AttachmentService
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.embrace.android.embracesdk.internal.logs.EmbraceLogService
import io.embrace.android.embracesdk.internal.logs.LogOrchestrator
import io.embrace.android.embracesdk.internal.logs.LogOrchestratorImpl
import io.embrace.android.embracesdk.internal.logs.LogService
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentService
import io.embrace.android.embracesdk.internal.network.logging.EmbraceDomainCountLimiter
import io.embrace.android.embracesdk.internal.network.logging.EmbraceNetworkCaptureService
import io.embrace.android.embracesdk.internal.network.logging.EmbraceNetworkLoggingService
Expand Down Expand Up @@ -61,7 +62,8 @@ internal class LogModuleImpl(
EmbraceLogService(
essentialServiceModule.logWriter,
configModule.configService,
essentialServiceModule.sessionPropertiesService
essentialServiceModule.sessionPropertiesService,
deliveryModule.payloadStore,
)
}

Expand All @@ -74,4 +76,8 @@ internal class LogModuleImpl(
payloadSourceModule.logEnvelopeSource,
)
}

override val attachmentService: AttachmentService by singleton {
AttachmentService()
}
}
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
@@ -1,83 +1,80 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import io.embrace.android.embracesdk.internal.arch.schema.EmbraceAttributeKey
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.ATTACHMENT_TOO_LARGE
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.OVER_MAX_ATTACHMENTS
import io.embrace.android.embracesdk.internal.logs.attachments.AttachmentErrorCode.UNKNOWN
import io.embrace.android.embracesdk.internal.opentelemetry.embAttachmentErrorCode
import io.embrace.android.embracesdk.internal.opentelemetry.embAttachmentId
import io.embrace.android.embracesdk.internal.opentelemetry.embAttachmentSize
import io.embrace.android.embracesdk.internal.opentelemetry.embAttachmentUrl
import io.embrace.android.embracesdk.internal.utils.toNonNullMap
import java.util.UUID

/**
* Holds attributes that describe an attachment to a log record.
*/
internal sealed class Attachment(
val size: Long,
val id: String,
val counter: AttachmentCounter,
) {
sealed class Attachment(val id: String) {

internal companion object {
const val ATTR_KEY_SIZE = "emb.attachment_size"
const val ATTR_KEY_URL = "emb.attachment_url"
const val ATTR_KEY_ID = "emb.attachment_id"
const val ATTR_KEY_ERR_CODE = "emb.attachment_error_code"
private const val LIMIT_MB = 1 * 1024 * 1024
}

abstract val attributes: Map<String, String>
abstract val attributes: Map<EmbraceAttributeKey, String>
abstract val errorCode: AttachmentErrorCode?

protected fun constructAttributes(
size: Long,
id: String,
errorCode: AttachmentErrorCode? = null
) = mapOf(
ATTR_KEY_SIZE to size.toString(),
ATTR_KEY_ID to id,
ATTR_KEY_ERR_CODE to errorCode?.name
errorCode: AttachmentErrorCode? = null,

Check warning on line 28 in embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/logs/attachments/Attachment.kt

View check run for this annotation

Codecov / codecov/patch

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/logs/attachments/Attachment.kt#L28

Added line #L28 was not covered by tests
): Map<EmbraceAttributeKey, String> = mapOf(
embAttachmentId to id,
embAttachmentErrorCode to errorCode?.name
).toNonNullMap()

/**
* An attachment that is uploaded to Embrace's backend.
*/
class EmbraceHosted(
val bytes: ByteArray,
counter: AttachmentCounter,
counter: () -> Boolean,
) : Attachment(
bytes.size.toLong(),
UUID.randomUUID().toString(),
counter
UUID.randomUUID().toString()
) {

private val errorCode: AttachmentErrorCode? = when {
!counter.incrementAndCheckAttachmentLimit() -> OVER_MAX_ATTACHMENTS
private val size: Long = bytes.size.toLong()

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

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

fun shouldAttemptUpload(): Boolean = errorCode == null
}

/**
* An attachment that is uploaded to a user-supplied backend.
*/
class UserHosted(
size: Long,
id: String,
val url: String,
counter: AttachmentCounter,
) : Attachment(size, id, counter) {
counter: () -> Boolean,
) : Attachment(id) {

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

override val attributes: Map<String, String> =
constructAttributes(size, id, errorCode).plus(
ATTR_KEY_URL 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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package io.embrace.android.embracesdk.internal.logs.attachments
/**
* Enumerates the states where an attachment could not be added to a log record.
*/
internal enum class AttachmentErrorCode {
enum class AttachmentErrorCode {
ATTACHMENT_TOO_LARGE,
UNSUCCESSFUL_UPLOAD,
OVER_MAX_ATTACHMENTS,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.embrace.android.embracesdk.internal.logs.attachments

import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.EmbraceHosted
import io.embrace.android.embracesdk.internal.logs.attachments.Attachment.UserHosted
import io.embrace.android.embracesdk.internal.session.MemoryCleanerListener
import java.util.concurrent.atomic.AtomicInteger

/**
* Counts the number of attachments that should be added to log records.
*/
class AttachmentService(private val limit: Int = 5) : MemoryCleanerListener {

fun createAttachment(attachment: ByteArray): EmbraceHosted =
EmbraceHosted(attachment, ::incrementAndCheckAttachmentLimit)

fun createAttachment(
attachmentId: String,
attachmentUrl: String,
): UserHosted = UserHosted(
attachmentId,
attachmentUrl,
::incrementAndCheckAttachmentLimit
)

private val count: AtomicInteger = AtomicInteger(0)

override fun cleanCollections() = count.set(0)

/**
* Increments the counter of attachments for this session and returns true if an attachment can be uploaded.
*/
private fun incrementAndCheckAttachmentLimit(): Boolean = count.incrementAndGet() <= limit
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,23 @@ val embFreeDiskBytes: EmbraceAttributeKey = EmbraceAttributeKey("disk_free_bytes
* Attribute name that identifies how a signal should be delivered to the Embrace backend
*/
val embSendMode: EmbraceAttributeKey = EmbraceAttributeKey(id = "send_mode", isPrivate = true)

/**
* The size of a log message attachment in bytes
*/
val embAttachmentSize: EmbraceAttributeKey = EmbraceAttributeKey("attachment_size")

/**
* The URL of a user-hosted log message attachment
*/
val embAttachmentUrl: EmbraceAttributeKey = EmbraceAttributeKey("attachment_url")

/**
* The ID of a user-hosted log message attachment
*/
val embAttachmentId: EmbraceAttributeKey = EmbraceAttributeKey("attachment_id")

/**
* The error code associated with a failed log message attachment
*/
val embAttachmentErrorCode: EmbraceAttributeKey = EmbraceAttributeKey("attachment_error_code")
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 @@ -28,6 +28,11 @@ interface PayloadStore : CrashTeardownHandler {
*/
fun storeLogPayload(envelope: Envelope<LogPayload>, attemptImmediateRequest: Boolean)

/**
* Stores a log attachment.
*/
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
* exist at one time.
Expand Down
Loading
Loading