├── .xcode-version ├── CODEOWNERS ├── Sources ├── BlackBox │ ├── Documentation.docc │ │ ├── Resources │ │ │ ├── CustomLoggers │ │ │ │ ├── Base │ │ │ │ │ ├── CustomLogger_Base_1.swift │ │ │ │ │ ├── CustomLogger_Base_2.swift │ │ │ │ │ ├── CustomLogger_Base_5.swift │ │ │ │ │ ├── CustomLogger_Base_3.swift │ │ │ │ │ └── CustomLogger_Base_4.swift │ │ │ │ ├── Toggle │ │ │ │ │ ├── CustomLogger_Toggle_4.swift │ │ │ │ │ ├── CustomLogger_Toggle_5.swift │ │ │ │ │ ├── CustomLogger_Toggle_1.swift │ │ │ │ │ ├── CustomLogger_Toggle_2.swift │ │ │ │ │ └── CustomLogger_Toggle_3.swift │ │ │ │ ├── Filter │ │ │ │ │ ├── CustomLogger_Filter_1.swift │ │ │ │ │ ├── CustomLogger_Filter_2.swift │ │ │ │ │ └── CustomLogger_Filter_3.swift │ │ │ │ └── point.topleft.down.curvedto.point.filled.bottomright.up.svg │ │ │ └── bb_logo.png │ │ ├── Extensions │ │ │ ├── OSSignpostLogger.md │ │ │ └── OSLogger.md │ │ ├── Articles │ │ │ ├── Installation.md │ │ │ ├── BlackBox.md │ │ │ ├── LoggingErrors.md │ │ │ └── README.md │ │ └── Tutorials │ │ │ ├── Tutorial Table of Contents.tutorial │ │ │ └── CustomLoggers │ │ │ ├── CustomLoggers_2_Filters.tutorial │ │ │ ├── CustomLoggers_1_Base.tutorial │ │ │ └── CustomLoggers_3_Toggle.tutorial │ ├── BBLoggerProtocol.swift │ ├── Helpers │ │ └── BBHelpers.swift │ ├── BBLogLevel.swift │ ├── BBLogFormat.swift │ ├── Loggers │ │ ├── OSSignpostLogger.swift │ │ ├── FSLogger.swift │ │ └── OSLogger.swift │ ├── BlackBoxEvents.swift │ └── BlackBox.swift └── ExampleModule │ └── ExampleService.swift ├── Tests └── BlackBoxTests │ ├── Helpers │ ├── Lightsaber.swift │ └── AnakinKills.swift │ ├── BlackBoxTestCase.swift │ ├── LoggerMock.swift │ ├── BlackBoxErrorEventTests.swift │ ├── BlackBoxGenericEventTests.swift │ ├── BlackBoxStartEventTests.swift │ ├── BlackBoxEndEventTests.swift │ ├── OSSignpostLoggerTests.swift │ ├── OSLoggerTests.swift │ └── BackwardsCompatibilityTests.swift ├── .github ├── workflows │ ├── release.yml │ ├── update_docs.yml │ └── unit_tests.yml ├── actions │ └── prepare_env_app_build │ │ └── action.yml ├── dependabot.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── README.md ├── Package.resolved ├── Package.swift ├── .gitignore └── LICENSE /.xcode-version: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @alldmeat 2 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Base/CustomLogger_Base_1.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Base/CustomLogger_Base_2.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/bb_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dodobrands/BlackBox/HEAD/Sources/BlackBox/Documentation.docc/Resources/bb_logo.png -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Extensions/OSSignpostLogger.md: -------------------------------------------------------------------------------- 1 | # ``BlackBox/OSSignpostLogger`` 2 | 3 | ## Overview 4 | 5 | [Usage Example](https://habr.com/ru/company/dododev/blog/690542/) 6 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Toggle/CustomLogger_Toggle_4.swift: -------------------------------------------------------------------------------- 1 | let remoteFlagName = "AlertLoggerEnabled" 2 | 3 | var isAlertLoggerEnabled: () -> Bool = { remoteFeatureFlagProvider.isEnabled(remoteFlagName) } 4 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/Helpers/Lightsaber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SomeStruct.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Lightsaber: Equatable { 11 | let color: String 12 | } 13 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Extensions/OSLogger.md: -------------------------------------------------------------------------------- 1 | # ``BlackBox/OSLogger`` 2 | 3 | ## Overview 4 | 5 | [Usage Example](https://habr.com/ru/company/dododev/blog/689758/) 6 | [Console.app Apple Documentation](https://support.apple.com/en-gb/guide/console/welcome/mac) 7 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Base/CustomLogger_Base_5.swift: -------------------------------------------------------------------------------- 1 | let AlertLogger = AlertLogger() 2 | let defaultLoggers = BlackBox.defaultLoggers 3 | let loggers = defaultLoggers + [AlertLogger] 4 | 5 | BlackBox.instance = BlackBox(loggers: loggers) 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | jobs: 9 | release: 10 | runs-on: 'ubuntu-latest' 11 | 12 | timeout-minutes: 5 13 | 14 | steps: 15 | - name: Release 16 | uses: softprops/action-gh-release@v2 17 | with: 18 | generate_release_notes: true 19 | -------------------------------------------------------------------------------- /.github/actions/prepare_env_app_build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Prepare Environment for App Build' 2 | 3 | runs: 4 | using: "composite" 5 | 6 | steps: 7 | - name: Get Xcode Version 8 | shell: bash 9 | run: echo "XCODE_VERSION=$(<.xcode-version)" >> $GITHUB_ENV 10 | 11 | - name: Select Xcode 12 | uses: maxim-lobanov/setup-xcode@v1 13 | with: 14 | xcode-version: ${{ env.XCODE_VERSION }} -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Toggle/CustomLogger_Toggle_5.swift: -------------------------------------------------------------------------------- 1 | let remoteFlagName = "AlertLoggerEnabled" 2 | 3 | var isAlertLoggerEnabled: () -> Bool = { remoteFeatureFlagProvider.isEnabled(remoteFlagName) } 4 | 5 | let logger = AlertLogger(alertService: AlertService(), 6 | levels: [.info, .warning, .error], 7 | isEnabled: isAlertLoggerEnabled) 8 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Base/CustomLogger_Base_3.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | func log(_ event: BlackBox.GenericEvent) { 3 | 4 | } 5 | func log(_ event: BlackBox.ErrorEvent) { 6 | 7 | } 8 | func logStart(_ event: BlackBox.StartEvent) { 9 | 10 | } 11 | func logEnd(_ event: BlackBox.EndEvent) { 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Articles/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | How to add BlackBox to your project 3 | 4 | BlackBox Supports: 5 | - iOS 12 and newer; 6 | - macOS 10.15 and newer; 7 | - tvOS 12 and newer; 8 | - watchOS 5 and newer. 9 | 10 | SPM: 11 | ```swift 12 | dependencies: [ 13 | .package( 14 | url: "https://github.com/dodopizza/BlackBox.git", 15 | .upToNextMajor(from: "4.0.0") 16 | ) 17 | ] 18 | ``` 19 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/Helpers/AnakinKills.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnakinKills.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AnakinKills: Error, Equatable, CustomNSError { 11 | case maceWindu 12 | case younglings(count: Int) 13 | 14 | var errorUserInfo: [String : Any] { 15 | switch self { 16 | case .maceWindu: 17 | return [:] 18 | case .younglings(let count): 19 | return ["count": count] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Articles/BlackBox.md: -------------------------------------------------------------------------------- 1 | # ``BlackBox/BlackBox`` 2 | 3 | Entry points for logs. 4 | 5 | ## Overview 6 | 7 | BlackBox takes logs and redirects them to target destinations, such as ``OSLogger`` and ``OSSignpostLogger``. 8 | 9 | Each log is processed on background queue so that your app performance won't suffer. 10 | 11 | When creating new BlackBox instance you can override default `DispatchQueue` that logs are processed on. If you do so make sure your custom queue is serial, otherwise logs order may be unpredicable. 12 | -------------------------------------------------------------------------------- /Sources/ExampleModule/ExampleService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleService.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 14.10.2022. 6 | // 7 | 8 | import Foundation 9 | import BlackBox 10 | 11 | class ExampleService { 12 | func doSomeWork() { 13 | BlackBox.log("Doing some work") 14 | } 15 | 16 | func logSomeError() { 17 | BlackBox.log(ExampleError.taskFailed) 18 | } 19 | 20 | func finishLog(_ log: BlackBox.StartEvent) { 21 | BlackBox.logEnd(log) 22 | } 23 | } 24 | 25 | enum ExampleError: Error { 26 | case taskFailed 27 | } 28 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Tutorials/Tutorial Table of Contents.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorials(name: "Tutorials ToC") { 2 | @Intro(title: "BlackBox Interactive Tutorials") { 3 | 4 | } 5 | 6 | @Chapter(name: "Custom Loggers") { 7 | @Image(source: "point.topleft.down.curvedto.point.filled.bottomright.up", alt: "Two docs connected with a curve") 8 | @TutorialReference(tutorial: "doc:CustomLoggers_1_Base") 9 | @TutorialReference(tutorial: "doc:CustomLoggers_2_Filters") 10 | @TutorialReference(tutorial: "doc:CustomLoggers_3_Toggle") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlackBox 2 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FBlackBox%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dodobrands/BlackBox) 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdodobrands%2FBlackBox%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/dodobrands/BlackBox) 4 | 5 | Library for logs and measurements. 6 | 7 | [Documentation](https://dodobrands.github.io/BlackBox/documentation/blackbox). 8 | 9 | [Interactive Tutorials](https://dodobrands.github.io/BlackBox/tutorials/tutorial-table-of-contents). 10 | -------------------------------------------------------------------------------- /Sources/BlackBox/BBLoggerProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Receives all possible logs from BlackBox 4 | public protocol BBLoggerProtocol { 5 | /// Logs generic event 6 | /// - Parameter event: Generic event 7 | func log(_ event: BlackBox.GenericEvent) 8 | 9 | /// Logs error 10 | /// - Parameter event: Error event 11 | func log(_ event: BlackBox.ErrorEvent) 12 | 13 | // MARK: - Measurements 14 | /// Logs measurement start 15 | /// - Parameter event: Measurement start event 16 | func logStart(_ event: BlackBox.StartEvent) 17 | 18 | /// Logs measurement end 19 | /// - Parameter event: Measurement end event 20 | func logEnd(_ event: BlackBox.EndEvent) 21 | } 22 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Base/CustomLogger_Base_4.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | 4 | init(alertService: AlertService) { 5 | self.alertService = alertService 6 | } 7 | 8 | func log(_ event: BlackBox.GenericEvent) { 9 | alertService.showMessage(event.message) 10 | } 11 | 12 | func log(_ event: BlackBox.ErrorEvent) { 13 | alertService.showError(event.message) 14 | } 15 | 16 | func logStart(_ event: BlackBox.StartEvent) { 17 | // ignore 18 | } 19 | 20 | func logEnd(_ event: BlackBox.EndEvent) { 21 | alertService.showMessage(event.message) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Filter/CustomLogger_Filter_1.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | 4 | init(alertService: AlertService) { 5 | self.alertService = alertService 6 | } 7 | 8 | func log(_ event: BlackBox.GenericEvent) { 9 | alertService.showMessage(event.message) 10 | } 11 | 12 | func log(_ event: BlackBox.ErrorEvent) { 13 | alertService.showError(event.message) 14 | } 15 | 16 | func logStart(_ event: BlackBox.StartEvent) { 17 | // ignore 18 | } 19 | 20 | func logEnd(_ event: BlackBox.EndEvent) { 21 | alertService.showMessage(event.message) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/BlackBox/Helpers/BBHelpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Dictionary where Key == String, Value == Any { 4 | func bbLogDescription(with options: JSONSerialization.WritingOptions?) -> String { 5 | guard JSONSerialization.isValidJSONObject(self), 6 | let jsonData = try? JSONSerialization.data(withJSONObject: self, 7 | options: options ?? []), 8 | let jsonString = String(data: jsonData, encoding: .utf8) 9 | else { return String(describing: self) } 10 | 11 | return jsonString 12 | } 13 | } 14 | 15 | public extension UInt64 { 16 | static var random: Self { 17 | random(in: min...max) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | 8 | updates: 9 | - package-ecosystem: "swift" 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | day: "monday" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" # Location of package manifests 17 | schedule: 18 | interval: "weekly" 19 | day: "monday" 20 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BlackBoxTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackBoxTestCase.swift 3 | // BlackBoxTestCase 4 | // 5 | // Created by Алексей Берёзка on 01.08.2020. 6 | // Copyright © 2020 Dodo Pizza Engineering. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import BlackBox 11 | 12 | class BlackBoxTestCase: XCTestCase { 13 | let timeout: TimeInterval = 1 14 | var logger: BBLoggerProtocol! 15 | var testableLogger: TestableLoggerProtocol { logger as! TestableLoggerProtocol } 16 | 17 | override func setUp() async throws { 18 | try await super.setUp() 19 | logger = LoggerMock() 20 | BlackBox.instance = .init(loggers: [logger]) 21 | } 22 | 23 | override func tearDown() async throws { 24 | logger = nil 25 | try await super.tearDown() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Filter/CustomLogger_Filter_2.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | let levels: [BBLogLevel] 4 | 5 | init(alertService: AlertService, 6 | levels: [BBLogLevel]) { 7 | self.alertService = alertService 8 | self.levels = levels 9 | } 10 | 11 | func log(_ event: BlackBox.GenericEvent) { 12 | alertService.showMessage(event.message) 13 | } 14 | 15 | func log(_ event: BlackBox.ErrorEvent) { 16 | alertService.showError(event.message) 17 | } 18 | 19 | func logStart(_ event: BlackBox.StartEvent) { 20 | // ignore 21 | } 22 | 23 | func logEnd(_ event: BlackBox.EndEvent) { 24 | alertService.showMessage(event.message) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Filter/CustomLogger_Filter_3.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | let levels: [BBLogLevel] 4 | 5 | init(alertService: AlertService, 6 | levels: [BBLogLevel]) { 7 | self.alertService = alertService 8 | self.levels = levels 9 | } 10 | 11 | func log(_ event: BlackBox.GenericEvent) { 12 | guard levels.contains(event.level) else { return } 13 | alertService.showMessage(event.message) 14 | } 15 | 16 | func log(_ event: BlackBox.ErrorEvent) { 17 | guard levels.contains(event.level) else { return } 18 | alertService.showError(event.message) 19 | } 20 | 21 | func logStart(_ event: BlackBox.StartEvent) { 22 | // ignore 23 | } 24 | 25 | func logEnd(_ event: BlackBox.EndEvent) { 26 | guard levels.contains(event.level) else { return } 27 | alertService.showMessage(event.message) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Toggle/CustomLogger_Toggle_1.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | let levels: [BBLogLevel] 4 | 5 | init(alertService: AlertService, 6 | levels: [BBLogLevel]) { 7 | self.alertService = alertService 8 | self.levels = levels 9 | } 10 | 11 | func log(_ event: BlackBox.GenericEvent) { 12 | guard levels.contains(event.level) else { return } 13 | alertService.showMessage(event.message) 14 | } 15 | 16 | func log(_ event: BlackBox.ErrorEvent) { 17 | guard levels.contains(event.level) else { return } 18 | alertService.showError(event.message) 19 | } 20 | 21 | func logStart(_ event: BlackBox.StartEvent) { 22 | // ignore 23 | } 24 | 25 | func logEnd(_ event: BlackBox.EndEvent) { 26 | guard levels.contains(event.level) else { return } 27 | alertService.showMessage(event.message) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/LoggerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggerMock.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import XCTest 9 | import BlackBox 10 | 11 | protocol TestableLoggerProtocol { 12 | var genericEvent: BlackBox.GenericEvent? { get set } 13 | var errorEvent: BlackBox.ErrorEvent? { get set } 14 | var startEvent: BlackBox.StartEvent? { get set } 15 | var endEvent: BlackBox.EndEvent? { get set } 16 | } 17 | 18 | class LoggerMock: BBLoggerProtocol, TestableLoggerProtocol { 19 | 20 | var genericEvent: BlackBox.GenericEvent? 21 | func log(_ event: BlackBox.GenericEvent) { genericEvent = event } 22 | 23 | var errorEvent: BlackBox.ErrorEvent? 24 | func log(_ event: BlackBox.ErrorEvent) { errorEvent = event } 25 | 26 | var startEvent: BlackBox.StartEvent? 27 | func logStart(_ event: BlackBox.StartEvent) { startEvent = event } 28 | 29 | var endEvent: BlackBox.EndEvent? 30 | func logEnd(_ event: BlackBox.EndEvent) { endEvent = event } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "ba5d9f47ff47689e63598cb4c794401a0d9b7dbddf3c9b8c7a391e2e77453029", 3 | "pins" : [ 4 | { 5 | "identity" : "dbthreadsafe-ios", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/dodobrands/DBThreadSafe-ios.git", 8 | "state" : { 9 | "revision" : "98f11ae07f2764e4228bf55c18c48e04a16b18bd", 10 | "version" : "2.3.0" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-docc-plugin", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-docc-plugin", 17 | "state" : { 18 | "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", 19 | "version" : "1.4.5" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-docc-symbolkit", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 26 | "state" : { 27 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 28 | "version" : "1.0.0" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Toggle/CustomLogger_Toggle_2.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | let levels: [BBLogLevel] 4 | let isEnabled: () -> Bool 5 | 6 | init(alertService: AlertService, 7 | levels: [BBLogLevel], 8 | isEnabled: @escaping () -> Bool) { 9 | self.alertService = alertService 10 | self.levels = levels 11 | self.isEnabled = isEnabled 12 | } 13 | 14 | func log(_ event: BlackBox.GenericEvent) { 15 | guard levels.contains(event.level) else { return } 16 | alertService.showMessage(event.message) 17 | } 18 | 19 | func log(_ event: BlackBox.ErrorEvent) { 20 | guard levels.contains(event.level) else { return } 21 | alertService.showError(event.message) 22 | } 23 | 24 | func logStart(_ event: BlackBox.StartEvent) { 25 | // ignore 26 | } 27 | 28 | func logEnd(_ event: BlackBox.EndEvent) { 29 | guard levels.contains(event.level) else { return } 30 | alertService.showMessage(event.message) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Tutorials/CustomLoggers/CustomLoggers_2_Filters.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 2) { 2 | @Intro(title: "Filtering Events in Custom Logger") { 3 | Sometimes your need to filter logs based on their level. 4 | 5 | Each logger have it's own level filters, so you may want to implement that logic inside your custom logger too. 6 | } 7 | 8 | @Section(title: "Advanced Custom Logger") { 9 | @ContentAndMedia { 10 | In this tutorial we'll add level filters to existing custom logger 11 | } 12 | 13 | @Steps { 14 | @Step { 15 | Open your custom logger code 16 | 17 | @Code(name: "", file: "CustomLogger_Filter_1") 18 | } 19 | 20 | @Step { 21 | Add `levels` argument to initializer 22 | 23 | @Code(name: "", file: "CustomLogger_Filter_2") 24 | } 25 | 26 | @Step { 27 | Filter events based on this levels 28 | 29 | @Code(name: "", file: "CustomLogger_Filter_3") 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/Toggle/CustomLogger_Toggle_3.swift: -------------------------------------------------------------------------------- 1 | class AlertLogger: BBLoggerProtocol { 2 | let alertService: AlertService 3 | let levels: [BBLogLevel] 4 | let isEnabled: () -> Bool 5 | 6 | init(alertService: AlertService, 7 | levels: [BBLogLevel], 8 | isEnabled: @escaping () -> Bool) { 9 | self.alertService = alertService 10 | self.levels = levels 11 | self.isEnabled = isEnabled 12 | } 13 | 14 | func log(_ event: BlackBox.GenericEvent) { 15 | guard isEnabled() else { return } 16 | guard levels.contains(event.level) else { return } 17 | alertService.showMessage(event.message) 18 | } 19 | 20 | func log(_ event: BlackBox.ErrorEvent) { 21 | guard isEnabled() else { return } 22 | guard levels.contains(event.level) else { return } 23 | alertService.showError(event.message) 24 | } 25 | 26 | func logStart(_ event: BlackBox.StartEvent) { 27 | // ignore 28 | } 29 | 30 | func logEnd(_ event: BlackBox.EndEvent) { 31 | guard isEnabled() else { return } 32 | guard levels.contains(event.level) else { return } 33 | alertService.showMessage(event.message) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/update_docs.yml: -------------------------------------------------------------------------------- 1 | name: Update Documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | env: 20 | SCHEME: "BlackBox" 21 | DERIVED_DATA_PATH: './DerivedData/' 22 | DESTINATION: 'platform=iOS Simulator,name=iPhone 15' 23 | HOSTING_BASE_PATH: 'BlackBox' 24 | 25 | jobs: 26 | update-docs: 27 | runs-on: macos-15 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v6 31 | 32 | - name: Setup Pages 33 | uses: actions/configure-pages@v5 34 | 35 | - name: Build 36 | run: | 37 | swift package --allow-writing-to-directory ${{ env.DERIVED_DATA_PATH }} generate-documentation --target ${{ env.SCHEME }} --disable-indexing --transform-for-static-hosting --hosting-base-path '${{ env.HOSTING_BASE_PATH }}' --output-path ${{ env.DERIVED_DATA_PATH }} 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v4 41 | with: 42 | path: ${{ env.DERIVED_DATA_PATH }} 43 | 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | 48 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue with the owners of this repository before making a change. 4 | 5 | ## Pull Request Process 6 | 7 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 8 | 9 | Pull requests are the best way to propose changes to the codebase (we use [GitHub Flow](https://guides.github.com/introduction/flow/index.html)). We actively welcome your pull requests: 10 | 11 | 1. Fork the repo and create your branch from master. 12 | 2. If you've added code that should be tested, add tests. 13 | 3. If you've changed APIs, update the documentation. 14 | 4. Ensure the test suite passes. 15 | 5. Issue that pull request. 16 | 6. Properly fill pull request body section. Best practice here is to describe your changes as a list of changes and add link to the according issue for each change. 17 | 18 | ## Report bugs and feature suggestions using GitHub's issues 19 | 20 | We use GitHub issues to track public bugs and feature suggestions. Report a bug or suggest a feature by opening a new issue. 21 | 22 | ## License 23 | 24 | By contributing, you agree that your contributions will be licensed under its [Apache License 2.0](../LICENSE). 25 | 26 | ## Code of Conduct 27 | 28 | This project has adopted the [Code of Conduct](./CODE_OF_CONDUCT.md). 29 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Resources/CustomLoggers/point.topleft.down.curvedto.point.filled.bottomright.up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sources/BlackBox/BBLogLevel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum BBLogLevel: String, CaseIterable { 4 | case debug, info, warning, error 5 | } 6 | 7 | extension Array where Element == BBLogLevel { 8 | public static var allCases: [Element] { BBLogLevel.allCases } 9 | } 10 | 11 | extension BBLogLevel { 12 | /// Icons used when formatting messages with loggers 13 | /// 14 | /// See ``BBLogIcon`` for more details 15 | @available(*, deprecated, message: "Use BBLogFormat.Icons") 16 | public var icon: String { 17 | switch self { 18 | case .debug: return BBLogIcon.debug 19 | case .info: return BBLogIcon.info 20 | case .warning: return BBLogIcon.warning 21 | case .error: return BBLogIcon.error 22 | } 23 | } 24 | } 25 | 26 | /// Icons optionally used in messages to improve readability 27 | /// 28 | /// See ``BBLogFormat/levelsWithIcons`` for more details 29 | @available(*, deprecated, message: "Use struct BBLogFormat.Icons") 30 | public struct BBLogIcon { 31 | nonisolated(unsafe) public static var debug = "🛠" 32 | nonisolated(unsafe) public static var info = "ℹ️" 33 | nonisolated(unsafe) public static var warning = "⚠️" 34 | nonisolated(unsafe) public static var error = "❌" 35 | } 36 | 37 | public protocol BBLogLevelProvider where Self: Swift.Error { 38 | var level: BBLogLevel { get } 39 | } 40 | 41 | public extension Swift.Error { 42 | var level: BBLogLevel { 43 | if let levelProvider = self as? BBLogLevelProvider { 44 | return levelProvider.level 45 | } else { 46 | return .error 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Articles/LoggingErrors.md: -------------------------------------------------------------------------------- 1 | # Logging Errors 2 | Improved errors logging 3 | 4 | ### Codes and User Info 5 | `Swift.Error` doesn't provide error code, unless it's inherited from `Int`. But relying on it leads to collisions of error codes once you remove some cases from your custom Error. 6 | If you are planning to gather analytics based on your errors you definetely do not want that behaviour. 7 | 8 | 9 | You can provide custom and fixed error codes and even user info by implementing `CustomNSError` and overriding both `errorCode` and `errorUserInfo` for your Errors. 10 | ```swift 11 | extension ParsingError: CustomNSError { 12 | var errorCode: Int { 13 | switch self { 14 | case .unknownCategoryInDTO: 15 | return 0 16 | } 17 | } 18 | 19 | var errorUserInfo: [String : Any] { 20 | switch self { 21 | case .unknownCategoryInDTO(let rawValue): 22 | return ["rawValue": rawValue] 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ### Levels 29 | Each `Swift.Error` is logged with `.error` log level by default. 30 | 31 | Implement ``BBLogLevelProvider`` to provide custom log levels for your errors 32 | ```swift 33 | extension ParsingError: BBLogLevelProvider { 34 | var level: BBLogLevel { 35 | switch self { 36 | case .unknownCategoryInDTO(let rawValue): 37 | if rawValue == 2 { 38 | // deprecated category, may be present in orders created before 2019 39 | return .warning 40 | } else { 41 | // unsupported category 42 | return .error 43 | } 44 | } 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Tutorials/CustomLoggers/CustomLoggers_1_Base.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 2) { 2 | @Intro(title: "Creating Custom Logger") { 3 | If presented loggers doesn't fit your need you can create your very own logger and do whatever you want with it. 4 | } 5 | 6 | @Section(title: "Base Custom Logger") { 7 | @ContentAndMedia { 8 | In this tutorial we'll create custom logger with base functionality 9 | } 10 | 11 | @Steps { 12 | @Step { 13 | Create your logger 14 | 15 | @Code(name: "", file: "CustomLogger_Base_1.swift") 16 | } 17 | 18 | @Step { 19 | Conform it to ``BlackBox/BBLoggerProtocol`` 20 | 21 | @Code(name: "", file: "CustomLogger_Base_2.swift") 22 | } 23 | 24 | @Step { 25 | Implement methods required by ``BlackBox/BBLoggerProtocol`` 26 | 27 | @Code(name: "", file: "CustomLogger_Base_3.swift") 28 | } 29 | 30 | @Step { 31 | Redirect received events to whenever you want. 32 | For example, show them as alerts inside your app. 33 | 34 | @Code(name: "", file: "CustomLogger_Base_4.swift") 35 | } 36 | 37 | @Step { 38 | Assing new BlackBox instance with all desired loggers, and yours among them. 39 | 40 | @Code(name: "", file: "CustomLogger_Base_5.swift") 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let libraryName = "BlackBox" 7 | let packageName = libraryName 8 | let targetName = libraryName 9 | let testsTargetName = targetName + "Tests" 10 | 11 | let exampleModuleName = "ExampleModule" 12 | 13 | let package = Package( 14 | name: packageName, 15 | platforms: [ 16 | .iOS(.v12), 17 | .macOS(.v10_14), 18 | .tvOS(.v12), 19 | .watchOS(.v5) 20 | ], 21 | products: [ 22 | .library( 23 | name: libraryName, 24 | targets: [ 25 | targetName 26 | ] 27 | ), 28 | ], 29 | dependencies: [ 30 | .package(url: "https://github.com/apple/swift-docc-plugin", .upToNextMajor(from: "1.0.0")), 31 | .package(url: "https://github.com/dodobrands/DBThreadSafe-ios.git", .upToNextMajor(from: "2.0.0")) 32 | ], 33 | targets: [ 34 | .target( 35 | name: targetName, 36 | dependencies: [ 37 | .product(name: "DBThreadSafe", package: "DBThreadSafe-ios") 38 | ] 39 | ), 40 | .target( 41 | name: exampleModuleName, 42 | dependencies: [ 43 | .init(stringLiteral: targetName) 44 | ] 45 | ), 46 | .testTarget( 47 | name: testsTargetName, 48 | dependencies: [ 49 | .init(stringLiteral: targetName), 50 | .init(stringLiteral: exampleModuleName) 51 | ] 52 | ), 53 | ], 54 | swiftLanguageModes: [.v6] 55 | ) 56 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | tests: 15 | runs-on: macos-15 16 | 17 | timeout-minutes: 10 18 | 19 | env: 20 | SCHEME: "BlackBox" 21 | 22 | strategy: 23 | matrix: 24 | DESTINATION: ["platform=iOS Simulator,name=iPhone 16", "platform=OS X", "platform=tvOS Simulator,name=Apple TV", "platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm)"] 25 | 26 | steps: 27 | - name: Get source code 28 | uses: actions/checkout@v6 29 | 30 | - name: Prepare Environment for App Build 31 | uses: ./.github/actions/prepare_env_app_build 32 | 33 | - name: Build 34 | run: > 35 | xcodebuild build-for-testing 36 | -scheme ${{ env.SCHEME }} 37 | -destination '${{ matrix.DESTINATION }}' 38 | -quiet 39 | 40 | - name: Test 41 | id: test 42 | run: | 43 | xcresult="${{ env.SCHEME }}-${{ matrix.DESTINATION }}.xcresult" 44 | xcodebuild test-without-building \ 45 | -scheme ${{ env.SCHEME }} \ 46 | -destination '${{ matrix.DESTINATION }}' \ 47 | -resultBundlePath "$xcresult" \ 48 | -quiet 49 | 50 | echo "xcresult=$xcresult" >> $GITHUB_OUTPUT 51 | 52 | - uses: actions/upload-artifact@v6 53 | with: 54 | path: "${{ steps.test.outputs.xcresult }}" 55 | name: "${{ steps.test.outputs.xcresult }}" 56 | 57 | # This allows us to have a branch protection rule for tests and deploys with matrix 58 | status-for-matrix: 59 | runs-on: 'ubuntu-latest' 60 | needs: tests 61 | if: always() 62 | steps: 63 | - name: Calculate matrix result 64 | run: | 65 | result="${{ needs.tests.result }}" 66 | if [[ $result == "success" ]]; then 67 | exit 0 68 | else 69 | exit 1 70 | fi 71 | -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Tutorials/CustomLoggers/CustomLoggers_3_Toggle.tutorial: -------------------------------------------------------------------------------- 1 | @Tutorial(time: 2) { 2 | @Intro(title: "Turning Custom Logger On and Off Based on Toggle") { 3 | When you're not sure if your new custom logger is stable enough you may want to have an ability to turn it off remotely and instantly, without releasing a new version of your app. 4 | 5 | In this tutorial we take a look at one possible solution for this. 6 | } 7 | 8 | @Section(title: "Advanced Custom Logger") { 9 | @ContentAndMedia { 10 | This tutorial relies on remote feature toggles that are already integrated in your app. 11 | 12 | If you not quite sure what is remote feature toggle and how does it work, make sure to read some articles on the web. 13 | For example, [Remote Config Feature Flagging: A Full Walkthrough](https://medium.com/firebase-developers/remote-config-feature-flagging-a-full-walkthrough-9b2f2188bb47 ). 14 | } 15 | 16 | @Steps { 17 | @Step { 18 | Open your custom logger code 19 | 20 | @Code(name: "", file: "CustomLogger_Toggle_1") 21 | } 22 | 23 | @Step { 24 | Add `isEnabled` argument to initializer 25 | 26 | We are using closure instead of `Bool`. 27 | Closure will be invoked each time your logger receives new log. 28 | This opens up possibility to turn your logger On or Off anytime during your app lifecycle. 29 | 30 | @Code(name: "", file: "CustomLogger_Toggle_2") 31 | } 32 | 33 | @Step { 34 | Prevent logging if `isEnabled` returns `false` 35 | 36 | @Code(name: "", file: "CustomLogger_Toggle_3") 37 | } 38 | 39 | @Step { 40 | Get correct value for your remote toggle. 41 | 42 | @Code(name: "", file: "CustomLogger_Toggle_4") 43 | } 44 | 45 | @Step { 46 | Pass that value right into your logger initializer 47 | 48 | @Code(name: "", file: "CustomLogger_Toggle_5") 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Brewfile.lock.json 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | 30 | ## App packaging 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | **/.DS_Store 95 | .swiftpm -------------------------------------------------------------------------------- /Sources/BlackBox/Documentation.docc/Articles/README.md: -------------------------------------------------------------------------------- 1 | # ``BlackBox`` 2 | 3 | Library for logs and measurements. 4 | 5 | ![BlackBox logo](bb_logo.png) 6 | 7 | BlackBox provides convenience ways to log and measure what happens in your app: 8 | - Events; 9 | - Errors; 10 | - How much time it took to execute some code; 11 | - etc. 12 | 13 | Moreover, you can redirect the logs wherever you want. Few destinations are supported out of the box, and you can easily add any other destination by yourself. 14 | 15 | 16 | For installation tips, see 17 | 18 | ## Writing logs 19 | Log debug message: 20 | ```swift 21 | BlackBox.debug("Hello world") 22 | ``` 23 | 24 | Log info message: 25 | ```swift 26 | BlackBox.info("Hello world") 27 | ``` 28 | 29 | Provide additional information: 30 | ```swift 31 | BlackBox.debug("Logged in", userInfo: ["userId": user.id]) 32 | ``` 33 | > Important: Do not include sensitive data in logs 34 | 35 | Categorize logs: 36 | ```swift 37 | BlackBox.debug("Logged in", userInfo: ["userId": someUserId], category: "App lifecycle") 38 | ``` 39 | 40 | Provide log level using argument: 41 | ```swift 42 | BlackBox.log("Tried to open AuthScreen multiple times", level: .warning) 43 | ``` 44 | 45 | Log errors: 46 | ```swift 47 | enum ParsingError: Error { 48 | case unknownCategoryInDTO(rawValue: Int) 49 | } 50 | 51 | BlackBox.log(ParsingError.unknownCategoryInDTO(rawValue: 9)) 52 | ``` 53 | 54 | > Tip: For improved errors logging, see 55 | 56 | Measure your code: 57 | ```swift 58 | let log = BlackBox.debugStart("Parse menu") // or infoStart 59 | let menuModel = MenuModel(dto: menuDto) 60 | // any other hard work 61 | BlackBox.logEnd(log) 62 | ``` 63 | 64 | or provide log level using argument: 65 | ```swift 66 | let log = BlackBox.logStart("Parse menu", level: .warning) 67 | ``` 68 | 69 | Mix all of the above altogether: 70 | ```swift 71 | BlackBox.info( 72 | "Geolocation service started", 73 | userInfo: ["accuracy": "low"] 74 | level: .info, 75 | category: "Location" 76 | ) 77 | ``` 78 | 79 | 80 | ## Reading logs 81 | 82 | BlackBox redirects all received logs to loggers. 83 | Each logger in turn redirects logs to some target system, so to read logs you have to go there. 84 | 85 | ### Available Loggers 86 | - ``OSLogger`` — logs to macOS Console.app and Xcode console. 87 | - ``OSSignpostLogger`` — logs to Time Profiler. 88 | - ``FSLogger`` — logs to text file. 89 | 90 | > Tip: You can create your very own loggers and use it with BlackBox. For more information, see 91 | 92 | #### External Loggers 93 | - [BlackBoxFirebasePerformance](https://github.com/dodobrands/BlackBoxFirebasePerformance) — redirects logs to Firebase Performance 94 | - [BlackBoxFirebaseCrashlytics](https://github.com/dodobrands/BlackBoxFirebaseCrashlytics) — redirects logs to Firebase Crashlytics 95 | 96 | If you've created your own logger — feel free to extend this list with PR. 97 | 98 | ### Setting up loggers 99 | BlackBox automatically enables `OSLogger` and `OSSignpostLogger` with all available log levels. 100 | You can customize this behaviour by assigning new BlackBox instance with required loggers: 101 | ```swift 102 | let loggers = [ 103 | OSLogger(levels: .allCases), 104 | OSSignpostLogger(levels: [.debug, .info]) 105 | YourCustomLogger() 106 | ] 107 | BlackBox.instance = BlackBox(loggers: loggers) 108 | ``` 109 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at devcommunity@dodopizza.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | 77 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BlackBoxErrorEventTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackBoxErrorEventTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import BlackBox 10 | @testable import ExampleModule 11 | 12 | class BlackBoxErrorEventTests: BlackBoxTestCase { 13 | func test_error() { 14 | BlackBox.log(AnakinKills.maceWindu) 15 | XCTAssertEqual(testableLogger.errorEvent?.error as? AnakinKills, AnakinKills.maceWindu) 16 | } 17 | 18 | func test_message() { 19 | BlackBox.log(AnakinKills.maceWindu) 20 | XCTAssertEqual(testableLogger.errorEvent?.message, "AnakinKills.maceWindu") 21 | } 22 | 23 | func test_message_fromAnotherModule() { 24 | ExampleService().logSomeError() 25 | XCTAssertEqual(testableLogger.errorEvent?.message, "ExampleError.taskFailed") 26 | } 27 | 28 | func test_message_hasNoWithAssociatedValue() { 29 | BlackBox.log(AnakinKills.younglings(count: 11)) 30 | XCTAssertEqual(testableLogger.errorEvent?.message, "AnakinKills.younglings") 31 | } 32 | 33 | func test_userInfo_hasAssociatedValue() { 34 | BlackBox.log(AnakinKills.younglings(count: 11)) 35 | XCTAssertEqual(testableLogger.errorEvent?.userInfo as? [String: Int], ["count": 11]) 36 | } 37 | 38 | func test_serviceInfo() { 39 | BlackBox.log(AnakinKills.maceWindu, serviceInfo: Lightsaber(color: "purple")) 40 | XCTAssertEqual(testableLogger.errorEvent?.serviceInfo as? Lightsaber, Lightsaber(color: "purple")) 41 | } 42 | 43 | func test_defaultLevel() { 44 | BlackBox.log(AnakinKills.maceWindu) 45 | XCTAssertEqual(testableLogger.errorEvent?.level, .error) 46 | } 47 | 48 | func test_category() { 49 | BlackBox.log(AnakinKills.maceWindu, category: "Analytics") 50 | XCTAssertEqual(testableLogger.errorEvent?.category, "Analytics") 51 | } 52 | 53 | func test_parentEvent() { 54 | let parentEvent = BlackBox.GenericEvent("Test") 55 | BlackBox.log(AnakinKills.maceWindu, parentEvent: parentEvent) 56 | XCTAssertEqual(testableLogger.errorEvent?.parentEvent, parentEvent) 57 | } 58 | 59 | func test_fileID() { 60 | BlackBox.log(AnakinKills.maceWindu) 61 | XCTAssertEqual(testableLogger.errorEvent?.source.fileID.description, "BlackBoxTests/BlackBoxErrorEventTests.swift") 62 | } 63 | 64 | func test_module() { 65 | BlackBox.log(AnakinKills.maceWindu) 66 | XCTAssertEqual(testableLogger.errorEvent?.source.module, "BlackBoxTests") 67 | } 68 | 69 | func test_filename() { 70 | BlackBox.log(AnakinKills.maceWindu) 71 | XCTAssertEqual(testableLogger.errorEvent?.source.filename, "BlackBoxErrorEventTests") 72 | } 73 | 74 | func test_function() { 75 | BlackBox.log(AnakinKills.maceWindu) 76 | XCTAssertEqual(testableLogger.errorEvent?.source.function.description, "test_function()") 77 | } 78 | 79 | func test_line() { 80 | BlackBox.log(AnakinKills.maceWindu) 81 | XCTAssertEqual(testableLogger.errorEvent?.source.line, 80) 82 | } 83 | 84 | func test_durationFormattedIsNil() { 85 | BlackBox.log(AnakinKills.maceWindu) 86 | XCTAssertNil(testableLogger.errorEvent?.formattedDuration(using: MeasurementFormatter())) 87 | } 88 | 89 | func test_messageWithDurationFormattedIsMessageItself() { 90 | BlackBox.log(AnakinKills.maceWindu) 91 | XCTAssertEqual(testableLogger.errorEvent?.message, testableLogger.errorEvent?.messageWithFormattedDuration(using: MeasurementFormatter())) 92 | } 93 | 94 | func test_isTrace() { 95 | BlackBox.log(AnakinKills.maceWindu) 96 | XCTAssertEqual(testableLogger.errorEvent?.isTrace, false) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BlackBoxGenericEventTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackBoxGenericEventTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import BlackBox 10 | @testable import ExampleModule 11 | 12 | class BlackBoxGenericEventTests: BlackBoxTestCase { 13 | func test_message() { 14 | BlackBox.log("Test") 15 | XCTAssertEqual(testableLogger.genericEvent?.message, "Test") 16 | } 17 | 18 | func test_userInfo() { 19 | BlackBox.log("Test", userInfo: ["name": "Kenobi"]) 20 | XCTAssertEqual(testableLogger.genericEvent?.userInfo as? [String: String], ["name": "Kenobi"]) 21 | } 22 | 23 | func test_serviceInfo() { 24 | BlackBox.log("Test", serviceInfo: Lightsaber(color: "purple")) 25 | XCTAssertEqual(testableLogger.genericEvent?.serviceInfo as? Lightsaber, Lightsaber(color: "purple")) 26 | } 27 | 28 | func test_level() { 29 | BlackBox.log("Test", level: .warning) 30 | XCTAssertEqual(testableLogger.genericEvent?.level, .warning) 31 | } 32 | 33 | func test_levelInFuncNameDebug() { 34 | BlackBox.debug("Test") 35 | XCTAssertEqual(testableLogger.genericEvent?.level, .debug) 36 | } 37 | 38 | func test_levelInFuncNameInfo() { 39 | BlackBox.info("Test") 40 | XCTAssertEqual(testableLogger.genericEvent?.level, .info) 41 | } 42 | 43 | func test_defaultLevel() { 44 | BlackBox.log("Test") 45 | XCTAssertEqual(testableLogger.genericEvent?.level, .debug) 46 | } 47 | 48 | func test_category() { 49 | BlackBox.log("Test", category: "Analytics") 50 | XCTAssertEqual(testableLogger.genericEvent?.category, "Analytics") 51 | } 52 | 53 | func test_parentEvent() { 54 | let parentEvent = BlackBox.GenericEvent("Test") 55 | BlackBox.log("Test 2", parentEvent: parentEvent) 56 | XCTAssertEqual(testableLogger.genericEvent?.parentEvent, parentEvent) 57 | } 58 | 59 | func test_fileID() { 60 | BlackBox.log("Test") 61 | XCTAssertEqual(testableLogger.genericEvent?.source.fileID.description, "BlackBoxTests/BlackBoxGenericEventTests.swift") 62 | } 63 | 64 | func test_module() { 65 | BlackBox.log("Test") 66 | XCTAssertEqual(testableLogger.genericEvent?.source.module, "BlackBoxTests") 67 | } 68 | 69 | func test_anotherModule() { 70 | ExampleService().doSomeWork() 71 | XCTAssertEqual(testableLogger.genericEvent?.source.module, "ExampleModule") 72 | } 73 | 74 | func test_filename() { 75 | BlackBox.log("Test") 76 | XCTAssertEqual(testableLogger.genericEvent?.source.filename, "BlackBoxGenericEventTests") 77 | } 78 | 79 | func test_function() { 80 | BlackBox.log("Test") 81 | XCTAssertEqual(testableLogger.genericEvent?.source.function.description, "test_function()") 82 | } 83 | 84 | func test_line() { 85 | BlackBox.log("Test") 86 | XCTAssertEqual(testableLogger.genericEvent?.source.line, 85) 87 | } 88 | 89 | func test_durationFormattedIsNil() { 90 | BlackBox.log("Test") 91 | XCTAssertNil(testableLogger.genericEvent?.formattedDuration(using: MeasurementFormatter())) 92 | } 93 | 94 | func test_messageWithDurationFormattedIsMessageItself() { 95 | BlackBox.log("Test") 96 | XCTAssertEqual(testableLogger.genericEvent?.message, testableLogger.genericEvent?.messageWithFormattedDuration(using: MeasurementFormatter())) 97 | } 98 | 99 | func test_isTrace() { 100 | BlackBox.log("Test") 101 | XCTAssertEqual(testableLogger.genericEvent?.isTrace, false) 102 | } 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /Sources/BlackBox/BBLogFormat.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct BBLogFormat { 4 | public struct Icons { 5 | public let debug: String? 6 | public let info: String? 7 | public let warning: String? 8 | public let error: String? 9 | 10 | public init( 11 | debug: String? = nil, 12 | info: String? = nil, 13 | warning: String? = nil, 14 | error: String? = nil 15 | ) { 16 | self.debug = debug 17 | self.info = info 18 | self.warning = warning 19 | self.error = error 20 | } 21 | 22 | func icon(for level: BBLogLevel) -> String? { 23 | switch level { 24 | case .debug: return debug 25 | case .info: return info 26 | case .warning: return warning 27 | case .error: return error 28 | } 29 | } 30 | 31 | public static var withDefaultIcons: Icons { 32 | Icons(debug: "🛠", info: "ℹ️", warning: "⚠️", error: "❌") 33 | } 34 | } 35 | /// Used for formatting JSONs to String 36 | public let userInfoFormatOptions: JSONSerialization.WritingOptions 37 | 38 | /// Defines how information about log source is formatter: multiline or one-line. 39 | /// > Examples: 40 | /// ``` 41 | /// // False 42 | /// [Source] 43 | /// OSLoggerTests:246 44 | /// test_whenLogFormatApplied_showingLevelIcon() 45 | /// ``` 46 | /// ``` 47 | /// // True 48 | /// [Source] OSLoggerTests:246 test_whenLogFormatApplied_showingLevelIcon() 49 | /// ``` 50 | public let sourceSectionInline: Bool 51 | 52 | /// Messages with this levels should get appropriate ``BBLogIcon`` icon in message. 53 | /// 54 | /// Icon position depends on formatter rules. 55 | @available(*, deprecated, message: "Use levelsIcons") 56 | public let levelsWithIcons: [BBLogLevel] 57 | 58 | public let levelsIcons: Icons 59 | 60 | /// Used for formatting traces duration 61 | public let measurementFormatter: MeasurementFormatter 62 | 63 | /// Improves logs readability in Xcode 14 embedded console 64 | /// > Examples: 65 | /// `False` 66 | /// ``` 67 | /// Hello there 68 | /// ``` 69 | /// 70 | /// `True` 71 | /// ``` 72 | /// 73 | /// 🛠 Hello there 74 | /// ``` 75 | public let addEmptyLinePrefix: Bool 76 | 77 | /// Creates `BBLogFormat` instance 78 | /// - Parameters: 79 | /// - userInfoFormatOptions:Options for output JSON data. 80 | /// - sourceSectionInline: Print `Source` section in console inline 81 | /// - levelsWithIcons: Logs with this levels should have appropriate level icon 82 | /// - levelsIcons: Icons for each log level 83 | /// - measurementFormatter: Formatter used for traces durations output 84 | /// - addEmptyLinePrefix: Logger should add empty line prefix before message 85 | public init( 86 | userInfoFormatOptions: JSONSerialization.WritingOptions = .prettyPrinted, 87 | sourceSectionInline: Bool = false, 88 | levelsWithIcons: [BBLogLevel] = [], 89 | levelsIcons: Icons = Icons(), 90 | measurementFormatter: MeasurementFormatter = MeasurementFormatter(), 91 | addEmptyLinePrefix: Bool = false 92 | ) { 93 | self.userInfoFormatOptions = userInfoFormatOptions 94 | self.sourceSectionInline = sourceSectionInline 95 | self.levelsWithIcons = levelsWithIcons 96 | self.levelsIcons = levelsIcons 97 | self.measurementFormatter = measurementFormatter 98 | self.addEmptyLinePrefix = addEmptyLinePrefix 99 | } 100 | } 101 | 102 | extension BBLogFormat { 103 | public func icon(for level: BBLogLevel) -> String? { 104 | if levelsWithIcons.contains(level) { 105 | return level.icon 106 | } else if let icon = levelsIcons.icon(for: level) { 107 | return icon 108 | } else { 109 | return nil 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/BlackBox/Loggers/OSSignpostLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os 3 | 4 | /// Redirects logs to Time Profiler 5 | public class OSSignpostLogger: BBLoggerProtocol { 6 | let levels: [BBLogLevel] 7 | 8 | public init(levels: [BBLogLevel]){ 9 | self.levels = levels 10 | } 11 | 12 | public func log(_ event: BlackBox.GenericEvent) { 13 | signpostLog(event: event) 14 | } 15 | 16 | public func log(_ event: BlackBox.ErrorEvent) { 17 | signpostLog(event: event) 18 | } 19 | 20 | public func logStart(_ event: BlackBox.StartEvent) { 21 | signpostLog(event: event) 22 | } 23 | 24 | public func logEnd(_ event: BlackBox.EndEvent) { 25 | signpostLog(event: event) 26 | } 27 | 28 | private func signpostLog(event: BlackBox.GenericEvent) { 29 | guard levels.contains(event.level) else { return } 30 | 31 | let data = LogData(from: event) 32 | 33 | signpostLog(data) 34 | } 35 | 36 | func signpostLog(_ data: LogData) { 37 | let log = OSLog( 38 | subsystem: data.subsystem, 39 | category: data.category 40 | ) 41 | 42 | os_signpost(data.signpostType, 43 | log: log, 44 | name: data.name, 45 | signpostID: data.signpostId, 46 | "%{public}@", data.message) 47 | } 48 | } 49 | 50 | extension OSSignpostLogger { 51 | struct LogData { 52 | let signpostType: OSSignpostType 53 | let signpostId: OSSignpostID 54 | let subsystem: String 55 | let category: String 56 | let name: StaticString 57 | let message: String 58 | 59 | init( 60 | signpostType: OSSignpostType, 61 | signpostId: OSSignpostID, 62 | subsystem: String, 63 | category: String, 64 | name: StaticString, 65 | message: String 66 | ) { 67 | self.signpostType = signpostType 68 | self.signpostId = signpostId 69 | self.subsystem = subsystem 70 | self.category = category 71 | self.name = name 72 | self.message = message 73 | } 74 | 75 | init(from event: BlackBox.GenericEvent) { 76 | // traces should be finished from where they've started 77 | let subsystem = (event.startEventIfExists ?? event).source.module 78 | let category = (event.startEventIfExists ?? event).category ?? (event.startEventIfExists ?? event).source.filename 79 | let name = (event.startEventIfExists ?? event).source.function 80 | 81 | let signpostId = OSSignpostID(event) 82 | self.init( 83 | signpostType: OSSignpostType(event), 84 | signpostId: signpostId, 85 | subsystem: subsystem, 86 | category: category, 87 | name: name, 88 | message: event.message 89 | ) 90 | } 91 | } 92 | } 93 | 94 | extension OSSignpostType { 95 | init(_ event: BlackBox.GenericEvent) { 96 | switch event { 97 | case _ as BlackBox.StartEvent: 98 | self = .begin 99 | case _ as BlackBox.EndEvent: 100 | self = .end 101 | default: 102 | self = .event 103 | } 104 | } 105 | } 106 | 107 | extension OSSignpostID { 108 | init(_ event: BlackBox.GenericEvent) { 109 | let id: UUID = (event.startEventIfExists ?? event).id 110 | self = OSSignpostID(id) 111 | } 112 | 113 | init(_ uuid: UUID) { 114 | let value = UInt64(abs(uuid.hashValue)) // uniqueness not guaranteed, but chances are ridiculous 115 | self = OSSignpostID(value) 116 | } 117 | } 118 | 119 | fileprivate extension BlackBox.GenericEvent { 120 | var startEventIfExists: BlackBox.StartEvent? { 121 | guard let endEvent = self as? BlackBox.EndEvent else { return nil } 122 | return endEvent.startEvent 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BlackBoxStartEventTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackBoxStartEventTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import BlackBox 10 | 11 | class BlackBoxStartEventTests: BlackBoxTestCase { 12 | func test_logStart_event() { 13 | let event = BlackBox.StartEvent("Test") 14 | BlackBox.logStart(event) 15 | XCTAssertEqual(testableLogger.startEvent, event) 16 | } 17 | func test_message() { 18 | let _ = BlackBox.logStart("Test") 19 | XCTAssertEqual(testableLogger.startEvent?.message, "Start: Test") 20 | } 21 | 22 | func test_rawMessage() { 23 | let _ = BlackBox.logStart("Test") 24 | XCTAssertEqual(testableLogger.startEvent?.rawMessage.description, "Test".description) 25 | } 26 | 27 | func test_userInfo() { 28 | let _ = BlackBox.logStart("Test", userInfo: ["name": "Kenobi"]) 29 | XCTAssertEqual(testableLogger.startEvent?.userInfo as? [String: String], ["name": "Kenobi"]) 30 | } 31 | 32 | func test_serviceInfo() { 33 | let _ = BlackBox.logStart("Test", serviceInfo: Lightsaber(color: "purple")) 34 | XCTAssertEqual(testableLogger.startEvent?.serviceInfo as? Lightsaber, Lightsaber(color: "purple")) 35 | } 36 | 37 | func test_level() { 38 | let _ = BlackBox.logStart("Test", level: .warning) 39 | XCTAssertEqual(testableLogger.startEvent?.level, .warning) 40 | } 41 | 42 | func test_levelInFuncNameDebug() { 43 | let _ = BlackBox.debugStart("Test") 44 | XCTAssertEqual(testableLogger.startEvent?.level, .debug) 45 | } 46 | 47 | func test_levelInFuncNameInfo() { 48 | let _ = BlackBox.infoStart("Test") 49 | XCTAssertEqual(testableLogger.startEvent?.level, .info) 50 | } 51 | 52 | func test_defaultLevel() { 53 | let _ = BlackBox.logStart("Test") 54 | XCTAssertEqual(testableLogger.startEvent?.level, .debug) 55 | } 56 | 57 | func test_category() { 58 | let _ = BlackBox.logStart("Test", category: "Analytics") 59 | XCTAssertEqual(testableLogger.startEvent?.category, "Analytics") 60 | } 61 | 62 | func test_parentEvent() { 63 | let parentEvent = BlackBox.StartEvent("Test") 64 | let _ = BlackBox.logStart("Test 2", parentEvent: parentEvent) 65 | XCTAssertEqual(testableLogger.startEvent?.parentEvent, parentEvent) 66 | } 67 | 68 | func test_fileID() { 69 | let _ = BlackBox.logStart("Test") 70 | XCTAssertEqual(testableLogger.startEvent?.source.fileID.description, "BlackBoxTests/BlackBoxStartEventTests.swift") 71 | } 72 | 73 | func test_module() { 74 | let _ = BlackBox.logStart("Test") 75 | XCTAssertEqual(testableLogger.startEvent?.source.module, "BlackBoxTests") 76 | } 77 | 78 | func test_filename() { 79 | let _ = BlackBox.logStart("Test") 80 | XCTAssertEqual(testableLogger.startEvent?.source.filename, "BlackBoxStartEventTests") 81 | } 82 | 83 | func test_function() { 84 | let _ = BlackBox.logStart("Test") 85 | XCTAssertEqual(testableLogger.startEvent?.source.function.description, "test_function()") 86 | } 87 | 88 | func test_line() { 89 | let _ = BlackBox.logStart("Test") 90 | XCTAssertEqual(testableLogger.startEvent?.source.line, 89) 91 | } 92 | 93 | func test_durationFormattedIsNil() { 94 | let _ = BlackBox.logStart("Test") 95 | XCTAssertNil(testableLogger.startEvent?.formattedDuration(using: MeasurementFormatter())) 96 | } 97 | 98 | func test_messageWithDurationFormattedIsMessageItself() { 99 | let _ = BlackBox.logStart("Test") 100 | XCTAssertEqual(testableLogger.startEvent?.message, testableLogger.startEvent?.messageWithFormattedDuration(using: MeasurementFormatter())) 101 | } 102 | 103 | func test_isTrace() { 104 | let _ = BlackBox.logStart("Test") 105 | XCTAssertEqual(testableLogger.startEvent?.isTrace, true) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BlackBoxEndEventTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlackBoxEndEventTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import BlackBox 10 | 11 | class BlackBoxEndEventTests: BlackBoxTestCase { 12 | var event: BlackBox.StartEvent! 13 | 14 | override func setUpWithError() throws { 15 | try super.setUpWithError() 16 | event = BlackBox.StartEvent("Test") 17 | } 18 | 19 | func test_startEvent() { 20 | BlackBox.logEnd(event) 21 | XCTAssertEqual(testableLogger.endEvent?.startEvent, event) 22 | } 23 | 24 | func test_duration() { 25 | let startTimestamp = Date() 26 | let endTimestamp = startTimestamp.addingTimeInterval(10) 27 | 28 | let startEvent = BlackBox.StartEvent(timestamp: startTimestamp, "Test") 29 | let endEvent = BlackBox.EndEvent(timestamp: endTimestamp, message: "Test", startEvent: startEvent) 30 | 31 | XCTAssertEqual(endEvent.duration, 10) 32 | } 33 | 34 | func test_rawMessage() { 35 | BlackBox.logEnd(event) 36 | XCTAssertEqual(testableLogger.endEvent?.rawMessage.description, "Test".description) 37 | } 38 | 39 | func test_customMessage() throws { 40 | let event = BlackBox.StartEvent("Test") 41 | BlackBox.logEnd(event, message: "Custom Message") 42 | let endEvent = try XCTUnwrap(testableLogger.endEvent) 43 | XCTAssertTrue(endEvent.message.hasPrefix("End: Custom Message")) 44 | } 45 | 46 | func test_userInfo() { 47 | BlackBox.logEnd(event, userInfo: ["name": "Kenobi"]) 48 | XCTAssertEqual(testableLogger.endEvent?.userInfo as? [String: String], ["name": "Kenobi"]) 49 | } 50 | 51 | func test_serviceInfo() { 52 | BlackBox.logEnd(event, serviceInfo: Lightsaber(color: "purple")) 53 | XCTAssertEqual(testableLogger.endEvent?.serviceInfo as? Lightsaber, Lightsaber(color: "purple")) 54 | } 55 | 56 | func test_levelComeFromStartEvent() { 57 | event = BlackBox.StartEvent("Test", level: .warning) 58 | BlackBox.logEnd(event) 59 | XCTAssertEqual(testableLogger.endEvent?.level, .warning) 60 | } 61 | 62 | func test_defaultLevel() { 63 | BlackBox.logEnd(event) 64 | XCTAssertEqual(testableLogger.endEvent?.level, .debug) 65 | } 66 | 67 | func test_category() { 68 | BlackBox.logEnd(event, category: "Analytics") 69 | XCTAssertEqual(testableLogger.endEvent?.category, "Analytics") 70 | } 71 | 72 | func test_parentEvent() { 73 | BlackBox.logEnd(event) 74 | XCTAssertEqual(testableLogger.endEvent?.parentEvent, event) 75 | XCTAssertEqual(testableLogger.endEvent?.startEvent, event) 76 | } 77 | 78 | func test_fileID() { 79 | BlackBox.logEnd(event) 80 | XCTAssertEqual(testableLogger.endEvent?.source.fileID.description, "BlackBoxTests/BlackBoxEndEventTests.swift") 81 | } 82 | 83 | func test_module() { 84 | BlackBox.logEnd(event) 85 | XCTAssertEqual(testableLogger.endEvent?.source.module, "BlackBoxTests") 86 | } 87 | 88 | func test_filename() { 89 | BlackBox.logEnd(event) 90 | XCTAssertEqual(testableLogger.endEvent?.source.filename, "BlackBoxEndEventTests") 91 | } 92 | 93 | func test_function() { 94 | BlackBox.logEnd(event) 95 | XCTAssertEqual(testableLogger.endEvent?.source.function.description, "test_function()") 96 | } 97 | 98 | func test_line() { 99 | BlackBox.logEnd(event) 100 | XCTAssertEqual(testableLogger.endEvent?.source.line, 99) 101 | } 102 | 103 | func test_messageWithDurationFormatted() { 104 | let startTimestamp = Date() 105 | let endTimestamp = startTimestamp.addingTimeInterval(10) 106 | 107 | let startEvent = BlackBox.StartEvent(timestamp: startTimestamp, "Test") 108 | let endEvent = BlackBox.EndEvent(timestamp: endTimestamp, message: "Test", startEvent: startEvent) 109 | 110 | let formatter = MeasurementFormatter() 111 | formatter.locale = Locale(identifier: "en-US") 112 | 113 | XCTAssertEqual("10 sec", endEvent.formattedDuration(using: formatter)) 114 | XCTAssertEqual("End: Test, duration: 10 sec", endEvent.messageWithFormattedDuration(using: formatter)) 115 | } 116 | 117 | func test_isTrace() { 118 | BlackBox.logEnd(event) 119 | XCTAssertEqual(testableLogger.endEvent?.isTrace, true) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/BlackBox/Loggers/FSLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Redirects logs to text file 4 | /// > Warning: Doesn't support filesize limits, use at your own risk. 5 | public class FSLogger: BBLoggerProtocol { 6 | /// Full path to log file 7 | public let fullpath: URL 8 | private let levels: [BBLogLevel] 9 | private let queue: DispatchQueue? 10 | private let logFormat: BBLogFormat 11 | 12 | /// Creates FS logger 13 | /// - Parameters: 14 | /// - path: path to directory where log file will be stored 15 | /// - name: filename 16 | /// - levels: levels to log 17 | /// - queue: queue for logs to be prepared and stored at 18 | @available(*, deprecated, message: "Use throwing init(path:name:levels:queue:logFormat:)") 19 | public init( 20 | path: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!, 21 | name: String = "BlackBox_FSLogger", 22 | levels: [BBLogLevel], 23 | queue: DispatchQueue = DispatchQueue(label: String(describing: FSLogger.self)), 24 | logFormat: BBLogFormat = BBLogFormat() 25 | ) { 26 | self.fullpath = path.appendingPathComponent(name) 27 | self.levels = levels 28 | self.queue = queue 29 | self.logFormat = logFormat 30 | 31 | try? createDirectory(at: path) 32 | } 33 | 34 | /// Creates FS logger 35 | /// - Parameters: 36 | /// - path: path to directory where log file will be stored 37 | /// - name: filename 38 | /// - levels: levels to log 39 | /// - queue: queue for logs to be prepared and stored at 40 | public init( 41 | path: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!, 42 | name: String = "BlackBox_FSLogger", 43 | levels: [BBLogLevel], 44 | queue: DispatchQueue? = DispatchQueue(label: String(describing: FSLogger.self)), 45 | logFormat: BBLogFormat = BBLogFormat() 46 | ) throws { 47 | self.fullpath = path.appendingPathComponent(name) 48 | self.levels = levels 49 | self.queue = queue 50 | self.logFormat = logFormat 51 | 52 | try createDirectory(at: path) 53 | } 54 | 55 | private func createDirectory(at path: URL) throws { 56 | let fileManager = FileManager.default 57 | guard !fileManager.fileExists(atPath: path.path) else { return } 58 | try fileManager.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil) 59 | } 60 | 61 | public func log(_ event: BlackBox.GenericEvent) { 62 | fsLog(event) 63 | } 64 | 65 | public func log(_ event: BlackBox.ErrorEvent) { 66 | fsLog(event) 67 | } 68 | 69 | public func logStart(_ event: BlackBox.StartEvent) { 70 | fsLog(event) 71 | } 72 | 73 | public func logEnd(_ event: BlackBox.EndEvent) { 74 | fsLog(event) 75 | } 76 | } 77 | 78 | extension FSLogger { 79 | 80 | private func fsLog(_ event: BlackBox.GenericEvent) { 81 | guard levels.contains(event.level) else { return } 82 | 83 | let userInfo = event.userInfo?.bbLogDescription(with: logFormat.userInfoFormatOptions) ?? "nil" 84 | 85 | let icon = logFormat.icon(for: event.level) 86 | let timestamp = String(describing: event.timestamp) 87 | let title = [icon, timestamp] 88 | .compactMap { $0 } 89 | .joined(separator: " ") 90 | 91 | let subtitle = event.source.filename + ", " + event.source.function.description 92 | 93 | let content = event.messageWithFormattedDuration(using: logFormat.measurementFormatter) 94 | 95 | let footer = "[User Info]:" + "\n" + userInfo 96 | 97 | let messageToLog = title + "\n" + subtitle + "\n\n" + content + "\n\n" + footer + "\n\n\n" 98 | 99 | log(messageToLog, fullpath: fullpath) 100 | } 101 | 102 | private func log(_ string: String, fullpath: URL) { 103 | let work: @Sendable () -> () = { 104 | if let handle = try? FileHandle(forWritingTo: fullpath) { 105 | defer { handle.closeFile() } 106 | guard let data = string.data(using: .utf8) else { return } 107 | 108 | handle.seekToEndOfFile() 109 | handle.write(data) 110 | } else { 111 | try? string.write(to: fullpath, atomically: true, encoding: .utf8) 112 | } 113 | } 114 | 115 | if let queue { 116 | queue.async { [work] in 117 | work() 118 | } 119 | } else { 120 | work() 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/OSSignpostLoggerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSSignpostLoggerTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 26.10.2022. 6 | // 7 | 8 | @testable import BlackBox 9 | @testable import ExampleModule 10 | import Foundation 11 | import XCTest 12 | import os 13 | 14 | class OSSignpostLoggerTests: BlackBoxTestCase { 15 | var osSignpostLogger: OSSignpostLoggerMock! 16 | override func setUpWithError() throws { 17 | try super.setUpWithError() 18 | 19 | createOSSignpostLogger(levels: .allCases) 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | logger = nil 24 | osSignpostLogger = nil 25 | try super.tearDownWithError() 26 | } 27 | 28 | private func createOSSignpostLogger(levels: [BBLogLevel]) { 29 | osSignpostLogger = .init(levels: levels) 30 | BlackBox.instance = .init(loggers: [osSignpostLogger]) 31 | 32 | logger = osSignpostLogger 33 | } 34 | 35 | func test_startEvent_message() { 36 | let _ = BlackBox.logStart("Hello there") 37 | let expectedResult = "Start: Hello there" 38 | XCTAssertEqual(osSignpostLogger.data?.message, expectedResult) 39 | } 40 | 41 | 42 | func test_startEvent_invalidLevels() { 43 | createOSSignpostLogger(levels: [.error]) 44 | 45 | let logLevels: [BBLogLevel] = [.debug, .info, .warning] 46 | 47 | logLevels.forEach { level in 48 | let _ = BlackBox.logStart("Hello There", level: level) 49 | } 50 | 51 | XCTAssertNil(osSignpostLogger.data) 52 | } 53 | 54 | func test_startEvent_validLevel() { 55 | createOSSignpostLogger(levels: [.error]) 56 | 57 | let _ = BlackBox.logStart("Hello There", level: .error) 58 | XCTAssertNotNil(osSignpostLogger.data) 59 | } 60 | 61 | func test_genericEvent_signpostType_event() { 62 | BlackBox.log("Hello There", level: .debug) 63 | XCTAssertEqual(osSignpostLogger.data?.signpostType, OSSignpostType.event) 64 | } 65 | 66 | func test_errorEvent_signpostType_event() { 67 | enum Error: Swift.Error { 68 | case someError 69 | } 70 | BlackBox.log(Error.someError) 71 | XCTAssertEqual(osSignpostLogger.data?.signpostType, OSSignpostType.event) 72 | } 73 | 74 | func test_startEvent_signpostType_begin() { 75 | let _ = BlackBox.logStart("Process") 76 | XCTAssertEqual(osSignpostLogger.data?.signpostType, OSSignpostType.begin) 77 | } 78 | 79 | func test_endEvent_signpostType_begin() { 80 | BlackBox.logEnd(BlackBox.StartEvent("Process")) 81 | XCTAssertEqual(osSignpostLogger.data?.signpostType, OSSignpostType.end) 82 | } 83 | 84 | func test_startEvent_subsystem() { 85 | let _ = BlackBox.logStart("Hello There") 86 | XCTAssertEqual(osSignpostLogger.data?.subsystem, "BlackBoxTests") 87 | } 88 | 89 | func test_startEvent_categoryProvided() { 90 | let _ = BlackBox.logStart("Hello There", category: "Analytics") 91 | XCTAssertEqual(osSignpostLogger.data?.category, "Analytics") 92 | } 93 | 94 | func test_startEvent_categoryNotProvided() { 95 | let _ = BlackBox.logStart("Hello There") 96 | XCTAssertEqual(osSignpostLogger.data?.category, "OSSignpostLoggerTests") 97 | } 98 | 99 | func test_errorEvent() { 100 | enum Error: Swift.Error { 101 | case someError 102 | } 103 | BlackBox.log(Error.someError) 104 | XCTAssertNotNil(osSignpostLogger.data) 105 | } 106 | 107 | func test_startEvent_endEvent_shareSameId() throws { 108 | let startLog = BlackBox.logStart("Hello There") 109 | let startLogData = try XCTUnwrap(osSignpostLogger.data) 110 | 111 | BlackBox.logEnd(startLog) 112 | let endLogData = try XCTUnwrap(osSignpostLogger.data) 113 | 114 | XCTAssertEqual(startLogData.signpostId.rawValue, endLogData.signpostId.rawValue) 115 | } 116 | 117 | func test_startEvent_endEvent_shareSameSubsystem() throws { 118 | let startLog = BlackBox.logStart("Hello There") 119 | let startLogData = try XCTUnwrap(osSignpostLogger.data) 120 | 121 | ExampleModule.ExampleService().finishLog(startLog) 122 | let endLogData = try XCTUnwrap(osSignpostLogger.data) 123 | 124 | XCTAssertEqual(startLogData.subsystem, endLogData.subsystem) 125 | } 126 | 127 | func test_startEvent_endEvent_shareSameCategory() throws { 128 | let startLog = BlackBox.logStart("Hello There") 129 | let startLogData = try XCTUnwrap(osSignpostLogger.data) 130 | 131 | ExampleModule.ExampleService().finishLog(startLog) 132 | let endLogData = try XCTUnwrap(osSignpostLogger.data) 133 | 134 | XCTAssertEqual(startLogData.category, endLogData.category) 135 | } 136 | 137 | func test_startEvent_endEvent_shareSameName() throws { 138 | let startLog = BlackBox.logStart("Hello There") 139 | let startLogData = try XCTUnwrap(osSignpostLogger.data) 140 | 141 | ExampleModule.ExampleService().finishLog(startLog) 142 | let endLogData = try XCTUnwrap(osSignpostLogger.data) 143 | 144 | XCTAssertEqual(startLogData.name.description, endLogData.name.description) 145 | } 146 | } 147 | 148 | class OSSignpostLoggerMock: OSSignpostLogger { 149 | 150 | var data: LogData? 151 | override func signpostLog(_ data: OSSignpostLogger.LogData) { 152 | self.data = data 153 | super.signpostLog(data) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/BlackBox/Loggers/OSLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import os 3 | 4 | /// Redirects logs to Console.app and to Xcode console 5 | public class OSLogger: BBLoggerProtocol { 6 | let levels: [BBLogLevel] 7 | let logFormat: BBLogFormat 8 | 9 | public init( 10 | levels: [BBLogLevel], 11 | logFormat: BBLogFormat = BBLogFormat() 12 | ){ 13 | self.levels = levels 14 | self.logFormat = logFormat 15 | } 16 | 17 | public func log(_ event: BlackBox.GenericEvent) { 18 | osLog(event: event) 19 | } 20 | 21 | public func log(_ event: BlackBox.ErrorEvent) { 22 | osLog(event: event) 23 | } 24 | 25 | public func logStart(_ event: BlackBox.StartEvent) { 26 | osLog(event: event) 27 | } 28 | 29 | public func logEnd(_ event: BlackBox.EndEvent) { 30 | osLog(event: event) 31 | } 32 | 33 | private func osLog(event: BlackBox.GenericEvent) { 34 | guard levels.contains(event.level) else { return } 35 | 36 | let data = LogData(from: event, logFormat: logFormat) 37 | 38 | osLog(data) 39 | } 40 | 41 | func osLog(_ data: LogData) { 42 | let log = OSLog( 43 | subsystem: data.subsystem, 44 | category: data.category 45 | ) 46 | 47 | os_log( 48 | data.logType, 49 | log: log, 50 | "%{public}@", data.message 51 | ) 52 | } 53 | } 54 | 55 | extension OSLogger { 56 | struct LogData { 57 | let logType: OSLogType 58 | let subsystem: String 59 | let category: String 60 | let message: String 61 | 62 | init( 63 | logType: OSLogType, 64 | subsystem: String, 65 | category: String, 66 | message: String 67 | ) { 68 | self.logType = logType 69 | self.subsystem = subsystem 70 | self.category = category 71 | self.message = message 72 | } 73 | 74 | init(from event: BlackBox.GenericEvent, logFormat: BBLogFormat) { 75 | let subsystem = event.source.module 76 | let category = event.category ?? "" 77 | 78 | self.init( 79 | logType: OSLogType(event.level), 80 | subsystem: subsystem, 81 | category: category, 82 | message: Self.message(from: event, logFormat: logFormat) 83 | ) 84 | } 85 | 86 | private static func message(from event: BlackBox.GenericEvent, logFormat: BBLogFormat) -> String { 87 | func source(from event: BlackBox.GenericEvent) -> String { 88 | let fileWithLine = [event.source.filename, String(event.source.line)].joined(separator: ":") 89 | 90 | return [ 91 | "[Source]", 92 | fileWithLine, 93 | event.source.function.description 94 | ].joined(separator: logFormat.sourceSectionInline ? " " : "\n") 95 | } 96 | 97 | func userInfo(from event: BlackBox.GenericEvent) -> String? { 98 | guard let userInfo = event.userInfo else { return nil } 99 | 100 | return [ 101 | "[User Info]", 102 | userInfo.bbLogDescription(with: logFormat.userInfoFormatOptions) 103 | ].joined(separator: "\n") 104 | } 105 | 106 | let emptyLinePrefix: String? = logFormat.addEmptyLinePrefix ? "\n" : nil 107 | let iconPrefix = logFormat.icon(for: event.level).map { $0 + " " } 108 | 109 | let prefix = [emptyLinePrefix, iconPrefix].compactMap { $0 }.joined(separator: "") 110 | 111 | let message = prefix + event.messageWithFormattedDuration(using: logFormat.measurementFormatter) 112 | 113 | return [ 114 | message, 115 | source(from: event), 116 | userInfo(from: event) 117 | ] 118 | .compactMap { $0 } 119 | .joined(separator: "\n\n") 120 | } 121 | } 122 | } 123 | 124 | extension OSLogType { 125 | init(_ level: BBLogLevel) { 126 | switch level { 127 | case .debug: 128 | self = .default // .debug won't be shown in Console.app, so switching to .default instead 129 | case .info: 130 | self = .info 131 | case .warning: 132 | self = .error 133 | case .error: 134 | self = .fault 135 | } 136 | } 137 | } 138 | 139 | extension BlackBox.GenericEvent { 140 | /// Combines message and formatted duration together 141 | public func messageWithFormattedDuration(using formatter: MeasurementFormatter) -> String { 142 | [ 143 | message, 144 | formattedDuration(using: formatter) 145 | ] 146 | .compactMap { $0 } 147 | .joined(separator: ", duration: ") 148 | } 149 | 150 | /// Formats duration according to formatter rules 151 | public func formattedDuration(using formatter: MeasurementFormatter) -> String? { 152 | guard let endEvent = self as? BlackBox.EndEvent else { return nil } 153 | let duration = endEvent.duration 154 | 155 | let fallback = { return "\(duration) s" } 156 | 157 | if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) { 158 | let measurement = Measurement( 159 | value: duration, 160 | unit: UnitDuration.seconds 161 | ) 162 | return formatter.string(for: measurement) ?? fallback() 163 | } else { 164 | return fallback() 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/OSLoggerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLoggerTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 13.10.2022. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | @testable import BlackBox 11 | import os 12 | 13 | class OSLoggerTests: BlackBoxTestCase { 14 | var osLogger: OSLoggerMock! 15 | override func setUpWithError() throws { 16 | try super.setUpWithError() 17 | 18 | createOSLogger(levels: .allCases) 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | logger = nil 23 | osLogger = nil 24 | try super.tearDownWithError() 25 | } 26 | 27 | private func createOSLogger( 28 | levels: [BBLogLevel], 29 | logFormat: BBLogFormat = .fixedLocale 30 | ) { 31 | osLogger = .init(levels: levels, logFormat: logFormat) 32 | BlackBox.instance = .init(loggers: [osLogger]) 33 | 34 | logger = osLogger 35 | } 36 | 37 | func test_genericEvent_message() { 38 | BlackBox.log("Hello there") 39 | 40 | 41 | let expectedResult = """ 42 | Hello there 43 | 44 | [Source] 45 | OSLoggerTests:38 46 | test_genericEvent_message() 47 | """ 48 | XCTAssertEqual(osLogger.data?.message, expectedResult) 49 | } 50 | 51 | func test_genericEvent_userInfo() { 52 | BlackBox.log("Hello there", userInfo: ["response": "General Kenobi"]) 53 | 54 | let expectedResult = """ 55 | Hello there 56 | 57 | [Source] 58 | OSLoggerTests:52 59 | test_genericEvent_userInfo() 60 | 61 | [User Info] 62 | { 63 | "response" : "General Kenobi" 64 | } 65 | """ 66 | XCTAssertEqual(osLogger.data?.message, expectedResult) 67 | } 68 | 69 | struct Response { 70 | let value: String 71 | } 72 | func test_genericEvent_userInfo_nonCodable() { 73 | BlackBox.log("Hello there", userInfo: ["response": Response(value: "General Kenobi")]) 74 | 75 | let expectedResult = """ 76 | Hello there 77 | 78 | [Source] 79 | OSLoggerTests:73 80 | test_genericEvent_userInfo_nonCodable() 81 | 82 | [User Info] 83 | ["response": BlackBoxTests.OSLoggerTests.Response(value: "General Kenobi")] 84 | """ 85 | XCTAssertEqual(osLogger.data?.message, expectedResult) 86 | } 87 | 88 | func test_genericEvent_invalidLevels() { 89 | createOSLogger(levels: [.error]) 90 | 91 | let logLevels: [BBLogLevel] = [.debug, .info, .warning] 92 | 93 | logLevels.forEach { level in 94 | BlackBox.log("Hello There", level: level) 95 | } 96 | 97 | XCTAssertNil(osLogger.data) 98 | } 99 | 100 | func test_genericEvent_validLevel() { 101 | createOSLogger(levels: [.error]) 102 | 103 | BlackBox.log("Hello There", level: .error) 104 | let expectedResult = """ 105 | Hello There 106 | 107 | [Source] 108 | OSLoggerTests:103 109 | test_genericEvent_validLevel() 110 | """ 111 | XCTAssertEqual(osLogger.data?.message, expectedResult) 112 | } 113 | 114 | func test_genericEvent_level_debugMapsToDefault() { 115 | BlackBox.log("Hello There", level: .debug) 116 | XCTAssertEqual(osLogger.data?.logType.rawValue, OSLogType.default.rawValue) 117 | } 118 | 119 | func test_genericEvent_level_infoMapsToInfo() { 120 | BlackBox.log("Hello There", level: .info) 121 | XCTAssertEqual(osLogger.data?.logType.rawValue, OSLogType.info.rawValue) 122 | } 123 | 124 | func test_genericEvent_level_warningMapsToError() { 125 | BlackBox.log("Hello There", level: .warning) 126 | XCTAssertEqual(osLogger.data?.logType.rawValue, OSLogType.error.rawValue) 127 | } 128 | 129 | func test_genericEvent_level_errorMapsToFault() { 130 | BlackBox.log("Hello There", level: .error) 131 | XCTAssertEqual(osLogger.data?.logType.rawValue, OSLogType.fault.rawValue) 132 | } 133 | 134 | func test_genericEvent_subsystem() { 135 | BlackBox.log("Hello There") 136 | XCTAssertEqual(osLogger.data?.subsystem, "BlackBoxTests") 137 | } 138 | 139 | func test_genericEvent_categoryProvided() { 140 | BlackBox.log("Hello There", category: "Analytics") 141 | XCTAssertEqual(osLogger.data?.category, "Analytics") 142 | } 143 | 144 | func test_genericEvent_categoryNotProvided() { 145 | BlackBox.log("Hello There") 146 | XCTAssertEqual(osLogger.data?.category, "") 147 | } 148 | 149 | enum Error: Swift.Error { 150 | case someError 151 | } 152 | 153 | func test_errorEvent() { 154 | BlackBox.log(Error.someError) 155 | let expectedResult = """ 156 | OSLoggerTests.Error.someError 157 | 158 | [Source] 159 | OSLoggerTests:154 160 | test_errorEvent() 161 | """ 162 | XCTAssertEqual(osLogger.data?.message, expectedResult) 163 | } 164 | 165 | func test_startEvent() { 166 | let _ = BlackBox.logStart("Process") 167 | 168 | let expectedResult = """ 169 | Start: Process 170 | 171 | [Source] 172 | OSLoggerTests:166 173 | test_startEvent() 174 | """ 175 | XCTAssertEqual(osLogger.data?.message, expectedResult) 176 | } 177 | 178 | func test_endEvent() { 179 | let date = Date() 180 | let startEvent = BlackBox.StartEvent( 181 | timestamp: date, 182 | "Process" 183 | ) 184 | 185 | let endEvent = BlackBox.EndEvent( 186 | timestamp: date.addingTimeInterval(1), 187 | startEvent: startEvent 188 | ) 189 | 190 | BlackBox.logEnd(endEvent) 191 | 192 | let expectedResult = """ 193 | End: Process, duration: 1 sec 194 | 195 | [Source] 196 | OSLoggerTests:185 197 | test_endEvent() 198 | """ 199 | XCTAssertEqual(osLogger.data?.message, expectedResult) 200 | } 201 | 202 | func test_endEvent_durationFormat() { 203 | let formatter = MeasurementFormatter() 204 | formatter.locale = Locale(identifier: "es_AR") 205 | formatter.numberFormatter.minimumFractionDigits = 3 206 | createOSLogger(levels: .allCases, logFormat: BBLogFormat(measurementFormatter: formatter)) 207 | let date = Date() 208 | let startEvent = BlackBox.StartEvent( 209 | timestamp: date, 210 | "Process" 211 | ) 212 | 213 | let endEvent = BlackBox.EndEvent( 214 | timestamp: date.addingTimeInterval(1), 215 | startEvent: startEvent 216 | ) 217 | 218 | BlackBox.logEnd(endEvent) 219 | 220 | let expectedResult = """ 221 | End: Process, duration: 1,000 seg. 222 | 223 | [Source] 224 | OSLoggerTests:213 225 | test_endEvent_durationFormat() 226 | """ 227 | XCTAssertEqual(osLogger.data?.message, expectedResult) 228 | } 229 | } 230 | 231 | class OSLoggerMock: OSLogger { 232 | var data: LogData? 233 | override func osLog(_ data: LogData) { 234 | self.data = data 235 | super.osLog(data) 236 | } 237 | } 238 | 239 | 240 | // MARK: - BBLogFormat 241 | extension OSLoggerTests { 242 | func test_whenLogFormatApplied_showingLevelIcon() { 243 | let customLogFormat = BBLogFormat(userInfoFormatOptions: [], levelsWithIcons: [.debug]) 244 | createOSLogger(levels: .allCases, logFormat: customLogFormat) 245 | 246 | BlackBox.log("Hello there") 247 | 248 | let expectedResult = """ 249 | 🛠 Hello there 250 | 251 | [Source] 252 | OSLoggerTests:246 253 | test_whenLogFormatApplied_showingLevelIcon() 254 | """ 255 | XCTAssertEqual(osLogger.data?.message, expectedResult) 256 | 257 | } 258 | 259 | func test_whenLogFormatApplied_outputSourceSectionInline() { 260 | let customLogFormat = BBLogFormat(sourceSectionInline: true) 261 | createOSLogger(levels: .allCases, logFormat: customLogFormat) 262 | 263 | BlackBox.log("Hello there") 264 | 265 | let expectedResult = """ 266 | Hello there 267 | 268 | [Source] OSLoggerTests:263 test_whenLogFormatApplied_outputSourceSectionInline() 269 | """ 270 | XCTAssertEqual(osLogger.data?.message, expectedResult) 271 | 272 | } 273 | 274 | @available(iOS 13.0, tvOS 13.0, watchOS 13.0, *) 275 | func test_whenLogFormatApplied_userInfoFormatted() { 276 | let customLogFormat = BBLogFormat( 277 | userInfoFormatOptions: [ 278 | .prettyPrinted, 279 | .withoutEscapingSlashes 280 | ] 281 | ) 282 | createOSLogger(levels: .allCases, logFormat: customLogFormat) 283 | 284 | BlackBox.log("Hello there", userInfo: ["path": "/api/v1/getData"]) 285 | 286 | let expectedResult = """ 287 | Hello there 288 | 289 | [Source] 290 | OSLoggerTests:284 291 | test_whenLogFormatApplied_userInfoFormatted() 292 | 293 | [User Info] 294 | { 295 | \"path\" : \"/api/v1/getData\" 296 | } 297 | """ 298 | XCTAssertEqual(osLogger.data?.message, expectedResult) 299 | 300 | } 301 | 302 | func test_whenLogFormatWithEmptyLinePrefix_messageHaveEmptyLine() { 303 | let format = BBLogFormat(addEmptyLinePrefix: true) 304 | createOSLogger(levels: .allCases, logFormat: format) 305 | 306 | BlackBox.log("Hello there") 307 | 308 | let expectedResult = """ 309 | 310 | Hello there 311 | 312 | [Source] 313 | OSLoggerTests:306 314 | test_whenLogFormatWithEmptyLinePrefix_messageHaveEmptyLine() 315 | """ 316 | XCTAssertEqual(osLogger.data?.message, expectedResult) 317 | 318 | } 319 | } 320 | 321 | extension OSLoggerTests { 322 | func test_customLevelIcon_deprecated() { 323 | let format = BBLogFormat(levelsWithIcons: [.info]) 324 | createOSLogger(levels: .allCases, logFormat: format) 325 | BBLogIcon.info = "💎" 326 | BlackBox.log("Hello there", level: .info) 327 | 328 | let expectedResult = """ 329 | 💎 Hello there 330 | 331 | [Source] 332 | OSLoggerTests:326 333 | test_customLevelIcon_deprecated() 334 | """ 335 | XCTAssertEqual(osLogger.data?.message, expectedResult) 336 | } 337 | 338 | func test_customLevelIcon() { 339 | let format = BBLogFormat(levelsIcons: BBLogFormat.Icons(info: "💎")) 340 | createOSLogger(levels: .allCases, logFormat: format) 341 | BlackBox.log("Hello there", level: .info) 342 | 343 | let expectedResult = """ 344 | 💎 Hello there 345 | 346 | [Source] 347 | OSLoggerTests:341 348 | test_customLevelIcon() 349 | """ 350 | XCTAssertEqual(osLogger.data?.message, expectedResult) 351 | } 352 | } 353 | 354 | extension BBLogFormat { 355 | static var fixedLocale: BBLogFormat { BBLogFormat(measurementFormatter: .fixedLocale) } 356 | } 357 | 358 | extension MeasurementFormatter { 359 | static var fixedLocale: MeasurementFormatter { 360 | let formatter = MeasurementFormatter() 361 | formatter.locale = Locale(identifier: "en-GB") 362 | return formatter 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /Tests/BlackBoxTests/BackwardsCompatibilityTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PublicApiTests.swift 3 | // 4 | // 5 | // Created by Aleksey Berezka on 21.12.2023. 6 | // 7 | 8 | import Foundation 9 | import XCTest 10 | import BlackBox 11 | 12 | /// 13 | /// Anything that's public should be included in this test. Simple call is enough. 14 | /// 15 | /// If you're marking something already existing as public — add it here. 16 | /// If you're adding something new and it's public — add it here. 17 | /// 18 | /// If you're testing methods — make sure to include all possible arguments 19 | /// For optional argument provide nil, so that it won't become required without notice 20 | /// For collections provide any value that's suitable, so that test will fail if expected type changes 21 | /// 22 | /// If you're testing data — make sure to include all possible constants/variables of a struct/class/etc 23 | /// Check that variables are modifiable — so that test will fail if it's converted to constant 24 | /// 25 | /// If any of this tests won't compile — you've broke backwards compatibility. 26 | /// There are two options: 27 | /// 1. Restore backwards compatibility, so that api call remains the same 28 | /// 2. Go forward, but make sure this changes are released to public in a major release, not minor or patch. 29 | /// 30 | 31 | class PublicApiTests: XCTestCase { 32 | enum Error: Swift.Error, BBLogLevelProvider { 33 | case someError 34 | 35 | var level: BBLogLevel { .debug } 36 | } 37 | 38 | func test_BlackBox() { 39 | let defaultLoggers = BlackBox.defaultLoggers 40 | let instance = BlackBox(loggers: defaultLoggers) 41 | BlackBox.instance = instance 42 | 43 | BlackBox.log( 44 | "Message", 45 | userInfo: nil, 46 | serviceInfo: nil, 47 | level: .debug, 48 | category: nil, 49 | parentEvent: nil, 50 | fileID: #fileID, 51 | function: #function, 52 | line: #line 53 | ) 54 | 55 | BlackBox.log( 56 | Error.someError, 57 | serviceInfo: nil, 58 | category: nil, 59 | parentEvent: nil, 60 | fileID: #fileID, 61 | function: #function, 62 | line: #line 63 | ) 64 | 65 | let startEvent = BlackBox.logStart( 66 | "Message", 67 | userInfo: nil, 68 | serviceInfo: nil, 69 | level: .debug, 70 | category: nil, 71 | parentEvent: nil, 72 | fileID: #fileID, 73 | function: #function, 74 | line: #line 75 | ) 76 | 77 | BlackBox.logStart(startEvent) 78 | 79 | BlackBox.logEnd( 80 | startEvent, 81 | message: nil, 82 | userInfo: nil, 83 | serviceInfo: nil, 84 | category: nil, 85 | fileID: #fileID, 86 | function: #function, 87 | line: #line 88 | ) 89 | 90 | BlackBox.debug("Message") 91 | BlackBox.info("Message") 92 | 93 | let startEventDebug = BlackBox.debugStart("Message") 94 | BlackBox.logEnd(startEventDebug) 95 | 96 | let startEventInfo = BlackBox.infoStart("Message") 97 | BlackBox.logEnd(startEventInfo) 98 | } 99 | 100 | func test_events() { 101 | let source = BlackBox.GenericEvent.Source( 102 | fileID: #fileID, 103 | function: #function, 104 | line: #line 105 | ) 106 | 107 | let _ = source.fileID 108 | let _ = source.module 109 | let _ = source.filename 110 | let _ = source.function 111 | let _ = source.line 112 | 113 | let genericEvent = BlackBox.GenericEvent( 114 | id: UUID(), 115 | timestamp: Date(), 116 | "Message", 117 | userInfo: nil, 118 | serviceInfo: nil, 119 | level: .debug, 120 | category: nil, 121 | parentEvent: nil, 122 | source: source 123 | ) 124 | 125 | let _ = genericEvent.id 126 | let _ = genericEvent.timestamp 127 | let _ = genericEvent.message 128 | let _ = genericEvent.userInfo 129 | let _ = genericEvent.serviceInfo 130 | let _ = genericEvent.level 131 | let _ = genericEvent.category 132 | let _ = genericEvent.parentEvent 133 | let _ = genericEvent.source 134 | let _ = genericEvent.formattedDuration(using: MeasurementFormatter()) 135 | let _ = genericEvent.messageWithFormattedDuration(using: MeasurementFormatter()) 136 | let _ = genericEvent.isTrace 137 | 138 | let _ = BlackBox.GenericEvent( 139 | id: UUID(), 140 | timestamp: Date(), 141 | "Message", 142 | userInfo: nil, 143 | serviceInfo: nil, 144 | level: .debug, 145 | category: nil, 146 | parentEvent: nil, 147 | fileID: #fileID, 148 | function: #function, 149 | line: #line 150 | ) 151 | 152 | let errorEvent = BlackBox.ErrorEvent( 153 | id: UUID(), 154 | timestamp: Date(), 155 | error: Error.someError, 156 | serviceInfo: nil, 157 | category: nil, 158 | parentEvent: nil, 159 | source: source 160 | ) 161 | let _ = errorEvent.error 162 | 163 | let _ = BlackBox.ErrorEvent( 164 | id: UUID(), 165 | timestamp: Date(), 166 | error: Error.someError, 167 | serviceInfo: nil, 168 | category: nil, 169 | parentEvent: nil, 170 | fileID: #fileID, 171 | function: #function, 172 | line: #line 173 | ) 174 | 175 | let startEvent = BlackBox.StartEvent( 176 | id: UUID(), 177 | timestamp: Date(), 178 | "Message", 179 | userInfo: nil, 180 | serviceInfo: nil, 181 | level: .debug, 182 | category: nil, 183 | parentEvent: nil, 184 | source: source 185 | ) 186 | let _ = startEvent.rawMessage 187 | 188 | let _ = BlackBox.StartEvent( 189 | id: UUID(), 190 | timestamp: Date(), 191 | "Message", 192 | userInfo: nil, 193 | serviceInfo: nil, 194 | level: .debug, 195 | category: nil, 196 | parentEvent: nil, 197 | fileID: #fileID, 198 | function: #function, 199 | line: #line 200 | ) 201 | 202 | let endEvent = BlackBox.EndEvent( 203 | id: UUID(), 204 | timestamp: Date(), 205 | message: nil, 206 | startEvent: startEvent, 207 | userInfo: nil, 208 | serviceInfo: nil, 209 | level: .debug, 210 | category: nil, 211 | source: source 212 | ) 213 | let _ = endEvent.rawMessage 214 | let _ = endEvent.startEvent 215 | let _ = endEvent.duration 216 | 217 | let _ = BlackBox.EndEvent( 218 | id: UUID(), 219 | timestamp: Date(), 220 | message: nil, 221 | startEvent: startEvent, 222 | userInfo: nil, 223 | serviceInfo: nil, 224 | level: .debug, 225 | category: nil, 226 | fileID: #fileID, 227 | function: #function, 228 | line: #line 229 | ) 230 | } 231 | 232 | func test_levels() { 233 | let _ = BBLogLevel.debug 234 | let _ = BBLogLevel.info 235 | let _ = BBLogLevel.warning 236 | let _ = BBLogLevel.error 237 | let _ = BBLogLevel.allCases 238 | let _:[BBLogLevel] = .allCases 239 | } 240 | 241 | 242 | 243 | func test_levelsIcons_deprected() { 244 | let _ = BBLogLevel.debug.icon 245 | 246 | BBLogIcon.debug = "❤️" 247 | BBLogIcon.info = "❤️" 248 | BBLogIcon.warning = "❤️" 249 | BBLogIcon.error = "❤️" 250 | } 251 | 252 | func test_levelsIcons() { 253 | // support optional values 254 | _ = BBLogFormat.Icons( 255 | debug: nil, 256 | info: nil, 257 | warning: nil, 258 | error: nil 259 | ) 260 | 261 | // values are strings 262 | let icons = BBLogFormat.Icons( 263 | debug: "❤️", 264 | info: "❤️", 265 | warning: "❤️", 266 | error: "❤️" 267 | ) 268 | 269 | let format = BBLogFormat(levelsIcons: icons) 270 | _ = format.icon(for: .debug) 271 | } 272 | 273 | func test_loggerProtocol() { 274 | struct Logger: BBLoggerProtocol { 275 | func log(_ event: BlackBox.GenericEvent) {} 276 | func log(_ event: BlackBox.ErrorEvent) { } 277 | func logStart(_ event: BlackBox.StartEvent) { } 278 | func logEnd(_ event: BlackBox.EndEvent) { } 279 | } 280 | } 281 | 282 | func test_osLogger() { 283 | let logger = OSLogger( 284 | levels: [.debug], 285 | logFormat: BBLogFormat() 286 | ) 287 | 288 | let genericEvent = BlackBox.GenericEvent("Message") 289 | let errorEvent = BlackBox.ErrorEvent(error: Error.someError) 290 | let startEvent = BlackBox.StartEvent("Message") 291 | let endEvent: BlackBox.EndEvent = BlackBox.EndEvent(startEvent: startEvent) 292 | 293 | logger.log(genericEvent) 294 | logger.log(errorEvent) 295 | logger.logStart(startEvent) 296 | logger.logEnd(endEvent) 297 | } 298 | 299 | func test_osSignpostLogger() { 300 | let logger = OSSignpostLogger(levels: [.debug]) 301 | 302 | let genericEvent = BlackBox.GenericEvent("Message") 303 | let errorEvent = BlackBox.ErrorEvent(error: Error.someError) 304 | let startEvent = BlackBox.StartEvent("Message") 305 | let endEvent: BlackBox.EndEvent = BlackBox.EndEvent(startEvent: startEvent) 306 | 307 | logger.log(genericEvent) 308 | logger.log(errorEvent) 309 | logger.logStart(startEvent) 310 | logger.logEnd(endEvent) 311 | } 312 | 313 | func test_fsLogger() { 314 | let logger = FSLogger( 315 | path: URL(fileURLWithPath: "~/Caches"), 316 | name: "FSLogger", 317 | levels: [.debug], 318 | queue: .global(), 319 | logFormat: BBLogFormat() 320 | ) 321 | 322 | let genericEvent = BlackBox.GenericEvent("Message") 323 | let errorEvent = BlackBox.ErrorEvent(error: Error.someError) 324 | let startEvent = BlackBox.StartEvent("Message") 325 | let endEvent: BlackBox.EndEvent = BlackBox.EndEvent(startEvent: startEvent) 326 | 327 | logger.log(genericEvent) 328 | logger.log(errorEvent) 329 | logger.logStart(startEvent) 330 | logger.logEnd(endEvent) 331 | } 332 | 333 | func test_format() { 334 | let format = BBLogFormat( 335 | userInfoFormatOptions: [.fragmentsAllowed], 336 | sourceSectionInline: false, 337 | levelsWithIcons: [.debug], 338 | measurementFormatter: MeasurementFormatter(), 339 | addEmptyLinePrefix: false 340 | ) 341 | 342 | let _ = format.userInfoFormatOptions 343 | let _ = format.sourceSectionInline 344 | let _ = format.levelsWithIcons 345 | let _ = format.measurementFormatter 346 | let _ = format.addEmptyLinePrefix 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 - present © Dodo Engineering 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sources/BlackBox/BlackBoxEvents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias BBUserInfo = [String: Any] 4 | public typealias BBServiceInfo = Any 5 | 6 | extension BlackBox { 7 | /// Any log event 8 | public class GenericEvent: Equatable { 9 | /// Unique event ID 10 | public let id: UUID 11 | /// Timestamp when event occurred 12 | public let timestamp: Date 13 | /// Event message. May be formatted for some events. 14 | public let message: String 15 | /// Additional info. Place data you'd like to log here. 16 | public let userInfo: BBUserInfo? 17 | /// Place any additional data here. For example, per-event instructions for your custom loggers. 18 | public let serviceInfo: BBServiceInfo? 19 | /// Level of log 20 | public let level: BBLogLevel 21 | /// Category of log. E.g. View Lifecycle. 22 | public let category: String? 23 | /// Parent log of current log. May be useful for traces. 24 | public let parentEvent: GenericEvent? 25 | /// From where log originated 26 | public let source: Source 27 | 28 | public init( 29 | id: UUID = .init(), 30 | timestamp: Date = .init(), 31 | _ message: String, 32 | userInfo: BBUserInfo? = nil, 33 | serviceInfo: BBServiceInfo? = nil, 34 | level: BBLogLevel = .debug, 35 | category: String? = nil, 36 | parentEvent: GenericEvent? = nil, 37 | source: Source 38 | ) { 39 | self.id = id 40 | self.timestamp = timestamp 41 | self.message = message 42 | self.userInfo = userInfo 43 | self.serviceInfo = serviceInfo 44 | self.level = level 45 | self.category = category 46 | self.parentEvent = parentEvent 47 | self.source = source 48 | } 49 | 50 | public convenience init( 51 | id: UUID = .init(), 52 | timestamp: Date = .init(), 53 | _ message: String, 54 | userInfo: BBUserInfo? = nil, 55 | serviceInfo: BBServiceInfo? = nil, 56 | level: BBLogLevel = .debug, 57 | category: String? = nil, 58 | parentEvent: GenericEvent? = nil, 59 | fileID: StaticString = #fileID, 60 | function: StaticString = #function, 61 | line: UInt = #line 62 | ) { 63 | self.init( 64 | id: id, 65 | timestamp: timestamp, 66 | message, 67 | userInfo: userInfo, 68 | serviceInfo: serviceInfo, 69 | level: level, 70 | category: category, 71 | parentEvent: parentEvent, 72 | source: .init( 73 | fileID: fileID, 74 | function: function, 75 | line: line 76 | ) 77 | ) 78 | } 79 | 80 | public static func == (lhs: BlackBox.GenericEvent, rhs: BlackBox.GenericEvent) -> Bool { 81 | lhs.id == rhs.id 82 | } 83 | } 84 | } 85 | 86 | extension BlackBox { 87 | /// Error event 88 | public class ErrorEvent: GenericEvent { 89 | /// Original logged error 90 | public let error: Swift.Error 91 | 92 | public init( 93 | id: UUID = .init(), 94 | timestamp: Date = .init(), 95 | error: Swift.Error, 96 | serviceInfo: BBServiceInfo? = nil, 97 | category: String? = nil, 98 | parentEvent: GenericEvent? = nil, 99 | source: Source 100 | ) { 101 | self.error = error 102 | func domainWithoutModuleName(_ module: String) -> String { 103 | let nsError = error as NSError 104 | return nsError.domain 105 | .deletingPrefix(source.module) 106 | .deletingPrefix(".") 107 | } 108 | 109 | func nameWithoutUserInfo() -> String { 110 | let name = String(describing: error) 111 | guard let split = name.split(separator: "(").first else { return name } 112 | return String(split) 113 | } 114 | 115 | let message = [ 116 | domainWithoutModuleName(source.module), 117 | nameWithoutUserInfo() 118 | ].joined(separator: ".") 119 | 120 | super.init( 121 | id: id, 122 | timestamp: timestamp, 123 | message, 124 | userInfo: (error as? CustomNSError)?.errorUserInfo, 125 | serviceInfo: serviceInfo, 126 | level: error.level, 127 | category: category, 128 | parentEvent: parentEvent, 129 | source: source 130 | ) 131 | } 132 | 133 | public convenience init( 134 | id: UUID = .init(), 135 | timestamp: Date = .init(), 136 | error: Swift.Error, 137 | serviceInfo: BBServiceInfo? = nil, 138 | category: String? = nil, 139 | parentEvent: GenericEvent? = nil, 140 | fileID: StaticString = #fileID, 141 | function: StaticString = #function, 142 | line: UInt = #line 143 | ) { 144 | self.init( 145 | id: id, 146 | timestamp: timestamp, 147 | error: error, 148 | serviceInfo: serviceInfo, 149 | category: category, 150 | parentEvent: parentEvent, 151 | source: .init( 152 | fileID: fileID, 153 | function: function, 154 | line: line 155 | ) 156 | ) 157 | } 158 | } 159 | } 160 | 161 | extension BlackBox { 162 | /// Measurement start event 163 | public class StartEvent: GenericEvent { 164 | /// Original unformatted message 165 | public let rawMessage: StaticString 166 | 167 | public init( 168 | id: UUID = .init(), 169 | timestamp: Date = .init(), 170 | _ message: StaticString, 171 | userInfo: BBUserInfo? = nil, 172 | serviceInfo: BBServiceInfo? = nil, 173 | level: BBLogLevel = .debug, 174 | category: String? = nil, 175 | parentEvent: GenericEvent? = nil, 176 | source: Source 177 | ) { 178 | self.rawMessage = message 179 | super.init( 180 | id: id, 181 | timestamp: timestamp, 182 | "Start: \(message)", 183 | userInfo: userInfo, 184 | serviceInfo: serviceInfo, 185 | level: level, 186 | category: category, 187 | parentEvent: parentEvent, 188 | source: source 189 | ) 190 | } 191 | 192 | public convenience init( 193 | id: UUID = .init(), 194 | timestamp: Date = .init(), 195 | _ message: StaticString, 196 | userInfo: BBUserInfo? = nil, 197 | serviceInfo: BBServiceInfo? = nil, 198 | level: BBLogLevel = .debug, 199 | category: String? = nil, 200 | parentEvent: GenericEvent? = nil, 201 | fileID: StaticString = #fileID, 202 | function: StaticString = #function, 203 | line: UInt = #line 204 | ) { 205 | self.init( 206 | id: id, 207 | timestamp: timestamp, 208 | message, 209 | userInfo: userInfo, 210 | serviceInfo: serviceInfo, 211 | level: level, 212 | category: category, 213 | parentEvent: parentEvent, 214 | source: .init( 215 | fileID: fileID, 216 | function: function, 217 | line: line 218 | ) 219 | ) 220 | } 221 | } 222 | } 223 | 224 | extension BlackBox { 225 | /// Measurement end event 226 | public class EndEvent: GenericEvent { 227 | /// Original unformatted message 228 | public let rawMessage: StaticString 229 | 230 | /// Start event 231 | public let startEvent: StartEvent 232 | 233 | /// Duration between end event and start event 234 | public let duration: TimeInterval 235 | 236 | public init( 237 | id: UUID = .init(), 238 | timestamp: Date = .init(), 239 | message: StaticString? = nil, 240 | startEvent: StartEvent, 241 | userInfo: BBUserInfo? = nil, 242 | serviceInfo: BBServiceInfo? = nil, 243 | level: BBLogLevel = .debug, 244 | category: String? = nil, 245 | source: Source 246 | ) { 247 | self.rawMessage = message ?? startEvent.rawMessage 248 | self.startEvent = startEvent 249 | self.duration = timestamp.timeIntervalSince(startEvent.timestamp) 250 | 251 | let message = "End: \(rawMessage)" 252 | super.init( 253 | id: id, 254 | timestamp: timestamp, 255 | message, 256 | userInfo: userInfo, 257 | serviceInfo: serviceInfo, 258 | level: level, 259 | category: category, 260 | parentEvent: startEvent, 261 | source: source 262 | ) 263 | } 264 | 265 | public convenience init( 266 | id: UUID = .init(), 267 | timestamp: Date = .init(), 268 | message: StaticString? = nil, 269 | startEvent: StartEvent, 270 | userInfo: BBUserInfo? = nil, 271 | serviceInfo: BBServiceInfo? = nil, 272 | level: BBLogLevel = .debug, 273 | category: String? = nil, 274 | fileID: StaticString = #fileID, 275 | function: StaticString = #function, 276 | line: UInt = #line 277 | ) { 278 | self.init( 279 | id: id, 280 | timestamp: timestamp, 281 | message: message, 282 | startEvent: startEvent, 283 | userInfo: userInfo, 284 | serviceInfo: serviceInfo, 285 | level: level, 286 | category: category, 287 | source: .init( 288 | fileID: fileID, 289 | function: function, 290 | line: line 291 | ) 292 | ) 293 | } 294 | } 295 | } 296 | 297 | fileprivate extension String { 298 | var filename: String { 299 | URL(fileURLWithPath: self).deletingPathExtension().lastPathComponent 300 | } 301 | 302 | var module: String { 303 | firstIndex(of: "/").flatMap { String(self[self.startIndex ..< $0]) } ?? "" 304 | } 305 | } 306 | 307 | public extension BlackBox.GenericEvent { 308 | /// Event source 309 | struct Source { 310 | /// Unmodified file id 311 | public let fileID: StaticString 312 | /// Module 313 | public let module: String 314 | /// File name 315 | public let filename: String 316 | /// Function name 317 | public let function: StaticString 318 | /// Number of line 319 | public let line: UInt 320 | 321 | public init( 322 | fileID: StaticString = #fileID, 323 | function: StaticString = #function, 324 | line: UInt = #line 325 | ) { 326 | self.fileID = fileID 327 | self.module = fileID.description.module 328 | self.filename = fileID.description.filename 329 | self.function = function 330 | self.line = line 331 | } 332 | } 333 | } 334 | 335 | extension String { 336 | func deletingPrefix(_ prefix: String) -> String { 337 | guard hasPrefix(prefix) else { return self } 338 | return String(dropFirst(prefix.count)) 339 | } 340 | } 341 | 342 | extension BlackBox.GenericEvent { 343 | public var isTrace: Bool { 344 | self is BlackBox.StartEvent || self is BlackBox.EndEvent 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /Sources/BlackBox/BlackBox.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import DBThreadSafe 3 | 4 | public class BlackBox { 5 | /// Instance that holds loggers 6 | /// 7 | /// Create instance with desired loggers and replace this one 8 | public static var instance: BlackBox { 9 | get { _instance.read() } 10 | set { _instance.write(newValue) } 11 | } 12 | 13 | nonisolated(unsafe) private static var _instance = DBThreadSafeContainer(BlackBox.default) 14 | 15 | private let loggers: [BBLoggerProtocol] 16 | 17 | /// Creates `BlackBox` instance 18 | /// - Parameters: 19 | /// - loggers: Instances to receive logs from `BlackBox` 20 | public init(loggers: [BBLoggerProtocol]) { 21 | self.loggers = loggers 22 | } 23 | } 24 | 25 | // MARK: - Static 26 | extension BlackBox { 27 | /// Logs plain message 28 | /// - Parameters: 29 | /// - message: Message to log 30 | /// - userInfo: Additional info you'd like to see alongside log 31 | /// - serviceInfo: to be deleted 32 | /// - level: level of log 33 | /// - category: Category of log. E.g. View Lifecycle. 34 | /// - parentEvent: Parent log of current log. May be useful for traces. 35 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 36 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 37 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 38 | public static func log( 39 | _ message: StaticString, 40 | userInfo: BBUserInfo? = nil, 41 | serviceInfo: BBServiceInfo? = nil, 42 | level: BBLogLevel = .debug, 43 | category: String? = nil, 44 | parentEvent: GenericEvent? = nil, 45 | fileID: StaticString = #fileID, 46 | function: StaticString = #function, 47 | line: UInt = #line 48 | ) { 49 | BlackBox.instance.log( 50 | message, 51 | userInfo: userInfo, 52 | serviceInfo: serviceInfo, 53 | level: level, 54 | category: category, 55 | parentEvent: parentEvent, 56 | fileID: fileID, 57 | function: function, 58 | line: line 59 | ) 60 | } 61 | 62 | /// Logs error messages 63 | /// - Parameters: 64 | /// - error: Error to log 65 | /// - serviceInfo: to be deleted 66 | /// - category: Category of log. E.g. View Lifecycle. 67 | /// - parentEvent: Parent log of current log. May be useful for traces. 68 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 69 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 70 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 71 | public static func log( 72 | _ error: Swift.Error, 73 | serviceInfo: BBServiceInfo? = nil, 74 | category: String? = nil, 75 | parentEvent: GenericEvent? = nil, 76 | fileID: StaticString = #fileID, 77 | function: StaticString = #function, 78 | line: UInt = #line 79 | ) { 80 | BlackBox.instance.log( 81 | error, 82 | serviceInfo: serviceInfo, 83 | category: category, 84 | parentEvent: parentEvent, 85 | fileID: fileID, 86 | function: function, 87 | line: line 88 | ) 89 | } 90 | 91 | // MARK: - Measurements 92 | /// Logs measurement start 93 | /// - Parameters: 94 | /// - message: Measurement name 95 | /// - userInfo: Additional info you'd like to see alongside log 96 | /// - serviceInfo: to be deleted 97 | /// - level: level of log 98 | /// - category: Category of log. E.g. View Lifecycle. 99 | /// - parentEvent: Parent log of current log. May be useful for traces. 100 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 101 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 102 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 103 | /// - Returns: Started measurement 104 | public static func logStart( 105 | _ message: StaticString, 106 | userInfo: BBUserInfo? = nil, 107 | serviceInfo: BBServiceInfo? = nil, 108 | level: BBLogLevel = .debug, 109 | category: String? = nil, 110 | parentEvent: GenericEvent? = nil, 111 | fileID: StaticString = #fileID, 112 | function: StaticString = #function, 113 | line: UInt = #line 114 | ) -> StartEvent { 115 | BlackBox.instance.logStart( 116 | message, 117 | userInfo: userInfo, 118 | serviceInfo: serviceInfo, 119 | level: level, 120 | category: category, 121 | parentEvent: parentEvent, 122 | fileID: fileID, 123 | function: function, 124 | line: line 125 | ) 126 | } 127 | 128 | /// Logs measurement start 129 | /// - Parameter event: measurement start event 130 | public static func logStart( 131 | _ event: StartEvent 132 | ) { 133 | BlackBox.instance.logStart(event) 134 | } 135 | 136 | /// Logs measurement end 137 | /// - Parameters: 138 | /// - startEvent: Measurement start event 139 | /// - message: Alternate message to log instead of ``StartEvent`` message. 140 | /// - userInfo: Additional info you'd like to see alongside log 141 | /// - serviceInfo: to be deleted 142 | /// - category: Category of log. E.g. View Lifecycle. 143 | /// - parentEvent: Parent log of current log. May be useful for traces. 144 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 145 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 146 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 147 | public static func logEnd( 148 | _ event: StartEvent, 149 | message: StaticString? = nil, 150 | userInfo: BBUserInfo? = nil, 151 | serviceInfo: BBServiceInfo? = nil, 152 | category: String? = nil, 153 | fileID: StaticString = #fileID, 154 | function: StaticString = #function, 155 | line: UInt = #line 156 | ) { 157 | BlackBox.instance.logEnd( 158 | event, 159 | message: message, 160 | userInfo: userInfo, 161 | serviceInfo: serviceInfo, 162 | category: category, 163 | fileID: fileID, 164 | function: function, 165 | line: line 166 | ) 167 | } 168 | 169 | /// Logs measurement end 170 | /// - Parameter event: measurement end event 171 | public static func logEnd( 172 | _ event: EndEvent 173 | ) { 174 | BlackBox.instance.logEnd(event) 175 | } 176 | 177 | // MARK: - Level in function name 178 | 179 | /// Logs debug message 180 | /// - Parameters: 181 | /// - message: Message to log 182 | /// - userInfo: Additional info you'd like to see alongside log 183 | /// - serviceInfo: to be deleted 184 | /// - category: Category of log. E.g. View Lifecycle. 185 | /// - parentEvent: Parent log of current log. May be useful for traces. 186 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 187 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 188 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 189 | public static func debug( 190 | _ message: StaticString, 191 | userInfo: BBUserInfo? = nil, 192 | serviceInfo: BBServiceInfo? = nil, 193 | category: String? = nil, 194 | parentEvent: GenericEvent? = nil, 195 | fileID: StaticString = #fileID, 196 | function: StaticString = #function, 197 | line: UInt = #line 198 | ) { 199 | BlackBox.instance.log( 200 | message, 201 | userInfo: userInfo, 202 | serviceInfo: serviceInfo, 203 | level: .debug, 204 | category: category, 205 | parentEvent: parentEvent, 206 | fileID: fileID, 207 | function: function, 208 | line: line 209 | ) 210 | } 211 | 212 | /// Logs info message 213 | /// - Parameters: 214 | /// - message: Message to log 215 | /// - userInfo: Additional info you'd like to see alongside log 216 | /// - serviceInfo: to be deleted 217 | /// - category: Category of log. E.g. View Lifecycle. 218 | /// - parentEvent: Parent log of current log. May be useful for traces. 219 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 220 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 221 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 222 | public static func info( 223 | _ message: StaticString, 224 | userInfo: BBUserInfo? = nil, 225 | serviceInfo: BBServiceInfo? = nil, 226 | category: String? = nil, 227 | parentEvent: GenericEvent? = nil, 228 | fileID: StaticString = #fileID, 229 | function: StaticString = #function, 230 | line: UInt = #line 231 | ) { 232 | BlackBox.instance.log( 233 | message, 234 | userInfo: userInfo, 235 | serviceInfo: serviceInfo, 236 | level: .info, 237 | category: category, 238 | parentEvent: parentEvent, 239 | fileID: fileID, 240 | function: function, 241 | line: line 242 | ) 243 | } 244 | 245 | /// Logs measurement start with debug level 246 | /// - Parameters: 247 | /// - message: Measurement name 248 | /// - userInfo: Additional info you'd like to see alongside log 249 | /// - serviceInfo: to be deleted 250 | /// - category: Category of log. E.g. View Lifecycle. 251 | /// - parentEvent: Parent log of current log. May be useful for traces. 252 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 253 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 254 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 255 | /// - Returns: Started measurement 256 | public static func debugStart( 257 | _ message: StaticString, 258 | userInfo: BBUserInfo? = nil, 259 | serviceInfo: BBServiceInfo? = nil, 260 | category: String? = nil, 261 | parentEvent: GenericEvent? = nil, 262 | fileID: StaticString = #fileID, 263 | function: StaticString = #function, 264 | line: UInt = #line 265 | ) -> StartEvent { 266 | BlackBox.instance.logStart( 267 | message, 268 | userInfo: userInfo, 269 | serviceInfo: serviceInfo, 270 | level: .debug, 271 | category: category, 272 | parentEvent: parentEvent, 273 | fileID: fileID, 274 | function: function, 275 | line: line 276 | ) 277 | } 278 | 279 | /// Logs measurement start with info level 280 | /// - Parameters: 281 | /// - message: Measurement name 282 | /// - userInfo: Additional info you'd like to see alongside log 283 | /// - serviceInfo: to be deleted 284 | /// - category: Category of log. E.g. View Lifecycle. 285 | /// - parentEvent: Parent log of current log. May be useful for traces. 286 | /// - fileID: The fileID where the logs occurs. Containts module name and filename. The default is the fileID of the function where you call log. 287 | /// - function: The function where the logs occurs. The default is the function name from where you call log. 288 | /// - line: The line where the logs occurs. The default is the line in function from where you call log. 289 | /// - Returns: Started measurement 290 | public static func infoStart( 291 | _ message: StaticString, 292 | userInfo: BBUserInfo? = nil, 293 | serviceInfo: BBServiceInfo? = nil, 294 | category: String? = nil, 295 | parentEvent: GenericEvent? = nil, 296 | fileID: StaticString = #fileID, 297 | function: StaticString = #function, 298 | line: UInt = #line 299 | ) -> StartEvent { 300 | BlackBox.instance.logStart( 301 | message, 302 | userInfo: userInfo, 303 | serviceInfo: serviceInfo, 304 | level: .info, 305 | category: category, 306 | parentEvent: parentEvent, 307 | fileID: fileID, 308 | function: function, 309 | line: line 310 | ) 311 | } 312 | } 313 | 314 | // MARK: - Instance 315 | extension BlackBox { 316 | func log( 317 | _ message: StaticString, 318 | userInfo: BBUserInfo?, 319 | serviceInfo: BBServiceInfo?, 320 | level: BBLogLevel, 321 | category: String?, 322 | parentEvent: GenericEvent?, 323 | fileID: StaticString, 324 | function: StaticString, 325 | line: UInt 326 | ) { 327 | let source = GenericEvent.Source( 328 | fileID: fileID, 329 | function: function, 330 | line: line 331 | ) 332 | let event = BlackBox.GenericEvent( 333 | message.description, 334 | userInfo: userInfo, 335 | serviceInfo: serviceInfo, 336 | level: level, 337 | category: category, 338 | parentEvent: parentEvent, 339 | source: source 340 | ) 341 | 342 | loggers.forEach { $0.log(event) } 343 | } 344 | 345 | func log( 346 | _ error: Error, 347 | serviceInfo: BBServiceInfo?, 348 | category: String?, 349 | parentEvent: GenericEvent?, 350 | fileID: StaticString, 351 | function: StaticString, 352 | line: UInt 353 | ) { 354 | let source = GenericEvent.Source( 355 | fileID: fileID, 356 | function: function, 357 | line: line 358 | ) 359 | let event = BlackBox.ErrorEvent( 360 | error: error, 361 | serviceInfo: serviceInfo, 362 | category: category, 363 | parentEvent: parentEvent, 364 | source: source 365 | ) 366 | 367 | loggers.forEach { $0.log(event) } 368 | } 369 | 370 | func logStart( 371 | _ message: StaticString, 372 | userInfo: BBUserInfo?, 373 | serviceInfo: BBServiceInfo?, 374 | level: BBLogLevel, 375 | category: String?, 376 | parentEvent: GenericEvent?, 377 | fileID: StaticString, 378 | function: StaticString, 379 | line: UInt 380 | ) -> BlackBox.StartEvent { 381 | let source = GenericEvent.Source( 382 | fileID: fileID, 383 | function: function, 384 | line: line 385 | ) 386 | let event = StartEvent( 387 | message, 388 | userInfo: userInfo, 389 | serviceInfo: serviceInfo, 390 | level: level, 391 | category: category, 392 | parentEvent: parentEvent, 393 | source: source 394 | ) 395 | 396 | logStart(event) 397 | 398 | return event 399 | } 400 | 401 | func logStart( 402 | _ event: BlackBox.StartEvent 403 | ) { 404 | loggers.forEach { $0.logStart(event) } 405 | } 406 | 407 | func logEnd( 408 | _ startEvent: BlackBox.StartEvent, 409 | message: StaticString?, 410 | userInfo: BBUserInfo?, 411 | serviceInfo: BBServiceInfo?, 412 | category: String?, 413 | fileID: StaticString, 414 | function: StaticString, 415 | line: UInt 416 | ) { 417 | let source = GenericEvent.Source( 418 | fileID: fileID, 419 | function: function, 420 | line: line 421 | ) 422 | let event = EndEvent( 423 | message: message, 424 | startEvent: startEvent, 425 | userInfo: userInfo, 426 | serviceInfo: serviceInfo, 427 | level: startEvent.level, 428 | category: category, 429 | source: source 430 | ) 431 | 432 | logEnd(event) 433 | } 434 | 435 | func logEnd( 436 | _ event: BlackBox.EndEvent 437 | ) { 438 | loggers.forEach { $0.logEnd(event) } 439 | } 440 | } 441 | 442 | extension BlackBox { 443 | static var `default`: BlackBox { 444 | BlackBox(loggers: BlackBox.defaultLoggers) 445 | } 446 | 447 | public static var defaultLoggers: [BBLoggerProtocol] { 448 | [ 449 | OSLogger(levels: .allCases), 450 | OSSignpostLogger(levels: .allCases) 451 | ] 452 | } 453 | } 454 | --------------------------------------------------------------------------------