├── 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 | 
4 |
5 | [](https://github.com/cookpad/Puree-Swift/actions)
6 | [](https://swift.org)
7 | [](https://github.com/Carthage/Carthage)
8 | [](http://cocoadocs.org/docsets/Puree)
9 | [](http://cocoadocs.org/docsets/Puree)
10 | [](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 | 
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 |
--------------------------------------------------------------------------------