Skip to content

Commit

Permalink
Add first implementation of Sentry logger
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski committed Feb 16, 2024
1 parent d82d0fc commit 74b1c44
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
91 changes: 91 additions & 0 deletions Sources/ExtendedLogging/SentryFormatter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation
import Logging

public class SentryFormatter: LogFormatter {
public var metadata = Logger.Metadata() {
didSet {
self.prettyMetadata = self.prettify(self.metadata)
}
}

private var prettyMetadata: String?
private let jsonEncoder: JSONEncoder
private let newLine = "\n".data(using: .utf8)!

public init() {
self.jsonEncoder = JSONEncoder()
self.jsonEncoder.dateEncodingStrategy = JSONEncoder.DateEncodingStrategy.iso8601
}

public func format(label: String,
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
file: String,
function: String,
line: UInt) throws -> Data? {

// TODO: Save metadata as additional fields.
// let prettyMetadata = metadata?.isEmpty ?? true
// ? self.prettyMetadata
// : self.prettify(self.metadata.merging(metadata!, uniquingKeysWith: { _, new in new }))
// let messageLine = "(\(prettyMetadata.map { ", \($0)" } ?? "")): \(message)\n"

let eventId = UUID().uuidString // self.createRandomString(length: 32)
let currentDate = Date()

let header = EnvelopeHeader(eventId: eventId,
sentAt: currentDate,
sdk: EnvelopeSdk(name: "dev.mczachurski.mikroservices.extended-logging", version: "1.0.0"))

let type = EnvelopeType()
let content = self.getEnvelopeContent(level: level,
message: message.description,
eventId: eventId,
currentDate: currentDate,
fileName: file,
function: function,
line: line)

let headerData = try jsonEncoder.encode(header)
let typeData = try jsonEncoder.encode(type)
let contentData = try jsonEncoder.encode(content)

let data = headerData + newLine + typeData + newLine + contentData
return data
}

private func prettify(_ metadata: Logger.Metadata) -> String? {
return !metadata.isEmpty ? metadata.map { "\($0)=\($1)" }.joined(separator: " ") : nil
}

private func getEnvelopeContent(level: Logger.Level, message: String, eventId: String, currentDate: Date, fileName: String, function: String, line: UInt) -> EnvelopeContent {
if level == Logger.Level.critical || level == Logger.Level.error {
let lastFileName = String(fileName.split(separator: "/").last ?? "")

return EnvelopeContent(level: level,
eventId: eventId,
platform: "swift",
timestamp: currentDate.timeIntervalSince1970,
environment: "production",
message: nil,
exception: EnvelopeErrorValues(values: [
EnvelopeErrorValue(type: "Error",
value: message,
stackTrace: EnvelopeFrames(frames: [
EnvelopeFrame(fileName: lastFileName,
function: function,
lineNumber: line)
]))
]))
} else {
return EnvelopeContent(level: level,
eventId: eventId,
platform: "swift",
timestamp: currentDate.timeIntervalSince1970,
environment: "production",
message: message,
exception: nil)
}
}
}
60 changes: 60 additions & 0 deletions Sources/ExtendedLogging/SentryLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import Logging

public struct SentryLogger: LogHandler {

public var metadata = Logger.Metadata() {
didSet {
self.logFormatter.metadata = self.metadata
}
}

public var logLevel: Logger.Level = .info
private let sentryWriter: SentryWriter
private var logFormatter: LogFormatter

public init(label: String,
dsn: String,
level: Logger.Level = .debug,
metadata: Logger.Metadata = [:]) {
self.label = label
self.logLevel = level
self.metadata = metadata
self.logFormatter = SentryFormatter()
self.sentryWriter = SentryWriter(dsn: dsn)
}

public let label: String

public func log(level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
file: String,
function: String,
line: UInt) {

let message = try? self.logFormatter.format(label: self.label,
level: level,
message: message,
metadata: metadata,
file: file,
function: function,
line: line)

self.sentryWriter.write(message: message)
}

public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get {
return self.metadata[metadataKey]
}
set {
self.metadata[metadataKey] = newValue
}
}

// for testing
public func wait() -> Void {
// fileWriter.wait()
}
}
22 changes: 22 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import Logging

internal struct EnvelopeContent: Encodable {
let level: Logger.Level
let eventId: String
let platform: String
let timestamp: Double
let environment: String
let message: String?
let exception: EnvelopeErrorValues?

enum CodingKeys: String, CodingKey {
case level
case eventId = "event_id"
case platform
case timestamp
case environment
case message
case exception
}
}
17 changes: 17 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeErrorValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

internal struct EnvelopeErrorValues: Encodable {
let values: [EnvelopeErrorValue]
}

internal struct EnvelopeErrorValue: Encodable {
let type: String
let value: String
let stackTrace: EnvelopeFrames

enum CodingKeys: String, CodingKey {
case type
case value
case stackTrace = "stacktrace"
}
}
17 changes: 17 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeFrame.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

internal struct EnvelopeFrames: Encodable {
let frames: [EnvelopeFrame]
}

internal struct EnvelopeFrame: Encodable {
let fileName: String
let function: String
let lineNumber: UInt

enum CodingKeys: String, CodingKey {
case fileName = "filename"
case function
case lineNumber = "lineno"
}
}
13 changes: 13 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeHeader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

internal struct EnvelopeHeader: Encodable {
let eventId: String
let sentAt: Date
let sdk: EnvelopeSdk

enum CodingKeys: String, CodingKey {
case eventId = "event_id"
case sentAt = "sent_at"
case sdk
}
}
6 changes: 6 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeSdk.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

internal struct EnvelopeSdk: Encodable {
let name: String
let version: String
}
5 changes: 5 additions & 0 deletions Sources/ExtendedLogging/SentryModels/EnvelopeType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

internal struct EnvelopeType: Encodable {
let type = "event"
}
59 changes: 59 additions & 0 deletions Sources/ExtendedLogging/SentryWriter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation

internal class SentryWriter {
private let dsn: String;
private let requestQueue = DispatchQueue.init(label: "SentryWriter", qos: .utility)

init(dsn: String) {
self.dsn = dsn
}

func write(message: Data?) {
guard let data = message else {
print("[SentryWriter] Logged message cannot be nil.")
return
}

guard let url = self.convertToUrl() else {
print("[SentryWriter] Cannot parse Sentry DSN url.")
return
}

self.sendEvent(url: url, data: data)
}

private func sendEvent(url: URL, data: Data) {
requestQueue.async {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.httpBody = data

let task = URLSession.shared.dataTask(with: urlRequest)
task.resume()
}
}

private func convertToUrl() -> URL? {
let dsnUrl = URLComponents(string: self.dsn)

guard let scheme = dsnUrl?.scheme else {
return nil
}

guard let host = dsnUrl?.host else {
return nil
}

guard let key = dsnUrl?.user else {
return nil
}

guard let path = dsnUrl?.path else {
return nil
}

let url = "\(scheme)://\(host)/api\(path)/envelope/?sentry_key=\(key)&sentry_version=7&sentry_client=dev.mczachurski.mikroservices.extended-logging%2F1.0.0"

return URL(string: url)
}
}

0 comments on commit 74b1c44

Please sign in to comment.