Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(metrics): Add BeforeEmitMetrics callback #3799

Merged
merged 33 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e9bde83
chore(metrics): Add Set, Gauge, Distribution metric classes
philipphofmann Mar 22, 2024
1f3a779
chore(metrics): Add LocaleMetricsAggregator
philipphofmann Mar 20, 2024
2ec3fd7
call local aggregator in buckets aggregator
philipphofmann Mar 21, 2024
24ec7ca
backup
philipphofmann Mar 22, 2024
98c8900
Merge branch 'main' into feat/local-metrics-aggregator
philipphofmann Mar 26, 2024
4100ed7
make LocalMetricsAggregator struct private
philipphofmann Mar 26, 2024
74f0d7e
remove global tagskey function
philipphofmann Mar 26, 2024
605d126
ditch private typealiases
philipphofmann Mar 26, 2024
3921a17
fix build error
philipphofmann Mar 26, 2024
7e94683
Merge branch 'feat/local-metrics-aggregator' into feat/metrics
philipphofmann Mar 26, 2024
56ad35a
tags
philipphofmann Mar 26, 2024
34d29d1
Merge branch 'main' into feat/metrics
philipphofmann Mar 26, 2024
eec5849
changelog
philipphofmann Mar 26, 2024
63c6259
feat(metrics): Change adding set to string
philipphofmann Mar 26, 2024
6cec1cb
use isKindOfClass
philipphofmann Mar 26, 2024
74b9c83
Merge branch 'feat/metrics' into feat/strings-for-metric-set
philipphofmann Mar 26, 2024
4ad6c65
fix unit test compile issues
philipphofmann Mar 26, 2024
679e982
ref: Avoid casting for set metric
philipphofmann Mar 26, 2024
0c61e55
remove zlib
philipphofmann Mar 27, 2024
b8120c9
Merge branch 'main' into feat/metrics
philipphofmann Mar 27, 2024
a34ff72
Merge branch 'feat/metrics' into feat/strings-for-metric-set
philipphofmann Mar 27, 2024
0950289
Merge branch 'feat/strings-for-metric-set' into ref/avoid-casting-for…
philipphofmann Mar 27, 2024
9aed2e3
feat(metrics): Change adding set to string (#3792)
philipphofmann Mar 27, 2024
8943206
Merge branch 'feat/metrics' into ref/avoid-casting-for-set-metric
philipphofmann Mar 27, 2024
4489b13
fix merge problems
philipphofmann Mar 27, 2024
414c351
Merge branch 'main' into ref/avoid-casting-for-set-metric
philipphofmann Mar 27, 2024
fca1d5d
fix build error
philipphofmann Mar 27, 2024
ec97c43
feat(metrics): Add BeforeEmitMetrics callback
philipphofmann Mar 27, 2024
eaf4828
fix Xcode 13 build issues
philipphofmann Mar 28, 2024
87bc52a
Merge branch 'main' into ref/avoid-casting-for-set-metric
philipphofmann Mar 28, 2024
5a47197
Merge branch 'ref/avoid-casting-for-set-metric' into feat/metrics-bef…
philipphofmann Mar 28, 2024
f7b8cdf
make callback nullable
philipphofmann Mar 28, 2024
b316400
Merge branch 'main' into feat/metrics-before-emit-metrics
philipphofmann Mar 28, 2024
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Features

- Add Metrics API (#3791): Read our [docs](https://docs.sentry.io/platforms/apple/metrics/) to learn
- Add Metrics API (#3791, #3799): Read our [docs](https://docs.sentry.io/platforms/apple/metrics/) to learn
more about how to use the Metrics API.
- Pre-main profiling data is now attached to the app start transaction (#3736)

Expand Down
10 changes: 10 additions & 0 deletions Sources/Sentry/Public/SentryDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ typedef NSNumber *_Nullable (^SentryTracesSamplerCallback)(
*/
typedef void (^SentrySpanCallback)(id<SentrySpan> _Nullable span);

/**
* A callback block which gets called right before a metric is about to be emitted.

* @param key The key of the metric.
* @param tags A dictionary of key-value pairs associated with the metric.
* @return BOOL YES if the metric should be emitted, NO otherwise.
*/
typedef BOOL (^SentryBeforeEmitMetricCallback)(
NSString *_Nonnull key, NSDictionary<NSString *, NSString *> *_Nonnull tags);

/**
* Log level.
*/
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,11 @@ NS_SWIFT_NAME(Options)
*/
@property (nonatomic, assign) BOOL enableSpanLocalMetricAggregation;

/**
* This block can be used to modify the event before it will be serialized and sent.
*/
@property (nullable, nonatomic, copy) SentryBeforeEmitMetricCallback beforeEmitMetric;

@end

NS_ASSUME_NONNULL_END
11 changes: 6 additions & 5 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ - (instancetype)initWithClient:(nullable SentryClient *)client
SentryMetricsClient *metricsClient =
[[SentryMetricsClient alloc] initWithClient:statsdClient];
_metrics = [[SentryMetricsAPI alloc]
initWithEnabled:client.options.enableMetrics
client:metricsClient
currentDate:SentryDependencyContainer.sharedInstance.dateProvider
dispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper
random:SentryDependencyContainer.sharedInstance.random];
initWithEnabled:client.options.enableMetrics
client:metricsClient
currentDate:SentryDependencyContainer.sharedInstance.dateProvider
dispatchQueue:SentryDependencyContainer.sharedInstance.dispatchQueueWrapper
random:SentryDependencyContainer.sharedInstance.random
beforeEmitMetric:client.options.beforeEmitMetric];
[_metrics setDelegate:self];

_sessionLock = [[NSObject alloc] init];
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
[self setBool:options[@"enableSpanLocalMetricAggregation"]
block:^(BOOL value) { self->_enableSpanLocalMetricAggregation = value; }];

if ([self isBlock:options[@"beforeEmitMetric"]]) {
self.beforeEmitMetric = options[@"beforeEmitMetric"];
}

return YES;
}

Expand Down
87 changes: 71 additions & 16 deletions Sources/Swift/Metrics/BucketsMetricsAggregator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class BucketMetricsAggregator: MetricsAggregator {
private let currentDate: SentryCurrentDateProvider
private let dispatchQueue: SentryDispatchQueueWrapper
private let random: SentryRandomProtocol
private let beforeEmitMetric: BeforeEmitMetricCallback
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
private let totalMaxWeight: UInt
private let flushShift: TimeInterval
private let flushInterval: TimeInterval
Expand All @@ -35,6 +36,7 @@ class BucketMetricsAggregator: MetricsAggregator {
currentDate: SentryCurrentDateProvider,
dispatchQueue: SentryDispatchQueueWrapper,
random: SentryRandomProtocol,
beforeEmitMetric: @escaping BeforeEmitMetricCallback = { _, _ in true },
totalMaxWeight: UInt = 1_000,
flushInterval: TimeInterval = 10.0,
flushTolerance: TimeInterval = 0.5
Expand All @@ -43,6 +45,7 @@ class BucketMetricsAggregator: MetricsAggregator {
self.currentDate = currentDate
self.dispatchQueue = dispatchQueue
self.random = random
self.beforeEmitMetric = beforeEmitMetric

// The aggregator shifts its flushing by up to an entire rollup window to
// avoid multiple clients trampling on end of a 10 second window as all the
Expand Down Expand Up @@ -71,7 +74,72 @@ class BucketMetricsAggregator: MetricsAggregator {
self.timer = timer
}

func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) {
func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) {
self.add(type: MetricType.counter,
key: key, value: value,
unit: unit,
tags: tags,
localMetricsAggregator: localMetricsAggregator,
initMetric: {
CounterMetric(first: value, key: key, unit: unit, tags: tags)
}, addValueToMetric: { metric in
// Unit tests validate that the cast works. If it doesn't, a test will fail.
let castedMetric = metric as? CounterMetric
castedMetric?.add(value: value)
})
}

func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) {
self.add(type: MetricType.gauge,
key: key, value: value,
unit: unit,
tags: tags,
localMetricsAggregator: localMetricsAggregator,
initMetric: {
GaugeMetric(first: value, key: key, unit: unit, tags: tags)
}, addValueToMetric: { metric in
// Unit tests validate that the cast works. If it doesn't, a test will fail.
let castedMetric = metric as? GaugeMetric
castedMetric?.add(value: value)
})
}

func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) {
self.add(type: MetricType.distribution,
key: key, value: value,
unit: unit,
tags: tags,
localMetricsAggregator: localMetricsAggregator,
initMetric: {
DistributionMetric(first: value, key: key, unit: unit, tags: tags)
}, addValueToMetric: { metric in
// Unit tests validate that the cast works. If it doesn't, a test will fail.
let castedMetric = metric as? DistributionMetric
castedMetric?.add(value: value)
})
}

func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator? = nil) {
self.add(type: MetricType.set,
key: key,
value: 0, // Value is not used for set
unit: unit,
tags: tags,
localMetricsAggregator: localMetricsAggregator,
initMetric: {
SetMetric(first: value, key: key, unit: unit, tags: tags)
}, addValueToMetric: { metric in
// Unit tests validate that the cast works. If it doesn't, a test will fail.
let castedMetric = metric as? SetMetric
castedMetric?.add(value: value)
})
}

private func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?, initMetric: () -> any Metric, addValueToMetric: (any Metric) -> Void) {

if !beforeEmitMetric(key, tags) {
return
}

let tagsKey = tags.getMetricsTagsKey()
let bucketKey = "\(type)_\(key)_\(unit.unit)_\(tagsKey)"
Expand All @@ -84,11 +152,11 @@ class BucketMetricsAggregator: MetricsAggregator {
var bucket = buckets[bucketTimestamp] ?? [:]
let oldWeight = bucket[bucketKey]?.weight ?? 0

let metric = bucket[bucketKey] ?? initMetric(first: value, type: type, key: key, unit: unit, tags: tags)
let metric = bucket[bucketKey] ?? initMetric()
let metricExists = bucket[bucketKey] != nil

if metricExists {
metric.add(value: value)
addValueToMetric(metric)
}

let addedWeight = metric.weight - oldWeight
Expand All @@ -114,19 +182,6 @@ class BucketMetricsAggregator: MetricsAggregator {
})
}
}

private func initMetric(first: Double, type: MetricType, key: String, unit: MeasurementUnit, tags: [String: String]) -> Metric {
switch type {
case .counter:
return CounterMetric(first: first, key: key, unit: unit, tags: tags)
case .gauge:
return GaugeMetric(first: first, key: key, unit: unit, tags: tags)
case .distribution:
return DistributionMetric(first: first, key: key, unit: unit, tags: tags)
case .set:
return SetMetric(first: UInt(first), key: key, unit: unit, tags: tags)
}
}

func flush(force: Bool) {
var flushableBuckets: [BucketTimestamp: [Metric]] = [:]
Expand Down
1 change: 0 additions & 1 deletion Sources/Swift/Metrics/Metric.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ typealias Metric = MetricBase & MetricProtocol
protocol MetricProtocol {

var weight: UInt { get }
func add(value: Double)
func serialize() -> [String]
}

Expand Down
23 changes: 20 additions & 3 deletions Sources/Swift/Metrics/MetricsAggregator.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import Foundation

protocol MetricsAggregator {
func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?)
func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?)

func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?)

func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?)

func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?)

func flush(force: Bool)
func close()
Expand All @@ -16,8 +22,19 @@ extension Dictionary where Key == String, Value == String {
}

class NoOpMetricsAggregator: MetricsAggregator {

func add(type: MetricType, key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) {
func increment(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) {
// empty on purpose
}

func gauge(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) {
// empty on purpose
}

func distribution(key: String, value: Double, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) {
// empty on purpose
}

func set(key: String, value: UInt, unit: MeasurementUnit, tags: [String: String], localMetricsAggregator: LocalMetricsAggregator?) {
// empty on purpose
}

Expand Down
17 changes: 10 additions & 7 deletions Sources/Swift/Metrics/SentryMetricsAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import Foundation
func getLocalMetricsAggregator() -> LocalMetricsAggregator?
}

/// Using SentryBeforeEmitMetricCallback of SentryDefines.h leads to compiler errors because of
/// Swift to ObjC interoperability. Defining the callback again in Swift with the same signature is a workaround.
typealias BeforeEmitMetricCallback = (String, [String: String]) -> Bool

@objc public class SentryMetricsAPI: NSObject {

private let aggregator: MetricsAggregator

private weak var delegate: SentryMetricsAPIDelegate?

@objc init(enabled: Bool, client: SentryMetricsClient, currentDate: SentryCurrentDateProvider, dispatchQueue: SentryDispatchQueueWrapper, random: SentryRandomProtocol) {
@objc init(enabled: Bool, client: SentryMetricsClient, currentDate: SentryCurrentDateProvider, dispatchQueue: SentryDispatchQueueWrapper, random: SentryRandomProtocol, beforeEmitMetric: BeforeEmitMetricCallback?) {

if enabled {
self.aggregator = BucketMetricsAggregator(client: client, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random)
self.aggregator = BucketMetricsAggregator(client: client, currentDate: currentDate, dispatchQueue: dispatchQueue, random: random, beforeEmitMetric: beforeEmitMetric ?? { _, _ in true })
} else {
self.aggregator = NoOpMetricsAggregator()
}
Expand All @@ -34,7 +38,7 @@ import Foundation
/// - Parameter tags: Tags to associate with the metric.
@objc public func increment(key: String, value: Double = 1.0, unit: MeasurementUnit = .none, tags: [String: String] = [:]) {
let mergedTags = mergeDefaultTagsInto(tags: tags)
aggregator.add(type: MetricType.counter, key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
aggregator.increment(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
}

/// Emits a Gauge metric.
Expand All @@ -46,7 +50,7 @@ import Foundation
@objc
public func gauge(key: String, value: Double, unit: MeasurementUnit = .none, tags: [String: String] = [:]) {
let mergedTags = mergeDefaultTagsInto(tags: tags)
aggregator.add(type: MetricType.gauge, key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
aggregator.gauge(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
}

/// Emits a Distribution metric.
Expand All @@ -58,7 +62,7 @@ import Foundation
@objc
public func distribution(key: String, value: Double, unit: MeasurementUnit = .none, tags: [String: String] = [:]) {
let mergedTags = mergeDefaultTagsInto(tags: tags)
aggregator.add(type: MetricType.distribution, key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
aggregator.distribution(key: key, value: value, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
}

/// Emits a Set metric.
Expand All @@ -70,10 +74,9 @@ import Foundation
@objc
public func set(key: String, value: String, unit: MeasurementUnit = .none, tags: [String: String] = [:]) {
let mergedTags = mergeDefaultTagsInto(tags: tags)

let crc32 = sentry_crc32ofString(value)

aggregator.add(type: MetricType.set, key: key, value: Double(crc32), unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
aggregator.set(key: key, value: crc32, unit: unit, tags: mergedTags, localMetricsAggregator: delegate?.getLocalMetricsAggregator())
}

@objc public func close() {
Expand Down
7 changes: 2 additions & 5 deletions Sources/Swift/Metrics/SetMetric.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ class SetMetric: Metric {
super.init(type: .set, key: key, unit: unit, tags: tags)
}

// This doesn't work with the full range of UInt.
// We still need to fix this.
func add(value: Double) {
if value >= Double(UInt.min) && value < Double(UInt.max) { set.insert(UInt(value))
}
func add(value: UInt) {
set.insert(value)
}

func serialize() -> [String] {
Expand Down
21 changes: 21 additions & 0 deletions Tests/SentryTests/SentryOptionsTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,27 @@
[self testBooleanField:@"enableSpanLocalMetricAggregation" defaultValue:YES];
}

- (void)testBeforeEmitMetric
{
SentryBeforeEmitMetricCallback callback
= ^(NSString *_Nonnull key, NSDictionary<NSString *, NSString *> *_Nonnull tags) {
// Use tags and key to silence unused compiler error
XCTAssertNotNil(key);
XCTAssertNotNil(tags);
return YES;

Check warning on line 1301 in Tests/SentryTests/SentryOptionsTest.m

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/SentryOptionsTest.m#L1299-L1301

Added lines #L1299 - L1301 were not covered by tests
};
SentryOptions *options = [self getValidOptions:@{ @"beforeEmitMetric" : callback }];

XCTAssertEqual(callback, options.beforeEmitMetric);
}

- (void)testDefaultBeforeEmitMetric
{
SentryOptions *options = [self getValidOptions:@{}];

XCTAssertNil(options.beforeEmitMetric);
}

#pragma mark - Private

- (void)assertArrayEquals:(NSArray<NSString *> *)expected actual:(NSArray<NSString *> *)actual
Expand Down
16 changes: 16 additions & 0 deletions Tests/SentryTests/SentrySDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,22 @@ class SentrySDKTests: XCTestCase {
expect(envelopeItem.header.type) == SentryEnvelopeItemTypeStatsd
expect(envelopeItem.header.contentType) == "application/octet-stream"
}

func testMetrics_BeforeEmitMetricCallback_DiscardEveryThing() throws {
let options = fixture.options
options.enableMetrics = true
options.beforeEmitMetric = { _, _ in false }

SentrySDK.start(options: options)
let client = try XCTUnwrap(TestClient(options: options))
let hub = SentryHub(client: client, andScope: nil)
SentrySDK.setCurrentHub(hub)

SentrySDK.metrics.increment(key: "key")
SentrySDK.flush(timeout: 1.0)

expect(client.captureEnvelopeInvocations.count) == 0
}

#if SENTRY_HAS_UIKIT

Expand Down
Loading
Loading