Skip to content

Commit 4f1d95b

Browse files
committed
RUM-8042 Group metrics with same cadinality
1 parent 04e471b commit 4f1d95b

File tree

4 files changed

+77
-36
lines changed

4 files changed

+77
-36
lines changed

DatadogCore/Sources/SDKMetrics/BatchMetrics.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ internal enum BatchClosedMetric {
134134
/// Definition of "Batch Blocked" telemetry.
135135
internal enum BatchBlockedMetric {
136136
/// Metric type value.
137-
static let typeValue = "batch blocked"
137+
static let typeValue = "batch_blocked"
138138
/// List of upload blocker reasons
139139
static let blockers = "blockers"
140140
/// The blocking failure reason.
@@ -144,5 +144,5 @@ internal enum BatchBlockedMetric {
144144
/// Definition of "Pending Batches" telemetry.
145145
internal enum PendingBatchMetric {
146146
/// Metric type value.
147-
static let typeValue = "pending batches"
147+
static let typeValue = "pending_batches"
148148
}

DatadogCore/Tests/Datadog/Core/Persistence/FilesOrchestrator+MetricsTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase {
8181
XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate)
8282

8383
let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in
84-
XCTAssertEqual(metric.metric, "pending batches")
84+
XCTAssertEqual(metric.metric, "pending_batches")
8585
XCTAssertEqual(metric.cardinalities["track"], .string("track name"))
8686
return count + metric.increment
8787
}
@@ -122,11 +122,11 @@ class FilesOrchestrator_MetricsTests: XCTestCase {
122122
let pendingBatches = telemetry.messages.reduce(0) { count, message in
123123
switch message {
124124
case let .metric(.record(metric, value, cardinalities)):
125-
XCTAssertEqual(metric, "pending batches")
125+
XCTAssertEqual(metric, "pending_batches")
126126
XCTAssertEqual(cardinalities["track"], .string("track name"))
127127
return value
128128
case let .metric(.increment(metric, value, cardinalities)):
129-
XCTAssertEqual(metric, "pending batches")
129+
XCTAssertEqual(metric, "pending_batches")
130130
XCTAssertEqual(cardinalities["track"], .string("track name"))
131131
return count + value
132132
default:
@@ -171,7 +171,7 @@ class FilesOrchestrator_MetricsTests: XCTestCase {
171171
XCTAssertEqual(batchDeleted.sampleRate, BatchDeletedMetric.sampleRate)
172172

173173
let pendingBatches = telemetry.messages.compactMap { $0.asMetricIncrement }.reduce(0) { count, metric in
174-
XCTAssertEqual(metric.metric, "pending batches")
174+
XCTAssertEqual(metric.metric, "pending_batches")
175175
XCTAssertEqual(metric.cardinalities["track"], .string("track name"))
176176
return count + metric.increment
177177
}

DatadogRUM/Sources/SDKMetrics/MetricTelemetryAggregator.swift

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,86 @@
77
import Foundation
88
import DatadogInternal
99

10+
/// A class that aggregates metric telemetry data before sending it to the Datadog.
11+
///
12+
/// This aggregator supports two types of metrics:
13+
/// - Counter metrics: Values that can only increase (e.g., number of events)
14+
/// - Gauge metrics: Values that can go up and down (e.g., current memory usage)
15+
///
16+
/// Metrics can be aggregated along different dimensions using cardinalities, allowing for
17+
/// detailed analysis of metric data across various contexts.
1018
internal final class MetricTelemetryAggregator {
11-
private struct AggregationKey: Hashable {
12-
let metric: String
13-
let cardinalities: MetricTelemetry.Cardinalities
14-
}
19+
private typealias AggregationKey = MetricTelemetry.Cardinalities
20+
private typealias AggregationValue = [String: Double]
1521

22+
/// The sample rate to apply to aggregated metrics.
1623
let sampleRate: SampleRate
1724

25+
/// Thread-safe storage for metric aggregations.
1826
@ReadWriteLock
19-
private var aggregations: [AggregationKey: Double] = [:]
27+
private var aggregations: [AggregationKey: AggregationValue] = [:]
2028

29+
/// Creates a new metric telemetry aggregator.
30+
///
31+
/// - Parameter sampleRate: The sample rate to apply to aggregated metrics.
32+
/// Defaults to maximum sample rate (100%).
2133
init(sampleRate: SampleRate = .maxSampleRate) {
2234
self.sampleRate = sampleRate
2335
}
2436

37+
/// Increments a counter metric by a specified value.
38+
///
39+
/// This method adds the specified value to the current value of the metric.
40+
/// If the metric doesn't exist, it will be initialized with the specified value.
41+
///
42+
/// - Parameters:
43+
/// - metric: The name of the metric to increment.
44+
/// - value: The amount to increment the metric by.
45+
/// - cardinalities: The dimensions along which the metric will be aggregated.
2546
func increment(_ metric: String, by value: Double, cardinalities: MetricTelemetry.Cardinalities) {
26-
_aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] += value }
47+
_aggregations.mutate { aggregations in
48+
var aggregation = aggregations[cardinalities, default: [metric: 0]]
49+
aggregation[metric, default: 0] += value
50+
aggregations[cardinalities] = aggregation
51+
}
2752
}
2853

54+
/// Records a gauge metric with a specified value.
55+
///
56+
/// This method sets the metric to the specified value, replacing any previous value.
57+
/// Gauge metrics are used for values that can fluctuate up and down.
58+
///
59+
/// - Parameters:
60+
/// - metric: The name of the metric to record.
61+
/// - value: The value to record for the metric.
62+
/// - cardinalities: The dimensions along which the metric will be aggregated.
2963
func record(_ metric: String, value: Double, cardinalities: MetricTelemetry.Cardinalities) {
30-
_aggregations.mutate { $0[AggregationKey(metric: metric, cardinalities: cardinalities), default: 0] = value }
64+
_aggregations.mutate { aggregations in
65+
var aggregation = aggregations[cardinalities, default: [metric: 0]]
66+
aggregation[metric, default: 0] = value
67+
aggregations[cardinalities] = aggregation
68+
}
3169
}
3270

71+
/// Flushes all aggregated metrics and returns them as telemetry events.
72+
///
73+
/// This method:
74+
/// 1. Converts all aggregated metrics into telemetry events
75+
/// 2. Clears the internal aggregation storage
76+
/// 3. Returns the generated events
77+
///
78+
/// - Returns: An array of metric telemetry events ready to be sent to the backend.
3379
func flush() -> [MetricTelemetry.Event] {
34-
_aggregations.mutate { counters in
35-
defer { counters = [:] }
80+
_aggregations.mutate { aggregations in
81+
defer { aggregations = [:] }
3682

37-
return counters.map { key, value in
38-
var attributes: [String: Encodable] = key.cardinalities
39-
attributes[SDKMetricFields.typeKey] = key.metric
40-
attributes[SDKMetricFields.valueKey] = value
83+
return aggregations.map { key, value in
84+
// Group metrics with same cardinality in the same
85+
// telemetry event
86+
var attributes: [String: Encodable] = key
87+
attributes.merge(value, uniquingKeysWith: { $1 })
4188
return MetricTelemetry.Event(
42-
name: key.metric,
89+
name: value.keys.joined(separator: ","),
4390
attributes: attributes,
4491
sampleRate: sampleRate
4592
)

DatadogRUM/Tests/SDKMetrics/MetricTelemetryAggregatorTests.swift

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,11 @@ class MetricTelemetryAggregatorTests: XCTestCase {
3737

3838
// Then
3939
let events = aggregator.flush()
40-
XCTAssertEqual(events.count, 4)
41-
42-
let event1 = try XCTUnwrap(events.first(where: { metric1 == $0.name }))
43-
XCTAssertEqual(event1.attributes["value"] as? Double, Double(iterations))
44-
45-
let event2 = try XCTUnwrap(events.first(where: { metric2 == $0.name }))
46-
XCTAssertEqual(event2.attributes["value"] as? Double, Double(iterations))
47-
40+
XCTAssertEqual(events.count, 2)
41+
XCTAssertEqual(events.first?.attributes[metric1] as? Double, Double(iterations))
42+
XCTAssertEqual(events.first?.attributes[metric2] as? Double, Double(iterations))
43+
XCTAssertEqual(events.last?.attributes[metric1] as? Double, Double(iterations))
44+
XCTAssertEqual(events.last?.attributes[metric2] as? Double, Double(iterations))
4845
XCTAssertTrue(aggregator.flush().isEmpty)
4946
}
5047

@@ -76,14 +73,11 @@ class MetricTelemetryAggregatorTests: XCTestCase {
7673

7774
// Then
7875
let events = aggregator.flush()
79-
XCTAssertEqual(events.count, 4)
80-
81-
let event1 = try XCTUnwrap(events.first(where: { metric1 == $0.name }))
82-
XCTAssertEqual(event1.attributes["value"] as? Double, value1)
83-
84-
let event2 = try XCTUnwrap(events.first(where: { metric2 == $0.name }))
85-
XCTAssertEqual(event2.attributes["value"] as? Double, value2)
86-
76+
XCTAssertEqual(events.count, 2)
77+
XCTAssertEqual(events.first?.attributes[metric1] as? Double, value1)
78+
XCTAssertEqual(events.first?.attributes[metric2] as? Double, value2)
79+
XCTAssertEqual(events.last?.attributes[metric1] as? Double, value1)
80+
XCTAssertEqual(events.last?.attributes[metric2] as? Double, value2)
8781
XCTAssertTrue(aggregator.flush().isEmpty)
8882
}
8983

0 commit comments

Comments
 (0)