Skip to content

Commit

Permalink
OTel Logs with file attachments (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoEmbrace authored Jan 21, 2025
1 parent a24dc6e commit 7e3ea2c
Show file tree
Hide file tree
Showing 33 changed files with 767 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DefaultDispatchableQueue: DispatchableQueue {
public func sync(execute block: () -> Void) {
queue.sync(execute: block)
}

public static func with(label: String) -> DispatchableQueue {
DefaultDispatchableQueue(queue: .init(label: label))
}
Expand Down
9 changes: 7 additions & 2 deletions Sources/EmbraceCore/Internal/Embrace+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,17 @@ extension Embrace {

let baseUrl = EMBDevice.isDebuggerAttached ? endpoints.developmentBaseURL : endpoints.baseURL
guard let spansURL = URL.spansEndpoint(basePath: baseUrl),
let logsURL = URL.logsEndpoint(basePath: baseUrl) else {
let logsURL = URL.logsEndpoint(basePath: baseUrl),
let attachmentsURL = URL.attachmentsEndpoint(basePath: baseUrl) else {
Embrace.logger.error("Failed to initialize endpoints!")
return nil
}

let uploadEndpoints = EmbraceUpload.EndpointOptions(spansURL: spansURL, logsURL: logsURL)
let uploadEndpoints = EmbraceUpload.EndpointOptions(
spansURL: spansURL,
logsURL: logsURL,
attachmentsURL: attachmentsURL
)

// cache
guard let cacheUrl = EmbraceFileSystem.uploadsDirectoryPath(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class EmbraceLogAttributesBuilder {
session ?? sessionControllable?.currentSession
}

init(storage: EmbraceStorageMetadataFetcher,
init(storage: EmbraceStorageMetadataFetcher?,
sessionControllable: SessionControllable,
initialAttributes: [String: String]) {
self.storage = storage
Expand Down
99 changes: 99 additions & 0 deletions Sources/EmbraceCore/Internal/Logs/LogController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,39 @@ import EmbraceUploadInternal
import EmbraceCommonInternal
import EmbraceSemantics
import EmbraceConfigInternal
import EmbraceOTelInternal

protocol LogControllable: LogBatcherDelegate {
func uploadAllPersistedLogs()
func createLog(
_ message: String,
severity: LogSeverity,
type: LogType,
timestamp: Date,
attachment: Data?,
attachmentId: String?,
attachmentUrl: URL?,
attachmentSize: Int?,
attributes: [String: String],
stackTraceBehavior: StackTraceBehavior
)
}

class LogController: LogControllable {
private(set) weak var sessionController: SessionControllable?
private weak var storage: Storage?
private weak var upload: EmbraceLogUploader?
private weak var config: EmbraceConfig?

var otel: EmbraceOTelBridge = EmbraceOTel() // var so we can inject a mock for testing

/// This will probably be injected eventually.
/// For consistency, I created a constant
static let maxLogsPerBatch: Int = 20

static let attachmentLimit: Int = 5
static let attachmentSizeLimit: Int = 1048576 // 1 MiB

private var isSDKEnabled: Bool {
guard let config = config else {
return true
Expand Down Expand Up @@ -53,6 +72,86 @@ class LogController: LogControllable {
try? storage.removeAllLogs()
}
}

public func createLog(
_ message: String,
severity: LogSeverity,
type: LogType = .message,
timestamp: Date = Date(),
attachment: Data? = nil,
attachmentId: String? = nil,
attachmentUrl: URL? = nil,
attachmentSize: Int? = nil,
attributes: [String: String] = [:],
stackTraceBehavior: StackTraceBehavior = .default
) {
guard let sessionController = sessionController else {
return
}

// generate attributes
let attributesBuilder = EmbraceLogAttributesBuilder(
storage: storage,
sessionControllable: sessionController,
initialAttributes: attributes
)

/*
If we want to keep this method cleaner, we could move this log to `EmbraceLogAttributesBuilder`
However that would cause to always add a frame to the stacktrace.
*/
if stackTraceBehavior == .default && (severity == .warn || severity == .error) {
let stackTrace: [String] = Thread.callStackSymbols
attributesBuilder.addStackTrace(stackTrace)
}

var finalAttributes = attributesBuilder
.addLogType(type)
.addApplicationState()
.addApplicationProperties()
.addSessionIdentifier()
.build()

// handle attachment data
if let attachment = attachment {

sessionController.increaseAttachmentCount()

let id = UUID().withoutHyphen
finalAttributes[LogSemantics.keyAttachmentId] = id

let size = attachment.count
finalAttributes[LogSemantics.keyAttachmentSize] = String(size)

// check attachment count limit
if sessionController.attachmentCount >= Self.attachmentLimit {
finalAttributes[LogSemantics.keyAttachmentErrorCode] = LogSemantics.attachmentLimitReached

// check attachment size limit
} else if size > Self.attachmentSizeLimit {
finalAttributes[LogSemantics.keyAttachmentErrorCode] = LogSemantics.attachmentTooLarge
}

// upload attachment
else {
upload?.uploadAttachment(id: id, data: attachment, completion: nil)
}
}

// handle pre-uploaded attachment
else if let attachmentId = attachmentId,
let attachmentUrl = attachmentUrl {

finalAttributes[LogSemantics.keyAttachmentId] = attachmentId
finalAttributes[LogSemantics.keyAttachmentUrl] = attachmentUrl.absoluteString

if let attachmentSize = attachmentSize {
finalAttributes[LogSemantics.keyAttachmentSize] = String(attachmentSize)
}
}

otel.log(message, severity: severity, timestamp: timestamp, attributes: finalAttributes)
}
}

extension LogController {
Expand Down
103 changes: 81 additions & 22 deletions Sources/EmbraceCore/Public/Embrace+OTel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension Embrace: EmbraceOpenTelemetry {
)
}

private var otel: EmbraceOTel { EmbraceOTel() }
var otel: EmbraceOTel { EmbraceOTel() }

/// - Parameters:
/// - instrumentationName: The name of the instrumentation library requesting the tracer.
Expand Down Expand Up @@ -118,6 +118,7 @@ extension Embrace: EmbraceOpenTelemetry {
/// - message: Body of the log.
/// - severity: `LogSeverity` for the log.
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
public func log(
_ message: String,
severity: LogSeverity,
Expand All @@ -141,37 +142,95 @@ extension Embrace: EmbraceOpenTelemetry {
/// - severity: `LogSeverity` for the log.
/// - timestamp: Timestamp for the log.
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
public func log(
_ message: String,
severity: LogSeverity,
type: LogType = .message,
timestamp: Date,
attributes: [String: String],
attributes: [String: String] = [:],
stackTraceBehavior: StackTraceBehavior = .default
) {
let attributesBuilder = EmbraceLogAttributesBuilder(
storage: storage,
sessionControllable: sessionController,
initialAttributes: attributes
logController.createLog(
message,
severity: severity,
type: type,
timestamp: timestamp,
attachment: nil,
attachmentId: nil,
attachmentUrl: nil,
attachmentSize: nil,
attributes: attributes,
stackTraceBehavior: stackTraceBehavior
)
}

/*
If we want to keep this method cleaner, we could move this log to `EmbraceLogAttributesBuilder`
However that would cause to always add a frame to the stacktrace.
*/
if stackTraceBehavior == .default && (severity == .warn || severity == .error) {
let stackTrace: [String] = Thread.callStackSymbols
attributesBuilder.addStackTrace(stackTrace)
}

let finalAttributes = attributesBuilder
.addLogType(type)
.addApplicationState()
.addApplicationProperties()
.addSessionIdentifier()
.build()
/// Creates and adds a log with the given data as an attachment for the current session span.
/// The attachment will be hosted by Embrace and will be accessible through the dashboard.
/// - Parameters:
/// - message: Body of the log.
/// - severity: `LogSeverity` for the log.
/// - timestamp: Timestamp for the log.
/// - attachment: Data of the attachment
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
public func log(
_ message: String,
severity: LogSeverity,
type: LogType = .message,
timestamp: Date = Date(),
attachment: Data,
attributes: [String: String] = [:],
stackTraceBehavior: StackTraceBehavior = .default
) {
logController.createLog(
message,
severity: severity,
type: type,
timestamp: timestamp,
attachment: attachment,
attachmentId: nil,
attachmentUrl: nil,
attachmentSize: nil,
attributes: attributes,
stackTraceBehavior: stackTraceBehavior
)
}

otel.log(message, severity: severity, attributes: finalAttributes)
/// Creates and adds a log with the given attachment info for the current session span.
/// Use this method for attachments hosted outside of Embrace.
/// - Parameters:
/// - message: Body of the log.
/// - severity: `LogSeverity` for the log.
/// - timestamp: Timestamp for the log.
/// - attachmentId: Identifier of the attachment
/// - attachmentUrl: URL to dowload the attachment data
/// - attachmentSize: Size of the attachment (optional)
/// - attributes: Attributes for the log.
/// - stackTraceBehavior: Defines if the stack trace information should be added to the log
public func log(
_ message: String,
severity: LogSeverity,
type: LogType = .message,
timestamp: Date = Date(),
attachmentId: String,
attachmentUrl: URL,
attachmentSize: Int? = nil,
attributes: [String: String],
stackTraceBehavior: StackTraceBehavior = .default
) {
logController.createLog(
message,
severity: severity,
type: type,
timestamp: timestamp,
attachment: nil,
attachmentId: attachmentId,
attachmentUrl: attachmentUrl,
attachmentSize: attachmentSize,
attributes: attributes,
stackTraceBehavior: stackTraceBehavior
)
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/EmbraceCore/Session/SessionControllable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ protocol SessionControllable: AnyObject {

func update(state: SessionState)
func update(appTerminated: Bool)

var attachmentCount: Int { get }
func increaseAttachmentCount()
}
10 changes: 9 additions & 1 deletion Sources/EmbraceCore/Session/SessionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class SessionController: SessionControllable {
@ThreadSafe
private(set) var currentSessionSpan: Span?

@ThreadSafe
private(set) var attachmentCount: Int = 0

// Lock used for session boundaries. Will be shared at both start/end of session
private let lock = UnfairLock()

Expand Down Expand Up @@ -138,6 +141,7 @@ class SessionController: SessionControllable {
NotificationCenter.default.post(name: .embraceSessionDidStart, object: session)

firstSession = false
attachmentCount = 0

return session
}
Expand Down Expand Up @@ -222,6 +226,10 @@ class SessionController: SessionControllable {
UnsentDataHandler.sendSession(session, storage: storage, upload: upload)
}
}

func increaseAttachmentCount() {
attachmentCount += 1
}
}

extension SessionController {
Expand Down Expand Up @@ -253,7 +261,7 @@ extension SessionController {
currentSession = nil
currentSessionSpan = nil
}

private var isSDKEnabled: Bool {
guard let config = config else {
return true
Expand Down
4 changes: 4 additions & 0 deletions Sources/EmbraceCore/Utils/URL+Embrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ extension URL {
static func logsEndpoint(basePath: String) -> URL? {
return endpoint(basePath: basePath, apiPath: "/v2/logs")
}

static func attachmentsEndpoint(basePath: String) -> URL? {
return endpoint(basePath: basePath, apiPath: "/v2/attachments")
}
}
Loading

0 comments on commit 7e3ea2c

Please sign in to comment.