From 149e20c39d55e3013802511080155027de2fc064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Bedn=C3=A1=C5=99?= Date: Thu, 14 Jul 2022 15:15:00 +0200 Subject: [PATCH] feat: add logging HTTP requests and responses (#52) --- CHANGELOG.md | 3 + Package.swift | 7 +- README.md | 51 ++++++------- Sources/InfluxDBSwift/InfluxDBClient.swift | 15 +++- .../Internal/LoggingHelper.swift | 71 +++++++++++++++++++ .../Generated/URLSessionImplementations.swift | 7 +- .../InfluxDBClientTests.swift | 59 +++++++++++++++ 7 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 Sources/InfluxDBSwift/Internal/LoggingHelper.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 32091ac2..5d292e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.3.0 [unreleased] +### Features +1. [#52](https://github.com/influxdata/influxdb-client-swift/pull/52): Add logging for HTTP requests + ## 1.2.0 [2022-05-20] ### Features diff --git a/Package.swift b/Package.swift index 9c959038..06fc840c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,11 +14,16 @@ let package = Package( .package(name: "Gzip", url: "https://github.com/1024jp/GzipSwift", from: "5.1.1"), .package(name: "CSV.swift", url: "https://github.com/yaslab/CSV.swift", from: "2.4.2"), .package(name: "SwiftTestReporter", url: "https://github.com/allegro/swift-junit.git", from: "2.0.0"), + .package(name: "swift-log", url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target(name: "InfluxDBSwift", dependencies: ["Gzip", .product(name: "CSV", package: "CSV.swift")]), + .target(name: "InfluxDBSwift", dependencies: [ + "Gzip", + .product(name: "CSV", package: "CSV.swift"), + .product(name: "Logging", package: "swift-log") + ]), .target(name: "InfluxDBSwiftApis", dependencies: ["InfluxDBSwift"]), .testTarget(name: "InfluxDBSwiftTests", dependencies: ["InfluxDBSwift", "SwiftTestReporter"]), .testTarget(name: "InfluxDBSwiftApisTests", dependencies: ["InfluxDBSwiftApis", "SwiftTestReporter"]), diff --git a/README.md b/README.md index 86f50fd6..e6a078bb 100644 --- a/README.md +++ b/README.md @@ -106,14 +106,15 @@ client.close() #### Client Options -| Option | Description | Type | Default | -|---|---|---|---| -| bucket | Default destination bucket for writes | String | none | -| org | Default organization bucket for writes | String | none | -| precision | Default precision for the unix timestamps within the body line-protocol | TimestampPrecision | ns | -| timeoutIntervalForRequest | The timeout interval to use when waiting for additional data. | TimeInterval | 60 sec | -| timeoutIntervalForResource | The maximum amount of time that a resource request should be allowed to take. | TimeInterval | 5 min | -| enableGzip | Enable Gzip compression for HTTP requests. | Bool | false | +| Option | Description | Type | Default | +|----------------------------|-------------------------------------------------------------------------------|--------------------|---------| +| bucket | Default destination bucket for writes | String | none | +| org | Default organization bucket for writes | String | none | +| precision | Default precision for the unix timestamps within the body line-protocol | TimestampPrecision | ns | +| timeoutIntervalForRequest | The timeout interval to use when waiting for additional data. | TimeInterval | 60 sec | +| timeoutIntervalForResource | The maximum amount of time that a resource request should be allowed to take. | TimeInterval | 5 min | +| enableGzip | Enable Gzip compression for HTTP requests. | Bool | false | +| debugging | Enable debugging for HTTP request/response. | Bool | false | ##### Configure default `Bucket`, `Organization` and `Precision` @@ -589,23 +590,23 @@ DeleteData.main() The client supports following management API: -| | API docs | -| --- |---------------------------------------------------------------------| -| [**AuthorizationsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/AuthorizationsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Authorizations | -| [**BucketsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/BucketsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Buckets | -| [**DBRPsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/DBRPsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/DBRPs | -| [**HealthAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/HealthAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Health | -| [**PingAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/PingAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Ping | -| [**LabelsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/LabelsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Labels | -| [**OrganizationsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/OrganizationsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Organizations | -| [**ReadyAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/ReadyAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Ready | -| [**ScraperTargetsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/ScraperTargetsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/ScraperTargets | -| [**SecretsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SecretsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Secrets | -| [**SetupAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SetupAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Tasks | -| [**SourcesAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SourcesAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Sources | -| [**TasksAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/TasksAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Tasks | -| [**UsersAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/UsersAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Users | -| [**VariablesAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/VariablesAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Variables | +| | API docs | +|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| [**AuthorizationsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/AuthorizationsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Authorizations | +| [**BucketsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/BucketsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Buckets | +| [**DBRPsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/DBRPsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/DBRPs | +| [**HealthAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/HealthAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Health | +| [**PingAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/PingAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Ping | +| [**LabelsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/LabelsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Labels | +| [**OrganizationsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/OrganizationsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Organizations | +| [**ReadyAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/ReadyAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Ready | +| [**ScraperTargetsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/ScraperTargetsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/ScraperTargets | +| [**SecretsAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SecretsAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Secrets | +| [**SetupAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SetupAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Tasks | +| [**SourcesAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/SourcesAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Sources | +| [**TasksAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/TasksAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Tasks | +| [**UsersAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/UsersAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Users | +| [**VariablesAPI**](https://influxdata.github.io/influxdb-client-swift/Classes/InfluxDB2API/VariablesAPI.html) | https://docs.influxdata.com/influxdb/latest/api/#tag/Variables | The following example demonstrates how to use a InfluxDB 2.0 Management API to create new bucket. For further information see docs and [examples](/Examples). diff --git a/Sources/InfluxDBSwift/InfluxDBClient.swift b/Sources/InfluxDBSwift/InfluxDBClient.swift index c71b4a0a..34006d35 100644 --- a/Sources/InfluxDBSwift/InfluxDBClient.swift +++ b/Sources/InfluxDBSwift/InfluxDBClient.swift @@ -31,6 +31,8 @@ public class InfluxDBClient { internal let token: String /// The options to configure client. internal let options: InfluxDBOptions + /// Enable debugging for HTTP request/response. + public let debugging: Bool /// Shared URLSession across the client. public let session: URLSession @@ -55,14 +57,20 @@ public class InfluxDBClient { /// - url: InfluxDB host and port. /// - token: Authentication token. /// - options: optional `InfluxDBOptions` to use for this client. + /// - debugging: optional Enable debugging for HTTP request/response. Default `false`. /// - protocolClasses: optional array of extra protocol subclasses that handle requests. /// /// - SeeAlso: https://docs.influxdata.com/influxdb/latest/reference/urls/#influxdb-oss-urls /// - SeeAlso: https://docs.influxdata.com/influxdb/latest/security/tokens/ - public init(url: String, token: String, options: InfluxDBOptions? = nil, protocolClasses: [AnyClass]? = nil) { + public init(url: String, + token: String, + options: InfluxDBOptions? = nil, + debugging: Bool? = nil, + protocolClasses: [AnyClass]? = nil) { self.url = url.hasSuffix("/") ? String(url.dropLast(1)) : url self.token = token self.options = options ?? InfluxDBClient.InfluxDBOptions() + self.debugging = debugging ?? false var headers: [AnyHashable: Any] = [:] headers["Authorization"] = "Token \(token)" @@ -309,6 +317,9 @@ extension InfluxDBClient { request.setValue("\(value)", forHTTPHeaderField: "\(key)") } + let logger = InfluxDBClient.HTTPLogger(debugging: debugging) + logger.log(request) + let task = session.dataTask(with: request) { data, response, error in responseQueue.async { if let error = error { @@ -329,6 +340,8 @@ extension InfluxDBClient { return } + logger.log(httpResponse, data) + guard Array(200..<300).contains(httpResponse.statusCode) else { completion(.failure(InfluxDBClient.InfluxDBError.error( httpResponse.statusCode, diff --git a/Sources/InfluxDBSwift/Internal/LoggingHelper.swift b/Sources/InfluxDBSwift/Internal/LoggingHelper.swift new file mode 100644 index 00000000..2c381af0 --- /dev/null +++ b/Sources/InfluxDBSwift/Internal/LoggingHelper.swift @@ -0,0 +1,71 @@ +// +// Created by Jakub Bednář on 13.07.2022. +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Logging + +extension InfluxDBClient { + /// The logger for logging HTTP request/response. + public class HTTPLogger { + fileprivate var logger: Logger { + var logger = Logger(label: "http-logger") + logger.logLevel = .debug + return logger + } + /// Enable debugging for HTTP request/response. + internal let debugging: Bool + + /// Create a new HTTPLogger. + /// + /// - Parameters: + /// - debugging: optional Enable debugging for HTTP request/response. Default `false`. + public init(debugging: Bool? = nil) { + self.debugging = debugging ?? false + } + + /// Log the HTTP request. + /// + /// - Parameter request: to log + public func log(_ request: URLRequest?) { + if debugging { + logger.debug(">>> Request: '\(request?.httpMethod ?? "") \(request?.url?.absoluteString ?? "")'") + log_headers(headers: request?.allHTTPHeaderFields, prefix: ">>>") + log_body(body: request?.httpBody, prefix: ">>>") + } + } + + /// Log the HTTP response. + /// + /// - Parameters: + /// - response: to log + /// - data: response data + public func log(_ response: URLResponse?, _ data: Data?) { + if debugging { + let httpResponse = response as? HTTPURLResponse + logger.debug("<<< Response: \(httpResponse?.statusCode ?? 0)") + log_headers(headers: httpResponse?.allHeaderFields, prefix: "<<<") + log_body(body: data, prefix: "<<<") + } + } + + func log_body(body: Data?, prefix: String) { + if let body = body { + logger.debug("\(prefix) Body: \(String(decoding: body, as: UTF8.self))") + } + } + + func log_headers(headers: [AnyHashable: Any]?, prefix: String) { + headers?.forEach { key, value in + var sanitized = value + if "authorization" == String(describing: key).lowercased() { + sanitized = "***" + } + logger.debug("\(prefix) \(key): \(sanitized)") + } + } + } +} diff --git a/Sources/InfluxDBSwiftApis/Generated/URLSessionImplementations.swift b/Sources/InfluxDBSwiftApis/Generated/URLSessionImplementations.swift index 5996b6cb..0fe39631 100644 --- a/Sources/InfluxDBSwiftApis/Generated/URLSessionImplementations.swift +++ b/Sources/InfluxDBSwiftApis/Generated/URLSessionImplementations.swift @@ -109,10 +109,15 @@ internal class URLSessionRequestBuilder: RequestBuilder { do { let request = try createURLRequest(urlSession: urlSession, method: xMethod, encoding: encoding, headers: headers) - + + let logger = InfluxDBClient.HTTPLogger(debugging: influxDB2API.client.debugging) + logger.log(request) + let dataTask = urlSession.dataTask(with: request) { [weak self] data, response, error in guard let self = self else { return } + + logger.log(response, data) if let taskCompletionShouldRetry = self.taskCompletionShouldRetry { diff --git a/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift b/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift index 14c2d3c1..7365404f 100644 --- a/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift +++ b/Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift @@ -6,6 +6,7 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +import Logging @testable import InfluxDBSwift import XCTest @@ -175,6 +176,33 @@ final class InfluxDBClientTests: XCTestCase { waitForExpectations(timeout: 1, handler: nil) } + + func testHTTPLogging() { + TestLogHandler.content = "" + let expectation = self.expectation(description: "Success response from API doesn't arrive") + LoggingSystem.bootstrap(TestLogHandler.init) + + client = InfluxDBClient(url: Self.dbURL(), token: "my-token", debugging: true) + + MockURLProtocol.handler = { _, _ in + expectation.fulfill() + + let response = HTTPURLResponse(statusCode: 200) + return (response, "csv".data(using: .utf8)!) + } + + client.queryAPI.query(query: "from(bucket:\"my-bucket\") |> range(start: -1h)", org: "my-org") { _, error in + if let error = error { + XCTFail("Error occurs: \(error)") + } + + expectation.fulfill() + } + + waitForExpectations(timeout: 1, handler: nil) + + XCTAssertTrue(TestLogHandler.content.contains("Authorization: ***"), TestLogHandler.content) + } } final class InfluxDBErrorTests: XCTestCase { @@ -199,3 +227,34 @@ extension XCTestCase { return "http://localhost:8086" } } + +internal class TestLogHandler: LogHandler { + var metadata = Logger.Metadata() + var logLevel = Logger.Level.debug + static var content = "" + + init(label: String) { + } + + subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { + metadata[metadataKey] + } + set { + metadata[metadataKey] = newValue + } + } + + // swiftlint:disable function_parameter_count + func log(level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt) { + Self.content.append(message.description) + Self.content.append("\n") + } + // swiftlint:enable function_parameter_count +}