diff --git a/.ruby-version b/.ruby-version index 1f78bde..d4315a5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1,2 +1,2 @@ -2.5.3 +3.1.3 diff --git a/Example/.ruby-version b/Example/.ruby-version new file mode 100644 index 0000000..ff365e0 --- /dev/null +++ b/Example/.ruby-version @@ -0,0 +1 @@ +3.1.3 diff --git a/Example/JustLog/AppDelegate.swift b/Example/JustLog/AppDelegate.swift index 3ca1498..5e9aff0 100644 --- a/Example/JustLog/AppDelegate.swift +++ b/Example/JustLog/AppDelegate.swift @@ -37,6 +37,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { forceSendLogs(application) } + private var logger: Logger! + func redactValues(message: String, loggerExeptionList: [ExceptionListResponse], matches: [NSTextCheckingResult]) -> String { var redactedLogMessage = message @@ -81,51 +83,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate { identifier = UIBackgroundTaskIdentifier.invalid }) - Logger.shared.forceSend { completionHandler in + logger.forceSend { completionHandler in application.endBackgroundTask(identifier) identifier = UIBackgroundTaskIdentifier.invalid } } private func setupLogger() { - let logger = Logger.shared let decoder = JSONDecoder() - - // custom keys - logger.logTypeKey = "logtype" - logger.appVersionKey = "app_version" - logger.iosVersionKey = "ios_version" - logger.deviceTypeKey = "ios_device" - - // file destination - logger.logFilename = "justeat-demo.log" - - // logstash destination - logger.logstashHost = "listener.logz.io" - logger.logstashPort = 5052 - logger.logstashTimeout = 5 - logger.logLogstashSocketActivity = true - - - let regexRuleList = "[{\"pattern\": \"(name) = \\\\\\\\*\\\"(.*?[^\\\\\\\\]+)\", \"minimumLogLevel\": \"warning\"}, {\"pattern\": \"(token) = \\\\\\\\*\\\"(.*?[^\\\\\\\\]+)\", \"minimumLogLevel\": \"warning\"}]".data(using: .utf8) - let sanitizationExeceptionList = "[{\"value\": \"Dan Jones\"}, {\"value\": \"Jack Jones\"}]".data(using: .utf8) - + + let configuration = Configuration( + logFilename: "justeat-demo.log", + logstashHost: "listener.logz.io", + logstashPort: 5052, + logstashTimeout: 5, + logLogstashSocketActivity: true + ) + + logger = Logger( + configuration: configuration, + logMessageFormatter: JSONStringLogMessageFormatter(keys: FormatterKeys()) + ) + + let regexRuleList = "[{\"pattern\": \"(name) = \\\\\\\\*\\\"(.*?[^\\\\\\\\]+)\", \"minimumLogLevel\": \"warning\"}, {\"pattern\": \"(token) = \\\\\\\\*\\\"(.*?[^\\\\\\\\]+)\", \"minimumLogLevel\": \"warning\"}]".data(using: .utf8) + let sanitizationExeceptionList = "[{\"value\": \"Dan Jones\"}, {\"value\": \"Jack Jones\"}]".data(using: .utf8) + logger.sanitize = { message, type in var sanitizedMessage = message - + guard let ruleList = try? decoder.decode([RegexListReponse].self, from: regexRuleList!) else { return "sanitizedMessage" } guard let exceptionList = try? decoder.decode([ExceptionListResponse].self, from: sanitizationExeceptionList!) else { return "sanitizedMessage" } - - for value in ruleList { - if (value.minimumLogLevel >= type.rawValue) { - if let regex = try? NSRegularExpression(pattern: value.pattern , options: NSRegularExpression.Options.caseInsensitive) { - - let range = NSRange(sanitizedMessage.startIndex..= type.rawValue) { + if let regex = try? NSRegularExpression(pattern: value.pattern , options: NSRegularExpression.Options.caseInsensitive) { + + let range = NSRange(sanitizedMessage.startIndex.. - - // default info - logger.defaultUserInfo = ["application": "JustLog iOS Demo", - "environment": "development", - "session": sessionID] - logger.setup() } diff --git a/Example/JustLog/ViewController.swift b/Example/JustLog/ViewController.swift index 1c10e99..ed828d2 100644 --- a/Example/JustLog/ViewController.swift +++ b/Example/JustLog/ViewController.swift @@ -12,31 +12,41 @@ import JustLog class ViewController: UIViewController { private var logNumber = 0 - + + private var logger: Logger = Logger( + configuration: Configuration(), + logMessageFormatter: JSONStringLogMessageFormatter(keys: FormatterKeys()) + ) + @IBAction func verbose() { - Logger.shared.verbose("[\(logNumber)] not so important", userInfo: ["userInfo key": "userInfo value"]) + let log = Log(type: .verbose, message: "[\(logNumber)] not so important", customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @IBAction func debug() { - Logger.shared.debug("[\(logNumber)] something to debug", userInfo: ["userInfo key": "userInfo value"]) + let log = Log(type: .debug, message: "[\(logNumber)] not so debug", customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @IBAction func info() { - Logger.shared.info("[\(logNumber)] a nice information", userInfo: ["userInfo key": "userInfo value"]) + let log = Log(type: .info, message: "[\(logNumber)] a nice information", customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @IBAction func warning() { - Logger.shared.warning("[\(logNumber)] oh no, that won’t be good", userInfo: ["userInfo key": "userInfo value"]) + let log = Log(type: .warning, message: "[\(logNumber)] oh no, that won’t be good", customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @IBAction func warningSanitized() { let messageToSanitize = "conversation ={\\n id = \\\"123455\\\";\\n};\\n from = {\\n id = 123456;\\n name = \\\"John Smith\\\";\\n; \\n token = \\\"123456\\\";\\n" - let sanitizedMessage = Logger.shared.sanitize(messageToSanitize, Logger.LogType.warning) - Logger.shared.warning(sanitizedMessage, userInfo: ["userInfo key": "userInfo value"]) + let sanitizedMessage = logger.sanitize(messageToSanitize, LogType.warning) + let log = Log(type: .warning, message: sanitizedMessage, customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @@ -57,16 +67,17 @@ class ViewController: UIViewController { let unreadableError = NSError(domain: "com.just-eat.test", code: 1234, userInfo: unreadableUserInfos) - Logger.shared.error("[\(logNumber)] ouch, an error did occur!", error: unreadableError, userInfo: ["userInfo key": "userInfo value"]) + let log = Log(type: .error, message: "[\(logNumber)] ouch, an error did occur!", error: unreadableError, customData: ["userInfo key": "userInfo value"]) + logger.send(log) logNumber += 1 } @IBAction func forceSend() { - Logger.shared.forceSend() + logger.forceSend() } @IBAction func cancel(_ sender: Any) { - Logger.shared.cancelSending() + logger.cancelSending() } } diff --git a/Example/Podfile.lock b/Example/Podfile.lock index bf4c751..84d970e 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - JustLog (4.0.0): + - JustLog (4.0.3): - SwiftyBeaver (= 1.9.6) - SwiftyBeaver (1.9.6) @@ -15,9 +15,9 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - JustLog: e447d77125f8224bc006f280aaa858b00240b2d5 + JustLog: c2bb751e8b6f5c14d17dcc301834fd6534c0482a SwiftyBeaver: c8241b4745aa269a78ca5a022532c6915e948536 PODFILE CHECKSUM: 6e0bf13c9ce9221fdbf6f8bd0ca7a205dcdaeb98 -COCOAPODS: 1.10.2 +COCOAPODS: 1.14.3 diff --git a/Example/Tests/CustomDestinationTests.swift b/Example/Tests/CustomDestinationTests.swift index 84ec234..35d2a44 100644 --- a/Example/Tests/CustomDestinationTests.swift +++ b/Example/Tests/CustomDestinationTests.swift @@ -1,10 +1,4 @@ -// // CustomDestinationTests.swift -// JustLog_Tests -// -// Created by Antonio Strijdom on 02/02/2021. -// Copyright © 2021 Just Eat. All rights reserved. -// import XCTest @testable import JustLog @@ -25,7 +19,7 @@ class MockCustomDestinationSender: CustomDestinationSender { } class CustomDestinationTests: XCTestCase { - + func testBasicLogging() throws { let expect = expectation(description: "Send log expectation") expect.expectedFulfillmentCount = 5 @@ -43,20 +37,18 @@ class CustomDestinationTests: XCTestCase { } func test_logger_sendsDeviceTimestampForEachLogType() { - let sut = Logger.shared - sut.enableCustomLogging = true - let expectation = expectation(description: #function) expectation.expectedFulfillmentCount = 5 - + let mockSender = MockCustomDestinationSender(expectation: expectation) - sut.setupWithCustomLogSender(mockSender) + + let sut = Logger(configuration: Configuration(), logMessageFormatter: JSONStringLogMessageFormatter(keys: FormatterKeys()), customLogSender: mockSender) - sut.verbose("Verbose Message", error: nil, userInfo: nil, #file, #function, #line) - sut.debug("Debug Message", error: nil, userInfo: nil, #file, #function, #line) - sut.info("Info Message", error: nil, userInfo: nil, #file, #function, #line) - sut.warning("Warning Message", error: nil, userInfo: nil, #file, #function, #line) - sut.error("Error Message", error: nil, userInfo: nil, #file, #function, #line) + sut.send(Log(type: .verbose, message: "Verbose Message")) + sut.send(Log(type: .debug, message: "Debug Message")) + sut.send(Log(type: .info, message: "Info Message")) + sut.send(Log(type: .warning, message: "Warning Message")) + sut.send(Log(type: .error, message: "Error Message")) mockSender.logs.forEach { XCTAssertTrue($0.contains("device_timestamp")) } self.waitForExpectations(timeout: 10.0, handler: nil) diff --git a/Example/Tests/Data_RepresentationTests.swift b/Example/Tests/Data_RepresentationTests.swift index 2c2a558..0838961 100644 --- a/Example/Tests/Data_RepresentationTests.swift +++ b/Example/Tests/Data_RepresentationTests.swift @@ -1,3 +1,5 @@ +// Data_RepresentationTests.swift + import Foundation import XCTest @testable import JustLog @@ -14,5 +16,4 @@ class Data_RepresentationTests: XCTestCase { XCTAssertEqual(representation, target) } - } diff --git a/Example/Tests/Dictionary_FlatteningTests.swift b/Example/Tests/Dictionary_FlatteningTests.swift index 282787a..1fceb9c 100644 --- a/Example/Tests/Dictionary_FlatteningTests.swift +++ b/Example/Tests/Dictionary_FlatteningTests.swift @@ -1,3 +1,5 @@ +// Dictionary_FlatteningTests.swift + import Foundation import XCTest @testable import JustLog @@ -12,7 +14,7 @@ class Dictionary_Flattening: XCTestCase { let merged = d1.merged(with: d2) let target = ["k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"] - XCTAssertEqual(NSDictionary(dictionary:merged), NSDictionary(dictionary: target)) + XCTAssertEqual(NSDictionary(dictionary: merged), NSDictionary(dictionary: target)) } func test_merge_withConflictingDictionies() { @@ -20,11 +22,10 @@ class Dictionary_Flattening: XCTestCase { let d1 = ["k1": "v1", "k2": "v2"] let d2 = ["k1": "v1b", "k3": "v3"] - let merged = d1.merged(with: d2) let target = ["k1": "v1b", "k2": "v2", "k3": "v3"] - XCTAssertEqual(NSDictionary(dictionary:merged), NSDictionary(dictionary:target)) + XCTAssertEqual(NSDictionary(dictionary: merged), NSDictionary(dictionary: target)) } func test_flattened() { @@ -33,7 +34,7 @@ class Dictionary_Flattening: XCTestCase { let error = NSError(domain: domain, code: 200, userInfo: ["k8": "v8"]) let nestedError = NSError(domain: domain, code: 200, userInfo: [NSUnderlyingErrorKey: NSError(domain: domain, code: 200, userInfo: ["k10": 10])]) let input = ["k1": "v1", "k2": ["k3": "v3"], "k4": 4, "k5": 5.1, "k6": true, - "k7": error, "k9": nestedError, "k11": [1, 2, 3]] as [String : Any] + "k7": error, "k9": nestedError, "k11": [1, 2, 3]] as [String: Any] let flattened = input.flattened() @@ -50,7 +51,7 @@ class Dictionary_Flattening: XCTestCase { func test_flattened_withConflictingDictionies() { - let input = ["k1": "v1", "k2": ["k1": "v3", "k4": "v4"]] as [String : Any] + let input = ["k1": "v1", "k2": ["k1": "v3", "k4": "v4"]] as [String: Any] let flattened = input.flattened() // can be either ["k1": "v1", "k4": "v4"] or ["k1": "v3", "k4": "v4"] @@ -62,9 +63,10 @@ class Dictionary_Flattening: XCTestCase { func test_flattened_recursive() { - let input = ["k1": "v1", - "k2": ["k3": "v3", - "k4": ["k5": "v5"]]] as [String : Any] + let input: [String: Any] = [ + "k1": "v1", + "k2": ["k3": "v3", "k4": ["k5": "v5"]] as [String: Any] + ] let flattened = input.flattened() // target = ["k1": "v1", "k3": "v3", "k5": "v5"] as [String : Any] diff --git a/Example/Tests/Dictionary_JSONTests.swift b/Example/Tests/Dictionary_JSONTests.swift index 5204c4e..c63ee44 100644 --- a/Example/Tests/Dictionary_JSONTests.swift +++ b/Example/Tests/Dictionary_JSONTests.swift @@ -1,3 +1,5 @@ +// Dictionary_JSONTests.swift + import Foundation import XCTest @testable import JustLog @@ -11,25 +13,24 @@ class Dictionary_JSON: XCTestCase { "k3": 42, "k4": true, "k5": [1, 2], - "k6": ["k": "v"]] as [String : Any] + "k6": ["k": "v"]] as [String: Any] let jsonRepresentation = input.toJSON()! - XCTAssertTrue(jsonRepresentation.range(of: "\"k1\":\"v1\"") != nil) - XCTAssertTrue(jsonRepresentation.range(of: "\"k2\":2.5") != nil) - XCTAssertTrue(jsonRepresentation.range(of: "\"k3\":42") != nil) - XCTAssertTrue(jsonRepresentation.range(of: "\"k4\":true") != nil) - XCTAssertTrue(jsonRepresentation.range(of: "\"k5\":[1,2]") != nil) - XCTAssertTrue(jsonRepresentation.range(of: "\"k6\":{\"k\":\"v\"}") != nil) + XCTAssertTrue(jsonRepresentation.contains("\"k1\":\"v1\"")) + XCTAssertTrue(jsonRepresentation.contains("\"k2\":2.5")) + XCTAssertTrue(jsonRepresentation.contains("\"k3\":42")) + XCTAssertTrue(jsonRepresentation.contains("\"k4\":true")) + XCTAssertTrue(jsonRepresentation.contains("\"k5\":[1,2]")) + XCTAssertTrue(jsonRepresentation.contains("\"k6\":{\"k\":\"v\"}")) } func test_JSON_invalid() { - let input = ["k1": "v1".data(using: String.Encoding.utf8)!] as [String : Any] + let input = ["k1": "v1".data(using: String.Encoding.utf8)!] as [String: Any] let jsonRepresentation = input.toJSON() XCTAssertNil(jsonRepresentation) } - } diff --git a/Example/Tests/JSONStringLogMessageFormatterTests.swift b/Example/Tests/JSONStringLogMessageFormatterTests.swift new file mode 100644 index 0000000..17e909e --- /dev/null +++ b/Example/Tests/JSONStringLogMessageFormatterTests.swift @@ -0,0 +1,95 @@ +// JSONStringLogMessageFormatterTests.swift + +import XCTest +@testable import JustLog + +class JSONStringLogMessageFormatterTests: XCTestCase { + + func test_errorDictionary_returnDictionaryForError() { + let userInfo = [ + NSLocalizedFailureReasonErrorKey: "error value", + NSLocalizedDescriptionKey: "description", + NSLocalizedRecoverySuggestionErrorKey: "recovery suggestion" + ] as [String: Any] + let error = NSError(domain: "com.just-eat.error", code: 1234, userInfo: userInfo) + + let sut = JSONStringLogMessageFormatter(keys: FormatterKeys()) + let dict = sut.errorDictionary(for: error) + + XCTAssertNotNil(dict["user_info"]) + XCTAssertNotNil(dict["error_code"]) + XCTAssertNotNil(dict["error_domain"]) + } + + func test_errorDictionary_returnDictionaryContainsUserInfo() { + let userInfo = [ + NSLocalizedFailureReasonErrorKey: "error value", + NSLocalizedDescriptionKey: "description", + NSLocalizedRecoverySuggestionErrorKey: "recovery suggestion" + ] as [String: Any] + let error = NSError(domain: "com.just-eat.error", code: 1234, userInfo: userInfo) + + let sut = JSONStringLogMessageFormatter(keys: FormatterKeys()) + let dict = sut.errorDictionary(for: error) + let dictUserInfo = dict["user_info"] as! [String: Any] + + XCTAssertEqual(userInfo[NSLocalizedFailureReasonErrorKey] as! String, dictUserInfo[NSLocalizedFailureReasonErrorKey] as! String) + XCTAssertEqual(userInfo[NSLocalizedDescriptionKey] as! String, dictUserInfo[NSLocalizedDescriptionKey] as! String) + XCTAssertEqual(userInfo[NSLocalizedRecoverySuggestionErrorKey] as! String, dictUserInfo[NSLocalizedRecoverySuggestionErrorKey] as! String) + XCTAssertNotNil(dict["error_domain"]) + } + + func test_metadataDictionary_returnDictionaryContainsMetadata() { + let keys = FormatterKeys() + + let iosVersion = UIDevice.current.systemVersion + let deviceType = UIDevice.current.platform() + let appBundleID = Bundle.main.bundleIdentifier + + let sut = JSONStringLogMessageFormatter(keys: keys) + let metadata = sut.metadataDictionary("TestFile", "testFunc", 42) + + XCTAssertEqual(metadata[keys.iosVersionKey] as! String, iosVersion) + XCTAssertEqual(metadata[keys.deviceTypeKey] as! String, deviceType) + XCTAssertEqual(metadata[keys.appBundleIDKey] as? String, appBundleID) + } + + func test_formatterKeys_canBeOverriden() { + var keys = FormatterKeys() + keys.fileKey = "NotFileKey" + keys.functionKey = "NotFunctionKey" + keys.lineKey = "NotLineKey" + + let sut = JSONStringLogMessageFormatter(keys: keys) + let metadata = sut.metadataDictionary("TestFile", "testFunc", 42) + + XCTAssertEqual(metadata["NotFileKey"] as! String, "TestFile") + XCTAssertEqual(metadata["NotFunctionKey"] as! String, "testFunc") + XCTAssertEqual(metadata["NotLineKey"] as! String, "42") + } + + func test_formattedLogMessage_haveDeviceTimestampAsMetadata() throws { + let sut = JSONStringLogMessageFormatter(keys: FormatterKeys()) + let log = Log(type: .error, message: "Log message") + + let message = sut.format(log) + + let data = try XCTUnwrap(message.data(using: .utf8)) + guard let parsedMessage = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { + XCTFail("Failed to parse log message") + return + } + + let parsedMetadata = try XCTUnwrap(parsedMessage["metadata"] as? [String: String]) + _ = try XCTUnwrap(parsedMetadata["device_timestamp"]) + } + + func test_format_returnsNotEmptyString() { + let log = Log(type: .error, message: "Log message") + + let sut = JSONStringLogMessageFormatter(keys: FormatterKeys()) + let string = sut.format(log) + + XCTAssertFalse(string.isEmpty) + } +} diff --git a/Example/Tests/LoggerTests.swift b/Example/Tests/LoggerTests.swift index 224063f..9e44f6d 100644 --- a/Example/Tests/LoggerTests.swift +++ b/Example/Tests/LoggerTests.swift @@ -1,58 +1,28 @@ -// // LoggerTests.swift -// JustLog -// -// Created by Alkiviadis Papadakis on 24/08/2017. -// Copyright © 2017 CocoaPods. All rights reserved. -// import XCTest @testable import JustLog class LoggerTests: XCTestCase { + var sut: Logger! override func setUp() { super.setUp() - Logger.shared.internalLogger.removeAllDestinations() + sut = Logger(configuration: Configuration(), logMessageFormatter: JSONStringLogMessageFormatter(keys: FormatterKeys())) + sut.internalLogger.removeAllDestinations() } - func test_errorDictionary_ReturnsDictionaryForError() { - let userInfo = [ - NSLocalizedFailureReasonErrorKey: "error value", - NSLocalizedDescriptionKey: "description", - NSLocalizedRecoverySuggestionErrorKey: "recovery suggestion" - ] as [String : Any] - let error = NSError(domain: "com.just-eat.error", code: 1234, userInfo: userInfo) - let dict = Logger.shared.errorDictionary(for: error) - - XCTAssertNotNil(dict["user_info"]) - XCTAssertNotNil(dict["error_code"]) - XCTAssertNotNil(dict["error_domain"]) - } - - func test_errorDictionary_ReturnedDictionaryContainsUserInfo() { - let userInfo = [ - NSLocalizedFailureReasonErrorKey: "error value", - NSLocalizedDescriptionKey: "description", - NSLocalizedRecoverySuggestionErrorKey: "recovery suggestion" - ] as [String : Any] - let error = NSError(domain: "com.just-eat.error", code: 1234, userInfo: userInfo) - let dict = Logger.shared.errorDictionary(for: error) - let dictUserInfo = dict["user_info"] as! [String : Any] - - XCTAssertEqual(userInfo[NSLocalizedFailureReasonErrorKey] as! String, dictUserInfo[NSLocalizedFailureReasonErrorKey] as! String) - XCTAssertEqual(userInfo[NSLocalizedDescriptionKey] as! String, dictUserInfo[NSLocalizedDescriptionKey] as! String) - XCTAssertEqual(userInfo[NSLocalizedRecoverySuggestionErrorKey] as! String, dictUserInfo[NSLocalizedRecoverySuggestionErrorKey] as! String) - XCTAssertNotNil(dict["error_domain"]) + override func tearDown() { + sut = nil + super.tearDown() } func test_logger_whenSetupNotCompleted_thenLogsQueued() { - let sut = Logger.shared - sut.verbose("Verbose Message", error: nil, userInfo: nil, #file, #function, #line) - sut.debug("Debug Message", error: nil, userInfo: nil, #file, #function, #line) - sut.info("Info Message", error: nil, userInfo: nil, #file, #function, #line) - sut.warning("Warning Message", error: nil, userInfo: nil, #file, #function, #line) - sut.error("Error Message", error: nil, userInfo: nil, #file, #function, #line) + sut.send(Log(type: .verbose, message: "Verbose Message")) + sut.send(Log(type: .debug, message: "Debug Message")) + sut.send(Log(type: .info, message: "Info Message")) + sut.send(Log(type: .warning, message: "Warning Message")) + sut.send(Log(type: .error, message: "Error Message")) XCTAssertFalse(sut.queuedLogs.isEmpty) XCTAssertEqual(sut.queuedLogs.count, 5) @@ -63,27 +33,12 @@ class LoggerTests: XCTestCase { XCTAssertEqual(sut.queuedLogs[4].message, "Error Message") } - func test_logger_whenSetupCompleted_thenLogsNotQueued() { - let sut = Logger.shared - sut.setup() - - sut.verbose("Verbose Message", error: nil, userInfo: nil, #file, #function, #line) - sut.debug("Debug Message", error: nil, userInfo: nil, #file, #function, #line) - sut.info("Info Message", error: nil, userInfo: nil, #file, #function, #line) - sut.warning("Warning Message", error: nil, userInfo: nil, #file, #function, #line) - sut.error("Error Message", error: nil, userInfo: nil, #file, #function, #line) - - XCTAssertTrue(sut.queuedLogs.isEmpty) - } - func test_logger_whenSetupCompletedAfterDelay_thenQueuedLogsSent() { - let sut = Logger.shared - - sut.verbose("Verbose Message", error: nil, userInfo: nil, #file, #function, #line) - sut.debug("Debug Message", error: nil, userInfo: nil, #file, #function, #line) - sut.info("Info Message", error: nil, userInfo: nil, #file, #function, #line) - sut.warning("Warning Message", error: nil, userInfo: nil, #file, #function, #line) - sut.error("Error Message", error: nil, userInfo: nil, #file, #function, #line) + sut.send(Log(type: .verbose, message: "Verbose Message")) + sut.send(Log(type: .debug, message: "Debug Message")) + sut.send(Log(type: .info, message: "Info Message")) + sut.send(Log(type: .warning, message: "Warning Message")) + sut.send(Log(type: .error, message: "Error Message")) XCTAssertFalse(sut.queuedLogs.isEmpty) XCTAssertEqual(sut.queuedLogs.count, 5) @@ -93,39 +48,19 @@ class LoggerTests: XCTestCase { XCTAssertEqual(sut.queuedLogs[3].message, "Warning Message") XCTAssertEqual(sut.queuedLogs[4].message, "Error Message") - sut.setup() - + sut = Logger(configuration: Configuration(), logMessageFormatter: JSONStringLogMessageFormatter(keys: FormatterKeys())) XCTAssertTrue(sut.queuedLogs.isEmpty) } func test_logger_whenLogMessagesAreSanitized_thenExpectedResultRetrived() { - let sut = Logger.shared - sut.setup() + sut = MockLoggerFactory().logger + var message = "conversation = {name = \\\"John Smith\\\";\\n; \\n token = \\\"123453423\\\";\\n" let expectedMessage = "conversation = {n***e = \\\"*****\\\";\\n; \\n t***n = \\\"*****\\\";\\n" - message = sut.sanitize(message, Logger.LogType.error) - sut.error(message, error: nil, userInfo: nil, #file, #function, #line) + message = sut.sanitize(message, LogType.error) + sut.send(Log(type: .error, message: message)) XCTAssertEqual(message, expectedMessage) } - - func test_logger_sendsDeviceTimestampAsMetadata() throws { - let sut = Logger.shared - - let currentDate = Date() - let message = sut.logMessage("Log message", error: nil, userInfo: nil, currentDate: currentDate, #file, #function, #line) - - let data = try XCTUnwrap(message.data(using: .utf8)) - guard let parsedMessage = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - XCTFail("Failed to parse log message") - return - } - - let parsedMetadata = try XCTUnwrap(parsedMessage["metadata"] as? [String: String]) - let parsedDeviceTimestamp = try XCTUnwrap(parsedMetadata["device_timestamp"]) - let expectedDeviceTimestamp = "\(currentDate.timeIntervalSince1970)" - - XCTAssertEqual(parsedDeviceTimestamp, expectedDeviceTimestamp) - } } diff --git a/Example/Tests/LogstashDestinationSendingSchedulerTests.swift b/Example/Tests/LogstashDestinationSendingSchedulerTests.swift new file mode 100644 index 0000000..50c77dd --- /dev/null +++ b/Example/Tests/LogstashDestinationSendingSchedulerTests.swift @@ -0,0 +1,63 @@ +// LogstashDestinationSendingSchedulerTests.swift + +import Foundation +import XCTest + +@testable import JustLog + +class LogstashDestinationSendingSchedulerTests: XCTestCase { + let sut = LogstashDestinationSendingScheduler() + + func test_givenScheduler_whenSendSuccessful_thenNoErrorsReturned() async { + let log = ["Testing": "Testing"] + let logs = [1: log] + + let result = await sut.scheduleSend(logs) { _ in } + + XCTAssertEqual(result.isEmpty, true) + } + + func test_givenScheduler_whenSendFails_thenErrorsReturned() async throws { + let log = ["Testing": "Testing"] + let logs = [1: log] + + let result = await sut.scheduleSend(logs) { _ in + throw SendError.generalError + } + + XCTAssertEqual(result.isEmpty, false) + let error = try XCTUnwrap(result.first?.value as? SendError) + XCTAssertEqual(error, SendError.generalError) + } + + func test_givenScheduler_whenOneSendFails_thenOtherLogSent() async { + let log = ["Testing": "Testing"] + let logs = [1: log, 2: log, 3: log] + let sendCounter = SendCounter() + + let result = await sut.scheduleSend(logs) { _ in + let count = await sendCounter.count + if count >= 2 { + throw SendError.generalError + } else { + await sendCounter.increment() + } + } + + XCTAssertEqual(result.isEmpty, false) + XCTAssertEqual(result.count, 1) + let finalCount = await sendCounter.count + XCTAssertEqual(finalCount, 2) + } +} + +actor SendCounter { + var count = 0 + func increment() { + count += 1 + } +} + +enum SendError: Error { + case generalError +} diff --git a/Example/Tests/LogstashDestinationTests.swift b/Example/Tests/LogstashDestinationTests.swift index a7000a3..e3c7724 100644 --- a/Example/Tests/LogstashDestinationTests.swift +++ b/Example/Tests/LogstashDestinationTests.swift @@ -1,10 +1,4 @@ -// // LogstashDestinationTests.swift -// JustLog_Tests -// -// Created by Antonio Strijdom on 13/07/2020. -// Copyright © 2020 Just Eat. All rights reserved. -// import XCTest @testable import JustLog @@ -16,7 +10,24 @@ class LogstashDestinationTests: XCTestCase { let mockSocket = MockLogstashDestinationSocket(host: "", port: 0, timeout: 5, logActivity: true, allowUntrustedServer: true) mockSocket.networkOperationCountExpectation = expect mockSocket.completionHandlerCalledExpectation = expectation(description: "completionHandlerCalledExpectation") - let destination = LogstashDestination(socket: mockSocket, logActivity: true) + let destination = LogstashDestination(sender: mockSocket, logActivity: true) + _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) + _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) + _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) + _ = destination.send(.warning, msg: "{}", thread: "", file: "", function: "", line: 0) + _ = destination.send(.error, msg: "{}", thread: "", file: "", function: "", line: 0) + expect.expectedFulfillmentCount = 5 + destination.forceSend() + self.waitForExpectations(timeout: 10.0, handler: nil) + } + + func testBasicLogging_HTTP() throws { + let expect = expectation(description: "Send log expectation") + var mockScheduler = MockLogstashDestinationSendingScheduler() + mockScheduler.networkOperationCountExpectation = expect + let sender = LogstashDestinationHTTP(host: "testing.com", port: 0, timeout: 5, logActivity: false) + sender.scheduler = mockScheduler + let destination = LogstashDestination(sender: sender, logActivity: true) _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) @@ -30,7 +41,7 @@ class LogstashDestinationTests: XCTestCase { func testCompletionHandler() throws { let expect = expectation(description: "Send log expectation") let mockSocket = MockLogstashDestinationSocket(host: "", port: 0, timeout: 5, logActivity: true, allowUntrustedServer: true) - let destination = LogstashDestination(socket: mockSocket, logActivity: true) + let destination = LogstashDestination(sender: mockSocket, logActivity: true) _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) @@ -46,7 +57,7 @@ class LogstashDestinationTests: XCTestCase { let expectation1 = expectation(description: "Send first log expectation") let expectation2 = expectation(description: "Send second log expectation") let mockSocket = MockLogstashDestinationSocket(host: "", port: 0, timeout: 5, logActivity: true, allowUntrustedServer: true) - let destination = LogstashDestination(socket: mockSocket, logActivity: true) + let destination = LogstashDestination(sender: mockSocket, logActivity: true) _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) @@ -68,7 +79,7 @@ class LogstashDestinationTests: XCTestCase { mockSocket.errorState = true mockSocket.networkOperationCountExpectation = expect mockSocket.completionHandlerCalledExpectation = expectation1 - let destination = LogstashDestination(socket: mockSocket, logActivity: true) + let destination = LogstashDestination(sender: mockSocket, logActivity: true) _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) @@ -94,7 +105,7 @@ class LogstashDestinationTests: XCTestCase { mockSocket.errorState = true mockSocket.networkOperationCountExpectation = expect mockSocket.completionHandlerCalledExpectation = expectation1 - let destination = LogstashDestination(socket: mockSocket, logActivity: true) + let destination = LogstashDestination(sender: mockSocket, logActivity: true) _ = destination.send(.verbose, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.debug, msg: "{}", thread: "", file: "", function: "", line: 0) _ = destination.send(.info, msg: "{}", thread: "", file: "", function: "", line: 0) @@ -119,7 +130,7 @@ enum LogstashDestinationTestError: Error { case whoops } -class MockLogstashDestinationSocket: NSObject, LogstashDestinationSocketProtocol { +class MockLogstashDestinationSocket: NSObject, LogstashDestinationSending { var networkOperationCountExpectation: XCTestExpectation? var completionHandlerCalledExpectation: XCTestExpectation? @@ -136,7 +147,7 @@ class MockLogstashDestinationSocket: NSObject, LogstashDestinationSocketProtocol func sendLogs(_ logs: [LogTag: LogContent], transform: (LogContent) -> Data, queue: DispatchQueue, - complete: @escaping LogstashDestinationSocketProtocolCompletion) { + complete: @escaping LogstashDestinationSendingCompletion) { let dispatchGroup = DispatchGroup() var sendStatus = [Int: Error]() diff --git a/Example/Tests/MockLoggerFactory.swift b/Example/Tests/MockLoggerFactory.swift new file mode 100644 index 0000000..de41ce3 --- /dev/null +++ b/Example/Tests/MockLoggerFactory.swift @@ -0,0 +1,94 @@ +// MockLoggerFactory.swift + +import Foundation +@testable import JustLog + +class MockLoggerFactory { + + var logger: Logger? + + struct RegexListReponse: Codable { + var pattern: String + var minimumLogLevel: String + } + + struct ExceptionListResponse: Codable { + var value: String + } + + func redactValues(message: String, loggerExceptionList: [ExceptionListResponse], matches: [NSTextCheckingResult]) -> String { + + var redactedLogMessage = message + + for match in matches.reversed() { + let key = match.range(at: 1) + let value = match.range(at: 2) + + let keyRange = Range(key, in: redactedLogMessage)! + let valueRange = Range(value, in: redactedLogMessage)! + + for exception in loggerExceptionList where exception.value == redactedLogMessage[valueRange] { + return redactedLogMessage + } + + var redactedKey = redactedLogMessage[keyRange] + let redactedKeyStartIndex = redactedKey.index(redactedKey.startIndex, offsetBy: 1) + let redactedKeyEndIndex = redactedKey.index(redactedKey.endIndex, offsetBy: -1) + + redactedKey.replaceSubrange(redactedKeyStartIndex..= type.rawValue { + if let regex = try? NSRegularExpression(pattern: value.pattern, options: NSRegularExpression.Options.caseInsensitive) { + + let range = NSRange(sanitizedMessage.startIndex.. [Int: Error] { + for _ in 0.. 'YES' } - s.source_files = 'JustLog/Classes/**/*', 'JustLog/Extensions/**/*' + s.source_files = 'JustLog/Classes/**/*', 'JustLog/Extensions/**/*', 'JustLog/Protocols/**/*' s.dependency 'SwiftyBeaver', '1.9.6' diff --git a/JustLog/Classes/Configuration.swift b/JustLog/Classes/Configuration.swift new file mode 100644 index 0000000..e786492 --- /dev/null +++ b/JustLog/Classes/Configuration.swift @@ -0,0 +1,65 @@ +// Configuration.swift + +import Foundation + +public struct Configuration: Configurable { + + public var logFormat: String + public var sendingInterval: TimeInterval + + // file + public var logFilename: String? + public var baseUrlForFileLogging: URL? + + // logstash + public var allowUntrustedServer: Bool + public var logstashHost: String + public var logstashPort: UInt16 + public var logstashTimeout: TimeInterval + public var logLogstashSocketActivity: Bool + public var logzioToken: String? + public var logstashOverHTTP: Bool + + // destinations + public var isConsoleLoggingEnabled: Bool + public var isFileLoggingEnabled: Bool + public var isLogstashLoggingEnabled: Bool + public var isCustomLoggingEnabled: Bool + + public init(logFormat: String = "$Dyyyy-MM-dd HH:mm:ss.SSS$d $T $C$L$c: $M", + sendingInterval: TimeInterval = 5, + logFilename: String? = nil, + baseUrlForFileLogging: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first, + allowUntrustedServer: Bool = false, + logstashHost: String = "", + logstashPort: UInt16 = 9300, + logstashTimeout: TimeInterval = 20, + logLogstashSocketActivity: Bool = false, + logzioToken: String? = nil, + logstashOverHTTP: Bool = false, + isConsoleLoggingEnabled: Bool = true, + isFileLoggingEnabled: Bool = true, + isLogstashLoggingEnabled: Bool = true, + isCustomLoggingEnabled: Bool = true) { + self.logFormat = logFormat + self.sendingInterval = sendingInterval + + self.logFilename = logFilename + self.baseUrlForFileLogging = baseUrlForFileLogging + + // logstash + self.allowUntrustedServer = allowUntrustedServer + self.logstashHost = logstashHost + self.logstashPort = logstashPort + self.logstashTimeout = logstashTimeout + self.logLogstashSocketActivity = logLogstashSocketActivity + self.logzioToken = logzioToken + self.logstashOverHTTP = logstashOverHTTP + + // destinations + self.isConsoleLoggingEnabled = isConsoleLoggingEnabled + self.isFileLoggingEnabled = isFileLoggingEnabled + self.isLogstashLoggingEnabled = isLogstashLoggingEnabled + self.isCustomLoggingEnabled = isCustomLoggingEnabled + } +} diff --git a/JustLog/Classes/ConsoleDestination.swift b/JustLog/Classes/ConsoleDestination.swift index c3cd731..18f27cc 100644 --- a/JustLog/Classes/ConsoleDestination.swift +++ b/JustLog/Classes/ConsoleDestination.swift @@ -1,10 +1,4 @@ -// // ConsoleDestination.swift -// JustLog -// -// Created by Alberto De Bortoli on 06/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation import SwiftyBeaver @@ -34,6 +28,4 @@ public class ConsoleDestination: BaseDestination { } return formattedString } - } - diff --git a/JustLog/Classes/CustomDestination.swift b/JustLog/Classes/CustomDestination.swift index 5c0376a..55689ed 100644 --- a/JustLog/Classes/CustomDestination.swift +++ b/JustLog/Classes/CustomDestination.swift @@ -1,10 +1,4 @@ -// // CustomDestination.swift -// JustLog -// -// Created by Antonio Strijdom on 02/02/2021. -// Copyright © 2021 Just Eat. All rights reserved. -// import Foundation import SwiftyBeaver diff --git a/JustLog/Classes/FileDestination.swift b/JustLog/Classes/FileDestination.swift index 2a2fe6b..bb7ff73 100644 --- a/JustLog/Classes/FileDestination.swift +++ b/JustLog/Classes/FileDestination.swift @@ -1,10 +1,4 @@ -// // FileDestination.swift -// JustLog -// -// Created by Alberto De Bortoli on 20/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation import SwiftyBeaver @@ -13,7 +7,7 @@ public class FileDestination: BaseDestination { public var logFileURL: URL? - var fileHandle: FileHandle? = nil + var fileHandle: FileHandle? public override init() { super.init() @@ -31,7 +25,7 @@ public class FileDestination: BaseDestination { let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line) if let str = formattedString { - let _ = saveToFile(str: str) + _ = saveToFile(str: str) } return formattedString } @@ -53,7 +47,7 @@ public class FileDestination: BaseDestination { fileHandle = try FileHandle(forWritingTo: url as URL) } if let fileHandle = fileHandle { - let _ = fileHandle.seekToEndOfFile() + _ = fileHandle.seekToEndOfFile() let line = str + "\n" if let data = line.data(using: String.Encoding.utf8) { fileHandle.write(data) diff --git a/JustLog/Classes/JSONStringLogMessageFormatter.swift b/JustLog/Classes/JSONStringLogMessageFormatter.swift new file mode 100644 index 0000000..9e99845 --- /dev/null +++ b/JustLog/Classes/JSONStringLogMessageFormatter.swift @@ -0,0 +1,121 @@ +// JSONStringLogMessageFormatter.swift + +import Foundation +import UIKit + +public struct FormatterKeys: FormatterKeysProviding { + + public var logTypeKey: String + public var appBundleIDKey: String + public var appVersionKey: String + public var deviceTimestampKey: String + public var deviceTypeKey: String + public var errorCodeKey: String + public var errorDomainKey: String + public var errorsKey: String + public var fileKey: String + public var functionKey: String + public var iosVersionKey: String + public var lineKey: String + public var messageKey: String + public var metadataKey: String + public var userInfoKey: String + + public init (logTypeKey: String = "log_type", + appBundleIDKey: String = "app_bundle_ID", + appVersionKey: String = "app_version", + deviceTimestampKey: String = "device_timestamp", + deviceTypeKey: String = "ios_device", + errorCodeKey: String = "error_code", + errorDomainKey: String = "error_domain", + errorsKey: String = "errors", + fileKey: String = "file", + functionKey: String = "function", + iosVersionKey: String = "ios_version", + lineKey: String = "line", + messageKey: String = "message", + metadataKey: String = "metadata", + userInfoKey: String = "user_info") { + self.logTypeKey = logTypeKey + self.appBundleIDKey = appBundleIDKey + self.appVersionKey = appVersionKey + self.deviceTimestampKey = deviceTimestampKey + self.deviceTypeKey = deviceTypeKey + self.errorCodeKey = errorCodeKey + self.errorDomainKey = errorDomainKey + self.errorsKey = errorsKey + self.fileKey = fileKey + self.functionKey = functionKey + self.iosVersionKey = iosVersionKey + self.lineKey = lineKey + self.messageKey = messageKey + self.metadataKey = metadataKey + self.userInfoKey = userInfoKey + } +} + +public class JSONStringLogMessageFormatter: LogMessageFormatting { + + public var keys: FormatterKeysProviding + public var defaultLogMetadata: DefaultLogMetadata? + + public init(keys: FormatterKeysProviding, defaultLogMetadata: DefaultLogMetadata? = nil) { + self.keys = keys + self.defaultLogMetadata = defaultLogMetadata + } + + public func format(_ log: Loggable) -> String { + var metadata = [String: Any]() + metadata[keys.messageKey] = log.message + metadata[keys.metadataKey] = metadataDictionary(log.file, log.function, log.line) + + var userInfo = defaultLogMetadata ?? [String: Any]() + userInfo[keys.logTypeKey] = log.type.rawValue + if let customData = log.customData { + for (key, value) in customData { + _ = userInfo.updateValue(value, forKey: key) + } + metadata[keys.userInfoKey] = userInfo + } + + if let error = log.error { + let errorDictionaries = error.disassociatedErrorChain() + .map { errorDictionary(for: $0) } + .filter { JSONSerialization.isValidJSONObject($0) } + metadata[keys.errorsKey] = errorDictionaries + } + + return metadata.toJSON() ?? "" + } + + func metadataDictionary(_ file: String, _ function: String, _ line: UInt, _ currentDate: Date = Date()) -> [String: Any] { + var metadata = [String: String]() + + if let url = URL(string: file) { + metadata[keys.fileKey] = URLComponents(url: url, resolvingAgainstBaseURL: false)?.url?.pathComponents.last ?? file + } + + metadata[keys.functionKey] = function + metadata[keys.lineKey] = String(line) + + if let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"], let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] { + metadata[keys.appVersionKey] = "\(bundleShortVersion) (\(bundleVersion))" + } + + metadata[keys.iosVersionKey] = UIDevice.current.systemVersion + metadata[keys.deviceTypeKey] = UIDevice.current.platform() + metadata[keys.appBundleIDKey] = Bundle.main.bundleIdentifier + metadata[keys.deviceTimestampKey] = "\(currentDate.timeIntervalSince1970)" + + return metadata + } + + func errorDictionary(for error: NSError) -> [String: Any] { + let userInfoConst = "user_info" + var errorInfo = [keys.errorDomainKey: error.domain, + keys.errorCodeKey: error.code] as [String: Any] + let errorUserInfo = error.humanReadableError().userInfo + errorInfo[userInfoConst] = errorUserInfo + return errorInfo + } +} diff --git a/JustLog/Classes/Log.swift b/JustLog/Classes/Log.swift new file mode 100644 index 0000000..3e876df --- /dev/null +++ b/JustLog/Classes/Log.swift @@ -0,0 +1,30 @@ +// Log.swift + +import Foundation + +public struct Log: Loggable { + + public let type: LogType + public let message: String + public let error: NSError? + public var customData: [String: Any]? + public let file: String + public let function: String + public let line: UInt + + public init(type: LogType, + message: String, + error: NSError? = nil, + customData: [String: Any]? = nil, + file: String = #file, + function: String = #function, + line: UInt = #line) { + self.type = type + self.message = message + self.error = error + self.customData = customData + self.file = file + self.function = function + self.line = line + } +} diff --git a/JustLog/Classes/Logger.swift b/JustLog/Classes/Logger.swift index 6f3db60..c80e6f5 100644 --- a/JustLog/Classes/Logger.swift +++ b/JustLog/Classes/Logger.swift @@ -1,42 +1,18 @@ -// // Logger.swift -// JustLog -// -// Created by Alberto De Bortoli on 06/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import UIKit import SwiftyBeaver -@objcMembers -public final class Logger: NSObject { - - public enum LogType: String { - case debug - case warning - case verbose - case error - case info - } - - internal struct QueuedLog { - let type: LogType - let message: String - let file: String - let function: String - let line: UInt - } +public final class Logger { public typealias SanitizeClosureType = (_ message: String, _ minimumLogType: LogType) -> String - private var _sanitize: SanitizeClosureType = { message, minimumLogType in - - return message + private var _sanitize: SanitizeClosureType = { message, _ in + message } private let sanitizePropertyQueue = DispatchQueue(label: "com.justeat.justlog.sanitizePropertyQueue", qos: .default, attributes: .concurrent) public var sanitize: SanitizeClosureType { get { - var sanitizeClosure: SanitizeClosureType? = nil + var sanitizeClosure: SanitizeClosureType? sanitizePropertyQueue.sync { sanitizeClosure = _sanitize } @@ -45,7 +21,7 @@ public final class Logger: NSObject { } else { assertionFailure("Sanitization closure not set") return { message, _ in - return message + message } } } @@ -56,198 +32,180 @@ public final class Logger: NSObject { } } - public var logTypeKey = "log_type" - - public var fileKey = "file" - public var functionKey = "function" - public var lineKey = "line" + private var configuration: Configurable! + private var logMessageFormatter: LogMessageFormatting! - public var appVersionKey = "app_version" - public var iosVersionKey = "ios_version" - public var deviceTypeKey = "ios_device" - public var appBundleID = "app_bundle_ID" - public var deviceTimestampKey = "device_timestamp" - - public var errorDomain = "error_domain" - public var errorCode = "error_code" - - public static let shared = Logger() - - // file conf - public var logFilename: String? - - // logstash conf - public var logstashHost: String = "" - public var logstashPort: UInt16 = 9300 - public var logstashTimeout: TimeInterval = 20 - public var logLogstashSocketActivity: Bool = false - public var logzioToken: String? - - /** - Default to `false`, if `true` untrusted certificates (as self-signed are) will be trusted - */ - public var allowUntrustedServer: Bool = false - - // logger conf - public var defaultUserInfo: [String : Any]? - public var enableConsoleLogging: Bool = true - public var enableFileLogging: Bool = true - public var enableLogstashLogging: Bool = true - public var enableCustomLogging: Bool = true - public var baseUrlForFileLogging = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first public let internalLogger = SwiftyBeaver.self - - private var timerInterval: TimeInterval = 5 private var timer: RepeatingTimer? - internal private(set) var queuedLogs = [QueuedLog]() + private(set) var queuedLogs = [Loggable]() // destinations public var console: ConsoleDestination! - public var logstash: LogstashDestination! + public var isConsoleLoggingEnabled: Bool { + get { configuration.isConsoleLoggingEnabled } + set { configuration.isConsoleLoggingEnabled = newValue } + } + public var file: FileDestination! + public var isFileLoggingEnabled: Bool { + get { configuration.isFileLoggingEnabled } + set { configuration.isFileLoggingEnabled = newValue } + } + + public var logstash: LogstashDestination! + public var isLogstashLoggingEnabled: Bool { + get { configuration.isLogstashLoggingEnabled } + set { configuration.isLogstashLoggingEnabled = newValue } + } + public var custom: CustomDestination? + public var isCustomLoggingEnabled: Bool { + get { configuration.isCustomLoggingEnabled } + set { configuration.isCustomLoggingEnabled = newValue } + } + deinit { timer?.suspend() timer = nil } - public func setup() { - setupWithCustomLogSender() - } - - public func setupWithCustomLogSender(_ customLogSender: CustomDestinationSender? = nil) { - - let format = "$Dyyyy-MM-dd HH:mm:ss.SSS$d $T $C$L$c: $M" + public init(configuration: Configurable, + logMessageFormatter: LogMessageFormatting, + customLogSender: CustomDestinationSender? = nil) { + self.configuration = configuration + self.logMessageFormatter = logMessageFormatter internalLogger.removeAllDestinations() - // console - if enableConsoleLogging { + setupConsoleDestination(with: configuration) + setupFileDestination(with: configuration) + setupLogstashDestination(with: configuration) + setupCustomDestination(with: configuration, customLogSender: customLogSender) + + sendQueuedLogsIfNeeded() + } + + private func setupConsoleDestination(with configuration: Configurable) { + if isConsoleLoggingEnabled { console = JustLog.ConsoleDestination() - console.format = format + console.format = configuration.logFormat internalLogger.addDestination(console) } - - // file - if enableFileLogging { + } + + private func setupFileDestination(with configuration: Configurable) { + if isFileLoggingEnabled { file = JustLog.FileDestination() - file.format = format - if let baseURL = self.baseUrlForFileLogging { - file.logFileURL = baseURL.appendingPathComponent(logFilename ?? "justeat.log", isDirectory: false) + file.format = configuration.logFormat + if let baseUrl = configuration.baseUrlForFileLogging { + let pathComponent = configuration.logFilename ?? "justeat.log" + file.logFileURL = baseUrl.appendingPathComponent(pathComponent, isDirectory: false) } internalLogger.addDestination(file) } - - // logstash - if enableLogstashLogging { - let socket = LogstashDestinationSocket(host: logstashHost, - port: logstashPort, - timeout: logstashTimeout, - logActivity: logLogstashSocketActivity, - allowUntrustedServer: allowUntrustedServer) - logstash = LogstashDestination(socket: socket, logActivity: logLogstashSocketActivity) - logstash.logzioToken = logzioToken + } + + private func setupLogstashDestination(with configuration: Configurable) { + if isLogstashLoggingEnabled { + let sender: LogstashDestinationSending = { + if configuration.logstashOverHTTP { + return LogstashDestinationHTTP(host: configuration.logstashHost, + port: configuration.logstashPort, + timeout: configuration.logstashTimeout, + logActivity: false) + } else { + return LogstashDestinationSocket(host: configuration.logstashHost, + port: configuration.logstashPort, + timeout: configuration.logstashTimeout, + logActivity: configuration.logLogstashSocketActivity, + allowUntrustedServer: configuration.allowUntrustedServer) + } + }() + logstash = LogstashDestination(sender: sender, logActivity: configuration.logLogstashSocketActivity) + logstash.logzioToken = configuration.logzioToken internalLogger.addDestination(logstash) - timer = RepeatingTimer(timeInterval: timerInterval) - timer?.eventHandler = { [weak self] in - guard let self = self else { return } - self.forceSend() - } - - timer?.cancelHandler = { [weak self] in - guard let self = self else { return } - self.cancelSending() - } - - timer?.run() + setupTimer(with: configuration) + } + } + + private func setupTimer(with configuraton: Configurable) { + timer = RepeatingTimer(timeInterval: configuraton.sendingInterval) + timer?.eventHandler = { [weak self] in + guard let self else { return } + self.forceSend() } - // custom logging - if enableCustomLogging, let customLogSender = customLogSender { + timer?.cancelHandler = { [weak self] in + guard let self else { return } + self.cancelSending() + } + + timer?.run() + } + + private func setupCustomDestination(with configuration: Configurable, customLogSender: CustomDestinationSender? = nil) { + if isCustomLoggingEnabled, let customLogSender = customLogSender { let customDestination = CustomDestination(sender: customLogSender) // always send all logs to custom destination customDestination.minLevel = .verbose internalLogger.addDestination(customDestination) - self.custom = customDestination + custom = customDestination } - - sendQueuedLogsIfNeeded() } private func sendQueuedLogsIfNeeded() { if !queuedLogs.isEmpty { - queuedLogs.forEach { queuedLog in - sendLogMessage(with: queuedLog.type, logMessage: queuedLog.message, queuedLog.file, queuedLog.function, queuedLog.line) + queuedLogs.forEach { log in + send(log) } queuedLogs.removeAll() } } public func forceSend(_ completionHandler: @escaping (_ error: Error?) -> Void = {_ in }) { - if enableLogstashLogging { + guard configuration != nil, + logMessageFormatter != nil else { + assertionFailure("Logger has not been configured yet") + return + } + + if isLogstashLoggingEnabled { logstash?.forceSend(completionHandler) } } public func cancelSending() { - if enableLogstashLogging { + guard configuration != nil, + logMessageFormatter != nil else { + assertionFailure("Logger has not been configured yet") + return + } + + if isLogstashLoggingEnabled { logstash?.cancelSending() } } } extension Logger: Logging { - - public func verbose(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) { - let file = String(describing: file) - let function = String(describing: function) - let updatedUserInfo = [logTypeKey: "verbose"].merged(with: userInfo ?? [String : String]()) - log(.verbose, message, error: error, userInfo: updatedUserInfo, file, function, line) - } - - public func debug(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) { - let file = String(describing: file) - let function = String(describing: function) - let updatedUserInfo = [logTypeKey: "debug"].merged(with: userInfo ?? [String : String]()) - log(.debug, message, error: error, userInfo: updatedUserInfo, file, function, line) - } - - public func info(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) { - let file = String(describing: file) - let function = String(describing: function) - let updatedUserInfo = [logTypeKey: "info"].merged(with: userInfo ?? [String : String]()) - log(.info, message, error: error, userInfo: updatedUserInfo, file, function, line) - } - - public func warning(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) { - let file = String(describing: file) - let function = String(describing: function) - let updatedUserInfo = [logTypeKey: "warning"].merged(with: userInfo ?? [String : String]()) - log(.warning, message, error: error, userInfo: updatedUserInfo, file, function, line) - } - - public func error(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) { - let file = String(describing: file) - let function = String(describing: function) - let updatedUserInfo = [logTypeKey: "error"].merged(with: userInfo ?? [String : Any]()) - log(.error, message, error: error, userInfo: updatedUserInfo, file, function, line) - } - - internal func log(_ type: LogType, _ message: String, error: NSError?, userInfo: [String : Any]?, _ file: String, _ function: String, _ line: UInt) { - - let messageToLog = logMessage(message, error: error, userInfo: userInfo, file, function, line) - + public func send(_ log: Loggable) { + guard configuration != nil, + let formatter = logMessageFormatter else { + assertionFailure("Logger has not been configured yet") + return + } + if !internalLogger.destinations.isEmpty { - sendLogMessage(with: type, logMessage: sanitize(messageToLog, type), file, function, line) + sendLogMessage(with: log.type, logMessage: sanitize(formatter.format(log), log.type), log.file, log.function, log.line) } else { - queuedLogs.append(QueuedLog(type: type, message: sanitize(message, type), file: file, function: function, line: line)) + queuedLogs.append(log) } } - internal func sendLogMessage(with type: LogType, logMessage: String, _ file: String, _ function: String, _ line: UInt) { + private func sendLogMessage(with type: LogType, logMessage: String, _ file: String, _ function: String, _ line: UInt) { switch type { case .error: internalLogger.error(logMessage, file, function, line: Int(line)) @@ -262,67 +220,3 @@ extension Logger: Logging { } } } - -extension Logger { - - internal func logMessage(_ message: String, error: NSError?, userInfo: [String : Any]?, currentDate: Date = Date(), _ file: String, _ function: String, _ line: UInt) -> String { - - let messageConst = "message" - let userInfoConst = "user_info" - let metadataConst = "metadata" - let errorsConst = "errors" - - var options = defaultUserInfo ?? [String : Any]() - - var retVal = [String : Any]() - retVal[messageConst] = message - retVal[metadataConst] = metadataDictionary(file, function, line, currentDate) - - if let userInfo = userInfo { - for (key, value) in userInfo { - _ = options.updateValue(value, forKey: key) - } - retVal[userInfoConst] = options - } - - if let error = error { - let errorDictionaries = error.disassociatedErrorChain() - .map { errorDictionary(for: $0) } - .filter { JSONSerialization.isValidJSONObject($0) } - retVal[errorsConst] = errorDictionaries - } - - return retVal.toJSON() ?? "" - } - - private func metadataDictionary(_ file: String, _ function: String, _ line: UInt, _ currentDate: Date) -> [String: Any] { - var fileMetadata = [String : String]() - - if let url = URL(string: file) { - fileMetadata[fileKey] = URLComponents(url: url, resolvingAgainstBaseURL: false)?.url?.pathComponents.last ?? file - } - - fileMetadata[functionKey] = function - fileMetadata[lineKey] = String(line) - - if let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"], let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] { - fileMetadata[appVersionKey] = "\(bundleShortVersion) (\(bundleVersion))" - } - - fileMetadata[iosVersionKey] = UIDevice.current.systemVersion - fileMetadata[deviceTypeKey] = UIDevice.current.platform() - fileMetadata[appBundleID] = Bundle.main.bundleIdentifier - fileMetadata[deviceTimestampKey] = "\(currentDate.timeIntervalSince1970)" - - return fileMetadata - } - - internal func errorDictionary(for error: NSError) -> [String : Any] { - let userInfoConst = "user_info" - var errorInfo = [errorDomain: error.domain, - errorCode: error.code] as [String : Any] - let errorUserInfo = error.humanReadableError().userInfo - errorInfo[userInfoConst] = errorUserInfo - return errorInfo - } -} diff --git a/JustLog/Classes/Logging.swift b/JustLog/Classes/Logging.swift deleted file mode 100644 index 2de420f..0000000 --- a/JustLog/Classes/Logging.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Logging.swift -// JustLog -// -// Created by Oliver Pearmain (Contractor) on 08/06/2017. -// Copyright © 2017 Just Eat. All rights reserved. -// - -import UIKit - -public protocol Logging { - - func verbose(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) - - func debug(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) - - func info(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) - - func warning(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) - - func error(_ message: String, error: NSError?, userInfo: [String : Any]?, _ file: StaticString, _ function: StaticString, _ line: UInt) - -} - - -// We have to use extension methods to set the default values since protocols don't direct support default values - -extension Logging { - - public func verbose(_ message: String, error: NSError? = nil, userInfo: [String : Any]? = nil, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line) { - verbose(message, error: error, userInfo: userInfo, file, function, line) - } - - public func debug(_ message: String, error: NSError? = nil, userInfo: [String : Any]? = nil, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line) { - debug(message, error: error, userInfo: userInfo, file, function, line) - } - - public func info(_ message: String, error: NSError? = nil, userInfo: [String : Any]? = nil, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line) { - info(message, error: error, userInfo: userInfo, file, function, line) - } - - public func warning(_ message: String, error: NSError? = nil, userInfo: [String : Any]? = nil, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line) { - warning(message, error: error, userInfo: userInfo, file, function, line) - } - - public func error(_ message: String, error: NSError? = nil, userInfo: [String : Any]? = nil, _ file: StaticString = #file, _ function: StaticString = #function, _ line: UInt = #line) { - self.error(message, error: error, userInfo: userInfo, file, function, line) - } - -} diff --git a/JustLog/Classes/LogstashDestination.swift b/JustLog/Classes/LogstashDestination.swift index 95bc36c..a700e07 100644 --- a/JustLog/Classes/LogstashDestination.swift +++ b/JustLog/Classes/LogstashDestination.swift @@ -1,10 +1,4 @@ -// // LogstashDestination.swift -// JustLog -// -// Created by Shabeer Hussain on 06/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation import SwiftyBeaver @@ -21,7 +15,7 @@ typealias LogTag = Int /// Once the writer has completed all operations, it calls the completion handler passing the logs that failed to push. /// Those logs are added back to `logsToShip` /// An optional `completionHandler` is called when all logs existing before the `forceSend` call have been tried to send once. -public class LogstashDestination: BaseDestination { +public class LogstashDestination: BaseDestination { /// Settings var shouldLogActivity: Bool = false @@ -35,19 +29,19 @@ public class LogstashDestination: BaseDestination { private let logzioTokenKey = "token" /// Socket - private let socket: LogstashDestinationSocketProtocol + private let sender: LogstashDestinationSending @available(*, unavailable) override init() { fatalError() } - public required init(socket: LogstashDestinationSocketProtocol, logActivity: Bool) { + required init(sender: LogstashDestinationSending, logActivity: Bool) { self.operationQueue = OperationQueue() self.operationQueue.underlyingQueue = dispatchQueue self.operationQueue.maxConcurrentOperationCount = 1 self.operationQueue.name = "com.justlog.LogstashDestination.operationQueue" - self.socket = socket + self.sender = sender super.init() self.shouldLogActivity = logActivity } @@ -59,10 +53,10 @@ public class LogstashDestination: BaseDestination { public func cancelSending() { self.operationQueue.cancelAllOperations() self.operationQueue.addOperation { [weak self] in - guard let self = self else { return } + guard let self else { return } self.logsToShip = [LogTag: LogContent]() } - self.socket.cancel() + self.sender.cancel() } // MARK: - Log dispatching @@ -88,7 +82,7 @@ public class LogstashDestination: BaseDestination { private func addLog(_ dict: LogContent) { operationQueue.addOperation { [weak self] in - guard let self = self else { return } + guard let self else { return } let time = mach_absolute_time() let logTag = Int(truncatingIfNeeded: time) @@ -98,18 +92,18 @@ public class LogstashDestination: BaseDestination { public func forceSend(_ completionHandler: @escaping (_ error: Error?) -> Void = {_ in }) { operationQueue.addOperation { [weak self] in - guard let self = self else { return } - let writer = LogstashDestinationWriter(socket: self.socket, shouldLogActivity: self.shouldLogActivity) + guard let self else { return } + let writer = LogstashDestinationWriter(sender: self.sender, shouldLogActivity: self.shouldLogActivity) let logsBatch = self.logsToShip self.logsToShip = [LogTag: LogContent]() writer.write(logs: logsBatch, queue: self.dispatchQueue) { [weak self] missing, error in - guard let self = self else { + guard let self else { completionHandler(error) return } if let unsent = missing { - self.logsToShip.merge(unsent) { lhs, rhs in lhs } + self.logsToShip.merge(unsent) { lhs, _ in lhs } self.printActivity("🔌 , \(unsent.count) failed tasks") } completionHandler(error) diff --git a/JustLog/Classes/LogstashDestinationHTTP.swift b/JustLog/Classes/LogstashDestinationHTTP.swift new file mode 100644 index 0000000..9c5b06b --- /dev/null +++ b/JustLog/Classes/LogstashDestinationHTTP.swift @@ -0,0 +1,66 @@ +// LogstashDestinationHTTP.swift + +import Foundation + +class LogstashDestinationHTTP: NSObject, LogstashDestinationSending { + + /// Settings + private let allowUntrustedServer: Bool + private let host: String + private let timeout: TimeInterval + private let logActivity: Bool + + private var session: URLSession + var scheduler: LogstashDestinationSendingScheduling + + required init(host: String, + port: UInt16, + timeout: TimeInterval, + logActivity: Bool, + allowUntrustedServer: Bool = false) { + + self.allowUntrustedServer = allowUntrustedServer + self.host = host + self.timeout = timeout + self.logActivity = logActivity + self.session = URLSession(configuration: .ephemeral) + self.scheduler = LogstashDestinationSendingScheduler() + super.init() + } + + /// Cancel all active tasks and invalidate the session + func cancel() { + self.session.invalidateAndCancel() + self.session = URLSession(configuration: .ephemeral) + } + + /// Create (and resume) stream tasks to send the logs provided to the server + func sendLogs(_ logs: [Int: [String: Any]], + transform: @escaping LogstashDestinationSendingTransform, + queue: DispatchQueue, + complete: @escaping LogstashDestinationSendingCompletion) { + Task { + var components = URLComponents() + components.scheme = "https" + components.host = self.host + components.path = "/applications/logging" + guard let url = components.url else { + complete([:]) + return + } + + let sendStatus = await scheduler.scheduleSend(logs) { log in + let logData = transform(log) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = logData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + _ = try await self.session.data(for: request) + } + + queue.async { + complete(sendStatus) + } + } + } +} diff --git a/JustLog/Classes/LogstashDestinationSending.swift b/JustLog/Classes/LogstashDestinationSending.swift new file mode 100644 index 0000000..e924f1a --- /dev/null +++ b/JustLog/Classes/LogstashDestinationSending.swift @@ -0,0 +1,18 @@ +// LogstashDestinationSending.swift + +import Foundation + +protocol LogstashDestinationSending { + + typealias LogstashDestinationSendingCompletion = ([Int: Error]) -> Void + typealias LogstashDestinationSendingTransform = ([String: Any]) -> Data + + init(host: String, port: UInt16, timeout: TimeInterval, logActivity: Bool, allowUntrustedServer: Bool) + + func cancel() + + func sendLogs(_ logs: [Int: [String: Any]], + transform: @escaping LogstashDestinationSendingTransform, + queue: DispatchQueue, + complete: @escaping LogstashDestinationSendingCompletion) +} diff --git a/JustLog/Classes/LogstashDestinationSendingScheduler.swift b/JustLog/Classes/LogstashDestinationSendingScheduler.swift new file mode 100644 index 0000000..47300c8 --- /dev/null +++ b/JustLog/Classes/LogstashDestinationSendingScheduler.swift @@ -0,0 +1,42 @@ +// LogstashDestinationSendingScheduler.swift + +import Foundation + +protocol LogstashDestinationSendingScheduling { + typealias SendClosure = ([String: Any]) async throws -> Void + func scheduleSend(_ logs: [Int: [String: Any]], + send: @escaping SendClosure) async -> [Int: Error] +} + +struct LogstashDestinationSendingScheduler: LogstashDestinationSendingScheduling { + struct LogResult { + let tag: Int + let error: Error? + } + + func scheduleSend(_ logs: [Int: [String: Any]], + send: @escaping SendClosure) async -> [Int: Error] { + let sendStatus = await withTaskGroup(of: LogResult.self) { group in + for log in logs.sorted(by: { $0.0 < $1.0 }) { + group.addTask { + let tag = log.0 + do { + try await send(log.1) + return LogResult(tag: tag, error: nil) + } catch { + return LogResult(tag: tag, error: error) + } + } + } + + var results = [Int: Error]() + for await result in group { + if let error = result.error { + results[result.tag] = error + } + } + return results + } + return sendStatus + } +} diff --git a/JustLog/Classes/LogstashDestinationSocket.swift b/JustLog/Classes/LogstashDestinationSocket.swift index 7778a5c..f6b7066 100644 --- a/JustLog/Classes/LogstashDestinationSocket.swift +++ b/JustLog/Classes/LogstashDestinationSocket.swift @@ -1,30 +1,8 @@ -// // LogstashDestinationSocket.swift -// JustLog -// -// Created by Antonio Strijdom on 29/06/2020. -// Copyright © 2020 Just Eat. All rights reserved. -// import Foundation -public protocol LogstashDestinationSocketProtocol { - - typealias LogstashDestinationSocketProtocolCompletion = ([Int: Error]) -> Void - typealias LogstashDestinationSocketProtocolTransform = ([String: Any]) -> Data - - init(host: String, port: UInt16, timeout: TimeInterval, logActivity: Bool, allowUntrustedServer: Bool) - - func cancel() - - func sendLogs(_ logs: [Int: [String: Any]], - transform: @escaping LogstashDestinationSocketProtocolTransform, - queue: DispatchQueue, - complete: @escaping LogstashDestinationSocketProtocolCompletion) -} - -class LogstashDestinationSocket: NSObject, LogstashDestinationSocketProtocol { - +class LogstashDestinationSocket: NSObject, LogstashDestinationSending { /// Settings private let allowUntrustedServer: Bool @@ -68,11 +46,11 @@ class LogstashDestinationSocket: NSObject, LogstashDestinationSocketProtocol { /// Create (and resume) stream tasks to send the logs provided to the server func sendLogs(_ logs: [Int: [String: Any]], - transform: @escaping LogstashDestinationSocketProtocolTransform, + transform: @escaping LogstashDestinationSendingTransform, queue: DispatchQueue, - complete: @escaping LogstashDestinationSocketProtocolCompletion) { + complete: @escaping LogstashDestinationSendingCompletion) { dispatchQueue.async { [weak self] in - guard let self = self else { + guard let self else { complete([:]) return } @@ -88,7 +66,7 @@ class LogstashDestinationSocket: NSObject, LogstashDestinationSocketProtocol { let logData = transform(log.1) dispatchGroup.enter() task.write(logData, timeout: self.timeout) { [weak self] error in - guard let self = self else { + guard let self else { dispatchGroup.leave() return } diff --git a/JustLog/Classes/LogstashDestinationWriter.swift b/JustLog/Classes/LogstashDestinationWriter.swift index 2a9ae61..f210716 100644 --- a/JustLog/Classes/LogstashDestinationWriter.swift +++ b/JustLog/Classes/LogstashDestinationWriter.swift @@ -1,20 +1,15 @@ -// // LogstashDestinationWriter.swift -// JustLog -// -// Created by Luigi Parpinel on 25/05/21. -// import Foundation class LogstashDestinationWriter { - private let socket: LogstashDestinationSocketProtocol + private let sender: LogstashDestinationSending private let shouldLogActivity: Bool - init(socket: LogstashDestinationSocketProtocol, shouldLogActivity: Bool) { - self.socket = socket + init(sender: LogstashDestinationSending, shouldLogActivity: Bool) { + self.sender = sender self.shouldLogActivity = shouldLogActivity } @@ -29,7 +24,7 @@ class LogstashDestinationWriter { } let shouldLogActivity = self.shouldLogActivity - socket.sendLogs(logs, transform: transformLogToData, queue: queue) { status in + sender.sendLogs(logs, transform: transformLogToData, queue: queue) { status in let unsentLog = logs.filter { status.keys.contains($0.key) } if unsentLog.isEmpty { Self.printActivity("🔌 , did write tags: \(logs.keys)", shouldLogActivity: shouldLogActivity) @@ -49,7 +44,7 @@ class LogstashDestinationWriter { private func transformLogToData(_ dict: LogContent) -> Data { do { - var data = try JSONSerialization.data(withJSONObject:dict, options:[]) + var data = try JSONSerialization.data(withJSONObject: dict, options: []) if let encodedData = "\n".data(using: String.Encoding.utf8) { data.append(encodedData) } diff --git a/JustLog/Classes/RepeatingTimer.swift b/JustLog/Classes/RepeatingTimer.swift index 8fb7c68..b2a4d3b 100644 --- a/JustLog/Classes/RepeatingTimer.swift +++ b/JustLog/Classes/RepeatingTimer.swift @@ -1,14 +1,9 @@ -// // RepeatingTimer.swift -// JustLog -// -// Created by Junaid Younus on 08/06/2020. -// Copyright © 2020 Just Eat. All rights reserved. -// -// https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 import Foundation +/// https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 + /// RepeatingTimer mimics the API of DispatchSourceTimer but in a way that prevents crashes that occur from calling resume multiple times on a timer that is /// already resumed (noted by https://github.com/SiftScience/sift-ios/issues/52) internal class RepeatingTimer { diff --git a/JustLog/Extensions/Data+Representation.swift b/JustLog/Extensions/Data+Representation.swift index d2be6e7..da8d60b 100644 --- a/JustLog/Extensions/Data+Representation.swift +++ b/JustLog/Extensions/Data+Representation.swift @@ -1,16 +1,10 @@ -// // Data+Representation.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation extension Data { func stringRepresentation() -> String { - return String(data: self, encoding: .utf8) ?? "" + String(data: self, encoding: .utf8) ?? "" } } diff --git a/JustLog/Extensions/Dictionary+Flattening.swift b/JustLog/Extensions/Dictionary+Flattening.swift index 31ef5a2..d9b2a47 100644 --- a/JustLog/Extensions/Dictionary+Flattening.swift +++ b/JustLog/Extensions/Dictionary+Flattening.swift @@ -1,10 +1,4 @@ -// // Dictionary+Flattening.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation @@ -15,9 +9,9 @@ extension Dictionary where Key == String { /// - override: Overrides the keys and the values. /// - encapsulateFlatten: keeps the keys and adds the values to an array. - func flattened() -> [String : Any] { + func flattened() -> [String: Any] { - var retVal = [String : Any]() + var retVal = [String: Any]() for (k, v) in self { switch v { @@ -26,15 +20,15 @@ extension Dictionary where Key == String { is Double, is Bool: retVal.updateValue(v, forKey: k) - case is [String : Any]: - if let value: [String : Any] = v as? [String : Any] { + case is [String: Any]: + if let value: [String: Any] = v as? [String: Any] { let inner = value.flattened() retVal = retVal.merged(with: inner) } else { continue } - case is Array: + case is [Any]: retVal.updateValue(String(describing: v), forKey: k) case is NSError: if let inner = v as? NSError { @@ -51,8 +45,8 @@ extension Dictionary where Key == String { return retVal } - func merged(with dictionary: [String : Any]) -> [String : Any] { - var retValue = self as [String :Any] + func merged(with dictionary: [String: Any]) -> [String: Any] { + var retValue = self as [String: Any] dictionary.forEach { (key, value) in retValue.updateValue(value, forKey: key) } diff --git a/JustLog/Extensions/Dictionary+JSON.swift b/JustLog/Extensions/Dictionary+JSON.swift index f58d935..41e4786 100644 --- a/JustLog/Extensions/Dictionary+JSON.swift +++ b/JustLog/Extensions/Dictionary+JSON.swift @@ -1,10 +1,4 @@ -// // Dictionary+JSON.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation @@ -15,9 +9,9 @@ extension Dictionary { /// - Returns: The String representation of the dictionary func toJSON() -> String? { - var json: String? = nil + var json: String? - if (JSONSerialization.isValidJSONObject(self)) { + if JSONSerialization.isValidJSONObject(self) { do { json = try JSONSerialization.data(withJSONObject: self).stringRepresentation() } catch { @@ -27,5 +21,4 @@ extension Dictionary { return json } - } diff --git a/JustLog/Extensions/Logger+ObjC.swift b/JustLog/Extensions/Logger+ObjC.swift deleted file mode 100644 index b5294d9..0000000 --- a/JustLog/Extensions/Logger+ObjC.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// Logger+ObjC.swift -// JustLog -// -// Created by Alberto De Bortoli on 20/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// - -import Foundation - -extension Logger { - - //MARK: verbose - - @objc public func verbose_objc(_ message: String) { - self.verbose(message) - } - - @objc public func verbose_objc(_ message: String, error: NSError?) { - self.verbose(message, error: error) - } - - @objc public func verbose_objc(_ message: String, userInfo: [String : Any]?) { - self.verbose(message, userInfo: userInfo) - } - - @objc public func verbose_objc(_ message: String, error: NSError?, userInfo: [String : Any]?) { - self.verbose(message, error: error, userInfo: userInfo) - } - - //MARK: debug - - @objc public func debug_objc(_ message: String) { - self.debug(message) - } - - @objc public func debug_objc(_ message: String, error: NSError?) { - self.debug(message, error: error) - } - - @objc public func debug_objc(_ message: String, userInfo: [String : Any]?) { - self.debug(message, userInfo: userInfo) - } - - @objc public func debug_objc(_ message: String, error: NSError?, userInfo: [String : Any]?) { - self.debug(message, error: error, userInfo: userInfo) - } - - //MARK: info - - @objc public func info_objc(_ message: String) { - self.info(message) - } - - @objc public func info_objc(_ message: String, error: NSError?) { - self.info(message, error: error) - } - - @objc public func info_objc(_ message: String, userInfo: [String : Any]?) { - self.info(message, userInfo: userInfo) - } - - @objc public func info_objc(_ message: String, error: NSError?, userInfo: [String : Any]?) { - self.info(message, error: error, userInfo: userInfo) - } - - //MARK: warning - - @objc public func warning_objc(_ message: String) { - self.warning(message) - } - - @objc public func warning_objc(_ message: String, error: NSError?) { - self.warning(message, error: error) - } - - @objc public func warning_objc(_ message: String, userInfo: [String : Any]?) { - self.warning(message, userInfo: userInfo) - } - - @objc public func warning_objc(_ message: String, error: NSError?, userInfo: [String : Any]?) { - self.warning(message, error: error, userInfo: userInfo) - } - - //MARK: error - - @objc public func error_objc(_ message: String) { - self.error(message) - } - - @objc public func error_objc(_ message: String, error: NSError?) { - self.error(message, error: error) - } - - @objc public func error_objc(_ message: String, userInfo: [String : Any]?) { - self.error(message, userInfo: userInfo) - } - - @objc public func error_objc(_ message: String, error: NSError?, userInfo: [String : Any]?) { - self.error(message, error: error, userInfo: userInfo) - } -} diff --git a/JustLog/Extensions/NSError+Readability.swift b/JustLog/Extensions/NSError+Readability.swift index 5b44e0b..771c6d2 100644 --- a/JustLog/Extensions/NSError+Readability.swift +++ b/JustLog/Extensions/NSError+Readability.swift @@ -1,16 +1,9 @@ -// // NSError+Readability.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// -// import Foundation extension NSError { - /// Parses Data values in the user info key as String and recursively does the same to all associated underying errors. /// /// - Returns: A copy of the error including @@ -20,7 +13,7 @@ extension NSError { for (key, value) in userInfo { - switch (value) { + switch value { case let string as String: flattenedUserInfo[key] = string case let data as Data: diff --git a/JustLog/Extensions/NSError+UnderlyingErrors.swift b/JustLog/Extensions/NSError+UnderlyingErrors.swift index 268bc41..31a5283 100644 --- a/JustLog/Extensions/NSError+UnderlyingErrors.swift +++ b/JustLog/Extensions/NSError+UnderlyingErrors.swift @@ -1,10 +1,4 @@ -// // NSError+UnderlyingErrors.swift -// Pods -// -// Created by Alkiviadis Papadakis on 22/08/2017. -// -// import Foundation extension NSError { diff --git a/JustLog/Extensions/String+Conversion.swift b/JustLog/Extensions/String+Conversion.swift index 60ecd26..5a6375d 100644 --- a/JustLog/Extensions/String+Conversion.swift +++ b/JustLog/Extensions/String+Conversion.swift @@ -1,25 +1,18 @@ -// // String+Conversion.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import Foundation extension String { - func toDictionary() -> [String : Any]? { + func toDictionary() -> [String: Any]? { if let data = self.data(using: .utf8) { do { - return try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] } catch { print(error.localizedDescription) } } return nil } - } diff --git a/JustLog/Extensions/UIDevice+Info.swift b/JustLog/Extensions/UIDevice+Info.swift index d1623a5..d162312 100644 --- a/JustLog/Extensions/UIDevice+Info.swift +++ b/JustLog/Extensions/UIDevice+Info.swift @@ -1,10 +1,4 @@ -// // UIDevice+Info.swift -// JustLog -// -// Created by Alberto De Bortoli on 15/12/2016. -// Copyright © 2017 Just Eat. All rights reserved. -// import UIKit @@ -12,9 +6,9 @@ extension UIDevice { func platform() -> String { var size: size_t = 0 - sysctlbyname("hw.machine", nil, &size, nil, 0); - var machine = [CChar](repeating: 0, count: Int(size)) - sysctlbyname("hw.machine", &machine, &size, nil, 0); + sysctlbyname("hw.machine", nil, &size, nil, 0) + var machine = [CChar](repeating: 0, count: Int(size)) + sysctlbyname("hw.machine", &machine, &size, nil, 0) return String(cString: machine) } } diff --git a/JustLog/Protocols/Configurable.swift b/JustLog/Protocols/Configurable.swift new file mode 100644 index 0000000..5705b37 --- /dev/null +++ b/JustLog/Protocols/Configurable.swift @@ -0,0 +1,29 @@ +// Configurable.swift + +import Foundation + +public protocol Configurable { + + // please see https://docs.swiftybeaver.com/article/20-custom-format for variable log format options + var logFormat: String { get set } + var sendingInterval: TimeInterval { get set } + + // file + var logFilename: String? { get set } + var baseUrlForFileLogging: URL? { get set } + + // logstash + var allowUntrustedServer: Bool { get set } + var logstashHost: String { get set } + var logstashPort: UInt16 { get set } + var logstashTimeout: TimeInterval { get set } + var logLogstashSocketActivity: Bool { get set } + var logzioToken: String? { get set } + var logstashOverHTTP: Bool { get set } + + // destinations + var isConsoleLoggingEnabled: Bool { get set } + var isFileLoggingEnabled: Bool { get set } + var isLogstashLoggingEnabled: Bool { get set } + var isCustomLoggingEnabled: Bool { get set } +} diff --git a/JustLog/Protocols/LogMessageFormatting.swift b/JustLog/Protocols/LogMessageFormatting.swift new file mode 100644 index 0000000..05e52cf --- /dev/null +++ b/JustLog/Protocols/LogMessageFormatting.swift @@ -0,0 +1,32 @@ +// LogMessageFormatting.swift + +import Foundation + +public typealias DefaultLogMetadata = [String: Any] + +public protocol FormatterKeysProviding { + + var logTypeKey: String { get set } + var appBundleIDKey: String { get set } + var appVersionKey: String { get set } + var deviceTimestampKey: String { get set } + var deviceTypeKey: String { get set } + var errorCodeKey: String { get set } + var errorDomainKey: String { get set } + var errorsKey: String { get set } + var fileKey: String { get set } + var functionKey: String { get set } + var iosVersionKey: String { get set } + var lineKey: String { get set } + var messageKey: String { get set } + var metadataKey: String { get set } + var userInfoKey: String { get set } +} + +public protocol LogMessageFormatting { + + var keys: FormatterKeysProviding { get set } + var defaultLogMetadata: DefaultLogMetadata? { get set } + + func format(_ log: Loggable) -> String +} diff --git a/JustLog/Protocols/Loggable.swift b/JustLog/Protocols/Loggable.swift new file mode 100644 index 0000000..adf49d8 --- /dev/null +++ b/JustLog/Protocols/Loggable.swift @@ -0,0 +1,22 @@ +// Loggable.swift + +import Foundation + +public enum LogType: String { + case debug + case warning + case verbose + case error + case info +} + +public protocol Loggable { + + var type: LogType { get } + var message: String { get } + var error: NSError? { get } + var customData: [String: Any]? { get set } + var file: String { get } + var function: String { get } + var line: UInt { get } +} diff --git a/JustLog/Protocols/Logging.swift b/JustLog/Protocols/Logging.swift new file mode 100644 index 0000000..5c4484b --- /dev/null +++ b/JustLog/Protocols/Logging.swift @@ -0,0 +1,8 @@ +// Logging.swift + +import Foundation + +public protocol Logging { + + func send(_ log: Loggable) +} diff --git a/JustLog/Supporting Files/Info.plist b/JustLog/Supporting Files/Info.plist deleted file mode 100644 index 3d809ff..0000000 --- a/JustLog/Supporting Files/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 2.1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/JustLog/Supporting Files/JustLog.h b/JustLog/Supporting Files/JustLog.h deleted file mode 100644 index faae69b..0000000 --- a/JustLog/Supporting Files/JustLog.h +++ /dev/null @@ -1,11 +0,0 @@ -#import - -//! Project version number for JustLog. -FOUNDATION_EXPORT double JustLogVersionNumber; - -//! Project version string for JustLog. -FOUNDATION_EXPORT const unsigned char JustLogVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Package.resolved b/Package.resolved index 7567066..bd52d2e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,14 @@ { - "object": { - "pins": [ - { - "package": "SwiftyBeaver", - "repositoryURL": "https://github.com/SwiftyBeaver/SwiftyBeaver", - "state": { - "branch": null, - "revision": "12b5acf96d98f91d50de447369bd18df74600f1a", - "version": "1.9.6" - } + "pins" : [ + { + "identity" : "swiftybeaver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftyBeaver/SwiftyBeaver", + "state" : { + "revision" : "12b5acf96d98f91d50de447369bd18df74600f1a", + "version" : "1.9.6" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 478d288..0d741ad 100644 --- a/Package.swift +++ b/Package.swift @@ -1,29 +1,40 @@ -// swift-tools-version:5.3 +// swift-tools-version: 5.7 + +// This file was automatically generated by PackageGenerator and untracked +// PLEASE DO NOT EDIT MANUALLY import PackageDescription let package = Package( name: "JustLog", - platforms: [ - .iOS("13.4"), - .tvOS("13.4") - ], + defaultLocalization: "en", + platforms: [.iOS(.v15)], products: [ .library( name: "JustLog", - targets: ["JustLog"]), + targets: ["JustLog"] + ), ], dependencies: [ .package( url: "https://github.com/SwiftyBeaver/SwiftyBeaver", - .exact("1.9.6") - ) + exact: "1.9.6" + ), ], targets: [ .target( name: "JustLog", - dependencies: ["SwiftyBeaver"], - path: "JustLog/", - exclude: ["Supporting Files/Info.plist", "Supporting Files/JustLog.h"]), + dependencies: [ + .product(name: "SwiftyBeaver", package: "SwiftyBeaver"), + ], + path: "JustLog" + ), + .testTarget( + name: "JustLogTests", + dependencies: [ + .byName(name: "JustLog"), + ], + path: "Example/Tests" + ), ] )