From 7fabc8937b132c2196b6a92a3d6671b6571c120a Mon Sep 17 00:00:00 2001 From: James Go Date: Sat, 23 Mar 2024 00:50:59 -0700 Subject: [PATCH] Implement Exponential Histgoram metrics (#525) Implement Exponential Histgoram metrics --- .../metric/MetricsAdapter.swift | 49 ++++- .../AdaptingCircularBufferCounter.swift | 91 ++++++++ .../Aggregation/AdaptingIntegerArray.swift | 163 ++++++++++++++ .../Stable/Aggregation/Aggregation.swift | 8 +- ...xponentialBucketHistogramAggregation.swift | 40 ---- ...Base2ExponentialHistogramAggregation.swift | 39 ++++ ...eBase2ExponentialHistogramAggregator.swift | 208 ++++++++++++++---- .../Base2ExponentialHistogramIndexer.swift | 149 ++++++------- ...ubleBase2ExponentialHistogramBuckets.swift | 139 ++++++++++-- .../EmptyExponentialHistogramBuckets.swift | 19 ++ .../ExponentialHistogramBuckets.swift | 4 +- .../Internal/ExponentialHistogramData.swift | 6 - .../ExponentialHistogramPointData.swift | 97 ++++---- .../LongToDoubleExemplarReservoir.swift | 28 +++ .../MetricsAdapterTest.swift | 31 +++ .../AdaptingCircularBufferCounterTests.swift | 72 ++++++ .../AdaptingIntegerArrayTests.swift | 111 ++++++++++ .../Aggregation/AggregationTests.swift | 7 +- ...ExponentialHistogramAggregationTests.swift | 55 +++++ ...2ExponentialHistogramAggregatorTests.swift | 145 ++++++++++++ ...ase2ExponentialHistogramBucketsTests.swift | 52 +++++ .../Data/StableMetricDataTests.swift | 38 +++- 22 files changed, 1314 insertions(+), 237 deletions(-) create mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingCircularBufferCounter.swift create mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingIntegerArray.swift delete mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialBucketHistogramAggregation.swift create mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialHistogramAggregation.swift create mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/EmptyExponentialHistogramBuckets.swift delete mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramData.swift create mode 100644 Sources/OpenTelemetrySdk/Metrics/Stable/Exemplar/LongToDoubleExemplarReservoir.swift create mode 100644 Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingCircularBufferCounterTests.swift create mode 100644 Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingIntegerArrayTests.swift create mode 100644 Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/Base2ExponentialHistogramAggregationTests.swift create mode 100644 Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramAggregatorTests.swift create mode 100644 Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramBucketsTests.swift diff --git a/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift b/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift index dce378eb..9c734441 100644 --- a/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift +++ b/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift @@ -152,12 +152,57 @@ public enum MetricsAdapter { protoMetric.histogram.aggregationTemporality = stableMetric.data.aggregationTemporality.convertToProtoEnum() protoMetric.histogram.dataPoints.append(protoDataPoint) case .ExponentialHistogram: - // TODO: implement - break + guard let exponentialHistogramData = $0 as? ExponentialHistogramPointData else { + break + } + var protoDataPoint = Opentelemetry_Proto_Metrics_V1_ExponentialHistogramDataPoint() + injectPointData(protoExponentialHistogramPoint: &protoDataPoint, pointData: exponentialHistogramData) + protoDataPoint.scale = Int32(exponentialHistogramData.scale) + protoDataPoint.sum = Double(exponentialHistogramData.sum) + protoDataPoint.count = UInt64(exponentialHistogramData.count) + protoDataPoint.zeroCount = UInt64(exponentialHistogramData.zeroCount) + protoDataPoint.max = exponentialHistogramData.max + protoDataPoint.min = exponentialHistogramData.min + + var positiveBuckets = Opentelemetry_Proto_Metrics_V1_ExponentialHistogramDataPoint.Buckets() + positiveBuckets.offset = Int32(exponentialHistogramData.positiveBuckets.offset) + positiveBuckets.bucketCounts = exponentialHistogramData.positiveBuckets.bucketCounts.map { UInt64($0) } + + var negativeBuckets = Opentelemetry_Proto_Metrics_V1_ExponentialHistogramDataPoint.Buckets() + negativeBuckets.offset = Int32(exponentialHistogramData.negativeBuckets.offset) + negativeBuckets.bucketCounts = exponentialHistogramData.negativeBuckets.bucketCounts.map { UInt64($0) } + + protoDataPoint.positive = positiveBuckets + protoDataPoint.negative = negativeBuckets + + protoMetric.exponentialHistogram.aggregationTemporality = stableMetric.data.aggregationTemporality.convertToProtoEnum() + protoMetric.exponentialHistogram.dataPoints.append(protoDataPoint) } } return protoMetric } + + static func injectPointData(protoExponentialHistogramPoint protoPoint: inout Opentelemetry_Proto_Metrics_V1_ExponentialHistogramDataPoint, pointData: PointData) { + protoPoint.timeUnixNano = pointData.endEpochNanos + protoPoint.startTimeUnixNano = pointData.startEpochNanos + + pointData.attributes.forEach { + protoPoint.attributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value)) + } + + pointData.exemplars.forEach { + var protoExemplar = Opentelemetry_Proto_Metrics_V1_Exemplar() + protoExemplar.timeUnixNano = $0.epochNanos + + $0.filteredAttributes.forEach { + protoExemplar.filteredAttributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value)) + } + if let spanContext = $0.spanContext { + protoExemplar.spanID = TraceProtoUtils.toProtoSpanId(spanId: spanContext.spanId) + protoExemplar.traceID = TraceProtoUtils.toProtoTraceId(traceId: spanContext.traceId) + } + } + } static func injectPointData(protoHistogramPoint protoPoint: inout Opentelemetry_Proto_Metrics_V1_HistogramDataPoint, pointData: PointData) { protoPoint.timeUnixNano = pointData.endEpochNanos diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingCircularBufferCounter.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingCircularBufferCounter.swift new file mode 100644 index 00000000..366e48b7 --- /dev/null +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingCircularBufferCounter.swift @@ -0,0 +1,91 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +class AdaptingCircularBufferCounter: NSCopying { + func copy(with zone: NSZone? = nil) -> Any { + let copy = AdaptingCircularBufferCounter(maxSize: maxSize) + copy.startIndex = startIndex + copy.endIndex = endIndex + copy.baseIndex = baseIndex + copy.backing = backing.copy() as! AdaptingIntegerArray + return copy + } + + public private(set) var endIndex = Int.nullIndex + public private(set) var startIndex = Int.nullIndex + private var baseIndex = Int.nullIndex + private var backing: AdaptingIntegerArray + private let maxSize: Int + + init(maxSize: Int) { + backing = AdaptingIntegerArray(size: maxSize) + self.maxSize = maxSize + } + + @discardableResult func increment(index: Int, delta: Int64) -> Bool{ + if baseIndex == Int.min { + startIndex = index + endIndex = index + baseIndex = index + backing.increment(index: 0, count: delta) + return true + } + + if index > endIndex { + if (index - startIndex + 1) > backing.length() { + return false + } + endIndex = index + } else if index < startIndex { + if (endIndex - index + 1) > backing.length() { + return false + } + self.startIndex = index + } + + let realIndex = toBufferIndex(index: index) + backing.increment(index: realIndex, count: delta) + return true + } + + func get(index: Int) -> Int64 { + if (index < startIndex || index > endIndex) { + return 0 + } else { + return backing.get(index: toBufferIndex(index: index)) + } + } + + func isEmpty() -> Bool { + return baseIndex == Int.nullIndex + } + + func getMaxSize() -> Int { + return backing.length() + } + + func clear() { + backing.clear() + baseIndex = Int.nullIndex + startIndex = Int.nullIndex + endIndex = Int.nullIndex + } + + private func toBufferIndex(index: Int) -> Int { + var result = index - baseIndex + if (result >= backing.length()) { + result -= backing.length() + } else if (result < 0) { + result += backing.length() + } + return result + } +} + +extension Int { + static let nullIndex = Int.min +} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingIntegerArray.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingIntegerArray.swift new file mode 100644 index 00000000..c963094d --- /dev/null +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AdaptingIntegerArray.swift @@ -0,0 +1,163 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +class AdaptingIntegerArray: NSCopying { + + func copy(with zone: NSZone? = nil) -> Any { + let copy = AdaptingIntegerArray(size: size) + copy.cellSize = cellSize + switch (cellSize) { + case .byte: + copy.byteBacking = byteBacking + case .short: + copy.shortBacking = shortBacking + case .int: + copy.intBacking = intBacking + case .long: + copy.longBacking = longBacking + } + return copy + } + + var byteBacking: Array? + var shortBacking: Array? + var intBacking: Array? + var longBacking: Array? + var size: Int + + enum ArrayCellSize { + case byte + case short + case int + case long + } + + var cellSize: ArrayCellSize + + init(size: Int) { + self.size = size + cellSize = ArrayCellSize.byte + byteBacking = Array(repeating: Int8(0), count: size) + } + + func increment(index: Int, count: Int64) { + + if cellSize == .byte, var byteBacking = self.byteBacking { + let result = Int64(byteBacking[index]) + count + if result > Int8.max { + resizeToShort() + increment(index: index, count: count) + } else { + byteBacking[index] = Int8(result) + self.byteBacking = byteBacking + } + } else if cellSize == .short, var shortBacking = self.shortBacking { + let result = Int64(shortBacking[index]) + count + if result > Int16.max { + resizeToInt() + increment(index: index, count: count) + } else { + shortBacking[index] = Int16(result) + self.shortBacking = shortBacking + } + } else if cellSize == .int, var intBacking = self.intBacking { + let result = Int64(intBacking[index]) + count + if result > Int32.max { + resizeToLong() + increment(index: index, count: count) + } else { + intBacking[index] = Int32(result) + self.intBacking = intBacking + } + } else if cellSize == .long, var longBacking = self.longBacking { + let result = longBacking[index] + count + longBacking[index] = result + self.longBacking = longBacking + } + } + + func get(index: Int) -> Int64 { + + if cellSize == .byte, let byteBacking = self.byteBacking, index < byteBacking.count { + return Int64(byteBacking[index]) + } else if cellSize == .short, let shortBacking = self.shortBacking, index < shortBacking.count { + return Int64(shortBacking[index]) + } else if cellSize == .int, let intBacking = self.intBacking, index < intBacking.count { + return Int64(intBacking[index]) + } else if cellSize == .long, let longBacking = self.longBacking, index < longBacking.count { + return longBacking[index] + } + + return Int64(0) + } + + func length() -> Int { + var length = 0 + + if cellSize == .byte, let byteBacking = self.byteBacking { + length = byteBacking.count + } else if cellSize == .short, let shortBacking = self.shortBacking { + length = shortBacking.count + } else if cellSize == .int, let intBacking = self.intBacking { + length = intBacking.count + } else if cellSize == .long, let longBacking = self.longBacking { + length = longBacking.count + } + + return length + } + + func clear() { + switch (cellSize) { + case .byte: + byteBacking = Array(repeating: Int8(0), count: byteBacking?.count ?? 0) + case .short: + shortBacking = Array(repeating: Int16(0), count: shortBacking?.count ?? 0) + case .int: + intBacking = Array(repeating: Int32(0), count: intBacking?.count ?? 0) + case .long: + longBacking = Array(repeating: Int64(0), count: longBacking?.count ?? 0) + } + } + + private func resizeToShort() { + guard let byteBacking = byteBacking else { return } + var tmpShortBacking: Array = Array(repeating: Int16(0), count: byteBacking.count) + + for (index, value) in byteBacking.enumerated() { + tmpShortBacking[index] = Int16(value) + } + cellSize = ArrayCellSize.short + shortBacking = tmpShortBacking + self.byteBacking = nil + } + + private func resizeToInt() { + guard let shortBacking = shortBacking else { return } + var tmpIntBacking: Array = Array(repeating: Int32(0), count: shortBacking.count) + + for (index, value) in shortBacking.enumerated() { + tmpIntBacking[index] = Int32(value) + } + cellSize = ArrayCellSize.int + intBacking = tmpIntBacking + self.shortBacking = nil + } + + private func resizeToLong() { + guard let intBacking = intBacking else { return } + var tmpLongBacking: Array = Array(repeating: Int64(0), count: intBacking.count) + + for (index, value) in intBacking.enumerated() { + tmpLongBacking[index] = Int64(value) + } + cellSize = ArrayCellSize.long + longBacking = tmpLongBacking + self.intBacking = nil + } +} + diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Aggregation.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Aggregation.swift index bfcae9a2..830285bb 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Aggregation.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Aggregation.swift @@ -30,11 +30,11 @@ public enum Aggregations { ExplicitBucketHistogramAggregation(bucketBoundaries: buckets) } - static func base2ExponentialBucketHistogram() { - // todo + static func base2ExponentialBucketHistogram() -> Aggregation { + Base2ExponentialHistogramAggregation.instance } - static func base2ExponentialBucketHistogram(maxBuckets: Int, maxScale: Int) { - // todo + static func base2ExponentialBucketHistogram(maxBuckets: Int, maxScale: Int) -> Aggregation { + Base2ExponentialHistogramAggregation(maxBuckets: maxBuckets, maxScale: maxScale) } } diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialBucketHistogramAggregation.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialBucketHistogramAggregation.swift deleted file mode 100644 index 1deaabc0..00000000 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialBucketHistogramAggregation.swift +++ /dev/null @@ -1,40 +0,0 @@ -//// -//// Copyright The OpenTelemetry Authors -//// SPDX-License-Identifier: Apache-2.0 -//// -// -//import Foundation -//import OpenTelemetryApi -// -//public class Base2ExponentialBucketHistogramAggregation : Aggregation { -// private static let defaultMaxBuckets = 160 -// private static let defaultMaxScale = 20 -// -// public private(set) static var instance = Base2ExponentialBucketHistogramAggregation(maxBuckets: defaultMaxBuckets, maxScale: defaultMaxScale) -// -// -// private let maxBuckets : Int -// private let maxScale : Int -// -// public init(maxBuckets : Int, maxScale : Int) { -// -// self.maxScale = maxScale <= 20 && maxScale >= -10 ? maxScale : Self.defaultMaxScale -// self.maxBuckets = maxBuckets > 0 ? maxBuckets : Self.defaultMaxScale -// } -// -// public func createAggregator(descriptor: InstrumentDescriptor, exemplarFilter: ExemplarFilter) -> StableAggregator { -// <#code#> -// } -// -// public func isCompatible(with descriptor: InstrumentDescriptor) -> Bool { -// switch descriptor.type { -// case .counter, .histogram: -// return true -// default: -// return false -// } -// } -// -// -// -//} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialHistogramAggregation.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialHistogramAggregation.swift new file mode 100644 index 00000000..bcafec86 --- /dev/null +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/Base2ExponentialHistogramAggregation.swift @@ -0,0 +1,39 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import OpenTelemetryApi + +public class Base2ExponentialHistogramAggregation : Aggregation { + private static let defaultMaxBuckets = 160 + private static let defaultMaxScale = 20 + + public private(set) static var instance = Base2ExponentialHistogramAggregation(maxBuckets: defaultMaxBuckets, maxScale: defaultMaxScale) + + + internal let maxBuckets : Int + internal let maxScale : Int + + public init(maxBuckets : Int, maxScale : Int) { + self.maxScale = maxScale <= 20 && maxScale >= -10 ? maxScale : Self.defaultMaxScale + self.maxBuckets = maxBuckets >= 2 ? maxBuckets : Self.defaultMaxBuckets + } + + public func createAggregator(descriptor: InstrumentDescriptor, exemplarFilter: ExemplarFilter) -> StableAggregator { + DoubleBase2ExponentialHistogramAggregator(maxBuckets: self.maxBuckets, maxScale: self.maxScale) { + FilteredExemplarReservoir(filter: exemplarFilter, reservoir: LongToDoubleExemplarReservoir(reservoir: RandomFixedSizedExemplarReservoir.createDouble(clock: MillisClock(), size: 2))) + } + } + + public func isCompatible(with descriptor: InstrumentDescriptor) -> Bool { + switch descriptor.type { + case .counter, .histogram: + return true + default: + return false + } + } +} + diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleBase2ExponentialHistogramAggregator.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleBase2ExponentialHistogramAggregator.swift index 12d528cd..981c23a6 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleBase2ExponentialHistogramAggregator.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleBase2ExponentialHistogramAggregator.swift @@ -1,40 +1,172 @@ -//// -//// Copyright The OpenTelemetry Authors -//// SPDX-License-Identifier: Apache-2.0 -//// // -//import Foundation +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 // -//public class DoubleBase2ExponentialHistogramAggregator: StableAggregator { -// private var reservoirSupplier : () -> AnyExemplarReservoir -// private var maxBuckets : Int -// private var maxScale: Int -// -// init(reservoirSupplier: @escaping () -> AnyExemplarReservoir, maxBuckets: Int, maxScale: Int) { -// self.reservoirSupplier = reservoirSupplier -// self.maxBuckets = maxBuckets -// self.maxScale = maxScale -// } -// -// public func diff(previousCumulative: AnyPointData, currentCumulative: AnyPointData) throws -> AnyPointData { -// throw HistogramAggregatorError.unsupportedOperation("This aggregator does not support diff.") -// } -// -// public func toPoint(measurement: Measurement) throws -> AnyPointData { -// throw HistogramAggregatorError.unsupportedOperation("This aggregator does not support toPoint.") -// } -// -// -// public func createHandle() -> AggregatorHandle { -// <#code#> -// } -// -// public func toMetricData(resource: Resource, scope: InstrumentationScopeInfo, descriptor: MetricDescriptor, points: [AnyPointData], temporality: AggregationTemporality) -> StableMetricData { -// StableMetricData.createExponentialHistogram(resource: resource, instrumentationScopeInfo: scope, name: descriptor.name, description: descriptor.description, unit: descriptor.instrument.unit, data: StableExponentialHistogramData(aggregationTemporality: temporality, points: points)) -// } -// -// private class Handle : AggregatorHandle { -// var maxBuckets : Int -// -// } -//} + +import Foundation +import OpenTelemetryApi + +public class DoubleBase2ExponentialHistogramAggregator: StableAggregator { + private var reservoirSupplier : () -> ExemplarReservoir + private var maxBuckets : Int + private var maxScale: Int + + init(maxBuckets: Int, maxScale: Int, reservoirSupplier: @escaping () -> ExemplarReservoir) { + self.maxBuckets = maxBuckets + self.maxScale = maxScale + self.reservoirSupplier = reservoirSupplier + } + + public func diff(previousCumulative: PointData, currentCumulative: PointData) throws -> PointData { + throw HistogramAggregatorError.unsupportedOperation("This aggregator does not support diff.") + } + + public func toPoint(measurement: Measurement) throws -> PointData { + throw HistogramAggregatorError.unsupportedOperation("This aggregator does not support toPoint.") + } + + public func createHandle() -> AggregatorHandle { + return Handle(maxBuckets: maxBuckets, maxScale: maxScale, exemplarReservoir: reservoirSupplier()) + } + + public func toMetricData(resource: Resource, scope: InstrumentationScopeInfo, descriptor: MetricDescriptor, points: [PointData], temporality: AggregationTemporality) -> StableMetricData { + StableMetricData.createExponentialHistogram(resource: resource, instrumentationScopeInfo: scope, name: descriptor.name, description: descriptor.description, unit: descriptor.instrument.unit, data: StableExponentialHistogramData(aggregationTemporality: temporality, points: points)) + } + + class Handle : AggregatorHandle { + let lock = Lock() + var maxBuckets : Int + var maxScale: Int + + var zeroCount: UInt64 + var sum: Double + var min: Double + var max: Double + var count: UInt64 + var scale: Int + + var positiveBuckets: DoubleBase2ExponentialHistogramBuckets? + var negativeBuckets: DoubleBase2ExponentialHistogramBuckets? + + internal init(maxBuckets: Int, maxScale: Int, exemplarReservoir: ExemplarReservoir) { + self.maxBuckets = maxBuckets + self.maxScale = maxScale + + self.sum = 0 + self.zeroCount = 0 + self.min = Double.greatestFiniteMagnitude + self.max = -1 + self.count = 0 + self.scale = maxScale + + super.init(exemplarReservoir: exemplarReservoir) + } + + override func doRecordLong(value: Int) { + doRecordDouble(value: Double(value)) + } + + override func doAggregateThenMaybeReset(startEpochNano: UInt64, endEpochNano: UInt64, attributes: [String : AttributeValue], exemplars: [ExemplarData], reset: Bool) -> PointData { + lock.lock() + defer { + lock.unlock() + } + + let pointData = ExponentialHistogramPointData( + scale: scale, + sum: sum, + zeroCount: Int64(zeroCount), + hasMin: count > 0, + hasMax: count > 0, + min: min, + max: max, + positiveBuckets: resolveBuckets(buckets: positiveBuckets, scale: scale, reset: reset), + negativeBuckets: resolveBuckets(buckets: negativeBuckets, scale: scale, reset: reset), + startEpochNanos: startEpochNano, + epochNanos: endEpochNano, + attributes: attributes, + exemplars: exemplars + ) + + if reset { + sum = 0 + zeroCount = 0 + min = Double.greatestFiniteMagnitude + max = -1 + count = 0 + scale = maxScale + } + + return pointData + } + + override func doRecordDouble(value: Double) { + lock.lock() + defer { + lock.unlock() + } + + if !value.isFinite { + return + } + + sum += value + + min = Swift.min(min, value) + max = Swift.max(max, value) + count += 1 + + var buckets: DoubleBase2ExponentialHistogramBuckets + if value == 0.0 { + self.zeroCount += 1 + return + } else if value > 0.0 { + if let positiveBuckets = self.positiveBuckets { + buckets = positiveBuckets + } else { + buckets = DoubleBase2ExponentialHistogramBuckets(scale: scale, maxBuckets: maxBuckets) + positiveBuckets = buckets + } + } else { + if let negativeBuckets = negativeBuckets { + buckets = negativeBuckets + } else { + buckets = DoubleBase2ExponentialHistogramBuckets(scale: scale, maxBuckets: maxBuckets) + negativeBuckets = buckets + } + } + + if !buckets.record(value: value) { + downScale(by: buckets.getScaleReduction(value)) + buckets.record(value: value) + } + } + + private func resolveBuckets(buckets: DoubleBase2ExponentialHistogramBuckets?, scale: Int, reset: Bool) -> ExponentialHistogramBuckets { + guard let buckets = buckets else { + return EmptyExponentialHistogramBuckets(scale: scale) + } + + let copy = buckets.copy() as! DoubleBase2ExponentialHistogramBuckets + + if reset { + buckets.clear(scale: maxScale) + } + + return copy + } + + func downScale(by: Int) { + if let positiveBuckets = positiveBuckets { + positiveBuckets.downscale(by: by) + scale = positiveBuckets.scale + + } + + if let negativeBuckets = negativeBuckets { + negativeBuckets.downscale(by: by) + scale = negativeBuckets.scale + } + } + } +} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/Base2ExponentialHistogramIndexer.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/Base2ExponentialHistogramIndexer.swift index b2d52578..8206d46d 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/Base2ExponentialHistogramIndexer.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/Base2ExponentialHistogramIndexer.swift @@ -1,76 +1,77 @@ -//// -//// Copyright The OpenTelemetry Authors -//// SPDX-License-Identifier: Apache-2.0 -//// // -//import Foundation -//import OpenTelemetryApi +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 // -// -//public class Base2ExponentialHistogramIndexer { -// private static var cache = [Int: Base2ExponentialHistogramIndexer]() -// private static var cacheLock = Lock() -// private static let LOG_BASE2_E = 1.0 / log(2) -// private static let EXPONENT_BIT_MASK : Int = 0x7FF0_0000_0000_0000 -// private static let SIGNIFICAND_BIT_MASK : Int = 0xF_FFFF_FFFF_FFFF -// private static let EXPONENT_BIAS : Int = 1023 -// private static let SIGNIFICAND_WIDTH : Int = 52 -// private static let EXPONENT_WIDTH : Int = 11 -// -// private let scale : Int -// private let scaleFactor : Double -// -// -// init(scale: Int) { -// self.scale = scale -// self.scaleFactor = computeScaleFactor(scale: scale) -// } -// -// func get(_ scale: Int) -> Base2ExponentialHistogramIndexer { -// Self.cacheLock.lock() -// defer { -// Self.cacheLock.unlock() -// } -// if let indexer = Self.cache[scale] { -// return indexer -// } else { -// let indexer = Base2ExponentialHistogramIndexer(scale: scale) -// Self.cache[scale] = indexer -// return indexer -// } -// } -// -// func computeIndex(_ value: Double) -> Int { -// let absValue = abs(value) -// if scale > 0 { -// return indexByLogarithm(absValue) -// } -// if scale == 0 { -// return mapToIndexScaleZero(absValue) -// } -// return mapToIndexScaleZero(absValue) >> -scale -// } -// -// func indexByLogarithm(_ value : Double) -> Int { -// Int(ceil(log(value) * scaleFactor) - 1) -// } -// -// func mapToIndexScaleZero(_ value : Double) -> Int { -// let raw = value.bitPattern -// var rawExponent = (Int(raw) & Self.EXPONENT_BIT_MASK) >> Self.SIGNIFICAND_WIDTH // does `value.exponentBitPattern` work here? -// let rawSignificand = Int(raw) & Self.SIGNIFICAND_BIT_MASK // does `value.significandBitPattern` work here? -// if rawExponent == 0 { -// rawExponent -= (rawSignificand - 1).leadingZeroBitCount - Self.EXPONENT_WIDTH - 1 -// } -// let ieeeExponent = rawExponent - Self.EXPONENT_BIAS -// if rawSignificand == 0 { -// return ieeeExponent - 1 -// } -// return ieeeExponent -// } -// -// -// func computeScaleFactor(scale: Int) -> Double { -// Self.LOG_BASE2_E * pow(2.0, Double(scale)) -// } -//} + +import Foundation +import OpenTelemetryApi + +public class Base2ExponentialHistogramIndexer { + private static var cache = [Int: Base2ExponentialHistogramIndexer]() + private static var cacheLock = Lock() + + private let scale : Int + private let scaleFactor : Double + + init(scale: Int) { + self.scale = scale + scaleFactor = Self.computeScaleFactor(scale: scale) + } + + func get(_ scale: Int) -> Base2ExponentialHistogramIndexer { + Self.cacheLock.lock() + defer { + Self.cacheLock.unlock() + } + if let indexer = Self.cache[scale] { + return indexer + } else { + let indexer = Base2ExponentialHistogramIndexer(scale: scale) + Self.cache[scale] = indexer + return indexer + } + } + + func computeIndex(_ value: Double) -> Int { + let absValue = abs(value) + if scale > 0 { + return indexByLogarithm(absValue) + } + if scale == 0 { + return mapToIndexScaleZero(absValue) + } + return mapToIndexScaleZero(absValue) >> -scale + } + + func indexByLogarithm(_ value : Double) -> Int { + Int(ceil(log(value) * scaleFactor) - 1) + } + + func mapToIndexScaleZero(_ value : Double) -> Int { + let raw = value.bitPattern + var rawExponent = Int((Int64(raw) & Int64(0x7FF0_0000_0000_0000)) >> Int.significandWidth) + let rawSignificand = Int(Int64(raw) & Int64(0xF_FFFF_FFFF_FFFF)) + if rawExponent == 0 { + rawExponent -= (rawSignificand - 1).leadingZeroBitCount - Int.exponentWidth - 1 + } + let ieeeExponent = rawExponent - Int.exponentBias + if rawSignificand == 0 { + return ieeeExponent - 1 + } + return ieeeExponent + } + + static func computeScaleFactor(scale: Int) -> Double { + Double.logBase2E * pow(2.0, Double(scale)) + } +} + +extension Int { + static let exponentBias = 1023 + static let significandWidth = 52 + static let exponentWidth = 11 +} + +extension Double { + static let logBase2E = 1.0 / log(2) +} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/DoubleBase2ExponentialHistogramBuckets.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/DoubleBase2ExponentialHistogramBuckets.swift index 3a656448..1de52337 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/DoubleBase2ExponentialHistogramBuckets.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/DoubleBase2ExponentialHistogramBuckets.swift @@ -1,23 +1,124 @@ // // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// - -//import Foundation -//import OpenTelemetryApi // -//public class DoubleBase2ExponentialHistogramBuckets : ExponentialHistogramBuckets { -// public var scale: Int -// -// public var offset: Int -// -// public var bucketCounts: [Int] -// -// public var totalCount: Int -// -// var base2ExponentialHistogramIndexer : Base2ExponentialHistogramIndexer -// -// init(scale: Int, maxBuckets: Int) { -// } -// -//} + +import Foundation +import OpenTelemetryApi + +public class DoubleBase2ExponentialHistogramBuckets: ExponentialHistogramBuckets, NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + let copy = DoubleBase2ExponentialHistogramBuckets(scale: scale, maxBuckets: 0) + copy.counts = counts.copy() as! AdaptingCircularBufferCounter + copy.base2ExponentialHistogramIndexer = base2ExponentialHistogramIndexer + copy.totalCount = totalCount + return copy + } + + public var totalCount: Int + public var scale: Int + + public var offset: Int { + get { + if self.counts.isEmpty() { + return 0 + } else { + return self.counts.startIndex + } + } + } + + public var bucketCounts: [Int64] { + get { + if self.counts.isEmpty() { + return [] + } + + let length = self.counts.endIndex - self.counts.startIndex + 1 + var countsArr: Array = Array(repeating: Int64(0), count: length) + + for i in 0.. Bool { + guard value != 0.0 else { return false } + + let index = self.base2ExponentialHistogramIndexer.computeIndex(value) + let recordingSuccessful = self.counts.increment(index: index, delta: 1) + if recordingSuccessful { + self.totalCount += 1 + } + return recordingSuccessful + } + + func downscale(by: Int) { + if by == 0 { + return + } else if by < 0 { + return + } + + if !self.counts.isEmpty() { + let newCounts = self.counts.copy() as! AdaptingCircularBufferCounter + newCounts.clear() + + for i in self.counts.startIndex...self.counts.endIndex { + let count = self.counts.get(index: i) + if count > 0 { + if !newCounts.increment(index: i >> by, delta: count) { + return + } + } + } + + self.counts = newCounts + } + + self.scale = self.scale - by + self.base2ExponentialHistogramIndexer = Base2ExponentialHistogramIndexer(scale: self.scale) + } + + func getScaleReduction(_ value: Double) -> Int { + let index = self.base2ExponentialHistogramIndexer.computeIndex(value) + let newStart = Swift.min(index, self.counts.startIndex) + let newEnd = Swift.max(index, self.counts.endIndex) + return getScaleReduction(newStart: newStart, newEnd: newEnd) + } + + func getScaleReduction(newStart: Int, newEnd: Int) -> Int { + var scaleReduction = 0 + var newStart = newStart + var newEnd = newEnd + + while (newEnd - newStart + 1 > self.counts.getMaxSize()) { + newStart >>= 1 + newEnd >>= 1 + scaleReduction += 1 + } + + return scaleReduction + } +} + diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/EmptyExponentialHistogramBuckets.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/EmptyExponentialHistogramBuckets.swift new file mode 100644 index 00000000..32e2de8a --- /dev/null +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/EmptyExponentialHistogramBuckets.swift @@ -0,0 +1,19 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +public class EmptyExponentialHistogramBuckets: ExponentialHistogramBuckets { + + public var scale: Int + public var offset: Int = 0 + public var bucketCounts: [Int64] = [] + public var totalCount: Int = 0 + + init(scale: Int) { + self.scale = scale + } +} + diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramBuckets.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramBuckets.swift index 432fe2bb..49fa0f0f 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramBuckets.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramBuckets.swift @@ -1,7 +1,7 @@ // // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -// +// import Foundation import OpenTelemetryApi @@ -9,6 +9,6 @@ import OpenTelemetryApi public protocol ExponentialHistogramBuckets { var scale : Int { get } var offset : Int { get } - var bucketCounts : [Int] { get } + var bucketCounts : [Int64] { get } var totalCount : Int { get } } diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramData.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramData.swift deleted file mode 100644 index 4bbe040f..00000000 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramData.swift +++ /dev/null @@ -1,6 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramPointData.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramPointData.swift index 872f598a..45905948 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramPointData.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/Internal/ExponentialHistogramPointData.swift @@ -1,47 +1,52 @@ -//// -//// Copyright The OpenTelemetry Authors -//// SPDX-License-Identifier: Apache-2.0 -//// // -// import Foundation -// import OpenTelemetryApi -// -// public class ImmutableExponentialHistogramPointData : AnyPointData, ExponentialHistogramPointData { -// public var scale: Int -// -// public var sum: Double -// -// public var count: Int -// -// public var zeroCount: Int -// -// public var hasMin: Bool -// -// public var hasMax: Bool -// -// public var max: Double -// -// public var positiveBuckets: ExponentialHistogramBuckets -// -// public var negativeBuckets: ExponentialHistogramBuckets -// -// public init(scale: Int, sum: Double, zeroCount: Int, hasMin: Bool, hasMax: Bool, max: Double, positiveBuckets: ExponentialHistogramBuckets, negativeBuckets: ExponentialHistogramBuckets, startEpochNanos: UInt64, epochNanos: UInt64, attributes: [String: AttributeValue], exemplars: [AnyDoubleExemplarData]) { -// self.scale = scale -// self.sum = sum -// self.zeroCount = zeroCount -// self.hasMin = hasMin -// self.hasMax = hasMax -// self.max = max -// self.positiveBuckets = positiveBuckets -// self.negativeBuckets = negativeBuckets -// -// self.count = zeroCount + positiveBuckets.totalCount + negativeBuckets.totalCount -// -// super.init(startEpochNanos: startEpochNanos, endEpochNanos: epochNanos, attributes: attributes, exemplars: exemplars) -// -// } -// -// -// -// -// } +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import OpenTelemetryApi + +public class ExponentialHistogramPointData: PointData { + + public var scale: Int + public var sum: Double + public var count: Int + public var zeroCount: Int64 + public var hasMin: Bool + public var hasMax: Bool + public var min: Double + public var max: Double + public var positiveBuckets: ExponentialHistogramBuckets + public var negativeBuckets: ExponentialHistogramBuckets + + public init( + scale: Int, + sum: Double, + zeroCount: Int64, + hasMin: Bool, + hasMax: Bool, + min: Double, + max: Double, + positiveBuckets: ExponentialHistogramBuckets, + negativeBuckets: ExponentialHistogramBuckets, + startEpochNanos: UInt64, + epochNanos: UInt64, + attributes: [String: AttributeValue], + exemplars: [ExemplarData] + ) { + + self.scale = scale + self.sum = sum + self.zeroCount = zeroCount + self.hasMin = hasMin + self.hasMax = hasMax + self.min = min + self.max = max + self.positiveBuckets = positiveBuckets + self.negativeBuckets = negativeBuckets + + self.count = Int(zeroCount) + positiveBuckets.totalCount + negativeBuckets.totalCount + + super.init(startEpochNanos: startEpochNanos, endEpochNanos: epochNanos, attributes: attributes, exemplars: exemplars) + } +} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Exemplar/LongToDoubleExemplarReservoir.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Exemplar/LongToDoubleExemplarReservoir.swift new file mode 100644 index 00000000..5cd9135c --- /dev/null +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Exemplar/LongToDoubleExemplarReservoir.swift @@ -0,0 +1,28 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import OpenTelemetryApi + +public class LongToDoubleExemplarReservoir: ExemplarReservoir { + let reservoir: ExemplarReservoir + + public init(reservoir: ExemplarReservoir) { + self.reservoir = reservoir + } + + public override func collectAndReset(attribute: [String: AttributeValue]) -> [ExemplarData] { + return reservoir.collectAndReset(attribute: attribute) + } + + public override func offerDoubleMeasurement(value: Double, attributes: [String: OpenTelemetryApi.AttributeValue]) { + return reservoir.offerDoubleMeasurement(value: value, attributes: attributes) + } + + public override func offerLongMeasurement(value: Int, attributes: [String: OpenTelemetryApi.AttributeValue]) { + return offerDoubleMeasurement(value: Double(value), attributes: attributes) + } +} + diff --git a/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift b/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift index 3e3dd5b9..8bdc30c3 100644 --- a/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift +++ b/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift @@ -107,4 +107,35 @@ final class MetricsAdapterTest: XCTestCase { XCTAssertEqual(value?.first?.sum, sum) XCTAssertEqual(value?.first?.count, UInt64(count)) } + + func testToProtoResourceMetricsWithExponentialHistogram() throws { + let positivieBuckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + positivieBuckets.downscale(by: 20) + positivieBuckets.record(value: 10.0) + positivieBuckets.record(value: 40.0) + positivieBuckets.record(value: 90.0) + positivieBuckets.record(value: 100.0) + let negativeBuckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + + let expHistogramPointData = ExponentialHistogramPointData(scale: 20, sum: 240.0, zeroCount: 0, hasMin: true, hasMax: true, min: 10.0, max: 100.0, positiveBuckets: positivieBuckets, negativeBuckets: negativeBuckets, startEpochNanos: 0, epochNanos: 1, attributes: [:], exemplars: []) + + let points = [expHistogramPointData] + let histogramData = StableExponentialHistogramData(aggregationTemporality: .delta, points: points) + let metricData = StableMetricData.createExponentialHistogram(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, data: histogramData) + + let result = MetricsAdapter.toProtoMetric(stableMetric: metricData) + guard let value = result?.exponentialHistogram.dataPoints as? [Opentelemetry_Proto_Metrics_V1_ExponentialHistogramDataPoint]? else { + let actualType = type(of: result?.gauge.dataPoints) + let errorMessage = "Got wrong type: \(actualType)" + XCTFail(errorMessage) + throw errorMessage + } + + XCTAssertEqual(value?.first?.scale, 20) + XCTAssertEqual(value?.first?.sum, 240) + XCTAssertEqual(value?.first?.count, 4) + XCTAssertEqual(value?.first?.min, 10) + XCTAssertEqual(value?.first?.max, 100) + XCTAssertEqual(value?.first?.zeroCount, 0) + } } diff --git a/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingCircularBufferCounterTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingCircularBufferCounterTests.swift new file mode 100644 index 00000000..9a6b4c05 --- /dev/null +++ b/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingCircularBufferCounterTests.swift @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@testable import OpenTelemetrySdk +import XCTest + +final class AdaptingCircularBufferCounterTests: XCTestCase { + + public func testReturnsZeroOutsidePopulatedRange() { + let counter = AdaptingCircularBufferCounter(maxSize: 10) + XCTAssertEqual(counter.get(index: 0), 0) + XCTAssertEqual(counter.get(index: 100), 0) + XCTAssertTrue(counter.increment(index: 2, delta: 1)) + XCTAssertFalse(counter.increment(index: 99, delta: 1)) + XCTAssertEqual(counter.get(index: 0), 0) + XCTAssertEqual(counter.get(index: 2), 1) + XCTAssertEqual(counter.get(index: 99), 0) + XCTAssertEqual(counter.get(index: 100), 0) + } + + public func testExpandLower() { + let counter = AdaptingCircularBufferCounter(maxSize: 160) + + XCTAssertTrue(counter.increment(index: 10, delta: 10)) + XCTAssertTrue(counter.increment(index: 0, delta: 1)) + XCTAssertEqual(counter.get(index: 10), 10) + XCTAssertEqual(counter.get(index: 0), 1) + XCTAssertEqual(counter.startIndex, 0) + XCTAssertEqual(counter.endIndex, 10) + + XCTAssertTrue(counter.increment(index: 20, delta: 20)) + XCTAssertEqual(counter.get(index: 20), 20) + XCTAssertEqual(counter.get(index: 10), 10) + XCTAssertEqual(counter.get(index: 0), 1) + XCTAssertEqual(counter.startIndex, 0) + XCTAssertEqual(counter.endIndex, 20) + } + + public func testShouldFailAtLimit() { + let counter = AdaptingCircularBufferCounter(maxSize: 160) + XCTAssertTrue(counter.increment(index: 0, delta: 1)) + XCTAssertTrue(counter.increment(index: 120, delta: 12)) + + XCTAssertEqual(counter.startIndex, 0) + XCTAssertEqual(counter.endIndex, 120) + XCTAssertEqual(counter.get(index: 0), 1) + XCTAssertEqual(counter.get(index: 120), 12) + + XCTAssertFalse(counter.increment(index: 161, delta: 1)) + } + + public func testShouldCopyCounter() { + let counter = AdaptingCircularBufferCounter(maxSize: 2) + XCTAssertTrue(counter.increment(index: 2, delta: 2)) + XCTAssertTrue(counter.increment(index: 1, delta: 1)) + XCTAssertFalse(counter.increment(index: 3, delta: 1)) + + let copy = counter.copy() as! AdaptingCircularBufferCounter + XCTAssertEqual(counter.get(index: 2), 2) + XCTAssertEqual(copy.get(index: 2), 2) + XCTAssertEqual(counter.getMaxSize(), copy.getMaxSize()) + XCTAssertEqual(counter.startIndex, copy.startIndex) + XCTAssertEqual(counter.endIndex, copy.endIndex) + + XCTAssertTrue(copy.increment(index: 2, delta: 2)) + XCTAssertEqual(copy.get(index: 2), 4) + XCTAssertEqual(counter.get(index: 2), 2) + } +} + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingIntegerArrayTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingIntegerArrayTests.swift new file mode 100644 index 00000000..8f344479 --- /dev/null +++ b/Tests/OpenTelemetrySdkTests/Metrics/Aggregators/AdaptingIntegerArrayTests.swift @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +@testable import OpenTelemetrySdk +import XCTest + +final class AdaptingIntegerArrayTests: XCTestCase { + + let intValues: [Int64] = [Int64(1), Int64(127 + 1), Int64(32767 + 1), Int64(2147483647 + 1)] + + public func testSize() { + for intValue in intValues { + let arr = AdaptingIntegerArray(size: 10) + XCTAssertEqual(arr.length(), 10) + arr.increment(index: 0, count: intValue) + XCTAssertEqual(arr.length(), 10) + } + } + + public func testIncrementAndGet() { + for intValue in intValues { + let arr = AdaptingIntegerArray(size: 10) + + for i in 0..<10 { + XCTAssertEqual(arr.get(index: i), 0) + arr.increment(index: i, count: Int64(1)) + XCTAssertEqual(arr.get(index: i), 1) + arr.increment(index: i, count: intValue) + arr.increment(index: i, count: Int64(intValue + 1)) + } + } + } + + public func testCopy() { + for intValue in intValues { + let arr = AdaptingIntegerArray(size: 1) + arr.increment(index: 0, count: intValue) + + let copy = arr.copy() as! AdaptingIntegerArray + XCTAssertEqual(arr.get(index: 0), intValue) + + arr.increment(index: 0, count: 1) + XCTAssertEqual(arr.get(index: 0), intValue + 1) + XCTAssertEqual(copy.get(index: 0), intValue) + } + } + + public func testClear() { + for intValue in intValues { + let arr = AdaptingIntegerArray(size: 1) + arr.increment(index: 0, count: intValue) + XCTAssertEqual(arr.get(index: 0), intValue) + + arr.clear() + arr.increment(index: 0, count: 1) + XCTAssertEqual(arr.get(index: 0), 1) + } + } + + public func testHandleResize() { + let arr = AdaptingIntegerArray(size: 4) + let byteValue = Int64(1) + arr.increment(index: 0, count: byteValue) + XCTAssertEqual(arr.get(index: 0), byteValue) + XCTAssertEqual(arr.cellSize, .byte) + XCTAssertNotNil(arr.byteBacking) + XCTAssertEqual(arr.shortBacking, nil) + XCTAssertEqual(arr.intBacking, nil) + XCTAssertEqual(arr.longBacking, nil) + XCTAssertEqual(arr.length(), 4) + + let shortValue = Int64(127 + 1) + arr.increment(index: 1, count: shortValue) + XCTAssertEqual(arr.get(index: 0), byteValue) + XCTAssertEqual(arr.get(index: 1), shortValue) + XCTAssertEqual(arr.cellSize, .short) + XCTAssertNotNil(arr.shortBacking) + XCTAssertEqual(arr.byteBacking, nil) + XCTAssertEqual(arr.intBacking, nil) + XCTAssertEqual(arr.longBacking, nil) + XCTAssertEqual(arr.length(), 4) + + let intValue = Int64(32767 + 1) + arr.increment(index: 2, count: intValue) + XCTAssertEqual(arr.get(index: 0), byteValue) + XCTAssertEqual(arr.get(index: 1), shortValue) + XCTAssertEqual(arr.get(index: 2), intValue) + XCTAssertEqual(arr.cellSize, .int) + XCTAssertNotNil(arr.intBacking) + XCTAssertEqual(arr.byteBacking, nil) + XCTAssertEqual(arr.shortBacking, nil) + XCTAssertEqual(arr.longBacking, nil) + XCTAssertEqual(arr.length(), 4) + + let longValue = Int64(2147483647 + 1) + arr.increment(index: 3, count: longValue) + XCTAssertEqual(arr.get(index: 0), byteValue) + XCTAssertEqual(arr.get(index: 1), shortValue) + XCTAssertEqual(arr.get(index: 2), intValue) + XCTAssertEqual(arr.get(index: 3), longValue) + XCTAssertEqual(arr.cellSize, .long) + XCTAssertNotNil(arr.longBacking) + XCTAssertEqual(arr.byteBacking, nil) + XCTAssertEqual(arr.shortBacking, nil) + XCTAssertEqual(arr.intBacking, nil) + XCTAssertEqual(arr.length(), 4) + } +} + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/AggregationTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/AggregationTests.swift index bd47ad39..56909a6a 100644 --- a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/AggregationTests.swift +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/AggregationTests.swift @@ -35,10 +35,13 @@ public class AggregationsTests: XCTestCase { } func testBase2ExponentialBucketHistogram() { - // TODO: write test case + XCTAssert(Aggregations.base2ExponentialBucketHistogram() === Base2ExponentialHistogramAggregation.instance) } func testBase2ExponentialBucketHistogramWithMaxBucketsAndMaxScale() { - // TODO: write test case + let aggregation = Aggregations.base2ExponentialBucketHistogram(maxBuckets: 150, maxScale: 10) as? Base2ExponentialHistogramAggregation + XCTAssertEqual(aggregation?.maxBuckets, 150) + XCTAssertEqual(aggregation?.maxScale, 10) } } + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/Base2ExponentialHistogramAggregationTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/Base2ExponentialHistogramAggregationTests.swift new file mode 100644 index 00000000..9067be87 --- /dev/null +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/Base2ExponentialHistogramAggregationTests.swift @@ -0,0 +1,55 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import OpenTelemetryApi +@testable import OpenTelemetrySdk +import XCTest + +class Base2ExponentialHistogramAggregationTests: XCTestCase { + + func testInit() { + XCTAssertNotNil(Base2ExponentialHistogramAggregation.instance) + XCTAssertNotNil(Base2ExponentialHistogramAggregation(maxBuckets: 160, maxScale: 20)) + } + + func testInvalidConfigsDefault() { + + // Current init doesn't throw exception for out of bounds config values; instead it falls back to the default values in iOS + + let invalidMaxBuckets = Base2ExponentialHistogramAggregation(maxBuckets: 0, maxScale: 20) + let invalidMaxScaleUpper = Base2ExponentialHistogramAggregation(maxBuckets: 2, maxScale: 21) + let invalidMaxScaleLower = Base2ExponentialHistogramAggregation(maxBuckets: 2, maxScale: -11) + + XCTAssertEqual(invalidMaxBuckets.maxBuckets, 160) + XCTAssertEqual(invalidMaxScaleUpper.maxScale, 20) + XCTAssertEqual(invalidMaxScaleLower.maxScale, 20) + } + + func testCreateAggregator() throws { + let aggregation = Base2ExponentialHistogramAggregation(maxBuckets: 160, maxScale: 20) + let descriptor = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .histogram, valueType: .double) + let exemplarFilter = AlwaysOnFilter() + let aggregator = try XCTUnwrap(aggregation.createAggregator(descriptor: descriptor, exemplarFilter: exemplarFilter) as? DoubleBase2ExponentialHistogramAggregator) + + XCTAssertNotNil(aggregator) + } + + func testIsCompatible() { + let aggregation = Base2ExponentialHistogramAggregation(maxBuckets: 160, maxScale: 20) + + let compatibleDescriptor1 = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .counter, valueType: .double) + let compatibleDescriptor2 = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .histogram, valueType: .double) + let incompatibleDescriptor1 = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .observableGauge, valueType: .double) + let incompatibleDescriptor2 = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .observableCounter, valueType: .double) + let incompatibleDescriptor3 = InstrumentDescriptor(name: "test", description: "test", unit: "unit", type: .observableUpDownCounter, valueType: .double) + + XCTAssertTrue(aggregation.isCompatible(with: compatibleDescriptor1)) + XCTAssertTrue(aggregation.isCompatible(with: compatibleDescriptor2)) + XCTAssertFalse(aggregation.isCompatible(with: incompatibleDescriptor1)) + XCTAssertFalse(aggregation.isCompatible(with: incompatibleDescriptor2)) + XCTAssertFalse(aggregation.isCompatible(with: incompatibleDescriptor3)) + } +} + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramAggregatorTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramAggregatorTests.swift new file mode 100644 index 00000000..e5eea633 --- /dev/null +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramAggregatorTests.swift @@ -0,0 +1,145 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import OpenTelemetryApi +@testable import OpenTelemetrySdk +import XCTest + +class DoubleBase2ExponentialHistogramAggregatorTests: XCTestCase { + + let reservoir = LongToDoubleExemplarReservoir(reservoir: RandomFixedSizedExemplarReservoir.createDouble(clock: MillisClock(), size: 2)) + + func valueToIndex(scale: Int, value: Double) -> Int { + let scaleFactor = (1.0 / log(2)) * pow(2.0, Double(scale)) + return Int(ceil(log(value) * scaleFactor) - 1) + } + + func testCreateHandle() { + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 150, maxScale: 10, reservoirSupplier: { ExemplarReservoir() }) + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + + XCTAssertEqual(handle.maxBuckets, 150) + XCTAssertEqual(handle.maxScale, 10) + XCTAssertEqual(handle.zeroCount, 0) + XCTAssertEqual(handle.min, Double.greatestFiniteMagnitude) + XCTAssertEqual(handle.max, -1) + XCTAssertEqual(handle.count, 0) + XCTAssertEqual(handle.scale, 10) + } + + func testDoRecordDouble() { + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 160, maxScale: 20, reservoirSupplier: { ExemplarReservoir() }) + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + + handle.doRecordDouble(value: 0.5) + handle.doRecordDouble(value: 1.0) + handle.doRecordDouble(value: 12.0) + handle.doRecordDouble(value: 15.213) + handle.doRecordDouble(value: 12.0) + handle.doRecordDouble(value: -13.2) + handle.doRecordDouble(value: -2.01) + handle.doRecordDouble(value: -1) + handle.doRecordDouble(value: 0.0) + handle.doRecordDouble(value: 0) + + let pointData = handle.aggregateThenMaybeReset(startEpochNano: 0, endEpochNano: 1, attributes: [:], reset: false) as! ExponentialHistogramPointData + let expectedScale = 5 + let positiveCounts = pointData.positiveBuckets.bucketCounts + let negativeCounts = pointData.negativeBuckets.bucketCounts + + XCTAssertEqual(pointData.scale, expectedScale) + XCTAssertEqual(pointData.positiveBuckets.scale, expectedScale) + XCTAssertEqual(pointData.negativeBuckets.scale, expectedScale) + XCTAssertEqual(pointData.zeroCount, 2) + + let positiveOffset = pointData.positiveBuckets.offset + XCTAssertEqual(pointData.positiveBuckets.totalCount, 5) + XCTAssertEqual(positiveCounts[(valueToIndex(scale: expectedScale, value: 0.5) - positiveOffset)], 1) + XCTAssertEqual(positiveCounts[(valueToIndex(scale: expectedScale, value: 1.0) - positiveOffset)], 1) + XCTAssertEqual(positiveCounts[(valueToIndex(scale: expectedScale, value: 12.0) - positiveOffset)], 2) + XCTAssertEqual(positiveCounts[(valueToIndex(scale: expectedScale, value: 15.213) - positiveOffset)], 1) + + let negativeOffset = pointData.negativeBuckets.offset + XCTAssertEqual(pointData.negativeBuckets.totalCount, 3) + XCTAssertEqual(negativeCounts[(valueToIndex(scale: expectedScale, value: 13.2) - negativeOffset)], 1) + XCTAssertEqual(negativeCounts[(valueToIndex(scale: expectedScale, value: 2.01) - negativeOffset)], 1) + XCTAssertEqual(negativeCounts[(valueToIndex(scale: expectedScale, value: 1.0) - negativeOffset)], 1) + } + + func testInvalidRecording() { + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 160, maxScale: 20, reservoirSupplier: { ExemplarReservoir() }) + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + + handle.doRecordDouble(value: Double.infinity) + handle.doRecordDouble(value: Double.infinity * -1) + handle.doRecordDouble(value: Double.nan) + + let pointData = handle.aggregateThenMaybeReset(startEpochNano: 0, endEpochNano: 1, attributes: [:], reset: false) as! ExponentialHistogramPointData + XCTAssertNotNil(pointData) + XCTAssertEqual(pointData.sum, 0) + XCTAssertEqual(pointData.positiveBuckets.totalCount, 0) + XCTAssertEqual(pointData.negativeBuckets.totalCount, 0) + XCTAssertEqual(pointData.zeroCount, 0) + } + + func testAggregateThenMaybeResetWithExemplars() { + + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 160, maxScale: 20, reservoirSupplier: { self.reservoir }) + let attr: [String: AttributeValue] = ["test": .string("value")] + + let exemplar = DoubleExemplarData(value: 1.0, epochNanos: 2, filteredAttributes: attr) + + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + handle.recordDouble(value: 0, attributes: attr) + + let pointData = handle.aggregateThenMaybeReset(startEpochNano: 0, endEpochNano: 1, attributes: [:], reset: true) + XCTAssertNotNil(pointData) + let exemplars = pointData.exemplars + + let exemplarWithAttr = exemplars.filter{ return !$0.filteredAttributes.isEmpty } + XCTAssertEqual(exemplarWithAttr.count, 1) + XCTAssertEqual(exemplarWithAttr.first?.filteredAttributes, attr) + } + + func testAggregateThenMaybeReset() { + + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 160, maxScale: 20, reservoirSupplier: { ExemplarReservoir() }) + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + + handle.recordDouble(value: 5.0) + + let pointData = handle.aggregateThenMaybeReset(startEpochNano: 0, endEpochNano: 1, attributes: [:], reset: true) as! ExponentialHistogramPointData + let positiveBuckets = pointData.positiveBuckets.bucketCounts + XCTAssertNotNil(positiveBuckets) + XCTAssertEqual(positiveBuckets, [Int64(1)]) + } + + func testDownScale() { + + let aggregator = DoubleBase2ExponentialHistogramAggregator(maxBuckets: 160, maxScale: 20, reservoirSupplier: { ExemplarReservoir() }) + let handle = aggregator.createHandle() as! DoubleBase2ExponentialHistogramAggregator.Handle + + handle.recordDouble(value: 0.5) + handle.downScale(by: 20) + + handle.recordDouble(value: 1.0) + handle.recordDouble(value: 2.0) + handle.recordDouble(value: 4.0) + handle.recordDouble(value: 16.0) + + let pointData = handle.aggregateThenMaybeReset(startEpochNano: 0, endEpochNano: 1, attributes: [:], reset: true) as! ExponentialHistogramPointData + + XCTAssertEqual(pointData.scale, 0) + XCTAssertEqual(pointData.positiveBuckets.scale, 0) + XCTAssertEqual(pointData.negativeBuckets.scale, 0) + + let buckets = pointData.positiveBuckets + XCTAssertEqual(pointData.sum, 23.5) + XCTAssertEqual(buckets.offset, -2) + XCTAssertEqual(buckets.bucketCounts, [1, 1, 1, 1, 0, 1]) + XCTAssertEqual(buckets.totalCount, 5) + } +} + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramBucketsTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramBucketsTests.swift new file mode 100644 index 00000000..f25f2563 --- /dev/null +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Aggregation/DoubleBase2ExponentialHistogramBucketsTests.swift @@ -0,0 +1,52 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import OpenTelemetryApi +@testable import OpenTelemetrySdk +import XCTest + +class DoubleBase2ExponentialHistogramBucketsTests: XCTestCase { + + func testRecordValid() { + let buckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + XCTAssertTrue(buckets.record(value: 1)) + XCTAssertTrue(buckets.record(value: 1)) + XCTAssertTrue(buckets.record(value: 1)) + XCTAssertEqual(buckets.totalCount, 3) + XCTAssertEqual(buckets.bucketCounts, [3]) + } + + func testRecordZeroError() { + let buckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + XCTAssertFalse(buckets.record(value: 0)) + } + + func testDownscaleValid() { + let buckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + buckets.downscale(by: 20) + buckets.record(value: 1) + buckets.record(value: 2) + buckets.record(value: 4) + + XCTAssertEqual(buckets.scale, 0) + XCTAssertEqual(buckets.totalCount, 3) + XCTAssertEqual(buckets.bucketCounts, [1, 1, 1]) + XCTAssertEqual(buckets.offset, -1) + } + + func testClear() { + let buckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + XCTAssertTrue(buckets.record(value: 1)) + XCTAssertTrue(buckets.record(value: 1)) + XCTAssertEqual(buckets.totalCount, 2) + XCTAssertEqual(buckets.bucketCounts, [2]) + + buckets.clear(scale: 10) + XCTAssertEqual(buckets.totalCount, 0) + XCTAssertEqual(buckets.offset, 0) + XCTAssertEqual(buckets.bucketCounts, []) + } +} + diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift index 23a3f4d5..e7f7ca4f 100644 --- a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift @@ -20,7 +20,7 @@ class StableMetricDataTests: XCTestCase { let data = StableMetricData.Data(aggregationTemporality: .delta, points: emptyPointData) let metricData = StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, type: type, isMonotonic: false, data: data) - + assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data, data) @@ -73,6 +73,39 @@ class StableMetricDataTests: XCTestCase { XCTAssertNotNil(hpd) XCTAssertEqual(1, hpd.count) } + + func testCreateExponentialHistogramData() { + let type = MetricDataType.ExponentialHistogram + let positivieBuckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + positivieBuckets.downscale(by: 20) + positivieBuckets.record(value: 10.0) + positivieBuckets.record(value: 40.0) + positivieBuckets.record(value: 90.0) + positivieBuckets.record(value: 100.0) + + let negativeBuckets = DoubleBase2ExponentialHistogramBuckets(scale: 20, maxBuckets: 160) + + let expHistogramPointData = ExponentialHistogramPointData(scale: 20, sum: 240.0, zeroCount: 0, hasMin: true, hasMax: true, min: 10.0, max: 100.0, positiveBuckets: positivieBuckets, negativeBuckets: negativeBuckets, startEpochNanos: 0, epochNanos: 1, attributes: [:], exemplars: []) + + let points = [expHistogramPointData] + let histogramData = StableExponentialHistogramData(aggregationTemporality: .delta, points: points) + let metricData = StableMetricData.createExponentialHistogram(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, data: histogramData) + + assertCommon(metricData) + XCTAssertEqual(metricData.type, type) + XCTAssertEqual(metricData.data, histogramData) + XCTAssertEqual(metricData.data.aggregationTemporality, .delta) + XCTAssertEqual(metricData.isMonotonic, false) + + XCTAssertFalse(metricData.isEmpty()) + let histogramMetricData = metricData.data.points.first as! ExponentialHistogramPointData + XCTAssertEqual(histogramMetricData.scale, 20) + XCTAssertEqual(histogramMetricData.sum, 240) + XCTAssertEqual(histogramMetricData.count, 4) + XCTAssertEqual(histogramMetricData.min, 10) + XCTAssertEqual(histogramMetricData.max, 100) + XCTAssertEqual(histogramMetricData.zeroCount, 0) + } func testCreateDoubleGuage() { let type = MetricDataType.DoubleGauge @@ -132,9 +165,6 @@ class StableMetricDataTests: XCTestCase { XCTAssertEqual(metricData.isMonotonic, true) } - - - func assertCommon(_ metricData: StableMetricData) { XCTAssertEqual(metricData.resource, resource) XCTAssertEqual(metricData.instrumentationScopeInfo, instrumentationScopeInfo)