forked from DataDog/dd-sdk-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RUMM-110 Add basic components for logs upload
- Loading branch information
Showing
21 changed files
with
723 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,36 @@ | ||
import Foundation | ||
|
||
/// An exception thrown during Datadog SDK initialization if configuration is invalid. Check `description` for details. | ||
public struct DatadogInitializationException: Error { | ||
/// Describes the reason of failed initialization. | ||
public let description: String | ||
} | ||
|
||
/// Datadog SDK configuration object. | ||
public struct Datadog { | ||
public init() {} | ||
/// URL to upload logs to. | ||
internal let logsUploadURL: URL | ||
|
||
public init( | ||
logsEndpoint: String, | ||
clientToken: String | ||
) throws { | ||
self.logsUploadURL = try buildLogsUploadURLOrThrow(logsEndpoint: logsEndpoint, clientToken: clientToken) | ||
} | ||
} | ||
|
||
private func buildLogsUploadURLOrThrow(logsEndpoint: String, clientToken: String) throws -> URL { | ||
guard !logsEndpoint.isEmpty else { | ||
throw DatadogInitializationException(description: "`logsEndpoint` cannot be empty.") | ||
} | ||
guard !clientToken.isEmpty else { | ||
throw DatadogInitializationException(description: "`clientToken` cannot be empty.") | ||
} | ||
guard let endpointWithClientToken = URL(string: logsEndpoint)?.appendingPathComponent(clientToken) else { | ||
throw DatadogInitializationException(description: "Invalid `logsEndpoint` or `clientToken`.") | ||
} | ||
guard let url = URL(string: "\(endpointWithClientToken.absoluteString)?ddsource=mobile") else { | ||
throw DatadogInitializationException(description: "Cannot build logs upload URL.") | ||
} | ||
return url | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
|
||
/// Representation of log uploaded to server. | ||
struct Log: Codable, Equatable { | ||
let date: Date | ||
let status: String | ||
let message: String | ||
let service: String | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import Foundation | ||
|
||
public class Logger { | ||
private let uploader: LogsUploader | ||
|
||
public convenience init(configuration: Datadog) { | ||
self.init( | ||
uploader: LogsUploader( | ||
configuration: configuration, | ||
httpClient: HTTPClient(transport: URLSessionTransport()) | ||
) | ||
) | ||
} | ||
|
||
internal init(uploader: LogsUploader) { | ||
self.uploader = uploader | ||
} | ||
|
||
/// Logs INFO message. | ||
/// | ||
/// - Parameter message: the message | ||
public func info(_ message: String) { | ||
log(status: "INFO", message: message) | ||
} | ||
|
||
private func log(status: String, message: String) { | ||
let log = Log(date: Date(), status: status, message: message, service: "ios-sdk-test-service") | ||
do { | ||
try uploader.upload(logs: [log]) { (status) in | ||
print("ℹ️ logs delivery status: \(status)") | ||
} | ||
} catch { | ||
print("🔥 logs not delivered due to: \(error)") | ||
} | ||
} | ||
} | ||
|
||
//private func createLog() -> Log { | ||
// return Log( | ||
// date: ISO8601DateFormatter().string(from: Date()), | ||
// status: "INFO", | ||
// message: "Random value: \(Int.random(in: 100..<200))", | ||
// service: "ios-app-example" | ||
// ) | ||
//} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import Foundation | ||
|
||
enum LogsDeliveryStatus: Equatable { | ||
/// Corresponds to HTTP 2xx response status codes. | ||
case success(logs: [Log]) | ||
/// Corresponds to HTTP 3xx response status codes. | ||
case redirection(logs: [Log]) | ||
/// Corresponds to HTTP 4xx response status codes. | ||
case clientError(logs: [Log]) | ||
/// Corresponds to HTTP 5xx response status codes. | ||
case serverError(logs: [Log]) | ||
/// Means transportation error and no delivery at all. | ||
case networkError(logs: [Log]) | ||
/// Corresponds to unknown HTTP response status code. | ||
case unknown(logs: [Log]) | ||
|
||
init(from httpResponse: HTTPResponse, logs: [Log]) { | ||
switch httpResponse.code { | ||
case 200...299: self = .success(logs: logs) | ||
case 300...399: self = .redirection(logs: logs) | ||
case 400...499: self = .clientError(logs: logs) | ||
case 500...599: self = .serverError(logs: logs) | ||
default: self = .unknown(logs: logs) | ||
} | ||
} | ||
|
||
init(from httpRequestDeliveryError: HTTPRequestDeliveryError, logs: [Log]) { | ||
self = .networkError(logs: logs) | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
Sources/Datadog/Logs/Upload/LogsUploadRequestEncoder.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
|
||
/// Builds `HTTPRequest` for sending logs to the server. | ||
struct LogsUploadRequestEncoder { | ||
private let url: URL | ||
private let headers = ["Content-Type": "application/json"] | ||
private let method = "POST" | ||
private let jsonEncoder: JSONEncoder | ||
|
||
init(uploadURL: URL) { | ||
self.url = uploadURL | ||
self.jsonEncoder = JSONEncoder() | ||
jsonEncoder.dateEncodingStrategy = .iso8601 | ||
} | ||
|
||
func encodeRequest(with logs: [Log]) throws -> HTTPRequest { | ||
return HTTPRequest(url: url, headers: headers, method: method, body: try jsonEncoder.encode(logs)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import Foundation | ||
|
||
/// Sends logs to server. | ||
final class LogsUploader { | ||
|
||
private let httpClient: HTTPClient | ||
private let requestBuilder: LogsUploadRequestEncoder | ||
|
||
init(configuration: Datadog, httpClient: HTTPClient) { | ||
self.httpClient = httpClient | ||
self.requestBuilder = LogsUploadRequestEncoder(uploadURL: configuration.logsUploadURL) | ||
} | ||
|
||
func upload(logs: [Log], completion: @escaping (LogsDeliveryStatus) -> Void) throws { | ||
let request = try requestBuilder.encodeRequest(with: logs) | ||
httpClient.send(request: request) { (result) in | ||
switch result { | ||
case .success(let httpResponse): | ||
completion(LogsDeliveryStatus(from: httpResponse, logs: logs)) | ||
case .failure(let httpRequestDeliveryError): | ||
completion(LogsDeliveryStatus(from: httpRequestDeliveryError, logs: logs)) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,48 @@ | ||
import Foundation | ||
|
||
/// Basic HTTP request representation - limited to information that SDK needs to use. | ||
struct HTTPRequest { | ||
let url: URL | ||
let headers: [String: String] | ||
let method: String | ||
let body: Data | ||
} | ||
|
||
/// Basic HTTP response representation - limited to information that SDK needs to use. | ||
struct HTTPResponse { | ||
let code: Int | ||
} | ||
|
||
/// Error related to request delivery, like unreachable server, no internet connection etc. | ||
struct HTTPRequestDeliveryError: Error { | ||
let details: Error | ||
} | ||
|
||
/// Client for sending requests over HTTP. | ||
final class HTTPClient { | ||
private let transport: HTTPTransport | ||
|
||
private let session: URLSession | ||
|
||
init() { | ||
self.session = URLSession(configuration: .default) | ||
init(transport: HTTPTransport) { | ||
self.transport = transport | ||
} | ||
|
||
func send(request: URLRequest) { | ||
let task = session.dataTask(with: request) { (data, response, error) in | ||
print("🔥 error: \(error.debugDescription)") | ||
print("⭐️ response: \(response?.description ?? "")") | ||
print("⭐️ data of size: \(data?.count ?? 0)") | ||
|
||
func send(request: HTTPRequest, completion: @escaping (Result<HTTPResponse, HTTPRequestDeliveryError>) -> Void) { | ||
let urlRequest = buildURLRequest(from: request) | ||
transport.send(request: urlRequest) { result in | ||
switch result { | ||
case .response(let response, _): | ||
completion(.success(HTTPResponse(code: response.statusCode))) | ||
case .error(let error, _): | ||
completion(.failure(HTTPRequestDeliveryError(details: error))) | ||
} | ||
} | ||
task.resume() | ||
} | ||
|
||
private func buildURLRequest(from httpRequest: HTTPRequest) -> URLRequest { | ||
var request = URLRequest(url: httpRequest.url) | ||
request.httpMethod = httpRequest.method | ||
request.allHTTPHeaderFields = httpRequest.headers | ||
request.httpBody = httpRequest.body | ||
return request | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import Foundation | ||
|
||
/// A result of request sending request with `HTTPTransport`. | ||
enum HTTPTransportResult { | ||
/// Means successful request delivery. | ||
case response(HTTPURLResponse, Data?) | ||
/// Means transportation error (unreachable server, no internet connection, ...). | ||
case error(Error, Data?) | ||
} | ||
|
||
/// A type sending requests over HTTP. | ||
protocol HTTPTransport { | ||
func send(request: URLRequest, callback: @escaping (HTTPTransportResult) -> Void) | ||
} |
43 changes: 43 additions & 0 deletions
43
Sources/Datadog/Network/Transport/URLSessionTransport.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import Foundation | ||
|
||
/// An implementation of `HTTPTransport` which uses `URLSession` for requests delivery. | ||
final class URLSessionTransport: HTTPTransport { | ||
private let session: URLSession | ||
|
||
convenience init() { | ||
let configuration: URLSessionConfiguration = .ephemeral | ||
// TODO: RUMM-123 Optimize `URLSessionConfiguration` for good traffic performance | ||
self.init(session: URLSession(configuration: configuration)) | ||
} | ||
|
||
init(session: URLSession) { | ||
self.session = session | ||
} | ||
|
||
func send(request: URLRequest, callback: @escaping (HTTPTransportResult) -> Void) { | ||
let task = session.dataTask(with: request) { (data, response, error) in | ||
callback(transportResult(for: (data, response, error))) | ||
} | ||
task.resume() | ||
} | ||
} | ||
|
||
/// An error returned if given `URLSession` response state is inconsistent (like no data, no response and no error). | ||
/// The code execution in `URLSessionTransport` should never reach its initialization. | ||
struct URLSessionTransportInconsistencyException: Error {} | ||
|
||
/// As `URLSession` returns 3-values-touple for request execution, this function applies consistency constraints and turns | ||
/// it into only two possible states of `HTTPTransportResult`. | ||
private func transportResult(for urlSessionTaskCompletion: (Data?, URLResponse?, Error?)) -> HTTPTransportResult { | ||
let (data, response, error) = urlSessionTaskCompletion | ||
|
||
if let error = error { | ||
return .error(error, data) | ||
} | ||
|
||
if let httpResponse = response as? HTTPURLResponse, let data = data { | ||
return .response(httpResponse, data) | ||
} | ||
|
||
return .error(URLSessionTransportInconsistencyException(), data) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import Foundation | ||
@testable import Datadog | ||
|
||
/* | ||
A collection of mock configurations for SDK. | ||
It follows the mocking conventions described in `FoundationMocks.swift`. | ||
*/ | ||
|
||
extension Datadog { | ||
static func mockAny() -> Datadog { | ||
return .mockUsing(logsEndpoint: "https://api.example.com/v1", clientToken: "abcdefghi") | ||
} | ||
|
||
static func mockUsing(logsEndpoint: String, clientToken: String) -> Datadog { | ||
return try! Datadog(logsEndpoint: logsEndpoint, clientToken: clientToken) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,33 @@ | ||
import XCTest | ||
@testable import Datadog | ||
|
||
class DatadogTests: XCTestCase { | ||
|
||
func testItDoesSomething() { | ||
// 🐶 | ||
func testWhenCorrectEndpointAndClientTokenAreSet_itBuildsLogsUploadURL() throws { | ||
let datadog1 = try Datadog( | ||
logsEndpoint: "https://api.example.com/v1/logs/", | ||
clientToken: "abcdefghi" | ||
) | ||
XCTAssertEqual(datadog1.logsUploadURL, URL(string: "https://api.example.com/v1/logs/abcdefghi?ddsource=mobile")!) | ||
XCTAssertEqual(datadog1.logsUploadURL.query, "ddsource=mobile") | ||
|
||
let datadog2 = try Datadog( | ||
logsEndpoint: "https://api.example.com/v1/logs", // not normalized URL | ||
clientToken: "abcdefghi" | ||
) | ||
XCTAssertEqual(datadog2.logsUploadURL, URL(string: "https://api.example.com/v1/logs/abcdefghi?ddsource=mobile")!) | ||
XCTAssertEqual(datadog2.logsUploadURL.query, "ddsource=mobile") | ||
} | ||
|
||
func testWhenEmptyClientTokenIsNotSet_itThrows() { | ||
XCTAssertThrowsError(try Datadog(logsEndpoint: "https://api.example.com/v1/logs", clientToken: "")) { (error) in | ||
XCTAssertTrue((error as? DatadogInitializationException)?.description == "`clientToken` cannot be empty.") | ||
} | ||
} | ||
|
||
func testWhenLogsEndpointIsNotSet_itThrows() { | ||
XCTAssertThrowsError(try Datadog(logsEndpoint: "", clientToken: "abcdefghi")) { (error) in | ||
XCTAssertTrue((error as? DatadogInitializationException)?.description == "`logsEndpoint` cannot be empty.") | ||
} | ||
} | ||
} |
Oops, something went wrong.