|
| 1 | +// |
| 2 | +// Copyright The OpenTelemetry Authors |
| 3 | +// SPDX-License-Identifier: Apache-2.0 |
| 4 | +// |
| 5 | + |
| 6 | +import Foundation |
| 7 | +import Logging |
| 8 | +import GRPC |
| 9 | +import NIO |
| 10 | +import OpenTelemetryApi |
| 11 | +@testable import OpenTelemetryProtocolExporter |
| 12 | +@testable import OpenTelemetrySdk |
| 13 | +import XCTest |
| 14 | + |
| 15 | + |
| 16 | +class OtlpLogRecordExporterTests: XCTestCase { |
| 17 | + let traceIdBytes: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4] |
| 18 | + let spanIdBytes: [UInt8] = [0, 0, 0, 0, 4, 3, 2, 1] |
| 19 | + var traceId: TraceId! |
| 20 | + var spanId: SpanId! |
| 21 | + let tracestate = TraceState() |
| 22 | + var spanContext: SpanContext! |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | + var fakeCollector: FakeLogCollector! |
| 28 | + var server: EventLoopFuture<Server>! |
| 29 | + var channel: ClientConnection! |
| 30 | + |
| 31 | + let channelGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) |
| 32 | + let serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) |
| 33 | + |
| 34 | + func startServer() -> EventLoopFuture<Server> { |
| 35 | + // Start the server and print its address once it has started. |
| 36 | + let server = Server.insecure(group: serverGroup) |
| 37 | + .withServiceProviders([fakeCollector]) |
| 38 | + .bind(host: "localhost", port: 4317) |
| 39 | + |
| 40 | + server.map { |
| 41 | + $0.channel.localAddress |
| 42 | + }.whenSuccess { address in |
| 43 | + print("server started on port \(address!.port!)") |
| 44 | + } |
| 45 | + return server |
| 46 | + } |
| 47 | + |
| 48 | + func startChannel() -> ClientConnection { |
| 49 | + let channel = ClientConnection.insecure(group: channelGroup) |
| 50 | + .connect(host: "localhost", port: 4317) |
| 51 | + return channel |
| 52 | + } |
| 53 | + |
| 54 | + override func setUp() { |
| 55 | + fakeCollector = FakeLogCollector() |
| 56 | + server = startServer() |
| 57 | + channel = startChannel() |
| 58 | + traceId = TraceId(fromBytes: traceIdBytes) |
| 59 | + spanId = SpanId(fromBytes: spanIdBytes) |
| 60 | + spanContext = SpanContext.create(traceId: traceId, spanId: spanId, traceFlags: TraceFlags(), traceState: tracestate) |
| 61 | + |
| 62 | + } |
| 63 | + |
| 64 | + override func tearDown() { |
| 65 | + try! serverGroup.syncShutdownGracefully() |
| 66 | + try! channelGroup.syncShutdownGracefully() |
| 67 | + } |
| 68 | + |
| 69 | + func testExport() { |
| 70 | + let logRecord = ReadableLogRecord(resource: Resource(), |
| 71 | + instrumentationScopeInfo: InstrumentationScopeInfo(name: "scope"), |
| 72 | + timestamp: Date(), |
| 73 | + observedTimestamp: Date.distantPast, |
| 74 | + spanContext: spanContext, |
| 75 | + severity: .fatal, |
| 76 | + body: "Hello, world", |
| 77 | + attributes: ["event.name":AttributeValue.string("name"), "event.domain": AttributeValue.string("domain")]) |
| 78 | + |
| 79 | + let exporter = OtlpLogExporter(channel: channel) |
| 80 | + let result = exporter.export(logRecords: [logRecord]) |
| 81 | + XCTAssertEqual(result, ExportResult.success) |
| 82 | + XCTAssertEqual(fakeCollector.receivedLogs, LogRecordAdapter.toProtoResourceRecordLog(logRecordList: [logRecord])) |
| 83 | + exporter.shutdown() |
| 84 | + } |
| 85 | + |
| 86 | + func testImplicitGrpcLoggingConfig() throws { |
| 87 | + let exporter = OtlpLogExporter(channel: channel) |
| 88 | + guard let logger = exporter.callOptions?.logger else { |
| 89 | + throw "Missing logger" |
| 90 | + } |
| 91 | + XCTAssertEqual(logger.label, "io.grpc") |
| 92 | + } |
| 93 | + func testExplicitGrpcLoggingConfig() throws { |
| 94 | + let exporter = OtlpLogExporter(channel: channel, logger: Logger(label: "my.grpc.logger")) |
| 95 | + guard let logger = exporter.callOptions?.logger else { |
| 96 | + throw "Missing logger" |
| 97 | + } |
| 98 | + XCTAssertEqual(logger.label, "my.grpc.logger") |
| 99 | + } |
| 100 | + |
| 101 | + func testConfigHeadersIsNil_whenDefaultInitCalled() throws { |
| 102 | + let exporter = OtlpLogExporter(channel: channel) |
| 103 | + XCTAssertNil(exporter.config.headers) |
| 104 | + } |
| 105 | + |
| 106 | + func testConfigHeadersAreSet_whenInitCalledWithCustomConfig() throws { |
| 107 | + let config: OtlpConfiguration = OtlpConfiguration(timeout: TimeInterval(10), headers: [("FOO", "BAR")]) |
| 108 | + let exporter = OtlpLogExporter(channel: channel, config: config) |
| 109 | + XCTAssertNotNil(exporter.config.headers) |
| 110 | + XCTAssertEqual(exporter.config.headers?[0].0, "FOO") |
| 111 | + XCTAssertEqual(exporter.config.headers?[0].1, "BAR") |
| 112 | + XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO")) |
| 113 | + } |
| 114 | + |
| 115 | + func testConfigHeadersAreSet_whenInitCalledWithExplicitHeaders() throws { |
| 116 | + let exporter = OtlpLogExporter(channel: channel, envVarHeaders: [("FOO", "BAR")]) |
| 117 | + XCTAssertNil(exporter.config.headers) |
| 118 | + XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO")) |
| 119 | + } |
| 120 | + |
| 121 | + func testExportAfterShutdown() { |
| 122 | + let logRecord = ReadableLogRecord(resource: Resource(), |
| 123 | + instrumentationScopeInfo: InstrumentationScopeInfo(name: "scope"), |
| 124 | + timestamp: Date(), |
| 125 | + observedTimestamp: Date.distantPast, |
| 126 | + spanContext: spanContext, |
| 127 | + severity: .fatal, |
| 128 | + body: "Hello, world", |
| 129 | + attributes: ["event.name":AttributeValue.string("name"), "event.domain": AttributeValue.string("domain")]) |
| 130 | + let exporter = OtlpLogExporter(channel: channel) |
| 131 | + exporter.shutdown() |
| 132 | + let result = exporter.export(logRecords: [logRecord]) |
| 133 | + XCTAssertEqual(result, ExportResult.failure) |
| 134 | + } |
| 135 | + |
| 136 | + func testExportCancelled() { |
| 137 | + fakeCollector.returnedStatus = GRPCStatus(code: .cancelled, message: nil) |
| 138 | + let exporter = OtlpLogExporter(channel: channel) |
| 139 | + let logRecord = ReadableLogRecord(resource: Resource(), |
| 140 | + instrumentationScopeInfo: InstrumentationScopeInfo(name: "scope"), |
| 141 | + timestamp: Date(), |
| 142 | + observedTimestamp: Date.distantPast, |
| 143 | + spanContext: spanContext, |
| 144 | + severity: .fatal, |
| 145 | + body: "Hello, world", |
| 146 | + attributes: ["event.name":AttributeValue.string("name"), |
| 147 | + "event.domain": AttributeValue.string("domain")]) |
| 148 | + let result = exporter.export(logRecords: [logRecord]) |
| 149 | + XCTAssertEqual(result, ExportResult.failure) |
| 150 | + exporter.shutdown() |
| 151 | + } |
| 152 | + |
| 153 | +} |
| 154 | + |
| 155 | + |
| 156 | + |
| 157 | +class FakeLogCollector: Opentelemetry_Proto_Collector_Logs_V1_LogsServiceProvider { |
| 158 | + var interceptors: OpenTelemetryProtocolExporter.Opentelemetry_Proto_Collector_Logs_V1_LogsServiceServerInterceptorFactoryProtocol? |
| 159 | + |
| 160 | + func export(request: OpenTelemetryProtocolExporter.Opentelemetry_Proto_Collector_Logs_V1_ExportLogsServiceRequest, context: GRPC.StatusOnlyCallContext) -> NIOCore.EventLoopFuture<OpenTelemetryProtocolExporter.Opentelemetry_Proto_Collector_Logs_V1_ExportLogsServiceResponse> { |
| 161 | + receivedLogs.append(contentsOf: request.resourceLogs) |
| 162 | + if returnedStatus != GRPCStatus.ok { |
| 163 | + return context.eventLoop.makeFailedFuture(returnedStatus) |
| 164 | + } |
| 165 | + return context.eventLoop.makeSucceededFuture(Opentelemetry_Proto_Collector_Logs_V1_ExportLogsServiceResponse()) |
| 166 | + } |
| 167 | + |
| 168 | + var receivedLogs = [Opentelemetry_Proto_Logs_V1_ResourceLogs]() |
| 169 | + var returnedStatus = GRPCStatus.ok |
| 170 | + |
| 171 | +} |
0 commit comments