Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flexible & Scalable logs on iOS

Flexible & Scalable logs on iOS

- Discover an approach to structure & architecture your app to have a flexible & scalable logging layer.
- Learn why it is necessary to do your own log layer ;)

Jean-Charles SORIN

November 08, 2018
Tweet

More Decks by Jean-Charles SORIN

Other Decks in Programming

Transcript

  1. From yesterday to powerful tools Issues & need on an

    iOS app Layer architecture for logs From yesterday to powerful tools Issues & need on an iOS app Create your own logger layer Flexible & Scalable logs on iOS
  2. What’s wrong? Console display bad visibility Uncentralized logging Displayed on

    Debug && Release Some bunnies will die somewhere in the world
  3. We need more! Production monitoring - Boards & alerts (Datadog)

    - Firebase & Fabric: non-fatal errors monitoring Flexibility - Realtime log enabling/disabling - Enabling by configurations Scalability - Change, add or remove logs destinations at any time
  4. One day… - Your team: "Hey! What about changing our

    Third Party Logger Dependency across the entire app, it’s not maintained anymore! " - You:
  5. App architecture 1. One Framework by feature 2. Create a

    Logger framework App Logger AppUI AppCore Feature 1 Feature 2
  6. Logger Architecture One framework: exportable No global shared instance: multiple

    apps / feature = multiple loggers One entry point but multiple available destinations No need to update your code Logger Basic output console SwiftyBeaver NSLogger Logmatic Firebase / Fabric
  7. Define a unique Logger public final class Logger { public

    struct Identification { let identifier: String let bundleIdentifier: String } private let identification: Identification public init(identifier: String, bundleIdentifier: String? = Bundle.main.bundleIdentifier) { let bundleID = bundleIdentifier ?? "Bundle undefined" self.identification = Identification(identifier: identifier, bundleIdentifier: bundleID) } } static let logger = Logger(identifier: "Diagnostic")
  8. What do we want to log? Date Date() ✅ Class

    Name #file ✅ Function Name #function ✅ Line number #line ✅ Log level ? ❌ Context ? ❌ Module ? ❌
  9. Define the level public final class Logger { public enum

    Level: Int { case verbose = 0 case debug = 1 case info = 2 case warning = 3 case error = 4 } }
  10. Define the context public struct LogContext: RawRepresentable { public var

    isEnabled = true public let rawValue: String public init(rawValue: String) { self.rawValue = rawValue } public static let app = LogContext(rawValue: "App") public static let layout = LogContext(rawValue: "View Layout") public static let routing = LogContext(rawValue: "Routing") public static let service = LogContext(rawValue: "Service") public static let model = LogContext(rawValue: Model") public static func custom(_ value: String, enabled: Bool = true) -> LogContext { var context = LogContext(rawValue: value) context.isEnabled = enabled return context } }
  11. More clarity? Use emojis! public static let app = LogContext(rawValue:

    " App") public static let layout = LogContext(rawValue: " View Layout") public static let routing = LogContext(rawValue: "⛵ Routing") public static let service = LogContext(rawValue: " Service") public static let model = LogContext(rawValue: " Model")
  12. Logging methods public final class Logger { public func custom(level:

    Logger.Level, message: @autoclosure () -> Any, file: String = #file, function: String = #function, line: Int = #line, context: LogContext) { guard context.isEnabled else { return } dispatch_send(level: level, message… … } public func verbose(_ message: @autoclosure () -> Any… public func debug… public func info… public func warning… public func error… } logger.info("Stop audio for speech recognizing", context: .microphone) logger.error("Cannot recognize speech: \(error)", context: .microphone)
  13. What we want to log? Date Date() ✅ Class Name

    #file ✅ Function Name #function ✅ Line number #line ✅ Log level enum Level ✅ Contexte LogContext ✅ Module Logger.identifier ✅
  14. Create the destinations - Destination conformance - String format -

    Minimum log level - "Send" method - Anything you want Logger Basic output console SwiftyBeaver NSLogger Logmatic Firebase / Fabric
  15. LoggerDestination open class LoggerDestination: Hashable, Equatable { open var format

    = "$DHH:mm:ss.SSS$d $C$L [$i-$X]$c $N.$F:$l - $M" open var minLevel = Logger.Level.verbose /// set custom log level colors for each level open var levelColor = LevelColor() // For a colored log level word in a logged line // empty on default public struct LevelColor { public var verbose = "" // silver public var debug = "" // green public var info = "" // blue public var warning = "" // yellow public var error = "" // red } open var levelString = LevelString() let moduleIdentification: Logger.Identification public init(identification: Logger.Identification, level: Logger.Level = .verbose) { open func send(_ level: Logger.Level, msg: String, thread: String, file: String, function: String, line: Int, context: LogContext = LogContext.app) -> String? {}
  16. Let’s configure it! final class ConsoleDestination: LoggerDestination { override init(identification:

    Logger.Identification, level: Logger.Level = .verbose) { super.init(identification: identification, level: level) self.levelColor.verbose = "⚪ " self.levelColor.debug = "☑ " self.levelColor.info = " " self.levelColor.warning = " " self.levelColor.error = " " } // print to Xcode Console. uses full base class functionality override func send(_ level: Logger.Level, msg: String, thread: String, file: String, function: String, line: Int, context: LogContext) -> String? { let formattedString = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context) if let str = formattedString { print(str) } return formattedString } }
  17. Prepare our Logger to be plug & play public final

    class Logger { // a set of active destinations public private(set) var destinations = Set<LoggerDestination>() // Destinations private lazy var basicDestination = BasicConsoleDestination(loggerIdentifier: self.identifier) private lazy var nsloggerDestination = NSLoggerDestination(loggerIdentifier: self.identifier) // MARK: Destinations set up public func setBasicConsole(enabled isEnabled: Bool) { if isEnabled { self.addDestination(self.basicDestination) } else { self.removeDestination(self.basicDestination) } } public func setNSLogger(enabled isEnabled: Bool) {}
  18. Create our Logger public struct DiagnosticModule { static let logger

    = Logger(identifier: "", bundleIdentifier: "com.bunny.diagnostic") public static func configure() { self.setLogging(enabled: true) } static func setLogging(enabled: Bool) { if enabled { #if DEBUG logger.setBasicConsole(enabled: true) logger.setNSLogger(enabled: true) #else logger.setBasicConsole(enabled: false, level: .error) logger.setNSLogger(enabled: true, level: .error) #endif } } }
  19. Create our LogContexts public struct DiagnosticModule { struct Log {

    static let defaultContext = LogContext.custom("Diagnostic ") static let microphoneContext = LogContext.custom("Microphone ") static let photoContext = LogContext.custom("Photo Capture ») static let speakerContext = LogContext.custom("Speaker ") static let reachabilityContext = LogContext.custom("Reachability ") static let screenContext = LogContext.custom("Screen ") static let sensorContext = LogContext.custom("Sensor ") static let buttonsContext = LogContext.custom("Buttons ») static let batteryContext = LogContext.custom("Battery ") static let locationContext = LogContext.custom("Location ", enabled: false) } }
  20. How does it look likes? 17:53:09.225 INFO [ -Sensor ]

    ProximitySensor.stopObserving():51 - Stop observing proximity sensor 17:53:09.227 ERROR [ -Photo Capture ] Torch.init():30 - Cannot create torch instance, no torch device available 17:53:09.231 INFO [ -Battery ] Battery.stopObservingBatteryStateChanges():72 - Stop observing battery state changes 17:55:37.747 ☑ DEBUG [ -Screen ] ScreenTouchMonitor.checkCompletionState():105 - Did finish validating zones 2018-10-27 17:55:37.759907+0200 Troy[73854:29234595] [framework] CUIThemeStore: No theme registered with id=0 17:55:41.158 ☑ DEBUG [ -Diagnostic ] PermissionType.permissionStatus():45 - Permission camera state isDenied = false 17:55:41.159 ERROR [ -Photo Capture ] CameraRecorder.init():75 - No available device for AVCaptureDeviceType(_rawValue: AVCaptureDeviceTypeBuiltInWideAngleCamera) 17:55:41.160 ERROR [ - App] CameraCaptureViewModel.prepareCamera():125 - noDeviceAvailable(position: Diagnostic.CameraRecorder.DevicePosition.back) 17:55:41.194 ERROR [ -Photo Capture ] CameraRecorder.init():75 - No available device for AVCaptureDeviceType(_rawValue: AVCaptureDeviceTypeBuiltInWideAngleCamera) 17:55:41.195 ERROR [ - App] CameraCaptureViewModel.prepareCamera():125 - noDeviceAvailable(position: Diagnostic.CameraRecorder.DevicePosition.front) ❤
  21. NSLoggerDestination final class NSLoggerDestination: LoggerDestination { private let nslogger =

    NSLogger.Logger.shared override func send(_ level: Logger.Level, msg: String, thread: String, file: String, function: String, line: Int, context: LogContext) -> String? { let domain = NSLogger.Logger.Domain.custom(context.rawValue) self.nslogger.log(domain, self.relatedLevel(for: level), msg, file, line, function) return super.send(level, msg: msg, thread: thread, file: file, function: function, line: line, context: context) } } private extension NSLoggerDestination { func relatedLevel(for logLevel: Logger.Level) -> NSLogger.Logger.Level { switch logLevel { case .debug: return NSLogger.Logger.Level.debug …… } } }
  22. OSLog Unified logging is available in iOS 10.0 and later,

    macOS 10.12 and later, tvOS 10.0 and later, and watchOS 3.0 and later, and supersedes ASL (Apple System Logger) and the Syslog APIs.
  23. OSLog let customLog = OSLog(subsystem: "com.world.hello", category: "your_category_name") os_log("This is

    info that may be helpful during development or debugging.", log: customLog, type: .debug) - https://developer.apple.com/documentation/os/logging
  24. Let’s create our OSLog destination! final class OSLogDestination: LoggerDestination {

    override init(identification: Logger.Identification, level: Logger.Level = .verbose) { super.init(identification: identification, level: level) self.format = "$C$L $c $N.$F:$l - $M" } override func send(_ level: Logger.Level, msg: String, thread: String, file: String, function: String, line: Int, context: LogContext) -> String? { let formatedMsg = super.send(level, msg: msg, thread: thread, file: file, function: function, line: line) if let formatedMsg = formatedMsg { let log = self.createOSLog(context: context) os_log("%@", log: log, type: self.osLogLevelRelated(to: level), formatedMsg) return formatedMsg } return formatedMsg } }
  25. Route Logger.Level to OSLog.Level private extension OSLogDestination { func createOSLog(context:

    LogContext) -> OSLog { let category = context.rawValue let subsystem = self.moduleIdentification.bundleIdentifier let customLog = OSLog(subsystem: subsystem, category: category) return customLog } func osLogLevelRelated(to logLevel: Logger.Level) -> OSLogType { var logType: OSLogType switch logLevel { case .debug: logType = .debug case .verbose: logType = .default case .info: logType = .info case .warning: //We use "error" here because of indicator in the Console logType = .error case .error: //We use "fault" here because of indicator in the Console logType = .fault } return logType } }
  26. Let’s recap ✓Architecture your app before, with modules ✓Create your

    own Logger layer as a module ✓Flexibility & scalability with configurable destinations ✓OSLog is your friend: unlock the power ✓Bunnies are safe
  27. Links 1/2 Powerful tools - https://github.com/CocoaLumberjack/CocoaLumberjack - https://github.com/SwiftyBeaver/SwiftyBeaver - https://github.com/fpillet/NSLogger

    - https://github.com/emaloney/CleanroomLogger - https://github.com/Nike-Inc/Willow - https://www.datadoghq.com/log-management/
  28. Links 2/2 OSLog ➡Discovering & using OSLog output console with

    SwiftyBeaver on iOS - https://medium.com/back- market-engineering/discovering-using-oslog-output- console-with-swiftybeaver-on-ios-54d7bfbe3b97 ➡https://www.testdevlab.com/blog/2018/04/how-to- create-categorize-and-filter-ios-logs/ ➡https://www.bignerdranch.com/blog/migrating-to- unified-logging-swift-edition/