Skip to content

Commit

Permalink
1.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed May 5, 2024
1 parent 230f7bb commit d5f8245
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 26 deletions.
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"version" : "1.5.4"
}
},
{
"identity" : "swift-metrics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-metrics.git",
"state" : {
"revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1",
"version" : "2.4.1"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
Expand Down
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ var package = Package(
.library(name: "SwiftAPIClient", targets: ["SwiftAPIClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.3"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.0.0")
],
targets: [
.target(
name: "SwiftAPIClient",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Metrics", package: "swift-metrics"),
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
]
Expand Down
6 changes: 4 additions & 2 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ var package = Package(
.library(name: "SwiftAPIClient", targets: ["SwiftAPIClient"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.3"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.2"),
],
targets: [
Expand All @@ -26,6 +27,7 @@ var package = Package(
dependencies: [
.target(name: "SwiftAPIClientMacros"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Metrics", package: "swift-metrics"),
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "HTTPTypesFoundation", package: "swift-http-types"),
]
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [Table of Contents](#table-of-contents)
- [Main Goals of the Library](#main-goals-of-the-library)
- [Usage](#usage)
- [Macros](#macros)
- [What is `APIClient`](#what-is-apiclient)
- [Built-in `APIClient` Extensions](#built-in-apiclient-extensions)
- [Request building](#request-building)
Expand All @@ -17,9 +18,10 @@
- [Token refresher](#token-refresher)
- [Mocking](#mocking)
- [Logging](#logging)
- [Metrics](#metrics)
- [`APIClient.Configs`](#apiclientconfigs)
- [Configs Modifications Order](#configs-modifications-order)
- [Macros](#macros)
- [Macros](#macros-1)
- [Introducing `swift-api-client-addons`](#introducing-swift-api-client-addons)
- [Installation](#installation)
- [Author](#author)
Expand Down Expand Up @@ -76,6 +78,8 @@ try await johnClient.body(updatedUser).put()
try await johnClient.delete()
```

### Macros

Also, you can use macros for API declaration:
```swift
/// /pet
Expand Down Expand Up @@ -215,6 +219,15 @@ Content-Type: application/json
```
Log message format can be customized with the `.loggingComponents(_:)` modifier.

### Metrics
`swift-api-client` employs `swift-metrics` for metrics, with `.reportMetrics` configuration customizable via `.reportMetrics(_:)` modifier.

`swift-api-client` reports:
- `api_client_requests_total`: total requests count.
- `api_client_responses_total`: total responses count.
- `api_client_errors_total`: total errors count.
- `http_client_request_duration_seconds`: http requests duration.

## `APIClient.Configs`
A collection of config values is propagated through the modifier chain. These configs are accessible in all core methods: `modifyRequest`, `withRequest`, and `withConfigs`.

Expand Down
22 changes: 17 additions & 5 deletions Sources/SwiftAPIClient/APIClientCaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,25 @@ public extension APIClient {
let message = configs.loggingComponents.requestMessage(for: request, uuid: uuid, fileIDLine: fileIDLine)
configs.logger.log(level: configs.logLevel, "\(message)")
}

if let mock = try configs.getMockIfNeeded(for: Value.self, serializer: serializer) {
return try caller.mockResult(for: mock)
}
if let mock = try configs.getMockIfNeeded(for: Value.self, serializer: serializer) {
return try caller.mockResult(for: mock)
}
if configs.reportMetrics {
updateTotalRequestsMetrics(for: request)
}

return try caller.call(uuid: uuid, request: request, configs: configs) { response, validate in
do {
try validate()
return try serializer.serialize(response, configs)
let result = try serializer.serialize(response, configs)
if configs.reportMetrics {
updateTotalResponseMetrics(for: request, successful: true)
}
return result
} catch {
if configs.reportMetrics {
updateTotalResponseMetrics(for: request, successful: false)
}
if let data = response as? Data, let failure = configs.errorDecoder.decodeError(data, configs) {
try configs.errorHandler(failure, configs)
throw failure
Expand All @@ -217,6 +226,9 @@ public extension APIClient {
)
configs.logger.log(level: configs.logLevel, "\(message)")
}
if configs.reportMetrics {
updateTotalErrorsMetrics(for: nil)
}
try configs.errorHandler(error, configs)
}
throw error
Expand Down
29 changes: 19 additions & 10 deletions Sources/SwiftAPIClient/Clients/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,19 @@ extension APIClientCaller where Result == AsyncThrowingValue<Value> {
let start = Date()
do {
(value, response) = try await configs.httpClientMiddleware.execute(request: request, configs: configs, next: task)
} catch {
let duration = Date().timeIntervalSince(start)
if !configs.loggingComponents.isEmpty {
let message = configs.loggingComponents.errorMessage(
uuid: uuid,
error: error,
duration: duration
)
configs.logger.log(level: configs.logLevel, "\(message)")
}
} catch {
let duration = Date().timeIntervalSince(start)
if !configs.loggingComponents.isEmpty {
let message = configs.loggingComponents.errorMessage(
uuid: uuid,
error: error,
duration: duration
)
configs.logger.log(level: configs.logLevel, "\(message)")
}
if configs.reportMetrics {
updateHTTPMetrics(for: request, status: nil, duration: duration, successful: false)
}
throw error
}
let duration = Date().timeIntervalSince(start)
Expand All @@ -124,6 +127,9 @@ extension APIClientCaller where Result == AsyncThrowingValue<Value> {
)
configs.logger.log(level: configs.logLevel, "\(message)")
}
if configs.reportMetrics {
updateHTTPMetrics(for: request, status: response.status, duration: duration, successful: true)
}
return result
} catch {
if !configs.loggingComponents.isEmpty {
Expand All @@ -136,6 +142,9 @@ extension APIClientCaller where Result == AsyncThrowingValue<Value> {
)
configs.logger.log(level: configs.logLevel, "\(message)")
}
if configs.reportMetrics {
updateHTTPMetrics(for: request, status: response.status, duration: duration, successful: false)
}
throw error
}
}
Expand Down
20 changes: 20 additions & 0 deletions Sources/SwiftAPIClient/Modifiers/MetricsModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

public extension APIClient {

/// Whether to report metrics.
/// - Parameter reportMetrics: A boolean value indicating whether to report metrics.
/// - Returns: An instance of `APIClient` configured with the specified metrics reporting setting.
func reportMetrics(_ reportMetrics: Bool) -> APIClient {
configs(\.reportMetrics, reportMetrics)
}
}

public extension APIClient.Configs {

/// Whether to report metrics.
var reportMetrics: Bool {
get { self[\.reportMetrics] ?? true }
set { self[\.reportMetrics] = newValue }
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftAPIClient/Modifiers/RetryModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private struct RetryMiddleware: HTTPClientMiddleware {
}
throw error
}
if response.status.kind != .successful, needRetry() {
if response.status.kind.isError, needRetry() {
return try await retry()
}
return (data, response)
Expand Down
15 changes: 10 additions & 5 deletions Sources/SwiftAPIClient/Types/LoggingComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,16 @@ public extension LoggingComponents {
if contains(.uuid) {
message = "[\(uuid.uuidString)]\n" + message
}
if statusCode?.kind == .successful, error == nil {
message.append("")
} else {
message.append("🛑")
}
switch (statusCode?.kind, error) {
case (_, .some), (.serverError, _), (.clientError, _), (.invalid, _):
message.append("🛑")
case (.successful, _), (nil, nil):
message.append("")
case (.informational, _):
message.append("ℹ️")
case (.redirection, _):
message.append("🔀")
}
var isMultiline = false
if let statusCode, contains(.statusCode) {
message += " \(statusCode.code) \(statusCode.reasonPhrase)"
Expand Down
9 changes: 9 additions & 0 deletions Sources/SwiftAPIClient/Utils/Status+Ext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation
import HTTPTypes

extension HTTPResponse.Status.Kind {

var isError: Bool {
self == .clientError || self == .serverError || self == .invalid
}
}
60 changes: 60 additions & 0 deletions Sources/SwiftAPIClient/Utils/UpdateMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Metrics
import HTTPTypes
import Foundation

func updateTotalRequestsMetrics(
for request: HTTPRequestComponents
) {
Counter(
label: "api_client_requests_total",
dimensions: dimensions(for: request)
).increment()
}

func updateTotalResponseMetrics(
for request: HTTPRequestComponents,
successful: Bool
) {
Counter(
label: "api_client_responses_total",
dimensions: dimensions(for: request) + [("successful", successful.description)]
).increment()
if !successful {
updateTotalErrorsMetrics(for: request)
}
}

func updateTotalErrorsMetrics(
for request: HTTPRequestComponents?
) {
Counter(
label: "api_client_errors_total",
dimensions: dimensions(for: request)
).increment()
}

func updateHTTPMetrics(
for request: HTTPRequestComponents?,
status: HTTPResponse.Status?,
duration: Double,
successful: Bool
) {
var dimensions = dimensions(for: request)
dimensions.append(("status", status?.code.description ?? "undefined"))
dimensions.append(("successful", successful.description))
Timer(
label: "http_client_request_duration_seconds",
dimensions: dimensions,
preferredDisplayUnit: .seconds
)
.recordSeconds(duration)
}

private func dimensions(
for request: HTTPRequestComponents?
) -> [(String, String)] {
[
("method", request?.method.rawValue ?? "undefined"),
("path", request?.urlComponents.path ?? "undefined")
]
}

0 comments on commit d5f8245

Please sign in to comment.