From 09e14b1639a518286b1f58f518154d6c5f2f5faf Mon Sep 17 00:00:00 2001 From: mamunto Date: Wed, 6 Nov 2024 11:06:53 -0500 Subject: [PATCH] added http metric compression --- .../StableOtlpHTTPExporterBase.swift | 25 +++- .../StableOtlpHTTPExporterBaseTests.swift | 130 ++++++++++++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPExporterBaseTests.swift diff --git a/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift b/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift index 2b7db4f9..dad17503 100644 --- a/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift +++ b/Sources/Exporters/OpenTelemetryProtocolHttp/StableOtlpHTTPExporterBase.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 // +import DataCompression import Foundation import OpenTelemetryProtocolExporterCommon import SwiftProtobuf @@ -44,14 +45,34 @@ public class StableOtlpHTTPExporterBase { } do { + let rawData = try body.serializedData() request.httpMethod = "POST" - request.httpBody = try body.serializedData() request.setValue(Headers.getUserAgentHeader(), forHTTPHeaderField: Constants.HTTP.userAgent) request.setValue("application/x-protobuf", forHTTPHeaderField: "Content-Type") + + var compressedData = rawData + switch config.compression { + case .gzip: + if let data = rawData.gzip() { + compressedData = data + request.setValue("gzip", forHTTPHeaderField: "Content-Encoding") + } + + case .deflate: + if let data = rawData.deflate() { + compressedData = data + request.setValue("deflate", forHTTPHeaderField: "Content-Encoding") + } + + case .none: + break + } + // Apply final data. Could be compressed or raw + // but it doesn't matter here + request.httpBody = compressedData } catch { print("Error serializing body: \(error)") } - return request } diff --git a/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPExporterBaseTests.swift b/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPExporterBaseTests.swift new file mode 100644 index 00000000..98ee71e1 --- /dev/null +++ b/Tests/ExportersTests/OpenTelemetryProtocol/StableOtlpHTTPExporterBaseTests.swift @@ -0,0 +1,130 @@ +// +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Logging +import NIO +import NIOHTTP1 +import NIOTestUtils +import OpenTelemetryApi +import OpenTelemetryProtocolExporterCommon +import DataCompression +@testable import OpenTelemetryProtocolExporterHttp +@testable import OpenTelemetrySdk +import XCTest + +class StableOtlpHTTPExporterBaseTests: XCTestCase { + + var exporter: StableOtlpHTTPExporterBase! + var spans: [SpanData] = [] + + override func setUp() { + super.setUp() + + spans = [] + let endpointName1 = "/api/foo" + String(Int.random(in: 1...100)) + let endpointName2 = "/api/bar" + String(Int.random(in: 100...500)) + spans.append(generateFakeSpan(endpointName: endpointName1)) + spans.append(generateFakeSpan(endpointName: endpointName2)) + } + + // Test for .gzip compression + func testCreateRequestWithGzipCompression() { + let config = OtlpConfiguration(compression: .gzip) + + exporter = StableOtlpHTTPExporterBase( + endpoint: URL(string: "http://example.com")!, + config: config + ) + + let body = Opentelemetry_Proto_Collector_Trace_V1_ExportTraceServiceRequest.with { + $0.resourceSpans = SpanAdapter.toProtoResourceSpans(spanDataList: spans) + } + + let request = exporter.createRequest(body: body, endpoint: URL(string: "http://example.com")!) + + /// gzip + let data = try! body.serializedData().gzip() + + // Verify Content-Encoding header is set to "gzip" + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Encoding"), "gzip") + XCTAssertNotNil(request.httpBody) + XCTAssertEqual(request.httpBody!.count, data!.count) + } + + // Test for .deflate compression + func testCreateRequestWithDeflateCompression() { + let config = OtlpConfiguration(compression: .deflate) + + exporter = StableOtlpHTTPExporterBase( + endpoint: URL(string: "http://example.com")!, + config: config + ) + + let body = Opentelemetry_Proto_Collector_Trace_V1_ExportTraceServiceRequest.with { + $0.resourceSpans = SpanAdapter.toProtoResourceSpans(spanDataList: spans) + } + + let request = exporter.createRequest(body: body, endpoint: URL(string: "http://example.com")!) + + /// deflate + let data = try! body.serializedData().deflate() + + // Verify Content-Encoding header is set to "deflate" + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Encoding"), "deflate") + XCTAssertNotNil(request.httpBody) + XCTAssertEqual(request.httpBody!.count, data!.count) + } + + // Test for .none compression (no compression) + func testCreateRequestWithNoCompression() { + let config = OtlpConfiguration(compression: .none) + + exporter = StableOtlpHTTPExporterBase( + endpoint: URL(string: "http://example.com")!, + config: config + ) + + let body = Opentelemetry_Proto_Collector_Trace_V1_ExportTraceServiceRequest.with { + $0.resourceSpans = SpanAdapter.toProtoResourceSpans(spanDataList: spans) + } + + let request = exporter.createRequest(body: body, endpoint: URL(string: "http://example.com")!) + + let data = try! body.serializedData() + + // Verify Content-Encoding header is set to "deflate" + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Encoding"), nil) + XCTAssertNotNil(request.httpBody) + XCTAssertEqual(request.httpBody!.count, data.count) + } + + private func generateFakeSpan(endpointName: String = "/api/endpoint") -> SpanData { + let duration = 0.9 + let start = Date() + let end = start.addingTimeInterval(duration) + let testattributes: [String: AttributeValue] = ["foo": AttributeValue("bar")!, "fizz": AttributeValue("buzz")!] + + var testData = SpanData(traceId: TraceId.random(), + spanId: SpanId.random(), + name: "GET " + endpointName, + kind: SpanKind.server, + startTime: start, + endTime: end, + totalAttributeCount: 2) + testData.settingAttributes(testattributes) + testData.settingTotalAttributeCount(2) + testData.settingHasEnded(true) + testData.settingTotalRecordedEvents(0) + testData.settingLinks([SpanData.Link]()) + testData.settingTotalRecordedLinks(0) + testData.settingStatus(.ok) + + return testData + } +}