Skip to content

feat: add logging HTTP requests and responses #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
51 changes: 26 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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).
Expand Down
15 changes: 14 additions & 1 deletion Sources/InfluxDBSwift/InfluxDBClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)"
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
71 changes: 71 additions & 0 deletions Sources/InfluxDBSwift/Internal/LoggingHelper.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,15 @@ internal class URLSessionRequestBuilder<T>: RequestBuilder<T> {

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 {

Expand Down
59 changes: 59 additions & 0 deletions Tests/InfluxDBSwiftTests/InfluxDBClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Logging

@testable import InfluxDBSwift
import XCTest
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}