Skip to content

Commit

Permalink
Facilitate additional auth headers to be included within the metrics …
Browse files Browse the repository at this point in the history
…request (#44)

* Added support for authorizationHeader

* Expanded example of auth header usage

* Changed auth header to support multiple aditional headers

* Added additional docs for auth key and value
  • Loading branch information
dlbuckley authored May 18, 2021
1 parent 14a5d12 commit 1317797
Show file tree
Hide file tree
Showing 13 changed files with 87 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum ControllerFactory {
buildDirectory: command.buildDirectory,
projectName: command.projectName,
serviceURL: serviceURL,
additionalHeaders: command.additionalHeaders,
timeout: command.timeout,
isCI: command.isCI,
plugins: plugins,
Expand Down
15 changes: 10 additions & 5 deletions Sources/XCMetricsClient/Mobius/Domain/MetricsUploaderLogic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,14 @@ enum MetricsUploaderLogic {
})

if !uploadRequests.isEmpty {
effects.append(.uploadLogs(serviceURL: model.serviceURL,
projectName: model.projectName,
isCI: model.isCI,
skipNotes: model.skipNotes,
logs: uploadRequests))
effects.append(.uploadLogs(
serviceURL: model.serviceURL,
additionalHeaders: model.additionalHeaders,
projectName: model.projectName,
isCI: model.isCI,
skipNotes: model.skipNotes,
logs: uploadRequests
))
}
let updatedModel = model.withChanged(
parsedRequests: model.parsedRequests.union(cachedUploadRequest.prefix(maximumNumberOfParsedRequestsToSend)),
Expand All @@ -96,6 +99,7 @@ enum MetricsUploaderLogic {
if effects.isEmpty {
return .next(updatedModel, effects: [.uploadLogs(
serviceURL: model.serviceURL,
additionalHeaders: model.additionalHeaders,
projectName: model.projectName,
isCI: model.isCI,
skipNotes: model.skipNotes,
Expand All @@ -114,6 +118,7 @@ enum MetricsUploaderLogic {
return .next(updatedModel, effects: [
.uploadLogs(
serviceURL: model.serviceURL,
additionalHeaders: model.additionalHeaders,
projectName: model.projectName,
isCI: model.isCI,
skipNotes: model.skipNotes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct MetricsUploaderModel: Equatable, Hashable {
let projectName: String
/// The URL of the service where to send metrics to.
let serviceURL: URL
/// Additional headers to be sent with the request.
let additionalHeaders: [String: String]
/// The amount of seconds to wait for the Xcode's log to appear.
let timeout: Int
/// Whether or not this build was performed in a continuous integration environment.
Expand All @@ -47,6 +49,7 @@ struct MetricsUploaderModel: Equatable, Hashable {
buildDirectory: String,
projectName: String,
serviceURL: URL,
additionalHeaders: [String: String],
timeout: Int,
isCI: Bool,
plugins: [XCMetricsPlugin],
Expand All @@ -57,6 +60,7 @@ struct MetricsUploaderModel: Equatable, Hashable {
self.buildDirectory = buildDirectory
self.projectName = projectName
self.serviceURL = serviceURL
self.additionalHeaders = additionalHeaders
self.plugins = plugins
self.timeout = timeout
self.isCI = isCI
Expand All @@ -69,6 +73,7 @@ struct MetricsUploaderModel: Equatable, Hashable {
self.buildDirectory = ""
self.projectName = ""
self.serviceURL = URL(string: "")!
self.additionalHeaders = [:]
self.plugins = []
self.timeout = 0
self.isCI = false
Expand Down Expand Up @@ -154,7 +159,7 @@ enum MetricsUploaderEffect: Hashable {
/// Executes the custom plugins configured if any to add more data to the build.
case executePlugins(request: MetricsUploadRequest, plugins: [XCMetricsPlugin])
/// Uploads the given log upload requests to the specified backend service.
case uploadLogs(serviceURL: URL, projectName: String, isCI: Bool, skipNotes: Bool, logs: Set<MetricsUploadRequest>)
case uploadLogs(serviceURL: URL, additionalHeaders: [String: String], projectName: String, isCI: Bool, skipNotes: Bool, logs: Set<MetricsUploadRequest>)
/// Uploaded logs should be renamed to signal their status and differentiate them from logs yet to be uploaded.
case tagLogsAsUploaded(logs: Set<URL>)
/// Logs failed to upload are saved to disk in order to preserve the metadata collected (the actual xcactivitylog is always kept on disk for 7 days).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ struct UploadMetricsEffectHandler: EffectHandler {
self.metricsPublisher = metricsPublisher
}

func handle(_ effectParameters: (serviceURL: URL, projectName: String, isCI: Bool, skipNotes: Bool,
logs: Set<MetricsUploadRequest>), _ callback: EffectCallback<MetricsUploaderEvent>) -> Disposable {
func handle(_ effectParameters: (serviceURL: URL, additionalHeaders: [String: String], projectName: String, isCI: Bool, skipNotes: Bool, logs: Set<MetricsUploadRequest>), _ callback: EffectCallback<MetricsUploaderEvent>) -> Disposable {
log("Started uploading metrics.")
metricsPublisher.uploadMetrics(
serviceURL: effectParameters.serviceURL,
additionalHeaders: effectParameters.additionalHeaders,
projectName: effectParameters.projectName,
isCI: effectParameters.isCI,
skipNotes: effectParameters.skipNotes,
Expand Down
6 changes: 4 additions & 2 deletions Sources/XCMetricsClient/Network/MetricsPublisherService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ import XCMetricsProto
/// Defines the required methods for a publisher service.
protocol MetricsPublisherService {
/// Upload the given metrics and returns the result in a completion block.
/// - Parameter serviceURL: The URL of the backend service where the metrics wil be sent.
/// - Parameter serviceURL: The URL of the backend service where the metrics will be sent.
/// - Parameter additionalHeaders: Additional headers to be sent with the request.
/// - Parameter uploadRequests: The upload requests to be sent to the backend service.
/// - Parameter completion: The result is successfull if no error occurred. The .success enum case contains the URLs of the uploaded metrics.
/// - Parameter completion: The result is successful if no error occurred. The .success enum case contains the URLs of the uploaded metrics.
/// - Parameter projectName: The name of the project
/// - Parameter isCI: Boolean. If XCMetrics is running in CI or note
/// - Parameter skipNotes: Boolean. If the Notes found in the log won't be inserted in the database
func uploadMetrics(
serviceURL: URL,
additionalHeaders: [String: String],
projectName: String,
isCI: Bool,
skipNotes: Bool,
Expand Down
19 changes: 12 additions & 7 deletions Sources/XCMetricsClient/Network/MetricsPublisherServiceHTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class MetricsPublisherServiceHTTP: MetricsPublisherService {

func uploadMetrics(
serviceURL: URL,
additionalHeaders: [String: String],
projectName: String,
isCI: Bool,
skipNotes: Bool,
Expand All @@ -50,7 +51,8 @@ public class MetricsPublisherServiceHTTP: MetricsPublisherService {
for uploadRequest in uploadRequests {
self.dispatchGroup.enter()

self.uploadLog(uploadRequest, to: serviceURL, projectName: projectName, isCI: isCI, skipNotes: skipNotes) { (result: Result<Void, LogUploadError>) in
self.uploadLog(uploadRequest, to: serviceURL, additionalHeaders: additionalHeaders, projectName: projectName, isCI: isCI, skipNotes: skipNotes) { (result: Result<Void, LogUploadError>) in

switch result {
case .success:
successfulURLsLock.lock()
Expand Down Expand Up @@ -80,6 +82,7 @@ public class MetricsPublisherServiceHTTP: MetricsPublisherService {
private func uploadLog(
_ uploadRequest: MetricsUploadRequest,
to requestUrl: URL,
additionalHeaders: [String: String],
projectName: String,
isCI: Bool,
skipNotes: Bool,
Expand All @@ -89,12 +92,14 @@ public class MetricsPublisherServiceHTTP: MetricsPublisherService {
/// based on its configuration
let machineName = HashedMacOSMachineNameReader(encrypted: false).machineName ?? "none"
do {
let request = try MultipartRequestBuilder(request: uploadRequest,
url: requestUrl,
machineName: machineName,
projectName: projectName,
isCI: isCI,
skipNotes: skipNotes).build()
let request = try MultipartRequestBuilder(
request: uploadRequest,
url: requestUrl,
additionalHeaders: additionalHeaders,
machineName: machineName,
projectName: projectName,
isCI: isCI,
skipNotes: skipNotes).build()

getURLSession().dataTask(with: request) { (data, response, error) in
defer {
Expand Down
4 changes: 4 additions & 0 deletions Sources/XCMetricsClient/Network/MultipartRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,22 @@ class MultipartRequestBuilder {

public let request: MetricsUploadRequest
public let url: URL
public let additionalHeaders: [String: String]
public let machineName: String
public let projectName: String
public let isCI: Bool
public let skipNotes: Bool

public init(request: MetricsUploadRequest,
url: URL,
additionalHeaders: [String: String],
machineName: String,
projectName: String,
isCI: Bool,
skipNotes: Bool) {
self.request = request
self.url = url
self.additionalHeaders = additionalHeaders
self.machineName = machineName
self.projectName = projectName
self.isCI = isCI
Expand All @@ -52,6 +55,7 @@ class MultipartRequestBuilder {
let boundary = "Boundary-\(uuid)"
var request = URLRequest(url: url)
request.httpMethod = "PUT"
additionalHeaders.forEach { request.addValue($1, forHTTPHeaderField: $0) }
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// If this is a retry for a previously failed request, simply set the body. Otherwise compute it.
let body: Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extension MetricsUploaderModel {
buildDirectory: self.buildDirectory,
projectName: self.projectName,
serviceURL: self.serviceURL,
additionalHeaders: self.additionalHeaders,
timeout: self.timeout,
isCI: self.isCI,
plugins: self.plugins,
Expand Down
28 changes: 27 additions & 1 deletion Sources/XCMetricsClient/XCMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct Command {
let serviceURL: String
let isCI: Bool
let skipNotes: Bool
let additionalHeaders: [String: String]
}


Expand Down Expand Up @@ -109,6 +110,14 @@ public struct XCMetrics: ParsableCommand {
@Option(name: [.customLong("skipNotes")], help: "Notes found in logs won't be processed")
public var skipNotes: Bool = false

/// An optional authorization/token header **key** to be included in the upload request. Must be used in conjunction with `authorizationValue.`
@Option(name: [.customLong("authorizationKey"), .customShort("k")], help: "An optional authorization header key to be included in the upload request e.g 'Authorization' or 'x-api-key' etc. Must be used in conjunction with `authorizationValue`")
public var authorizationKey: String?

/// An optional authorization/token header **value** to be included in the upload request. Must be used in conjunction with `authorizationKey.`
@Option(name: [.customLong("authorizationValue"), .customShort("a")], help: "An optional authorization header value to be included in the upload request e.g 'Basic YWxhZGRpbjpvcGVuc2VzYW1l' or `hYDqG78OIUDIWKLdwjdwhdu8` etc. Must be used in conjunction with `authorizationKey`")
public var authorizationValue: String?

private static let loop = XCMetricsLoop()

/// The default initializer for the `XCMetrics` object.
Expand Down Expand Up @@ -139,6 +148,7 @@ public struct XCMetrics: ParsableCommand {
The --timeout argument is optional and defaults to 5 seconds.
The --isCI argument is optional and defaults to false.
The --skipNotes argument is optional and defaults to false.
The --authorizationKey must be used in conjunction with --authorizationValue. One cannot be used without the other.
Type 'XCMetrics --help' for more information.
""")
}
Expand Down Expand Up @@ -168,13 +178,29 @@ public struct XCMetrics: ParsableCommand {
throw argumentError()
}

var authorizationKey = ""
var authorizationValue = ""

switch (self.authorizationKey, self.authorizationValue) {
case (let .some(authKey), let .some(authValue)):
authorizationKey = authKey
authorizationValue = authValue
case (.none, .none):
break
default:
throw argumentError()
}

let command = Command(
buildDirectory: directoryBuild,
projectName: name,
timeout: timeout,
serviceURL: serviceURLValue,
isCI: isCI,
skipNotes: skipNotes
skipNotes: skipNotes,
additionalHeaders: [
authorizationKey: authorizationValue
]
)
return command
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class MockMetricsPublisher: MetricsPublisherService {

func uploadMetrics(
serviceURL: URL,
additionalHeaders: [String : String],
projectName: String,
isCI: Bool,
skipNotes: Bool,
Expand Down Expand Up @@ -73,8 +74,7 @@ final class UploadMetricsEffectHandlerTests: XCTestCase {
XCTFail("Expected .logsUploaded or , got: \(event)")
}
}
_ = effectHandler.handle((serviceURL: serviceURL, projectName: projectName, isCI: false, skipNotes: false, logs: uploadRequests),
effectCallback)
_ = effectHandler.handle((serviceURL: serviceURL, additionalHeaders: additionalHeaders, projectName: projectName, isCI: false, skipNotes: false, logs: uploadRequests), effectCallback)
XCTAssertTrue(effectCallback.ended)
}
}
4 changes: 4 additions & 0 deletions Tests/XCMetricsTests/MetricsUploaderLogicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ extension TemporaryFile: Hashable {

let projectName = "Project Name"
let serviceURL = URL(string: "https://example.com/v1/metrics")!
let additionalHeaders = ["key": "value"]

class MetricsUploaderLogicTests: XCTestCase {

private let spec = UpdateSpec(MetricsUploaderLogic.update)
private let initial = MetricsUploaderModel(buildDirectory: "BUILD_DIR",
projectName: projectName,
serviceURL: serviceURL,
additionalHeaders: additionalHeaders,
timeout: 1,
isCI: false,
plugins: [],
Expand Down Expand Up @@ -82,6 +84,7 @@ class MetricsUploaderLogicTests: XCTestCase {
hasEffects([
.uploadLogs(
serviceURL: serviceURL,
additionalHeaders: additionalHeaders,
projectName: projectName,
isCI: false,
skipNotes: false,
Expand Down Expand Up @@ -109,6 +112,7 @@ class MetricsUploaderLogicTests: XCTestCase {
),
.uploadLogs(
serviceURL: serviceURL,
additionalHeaders: additionalHeaders,
projectName: projectName,
isCI: false,
skipNotes: false,
Expand Down
12 changes: 10 additions & 2 deletions Tests/XCMetricsTests/XCMetricsUploaderArgumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ class XCMetricsArgumentTests: XCTestCase {
"--buildDir",
"/Users/Username/Library/Developer/Xcode/DerivedData/test/",
"--timeout",
"10"
"10",
"--authorizationKey",
"Authorization",
"--authorizationValue",
"Bearer XXXXXXXXXXXXXXX"
]))

XCTAssertNoThrow(try XCMetrics.parse([
Expand All @@ -56,7 +60,11 @@ class XCMetricsArgumentTests: XCTestCase {
"-b",
"/Users/Username/Library/Developer/Xcode/DerivedData/test/",
"-t",
"10"
"10",
"-k",
"Authorization",
"-a",
"Bearer XXXXXXXXXXXXXXX"
]))
}

Expand Down
5 changes: 4 additions & 1 deletion docs/Getting Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ This is how the post-action scheme should look like. Let's break it down:
- `--serviceURL`: the URL of the service receiving the collected metrics. If you haven't deployed a service yet, please head over to ["Deploy Backend"](https://github.com/spotify/XCMetrics/blob/main/docs/How%20to%20Deploy%20Backend.md) first.
- `--timeout`: the number of seconds to wait for the Xcode log to appear. The default value is 5s.
- `--isCI`: either true or false based on if the current build is running on CI or not. This is useful to categorize builds as local or continuous integration builds.
- `--skipNotes`: true or false. If true, the Notes found in a log (Xcode adds them to with things like the output of Post build phase's scripts like Swiftlint) won't be inserted in the Database. Useful if you have thousands of these in your log and want to save some space.
- `--skipNotes`: true or false. If true, the Notes found in a log (Xcode adds them to with things like the output of Post build phase's scripts like Swiftlint) won't be inserted in the Database. Useful if you have thousands of these in your log and want to save some space.
- `--authorizationKey`: An optional authorization header **key** to be included in the upload request e.g 'Authorization' or 'x-api-key' etc. This is ignored by the XCMetrics backend service, but can be consumed by a 3rd party service to facilitate a basic level of authentication. An example of this would be using an AWS API Gateway API Key. **Must** be used in conjunction with `authorizationValue`.
- `--authorizationValue`: An optional authorization header **value** to be included in the upload request e.g 'Basic YWxhZGRpbjpvcGVuc2VzYW1l' or `hYDqG78OIUDIWKLdwjdwhdu8` etc. This is ignored by the XCMetrics backend service, but can be consumed by a 3rd party service to facilitate a basic level of authentication. An example of this would be using an AWS API Gateway API Key. **Must** be used in conjunction with `authorizationKey`.


![](img/post-action-scheme.png)

Expand Down

0 comments on commit 1317797

Please sign in to comment.