Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 9 additions & 9 deletions Sources/DatadogSDKTesting/KnownTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,13 @@ struct KnownTestsFactory: FeatureFactory {
}

private func fetchTests() async throws -> KnownTestsMap {
let tests: KnownTestsMap
let result: KnownTestsResult
do {
tests = try await api.tests(service: service, env: environment,
repositoryURL: repository,
configurations: configurations,
customConfigurations: customConfigurations,
observer: telemetry?.knownTestsRequestObserver).tests
result = try await api.tests(service: service, env: environment,
repositoryURL: repository,
configurations: configurations,
customConfigurations: customConfigurations,
observer: telemetry?.knownTestsRequestObserver)
} catch {
throw LibraryConfigurationCommunicationError(
requestName: "Known Tests Request",
Expand All @@ -140,12 +140,12 @@ struct KnownTestsFactory: FeatureFactory {
)
}
if let telemetry {
let totalTests = tests.values.flatMap { $0.values }.reduce(0) { $0 + $1.count }
let totalTests = result.tests.values.flatMap { $0.values }.reduce(0) { $0 + $1.count }
telemetry.metrics.knownTests.responseTests.record(Double(totalTests))
}
// if we have empty array we should disable Known Tests functionality
guard tests.count > 0 else { throw FeatureEmptyResponseError(featureName: "Known Tests") }
return tests
guard result.tests.count > 0 else { throw FeatureEmptyResponseError(featureName: "Known Tests") }
return result.tests
}

private func saveTests(tests: KnownTestsMap) {
Expand Down
6 changes: 6 additions & 0 deletions Sources/DatadogSDKTesting/Telemetry/TelemetryMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,19 @@ extension Telemetry.Metrics {
let requestMs: Telemetry.Distribution<Telemetry.EmptyMetricTags>
let responseBytes: Telemetry.Distribution<Telemetry.EmptyMetricTags>
let responseTests: Telemetry.Distribution<Telemetry.EmptyMetricTags>
let pagesFetched: Telemetry.Distribution<Telemetry.EmptyMetricTags>
let totalFetchMs: Telemetry.Distribution<Telemetry.EmptyMetricTags>
let totalRequestMs: Telemetry.Distribution<Telemetry.EmptyMetricTags>

init(_ f: Telemetry.Factory) {
request = f.counter("known_tests.request")
requestErrors = f.counter("known_tests.request_errors")
requestMs = f.distribution("known_tests.request_ms")
responseBytes = f.distribution("known_tests.response_bytes")
responseTests = f.distribution("known_tests.response_tests")
pagesFetched = f.distribution("known_tests.pages_fetched")
totalFetchMs = f.distribution("known_tests.total_fetch_ms")
totalRequestMs = f.distribution("known_tests.total_request_ms")
}
}

Expand Down
19 changes: 13 additions & 6 deletions Sources/DatadogSDKTesting/Telemetry/TelemetryObservers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,20 @@ extension Telemetry {
)
}

var knownTestsRequestObserver: RequestMetricsObserver {
var knownTestsRequestObserver: PagedRequestObserver {
let m = metrics.knownTests
return RequestMetricsObserver(
onRequest: { m.request.add() },
onDurationMs: { m.requestMs.record($0) },
onResponseBytes: { m.responseBytes.record(Double($0)) },
onError: { m.requestErrors.add(errorType: $0) }
return PagedRequestObserver(
wrapping: RequestMetricsObserver(
onRequest: { m.request.add() },
onDurationMs: { m.requestMs.record($0) },
onResponseBytes: { m.responseBytes.record(Double($0)) },
onError: { m.requestErrors.add(errorType: $0) }
),
onPagesFetched: { count, totalFetchMs, totalRequestMs in
m.pagesFetched.record(Double(count))
m.totalFetchMs.record(totalFetchMs)
m.totalRequestMs.record(totalRequestMs)
}
)
}

Expand Down
10 changes: 8 additions & 2 deletions Sources/EventsExporter/API/KnownTestsApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public protocol KnownTestsApi: APIService {
service: String, env: String, repositoryURL: String,
configurations: [String: String],
customConfigurations: [String: String],
observer: RequestObserver?
observer: PagedRequestObserver?
) async throws(APICallError) -> KnownTestsResult
}

Expand All @@ -67,17 +67,20 @@ extension KnownTestsApi {
service: String, env: String, repositoryURL: String,
configurations: [String: String],
customConfigurations: [String: String],
observer: RequestObserver?
observer: PagedRequestObserver?
) async throws(APICallError) -> KnownTestsResult {
let startTime = Date().timeIntervalSince1970 * 1000
var tests: KnownTestsMap = [:]
var page: KnownTestsPageInfo = .init()
var size: Int = 0

repeat {
let result = try await self.tests(
service: service, env: env, repositoryURL: repositoryURL,
configurations: configurations, customConfigurations: customConfigurations,
page: page, observer: observer
)

tests = tests.merging(result.tests) { (current, new) in
current.merging(new) { (current, new) in
Array(Set(current).union(new))
Expand All @@ -87,6 +90,9 @@ extension KnownTestsApi {
size += result.pageInfo.size
} while page.hasNext

let totalFetchMs = Date().timeIntervalSince1970 * 1000 - startTime
observer?.finished(totalFetchMs: totalFetchMs)

return KnownTestsResult(tests: tests,
pageInfo: .init(cursor: page.cursor,
pageSize: page.pageSize,
Expand Down
38 changes: 38 additions & 0 deletions Sources/EventsExporter/Telemetry/MetricObservers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,44 @@ public protocol RequestObserver: Sendable {
statusCode: Int?, transportError: (any Error)?, failed: Bool)
}

/// Observes a sequence of paginated requests (e.g. Known Tests). Conforms to
/// `RequestObserver` so the same instance can be handed to each per-page HTTP
/// call: it forwards every call unchanged to the wrapped observer while
/// summing the `durationMs` of every call (successful or not — retries still
/// cost wall-clock time) and counting only the succeeded pages, so the caller
/// never times a page itself. Call `finished(totalFetchMs:)` once, after the
/// last page, to report the pagination-level aggregate.
public final class PagedRequestObserver: RequestObserver, Sendable {
private let wrapped: RequestObserver?
private let onPagesFetched: (@Sendable (_ count: Int, _ totalFetchMs: Double, _ totalRequestMs: Double) -> Void)?
private let accumulated = Synced<(pageCount: Int, totalRequestMs: Double)>((0, 0))

public init(wrapping observer: RequestObserver? = nil,
onPagesFetched: (@Sendable (_ count: Int, _ totalFetchMs: Double, _ totalRequestMs: Double) -> Void)? = nil) {
self.wrapped = observer
self.onPagesFetched = onPagesFetched
}

public func requestFinished(durationMs: Double, requestBytes: Int, responseBytes: Int,
statusCode: Int?, transportError: (any Error)?, failed: Bool) {
accumulated.update { state in
state.totalRequestMs += durationMs
if !failed { state.pageCount += 1 }
}
wrapped?.requestFinished(durationMs: durationMs, requestBytes: requestBytes,
responseBytes: responseBytes, statusCode: statusCode,
transportError: transportError, failed: failed)
}

/// Reports the pagination-level aggregate; call once after the last page.
/// `totalFetchMs` is the wall-clock time from the first page request to
/// the last, supplied by the caller since only it spans the whole loop.
public func finished(totalFetchMs: Double) {
let (pageCount, totalRequestMs) = accumulated.value
onPagesFetched?(pageCount, totalFetchMs, totalRequestMs)
}
}

/// Observes the background upload pipeline that drains stored batches to the
/// intake (one observer per feature store, e.g. spans vs coverage).
///
Expand Down
Loading