diff --git a/README.md b/README.md index eef259d..db04d66 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # GoogleCloudLogging -Event logging for client applications on [Apple platforms](#supported-platforms) with support for offline work and automatic upload to [Google Cloud (GCP)](https://cloud.google.com). The package depends on [SwiftLog](https://github.com/apple/swift-log) - an official logging API for Swift, so it can be easly integrated into the project and combined with other logging backends. Log events are stored locally in the [JSON Lines](http://jsonlines.org) file format and bulk uploaded to GCP using the [Cloud Logging API v2](https://cloud.google.com/logging/docs/reference/v2/rest) at time intervals, upon defined event or explicit request. _And yes, it logs itself! (with recursion protection)_ 🤘 +Event logging for client applications on [Apple platforms](#supported-platforms) with support for offline work and automatic upload to [Google Cloud (GCP)](https://cloud.google.com). The package depends on [SwiftLog](https://github.com/apple/swift-log) - an official logging API for Swift, so it can be easly integrated into the project and combined with other logging backends. Log events are stored locally in the [JSON Lines](http://jsonlines.org) file format and bulk uploaded to GCP using the [Cloud Logging API v2](https://cloud.google.com/logging/docs/reference/v2/rest) at time intervals, upon defined event or explicit request. + +> And yes, it logs itself! (with recursion protection) 🤘 ## Rationale -Google recommended logging solution for client applications is Analytics framework, which is now part of the Firebase SDK. Here is a comparison of that framework and this library in terms of logging: +Google-recommended logging solution for client applications is the Analytics framework, which is now part of the Firebase SDK. Here is a comparison of their framework and this library in terms of logging: Library | FirebaseAnalytics | GoogleCloudLogging --- | --- | --- Platform | Mobile only. _Even Catalyst is not currently supported._ | All modern Apple's OSs. _It is essential for development of universal SwiftUI apps._ @@ -19,7 +21,9 @@ Logging | Proprietary logging functions and implicit usage tracking. | SwiftLog Open your application project in Xcode 11 or later, go to menu `File -> Swift Packages -> Add Package Dependency...` and paste the package repository URL `https://github.com/DnV1eX/GoogleCloudLogging.git`. ### Create Service Account -In your web browser open the [Google Cloud Console](https://console.cloud.google.com) and create a new project. In `IAM & Admin -> Service Accounts` create a service account choosing `Logging -> Logs Writer` role. In the last step, create and download private key choosing `JSON` format. You need to include this file in your application bundle, just drag the file into the Xcode project and tick the desired targets in the file inspector. +In your web browser, open the [Google Cloud Console](https://console.cloud.google.com) and create a new project. In `IAM & Admin -> Service Accounts` create a service account choosing `Logging -> Logs Writer` role. In the last step, create and download private key choosing `JSON` format. You need to include this file in your application bundle. + +> Just drag the file into the Xcode project and tick the desired targets in the file inspector. ### Setup Logging 1. Import both `SwiftLog` and `GoogleCloudLogging` modules: @@ -74,7 +78,7 @@ logger.error(/* Logged error message */, metadata: [LogKey.error: "\(error)"]) > `GoogleCloudLogHandler.globalMetadata` takes precedence over `Logger` metadata which in turn takes precedence over log message metadata in case of key overlapping. ### Analyze Logs -In your web browser open the [GCP Operations Logging](https://console.cloud.google.com/logs) and select your project. You will see a list of logs for a given **time range** which can be filtered by **log name** _(logger label)_, **severity** _(log level)_, **text payload** _(message)_, **labels** _(metadata)_ etc. **Resource type** for logs produced by GoogleCloudLogHandler is always _Global_. +In your web browser, open the [GCP Operations Logging](https://console.cloud.google.com/logs) and select your project. You will see a list of logs for a given **time range** which can be filtered by **log name** _(logger label)_, **severity** _(log level)_, **text payload** _(message)_, **labels** _(metadata)_ etc. **Resource type** for logs produced by GoogleCloudLogHandler is always _Global_. > You can switch to the new Logs Viewer Preview that introduces new features, such as advanced log queries and histograms. diff --git a/Sources/GoogleCloudLogging/GoogleCloudLogging.swift b/Sources/GoogleCloudLogging/GoogleCloudLogging.swift index 702eeb7..0261a2f 100644 --- a/Sources/GoogleCloudLogging/GoogleCloudLogging.swift +++ b/Sources/GoogleCloudLogging/GoogleCloudLogging.swift @@ -308,7 +308,7 @@ class GoogleCloudLogging { request.setValue("application/json", forHTTPHeaderField: "Content-Type") do { let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601WithFractionalSeconds + encoder.dateEncodingStrategy = .iso8601WithNanoseconds let entries: [Log.Entry] = entries.map { var entry = $0 entry.logName = Log.name(projectId: self.serviceAccountCredentials.projectId, logId: $0.logName) @@ -342,13 +342,25 @@ extension Data { +extension ISO8601DateFormatter { + + static func internetDateTimeWithNanosecondsString(from date: Date, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) -> String { + var string = ISO8601DateFormatter.string(from: date, timeZone: timeZone, formatOptions: .withInternetDateTime) + var timeInterval = date.timeIntervalSinceReferenceDate + if timeInterval < 0 { + timeInterval += (-timeInterval * 2).rounded(.up) + } + string.insert(contentsOf: "\(timeInterval)".drop { $0 != "." }.prefix(10), at: string.index(string.startIndex, offsetBy: 19)) + return string + } +} + + extension JSONEncoder.DateEncodingStrategy { - static let iso8601WithFractionalSeconds = custom { date, encoder in - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + static let iso8601WithNanoseconds = custom { date, encoder in var container = encoder.singleValueContainer() - try container.encode(dateFormatter.string(from: date)) + try container.encode(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: date)) } } @@ -369,7 +381,6 @@ extension CharacterSet { } - extension String { func safeLogId() -> String { diff --git a/Tests/GoogleCloudLoggingTests/GoogleCloudLoggingTests.swift b/Tests/GoogleCloudLoggingTests/GoogleCloudLoggingTests.swift index bb3d155..89d702d 100644 --- a/Tests/GoogleCloudLoggingTests/GoogleCloudLoggingTests.swift +++ b/Tests/GoogleCloudLoggingTests/GoogleCloudLoggingTests.swift @@ -81,6 +81,16 @@ final class GoogleCloudLoggingTests: XCTestCase { } + func testISO8601DateFormatterNanoseconds() { + + XCTAssertEqual(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: Date(timeIntervalSinceReferenceDate: 615695580)), "2020-07-06T02:33:00.0Z") + XCTAssertEqual(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: Date(timeIntervalSinceReferenceDate: 615695580.235942)), "2020-07-06T02:33:00.235942Z") + XCTAssertEqual(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: Date(timeIntervalSinceReferenceDate: 615695580.987654321)), "2020-07-06T02:33:00.9876543Z") + XCTAssertEqual(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: Date(timeIntervalSinceReferenceDate: 0.987654321)), "2001-01-01T00:00:00.987654321Z") + XCTAssertEqual(ISO8601DateFormatter.internetDateTimeWithNanosecondsString(from: Date(timeIntervalSinceReferenceDate: -0.9876543211)), "2000-12-31T23:59:59.012345678Z") + } + + func testSafeLogId() { XCTAssertEqual("My_class-1.swift".safeLogId(), "My_class-1.swift") @@ -97,14 +107,4 @@ final class GoogleCloudLoggingTests: XCTestCase { logger.critical("LoggerMessage", metadata: ["MessageMetadataKey": "MessageMetadataValue"]) Thread.sleep(forTimeInterval: 3) } - - - static var allTests = [ - ("testTokenRequest", testTokenRequest), - ("testEntriesWrite", testEntriesWrite), - ("testLogHandler", testLogHandler), - ("testDictionaryUpdate", testDictionaryUpdate), - ("testSafeLogId", testSafeLogId), - ("testGoogleCloudLogHandler", testGoogleCloudLogHandler), - ] } diff --git a/Tests/GoogleCloudLoggingTests/XCTestManifests.swift b/Tests/GoogleCloudLoggingTests/XCTestManifests.swift deleted file mode 100644 index 235cf8c..0000000 --- a/Tests/GoogleCloudLoggingTests/XCTestManifests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest - -#if !canImport(ObjectiveC) -public func allTests() -> [XCTestCaseEntry] { - return [ - testCase(GoogleCloudLoggingTests.allTests), - ] -} -#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 2dabbaf..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import GoogleCloudLoggingTests - -var tests = [XCTestCaseEntry]() -tests += GoogleCloudLoggingTests.allTests() -XCTMain(tests)