Skip to content

Commit 0e027f5

Browse files
author
Ignacio Bonafonte
authored
Merge pull request #309 from BlueOwlOpenSource/enable-logging-grpc-calloptions
Adds logger to OtlpTraceExporter init so GRPC calls can be logged. The CallOptions created in the constructor for OtlpTraceExporter includes an optional logger parameter. The change in this PR makes it possible for the caller to provide a logger that will be used when GRPC calls are made.
2 parents 9829ff1 + 77c3c1c commit 0e027f5

File tree

7 files changed

+182
-14
lines changed

7 files changed

+182
-14
lines changed

.codecov.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ignore:
2+
- "Tests"
3+

Sources/Exporters/OpenTelemetryProtocol/common/EnvVarHeaders.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,37 @@ import OpenTelemetryApi
88

99
/// Provides a framework for detection of resource information from the environment variable
1010
public struct EnvVarHeaders {
11-
private static let otelAttributesEnv = "OTEL_EXPORTER_OTLP_HEADERS"
1211
private static let labelListSplitter = Character(",")
1312
private static let labelKeyValueSplitter = Character("=")
1413

1514
/// This resource information is loaded from the
1615
/// environment variable.
17-
public static let attributes : [(String,String)]? = parseAttributes(rawEnvAttributes: ProcessInfo.processInfo.environment[otelAttributesEnv])
16+
public static let attributes : [(String,String)]? = EnvVarHeaders.attributes()
17+
18+
public static func attributes(for rawEnvAttributes: String? = ProcessInfo.processInfo.environment["OTEL_EXPORTER_OTLP_HEADERS"]) -> [(String,String)]? {
19+
parseAttributes(rawEnvAttributes: rawEnvAttributes)
20+
}
1821

1922
private init() {}
2023

24+
private static func isKey(token: String) -> Bool {
25+
let alpha = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
26+
let digit = CharacterSet(charactersIn: "0123456789")
27+
let special = CharacterSet(charactersIn: "!#$%&'*+-.^_`|~")
28+
let tchar = special.union(alpha).union(digit)
29+
return tchar.isSuperset(of: CharacterSet(charactersIn: token))
30+
}
31+
32+
private static func isValue(baggage: String) -> Bool {
33+
let asciiSet = CharacterSet(charactersIn: UnicodeScalar(0) ..< UnicodeScalar(0x80))
34+
let special = CharacterSet(charactersIn: "^\"|\"$")
35+
let baggageOctet = asciiSet.subtracting(.controlCharacters).subtracting(.whitespaces).union(special)
36+
return baggageOctet.isSuperset(of: CharacterSet(charactersIn: baggage))
37+
}
38+
2139
/// Creates a label map from the environment variable string.
2240
/// - Parameter rawEnvLabels: the comma-separated list of labels
41+
/// NOTE: Parsing does not fully match W3C Correlation-Context
2342
private static func parseAttributes(rawEnvAttributes: String?) -> [(String, String)]? {
2443
guard let rawEnvLabels = rawEnvAttributes else { return nil }
2544

@@ -30,10 +49,17 @@ public struct EnvVarHeaders {
3049
if split.count != 2 {
3150
return
3251
}
52+
3353
let key = split[0].trimmingCharacters(in: .whitespaces)
34-
let value = split[1].trimmingCharacters(in: CharacterSet(charactersIn: "^\"|\"$"))
54+
guard isKey(token: key) else { return }
55+
56+
let value = split[1].trimmingCharacters(in: .whitespaces)
57+
guard isValue(baggage: value) else { return }
58+
3559
labels.append((key,value))
3660
}
37-
return labels
61+
return labels.count > 0 ? labels : nil
3862
}
3963
}
64+
65+

Sources/Exporters/OpenTelemetryProtocol/metric/OtlpMetricExporter.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import Foundation
7+
import Logging
78
import GRPC
89
import NIO
910
import NIOHPACK
@@ -16,17 +17,19 @@ public class OtlpMetricExporter: MetricExporter {
1617
let config : OtlpConfiguration
1718
var callOptions : CallOptions? = nil
1819

20+
1921

20-
21-
public init(channel: GRPCChannel, config: OtlpConfiguration = OtlpConfiguration()) {
22+
public init(channel: GRPCChannel, config: OtlpConfiguration = OtlpConfiguration(), logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }), envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes) {
2223
self.channel = channel
2324
self.config = config
2425
self.metricClient = Opentelemetry_Proto_Collector_Metrics_V1_MetricsServiceClient(channel: self.channel)
25-
26-
if let headers = EnvVarHeaders.attributes {
27-
callOptions = CallOptions(customMetadata: HPACKHeaders(headers))
26+
if let headers = envVarHeaders {
27+
callOptions = CallOptions(customMetadata: HPACKHeaders(headers), logger: logger)
2828
} else if let headers = config.headers {
29-
callOptions = CallOptions(customMetadata: HPACKHeaders(headers))
29+
callOptions = CallOptions(customMetadata: HPACKHeaders(headers), logger: logger)
30+
}
31+
else {
32+
callOptions = CallOptions(logger: logger)
3033
}
3134
}
3235

Sources/Exporters/OpenTelemetryProtocol/trace/OtlpTraceExporter.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import Foundation
7+
import Logging
78
import GRPC
89
import NIO
910
import NIOHPACK
@@ -16,14 +17,17 @@ public class OtlpTraceExporter: SpanExporter {
1617
let config : OtlpConfiguration
1718
var callOptions : CallOptions? = nil
1819

19-
public init(channel: GRPCChannel, config: OtlpConfiguration = OtlpConfiguration()) {
20+
public init(channel: GRPCChannel, config: OtlpConfiguration = OtlpConfiguration(), logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }), envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes) {
2021
self.channel = channel
2122
traceClient = Opentelemetry_Proto_Collector_Trace_V1_TraceServiceClient(channel: channel)
2223
self.config = config
23-
if let headers = EnvVarHeaders.attributes {
24-
callOptions = CallOptions(customMetadata: HPACKHeaders(headers))
24+
if let headers = envVarHeaders {
25+
callOptions = CallOptions(customMetadata: HPACKHeaders(headers), logger: logger)
2526
} else if let headers = config.headers {
26-
callOptions = CallOptions(customMetadata: HPACKHeaders(headers))
27+
callOptions = CallOptions(customMetadata: HPACKHeaders(headers), logger: logger)
28+
}
29+
else {
30+
callOptions = CallOptions(logger: logger)
2731
}
2832
}
2933

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import Foundation
7+
import OpenTelemetryProtocolExporter
8+
import XCTest
9+
10+
class EnvVarHeadersTests: XCTestCase {
11+
12+
func test_attributesIsNil_whenAccessedThroughStaticProperty() throws {
13+
XCTAssertNil(EnvVarHeaders.attributes)
14+
}
15+
16+
func test_attributesIsNil_whenNoRawValueProvided() throws {
17+
XCTAssertNil(EnvVarHeaders.attributes(for: nil))
18+
}
19+
20+
func test_attributesContainOneKeyValuePair_whenRawValueProvidedHasOneKeyValuePair() throws {
21+
let capturedAttributes = EnvVarHeaders.attributes(for: "key1=value1")
22+
XCTAssertNotNil(capturedAttributes)
23+
XCTAssertEqual(capturedAttributes![0].0, "key1")
24+
XCTAssertEqual(capturedAttributes![0].1, "value1")
25+
}
26+
27+
func test_attributesIsNil_whenInvalidRawValueProvided() throws {
28+
XCTAssertNil(EnvVarHeaders.attributes(for: "key1"))
29+
}
30+
31+
func test_attributesContainTwoKeyValuePair_whenRawValueProvidedHasTwoKeyValuePair() throws {
32+
let capturedAttributes = EnvVarHeaders.attributes(for: " key1=value1, key2 = value2 ")
33+
XCTAssertNotNil(capturedAttributes)
34+
XCTAssertEqual(capturedAttributes![0].0, "key1")
35+
XCTAssertEqual(capturedAttributes![0].1, "value1")
36+
XCTAssertEqual(capturedAttributes![1].0, "key2")
37+
XCTAssertEqual(capturedAttributes![1].1, "value2")
38+
}
39+
40+
func test_attributesIsNil_whenRawValueContainsWhiteSpace() throws {
41+
let capturedAttributes = EnvVarHeaders.attributes(for: "key=value with\twhitespace")
42+
XCTAssertNil(capturedAttributes)
43+
}
44+
45+
func test_attributesExcludesInvalidTuples_whenRawValueContainsInvalidCharacters() throws {
46+
let capturedAttributes = EnvVarHeaders.attributes(for: "key=value with whitespace")
47+
XCTAssertNil(capturedAttributes)
48+
}
49+
50+
}

Tests/ExportersTests/OpenTelemetryProtocol/OtlpMetricExporterTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
15

26
import Foundation
7+
import Logging
38
import GRPC
49
import NIO
510
import OpenTelemetryApi
@@ -37,6 +42,42 @@ class OtlpMetricExproterTests: XCTestCase {
3742
exporter.shutdown()
3843
}
3944

45+
func testImplicitGrpcLoggingConfig() throws {
46+
let exporter = OtlpMetricExporter(channel: channel)
47+
guard let logger = exporter.callOptions?.logger else {
48+
throw "Missing logger"
49+
}
50+
XCTAssertEqual(logger.label, "io.grpc")
51+
}
52+
53+
func testExplicitGrpcLoggingConfig() throws {
54+
let exporter = OtlpMetricExporter(channel: channel, logger: Logger(label: "my.grpc.logger"))
55+
guard let logger = exporter.callOptions?.logger else {
56+
throw "Missing logger"
57+
}
58+
XCTAssertEqual(logger.label, "my.grpc.logger")
59+
}
60+
61+
func testConfigHeadersIsNil_whenDefaultInitCalled() throws {
62+
let exporter = OtlpMetricExporter(channel: channel)
63+
XCTAssertNil(exporter.config.headers)
64+
}
65+
66+
func testConfigHeadersAreSet_whenInitCalledWithCustomConfig() throws {
67+
let config: OtlpConfiguration = OtlpConfiguration(timeout: TimeInterval(10), headers: [("FOO", "BAR")])
68+
let exporter = OtlpMetricExporter(channel: channel, config: config)
69+
XCTAssertNotNil(exporter.config.headers)
70+
XCTAssertEqual(exporter.config.headers?[0].0, "FOO")
71+
XCTAssertEqual(exporter.config.headers?[0].1, "BAR")
72+
XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO"))
73+
}
74+
75+
func testConfigHeadersAreSet_whenInitCalledWithExplicitHeaders() throws {
76+
let exporter = OtlpMetricExporter(channel: channel, envVarHeaders: [("FOO", "BAR")])
77+
XCTAssertNil(exporter.config.headers)
78+
XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO"))
79+
}
80+
4081
func testGaugeExport() {
4182
let metric = generateGaugeMetric()
4283
let exporter = OtlpMetricExporter(channel: channel)

Tests/ExportersTests/OpenTelemetryProtocol/OtlpTraceExporterTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
*/
55

66
import Foundation
7+
import Logging
78
import GRPC
89
import NIO
910
import OpenTelemetryApi
1011
@testable import OpenTelemetryProtocolExporter
1112
@testable import OpenTelemetrySdk
1213
import XCTest
1314

15+
extension String: LocalizedError {
16+
public var errorDescription: String? { return self }
17+
}
18+
1419
class OtlpTraceExporterTests: XCTestCase {
1520
let traceId = "00000000000000000000000000abc123"
1621
let spanId = "0000000000def456"
@@ -42,6 +47,42 @@ class OtlpTraceExporterTests: XCTestCase {
4247
exporter.shutdown()
4348
}
4449

50+
func testImplicitGrpcLoggingConfig() throws {
51+
let exporter = OtlpTraceExporter(channel: channel)
52+
guard let logger = exporter.callOptions?.logger else {
53+
throw "Missing logger"
54+
}
55+
XCTAssertEqual(logger.label, "io.grpc")
56+
}
57+
58+
func testExplicitGrpcLoggingConfig() throws {
59+
let exporter = OtlpTraceExporter(channel: channel, logger: Logger(label: "my.grpc.logger"))
60+
guard let logger = exporter.callOptions?.logger else {
61+
throw "Missing logger"
62+
}
63+
XCTAssertEqual(logger.label, "my.grpc.logger")
64+
}
65+
66+
func testConfigHeadersIsNil_whenDefaultInitCalled() throws {
67+
let exporter = OtlpTraceExporter(channel: channel)
68+
XCTAssertNil(exporter.config.headers)
69+
}
70+
71+
func testConfigHeadersAreSet_whenInitCalledWithCustomConfig() throws {
72+
let config: OtlpConfiguration = OtlpConfiguration(timeout: TimeInterval(10), headers: [("FOO", "BAR")])
73+
let exporter = OtlpTraceExporter(channel: channel, config: config)
74+
XCTAssertNotNil(exporter.config.headers)
75+
XCTAssertEqual(exporter.config.headers?[0].0, "FOO")
76+
XCTAssertEqual(exporter.config.headers?[0].1, "BAR")
77+
XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO"))
78+
}
79+
80+
func testConfigHeadersAreSet_whenInitCalledWithExplicitHeaders() throws {
81+
let exporter = OtlpTraceExporter(channel: channel, envVarHeaders: [("FOO", "BAR")])
82+
XCTAssertNil(exporter.config.headers)
83+
XCTAssertEqual("BAR", exporter.callOptions?.customMetadata.first(name: "FOO"))
84+
}
85+
4586
func testExportMultipleSpans() {
4687
var spans = [SpanData]()
4788
for _ in 0 ..< 10 {

0 commit comments

Comments
 (0)