관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] os_log, Logger, 통합 로깅 시스템 (unified logging system) 본문

iOS 응용 (swift)

[iOS - swift] os_log, Logger, 통합 로깅 시스템 (unified logging system)

jake-kim 2021. 3. 21. 00:09

통합 로깅 시스템 (unified logging system)

  • 통합의 의미: 로그 데이터 저장을 level에 따라 메모리와 디스크의 데이터 저장소에 모으는 것 (iOS 10+ 에서 가능)
  • 장점: 모든 level의 시스템에서 로깅표출 가능
  • xcode를 실행하지 않아도 console앱을 실행시키면 로깅 가능
  • os_log를 사용하면 mac의 콘솔 앱에서 로그를 쉽게 확인(필터 기능) & 저장기능 등을 활용 가능

level

  • 주로 Fault를 제외한 4가지 사용
level 종류 disk에 저장 내용
Default (notice) o 문제 해결을 위한 level
Info o Error케이스와 유사하지만, 에러 설명이 긴 경우
Debug x 개발 환경에서의 간단한 로깅 (mac의 '콘솔'앱에는 찍히지 않고 xcode console에만 표출)
Error o Info케이스와 유사하지만, 간단한 에러인 경우
Fault o Error와 유사하지만 시스템 레벨 or 다중 프로세스 오류 캡쳐를 위한 것

os_log 사용방법

0. OSLog프레임워크 link

 

iOS14+에서만 지원하므로 그 이하버전 지원 시 크래시 나지 않도록 프레임워크 Optional 설정

1. OSLog 속성 정의

  • subsystem 문자열 정의 (bundleIdentifier를 이용)
  • category정의
extension OSLog {
    static let subsystem = Bundle.main.bundleIdentifier!
    static let network = OSLog(subsystem: subsystem, category: "Network")
    static let debug = OSLog(subsystem: subsystem, category: "Debug")
    static let info = OSLog(subsystem: subsystem, category: "Info")
    static let error = OSLog(subsystem: subsystem, category: "Error")
}

2. os_log를 편리하게 사용할 os_log메소드를 실행하는 Wrapper 형태 구현

  • 종속성
import os.log
  • os.log를 사용하는 Wrapper로 구현 `Log`
import os.log

extension OSLog {
    ...
}

struct Log {
    // TODO
}
  • Log의 level 정의
extension OSLog {
    ...
}

struct Log {
    enum Level {
        case debug
        case info
        case network
        case error
        case custom(categoryName: String)
    }
}
  • category 이름을 반환하는 `categoryName`변수 추가
extension OSLog {
    ...
}


struct Log {
    enum Level {
        ...
    
        fileprivate var categoryName: String {
            switch self {
                case .debug:
                    return "Debug"
                case .info:
                    return "Info"
                case .network:
                    return "Network"
                case .error:
                    return "Error"
                case .custom(let categoryName):
                    return categoryName
            }
        }
    }
}
  • level타입에 따라 OSLog의 level 객체를 반환하는 osLog 변수 추가(os_log메소드의 파라미터에 사용하기 위함)
extension OSLog {
    ...
}


struct Log {
    enum Level {
        ...

        fileprivate var category: String {
            ...
        }

        fileprivate var osLog: OSLog {
            switch self {
            case .debug:
                return OSLog.debug
            case .info:
                return OSLog.info
            case .network:
                return OSLog.network
            case .error:
                return OSLog.error
            case .custom:
                return OSLog.debug
            }
        }
    }
}
  • level 타입에 따라 OSLog의 level type을 반환하는 osLogType 변수 추가(os_log메소드의 파라미터에 사용하기 위함)
extension OSLog {
    ...
}


struct Log {
    enum Level {
        ...

        fileprivate var category: String {
            ...
        }

        fileprivate var osLog: OSLog {
            ...
        }

        fileprivate var osLogType: OSLogType {
            switch self {
            case .debug:
                return .debug
            case .info:
                return .info
            case .network:
                return .default
            case .error:
                return .error
            case .custom:
                return .debug
            }
        }
    }
}
  • os_log (iOS 14.0 미만), Logger(iOS 14.0 이상) wrapper 함수 정의
extension OSLog {
    ...
}

struct Log {
    enum Level {
        ...
    }
    
    static private func log(_ message: Any, _ arguments: [Any], level: Level) {
        #if LOG
            // 이곳에 os_log, Logger를 통해 로깅하는 부분 구현
        #endif
    }
}
  • log(_:_:level) 메소드 구현
extension OSLog {
    ...
}

struct Log {
    enum Level {
        ...
    }
    
    static private func log(_ message: Any, _ arguments: [Any], level: Level) {
        #if LOG
        if #available(iOS 14.0, *) {
            let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
            let logger = Logger(subsystem: OSLog.subsystem, category: level.category)
            let logMessage = "\(message) \(extraMessage)"
            switch level {
            case .debug,
                 .custom:
                logger.debug("\(logMessage, privacy: .public)")
            case .info:
                logger.info("\(logMessage, privacy: .public)")
            case .network:
                logger.log("\(logMessage, privacy: .public)")
            case .error:
                logger.error("\(logMessage, privacy: .public)")
            }
        } else {
            let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
            os_log("%{public}@", log: level.osLog, type: level.osLogType, "\(message) \(extraMessage)")
        }
        #endif
    }
}

3. 외부에서 `Log.`으로 접근하는 메소드 정의

extension OSLog {
    ...
}

struct Log {
    enum Level {
        ... 
    }
    
    static private func log(_:_:level:) {
        ... 
    }
}

// MARK: - utils

extension Log {
    static func debug(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .debug)
    }

    static func info(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .info)
    }

    static func network(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .network)
    }

    static func error(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .network)
    }

    static func custom(category: String, _ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .custom(categoryName: category))
    }
}
  • 전체 코드
import Foundation
import os.log

extension OSLog {
    static let subsystem = Bundle.main.bundleIdentifier!
    static let network = OSLog(subsystem: subsystem, category: "Network")
    static let debug = OSLog(subsystem: subsystem, category: "Debug")
    static let info = OSLog(subsystem: subsystem, category: "Info")
    static let error = OSLog(subsystem: subsystem, category: "Error")
}

struct Log {
    enum Level {
        case debug
        case info
        case network
        case error
        case custom(categoryName: String)

        fileprivate var category: String {
            switch self {
            case .debug:
                return "Debug"
            case .info:
                return "Info"
            case .network:
                return "Network"
            case .error:
                return "Error"
            case .custom(let categoryName):
                return categoryName
            }
        }

        fileprivate var osLog: OSLog {
            switch self {
            case .debug:
                return OSLog.debug
            case .info:
                return OSLog.info
            case .network:
                return OSLog.network
            case .error:
                return OSLog.error
            case .custom:
                return OSLog.debug
            }
        }

        fileprivate var osLogType: OSLogType {
            switch self {
            case .debug:
                return .debug
            case .info:
                return .info
            case .network:
                return .default
            case .error:
                return .error
            case .custom:
                return .debug
            }
        }
    }

    static private func log(_ message: Any, _ arguments: [Any], level: Level) {
        #if LOG
        if #available(iOS 14.0, *) {
            let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
            let logger = Logger(subsystem: OSLog.subsystem, category: level.category)
            let logMessage = "\(message) \(extraMessage)"
            switch level {
            case .debug,
                 .custom:
                logger.debug("\(logMessage, privacy: .public)")
            case .info:
                logger.info("\(logMessage, privacy: .public)")
            case .network:
                logger.log("\(logMessage, privacy: .public)")
            case .error:
                logger.error("\(logMessage, privacy: .public)")
            }
        } else {
            let extraMessage: String = arguments.map({ String(describing: $0) }).joined(separator: " ")
            os_log("%{public}@", log: level.osLog, type: level.osLogType, "\(message) \(extraMessage)")
        }
        #endif
    }
}

// MARK: - utils

extension Log {
    static func debug(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .debug)
    }

    static func info(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .info)
    }

    static func network(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .network)
    }

    static func error(_ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .network)
    }

    static func custom(category: String, _ message: Any, _ arguments: Any...) {
        log(message, arguments, level: .custom(categoryName: category))
    }
}

4. 디버그 모드 설정 방법

  • 전처리기 개념: c에서의 개념과 동일하게, #if TEST1, #endif와 같이 소스코드에서 표현되고, 전처리기에 따라 분기를 결정할 수 있는 요소
#if TEST1
  // debug
#endif
  • 전처리기 관련 문자열 ("TEST1"와 같은 경우)은 Xcode에서 추가: -D TEST1
    - Target -> Build Setting -> OTHER_SWIFT_FLAGS에 전처리기 관련 flag 추가

  • Log에서 정의한 전처리기 문자열은 "LOG"이므로, Release를 제외한 곳에 모두 "LOG"추가

  • Other_swift_flags에 $(inherited) 추가 - defalut로 추가되어 있지 않은 경우만 추가
    - 해주는 이유: cocoapods 프레임워크에서 "COCOAPODS"라는 플래그를 사용하는데, $(inherited)안에 포함된 -D COCOAPODS을 따로 추가해야, 종속적인 cocoapods 프레임워크를 사용할 때 빌드 warning이 나지 않기 때문

  • cf) OTHER_SWIFT_FLAG 대신에 Active Compilation Conditions를 사용해도 무방 (-D 없이 바로 기입하여 사용)

5. 사용 예시

Log.debug("123")
Log.network("[Send] Splash화면에서 api 호출")
Log.info("Splash화면에서 어떤 정보 로깅")
Log.error("Splash화면에서 에러")

xcode console화면

6. mac의 '콘솔' 앱을 사용하여 로깅

  • 콘솔 앱 실행

  • 로깅 확인

1. xcode에서 실행한 디바이스 or 시뮬레이터 선택

2. 하위 시스템으로 필터링 (OSLog의 subsystem을 bundle id로 했으므로 bundle id기입)

3. 로깅 확인

  • 검색했던 '필터' 저장 기능: 저장 버튼을 누르면 좌측에 지정한 이름으로 저장

  • 로깅 저장 방법 - terminal에서 저장할 곳으로 이동
sudo log collect --device --start <날짜> --output <저장될 이름>.logarchive

* 조금 더 추상적으로 os_log 사용 방법: https://ios-development.tistory.com/534

 

* 참고

- mokacoding.com/blog/cocoapods-the-inherited-flag/

- stackoverflow.com/questions/15343122/what-is-inherited-in-xcodes-search-path-settings

- developer.apple.com/documentation/os/logging

www.avanderlee.com/workflow/oslog-unified-logging/

Comments