diff --git a/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift b/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift index 610ad4e1..1ca37379 100644 --- a/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift +++ b/Examples/apps/DestinationsExample/DestinationsExample/ViewController.swift @@ -84,7 +84,7 @@ class ViewController: UIViewController { case .alias: aliasEvent() case .none: - print("Failed to establish event type") + analytics?.log(message: "Failed to establish event type", kind: .error) } clearAll() diff --git a/Examples/apps/SegmentExtensionsExample/SegmentExtensionsExample/LoginViewController.swift b/Examples/apps/SegmentExtensionsExample/SegmentExtensionsExample/LoginViewController.swift index 228e42d6..b5ce4969 100644 --- a/Examples/apps/SegmentExtensionsExample/SegmentExtensionsExample/LoginViewController.swift +++ b/Examples/apps/SegmentExtensionsExample/SegmentExtensionsExample/LoginViewController.swift @@ -107,7 +107,7 @@ extension LoginViewController: ASAuthorizationControllerDelegate { analytics?.track(name: "Saved to Keychain") } catch { //handle error and optionally track it - print("Unable to save userId to keychain.") + analytics?.log(message: "Unable to save userId to keychain.", kind: .error) } } @@ -140,7 +140,7 @@ extension LoginViewController: ASAuthorizationControllerDelegate { func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { //handle error - print(error) + analytics?.log(message: error, king: .error) } } diff --git a/Examples/apps/SegmentExtensionsExample/Supporting Files/KeychainItem.swift b/Examples/apps/SegmentExtensionsExample/Supporting Files/KeychainItem.swift index 715958a3..3ff5520c 100644 --- a/Examples/apps/SegmentExtensionsExample/Supporting Files/KeychainItem.swift +++ b/Examples/apps/SegmentExtensionsExample/Supporting Files/KeychainItem.swift @@ -143,7 +143,7 @@ struct KeychainItem { do { try KeychainItem(service: "co.alancharles.SegmentExtensionsExample", account: "userIdentifier").deleteItem() } catch { - print("Unable to delete userIdentifier from keychain") + analytics?.log(message: "Unable to delete userIdentifier from keychain", kind: .error) } } } diff --git a/Examples/destination_plugins/AppsFlyerDestination.swift b/Examples/destination_plugins/AppsFlyerDestination.swift index 9733e3b3..3151096e 100644 --- a/Examples/destination_plugins/AppsFlyerDestination.swift +++ b/Examples/destination_plugins/AppsFlyerDestination.swift @@ -254,7 +254,7 @@ extension AppsFlyerDestination: AppsFlyerLibDelegate { extension AppsFlyerDestination: DeepLinkDelegate, UIApplicationDelegate { func didResolveDeepLink(_ result: DeepLinkResult) { - print(result) + analytics?.log(message: "AppsFlyer Deeplink Result: \(result)") switch result.status { case .notFound: analytics?.log(message: "AppsFlyer: Deep link not found") diff --git a/Examples/other_plugins/ConsoleLogger.swift b/Examples/other_plugins/ConsoleLogger.swift index 02f03d7f..1e0d2726 100644 --- a/Examples/other_plugins/ConsoleLogger.swift +++ b/Examples/other_plugins/ConsoleLogger.swift @@ -53,8 +53,8 @@ class ConsoleLogger: Plugin { // we want to log every event, so lets override `execute`. func execute(event: T?) -> T? { if let json = event?.prettyPrint() { - print("event received on instance: \(name)") - print("\(json)\n") + analytics?.log(message: "event received on instance: \(name)") + analytics?.log(message: "\(json)\n") } return event } @@ -62,8 +62,7 @@ class ConsoleLogger: Plugin { // we also want to know when settings are retrieved or changed. func update(settings: Settings) { let json = settings.prettyPrint() - print("settings updated on instance: \(name)") - print("\(json)\n") + analytics?.log(message: "settings updated on instance: \(name)\nPayload: \(json)") } } diff --git a/Examples/other_plugins/UIKitScreenTracking.swift b/Examples/other_plugins/UIKitScreenTracking.swift index 670c07c9..cdb914f1 100644 --- a/Examples/other_plugins/UIKitScreenTracking.swift +++ b/Examples/other_plugins/UIKitScreenTracking.swift @@ -122,7 +122,7 @@ extension UIViewController { guard let top = Self.seg__visibleViewController(activeController()) else { return } var name = String(describing: top.self.classForCoder).replacingOccurrences(of: "ViewController", with: "") - print(name) + analytics?.log(message: "Auto-tracking Screen: \(name)") // name could've been just "ViewController"... if name.count == 0 { name = top.title ?? "Unknown" diff --git a/Segment.xcodeproj/project.pbxproj b/Segment.xcodeproj/project.pbxproj index 2804259f..94e03506 100644 --- a/Segment.xcodeproj/project.pbxproj +++ b/Segment.xcodeproj/project.pbxproj @@ -49,12 +49,14 @@ 46FE4D1D25A7A850003A7362 /* Storage_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46FE4D1C25A7A850003A7362 /* Storage_Tests.swift */; }; 9620862C2575C0C800314F8D /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620862B2575C0C800314F8D /* Events.swift */; }; 96208650257AA83E00314F8D /* iOSLifecycleMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */; }; + 96259F8326CEF526008AE301 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96259F8226CEF526008AE301 /* Logger.swift */; }; + 96259F8626CF1D45008AE301 /* ConsoleTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96259F8526CF1D45008AE301 /* ConsoleTarget.swift */; }; 966945D7259BDCDD00271339 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40ED259A7311008EB0B6 /* HTTPClient.swift */; }; 967C40DA258D472C008EB0B6 /* Logger_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40D9258D472C008EB0B6 /* Logger_Tests.swift */; }; 967C40E3258D4DAF008EB0B6 /* Metrics_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */; }; 9692724E25A4E5B7009B5298 /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9692724D25A4E5B7009B5298 /* Startup.swift */; }; 9692726825A583A6009B5298 /* SegmentDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9692726725A583A6009B5298 /* SegmentDestination.swift */; }; - 96C33A9C25880A5E00F3D538 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33A9B25880A5E00F3D538 /* Logger.swift */; }; + 96C33A9C25880A5E00F3D538 /* SegmentLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33A9B25880A5E00F3D538 /* SegmentLogger.swift */; }; 96C33AAC25892D6D00F3D538 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33AAB25892D6D00F3D538 /* Metrics.swift */; }; 96C33AB1258961F500F3D538 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C33AB0258961F500F3D538 /* Settings.swift */; }; 96DBF37B26F39B5500724B0B /* Timeline_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */; }; @@ -144,13 +146,15 @@ 9620862B2575C0C800314F8D /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = ""; }; 962086482579CCC200314F8D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 9620864F257AA83E00314F8D /* iOSLifecycleMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLifecycleMonitor.swift; sourceTree = ""; }; + 96259F8226CEF526008AE301 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 96259F8526CF1D45008AE301 /* ConsoleTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleTarget.swift; sourceTree = ""; }; 9679DD6226EFF00800A6933C /* ExampleDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleDestination.swift; sourceTree = ""; }; 967C40D9258D472C008EB0B6 /* Logger_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger_Tests.swift; sourceTree = ""; }; 967C40E2258D4DAF008EB0B6 /* Metrics_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metrics_Tests.swift; sourceTree = ""; }; 967C40ED259A7311008EB0B6 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 9692724D25A4E5B7009B5298 /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; }; 9692726725A583A6009B5298 /* SegmentDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentDestination.swift; sourceTree = ""; }; - 96C33A9B25880A5E00F3D538 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 96C33A9B25880A5E00F3D538 /* SegmentLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentLogger.swift; sourceTree = ""; }; 96C33AAB25892D6D00F3D538 /* Metrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metrics.swift; sourceTree = ""; }; 96C33AB0258961F500F3D538 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 96DBF37A26F39B5500724B0B /* Timeline_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline_Tests.swift; sourceTree = ""; }; @@ -277,8 +281,8 @@ 9620864E257AA82900314F8D /* Plugins */ = { isa = PBXGroup; children = ( + 96259F8426CEF534008AE301 /* Logger */, 969A533D25B0D510009227D9 /* Platforms */, - 96C33A9B25880A5E00F3D538 /* Logger.swift */, 96C33AAB25892D6D00F3D538 /* Metrics.swift */, 46A018C125E5857D00F9CCD8 /* Context.swift */, 9692726725A583A6009B5298 /* SegmentDestination.swift */, @@ -288,6 +292,16 @@ path = Plugins; sourceTree = ""; }; + 96259F8426CEF534008AE301 /* Logger */ = { + isa = PBXGroup; + children = ( + 96259F8226CEF526008AE301 /* Logger.swift */, + 96C33A9B25880A5E00F3D538 /* SegmentLogger.swift */, + 96259F8526CF1D45008AE301 /* ConsoleTarget.swift */, + ); + path = Logger; + sourceTree = ""; + }; 969A533D25B0D510009227D9 /* Platforms */ = { isa = PBXGroup; children = ( @@ -521,6 +535,7 @@ 4621080C2605332D00EBC4A8 /* KeyPath.swift in Sources */, A31A16262576B6F200C9CDDF /* Timeline.swift in Sources */, 96C33AB1258961F500F3D538 /* Settings.swift in Sources */, + 96259F8626CF1D45008AE301 /* ConsoleTarget.swift in Sources */, 46E382E72654429A00BA2502 /* Utils.swift in Sources */, A31A16B225781CB400C9CDDF /* JSON.swift in Sources */, 46022771261F7A4800A9E913 /* Atomic.swift in Sources */, @@ -535,12 +550,13 @@ 4663C729267A799100ADDD1A /* QueueTimer.swift in Sources */, 46FE4C9C25A3F41C003A7362 /* LinuxLifecycleMonitor.swift in Sources */, 460227422612987300A9E913 /* watchOSLifecycleEvents.swift in Sources */, + 96259F8326CEF526008AE301 /* Logger.swift in Sources */, 46F7485E26C718710042798E /* ObjCConfiguration.swift in Sources */, A31A162F2576B73F00C9CDDF /* State.swift in Sources */, 9692726825A583A6009B5298 /* SegmentDestination.swift in Sources */, 4602276C261E7BF900A9E913 /* iOSDelegation.swift in Sources */, 46A018D425E6C9C200F9CCD8 /* LinuxUtils.swift in Sources */, - 96C33A9C25880A5E00F3D538 /* Logger.swift in Sources */, + 96C33A9C25880A5E00F3D538 /* SegmentLogger.swift in Sources */, 46FE4C9725A3F35E003A7362 /* macOSLifecycleMonitor.swift in Sources */, 9620862C2575C0C800314F8D /* Events.swift in Sources */, A3AEE1882581A8F1002386EB /* Deprecations.swift in Sources */, diff --git a/Sources/Segment/Plugins/Logger.swift b/Sources/Segment/Plugins/Logger.swift deleted file mode 100644 index a6ae0f92..00000000 --- a/Sources/Segment/Plugins/Logger.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Logger.swift -// Segment -// -// Created by Cody Garvin on 12/14/20. -// - -import Foundation - -public enum LogType: Int { - case error = 0 // Not Verbose - case warning // Semi-verbose - case info // Verbose -} - -class Logger: UtilityPlugin { - public var filterType = LogType.info - - let type = PluginType.utility - var analytics: Analytics? - - private var messages = [LogMessage]() - - required init() { } - - func log(type: LogType, message: String, event: RawEvent?) { - print("\(type) -- Message: \(message)") - let message = LogMessage(type: type, message: message, event: event) - messages.append(message) - } - - func flush() { - print("Flushing All Logs") - for message in messages { - if message.type.rawValue <= filterType.rawValue { - print("[\(message.type)] \(message.message)") - } - } - messages.removeAll() - } -} - -fileprivate struct LogMessage { - let type: LogType - let message: String - let event: RawEvent? -} - -extension Analytics { - - /// Log a generic message to the system with a possible log type. If a type is not supplied the system - /// will use the current default setting (.info). - /// - Parameters: - /// - message: The message to be stored in the logging system. - /// - event: The event associated with the log (optional). - /// - type: The filter type for the message. If nil, defaults to logger setting. - public func log(message: String, event: RawEvent? = nil, type: LogType? = nil) { - apply { (potentialLogger) in - - if let logger = potentialLogger as? Logger { - - var loggingType = logger.filterType - if let type = type { - loggingType = type - } - logger.log(type: loggingType, message: message, event: event) - } - } - } - - public func logFlush() { - apply { (potentialLogger) in - if let logger = potentialLogger as? Logger { - logger.flush() - } - } - } -} diff --git a/Sources/Segment/Plugins/Logger/ConsoleTarget.swift b/Sources/Segment/Plugins/Logger/ConsoleTarget.swift new file mode 100644 index 00000000..a5940fc8 --- /dev/null +++ b/Sources/Segment/Plugins/Logger/ConsoleTarget.swift @@ -0,0 +1,18 @@ +// +// ConsoleTarget.swift +// ConsoleTarget +// +// Created by Cody Garvin on 8/19/21. +// + +import Foundation + +class ConsoleTarget: LogTarget { + func parseLog(_ log: LogMessage) { + var metadata = "" + if let function = log.function, let line = log.line { + metadata = " - \(function):\(line)" + } + print("[Segment \(log.kind.toString())\(metadata)]\n\(log.message)\n") + } +} diff --git a/Sources/Segment/Plugins/Logger/Logger.swift b/Sources/Segment/Plugins/Logger/Logger.swift new file mode 100644 index 00000000..a8ca5a83 --- /dev/null +++ b/Sources/Segment/Plugins/Logger/Logger.swift @@ -0,0 +1,207 @@ +// +// Logger.swift +// Logger +// +// Created by Cody Garvin on 8/19/21. +// + +import Foundation + +// MARK: - Logging Types + +/// The foundation for building out a special logger. If logs need to be directed to a certain area, this is the +/// interface to start off with. For instance a console logger, a networking logger or offline storage logger +/// would all start off with LogTarget. +public protocol LogTarget { + + /// Implement this method to process logging messages. This is where the logic for the target will be + /// added. Feel free to add your own data queueing and offline storage. + /// - important: Use the Segment Network stack for Segment library compatibility and simplicity. + func parseLog(_ log: LogMessage) + + /// Optional method to implement. This helps respond to potential queueing events being flushed out. + /// Perhaps responding to backgrounding or networking events, this gives a chance to empty a queue + /// or pump a firehose of logs. + func flush() +} + +/// Used for analytics.log() types. This lets the system know what to filter on and how to set priorities. +public enum LogFilterKind: Int { + case error = 0 // Not Verbose (fail cases | non-recoverable errors) + case warning // Semi-verbose (deprecations | potential issues) + case debug // Verbose (everything of interest) + + func toString() -> String { + switch (self) { + case .error: + return "ERROR" + case .warning: + return "Warning" + case .debug: + return "Debug" + } + } +} + +/// The Segment logging system has three types of logs: log, metric and history. When adding a target that +/// responds to logs, it is possible to adhere to 1 to many. In other words, a LoggingType can be .log & +/// .history. This is used to tell which targets logs are directed to. +public struct LoggingType: Hashable { + + public enum LogDestination { + case log + case metric + case history + } + + /// Convenience .log logging type + static let log = LoggingType(types: [.log]) + /// Convenience .metric logging type + static let metric = LoggingType(types: [.metric]) + /// Convenience .history logging type + static let history = LoggingType(types: [.history]) + + + /// Designated initializer for LoggingType. Add all the destinations this LoggingType should support. + /// - Parameter types: The LoggingDestination(s) that this LoggingType will support. + public init(types: [LogDestination]) { + // TODO: Failable scenario if types empty + self.allTypes = types + } + + // - Private Properties and Methods + private let allTypes: [LogDestination] + + /// Convience method to find if the LoggingType supports a particular destination. + /// - Parameter destination: The particular destination being tested for conformance. + /// - Returns: If the destination exists in this LoggingType `true` or `false` will be returned. + internal func contains(_ destination: LogDestination) -> Bool { + return allTypes.contains(destination) + } +} + + +/// The interface to the message being returned to `LogTarget` -> `parseLog()`. +public protocol LogMessage { + var kind: LogFilterKind { get } + var message: String { get } + var event: RawEvent? { get } + var function: String? { get } + var line: Int? { get } + var logType: LoggingType.LogDestination { get } + var dateTime: Date { get } +} + + +// MARK: - Public Logging API + +extension Analytics { + + /// The public logging method for capturing all general types of log messages related to Segment. + /// - Parameters: + /// - message: The main message of the log to be captured. + /// - kind: Usually .error, .warning or .debug, in order of serverity. This helps filter logs based on + /// this added metadata. + /// - function: The name of the function the log came from. This will be captured automatically. + /// - line: The line number in the function the log came from. This will be captured automatically. + public func log(message: String, kind: LogFilterKind? = nil, function: String = #function, line: Int = #line) { + apply { plugin in + // Check if we should send off the event + if Logger.loggingEnabled == false { + return + } + if let loggerPlugin = plugin as? Logger { + var filterKind = loggerPlugin.filterKind + if let logKind = kind { + filterKind = logKind + } + do { + let log = try LogFactory.buildLog(destination: .log, title: "", message: message, kind: filterKind, function: function, line: line) + loggerPlugin.log(log, destination: .log) + } catch { + segmentLog(message: "Could not build log: \(error.localizedDescription)", kind: .error) + } + } + } + } + + /// The public logging method for capturing metrics related to Segment or other libraries. + /// - Parameters: + /// - type: Metric type, usually .counter or .gauge. Select the one that makes sense for the metric. + /// - name: The title of the metric to track. + /// - value: The value associated with the metric. This would be an incrementing counter or time + /// or pressure gauge. + /// - tags: Any tags that should be associated with the metric. Any extra metadata that may help. + public func metric(_ type: String, name: String, value: Double, tags: [String]? = nil) { + apply { plugin in + // Check if we should send off the event + if Logger.loggingEnabled == false { + return + } + + if let loggerPlugin = plugin as? Logger { + do { + let log = try LogFactory.buildLog(destination: .metric, title: type, message: name, value: value, tags: tags) + loggerPlugin.log(log, destination: .metric) + } catch { + segmentLog(message: "Could not build metric: \(error.localizedDescription)", kind: .error) + } + } + } + } + + /// Used to track the history of events as the event data travels through the Segment Event Timeline. As + /// plugins manipulate the data at the `before`, `enrichment`, `destination`, + /// `destination timeline`, and `after` states, an event can be tracked. Starting with the first one + /// - Parameters: + /// - event: The timeline event that is to be processed. + /// - sender: Where the event came from. + /// - function: The name of the function the log came from. This will be captured automatically. + /// - line: The line number in the function the log came from. This will be captured automatically. + public func history(event: RawEvent, sender: AnyObject, function: String = #function, line: Int = #line) { + apply { plugin in + // Check if we should send off the event + if Logger.loggingEnabled == false { + return + } + + if let loggerPlugin = plugin as? Logger { + do { + let log = try LogFactory.buildLog(destination: .history, title: event.toString(), message: "", function: function, line: line, event: event, sender: sender) + loggerPlugin.log(log, destination: .metric) + } catch { + segmentLog(message: "Could not build history: \(error.localizedDescription)", kind: .error) + } + } + } + } +} + +extension Analytics { + + /// Add a logging target to the system. These `targets` can handle logs in various ways. Consider + /// sending logs to the console, the OS and a web service. Three targets can handle these scenarios. + /// - Parameters: + /// - target: A `LogTarget` that has logic to parse and handle log messages. + /// - type: The type consists of `log`, `metric` or `history`. These correspond to the + /// public API on Analytics. + public func add(target: LogTarget, type: LoggingType) throws { + apply { (potentialLogger) in + if let logger = potentialLogger as? Logger { + do { + try logger.add(target: target, for: type) + } catch { + log(message: "Could not add target: \(error.localizedDescription)", kind: .error) + } + } + } + } + + public func logFlush() { + apply { (potentialLogger) in + if let logger = potentialLogger as? Logger { + logger.flush() + } + } + } +} diff --git a/Sources/Segment/Plugins/Logger/SegmentLogger.swift b/Sources/Segment/Plugins/Logger/SegmentLogger.swift new file mode 100644 index 00000000..cbbe6732 --- /dev/null +++ b/Sources/Segment/Plugins/Logger/SegmentLogger.swift @@ -0,0 +1,173 @@ +// +// Logger.swift +// Segment +// +// Created by Cody Garvin on 12/14/20. +// + +import Foundation + +// MARK: - Plugin Implementation + +internal class Logger: UtilityPlugin { + public var filterKind = LogFilterKind.debug + var analytics: Analytics? + + let type = PluginType.utility + + fileprivate var loggingMediator = [LoggingType: LogTarget]() + internal static var loggingEnabled = true + + required init() { } + + func configure(analytics: Analytics) { + self.analytics = analytics + try? add(target: SystemTarget(), for: LoggingType.log) + } + + func update(settings: Settings) { + // Check for the server-side flag + if let settingsDictionary = settings.plan?.dictionaryValue, + let enabled = settingsDictionary["logging_enabled"] as? Bool { + Logger.loggingEnabled = enabled + } + + } + + internal func log(_ logMessage: LogMessage, destination: LoggingType.LogDestination) { + + for (logType, target) in loggingMediator { + if logType.contains(destination) { + target.parseLog(logMessage) + } + } + } + + internal func add(target: LogTarget, for loggingType: LoggingType) throws { + + // Verify the target does not exist, if it does bail out + let filtered = loggingMediator.filter { (type: LoggingType, existingTarget: LogTarget) in + Swift.type(of: existingTarget) == Swift.type(of: target) + } + if filtered.isEmpty == false { throw NSError(domain: "Target already exists", code: 2002, userInfo: nil) } + + // Finally add the target + loggingMediator[loggingType] = target + } + + internal func flush() { + for (_, target) in loggingMediator { + target.flush() + } + + // TODO: Clean up history container here + } +} + +// MARK: - Internal Types + +internal struct LogFactory { + static func buildLog(destination: LoggingType.LogDestination, + title: String, + message: String, + kind: LogFilterKind = .debug, + function: String? = nil, + line: Int? = nil, + event: RawEvent? = nil, + sender: Any? = nil, + value: Double? = nil, + tags: [String]? = nil) throws -> LogMessage { + + switch destination { + case .log: + return GenericLog(kind: kind, message: message, function: function, line: line) + case .metric: + return MetricLog(title: title, message: message, event: event, function: function, line: line) + case .history: + return HistoryLog(message: message, event: event, function: function, line: line, sender: sender) + } + } + + fileprivate struct GenericLog: LogMessage { + var kind: LogFilterKind + var message: String + var event: RawEvent? = nil + var function: String? + var line: Int? + var logType: LoggingType.LogDestination = .log + var dateTime = Date() + } + + fileprivate struct MetricLog: LogMessage { + var title: String + var kind: LogFilterKind = .debug + var message: String + var event: RawEvent? + var function: String? = nil + var line: Int? = nil + var logType: LoggingType.LogDestination = .metric + var dateTime = Date() + } + + fileprivate struct HistoryLog: LogMessage { + var kind: LogFilterKind = .debug + var message: String + var event: RawEvent? + var function: String? + var line: Int? + var sender: Any? + var logType: LoggingType.LogDestination = .history + var dateTime = Date() + } +} + +public extension LogTarget { + // Make flush optional with an empty implementation. + func flush() { } +} + +internal extension Analytics { + /// The internal logging method for capturing all general types of log messages related to Segment. + /// - Parameters: + /// - message: The main message of the log to be captured. + /// - kind: Usually .error, .warning or .debug, in order of serverity. This helps filter logs based on + /// this added metadata. + /// - function: The name of the function the log came from. This will be captured automatically. + /// - line: The line number in the function the log came from. This will be captured automatically. + func segmentLog(message: String, kind: LogFilterKind? = nil, function: String = #function, line: Int = #line) { + apply { plugin in + if let loggerPlugin = plugin as? Logger { + var filterKind = loggerPlugin.filterKind + if let logKind = kind { + filterKind = logKind + } + do { + let log = try LogFactory.buildLog(destination: .log, title: "", message: message, kind: filterKind, function: function, line: line) + loggerPlugin.log(log, destination: .log) + } catch { + // TODO: LOG TO PRIVATE SEGMENT LOG + } + } + } + } + + /// The internal logging method for capturing metrics related to Segment or other libraries. + /// - Parameters: + /// - type: Metric type, usually .counter or .gauge. Select the one that makes sense for the metric. + /// - name: The title of the metric to track. + /// - value: The value associated with the metric. This would be an incrementing counter or time + /// or pressure gauge. + /// - tags: Any tags that should be associated with the metric. Any extra metadata that may help. + func segmentMetric(_ type: String, name: String, value: Double, tags: [String]? = nil) { + apply { plugin in + if let loggerPlugin = plugin as? Logger { + do { + let log = try LogFactory.buildLog(destination: .metric, title: type, message: name, value: value, tags: tags) + loggerPlugin.log(log, destination: .metric) + } catch { + // TODO: LOG TO PRIVATE SEGMENT LOG + } + } + } + } +} diff --git a/Sources/Segment/Plugins/Logger/SystemTarget.swift b/Sources/Segment/Plugins/Logger/SystemTarget.swift new file mode 100644 index 00000000..1f5a2db2 --- /dev/null +++ b/Sources/Segment/Plugins/Logger/SystemTarget.swift @@ -0,0 +1,52 @@ +// +// File.swift +// File +// +// Created by Cody Garvin on 8/20/21. +// + +import Foundation +import os.log + +class SystemTarget: LogTarget { + + static let logCategory = OSLog(subsystem: "Segment", category: "Log") + static let metricsCategory = OSLog(subsystem: "Segment", category: "Metrics") + static let historyCategory = OSLog(subsystem: "Segment", category: "History") + + func parseLog(_ log: LogMessage) { + var metadata = "" + if let function = log.function, let line = log.line { + metadata = " - \(function):\(line)" + } + + os_log("[Segment %{public}@ %{public}@]\n%{public}@\n", + log: categoryFor(log: log), + type: osLogTypeFromFilterKind(kind: log.kind), + log.kind.toString(), metadata, log.message) // need to fix type + } + + private func categoryFor(log: LogMessage) -> OSLog { + switch log.logType { + case .log: + return SystemTarget.logCategory + case .metric: + return SystemTarget.metricsCategory + case .history: + return SystemTarget.historyCategory + } + } + + private func osLogTypeFromFilterKind(kind: LogFilterKind) -> OSLogType { + var osLogType: OSLogType + switch kind { + case .debug: + osLogType = .info + case .warning: + osLogType = .debug + case .error: + osLogType = .error + } + return osLogType + } +} diff --git a/Sources/Segment/Startup.swift b/Sources/Segment/Startup.swift index 1b24d037..af6fe498 100644 --- a/Sources/Segment/Startup.swift +++ b/Sources/Segment/Startup.swift @@ -11,6 +11,7 @@ import Sovran extension Analytics: Subscriber { internal func platformStartup() { + add(plugin: Logger()) add(plugin: StartupQueue()) // add segment destination plugin unless diff --git a/Tests/Segment-Tests/Logger_Tests.swift b/Tests/Segment-Tests/Logger_Tests.swift index 4e3e7c8e..6f41d99c 100644 --- a/Tests/Segment-Tests/Logger_Tests.swift +++ b/Tests/Segment-Tests/Logger_Tests.swift @@ -10,14 +10,26 @@ import XCTest @testable import Segment final class Logger_Tests: XCTestCase { + + var analytics: Analytics? + let mockLogger = LoggerMockPlugin() + + override func setUp() { + analytics = Analytics(configuration: Configuration(writeKey: "test")) + analytics?.add(plugin: mockLogger) + } + + override func tearDown() { + analytics = nil + } - class LoggerMock: Logger { - var logClosure: ((LogType, String) -> Void)? + class LoggerMockPlugin: Logger { + var logClosure: ((LogFilterKind, String) -> Void)? var closure: (() -> Void)? - override func log(type: LogType, message: String, event: RawEvent?) { - super.log(type: type, message: message, event: event) - logClosure?(type, message) + override func log(_ logMessage: LogMessage, destination: LoggingType.LogDestination) { + super.log(logMessage, destination: destination) + logClosure?(logMessage.kind, logMessage.message) } override func flush() { @@ -27,21 +39,68 @@ final class Logger_Tests: XCTestCase { } func testLogging() { - - let analytics = Analytics(configuration: Configuration(writeKey: "test")) - + + // Arrange let expectation = XCTestExpectation(description: "Called") - let mockLogger = LoggerMock() - mockLogger.logClosure = { (type, message) in + // Assert + mockLogger.logClosure = { (kind, message) in expectation.fulfill() - XCTAssertEqual(type, .info, "Type not correctly passed") + XCTAssertEqual(kind, .debug, "Type not correctly passed") XCTAssertEqual(message, "Something Other Than Awesome", "Message not correctly passed") } - analytics.add(plugin: mockLogger) - analytics.log(message: "Something Other Than Awesome") + + // Act + analytics?.log(message: "Something Other Than Awesome") wait(for: [expectation], timeout: 1.0) } + + func testTargetSuccess() { + + // Arrange + let expectation = XCTestExpectation(description: "Called") + + struct LogConsoleTarget: LogTarget { + var successClosure: ((String) -> Void) + + func parseLog(_ log: LogMessage) { + print("[Segment Tests - \(log.function ?? ""):\(String(log.line ?? 0))] \(log.message)\n") + successClosure(log.message) + } + } + + let logConsoleTarget = LogConsoleTarget(successClosure: { (logMessage: String) in + expectation.fulfill() + }) + let loggingType = LoggingType.log + analytics?.add(target: logConsoleTarget, type: loggingType) + + // Act + analytics?.log(message: "Should hit our proper target") + wait(for: [expectation], timeout: 1.0) + } + + func testTargetFailure() { + + // Arrange + struct LogConsoleTarget: LogTarget { + var successClosure: ((String) -> Void) + + func parseLog(_ log: LogMessage) { + print("[Segment Tests - \(log.function ?? ""):\(String(log.line ?? 0))] \(log.message)\n") + successClosure(log.message) + } + } + + let logConsoleTarget = LogConsoleTarget(successClosure: { (logMessage: String) in + XCTFail("Should not hit this since it was registered for history") + }) + let loggingType = LoggingType.history + analytics?.add(target: logConsoleTarget, type: loggingType) + + // Act + analytics?.log(message: "Should hit our proper target") + } }