Skip to content

Commit d91e11e

Browse files
author
Guilherme Souza
committed
Use Codable for encoding/decoding payload
1 parent 2bd56fb commit d91e11e

File tree

8 files changed

+156
-37
lines changed

8 files changed

+156
-37
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
Secrets.swift
2+
13
.DS_Store
24
/.build
35
/Packages

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let package = Package(
2222
]),
2323
.testTarget(
2424
name: "SupabaseLoggerTests",
25-
dependencies: ["SupabaseLogger"]),
25+
dependencies: ["SupabaseLogger"],
26+
exclude: ["_Secrets.swift"]),
2627
]
2728
)

Sources/SupabaseLogger/LogEntry.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
import Logging
3+
4+
struct LogEntry: Codable {
5+
let label: String
6+
let file: String
7+
let line: String
8+
let source: String
9+
let function: String
10+
let level: String
11+
let message: String
12+
let loggedAt: Date
13+
let metadata: Logger.Metadata?
14+
}
15+
16+
extension Logger.MetadataValue: Codable {
17+
18+
public init(from decoder: Decoder) throws {
19+
if let string = try? decoder.singleValueContainer().decode(String.self) {
20+
self = .string(string)
21+
} else if let dictionary = try? decoder.singleValueContainer().decode(Logger.Metadata.self) {
22+
self = .dictionary(dictionary)
23+
} else if let array = try? decoder.singleValueContainer().decode([Logger.MetadataValue].self) {
24+
self = .array(array)
25+
} else {
26+
throw DecodingError.dataCorrupted(
27+
DecodingError.Context(
28+
codingPath: decoder.codingPath,
29+
debugDescription: "Unsupported \(Logger.MetadataValue.self) type."))
30+
}
31+
}
32+
33+
public func encode(to encoder: Encoder) throws {
34+
var container = encoder.singleValueContainer()
35+
switch self {
36+
case .string(let value):
37+
try container.encode(value)
38+
case .stringConvertible(let value):
39+
try container.encode(value.description)
40+
case .dictionary(let value):
41+
try container.encode(value)
42+
case .array(let value):
43+
try container.encode(value)
44+
}
45+
}
46+
}
47+
48+
private let dateFormatter = { () -> DateFormatter in
49+
let formatter = DateFormatter()
50+
formatter.locale = Locale(identifier: "en_US_POSIX")
51+
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
52+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
53+
return formatter
54+
}()
55+
56+
let decoder = { () -> JSONDecoder in
57+
let decoder = JSONDecoder()
58+
decoder.dateDecodingStrategy = .formatted(dateFormatter)
59+
decoder.keyDecodingStrategy = .convertFromSnakeCase
60+
return decoder
61+
}()
62+
63+
let encoder = { () -> JSONEncoder in
64+
let encoder = JSONEncoder()
65+
encoder.dateEncodingStrategy = .formatted(dateFormatter)
66+
encoder.keyEncodingStrategy = .convertToSnakeCase
67+
return encoder
68+
}()
Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import Foundation
22

3-
final class LogsCache {
3+
final class LogsCache<T: Codable> {
44

5+
private let isDebug: Bool
56
private let maximumNumberOfLogsToPopAtOnce = 100
67

78
private let queue = DispatchQueue(
89
label: "co.binaryscraping.supabase-log-cache", attributes: .concurrent)
9-
private var cachedLogs: [[String: Any]] = []
10+
private var cachedLogs: [T] = []
1011

11-
func push(_ log: [String: Any]) {
12+
func push(_ log: T) {
1213
queue.sync { self.cachedLogs.append(log) }
1314
}
1415

15-
func push(_ logs: [[String: Any]]) {
16+
func push(_ logs: [T]) {
1617
queue.sync { self.cachedLogs.append(contentsOf: logs) }
1718
}
1819

19-
func pop() -> [[String: Any]] {
20-
var poppedLogs: [[String: Any]] = []
20+
func pop() -> [T] {
21+
var poppedLogs: [T] = []
2122
queue.sync(flags: .barrier) {
2223
let sliceSize = min(maximumNumberOfLogsToPopAtOnce, cachedLogs.count)
2324
poppedLogs = Array(cachedLogs[..<sliceSize])
@@ -33,27 +34,32 @@ final class LogsCache {
3334
try data.write(to: LogsCache.fileURL())
3435
self.cachedLogs = []
3536
} catch {
36-
print("Error saving Logs cache.")
37+
if isDebug {
38+
print("Error saving Logs cache.")
39+
}
3740
}
3841
}
3942
}
4043

41-
private static func fileURL() -> URL {
42-
try! FileManager.default.url(
44+
private static func fileURL() throws -> URL {
45+
try FileManager.default.url(
4346
for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false
4447
)
4548
.appendingPathComponent("supabase-log-cache")
4649
}
4750

48-
init() {
51+
init(isDebug: Bool) {
52+
self.isDebug = isDebug
4953
do {
5054
let data = try Data(contentsOf: LogsCache.fileURL())
5155
try FileManager.default.removeItem(at: LogsCache.fileURL())
5256

53-
let logs = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] ?? []
57+
let logs = try decoder.decode([T].self, from: data)
5458
self.cachedLogs = logs
5559
} catch {
56-
print("Error recovering logs from cache.")
60+
if isDebug {
61+
print("Error recovering logs from cache.")
62+
}
5763
}
5864
}
5965
}

Sources/SupabaseLogger/SupabaseLogHandler.swift

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,38 @@ public struct SupabaseLogConfig: Hashable {
3232
}
3333

3434
public struct SupabaseLogHandler: LogHandler {
35-
3635
public var metadata: Logger.Metadata = [:]
37-
3836
public var logLevel: Logger.Level = .debug
3937

4038
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
4139
get { metadata[key] }
4240
set { metadata[key] = newValue }
4341
}
4442

43+
private let label: String
4544
private let logManager: SupabaseLogManager
4645

47-
public init(config: SupabaseLogConfig) {
46+
public init(label: String, config: SupabaseLogConfig) {
47+
self.label = label
4848
logManager = SupabaseLogManager.shared(config)
4949
}
5050

5151
public func log(
5252
level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String,
5353
file: String, function: String, line: UInt
5454
) {
55-
var parameters = self.metadata
56-
if let metadata = metadata {
57-
parameters.merge(metadata) { _, new in new }
58-
}
59-
parameters["file"] = .string(file)
60-
parameters["line"] = .stringConvertible(line)
61-
parameters["source"] = .string(source)
62-
parameters["function"] = .string(function)
63-
64-
var payload: [String: Any] = [:]
65-
payload["level"] = level.rawValue
66-
payload["message"] = metadata?.description
67-
payload["metadata"] = parameters.mapValues(\.description)
55+
let entry = LogEntry(
56+
label: label, file: file, line: "\(line)", source: source, function: function,
57+
level: level.rawValue, message: message.description, loggedAt: Date(),
58+
metadata: self.metadata.merging(metadata ?? [:]) { $1 })
6859

69-
logManager.log(payload)
60+
logManager.log(entry)
7061
}
7162
}
7263

7364
final class SupabaseLogManager {
7465

75-
let cache: LogsCache
66+
let cache: LogsCache<LogEntry>
7667
let config: SupabaseLogConfig
7768

7869
private let minimumWaitTimeBetweenRequests: TimeInterval = 10
@@ -96,7 +87,7 @@ final class SupabaseLogManager {
9687

9788
private init(config: SupabaseLogConfig) {
9889
self.config = config
99-
self.cache = LogsCache()
90+
self.cache = LogsCache(isDebug: config.isDebug)
10091

10192
#if os(macOS)
10293
NotificationCenter.default.addObserver(
@@ -127,7 +118,7 @@ final class SupabaseLogManager {
127118
checkForLogsAndSend()
128119
}
129120

130-
func log(_ payload: [String: Any]) {
121+
func log(_ payload: LogEntry) {
131122
cache.push(payload)
132123
}
133124

@@ -137,7 +128,7 @@ final class SupabaseLogManager {
137128

138129
guard !logs.isEmpty else { return }
139130

140-
let data = try! JSONSerialization.data(withJSONObject: logs)
131+
let data = try! encoder.encode(logs)
141132
guard
142133
let url = URL(string: self.config.supabaseURL)?.appendingPathComponent(self.config.table)
143134
else {
@@ -147,6 +138,8 @@ final class SupabaseLogManager {
147138
var request = URLRequest(url: url)
148139
request.httpMethod = "POST"
149140
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
141+
request.setValue(config.supabaseAnonKey, forHTTPHeaderField: "apikey")
142+
request.setValue("Bearer \(config.supabaseAnonKey)", forHTTPHeaderField: "Authorization")
150143
request.httpBody = data
151144

152145
let config = URLSessionConfiguration.default
Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,48 @@
1+
import Logging
12
import XCTest
23

34
@testable import SupabaseLogger
45

56
final class SupabaseLogHandlerTests: XCTestCase {
6-
func testExample() throws {
7+
func testLive() throws {
8+
LoggingSystem.bootstrap { label in
9+
MultiplexLogHandler(
10+
[
11+
StreamLogHandler.standardOutput(label: label),
12+
SupabaseLogHandler(
13+
label: label,
14+
config: SupabaseLogConfig(
15+
supabaseURL: Secrets.supabaseURL, supabaseAnonKey: Secrets.supabaseAnonKey,
16+
isDebug: true)
17+
),
18+
]
19+
)
20+
}
21+
22+
let logger = Logger(label: "co.binaryscraping.supabase-log.tests")
23+
let metadata: Logger.Metadata = [
24+
"string": .string("string"),
25+
"integer": .stringConvertible(1),
26+
"array": .array([.string("a"), .string("b")]),
27+
"dictionary": .dictionary([
28+
"key": .string("value")
29+
]),
30+
]
31+
32+
logger.critical("This is a critical message", metadata: metadata)
33+
logger.debug("This is a debug message", metadata: metadata)
34+
logger.error("This is an error message", metadata: metadata)
35+
logger.info("This is an info message", metadata: metadata)
36+
logger.notice("This is a notice message", metadata: metadata)
37+
logger.trace("This is a trace message", metadata: metadata)
38+
logger.warning("This is a warning message", metadata: metadata)
39+
40+
let expectation = self.expectation(description: #function)
41+
42+
DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
43+
expectation.fulfill()
44+
}
45+
46+
wait(for: [expectation], timeout: 20)
747
}
848
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
enum Secrets {
2+
static let supabaseURL = "URL"
3+
static let supabaseAnonKey = "KEY"
4+
}

supabase-init.sql

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
create table logs (
22
id uuid default uuid_generate_v4() not null primary key,
3+
label text not null,
4+
file text not null,
5+
line text not null,
6+
source text not null,
7+
function text not null,
38
level text not null,
4-
message text,
9+
message text not null,
510
logged_at timestamp with time zone not null,
611
received_at timestamp with time zone default timezone('utc'::text, now()) not null,
7-
metadata jsonb not null
12+
metadata jsonb
813
);

0 commit comments

Comments
 (0)