Slide 1

Slide 1 text

Flexible & Scalable logs on iOS

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Yesterday NSLog("Look at this beautiful log") print("Look at this beautiful log")

Slide 4

Slide 4 text

What’s wrong? Console display bad visibility Uncentralized logging Displayed on Debug && Release Some bunnies will die somewhere in the world

Slide 5

Slide 5 text

How to resolve it?

Slide 6

Slide 6 text

Powerful tools Cocoalumberjack

Slide 7

Slide 7 text

Powerful tools Cocoalumberjack NSLogger

Slide 8

Slide 8 text

Powerful tools Cocoalumberjack NSLogger SwiftyBeaver

Slide 9

Slide 9 text

Powerful tools Cocoalumberjack NSLogger SwiftyBeaver ✓ Colored logs ✓ Cat eg ories ✓ Filters ✓ Cloud ☁

Slide 10

Slide 10 text

Today, we need more

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Tools do more too ' - CleanroomLogger - Willow (Nike)

Slide 13

Slide 13 text

One day… - Your team: "Hey! What about changing our Third Party Logger Dependency across the entire app, it’s not maintained anymore! " - You:

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Before continuing: architecture your app

Slide 16

Slide 16 text

App architecture 1. One Framework by feature 2. Create a Logger framework App Logger AppUI AppCore Feature 1 Feature 2

Slide 17

Slide 17 text

App architecture 1. One Framework by feature 2. Create a Logger framework

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Some code, please ,-

Slide 20

Slide 20 text

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")

Slide 21

Slide 21 text

What do we want to log? Date Date() ✅ Class Name #file ✅ Function Name #function ✅ Line number #line ✅ Log level ? ❌ Context ? ❌ Module ? ❌

Slide 22

Slide 22 text

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 } }

Slide 23

Slide 23 text

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 } }

Slide 24

Slide 24 text

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")

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

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 ✅

Slide 27

Slide 27 text

Create the destinations - Destination conformance - String format - Minimum log level - "Send" method - Anything you want Logger Basic output console SwiftyBeaver NSLogger Logmatic Firebase / Fabric

Slide 28

Slide 28 text

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? {}

Slide 29

Slide 29 text

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 } }

Slide 30

Slide 30 text

Prepare our Logger to be plug & play public final class Logger { // a set of active destinations public private(set) var destinations = Set() // 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) {}

Slide 31

Slide 31 text

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 } } }

Slide 32

Slide 32 text

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) } }

Slide 33

Slide 33 text

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) ❤

Slide 34

Slide 34 text

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 …… } } }

Slide 35

Slide 35 text

NSLogger

Slide 36

Slide 36 text

One more thing

Slide 37

Slide 37 text

OSLog

Slide 38

Slide 38 text

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.

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Use the Console.app

Slide 41

Slide 41 text

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 } }

Slide 42

Slide 42 text

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 } }

Slide 43

Slide 43 text

Et voilà!

Slide 44

Slide 44 text

Instruments + OSLog =

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Sources All the previous code is here on GitHub https://github.com/bill350/LoggerLayer

Slide 47

Slide 47 text

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/

Slide 48

Slide 48 text

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/

Slide 49

Slide 49 text

We’re hiring! iOS Software Engineer & Android Software Engineer

Slide 50

Slide 50 text

Thank you

Slide 51

Slide 51 text

No content