Skip to content

Commit

Permalink
Add support for Histogram metrics (#244)
Browse files Browse the repository at this point in the history
- Aggregator for histogram with set boundaries to sort into buckets
- OTLP exporter support
- Stub exporting support for Datadog and Prometheus
- Added unit tests for histogram aggregator
- Use cumulative aggregation for histogram data
- Use a default boundaries for histograms, optionally pass explicit boundaries
  • Loading branch information
trevor-dialpad authored Oct 12, 2021
1 parent 7387d58 commit eec1f8f
Show file tree
Hide file tree
Showing 17 changed files with 513 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal struct MetricUtils: Encodable {
switch metric.aggregationType {
case .doubleSum, .intSum:
return countType
case .doubleSummary, .intSummary, .intGauge, .doubleGauge:
case .doubleSummary, .intSummary, .intGauge, .doubleGauge, .doubleHistogram, .intHistogram:
return gaugeType
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ internal class MetricsExporter {
case .intSummary, .intGauge:
let summary = metricData as! SummaryData<Int>
return DDMetricPoint(timestamp: metricData.timestamp, value: Double(summary.sum))
case .intHistogram:
let histogram = metricData as! HistogramData<Int>
return DDMetricPoint(timestamp: metricData.timestamp, value: Double(histogram.sum))
case .doubleHistogram:
let histogram = metricData as! HistogramData<Double>
return DDMetricPoint(timestamp: metricData.timestamp, value: histogram.sum)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,48 @@ struct MetricsAdapter {
}

protoMetric.doubleSummary.dataPoints.append(protoDataPoint)
case .intHistogram:
guard let histogramData = $0 as? HistogramData<Int> else {
break
}
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_DoubleHistogramDataPoint()
protoDataPoint.sum = Double(histogramData.sum)
protoDataPoint.count = UInt64(histogramData.count)
protoDataPoint.startTimeUnixNano = histogramData.startTimestamp.timeIntervalSince1970.toNanoseconds
protoDataPoint.timeUnixNano = histogramData.timestamp.timeIntervalSince1970.toNanoseconds
protoDataPoint.explicitBounds = histogramData.buckets.boundaries.map { Double($0) }
protoDataPoint.bucketCounts = histogramData.buckets.counts.map { UInt64($0) }

histogramData.labels.forEach {
var kvp = Opentelemetry_Proto_Common_V1_StringKeyValue()
kvp.key = $0.key
kvp.value = $0.value
protoDataPoint.labels.append(kvp)
}

protoMetric.doubleHistogram.aggregationTemporality = .cumulative
protoMetric.doubleHistogram.dataPoints.append(protoDataPoint)
case .doubleHistogram:
guard let histogramData = $0 as? HistogramData<Double> else {
break
}
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_DoubleHistogramDataPoint()
protoDataPoint.sum = Double(histogramData.sum)
protoDataPoint.count = UInt64(histogramData.count)
protoDataPoint.startTimeUnixNano = histogramData.startTimestamp.timeIntervalSince1970.toNanoseconds
protoDataPoint.timeUnixNano = histogramData.timestamp.timeIntervalSince1970.toNanoseconds
protoDataPoint.explicitBounds = histogramData.buckets.boundaries.map { Double($0) }
protoDataPoint.bucketCounts = histogramData.buckets.counts.map { UInt64($0) }

histogramData.labels.forEach {
var kvp = Opentelemetry_Proto_Common_V1_StringKeyValue()
kvp.key = $0.key
kvp.value = $0.value
protoDataPoint.labels.append(kvp)
}

protoMetric.doubleHistogram.aggregationTemporality = .cumulative
protoMetric.doubleHistogram.dataPoints.append(protoDataPoint)
}
}
return protoMetric
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public enum PrometheusExporterExtensions {
let min = summary.min
let max = summary.max
output += PrometheusExporterExtensions.writeSummary(prometheusMetric: prometheusMetric, timeStamp: now, labels: labels, metricName: metric.name, sum: Double(sum), count: count, min: Double(min), max: Double(max))
case .intHistogram, .doubleHistogram:
break
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions Sources/OpenTelemetryApi/Metrics/BoundHistogramMetric.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation

/// Bound histogram metric
open class BoundHistogramMetric<T> {
public init(explicitBoundaries: Array<T>? = nil) {}

/// Record the given value to the bound histogram metric.
/// - Parameters:
/// - value: the histogram to be recorded.
open func record(value: T) {
}
}
71 changes: 71 additions & 0 deletions Sources/OpenTelemetryApi/Metrics/HistogramMetric.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation

/// Measure instrument.
public protocol HistogramMetric {
associatedtype T
/// Gets the bound histogram metric with given labelset.
/// - Parameters:
/// - labelset: The labelset from which bound instrument should be constructed.
/// - Returns: The bound histogram metric.

func bind(labelset: LabelSet) -> BoundHistogramMetric<T>

/// Gets the bound histogram metric with given labelset.
/// - Parameters:
/// - labels: The labels or dimensions associated with this value.
/// - Returns: The bound histogram metric.
func bind(labels: [String: String]) -> BoundHistogramMetric<T>
}

public extension HistogramMetric {
/// Records a histogram.
/// - Parameters:
/// - value: value to record.
/// - labelset: The labelset associated with this value.
func record(value: T, labelset: LabelSet) {
bind(labelset: labelset).record(value: value)
}

/// Records a histogram.
/// - Parameters:
/// - value: value to record.
/// - labels: The labels or dimensions associated with this value.
func record(value: T, labels: [String: String]) {
bind(labels: labels).record(value: value)
}
}

public struct AnyHistogramMetric<T>: HistogramMetric {
private let _bindLabelSet: (LabelSet) -> BoundHistogramMetric<T>
private let _bindLabels: ([String: String]) -> BoundHistogramMetric<T>

public init<U: HistogramMetric>(_ histogram: U) where U.T == T {
_bindLabelSet = histogram.bind(labelset:)
_bindLabels = histogram.bind(labels:)
}

public func bind(labelset: LabelSet) -> BoundHistogramMetric<T> {
_bindLabelSet(labelset)
}

public func bind(labels: [String: String]) -> BoundHistogramMetric<T> {
_bindLabels(labels)
}
}

public struct NoopHistogramMetric<T>: HistogramMetric {
public init() {}

public func bind(labelset: LabelSet) -> BoundHistogramMetric<T> {
BoundHistogramMetric<T>()
}

public func bind(labels: [String: String]) -> BoundHistogramMetric<T> {
BoundHistogramMetric<T>()
}
}
16 changes: 16 additions & 0 deletions Sources/OpenTelemetryApi/Metrics/Meter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ public protocol Meter {
/// - absolute: indicates if only positive values are expected.
/// - Returns:The measure instance.
func createDoubleMeasure(name: String, absolute: Bool) -> AnyMeasureMetric<Double>

/// Creates Int Histogram with given name and boundaries.
/// - Parameters:
/// - name: The name of the measure.
/// - explicitBoundaries: The boundary for sorting values into buckets
/// - absolute: indicates if only positive values are expected.
/// - Returns:The histogram instance.
func createIntHistogram(name: String, explicitBoundaries: Array<Int>?, absolute: Bool) -> AnyHistogramMetric<Int>

/// Creates Double Histogram with given name and boundaries.
/// - Parameters:
/// - name: The name of the measure.
/// - explicitBoundaries: The boundary for sorting values into buckets
/// - absolute: indicates if only positive values are expected.
/// - Returns:The histogram instance.
func createDoubleHistogram(name: String, explicitBoundaries: Array<Double>?, absolute: Bool) -> AnyHistogramMetric<Double>

/// Creates Int Observer with given name.
/// - Parameters:
Expand Down
8 changes: 8 additions & 0 deletions Sources/OpenTelemetryApi/Metrics/ProxyMeter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public struct ProxyMeter: Meter {
public func createDoubleMeasure(name: String, absolute: Bool) -> AnyMeasureMetric<Double> {
return realMeter?.createDoubleMeasure(name: name, absolute: absolute) ?? AnyMeasureMetric<Double>(NoopMeasureMetric<Double>())
}

public func createIntHistogram(name: String, explicitBoundaries: Array<Int>? = nil, absolute: Bool) -> AnyHistogramMetric<Int> {
return realMeter?.createIntHistogram(name: name, explicitBoundaries: explicitBoundaries, absolute: absolute) ?? AnyHistogramMetric<Int>(NoopHistogramMetric<Int>())
}

public func createDoubleHistogram(name: String, explicitBoundaries: Array<Double>?, absolute: Bool) -> AnyHistogramMetric<Double> {
return realMeter?.createDoubleHistogram(name: name, explicitBoundaries: explicitBoundaries, absolute: absolute) ?? AnyHistogramMetric<Double>(NoopHistogramMetric<Double>())
}

public func createIntObservableGauge(name: String, callback: @escaping (IntObserverMetric) -> Void) -> IntObserverMetric {
return realMeter?.createIntObservableGauge(name: name, callback: callback) ?? NoopIntObserverMetric()
Expand Down
104 changes: 104 additions & 0 deletions Sources/OpenTelemetrySdk/Metrics/Aggregators/HistogramAggregator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation

/// Aggregator which calculates histogram (bucket distribution, sum, count) from measures.
public class HistogramAggregator<T: SignedNumeric & Comparable>: Aggregator<T> {
fileprivate var histogram: Histogram<T>
fileprivate var pointCheck: Histogram<T>
fileprivate var boundaries: Array<T>

private let lock = Lock()
private let defaultBoundaries: Array<T> = [5, 10, 25, 50, 75, 100, 250, 500, 750, 1_000, 2_500, 5_000, 7_500,
10_000]

public init(explicitBoundaries: Array<T>? = nil) throws {
if let explicitBoundaries = explicitBoundaries, explicitBoundaries.count > 0 {
// we need to an ordered set to be able to correctly compute count for each
// boundary since we'll iterate on each in order.
self.boundaries = explicitBoundaries.sorted { $0 < $1 }
} else {
self.boundaries = defaultBoundaries
}

self.histogram = Histogram<T>(boundaries: self.boundaries)
self.pointCheck = Histogram<T>(boundaries: self.boundaries)
}

override public func update(value: T) {
lock.withLockVoid {
self.histogram.count += 1
self.histogram.sum += value

for i in 0..<self.boundaries.count {
if value < self.boundaries[i] {
self.histogram.buckets.counts[i] += 1
return
}
}
// value is above all observed boundaries
self.histogram.buckets.counts[self.boundaries.count] += 1
}
}

override public func checkpoint() {
lock.withLockVoid {
super.checkpoint()
pointCheck = histogram
histogram = Histogram<T>(boundaries: self.boundaries)
}
}

public override func toMetricData() -> MetricData {
return HistogramData<T>(startTimestamp: lastStart,
timestamp: lastEnd,
buckets: pointCheck.buckets,
count: pointCheck.count,
sum: pointCheck.sum)
}

public override func getAggregationType() -> AggregationType {
if T.self == Double.Type.self {
return .doubleHistogram
} else {
return .intHistogram
}
}
}

private struct Histogram<T> where T: SignedNumeric {
/*
* Buckets are implemented using two different arrays:
* - boundaries: contains every finite bucket boundary, which are inclusive lower bounds
* - counts: contains event counts for each bucket
*
* Note that we'll always have n+1 buckets, where n is the number of boundaries.
* This is because we need to count events that are below the lowest boundary.
*
* Example: if we measure the values: [5, 30, 5, 40, 5, 15, 15, 15, 25]
* with the boundaries [ 10, 20, 30 ], we will have the following state:
*
* buckets: {
* boundaries: [10, 20, 30],
* counts: [3, 3, 1, 2],
* }
*/
var buckets: (
boundaries: Array<T>,
counts: Array<Int>
)
var sum: T
var count: Int

init(boundaries: Array<T>) {
sum = 0
count = 0
buckets = (
boundaries: boundaries,
counts: Array(repeating: 0, count: boundaries.count + 1)
)
}
}
24 changes: 24 additions & 0 deletions Sources/OpenTelemetrySdk/Metrics/BoundHistogramMetricSdk.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation
import OpenTelemetryApi

internal class BoundHistogramMetricSdk<T: SignedNumeric & Comparable>: BoundHistogramMetricSdkBase<T> {
private var histogramAggregator: HistogramAggregator<T>

override init(explicitBoundaries: Array<T>? = nil) {
self.histogramAggregator = try! HistogramAggregator(explicitBoundaries: explicitBoundaries)
super.init(explicitBoundaries: explicitBoundaries)
}

override func record(value: T) {
histogramAggregator.update(value: value)
}

override func getAggregator() -> HistogramAggregator<T> {
return histogramAggregator
}
}
17 changes: 17 additions & 0 deletions Sources/OpenTelemetrySdk/Metrics/BoundHistogramMetricSdkBase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import Foundation
import OpenTelemetryApi

class BoundHistogramMetricSdkBase<T>: BoundHistogramMetric<T> {
override init(explicitBoundaries: Array<T>? = nil) {
super.init(explicitBoundaries: explicitBoundaries)
}

func getAggregator() -> Aggregator<T> {
fatalError()
}
}
2 changes: 2 additions & 0 deletions Sources/OpenTelemetrySdk/Metrics/Export/AggregationType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public enum AggregationType {
case intSum
case doubleSummary
case intSummary
case doubleHistogram
case intHistogram
}
12 changes: 12 additions & 0 deletions Sources/OpenTelemetrySdk/Metrics/Export/MetricData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ public struct SummaryData<T>: MetricData {
public var min: T
public var max: T
}

public struct HistogramData<T>: MetricData {
public var startTimestamp: Date
public var timestamp: Date
public var labels: [String: String] = [String: String]()
public var buckets: (
boundaries: Array<T>,
counts: Array<Int>
)
public var count: Int
public var sum: T
}
Loading

0 comments on commit eec1f8f

Please sign in to comment.