diff --git a/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift b/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift index dfce8cf2..1fb75de2 100644 --- a/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift +++ b/Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift @@ -103,8 +103,9 @@ public enum MetricsAdapter { var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint() injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData) protoDataPoint.value = .asInt(Int64(gaugeData.value)) - protoMetric.sum.aggregationTemporality = .cumulative + protoMetric.sum.aggregationTemporality = stableMetric.data.aggregationTemporality.convertToProtoEnum() protoMetric.sum.dataPoints.append(protoDataPoint) + protoMetric.sum.isMonotonic = stableMetric.isMonotonic case .DoubleGauge: guard let gaugeData = $0 as? DoublePointData else { break @@ -120,8 +121,9 @@ public enum MetricsAdapter { var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint() injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData) protoDataPoint.value = .asDouble(gaugeData.value) - protoMetric.sum.aggregationTemporality = .cumulative + protoMetric.sum.aggregationTemporality = stableMetric.data.aggregationTemporality.convertToProtoEnum() protoMetric.sum.dataPoints.append(protoDataPoint) + protoMetric.sum.isMonotonic = stableMetric.isMonotonic case .Summary: guard let summaryData = $0 as? SummaryPointData else { break @@ -147,7 +149,7 @@ public enum MetricsAdapter { protoDataPoint.count = UInt64(histogramData.count) protoDataPoint.explicitBounds = histogramData.boundaries.map { Double($0) } protoDataPoint.bucketCounts = histogramData.counts.map { UInt64($0) } - protoMetric.histogram.aggregationTemporality = .cumulative + protoMetric.histogram.aggregationTemporality = stableMetric.data.aggregationTemporality.convertToProtoEnum() protoMetric.histogram.dataPoints.append(protoDataPoint) case .ExponentialHistogram: // TODO: implement @@ -394,3 +396,14 @@ public enum MetricsAdapter { return protoMetric } } + +extension AggregationTemporality { + func convertToProtoEnum() -> Opentelemetry_Proto_Metrics_V1_AggregationTemporality { + switch self { + case .cumulative: + return .cumulative + case .delta: + return .delta + } + } +} diff --git a/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift b/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift new file mode 100644 index 00000000..4e01f44b --- /dev/null +++ b/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift @@ -0,0 +1,49 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import SwiftProtobuf +import OpenTelemetryProtocolExporterCommon + +public class StableOtlpHTTPExporterBase { + let endpoint: URL + let httpClient: HTTPClient + let envVarHeaders: [(String, String)]? + let config: OtlpConfiguration + + // MARK: - Init + + public init(endpoint: URL, config: OtlpConfiguration = OtlpConfiguration(), useSession: URLSession? = nil, envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes) { + self.envVarHeaders = envVarHeaders + self.endpoint = endpoint + self.config = config + if let providedSession = useSession { + self.httpClient = HTTPClient(session: providedSession) + } else { + self.httpClient = HTTPClient() + } + } + + public func createRequest(body: Message, endpoint: URL) -> URLRequest { + var request = URLRequest(url: endpoint) + + for header in config.headers ?? [] { + request.addValue(header.1, forHTTPHeaderField: header.0) + } + + do { + request.httpMethod = "POST" + request.httpBody = try body.serializedData() + request.setValue(Headers.getUserAgentHeader(), forHTTPHeaderField: Constants.HTTP.userAgent) + request.setValue("application/x-protobuf", forHTTPHeaderField: "Content-Type") + } catch { + print("Error serializing body: \(error)") + } + + return request + } + + public func shutdown(explicitTimeout: TimeInterval? = nil) { } +} diff --git a/Sources/Exporters/OpenTelemetryProtocolHttp/metric/StableOtlpHTTPMetricExporter.swift b/Sources/Exporters/OpenTelemetryProtocolHttp/metric/StableOtlpHTTPMetricExporter.swift new file mode 100644 index 00000000..f83ed5cc --- /dev/null +++ b/Sources/Exporters/OpenTelemetryProtocolHttp/metric/StableOtlpHTTPMetricExporter.swift @@ -0,0 +1,95 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import OpenTelemetrySdk +import OpenTelemetryProtocolExporterCommon + +public func defaultStableOtlpHTTPMetricsEndpoint() -> URL { + URL(string: "http://localhost:4318/v1/metrics")! +} + +public class StableOtlpHTTPMetricExporter: StableOtlpHTTPExporterBase, StableMetricExporter { + var aggregationTemporalitySelector: AggregationTemporalitySelector + var defaultAggregationSelector: DefaultAggregationSelector + + var pendingMetrics: [StableMetricData] = [] + + // MARK: - Init + + public init(endpoint: URL, config: OtlpConfiguration = OtlpConfiguration(), aggregationTemporalitySelector: AggregationTemporalitySelector = AggregationTemporality.alwaysCumulative(), defaultAggregationSelector: DefaultAggregationSelector = AggregationSelector.instance, useSession: URLSession? = nil, envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes) { + + self.aggregationTemporalitySelector = aggregationTemporalitySelector + self.defaultAggregationSelector = defaultAggregationSelector + + super.init(endpoint: endpoint, config: config, useSession: useSession, envVarHeaders: envVarHeaders) + } + + + // MARK: - StableMetricsExporter + + public func export(metrics : [StableMetricData]) -> ExportResult { + pendingMetrics.append(contentsOf: metrics) + let sendingMetrics = pendingMetrics + pendingMetrics = [] + let body = Opentelemetry_Proto_Collector_Metrics_V1_ExportMetricsServiceRequest.with { + $0.resourceMetrics = MetricsAdapter.toProtoResourceMetrics(stableMetricData: sendingMetrics) + } + + let request = createRequest(body: body, endpoint: endpoint) + httpClient.send(request: request) { [weak self] result in + switch result { + case .success(_): + break + case .failure(let error): + self?.pendingMetrics.append(contentsOf: sendingMetrics) + print(error) + } + } + + return .success + } + + public func flush() -> ExportResult { + var exporterResult: ExportResult = .success + + if !pendingMetrics.isEmpty { + let body = Opentelemetry_Proto_Collector_Metrics_V1_ExportMetricsServiceRequest.with { + $0.resourceMetrics = MetricsAdapter.toProtoResourceMetrics(stableMetricData: pendingMetrics) + } + let semaphore = DispatchSemaphore(value: 0) + let request = createRequest(body: body, endpoint: endpoint) + httpClient.send(request: request) { result in + switch result { + case .success(_): + break + case .failure(let error): + print(error) + exporterResult = .failure + } + semaphore.signal() + } + semaphore.wait() + } + + return exporterResult + } + + public func shutdown() -> ExportResult { + return .success + } + + // MARK: - AggregationTemporalitySelectorProtocol + + public func getAggregationTemporality(for instrument: OpenTelemetrySdk.InstrumentType) -> OpenTelemetrySdk.AggregationTemporality { + return aggregationTemporalitySelector.getAggregationTemporality(for: instrument) + } + + // MARK: - DefaultAggregationSelector + + public func getDefaultAggregation(for instrument: OpenTelemetrySdk.InstrumentType) -> OpenTelemetrySdk.Aggregation { + return defaultAggregationSelector.getDefaultAggregation(for: instrument) + } +} diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AggregationTemporality.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AggregationTemporality.swift index 0851fd91..60735502 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AggregationTemporality.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/AggregationTemporality.swift @@ -14,7 +14,7 @@ public class AggregationTemporalitySelector : AggregationTemporalitySelectorProt return aggregationTemporalitySelector(instrument) } - init(aggregationTemporalitySelector: @escaping (InstrumentType) -> AggregationTemporality) { + public init(aggregationTemporalitySelector: @escaping (InstrumentType) -> AggregationTemporality) { self.aggregationTemporalitySelector = aggregationTemporalitySelector } @@ -31,6 +31,12 @@ public enum AggregationTemporality { } } + + public static func alwaysDelta() -> AggregationTemporalitySelector { + return AggregationTemporalitySelector() { (type) in + .delta + } + } public static func deltaPreferred() -> AggregationTemporalitySelector { return AggregationTemporalitySelector() { type in diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleSumAggregator.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleSumAggregator.swift index 148db85e..57fac44c 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleSumAggregator.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/DoubleSumAggregator.swift @@ -22,7 +22,7 @@ public class DoubleSumAggregator: SumAggregator, StableAggregator { } public func toMetricData(resource: Resource, scope: InstrumentationScopeInfo, descriptor: MetricDescriptor, points: [PointData], temporality: AggregationTemporality) -> StableMetricData { - StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: scope, name: descriptor.instrument.name, description: descriptor.instrument.description, unit: descriptor.instrument.unit, data: StableSumData(aggregationTemporality: temporality, points: points as! [DoublePointData])) + StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: scope, name: descriptor.instrument.name, description: descriptor.instrument.description, unit: descriptor.instrument.unit, isMonotonic: self.isMonotonic, data: StableSumData(aggregationTemporality: temporality, points: points as! [DoublePointData])) } init(instrumentDescriptor: InstrumentDescriptor, reservoirSupplier: @escaping () -> ExemplarReservoir) { diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/LongSumAggregator.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/LongSumAggregator.swift index af33607c..b32ae926 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/LongSumAggregator.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Aggregation/LongSumAggregator.swift @@ -27,7 +27,7 @@ public class LongSumAggregator: SumAggregator, StableAggregator { } public func toMetricData(resource: Resource, scope: InstrumentationScopeInfo, descriptor: MetricDescriptor, points: [PointData], temporality: AggregationTemporality) -> StableMetricData { - StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: scope, name: descriptor.instrument.name, description: descriptor.instrument.description, unit: descriptor.instrument.unit, data: StableSumData(aggregationTemporality: temporality, points: points as! [LongPointData])) + StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: scope, name: descriptor.instrument.name, description: descriptor.instrument.description, unit: descriptor.instrument.unit, isMonotonic: self.isMonotonic, data: StableSumData(aggregationTemporality: temporality, points: points as! [LongPointData])) } private class Handle: AggregatorHandle { diff --git a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/StableMetricData.swift b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/StableMetricData.swift index cd205df7..0b9458d1 100644 --- a/Sources/OpenTelemetrySdk/Metrics/Stable/Data/StableMetricData.swift +++ b/Sources/OpenTelemetrySdk/Metrics/Stable/Data/StableMetricData.swift @@ -23,29 +23,33 @@ public struct StableMetricData: Equatable { public private(set) var description: String public private(set) var unit: String public private(set) var type: MetricDataType + public private(set) var isMonotonic: Bool public private(set) var data: Data - public static let empty = StableMetricData(resource: Resource.empty, instrumentationScopeInfo: InstrumentationScopeInfo(), name: "", description: "", unit: "", type: .Summary, data: StableMetricData.Data(points: [PointData]())) + public static let empty = StableMetricData(resource: Resource.empty, instrumentationScopeInfo: InstrumentationScopeInfo(), name: "", description: "", unit: "", type: .Summary, isMonotonic: false, data: StableMetricData.Data(aggregationTemporality: .cumulative, points: [PointData]())) public class Data: Equatable { public private(set) var points: [PointData] + public private(set) var aggregationTemporality: AggregationTemporality - internal init(points: [PointData]) { + internal init(aggregationTemporality: AggregationTemporality, points: [PointData]) { + self.aggregationTemporality = aggregationTemporality self.points = points } public static func == (lhs: StableMetricData.Data, rhs: StableMetricData.Data) -> Bool { - return lhs.points == rhs.points + return lhs.points == rhs.points && lhs.aggregationTemporality == rhs.aggregationTemporality } } - internal init(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, type: MetricDataType, data: StableMetricData.Data) { + internal init(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, type: MetricDataType, isMonotonic: Bool, data: StableMetricData.Data) { self.resource = resource self.instrumentationScopeInfo = instrumentationScopeInfo self.name = name self.description = description self.unit = unit self.type = type + self.isMonotonic = isMonotonic self.data = data } @@ -56,33 +60,34 @@ public struct StableMetricData: Equatable { lhs.description == rhs.description && lhs.unit == rhs.unit && lhs.type == rhs.type && - lhs.data.points == rhs.data.points + lhs.isMonotonic == rhs.isMonotonic && + lhs.data == rhs.data } } extension StableMetricData { static func createExponentialHistogram(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableExponentialHistogramData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .ExponentialHistogram, data: data) + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .ExponentialHistogram, isMonotonic: false, data: data) } static func createDoubleGauge(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableGaugeData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .DoubleGauge, data: data) + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .DoubleGauge, isMonotonic: false, data: data) } static func createLongGauge(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableGaugeData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .LongGauge, data: data) + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .LongGauge, isMonotonic: false, data: data) } - static func createDoubleSum(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableSumData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .DoubleSum, data: data) + static func createDoubleSum(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, isMonotonic: Bool, data: StableSumData) -> StableMetricData { + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .DoubleSum, isMonotonic: isMonotonic, data: data) } - static func createLongSum(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableSumData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .LongSum, data: data) + static func createLongSum(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, isMonotonic: Bool, data: StableSumData) -> StableMetricData { + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .LongSum, isMonotonic: isMonotonic, data: data) } static func createHistogram(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, name: String, description: String, unit: String, data: StableHistogramData) -> StableMetricData { - StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .Histogram, data: data) + StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, type: .Histogram, isMonotonic: false, data: data) } func isEmpty() -> Bool { @@ -99,41 +104,31 @@ extension StableMetricData { } public class StableHistogramData: StableMetricData.Data { - public private(set) var aggregationTemporality: AggregationTemporality init(aggregationTemporality: AggregationTemporality, points: [HistogramPointData]) { - self.aggregationTemporality = aggregationTemporality - super.init(points: points) + super.init(aggregationTemporality: aggregationTemporality, points: points) } } public class StableExponentialHistogramData: StableMetricData.Data { - public private(set) var aggregationTemporality: AggregationTemporality - init(aggregationTemporality: AggregationTemporality, points: [PointData]) { - self.aggregationTemporality = aggregationTemporality - super.init(points: points) + override init(aggregationTemporality: AggregationTemporality, points: [PointData]) { + super.init(aggregationTemporality: aggregationTemporality, points: points) } } public class StableGaugeData: StableMetricData.Data { - public private(set) var aggregationTemporality: AggregationTemporality - init(aggregationTemporality: AggregationTemporality, points: [PointData]) { - self.aggregationTemporality = aggregationTemporality - super.init(points: points) + override init(aggregationTemporality: AggregationTemporality, points: [PointData]) { + super.init(aggregationTemporality: aggregationTemporality, points: points) } } public class StableSumData: StableMetricData.Data { - public private(set) var aggregationTemporality: AggregationTemporality - init(aggregationTemporality: AggregationTemporality, points: [PointData]) { - self.aggregationTemporality = aggregationTemporality - super.init(points: points) + override init(aggregationTemporality: AggregationTemporality, points: [PointData]) { + super.init(aggregationTemporality: aggregationTemporality, points: points) } } public class StableSummaryData: StableMetricData.Data { - public private(set) var aggregationTemporality: AggregationTemporality - init(aggregationTemporality: AggregationTemporality, points: [PointData]) { - self.aggregationTemporality = aggregationTemporality - super.init(points: points) + override init(aggregationTemporality: AggregationTemporality, points: [PointData]) { + super.init(aggregationTemporality: aggregationTemporality, points: points) } } diff --git a/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift b/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift index 861d5959..3e3dd5b9 100644 --- a/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift +++ b/Tests/ExportersTests/OpenTelemetryProtocol/MetricsAdapterTest.swift @@ -35,7 +35,7 @@ final class MetricsAdapterTest: XCTestCase { let pointValue = Int.random(in: 1...999) let point:PointData = LongPointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: pointValue) let sumData = StableSumData(aggregationTemporality: .cumulative, points: [point]) - let metricData = StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, data: sumData) + let metricData = StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, isMonotonic: true, data: sumData) let result = MetricsAdapter.toProtoMetric(stableMetric: metricData) guard let value = result?.sum.dataPoints as? [Opentelemetry_Proto_Metrics_V1_NumberDataPoint] else { @@ -46,6 +46,7 @@ final class MetricsAdapterTest: XCTestCase { } XCTAssertEqual(value.first?.asInt, Int64(pointValue)) + XCTAssertEqual(result?.sum.isMonotonic, true) } func testToProtoResourceMetricsWithDoubleGuage() throws { @@ -69,7 +70,7 @@ final class MetricsAdapterTest: XCTestCase { let pointValue: Double = Double.random(in: 1...999) let point:PointData = DoublePointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: pointValue) let sumData = StableSumData(aggregationTemporality: .cumulative, points: [point]) - let metricData = StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, data: sumData) + let metricData = StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: name, description: description, unit: unit, isMonotonic: false, data: sumData) let result = MetricsAdapter.toProtoMetric(stableMetric: metricData) guard let value = result?.sum.dataPoints as? [Opentelemetry_Proto_Metrics_V1_NumberDataPoint] else { @@ -80,6 +81,7 @@ final class MetricsAdapterTest: XCTestCase { } XCTAssertEqual(value.first?.asDouble, pointValue) + XCTAssertEqual(result?.sum.isMonotonic, false) } func testToProtoResourceMetricsWithHistogram() throws { diff --git a/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPMetricsExporterTest.swift b/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPMetricsExporterTest.swift new file mode 100644 index 00000000..9566a89c --- /dev/null +++ b/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPMetricsExporterTest.swift @@ -0,0 +1,191 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Logging +import NIO +import NIOHTTP1 +import NIOTestUtils +import OpenTelemetryApi +import OpenTelemetryProtocolExporterCommon +@testable import OpenTelemetryProtocolExporterHttp +@testable import OpenTelemetrySdk +import XCTest + +class StableOtlpHttpMetricsExporterTest: XCTestCase { + var testServer: NIOHTTP1TestServer! + var group: MultiThreadedEventLoopGroup! + + override func setUp() { + group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + testServer = NIOHTTP1TestServer(group: group) + } + + override func tearDown() { + XCTAssertNoThrow(try testServer.stop()) + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // The shutdown() function is a no-op, This test is just here to make codecov happy + func testShutdown() { + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint) + XCTAssertEqual(exporter.shutdown(), .success) + } + + func testExportHeader() { + let metric = generateSumStableMetricData() + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint, config: OtlpConfiguration(headers: [("headerName", "headerValue")])) + let result = exporter.export(metrics: [metric]) + XCTAssertEqual(result, ExportResult.success) + + XCTAssertNoThrow(try testServer.receiveHeadAndVerify { head in + XCTAssertTrue(head.headers.contains(name: "headerName")) + XCTAssertEqual("headerValue", head.headers.first(name: "headerName")) + }) + + XCTAssertNotNil(try testServer.receiveBodyAndVerify()) + XCTAssertNoThrow(try testServer.receiveEnd()) + } + + func testExport() { + let words = ["foo", "bar", "fizz", "buzz"] + var metrics: [StableMetricData] = [] + var metricDescriptions: [String] = [] + for word in words { + let metricDescription = word + String(Int.random(in: 1...100)) + metricDescriptions.append(metricDescription) + metrics.append(generateSumStableMetricData(description: metricDescription)) + } + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint) + let result = exporter.export(metrics: metrics) + XCTAssertEqual(result, ExportResult.success) + + XCTAssertNoThrow(try testServer.receiveHeadAndVerify { head in + let otelVersion = Headers.getUserAgentHeader() + XCTAssertTrue(head.headers.contains(name: Constants.HTTP.userAgent)) + XCTAssertEqual(otelVersion, head.headers.first(name: Constants.HTTP.userAgent)) + }) + + XCTAssertNoThrow(try testServer.receiveBodyAndVerify { body in + var contentsBuffer = ByteBuffer(buffer: body) + let contents = contentsBuffer.readString(length: contentsBuffer.readableBytes)! + for metricDescription in metricDescriptions { + XCTAssertTrue(contents.contains(metricDescription)) + } + }) + + XCTAssertNoThrow(try testServer.receiveEnd()) + } + + func testGaugeExport() { + let words = ["foo", "bar", "fizz", "buzz"] + var metrics: [StableMetricData] = [] + var metricDescriptions: [String] = [] + for word in words { + let metricDescription = word + String(Int.random(in: 1...100)) + metricDescriptions.append(metricDescription) + metrics.append(generateGaugeStableMetricData(description: metricDescription)) + } + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint) + let result = exporter.export(metrics: metrics) + XCTAssertEqual(result, ExportResult.success) + + XCTAssertNoThrow(try testServer.receiveHeadAndVerify { head in + let otelVersion = Headers.getUserAgentHeader() + XCTAssertTrue(head.headers.contains(name: Constants.HTTP.userAgent)) + XCTAssertEqual(otelVersion, head.headers.first(name: Constants.HTTP.userAgent)) + }) + + XCTAssertNoThrow(try testServer.receiveBodyAndVerify { body in + var contentsBuffer = ByteBuffer(buffer: body) + let contents = contentsBuffer.readString(length: contentsBuffer.readableBytes)! + for metricDescription in metricDescriptions { + XCTAssertTrue(contents.contains(metricDescription)) + } + }) + + XCTAssertNoThrow(try testServer.receiveEnd()) + } + + func testFlushWithoutPendingMetrics() { + let metric = generateSumStableMetricData() + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint, config: OtlpConfiguration(headers: [("headerName", "headerValue")])) + XCTAssertEqual(exporter.flush(), .success) + } + + func testCustomAggregationTemporalitySelector() { + let aggregationTemporalitySelector = AggregationTemporalitySelector() { (type) in + switch type { + case .counter: + return .cumulative + case .histogram: + return .delta + case .observableCounter: + return .delta + case .observableGauge: + return .delta + case .observableUpDownCounter: + return .cumulative + case .upDownCounter: + return .delta + } + } + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint, aggregationTemporalitySelector: aggregationTemporalitySelector) + XCTAssertTrue(exporter.getAggregationTemporality(for: .counter) == .cumulative) + XCTAssertTrue(exporter.getAggregationTemporality(for: .histogram) == .delta) + XCTAssertTrue(exporter.getAggregationTemporality(for: .observableCounter) == .delta) + XCTAssertTrue(exporter.getAggregationTemporality(for: .observableGauge) == .delta) + XCTAssertTrue(exporter.getAggregationTemporality(for: .observableUpDownCounter) == .cumulative) + XCTAssertTrue(exporter.getAggregationTemporality(for: .upDownCounter) == .delta) + } + + func testCustomAggregation() { + let aggregationSelector = CustomDefaultAggregationSelector() + + let endpoint = URL(string: "http://localhost:\(testServer.serverPort)")! + let exporter = StableOtlpHTTPMetricExporter(endpoint: endpoint, defaultAggregationSelector: aggregationSelector) + XCTAssertTrue(exporter.getDefaultAggregation(for: .counter) is SumAggregation) + XCTAssertTrue(exporter.getDefaultAggregation(for: .histogram) is SumAggregation) + XCTAssertTrue(exporter.getDefaultAggregation(for: .observableCounter) is DropAggregation) + XCTAssertTrue(exporter.getDefaultAggregation(for: .upDownCounter) is DropAggregation) + } + + + func generateSumStableMetricData(description: String = "description") -> StableMetricData { + let scope = InstrumentationScopeInfo(name: "lib", version: "semver:0.0.0") + let sumPointData = DoublePointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: 1) + let metric = StableMetricData(resource: Resource(), instrumentationScopeInfo: scope, name: "metric", description: description, unit: "", type: .DoubleSum, isMonotonic: true, data: StableMetricData.Data(aggregationTemporality: .cumulative, points: [sumPointData])) + return metric + } + + func generateGaugeStableMetricData(description: String = "description") -> StableMetricData { + let scope = InstrumentationScopeInfo(name: "lib", version: "semver:0.0.0") + let sumPointData = DoublePointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: 100) + let metric = StableMetricData(resource: Resource(), instrumentationScopeInfo: scope, name: "MyGauge", description: description, unit: "", type: .LongGauge, isMonotonic: true, data: StableMetricData.Data(aggregationTemporality: .cumulative, points: [sumPointData])) + return metric + } +} + +public class CustomDefaultAggregationSelector: DefaultAggregationSelector { + public func getDefaultAggregation(for instrument: OpenTelemetrySdk.InstrumentType) -> OpenTelemetrySdk.Aggregation { + switch instrument { + case .counter, .histogram: + return SumAggregation() + default: + return DropAggregation() + } + } +} diff --git a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift index 9166280e..23a3f4d5 100644 --- a/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift +++ b/Tests/OpenTelemetrySdkTests/Metrics/StableMetrics/Data/StableMetricDataTests.swift @@ -17,17 +17,19 @@ class StableMetricDataTests: XCTestCase { func testStableMetricDataCreation() { let type = MetricDataType.Summary - let data = StableMetricData.Data(points: emptyPointData) - - let metricData = StableMetricData(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, type: type, data: data) + 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) + XCTAssertEqual(metricData.data.aggregationTemporality, .delta) + XCTAssertEqual(metricData.isMonotonic, false) } func testEmptyStableMetricData() { - XCTAssertEqual(StableMetricData.empty, StableMetricData(resource: Resource.empty, instrumentationScopeInfo: InstrumentationScopeInfo(), name: "", description: "", unit: "", type: .Summary, data: StableMetricData.Data(points: [PointData]()))) + XCTAssertEqual(StableMetricData.empty, StableMetricData(resource: Resource.empty, instrumentationScopeInfo: InstrumentationScopeInfo(), name: "", description: "", unit: "", type: .Summary, isMonotonic: false, data: StableMetricData.Data(aggregationTemporality: .cumulative, points: [PointData]()))) } func testCreateExponentialHistogram() { @@ -39,6 +41,8 @@ class StableMetricDataTests: XCTestCase { assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data, histogramData) + XCTAssertEqual(metricData.data.aggregationTemporality, .delta) + XCTAssertEqual(metricData.isMonotonic, false) } func testCreateHistogram() { @@ -60,6 +64,8 @@ class StableMetricDataTests: XCTestCase { assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data, histogramData) + XCTAssertEqual(metricData.data.aggregationTemporality, .cumulative) + XCTAssertEqual(metricData.isMonotonic, false) XCTAssertFalse(metricData.isEmpty()) @@ -79,6 +85,8 @@ class StableMetricDataTests: XCTestCase { assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data.points.first, point) + XCTAssertEqual(metricData.data.aggregationTemporality, .cumulative) + XCTAssertEqual(metricData.isMonotonic, false) } func testCreateDoubleSum() { @@ -87,11 +95,13 @@ class StableMetricDataTests: XCTestCase { let point:PointData = DoublePointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: d) let sumData = StableSumData(aggregationTemporality: .cumulative, points: [point]) - let metricData = StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, data: sumData) + let metricData = StableMetricData.createDoubleSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, isMonotonic: true, data: sumData) assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data.points.first, point) + XCTAssertEqual(metricData.data.aggregationTemporality, .cumulative) + XCTAssertEqual(metricData.isMonotonic, true) } func testCreateLongGuage() { @@ -104,6 +114,8 @@ class StableMetricDataTests: XCTestCase { assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data.points.first, point) + XCTAssertEqual(metricData.data.aggregationTemporality, .cumulative) + XCTAssertEqual(metricData.isMonotonic, false) } func testCreateLongSum() { @@ -111,11 +123,13 @@ class StableMetricDataTests: XCTestCase { let point:PointData = LongPointData(startEpochNanos: 0, endEpochNanos: 1, attributes: [:], exemplars: [], value: 55) let sumData = StableSumData(aggregationTemporality: .cumulative, points: [point]) - let metricData = StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, data: sumData) + let metricData = StableMetricData.createLongSum(resource: resource, instrumentationScopeInfo: instrumentationScopeInfo, name: metricName, description: metricDescription, unit: unit, isMonotonic: true, data: sumData) assertCommon(metricData) XCTAssertEqual(metricData.type, type) XCTAssertEqual(metricData.data.points.first, point) + XCTAssertEqual(metricData.data.aggregationTemporality, .cumulative) + XCTAssertEqual(metricData.isMonotonic, true) }