├── Documentation ├── logo.png └── overview.png ├── Tests ├── .swiftlint.yml └── PureeTests │ ├── Utilities │ ├── TestingBuffer.swift │ └── InMemoryLogStore.swift │ ├── Info.plist │ ├── LogStore │ └── FileLogStoreTests.swift │ ├── LogEntryTests.swift │ ├── TagPatternTests.swift │ ├── LoggerTests.swift │ └── Output │ └── BufferedOutputTests.swift ├── Puree.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── PureeTests.xcscheme │ │ └── Puree.xcscheme └── project.pbxproj ├── .swiftlint.yml ├── Sources └── Puree │ ├── Utilities │ └── DateProvider.swift │ ├── Puree.h │ ├── LogEntry.swift │ ├── Info.plist │ ├── LogStore │ ├── LogStore.swift │ └── FileLogStore.swift │ ├── Filter.swift │ ├── Output │ ├── Output.swift │ └── BufferedOutput.swift │ ├── TagPattern.swift │ └── Logger.swift ├── Puree.podspec ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── Package.swift ├── .gitignore └── README.md /Documentation/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cookpad/Puree-Swift/HEAD/Documentation/logo.png -------------------------------------------------------------------------------- /Documentation/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cookpad/Puree-Swift/HEAD/Documentation/overview.png -------------------------------------------------------------------------------- /Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_try 3 | - force_cast 4 | - nesting 5 | - identifier_name 6 | -------------------------------------------------------------------------------- /Puree.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests/PureeTests 4 | disabled_rules: 5 | - line_length 6 | - file_length 7 | - non_optional_string_data_conversion 8 | trailing_comma: 9 | mandatory_comma: true 10 | -------------------------------------------------------------------------------- /Sources/Puree/Utilities/DateProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DateProvider { 4 | var now: Date { get } 5 | } 6 | 7 | public struct DefaultDateProvider: DateProvider { 8 | public init() { } 9 | public var now: Date { 10 | return Date() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Puree.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/Puree/Puree.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Puree. 4 | FOUNDATION_EXPORT double PureeVersionNumber; 5 | 6 | //! Project version string for Puree. 7 | FOUNDATION_EXPORT const unsigned char PureeVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /Tests/PureeTests/Utilities/TestingBuffer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Puree 3 | 4 | class TestingBuffer { 5 | private var logs: [String: [LogEntry]] = [:] 6 | 7 | init() { } 8 | 9 | func logs(for key: String) -> [LogEntry] { 10 | return logs[key] ?? [] 11 | } 12 | 13 | func write(_ log: LogEntry, for key: String) { 14 | if logs[key] == nil { 15 | logs[key] = [] 16 | } 17 | logs[key]?.append(log) 18 | } 19 | 20 | func flush() { 21 | logs.removeAll() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Puree.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Puree" 3 | s.version = "5.3.0" 4 | s.summary = "Awesome log aggregator" 5 | s.homepage = "https://github.com/cookpad/Puree-Swift" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.authors = { "Tomohiro Moro" => "tomohiro-moro@cookpad.com", "Kohki Miki" => "koki-miki@cookpad.com", "Vincent Isambart" => "vincent-isambart@cookpad.com" } 8 | s.platform = :ios, "10.0" 9 | s.source = { :git => "https://github.com/cookpad/Puree-Swift.git", :tag => "#{s.version}" } 10 | s.source_files = "Sources/**/*.{h,swift}" 11 | s.swift_version = "5.0.0" 12 | end 13 | -------------------------------------------------------------------------------- /Sources/Puree/LogEntry.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct LogEntry: Codable, Hashable { 4 | public var identifier: UUID = UUID() 5 | public var tag: String 6 | public var date: Date 7 | public var userData: Data? 8 | 9 | public func hash(into hasher: inout Hasher) { 10 | hasher.combine(identifier) 11 | } 12 | 13 | public static func == (lhs: LogEntry, rhs: LogEntry) -> Bool { 14 | return lhs.identifier == rhs.identifier 15 | } 16 | 17 | public init(tag: String, date: Date = Date(), userData: Data? = nil) { 18 | self.tag = tag 19 | self.date = date 20 | self.userData = userData 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PureeTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Puree/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Sources/Puree/LogStore/LogStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol LogStore { 4 | func prepare() throws 5 | func retrieveLogs(of group: String, completion: (Set) -> Void) 6 | func add(_ log: LogEntry, for group: String, completion: (() -> Void)?) 7 | func add(_ logs: Set, for group: String, completion: (() -> Void)?) 8 | func remove(_ log: LogEntry, from group: String, completion: (() -> Void)?) 9 | func remove(_ logs: Set, from group: String, completion: (() -> Void)?) 10 | func flush() 11 | } 12 | 13 | public extension LogStore { 14 | func add(_ log: LogEntry, for group: String, completion: (() -> Void)?) { 15 | add([log], for: group, completion: completion) 16 | } 17 | 18 | func remove(_ log: LogEntry, from group: String, completion: (() -> Void)?) { 19 | remove([log], from: group, completion: completion) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Puree 2 | on: [push, pull_request] 3 | jobs: 4 | iOS: 5 | runs-on: macos-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Run Tests with Xcode 9 | run: | 10 | DESTINATION="platform=iOS Simulator,name=iPhone 8" SCHEME="Puree" 11 | xcodebuild test -project Puree.xcodeproj -scheme "${SCHEME}" -destination "${DESTINATION}" -configuration Debug CODE_SIGNING_ALLOWED=NO 12 | SwiftPM: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run Tests with Swift Package Manager 17 | run: swift test 18 | SwiftLint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: SwiftLint 23 | uses: norio-nomura/action-swiftlint@3.2.1 24 | with: 25 | args: --strict 26 | CocoaPodsLint: 27 | runs-on: macos-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Install Dependencies 31 | run: | 32 | gem install cocoapods 33 | pod repo update 34 | - name: CocoaPods 35 | run: pod lib lint --allow-warnings 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cookpad Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/PureeTests/Utilities/InMemoryLogStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Puree 3 | 4 | class InMemoryLogStore: LogStore { 5 | private var buffer: [String: Set] = [:] 6 | 7 | func prepare() throws { 8 | } 9 | 10 | func logs(for group: String) -> Set { 11 | if let logs = buffer[group] { 12 | return logs 13 | } 14 | return [] 15 | } 16 | 17 | func add(_ logs: Set, for group: String, completion: (() -> Void)?) { 18 | if buffer[group] == nil { 19 | buffer[group] = Set() 20 | } 21 | buffer[group]?.formUnion(logs) 22 | completion?() 23 | } 24 | 25 | func remove(_ logs: Set, from group: String, completion: (() -> Void)?) { 26 | buffer[group]?.subtract(logs) 27 | completion?() 28 | } 29 | 30 | func retrieveLogs(of group: String, completion: (Set) -> Void) { 31 | if let logs = buffer[group] { 32 | return completion(logs) 33 | } 34 | completion([]) 35 | } 36 | 37 | func flush() { 38 | buffer.removeAll() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Puree", 8 | platforms: [ 9 | .iOS(.v10), 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "Puree", 15 | targets: ["Puree"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "Puree", 26 | dependencies: []), 27 | .testTarget( 28 | name: "PureeTests", 29 | dependencies: ["Puree"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/Puree/Filter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol FilterSettingProtocol { 4 | func makeFilter() throws -> Filter 5 | } 6 | 7 | public struct FilterSetting: FilterSettingProtocol { 8 | 9 | public init(makeFiilter: @escaping () -> Filter) { 10 | self.makeFilterBlock = makeFiilter 11 | } 12 | 13 | public init(_ filter: F.Type, tagPattern: TagPattern, options: FilterOptions? = nil) { 14 | makeFilterBlock = { 15 | return F(tagPattern: tagPattern, options: options) 16 | } 17 | } 18 | 19 | @available(*, unavailable, message: "Please conform InstantiatableFilter or use init with closure.") 20 | public init(_ filter: F.Type, tagPattern: TagPattern, options: FilterOptions? = nil) { 21 | fatalError("unavailable") 22 | } 23 | 24 | public func makeFilter() throws -> Filter { 25 | return makeFilterBlock() 26 | } 27 | 28 | private let makeFilterBlock: () -> Filter 29 | } 30 | 31 | public protocol Filter { 32 | var tagPattern: TagPattern { get } 33 | 34 | func convertToLogs(_ payload: [String: Any]?, tag: String, captured: String?, logger: Logger) -> Set 35 | } 36 | 37 | public typealias FilterOptions = [String: Any] 38 | 39 | public protocol InstantiatableFilter: Filter { 40 | init(tagPattern: TagPattern, options: FilterOptions?) 41 | } 42 | -------------------------------------------------------------------------------- /Tests/PureeTests/LogStore/FileLogStoreTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import Puree 4 | 5 | class FileLogStoreTests: XCTestCase { 6 | var logStore: LogStore { 7 | return FileLogStore.default 8 | } 9 | 10 | override func setUp() { 11 | super.setUp() 12 | 13 | try! logStore.prepare() 14 | logStore.flush() 15 | } 16 | 17 | override func tearDown() { 18 | super.tearDown() 19 | logStore.flush() 20 | } 21 | 22 | func makeLog() -> LogEntry { 23 | return LogEntry(tag: "tag", date: Date()) 24 | } 25 | 26 | func testAddAndRemoveLogs() { 27 | var callbackIsCalled = false 28 | let log = makeLog() 29 | logStore.add(log, for: "foo") { 30 | callbackIsCalled = true 31 | } 32 | 33 | XCTAssertTrue(callbackIsCalled) 34 | 35 | logStore.retrieveLogs(of: "foo") { logs in 36 | XCTAssertEqual(logs.count, 1) 37 | } 38 | logStore.retrieveLogs(of: "bar") { logs in 39 | XCTAssertTrue(logs.isEmpty) 40 | } 41 | 42 | let anotherLog = makeLog() 43 | logStore.add(anotherLog, for: "foo", completion: nil) 44 | 45 | logStore.remove(log, from: "bar") { 46 | self.logStore.retrieveLogs(of: "foo") { logs in 47 | XCTAssertEqual(logs.count, 2) 48 | } 49 | } 50 | logStore.remove(log, from: "foo") { 51 | self.logStore.retrieveLogs(of: "foo") { logs in 52 | XCTAssertEqual(logs.count, 1) 53 | XCTAssertEqual(logs.first!, anotherLog) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Puree/Output/Output.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol OutputSettingProtocol { 4 | func makeOutput(_ logStore: LogStore) throws -> Output 5 | } 6 | 7 | public struct OutputSetting: OutputSettingProtocol { 8 | 9 | public init(makeOutput: @escaping (_ logStore: LogStore) -> Output) { 10 | self.makeOutputBlock = makeOutput 11 | } 12 | 13 | public init(_ output: O.Type, tagPattern: TagPattern, options: OutputOptions? = nil) { 14 | makeOutputBlock = { logStore in 15 | return O(logStore: logStore, tagPattern: tagPattern, options: options) 16 | } 17 | } 18 | 19 | @available(*, unavailable, message: "Please conform InstantiatableOutput or use init with closure.") 20 | public init(_ output: O.Type, tagPattern: TagPattern, options: OutputOptions? = nil) { 21 | fatalError("unavailable") 22 | } 23 | 24 | public func makeOutput(_ logStore: LogStore) -> Output { 25 | return makeOutputBlock(logStore) 26 | } 27 | 28 | private let makeOutputBlock: (_ logStore: LogStore) -> Output 29 | } 30 | 31 | public protocol Output { 32 | var tagPattern: TagPattern { get } 33 | 34 | func start() 35 | func resume() 36 | func suspend() 37 | func emit(log: LogEntry) 38 | 39 | } 40 | 41 | public typealias OutputOptions = [String: Any] 42 | 43 | public protocol InstantiatableOutput: Output { 44 | init(logStore: LogStore, tagPattern: TagPattern, options: OutputOptions?) 45 | } 46 | 47 | public extension Output { 48 | func start() { 49 | } 50 | 51 | func resume() { 52 | } 53 | 54 | func suspend() { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/8edb8a95c4c4b3dce71a378aaaf89275510b9cef/Swift.gitignore 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /Tests/PureeTests/LogEntryTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import Puree 4 | 5 | class LogEntryTests: XCTestCase { 6 | private func decode(_ data: Data) -> T { 7 | return try! JSONDecoder().decode(T.self, from: data) 8 | } 9 | 10 | private func encode(_ object: T) -> Data { 11 | let encoder = JSONEncoder() 12 | let data = try! encoder.encode(object) 13 | return data 14 | } 15 | 16 | func testDumpUserInfo() { 17 | var testLog = LogEntry(tag: "tag", date: Date()) 18 | let userInfo: [String: Any] = [ 19 | "key0": "hello", 20 | "key1": 20, 21 | "key2": ["a", "b"], 22 | "key3": true, 23 | "key4": ["spam": "ham"], 24 | ] 25 | testLog.userData = try! JSONSerialization.data(withJSONObject: userInfo, options: []) 26 | 27 | let encodedData = encode(testLog) 28 | let decodedLog: LogEntry = decode(encodedData) 29 | 30 | XCTAssertEqual(decodedLog.identifier, testLog.identifier) 31 | XCTAssertEqual(decodedLog.tag, "tag") 32 | XCTAssertEqual(decodedLog.date, testLog.date) 33 | 34 | guard let userData = decodedLog.userData, 35 | let object = try? JSONSerialization.jsonObject(with: userData, options: []), 36 | let decodedUserInfo = object as? [String: Any] else { 37 | return XCTFail("userInfo should be encoded") 38 | } 39 | 40 | XCTAssertEqual(decodedUserInfo.count, 5) 41 | XCTAssertEqual(decodedUserInfo["key0"] as! String, "hello") 42 | XCTAssertEqual(decodedUserInfo["key1"] as! Int, 20) 43 | XCTAssertEqual(decodedUserInfo["key2"] as! [String], ["a", "b"]) 44 | XCTAssertEqual(decodedUserInfo["key3"] as! Bool, true) 45 | XCTAssertEqual(decodedUserInfo["key4"] as! [String: String], ["spam": "ham"]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Puree.xcodeproj/xcshareddata/xcschemes/PureeTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Tests/PureeTests/TagPatternTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import Puree 4 | 5 | class TagPatternTests: XCTestCase { 6 | func assertMatched(_ match: TagPattern.Match?, capturedString expectedCapturedString: String?) { 7 | if let match = match { 8 | if match.captured != expectedCapturedString { 9 | XCTFail("capturedString should be \(String(describing: expectedCapturedString)) but actually is \(String(describing: match.captured))") 10 | } 11 | } else { 12 | XCTFail("should have matched") 13 | } 14 | } 15 | 16 | func assertNotMatched(_ match: TagPattern.Match?) { 17 | if match != nil { 18 | XCTFail("should not be matched") 19 | } 20 | } 21 | 22 | func testMatches() { 23 | assertMatched(TagPattern(string: "aaa")!.match(in: "aaa"), capturedString: nil) 24 | assertNotMatched(TagPattern(string: "bbb")!.match(in: "aaa")) 25 | assertMatched(TagPattern(string: "*")!.match(in: "aaa"), capturedString: "aaa") 26 | assertMatched(TagPattern(string: "*")!.match(in: "bbb"), capturedString: "bbb") 27 | assertNotMatched(TagPattern(string: "*")!.match(in: "aaa.bbb")) 28 | assertMatched(TagPattern(string: "aaa.bbb")!.match(in: "aaa.bbb"), capturedString: nil) 29 | assertMatched(TagPattern(string: "aaa.*")!.match(in: "aaa.bbb"), capturedString: "bbb") 30 | assertMatched(TagPattern(string: "aaa.*")!.match(in: "aaa.ccc"), capturedString: "ccc") 31 | assertNotMatched(TagPattern(string: "aaa.*")!.match(in: "aaa.bbb.ccc")) 32 | assertNotMatched(TagPattern(string: "aaa.*.ccc")!.match(in: "aaa.bbb.ccc")) 33 | assertNotMatched(TagPattern(string: "aaa.*.ccc")!.match(in: "aaa.ccc.ddd")) 34 | assertMatched(TagPattern(string: "a.**")!.match(in: "a"), capturedString: "") 35 | assertMatched(TagPattern(string: "a.**")!.match(in: "a.b"), capturedString: "b") 36 | assertMatched(TagPattern(string: "a.**")!.match(in: "a.b.c"), capturedString: "b.c") 37 | assertNotMatched(TagPattern(string: "a.**")!.match(in: "b.c")) 38 | } 39 | 40 | func testInvalidPatterns() { 41 | XCTAssertNil(TagPattern(string: "**.**")) 42 | XCTAssertNil(TagPattern(string: "**.*")) 43 | XCTAssertNil(TagPattern(string: "*.b.*")) 44 | XCTAssertNil(TagPattern(string: "a.**.**")) 45 | XCTAssertNil(TagPattern(string: "a..b.c")) 46 | XCTAssertNil(TagPattern(string: "")) 47 | XCTAssertNil(TagPattern(string: "a. .c")) 48 | XCTAssertNil(TagPattern(string: "a.\n.c")) 49 | XCTAssertNil(TagPattern(string: "a.b.\n ")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Puree/TagPattern.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let separator: Character = "." 4 | private let allWildcard = "**" 5 | private let wildcard = "*" 6 | 7 | public struct TagPattern: CustomStringConvertible { 8 | struct Match { 9 | let captured: String? 10 | } 11 | 12 | public let pattern: String 13 | 14 | public var description: String { 15 | return pattern 16 | } 17 | 18 | public init?(string patternString: String) { 19 | if TagPattern.isValidPattern(patternString) { 20 | pattern = patternString 21 | } else { 22 | return nil 23 | } 24 | } 25 | 26 | private static func isValidPattern(_ pattern: String) -> Bool { 27 | let patternElements = pattern.split(separator: separator, omittingEmptySubsequences: false) 28 | let isValidWildcardCount = patternElements.filter { $0 == allWildcard || $0 == wildcard }.count <= 1 29 | let isContainsInvalidElement = patternElements.contains { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } 30 | return isValidWildcardCount && !isContainsInvalidElement 31 | } 32 | 33 | func match(in tag: String) -> Match? { 34 | if tag == pattern { 35 | return Match(captured: nil) 36 | } 37 | 38 | let patternElements = pattern.split(separator: separator) 39 | let tagElements = tag.split(separator: separator) 40 | guard let lastPatternElement = patternElements.last, let lastTagElement = tagElements.last else { 41 | return nil 42 | } 43 | 44 | func matched(patternElements: [String.SubSequence], tagElements: [String.SubSequence]) -> Bool { 45 | for (index, (pattern, tag)) in zip(patternElements, tagElements).enumerated() { 46 | if index == patternElements.count - 1 { 47 | return true 48 | } 49 | if pattern != tag { 50 | return false 51 | } 52 | } 53 | return true 54 | } 55 | 56 | if lastPatternElement == allWildcard { 57 | if matched(patternElements: patternElements, tagElements: tagElements) { 58 | let location = patternElements.count - 1 59 | let capturedLength = tagElements.count - location 60 | let capturedString: String 61 | if capturedLength > 0 { 62 | capturedString = tagElements[location..<(location + capturedLength)].joined(separator: String(separator)) 63 | } else { 64 | capturedString = "" 65 | } 66 | return Match(captured: capturedString) 67 | } 68 | } else if lastPatternElement == wildcard && tagElements.count == patternElements.count { 69 | if !matched(patternElements: patternElements, tagElements: tagElements) { 70 | return nil 71 | } 72 | return Match(captured: String(lastTagElement)) 73 | } 74 | return nil 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Puree/Logger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class Logger { 4 | public struct Configuration { 5 | public var logStore: LogStore 6 | public var dateProvider: DateProvider = DefaultDateProvider() 7 | public var filterSettings: [FilterSettingProtocol] = [] 8 | public var outputSettings: [OutputSettingProtocol] = [] 9 | 10 | public init(logStore: LogStore = FileLogStore.default, 11 | dateProvider: DateProvider = DefaultDateProvider(), 12 | filterSettings: [FilterSettingProtocol], 13 | outputSettings: [OutputSettingProtocol]) { 14 | self.logStore = logStore 15 | self.dateProvider = dateProvider 16 | self.filterSettings = filterSettings 17 | self.outputSettings = outputSettings 18 | } 19 | } 20 | 21 | private let configuration: Configuration 22 | private let dispatchQueue = DispatchQueue(label: "com.cookpad.Puree.Logger", qos: .background) 23 | private(set) var filters: [Filter] = [] 24 | private(set) var outputs: [Output] = [] 25 | 26 | public var currentDate: Date { 27 | return configuration.dateProvider.now 28 | } 29 | 30 | public init(configuration: Configuration) throws { 31 | self.configuration = configuration 32 | 33 | try configuration.logStore.prepare() 34 | try configureFilterPlugins() 35 | try configureOutputPlugins() 36 | 37 | start() 38 | } 39 | 40 | private func configureFilterPlugins() throws { 41 | filters = try configuration.filterSettings.map { try $0.makeFilter() } 42 | } 43 | 44 | private func configureOutputPlugins() throws { 45 | outputs = try configuration.outputSettings.map { try $0.makeOutput(configuration.logStore) } 46 | } 47 | 48 | public func postLog(_ payload: [String: Any]?, tag: String) { 49 | dispatchQueue.async { 50 | func matchesOutputs(with tag: String) -> [Output] { 51 | return self.outputs.filter { $0.tagPattern.match(in: tag) != nil } 52 | } 53 | 54 | for log in self.filteredLogs(with: payload, tag: tag) { 55 | for output in matchesOutputs(with: tag) { 56 | output.emit(log: log) 57 | } 58 | } 59 | } 60 | } 61 | 62 | private func filteredLogs(with payload: [String: Any]?, tag: String) -> [LogEntry] { 63 | var logs: [LogEntry] = [] 64 | for filter in filters { 65 | let match = filter.tagPattern.match(in: tag) 66 | if let match = match { 67 | let filteredLogs = filter.convertToLogs(payload, tag: tag, captured: match.captured, logger: self) 68 | logs.append(contentsOf: filteredLogs) 69 | } else { 70 | continue 71 | } 72 | } 73 | return logs 74 | } 75 | 76 | private func start() { 77 | dispatchQueue.async { 78 | self.outputs.forEach { $0.start() } 79 | } 80 | } 81 | 82 | public func sendBufferedLogs() { 83 | dispatchQueue.sync { 84 | outputs.forEach { ($0 as? BufferedOutput)?.sendBufferedLogs() } 85 | } 86 | } 87 | 88 | public func suspend() { 89 | dispatchQueue.sync { 90 | outputs.forEach { $0.suspend() } 91 | } 92 | } 93 | 94 | public func resume() { 95 | dispatchQueue.async { 96 | self.outputs.forEach { $0.resume() } 97 | } 98 | } 99 | 100 | public func shutdown() { 101 | dispatchQueue.sync { 102 | filters.removeAll() 103 | outputs.forEach { $0.suspend() } 104 | outputs.removeAll() 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Puree.xcodeproj/xcshareddata/xcschemes/Puree.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Sources/Puree/LogStore/FileLogStore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol FileManagerProtocol { 4 | func load(from path: URL) -> Data? 5 | func write(_ data: Data, to path: URL) throws 6 | func remove(at path: URL) throws 7 | func removeDirectory(at path: URL) throws 8 | func createEmptyDirectoryIfNeeded(at path: URL) throws 9 | func cachesDirectoryURL() throws -> URL 10 | } 11 | 12 | struct SystemFileManager: FileManagerProtocol { 13 | private var fileManager: FileManager { 14 | return FileManager.default 15 | } 16 | 17 | public func load(from path: URL) -> Data? { 18 | return fileManager.contents(atPath: path.path) 19 | } 20 | 21 | public func write(_ data: Data, to path: URL) throws { 22 | try data.write(to: path) 23 | } 24 | 25 | public func remove(at path: URL) throws { 26 | try fileManager.removeItem(atPath: path.path) 27 | } 28 | 29 | public func removeDirectory(at path: URL) throws { 30 | if isExistsDirectory(at: path) { 31 | try fileManager.removeItem(at: path) 32 | } 33 | } 34 | 35 | public func isExistsDirectory(at path: URL) -> Bool { 36 | var isDirectory: ObjCBool = false 37 | return fileManager.fileExists(atPath: path.path, isDirectory: &isDirectory) 38 | } 39 | 40 | public func createEmptyDirectoryIfNeeded(at path: URL) throws { 41 | if !isExistsDirectory(at: path) { 42 | try fileManager.createDirectory(atPath: path.path, withIntermediateDirectories: false, attributes: nil) 43 | } 44 | } 45 | 46 | public func cachesDirectoryURL() throws -> URL { 47 | return try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) 48 | } 49 | } 50 | 51 | public class FileLogStore: LogStore { 52 | private static let directoryName = "PureeLogs" 53 | private var bundle: Bundle = Bundle.main 54 | private var baseDirectoryURL: URL? 55 | 56 | public enum Error: Swift.Error { 57 | case baseDirectoryUnavailable 58 | } 59 | 60 | public static let `default` = FileLogStore() 61 | 62 | private func fileURL(for group: String) -> URL? { 63 | // Tag patterns usually contain '*'. However we don't want to use special characters in filenames 64 | // so encode file names to Base16 65 | return baseDirectoryURL?.appendingPathComponent(encodeToBase16(group)) 66 | } 67 | private var fileManager: FileManagerProtocol = SystemFileManager() 68 | 69 | private func storedLogs(of group: String) -> Set { 70 | if let fileURL = fileURL(for: group), let data = fileManager.load(from: fileURL) { 71 | let decorder = PropertyListDecoder() 72 | if let logs = try? decorder.decode([LogEntry].self, from: data) { 73 | return Set(logs) 74 | } 75 | } 76 | return [] 77 | } 78 | 79 | private func write(_ logs: Set, for group: String) { 80 | let encoder = PropertyListEncoder() 81 | if let fileURL = fileURL(for: group), let data = try? encoder.encode(logs) { 82 | try? fileManager.write(data, to: fileURL) 83 | } 84 | } 85 | 86 | private func createCachesDirectory() throws { 87 | guard let baseDirectoryURL = self.baseDirectoryURL else { throw Error.baseDirectoryUnavailable } 88 | try fileManager.createEmptyDirectoryIfNeeded(at: baseDirectoryURL) 89 | } 90 | 91 | public func prepare() throws { 92 | let cacheDirectoryURL = try fileManager.cachesDirectoryURL() 93 | baseDirectoryURL = cacheDirectoryURL.appendingPathComponent(FileLogStore.directoryName) 94 | try createCachesDirectory() 95 | } 96 | 97 | public func add(_ logs: Set, for group: String, completion: (() -> Void)?) { 98 | let unioned = storedLogs(of: group).union(logs) 99 | write(unioned, for: group) 100 | completion?() 101 | } 102 | 103 | public func remove(_ logs: Set, from group: String, completion: (() -> Void)?) { 104 | let subtracted = storedLogs(of: group).subtracting(logs) 105 | write(subtracted, for: group) 106 | completion?() 107 | } 108 | 109 | public func retrieveLogs(of group: String, completion: (Set) -> Void) { 110 | let logs = storedLogs(of: group) 111 | completion(logs) 112 | } 113 | 114 | public func flush() { 115 | if let baseDirectoryURL = baseDirectoryURL { 116 | try? fileManager.removeDirectory(at: baseDirectoryURL) 117 | } 118 | try? createCachesDirectory() 119 | } 120 | 121 | private func encodeToBase16(_ string: String) -> String { 122 | return string.data(using: .utf8)!.map { String(format: "%02hhx", $0) }.joined() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Puree/Output/BufferedOutput.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class BufferedOutput: InstantiatableOutput { 4 | private let dateProvider: DateProvider = DefaultDateProvider() 5 | internal let readWriteQueue = DispatchQueue(label: "com.cookpad.Puree.Logger.BufferedOutput", qos: .background) 6 | 7 | required public init(logStore: LogStore, tagPattern: TagPattern, options: OutputOptions? = nil) { 8 | self.logStore = logStore 9 | self.tagPattern = tagPattern 10 | } 11 | 12 | public struct Chunk: Hashable { 13 | public let logs: Set 14 | private(set) var retryCount: Int = 0 15 | 16 | fileprivate init(logs: Set) { 17 | self.logs = logs 18 | } 19 | 20 | fileprivate mutating func incrementRetryCount() { 21 | retryCount += 1 22 | } 23 | 24 | public static func == (lhs: Chunk, rhs: Chunk) -> Bool { 25 | return lhs.logs == rhs.logs 26 | } 27 | 28 | public func hash(into hasher: inout Hasher) { 29 | hasher.combine(logs) 30 | } 31 | } 32 | public struct Configuration { 33 | public var logEntryCountLimit: Int 34 | public var flushInterval: TimeInterval 35 | public var retryLimit: Int 36 | public var chunkDataSizeLimit: Int? 37 | 38 | public static let `default` = Configuration(logEntryCountLimit: 5, flushInterval: 10, retryLimit: 3, chunkDataSizeLimit: nil) 39 | } 40 | 41 | public let tagPattern: TagPattern 42 | private let logStore: LogStore 43 | public var configuration: Configuration = .default 44 | 45 | private var buffer: Set = [] 46 | private var currentWritingChunks: Set = [] 47 | private var timer: Timer? 48 | private var lastFlushDate: Date? 49 | private var logLimit: Int { 50 | return configuration.logEntryCountLimit 51 | } 52 | private var flushInterval: TimeInterval { 53 | return configuration.flushInterval 54 | } 55 | 56 | private var retryLimit: Int { 57 | return configuration.retryLimit 58 | } 59 | 60 | private var sizeLimit: Int? { 61 | return configuration.chunkDataSizeLimit 62 | } 63 | 64 | private var currentDate: Date { 65 | return dateProvider.now 66 | } 67 | 68 | deinit { 69 | timer?.invalidate() 70 | } 71 | 72 | public func start() { 73 | reloadLogStore() 74 | sendBufferedLogs() 75 | setUpTimer() 76 | } 77 | 78 | public func resume() { 79 | reloadLogStore() 80 | sendBufferedLogs() 81 | setUpTimer() 82 | } 83 | 84 | public func suspend() { 85 | timer?.invalidate() 86 | } 87 | 88 | public func emit(log: LogEntry) { 89 | readWriteQueue.sync { 90 | if let logSizeLimit = configuration.chunkDataSizeLimit, (log.userData?.count ?? 0) > logSizeLimit { 91 | // Data whose size is larger than limit will never be sent. 92 | return 93 | } 94 | 95 | buffer.insert(log) 96 | logStore.add(log, for: storageGroup, completion: nil) 97 | 98 | if buffer.count >= logLimit { 99 | writeBufferedLogs() 100 | } else if let logSizeLimit = configuration.chunkDataSizeLimit { 101 | let currentBufferedLogSize = buffer.reduce(0, { (size, log) -> Int in 102 | size + (log.userData?.count ?? 0) 103 | }) 104 | 105 | if currentBufferedLogSize >= logSizeLimit { 106 | writeBufferedLogs() 107 | } 108 | } 109 | } 110 | } 111 | 112 | public func sendBufferedLogs() { 113 | readWriteQueue.sync { 114 | writeBufferedLogs() 115 | } 116 | } 117 | 118 | open func write(_ chunk: Chunk, completion: @escaping (Bool) -> Void) { 119 | completion(false) 120 | } 121 | 122 | open var storageGroup: String { 123 | let typeName = String(describing: type(of: self)) 124 | return "\(tagPattern.pattern)_\(typeName)" 125 | } 126 | 127 | private func setUpTimer() { 128 | self.timer?.invalidate() 129 | 130 | let timer = Timer(timeInterval: 1.0, 131 | target: self, 132 | selector: #selector(tick(_:)), 133 | userInfo: nil, 134 | repeats: true) 135 | RunLoop.main.add(timer, forMode: .common) 136 | self.timer = timer 137 | } 138 | 139 | @objc private func tick(_ timer: Timer) { 140 | if let lastFlushDate = lastFlushDate { 141 | if currentDate.timeIntervalSince(lastFlushDate) > flushInterval { 142 | readWriteQueue.async { 143 | self.writeBufferedLogs() 144 | } 145 | } 146 | } else { 147 | readWriteQueue.async { 148 | self.writeBufferedLogs() 149 | } 150 | } 151 | } 152 | 153 | private func reloadLogStore() { 154 | readWriteQueue.sync { 155 | buffer.removeAll() 156 | let semaphore = DispatchSemaphore(value: 0) 157 | logStore.retrieveLogs(of: storageGroup) { logs in 158 | let filteredLogs = logs.filter { log in 159 | return !currentWritingChunks.contains { chunk in 160 | return chunk.logs.contains(log) 161 | } 162 | } 163 | buffer = buffer.union(filteredLogs) 164 | semaphore.signal() 165 | } 166 | semaphore.wait() 167 | } 168 | } 169 | 170 | private func writeBufferedLogs() { 171 | dispatchPrecondition(condition: .onQueue(readWriteQueue)) 172 | 173 | lastFlushDate = currentDate 174 | 175 | if buffer.isEmpty { 176 | return 177 | } 178 | 179 | let logCount = min(buffer.count, logLimit) 180 | let newBuffer = Set(buffer.dropFirst(logCount)) 181 | let dropped = buffer.subtracting(newBuffer) 182 | buffer = newBuffer 183 | let logsToSend: Set 184 | if let chunkDataSizeLimit = configuration.chunkDataSizeLimit { 185 | var logsUnderSizeLimit = Set() 186 | 187 | var currentTotalLogSize = 0 188 | for log in dropped { 189 | if currentTotalLogSize + (log.userData?.count ?? 0) < chunkDataSizeLimit { 190 | logsUnderSizeLimit.insert(log) 191 | currentTotalLogSize += log.userData?.count ?? 0 192 | } else { 193 | buffer = dropped.subtracting(logsUnderSizeLimit) 194 | break 195 | } 196 | } 197 | logsToSend = logsUnderSizeLimit 198 | } else { 199 | logsToSend = dropped 200 | } 201 | callWriteChunk(Chunk(logs: logsToSend)) 202 | } 203 | 204 | open func delay(try count: Int) -> TimeInterval { 205 | return 2.0 * pow(2.0, Double(count - 1)) 206 | } 207 | 208 | private func callWriteChunk(_ chunk: Chunk) { 209 | dispatchPrecondition(condition: .onQueue(readWriteQueue)) 210 | 211 | currentWritingChunks.insert(chunk) 212 | write(chunk) { success in 213 | if success { 214 | self.readWriteQueue.async { 215 | self.currentWritingChunks.remove(chunk) 216 | self.logStore.remove(chunk.logs, from: self.storageGroup, completion: nil) 217 | } 218 | return 219 | } 220 | 221 | var chunk = chunk 222 | chunk.incrementRetryCount() 223 | 224 | if chunk.retryCount <= self.retryLimit { 225 | let delay: TimeInterval = self.delay(try: chunk.retryCount) 226 | self.readWriteQueue.asyncAfter(deadline: .now() + delay) { 227 | self.callWriteChunk(chunk) 228 | } 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puree 2 | 3 | ![](Documentation/logo.png) 4 | 5 | [![Build Status](https://github.com/cookpad/Puree-Swift/workflows/Puree/badge.svg)](https://github.com/cookpad/Puree-Swift/actions) 6 | [![Language](https://img.shields.io/badge/language-Swift%205.0-orange.svg)](https://swift.org) 7 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Puree.svg)](http://cocoadocs.org/docsets/Puree) 9 | [![Platform](https://img.shields.io/cocoapods/p/Puree.svg?style=flat)](http://cocoadocs.org/docsets/Puree) 10 | [![License](https://cocoapod-badges.herokuapp.com/l/Puree/badge.svg)](https://github.com/cookpad/Puree-Swift/blob/master/LICENSE) 11 | 12 | ## Description 13 | 14 | Puree is a log aggregator which provides the following features. 15 | 16 | - Filtering: Log entries can be processed before being sent. You can add common parameters, do random sampling, ... 17 | - Buffering: Log entries are stored in a buffer until it's time to send them. 18 | - Batching: Multiple log entries are grouped and sent in one request. 19 | - Retrying: Automatically retry to send after some backoff time if a transmission error occurred. 20 | 21 | ![](./Documentation/overview.png) 22 | 23 | Puree helps you unify your logging infrastructure. 24 | 25 | Currently in development so the interface might change. 26 | 27 | ## Installation 28 | 29 | ### Carthage 30 | 31 | ``` 32 | github "cookpad/Puree-Swift" 33 | ``` 34 | 35 | ### CocoaPods 36 | 37 | ```ruby 38 | use_frameworks! 39 | 40 | pod 'Puree', '~> 5.0' 41 | ``` 42 | 43 | ### Swift PM 44 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Puree-Swift does support its use on supported platforms. 45 | 46 | Once you have your Swift package set up, adding Puree-Swift as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. 47 | ```swift 48 | dependencies: [ 49 | .package(url: "https://github.com/cookpad/Puree-Swift.git", .upToNextMinor(from: "5.3.0")) 50 | ] 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Define your own Filter/Output 56 | 57 | #### Filter 58 | 59 | A `Filter` should convert any objects into `LogEntry`. 60 | 61 | ```swift 62 | import Foundation 63 | import Puree 64 | 65 | struct PVLogFilter: Filter { 66 | let tagPattern: TagPattern 67 | 68 | init(tagPattern: TagPattern) { 69 | self.tagPattern = tagPattern 70 | } 71 | 72 | func convertToLogs(_ payload: [String: Any]?, tag: String, captured: String?, logger: Logger) -> Set { 73 | let currentDate = logger.currentDate 74 | 75 | let userData: Data? 76 | if let payload = payload { 77 | userData = try! JSONSerialization.data(withJSONObject: payload) 78 | } else { 79 | userData = nil 80 | } 81 | let log = LogEntry(tag: tag, 82 | date: currentDate, 83 | userData: userData) 84 | return [log] 85 | } 86 | } 87 | ``` 88 | 89 | #### Output 90 | 91 | An `Output` should emit log entries to wherever they need. 92 | 93 | The following `ConsoleOutput` will output logs to the standard output. 94 | 95 | ```swift 96 | class ConsoleOutput: Output { 97 | let tagPattern: TagPattern 98 | 99 | required init(logStore: LogStore, tagPattern: TagPattern) { 100 | self.tagPattern = tagPattern 101 | } 102 | 103 | func emit(log: LogEntry) { 104 | if let userData = log.userData { 105 | let jsonObject = try! JSONSerialization.jsonObject(with: userData) 106 | print(jsonObject) 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | ##### BufferedOutput 113 | 114 | If you use `BufferedOutput` instead of raw `Output`, log entries are buffered and emitted on a routine schedule. 115 | 116 | ```swift 117 | class LogServerOutput: BufferedOutput { 118 | override func write(_ chunk: BufferedOutput.Chunk, completion: @escaping (Bool) -> Void) { 119 | let payload = chunk.logs.flatMap { log in 120 | if let userData = log.userData { 121 | return try? JSONSerialization.jsonObject(with: userData, options: []) 122 | } 123 | return nil 124 | } 125 | if let data = try? JSONSerialization.data(withJSONObject: payload, options: []) { 126 | let task = URLSession.shared.uploadTask(with: request, from: data) 127 | task.resume() 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | ### Make logger and post log 134 | 135 | After implementing filters and outputs, you can configure the routing with `Logger.Configuration`. 136 | 137 | ```swift 138 | import Puree 139 | 140 | let configuration = Logger.Configuration(filterSettings: [ 141 | FilterSetting { 142 | PVLogFilter(tagPattern: TagPattern(string: "pv.**")!) 143 | } 144 | ], 145 | outputSettings: [ 146 | OutputSetting { 147 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "activity.**")!) 148 | }, 149 | OutputSetting { 150 | ConsoleOutput(logStore: $0, tagPattern: TagPattern(string: "pv.**")!) 151 | }, 152 | OutputSetting { 153 | LogServerOutput(logStore: $0, tagPattern: TagPattern(string: "pv.**")!) 154 | }, 155 | ]) 156 | let logger = try! Logger(configuration: configuration) 157 | logger.postLog(["page_name": "top", "user_id": 100], tag: "pv.top") 158 | ``` 159 | 160 | Using this configuration, the expected result is as follows: 161 | 162 | |tag name |-> [ Filter Plugin ] |-> [ Output Plugin ] | 163 | |----------------------|---------------------|---------------------| 164 | |pv.recipe.list |-> [ `PVLogFilter` ] |-> [ `ConsoleOutput` ], [ `LogServerOutput` ]| 165 | |pv.recipe.detail |-> [ `PVLogFilter` ] |-> [ `ConsoleOutput` ], [ `LogServerOutput` ]| 166 | |activity.recipe.tap |-> ( no filter ) |-> [ `ConsoleOutput` ] | 167 | |event.special |-> ( no filter ) |-> ( no output ) | 168 | 169 | 170 | We recommend suspending loggers while the application is in the background. 171 | 172 | ```swift 173 | class AppDelegate: UIApplicationDelegate { 174 | func applicationDidEnterBackground(_ application: UIApplication) { 175 | logger.suspend() 176 | } 177 | 178 | func applicationWillEnterForeground(_ application: UIApplication) { 179 | logger.resume() 180 | } 181 | } 182 | ``` 183 | 184 | ## Tag system 185 | 186 | ### Tag 187 | 188 | A tag is consisted of multiple term delimited by `.`. 189 | For example `activity.recipe.view`, `pv.recipe_detail`. 190 | You can choose your tags logged freely. 191 | 192 | ### Pattern 193 | 194 | `Filter`, `Output` and `BufferedOutput` plugins are applied to log entries with a matching tag. 195 | You can specify tag pattern for plugin reaction rules. 196 | 197 | #### Simple pattern 198 | 199 | Pattern `aaa.bbb` matches tag `aaa.bbb`, doesn't match tag `aaa.ccc` (Perfect matching). 200 | 201 | #### Wildcard 202 | 203 | Pattern `aaa.*` matches tags `aaa.bbb` and `aaa.ccc`, but not `aaa` or `aaa.bbb.ccc` (single term). 204 | 205 | Pattern `aaa.**` matches tags `aaa`, `aaa.bbb` and `aaa.bbb.ccc`, but not `xxx.yyy.zzz` (zero or more terms). 206 | 207 | ## Log Store 208 | 209 | In the case an application couldn't send log entries (e.g. network connection unavailable), Puree stores the unsent entries. 210 | 211 | By default, Puree stores them in local files in the `Library/Caches` directory. 212 | 213 | You can also define your own custom log store backed by any storage (e.g. Core Data, Realm, YapDatabase, etc.). 214 | 215 | See the `LogStore` protocol for more details. 216 | 217 | ## License 218 | 219 | Please do read the [License](https://github.com/cookpad/Puree-Swift/blob/master/LICENSE) before contributing. 220 | -------------------------------------------------------------------------------- /Tests/PureeTests/LoggerTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import Puree 4 | 5 | let buffer = TestingBuffer() 6 | 7 | struct PVLogFilter: Filter { 8 | init(tagPattern: TagPattern, options: [String: Any]? = nil) { 9 | self.tagPattern = tagPattern 10 | } 11 | 12 | let tagPattern: TagPattern 13 | 14 | func convertToLogs(_ payload: [String: Any]?, tag: String, captured: String?, logger: Logger) -> Set { 15 | var log = LogEntry(tag: tag, date: logger.currentDate) 16 | guard let userInfo = payload, let userData = try? JSONSerialization.data(withJSONObject: userInfo, options: []) else { 17 | XCTFail("could not encode userInfo") 18 | return [] 19 | } 20 | log.userData = userData 21 | return [log] 22 | } 23 | } 24 | 25 | struct PVLogOutput: Output { 26 | let tagPattern: TagPattern 27 | 28 | init(logStore: LogStore, tagPattern: TagPattern, options: [String: Any]? = nil) { 29 | self.tagPattern = tagPattern 30 | } 31 | 32 | func emit(log: LogEntry) { 33 | buffer.write(log, for: tagPattern.pattern) 34 | } 35 | } 36 | 37 | class LoggerTests: XCTestCase { 38 | let logStore = InMemoryLogStore() 39 | 40 | func testLoggerWithSingleTag() { 41 | let configuration = Logger.Configuration(logStore: logStore, 42 | dateProvider: DefaultDateProvider(), 43 | filterSettings: [ 44 | FilterSetting { 45 | PVLogFilter(tagPattern: TagPattern(string: "pv")!) 46 | }, 47 | ], 48 | outputSettings: [ 49 | OutputSetting { 50 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv")!) 51 | }, 52 | ]) 53 | let logger = try! Logger(configuration: configuration) 54 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv") 55 | logger.suspend() 56 | 57 | XCTAssertEqual(buffer.logs(for: "pv").count, 1) 58 | 59 | let log = buffer.logs(for: "pv").first! 60 | guard let userInfo = try? (JSONSerialization.jsonObject(with: log.userData!, options: []) as! [String: Any]) else { 61 | return XCTFail("userInfo could not decoded") 62 | } 63 | XCTAssertEqual(userInfo["page_name"] as! String, "Top") 64 | XCTAssertEqual(userInfo["user_id"] as! Int, 100) 65 | } 66 | 67 | func testLoggerWithMultipleTag() { 68 | let configuration = Logger.Configuration(logStore: logStore, 69 | dateProvider: DefaultDateProvider(), 70 | filterSettings: [ 71 | FilterSetting { 72 | PVLogFilter(tagPattern: TagPattern(string: "pv")!) 73 | }, 74 | FilterSetting { 75 | PVLogFilter(tagPattern: TagPattern(string: "pv2")!) 76 | }, 77 | FilterSetting { 78 | PVLogFilter(tagPattern: TagPattern(string: "pv.*")!) 79 | }, 80 | ], 81 | outputSettings: [ 82 | OutputSetting { 83 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv")!) 84 | }, 85 | OutputSetting { 86 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv2")!) 87 | }, 88 | OutputSetting { 89 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv.*")!) 90 | }, 91 | ]) 92 | let logger = try! Logger(configuration: configuration) 93 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv.top") 94 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv2") 95 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv2") 96 | logger.suspend() 97 | 98 | XCTAssertEqual(buffer.logs(for: "pv").count, 0) 99 | XCTAssertEqual(buffer.logs(for: "pv2").count, 2) 100 | XCTAssertEqual(buffer.logs(for: "pv.*").count, 1) 101 | } 102 | 103 | func testLoggerWithCustomSetting() { 104 | struct CustomFilterSetting: FilterSettingProtocol { 105 | private let tableName: String 106 | 107 | init(tableName: String) { 108 | self.tableName = tableName 109 | } 110 | 111 | func makeFilter() throws -> Filter { 112 | return PVLogFilter(tagPattern: TagPattern(string: "pv2")!, options: ["table_name": tableName]) 113 | } 114 | } 115 | 116 | struct CustomOutputSetting: OutputSettingProtocol { 117 | private let tableName: String 118 | 119 | init(tableName: String) { 120 | self.tableName = tableName 121 | } 122 | 123 | func makeOutput(_ logStore: LogStore) throws -> Output { 124 | return PVLogOutput(logStore: logStore, tagPattern: TagPattern(string: "pv2")!, options: ["table_name": tableName]) 125 | } 126 | } 127 | 128 | let configuration = Logger.Configuration(logStore: logStore, 129 | dateProvider: DefaultDateProvider(), 130 | filterSettings: [ 131 | FilterSetting { 132 | PVLogFilter(tagPattern: TagPattern(string: "pv")!) 133 | }, 134 | CustomFilterSetting(tableName: "pv_log"), 135 | FilterSetting { 136 | PVLogFilter(tagPattern: TagPattern(string: "pv.*")!) 137 | }, 138 | ], 139 | outputSettings: [ 140 | OutputSetting { 141 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv")!) 142 | }, 143 | CustomOutputSetting(tableName: "pv_log"), 144 | OutputSetting { 145 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv.*")!) 146 | }, 147 | ]) 148 | let logger = try! Logger(configuration: configuration) 149 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv.top") 150 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv2") 151 | logger.postLog(["page_name": "Top", "user_id": 100], tag: "pv2") 152 | logger.suspend() 153 | 154 | XCTAssertEqual(buffer.logs(for: "pv").count, 0) 155 | XCTAssertEqual(buffer.logs(for: "pv2").count, 2) 156 | XCTAssertEqual(buffer.logs(for: "pv.*").count, 1) 157 | } 158 | 159 | func testLoggerWithMultiThread() { 160 | let configuration = Logger.Configuration(logStore: logStore, 161 | dateProvider: DefaultDateProvider(), 162 | filterSettings: [ 163 | FilterSetting { 164 | PVLogFilter(tagPattern: TagPattern(string: "pv")!) 165 | }, 166 | ], 167 | outputSettings: [ 168 | OutputSetting { 169 | PVLogOutput(logStore: $0, tagPattern: TagPattern(string: "pv")!) 170 | }, 171 | ]) 172 | let logger = try! Logger(configuration: configuration) 173 | 174 | let semaphore = DispatchSemaphore(value: 0) 175 | let testIndices = 0..<100 176 | 177 | for index in testIndices { 178 | DispatchQueue.global(qos: .background).async { 179 | logger.postLog(["queue": "global", "index": index], tag: "pv") 180 | semaphore.signal() 181 | } 182 | } 183 | 184 | for _ in testIndices { 185 | semaphore.wait() 186 | } 187 | logger.suspend() 188 | 189 | let logs = buffer.logs(for: "pv") 190 | XCTAssertEqual(logs.count, 100) 191 | 192 | for index in testIndices { 193 | let found = logs.contains { log -> Bool in 194 | guard let userInfo = try? JSONSerialization.jsonObject(with: log.userData!, options: []) as? [String: Any] else { 195 | XCTFail("userInfo could not decoded") 196 | return false 197 | } 198 | 199 | return (userInfo["index"] as! Int) == index 200 | } 201 | XCTAssertTrue(found) 202 | } 203 | } 204 | 205 | override func tearDown() { 206 | super.tearDown() 207 | 208 | buffer.flush() 209 | logStore.flush() 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Tests/PureeTests/Output/BufferedOutputTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import Puree 4 | 5 | private func makeLog() -> LogEntry { 6 | return LogEntry(tag: "foo", date: Date()) 7 | } 8 | 9 | class TestingBufferedOutput: BufferedOutput { 10 | var shouldSuccess: Bool = true 11 | fileprivate(set) var calledWriteCount: Int = 0 12 | var writeCallback: (() -> Void)? 13 | var waitUntilCurrentCompletionBlock: (() -> Void)? 14 | 15 | override func write(_ chunk: BufferedOutput.Chunk, completion: @escaping (Bool) -> Void) { 16 | calledWriteCount += 1 17 | completion(shouldSuccess) 18 | writeCallback?() 19 | } 20 | 21 | override func delay(try count: Int) -> TimeInterval { 22 | return 0.2 23 | } 24 | 25 | func waitUntilCurrentQueuedJobFinished() { 26 | waitUntilCurrentCompletionBlock?() 27 | readWriteQueue.sync { 28 | } 29 | } 30 | } 31 | 32 | class BufferedOutputTests: XCTestCase { 33 | var output: TestingBufferedOutput! 34 | let logStore = InMemoryLogStore() 35 | 36 | override func setUp() { 37 | output = TestingBufferedOutput(logStore: logStore, tagPattern: TagPattern(string: "pv")!) 38 | output.configuration.flushInterval = TimeInterval.infinity 39 | output.start() 40 | } 41 | 42 | func testBufferedOutput() { 43 | output.configuration.logEntryCountLimit = 1 44 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 45 | XCTAssertEqual(output.calledWriteCount, 0) 46 | output.emit(log: makeLog()) 47 | XCTAssertEqual(output.calledWriteCount, 1) 48 | } 49 | 50 | func testBufferedOutputWithAlreadyStoredLogs() { 51 | output.configuration.logEntryCountLimit = 10 52 | output.configuration.flushInterval = 1 53 | 54 | let storedLogs: Set = Set((0..<10).map { _ in makeLog() }) 55 | logStore.add(storedLogs, for: "pv_TestingBufferedOutput", completion: nil) 56 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 57 | XCTAssertEqual(output.calledWriteCount, 0) 58 | 59 | output.resume() 60 | output.waitUntilCurrentQueuedJobFinished() 61 | 62 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 63 | XCTAssertEqual(output.calledWriteCount, 1) 64 | } 65 | 66 | func testBufferedOutputSendBufferedLogs() { 67 | output.configuration.logEntryCountLimit = 10 68 | 69 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 70 | XCTAssertEqual(output.calledWriteCount, 0) 71 | 72 | output.emit(log: makeLog()) 73 | output.sendBufferedLogs() 74 | output.waitUntilCurrentQueuedJobFinished() 75 | 76 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 77 | XCTAssertEqual(output.calledWriteCount, 1) 78 | } 79 | 80 | func testBufferedOutputFlushedByInterval() { 81 | output.configuration.logEntryCountLimit = 10 82 | output.configuration.flushInterval = 1 83 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 84 | XCTAssertEqual(output.calledWriteCount, 0) 85 | output.emit(log: makeLog()) 86 | XCTAssertEqual(output.calledWriteCount, 0) 87 | 88 | let expectation = self.expectation(description: "logs should be flushed") 89 | output.writeCallback = { 90 | expectation.fulfill() 91 | } 92 | wait(for: [expectation], timeout: 10.0) 93 | } 94 | 95 | func testBufferedOutputNotFlushed() { 96 | output.configuration.logEntryCountLimit = 10 97 | output.configuration.flushInterval = 10 98 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 99 | XCTAssertEqual(output.calledWriteCount, 0) 100 | output.emit(log: makeLog()) 101 | XCTAssertEqual(output.calledWriteCount, 0) 102 | 103 | output.writeCallback = { 104 | XCTFail("writeBufferedLogs should not be called") 105 | } 106 | sleep(2) 107 | } 108 | 109 | func testHittingLogLimit() { 110 | output.configuration.logEntryCountLimit = 10 111 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 112 | XCTAssertEqual(output.calledWriteCount, 0) 113 | for i in 1..<10 { 114 | output.emit(log: makeLog()) 115 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, i) 116 | } 117 | XCTAssertEqual(output.calledWriteCount, 0) 118 | 119 | output.emit(log: makeLog()) 120 | XCTAssertEqual(output.calledWriteCount, 1) 121 | XCTAssertEqual(logStore.logs(for: "pv").count, 0) 122 | } 123 | 124 | func testHittingLogSizeLimit() { 125 | // Set maximum chunk size as 15bytes 126 | output.configuration.chunkDataSizeLimit = 15 127 | 128 | // Disable to send according to log count 129 | output.configuration.logEntryCountLimit = .max 130 | 131 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 132 | XCTAssertEqual(output.calledWriteCount, 0) 133 | 134 | // Emit one log entry whose size is 10bytes. 135 | // This should not be sent at this time because size limit has not exceeded yet. 136 | var log1 = makeLog() 137 | log1.userData = "0123456789".data(using: .utf8) 138 | output.emit(log: log1) 139 | XCTAssertEqual(output.calledWriteCount, 0) 140 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 1) 141 | 142 | // Emit one more log entry whose size is 10bytes. 143 | // Now `log1` should be sent and `log2` shoud not because reaching to size limit. `log2` should remain in the `logStore` . 144 | var log2 = makeLog() 145 | log2.userData = "0123456789".data(using: .utf8) 146 | output.emit(log: log2) 147 | output.waitUntilCurrentQueuedJobFinished() 148 | XCTAssertEqual(output.calledWriteCount, 1) 149 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 1) 150 | 151 | // Emit one more log entry whose size is 3bytes. 152 | // Now `log2` and `log3` should not be sent yet because total size has not reached the limit. 153 | var log3 = makeLog() 154 | log3.userData = "012".data(using: .utf8) 155 | output.emit(log: log3) 156 | output.waitUntilCurrentQueuedJobFinished() 157 | XCTAssertEqual(output.calledWriteCount, 1) 158 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 2) 159 | } 160 | 161 | func testEmittingOneLogLargerThanSizeLimit() { 162 | // Set maximum chunk size as 5bytes 163 | output.configuration.chunkDataSizeLimit = 5 164 | 165 | // Disable to send according to log count 166 | output.configuration.logEntryCountLimit = .max 167 | 168 | // A log entry that has data larger than limit will be discarded. 169 | var log1 = makeLog() 170 | log1.userData = "0123456789".data(using: .utf8) 171 | output.emit(log: log1) 172 | XCTAssertEqual(output.calledWriteCount, 0) 173 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 174 | } 175 | 176 | func testRetryWhenFailed() { 177 | output.shouldSuccess = false 178 | output.configuration.logEntryCountLimit = 10 179 | output.configuration.retryLimit = 3 180 | XCTAssertEqual(output.calledWriteCount, 0) 181 | for _ in 0..<10 { 182 | output.emit(log: makeLog()) 183 | } 184 | output.waitUntilCurrentQueuedJobFinished() 185 | 186 | var expectation = self.expectation(description: "retry writeChunk") 187 | XCTAssertEqual(output.calledWriteCount, 1) 188 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 189 | output.writeCallback = { 190 | expectation.fulfill() 191 | } 192 | wait(for: [expectation], timeout: 5.0) 193 | 194 | expectation = self.expectation(description: "retry writeChunk") 195 | XCTAssertEqual(output.calledWriteCount, 2) 196 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 197 | output.writeCallback = { 198 | expectation.fulfill() 199 | } 200 | wait(for: [expectation], timeout: 5.0) 201 | 202 | expectation = self.expectation(description: "retry writeChunk") 203 | XCTAssertEqual(output.calledWriteCount, 3) 204 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 205 | output.writeCallback = { 206 | expectation.fulfill() 207 | } 208 | wait(for: [expectation], timeout: 5.0) 209 | XCTAssertEqual(output.calledWriteCount, 4) 210 | } 211 | 212 | func testParallelWrite() { 213 | output.configuration.logEntryCountLimit = 2 214 | output.configuration.retryLimit = 3 215 | let testIndices = 0..<1000 216 | let expectedWriteCount = 500 217 | 218 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 219 | XCTAssertEqual(output.calledWriteCount, 0) 220 | 221 | var writeCallbackCalledCount = 0 222 | output.writeCallback = { 223 | writeCallbackCalledCount += 1 224 | } 225 | 226 | let semaphore = DispatchSemaphore(value: 0) 227 | for _ in testIndices { 228 | DispatchQueue.global(qos: .background).async { 229 | self.output.emit(log: makeLog()) 230 | semaphore.signal() 231 | } 232 | } 233 | 234 | for _ in testIndices { 235 | semaphore.wait() 236 | } 237 | output.resume() 238 | 239 | XCTAssertEqual(output.calledWriteCount, expectedWriteCount) 240 | XCTAssertEqual(writeCallbackCalledCount, expectedWriteCount) 241 | } 242 | 243 | override func tearDown() { 244 | super.tearDown() 245 | 246 | output.suspend() 247 | logStore.flush() 248 | } 249 | } 250 | 251 | class TestingBufferedOutputAsync: TestingBufferedOutput { 252 | override var storageGroup: String { 253 | return "pv_TestingBufferedOutput" 254 | } 255 | 256 | override func write(_ chunk: BufferedOutput.Chunk, completion: @escaping (Bool) -> Void) { 257 | calledWriteCount += 1 258 | DispatchQueue.global().async { 259 | Thread.sleep(forTimeInterval: 0.1) 260 | completion(self.shouldSuccess) 261 | self.writeCallback?() 262 | } 263 | } 264 | } 265 | 266 | class BufferedOutputAsyncTests: XCTestCase { 267 | var output: TestingBufferedOutputAsync! 268 | let logStore = InMemoryLogStore() 269 | 270 | override func setUp() { 271 | output = TestingBufferedOutputAsync(logStore: logStore, tagPattern: TagPattern(string: "pv")!) 272 | output.configuration.flushInterval = TimeInterval.infinity 273 | output.start() 274 | } 275 | 276 | func testBufferedOutput() { 277 | output.configuration.logEntryCountLimit = 1 278 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 279 | XCTAssertEqual(output.calledWriteCount, 0) 280 | output.emit(log: makeLog()) 281 | XCTAssertEqual(output.calledWriteCount, 1) 282 | } 283 | 284 | func testBufferedOutputWithAlreadyStoredLogs() { 285 | output.configuration.logEntryCountLimit = 10 286 | output.configuration.flushInterval = 1 287 | 288 | let expectation = self.expectation(description: "async writing") 289 | output.writeCallback = { 290 | expectation.fulfill() 291 | } 292 | output.waitUntilCurrentCompletionBlock = { [weak self] in 293 | self?.wait(for: [expectation], timeout: 1.0) 294 | } 295 | 296 | let storedLogs: Set = Set((0..<10).map { _ in makeLog() }) 297 | logStore.add(storedLogs, for: "pv_TestingBufferedOutput", completion: nil) 298 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 299 | XCTAssertEqual(output.calledWriteCount, 0) 300 | 301 | output.resume() 302 | output.waitUntilCurrentQueuedJobFinished() 303 | 304 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 305 | XCTAssertEqual(output.calledWriteCount, 1) 306 | } 307 | 308 | func testBufferedOutputSendBufferedLogs() { 309 | output.configuration.logEntryCountLimit = 10 310 | 311 | let expectation = self.expectation(description: "async writing") 312 | output.writeCallback = { 313 | expectation.fulfill() 314 | } 315 | output.waitUntilCurrentCompletionBlock = { [weak self] in 316 | self?.wait(for: [expectation], timeout: 1.0) 317 | } 318 | 319 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 320 | XCTAssertEqual(output.calledWriteCount, 0) 321 | 322 | output.emit(log: makeLog()) 323 | output.sendBufferedLogs() 324 | output.waitUntilCurrentQueuedJobFinished() 325 | 326 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 327 | XCTAssertEqual(output.calledWriteCount, 1) 328 | } 329 | 330 | func testBufferedOutputFlushedByInterval() { 331 | output.configuration.logEntryCountLimit = 10 332 | output.configuration.flushInterval = 1 333 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 334 | XCTAssertEqual(output.calledWriteCount, 0) 335 | output.emit(log: makeLog()) 336 | XCTAssertEqual(output.calledWriteCount, 0) 337 | 338 | let expectation = self.expectation(description: "logs should be flushed") 339 | output.writeCallback = { 340 | expectation.fulfill() 341 | } 342 | wait(for: [expectation], timeout: 10.0) 343 | } 344 | 345 | func testBufferedOutputNotFlushed() { 346 | output.configuration.logEntryCountLimit = 10 347 | output.configuration.flushInterval = 10 348 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 349 | XCTAssertEqual(output.calledWriteCount, 0) 350 | output.emit(log: makeLog()) 351 | XCTAssertEqual(output.calledWriteCount, 0) 352 | 353 | output.writeCallback = { 354 | XCTFail("writeBufferedLogs should not be called") 355 | } 356 | sleep(2) 357 | } 358 | 359 | func testHittingLogLimit() { 360 | output.configuration.logEntryCountLimit = 10 361 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 362 | XCTAssertEqual(output.calledWriteCount, 0) 363 | for i in 1..<10 { 364 | output.emit(log: makeLog()) 365 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, i) 366 | } 367 | XCTAssertEqual(output.calledWriteCount, 0) 368 | 369 | output.emit(log: makeLog()) 370 | XCTAssertEqual(output.calledWriteCount, 1) 371 | XCTAssertEqual(logStore.logs(for: "pv").count, 0) 372 | } 373 | 374 | func testRetryWhenFailed() { 375 | output.shouldSuccess = false 376 | output.configuration.logEntryCountLimit = 10 377 | output.configuration.retryLimit = 3 378 | 379 | var expectation = self.expectation(description: "async writing") 380 | output.writeCallback = { 381 | expectation.fulfill() 382 | } 383 | output.waitUntilCurrentCompletionBlock = { [weak self] in 384 | self?.wait(for: [expectation], timeout: 5.0) 385 | } 386 | 387 | XCTAssertEqual(output.calledWriteCount, 0) 388 | for _ in 0..<10 { 389 | output.emit(log: makeLog()) 390 | } 391 | output.waitUntilCurrentQueuedJobFinished() 392 | 393 | XCTAssertEqual(output.calledWriteCount, 1) 394 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 395 | 396 | expectation = self.expectation(description: "retry writeChunk") 397 | output.writeCallback = { 398 | expectation.fulfill() 399 | } 400 | wait(for: [expectation], timeout: 5.0) 401 | XCTAssertEqual(output.calledWriteCount, 2) 402 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 403 | 404 | expectation = self.expectation(description: "retry writeChunk") 405 | output.writeCallback = { 406 | expectation.fulfill() 407 | } 408 | wait(for: [expectation], timeout: 5.0) 409 | XCTAssertEqual(output.calledWriteCount, 3) 410 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 10) 411 | 412 | expectation = self.expectation(description: "retry writeChunk") 413 | output.writeCallback = { 414 | expectation.fulfill() 415 | } 416 | wait(for: [expectation], timeout: 5.0) 417 | XCTAssertEqual(output.calledWriteCount, 4) 418 | } 419 | 420 | func testParallelWrite() { 421 | output.configuration.logEntryCountLimit = 2 422 | output.configuration.retryLimit = 3 423 | let testIndices = 0..<1000 424 | let expectedWriteCount = 500 425 | 426 | XCTAssertEqual(logStore.logs(for: "pv_TestingBufferedOutput").count, 0) 427 | XCTAssertEqual(output.calledWriteCount, 0) 428 | 429 | let expectation = self.expectation(description: "async writing") 430 | expectation.expectedFulfillmentCount = expectedWriteCount 431 | output.waitUntilCurrentCompletionBlock = { [weak self] in 432 | self?.wait(for: [expectation], timeout: 20.0) 433 | } 434 | 435 | var writeCallbackCalledCount = 0 436 | output.writeCallback = { 437 | DispatchQueue.main.async { 438 | writeCallbackCalledCount += 1 439 | expectation.fulfill() 440 | } 441 | } 442 | 443 | let semaphore = DispatchSemaphore(value: 0) 444 | for _ in testIndices { 445 | DispatchQueue.global(qos: .background).async { 446 | self.output.emit(log: makeLog()) 447 | semaphore.signal() 448 | } 449 | } 450 | 451 | for _ in testIndices { 452 | semaphore.wait() 453 | } 454 | 455 | output.resume() 456 | output.waitUntilCurrentQueuedJobFinished() 457 | 458 | XCTAssertEqual(output.calledWriteCount, expectedWriteCount) 459 | XCTAssertEqual(writeCallbackCalledCount, expectedWriteCount) 460 | } 461 | 462 | override func tearDown() { 463 | output.writeCallback = nil 464 | 465 | output.suspend() 466 | logStore.flush() 467 | } 468 | } 469 | 470 | class BufferedOutputDispatchQueueTests: XCTestCase { 471 | 472 | func testFlushIntervalOnDifferentDispatchQueue() { 473 | let exp = expectation(description: #function) 474 | let dispatchQueue = DispatchQueue(label: "com.cookpad.Puree.Logger", qos: .background) 475 | 476 | dispatchQueue.async { 477 | let logStore = InMemoryLogStore() 478 | let output = TestingBufferedOutput(logStore: logStore, tagPattern: TagPattern(string: "pv")!) 479 | output.configuration = BufferedOutput.Configuration(logEntryCountLimit: 5, flushInterval: 0, retryLimit: 3) 480 | output.start() 481 | 482 | output.emit(log: makeLog()) 483 | 484 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { 485 | XCTAssertEqual(output.calledWriteCount, 1) 486 | exp.fulfill() 487 | } 488 | } 489 | 490 | waitForExpectations(timeout: 10.0) 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /Puree.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 83E040602105E69F00B92093 /* TestingBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E0405E2105E68C00B92093 /* TestingBuffer.swift */; }; 11 | BF_104851505209 /* InMemoryLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_613975198094 /* InMemoryLogStore.swift */; }; 12 | BF_180232540354 /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_848196260343 /* Output.swift */; }; 13 | BF_210674583290 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_176166977065 /* Filter.swift */; }; 14 | BF_219592565366 /* Puree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FR_479946421258 /* Puree.framework */; }; 15 | BF_278872148905 /* Puree.h in Headers */ = {isa = PBXBuildFile; fileRef = FR_793364177672 /* Puree.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | BF_364947189323 /* FileLogStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_716897034030 /* FileLogStoreTests.swift */; }; 17 | BF_415193818225 /* FileLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_735717710124 /* FileLogStore.swift */; }; 18 | BF_429021003566 /* LogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_818352195712 /* LogStore.swift */; }; 19 | BF_468687204183 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_912035359082 /* LogEntry.swift */; }; 20 | BF_485048067586 /* BufferedOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_630835297098 /* BufferedOutputTests.swift */; }; 21 | BF_595409884047 /* TagPatternTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_870064055253 /* TagPatternTests.swift */; }; 22 | BF_625371294693 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_775709644702 /* Logger.swift */; }; 23 | BF_647931598633 /* LogEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_167045655688 /* LogEntryTests.swift */; }; 24 | BF_859683188258 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_657386344080 /* LoggerTests.swift */; }; 25 | BF_884702636068 /* DateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_828954093903 /* DateProvider.swift */; }; 26 | BF_894429986524 /* TagPattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_644801390644 /* TagPattern.swift */; }; 27 | BF_950872890744 /* BufferedOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR_254889461063 /* BufferedOutput.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXContainerItemProxy section */ 31 | CIP_41209300418 /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = P_4799464212581 /* Project object */; 34 | proxyType = 1; 35 | remoteGlobalIDString = NT_479946421258; 36 | remoteInfo = Puree; 37 | }; 38 | /* End PBXContainerItemProxy section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 83E0405E2105E68C00B92093 /* TestingBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestingBuffer.swift; sourceTree = ""; }; 42 | FR_135157311129 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | FR_167045655688 /* LogEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntryTests.swift; sourceTree = ""; }; 44 | FR_176166977065 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 45 | FR_254889461063 /* BufferedOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferedOutput.swift; sourceTree = ""; }; 46 | FR_272688927660 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | FR_412093004181 /* PureeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PureeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | FR_479946421258 /* Puree.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Puree.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | FR_613975198094 /* InMemoryLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryLogStore.swift; sourceTree = ""; }; 50 | FR_630835297098 /* BufferedOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferedOutputTests.swift; sourceTree = ""; }; 51 | FR_644801390644 /* TagPattern.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPattern.swift; sourceTree = ""; }; 52 | FR_657386344080 /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; 53 | FR_716897034030 /* FileLogStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogStoreTests.swift; sourceTree = ""; }; 54 | FR_735717710124 /* FileLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogStore.swift; sourceTree = ""; }; 55 | FR_775709644702 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 56 | FR_793364177672 /* Puree.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Puree.h; sourceTree = ""; }; 57 | FR_818352195712 /* LogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStore.swift; sourceTree = ""; }; 58 | FR_828954093903 /* DateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateProvider.swift; sourceTree = ""; }; 59 | FR_848196260343 /* Output.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Output.swift; sourceTree = ""; }; 60 | FR_870064055253 /* TagPatternTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagPatternTests.swift; sourceTree = ""; }; 61 | FR_912035359082 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = ""; }; 62 | /* End PBXFileReference section */ 63 | 64 | /* Begin PBXFrameworksBuildPhase section */ 65 | FBP_41209300418 /* Frameworks */ = { 66 | isa = PBXFrameworksBuildPhase; 67 | buildActionMask = 2147483647; 68 | files = ( 69 | BF_219592565366 /* Puree.framework in Frameworks */, 70 | ); 71 | runOnlyForDeploymentPostprocessing = 0; 72 | }; 73 | /* End PBXFrameworksBuildPhase section */ 74 | 75 | /* Begin PBXGroup section */ 76 | 83935AC520F88CA800FA8B9A /* Output */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | FR_254889461063 /* BufferedOutput.swift */, 80 | FR_848196260343 /* Output.swift */, 81 | ); 82 | path = Output; 83 | sourceTree = ""; 84 | }; 85 | 83935AC620F8920500FA8B9A /* Output */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | FR_630835297098 /* BufferedOutputTests.swift */, 89 | ); 90 | path = Output; 91 | sourceTree = ""; 92 | }; 93 | G_1203410589804 /* LogStore */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | FR_735717710124 /* FileLogStore.swift */, 97 | FR_818352195712 /* LogStore.swift */, 98 | ); 99 | path = LogStore; 100 | sourceTree = ""; 101 | }; 102 | G_2671118715628 /* PureeTests */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | FR_135157311129 /* Info.plist */, 106 | FR_167045655688 /* LogEntryTests.swift */, 107 | FR_657386344080 /* LoggerTests.swift */, 108 | G_7185892225853 /* LogStore */, 109 | 83935AC620F8920500FA8B9A /* Output */, 110 | FR_870064055253 /* TagPatternTests.swift */, 111 | G_6662760494323 /* Utilities */, 112 | ); 113 | name = PureeTests; 114 | path = Tests/PureeTests; 115 | sourceTree = ""; 116 | }; 117 | G_3356739590045 /* Utilities */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | FR_828954093903 /* DateProvider.swift */, 121 | ); 122 | path = Utilities; 123 | sourceTree = ""; 124 | }; 125 | G_3400314782399 /* Puree */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | FR_176166977065 /* Filter.swift */, 129 | FR_272688927660 /* Info.plist */, 130 | FR_912035359082 /* LogEntry.swift */, 131 | FR_775709644702 /* Logger.swift */, 132 | G_1203410589804 /* LogStore */, 133 | 83935AC520F88CA800FA8B9A /* Output */, 134 | FR_793364177672 /* Puree.h */, 135 | FR_644801390644 /* TagPattern.swift */, 136 | G_3356739590045 /* Utilities */, 137 | ); 138 | name = Puree; 139 | path = Sources/Puree; 140 | sourceTree = ""; 141 | }; 142 | G_6662760494323 /* Utilities */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | FR_613975198094 /* InMemoryLogStore.swift */, 146 | 83E0405E2105E68C00B92093 /* TestingBuffer.swift */, 147 | ); 148 | path = Utilities; 149 | sourceTree = ""; 150 | }; 151 | G_7185892225853 /* LogStore */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | FR_716897034030 /* FileLogStoreTests.swift */, 155 | ); 156 | path = LogStore; 157 | sourceTree = ""; 158 | }; 159 | G_8448771205358 = { 160 | isa = PBXGroup; 161 | children = ( 162 | G_8620238527590 /* Products */, 163 | G_3400314782399 /* Puree */, 164 | G_2671118715628 /* PureeTests */, 165 | ); 166 | sourceTree = ""; 167 | }; 168 | G_8620238527590 /* Products */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | FR_479946421258 /* Puree.framework */, 172 | FR_412093004181 /* PureeTests.xctest */, 173 | ); 174 | name = Products; 175 | sourceTree = ""; 176 | }; 177 | /* End PBXGroup section */ 178 | 179 | /* Begin PBXHeadersBuildPhase section */ 180 | HBP_47994642125 /* Headers */ = { 181 | isa = PBXHeadersBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | BF_278872148905 /* Puree.h in Headers */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | /* End PBXHeadersBuildPhase section */ 189 | 190 | /* Begin PBXNativeTarget section */ 191 | NT_412093004181 /* PureeTests */ = { 192 | isa = PBXNativeTarget; 193 | buildConfigurationList = CL_412093004181 /* Build configuration list for PBXNativeTarget "PureeTests" */; 194 | buildPhases = ( 195 | SBP_41209300418 /* Sources */, 196 | FBP_41209300418 /* Frameworks */, 197 | ); 198 | buildRules = ( 199 | ); 200 | dependencies = ( 201 | TD_533620328712 /* PBXTargetDependency */, 202 | ); 203 | name = PureeTests; 204 | productName = PureeTests; 205 | productReference = FR_412093004181 /* PureeTests.xctest */; 206 | productType = "com.apple.product-type.bundle.unit-test"; 207 | }; 208 | NT_479946421258 /* Puree */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = "CL_479946421258-1" /* Build configuration list for PBXNativeTarget "Puree" */; 211 | buildPhases = ( 212 | SBP_47994642125 /* Sources */, 213 | HBP_47994642125 /* Headers */, 214 | ); 215 | buildRules = ( 216 | ); 217 | dependencies = ( 218 | ); 219 | name = Puree; 220 | productName = Puree; 221 | productReference = FR_479946421258 /* Puree.framework */; 222 | productType = "com.apple.product-type.framework"; 223 | }; 224 | /* End PBXNativeTarget section */ 225 | 226 | /* Begin PBXProject section */ 227 | P_4799464212581 /* Project object */ = { 228 | isa = PBXProject; 229 | attributes = { 230 | LastUpgradeCheck = 0940; 231 | TargetAttributes = { 232 | NT_412093004181 = { 233 | LastSwiftMigration = 1020; 234 | }; 235 | NT_479946421258 = { 236 | LastSwiftMigration = 1020; 237 | }; 238 | }; 239 | }; 240 | buildConfigurationList = CL_479946421258 /* Build configuration list for PBXProject "Puree" */; 241 | compatibilityVersion = "Xcode 3.2"; 242 | developmentRegion = en; 243 | hasScannedForEncodings = 0; 244 | knownRegions = ( 245 | en, 246 | Base, 247 | ); 248 | mainGroup = G_8448771205358; 249 | projectDirPath = ""; 250 | projectRoot = ""; 251 | targets = ( 252 | NT_479946421258 /* Puree */, 253 | NT_412093004181 /* PureeTests */, 254 | ); 255 | }; 256 | /* End PBXProject section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | SBP_41209300418 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | BF_485048067586 /* BufferedOutputTests.swift in Sources */, 264 | BF_364947189323 /* FileLogStoreTests.swift in Sources */, 265 | BF_104851505209 /* InMemoryLogStore.swift in Sources */, 266 | BF_647931598633 /* LogEntryTests.swift in Sources */, 267 | 83E040602105E69F00B92093 /* TestingBuffer.swift in Sources */, 268 | BF_859683188258 /* LoggerTests.swift in Sources */, 269 | BF_595409884047 /* TagPatternTests.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | SBP_47994642125 /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | BF_950872890744 /* BufferedOutput.swift in Sources */, 278 | BF_884702636068 /* DateProvider.swift in Sources */, 279 | BF_415193818225 /* FileLogStore.swift in Sources */, 280 | BF_210674583290 /* Filter.swift in Sources */, 281 | BF_468687204183 /* LogEntry.swift in Sources */, 282 | BF_429021003566 /* LogStore.swift in Sources */, 283 | BF_625371294693 /* Logger.swift in Sources */, 284 | BF_180232540354 /* Output.swift in Sources */, 285 | BF_894429986524 /* TagPattern.swift in Sources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | /* End PBXSourcesBuildPhase section */ 290 | 291 | /* Begin PBXTargetDependency section */ 292 | TD_533620328712 /* PBXTargetDependency */ = { 293 | isa = PBXTargetDependency; 294 | target = NT_479946421258 /* Puree */; 295 | targetProxy = CIP_41209300418 /* PBXContainerItemProxy */; 296 | }; 297 | /* End PBXTargetDependency section */ 298 | 299 | /* Begin XCBuildConfiguration section */ 300 | BC_349952077328 /* Debug */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 304 | BUNDLE_LOADER = "$(TEST_HOST)"; 305 | INFOPLIST_FILE = Tests/PureeTests/Info.plist; 306 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 307 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 308 | SDKROOT = iphoneos; 309 | SWIFT_VERSION = 5.0; 310 | TARGETED_DEVICE_FAMILY = "1,2"; 311 | }; 312 | name = Debug; 313 | }; 314 | BC_386656661306 /* Debug */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | CODE_SIGN_IDENTITY = ""; 319 | CURRENT_PROJECT_VERSION = 1; 320 | DEFINES_MODULE = YES; 321 | DYLIB_COMPATIBILITY_VERSION = 1; 322 | DYLIB_CURRENT_VERSION = 1; 323 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 324 | ENABLE_TESTABILITY = YES; 325 | INFOPLIST_FILE = Sources/Puree/Info.plist; 326 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 327 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 328 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 329 | PRODUCT_BUNDLE_IDENTIFIER = com.cookpad.Puree; 330 | SDKROOT = iphoneos; 331 | SKIP_INSTALL = YES; 332 | SWIFT_VERSION = 5.0; 333 | TARGETED_DEVICE_FAMILY = "1,2"; 334 | VERSIONING_SYSTEM = "apple-generic"; 335 | }; 336 | name = Debug; 337 | }; 338 | BC_479945831424 /* Debug */ = { 339 | isa = XCBuildConfiguration; 340 | buildSettings = { 341 | ALWAYS_SEARCH_USER_PATHS = NO; 342 | CLANG_ANALYZER_NONNULL = YES; 343 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_MODULES = YES; 347 | CLANG_ENABLE_OBJC_ARC = YES; 348 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 349 | CLANG_WARN_BOOL_CONVERSION = YES; 350 | CLANG_WARN_COMMA = YES; 351 | CLANG_WARN_CONSTANT_CONVERSION = YES; 352 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 353 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 354 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 360 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 361 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 364 | CLANG_WARN_STRICT_PROTOTYPES = YES; 365 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 366 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | COPY_PHASE_STRIP = NO; 370 | DEBUG_INFORMATION_FORMAT = dwarf; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | ENABLE_TESTABILITY = YES; 373 | GCC_C_LANGUAGE_STANDARD = gnu11; 374 | GCC_DYNAMIC_NO_PIC = NO; 375 | GCC_NO_COMMON_BLOCKS = YES; 376 | GCC_OPTIMIZATION_LEVEL = 0; 377 | GCC_PREPROCESSOR_DEFINITIONS = ( 378 | "$(inherited)", 379 | "DEBUG=1", 380 | ); 381 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 382 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 383 | GCC_WARN_UNDECLARED_SELECTOR = YES; 384 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 385 | GCC_WARN_UNUSED_FUNCTION = YES; 386 | GCC_WARN_UNUSED_VARIABLE = YES; 387 | MTL_ENABLE_DEBUG_INFO = YES; 388 | ONLY_ACTIVE_ARCH = YES; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 391 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 392 | SWIFT_VERSION = 4.0; 393 | }; 394 | name = Debug; 395 | }; 396 | BC_591140202817 /* Release */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | CODE_SIGN_IDENTITY = ""; 401 | CURRENT_PROJECT_VERSION = 1; 402 | DEFINES_MODULE = YES; 403 | DYLIB_COMPATIBILITY_VERSION = 1; 404 | DYLIB_CURRENT_VERSION = 1; 405 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 406 | ENABLE_TESTABILITY = YES; 407 | INFOPLIST_FILE = Sources/Puree/Info.plist; 408 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 409 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 410 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 411 | PRODUCT_BUNDLE_IDENTIFIER = com.cookpad.Puree; 412 | SDKROOT = iphoneos; 413 | SKIP_INSTALL = YES; 414 | SWIFT_VERSION = 5.0; 415 | TARGETED_DEVICE_FAMILY = "1,2"; 416 | VERSIONING_SYSTEM = "apple-generic"; 417 | }; 418 | name = Release; 419 | }; 420 | BC_730065666902 /* Release */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 424 | BUNDLE_LOADER = "$(TEST_HOST)"; 425 | INFOPLIST_FILE = Tests/PureeTests/Info.plist; 426 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 427 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 428 | SDKROOT = iphoneos; 429 | SWIFT_VERSION = 5.0; 430 | TARGETED_DEVICE_FAMILY = "1,2"; 431 | }; 432 | name = Release; 433 | }; 434 | BC_881114754245 /* Release */ = { 435 | isa = XCBuildConfiguration; 436 | buildSettings = { 437 | ALWAYS_SEARCH_USER_PATHS = NO; 438 | CLANG_ANALYZER_NONNULL = YES; 439 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 440 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 441 | CLANG_CXX_LIBRARY = "libc++"; 442 | CLANG_ENABLE_MODULES = YES; 443 | CLANG_ENABLE_OBJC_ARC = YES; 444 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 445 | CLANG_WARN_BOOL_CONVERSION = YES; 446 | CLANG_WARN_COMMA = YES; 447 | CLANG_WARN_CONSTANT_CONVERSION = YES; 448 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 449 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 450 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 451 | CLANG_WARN_EMPTY_BODY = YES; 452 | CLANG_WARN_ENUM_CONVERSION = YES; 453 | CLANG_WARN_INFINITE_RECURSION = YES; 454 | CLANG_WARN_INT_CONVERSION = YES; 455 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 456 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 457 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 459 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 460 | CLANG_WARN_STRICT_PROTOTYPES = YES; 461 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 462 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 463 | CLANG_WARN_UNREACHABLE_CODE = YES; 464 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 465 | COPY_PHASE_STRIP = NO; 466 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 467 | ENABLE_NS_ASSERTIONS = NO; 468 | ENABLE_STRICT_OBJC_MSGSEND = YES; 469 | GCC_C_LANGUAGE_STANDARD = gnu11; 470 | GCC_NO_COMMON_BLOCKS = YES; 471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 473 | GCC_WARN_UNDECLARED_SELECTOR = YES; 474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 475 | GCC_WARN_UNUSED_FUNCTION = YES; 476 | GCC_WARN_UNUSED_VARIABLE = YES; 477 | PRODUCT_NAME = "$(TARGET_NAME)"; 478 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 479 | SWIFT_VERSION = 4.0; 480 | VALIDATE_PRODUCT = YES; 481 | }; 482 | name = Release; 483 | }; 484 | /* End XCBuildConfiguration section */ 485 | 486 | /* Begin XCConfigurationList section */ 487 | CL_412093004181 /* Build configuration list for PBXNativeTarget "PureeTests" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | BC_349952077328 /* Debug */, 491 | BC_730065666902 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = ""; 495 | }; 496 | CL_479946421258 /* Build configuration list for PBXProject "Puree" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | BC_479945831424 /* Debug */, 500 | BC_881114754245 /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Debug; 504 | }; 505 | "CL_479946421258-1" /* Build configuration list for PBXNativeTarget "Puree" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | BC_386656661306 /* Debug */, 509 | BC_591140202817 /* Release */, 510 | ); 511 | defaultConfigurationIsVisible = 0; 512 | defaultConfigurationName = ""; 513 | }; 514 | /* End XCConfigurationList section */ 515 | }; 516 | rootObject = P_4799464212581 /* Project object */; 517 | } 518 | --------------------------------------------------------------------------------