Skip to content

Commit 6570e78

Browse files
authored
added initial exporter metric code (#637)
1 parent 725e134 commit 6570e78

File tree

6 files changed

+280
-52
lines changed

6 files changed

+280
-52
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//
2+
// Copyright The OpenTelemetry Authors
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
import Foundation
6+
import OpenTelemetryApi
7+
8+
/// `ExporterMetrics` will provide a way to track how many data have been seen or successfully exported,
9+
/// as well as how many failed. The exporter will adopt an instance of this and inject the provider as a dependency.
10+
/// The host application can then track different types of exporters, such as `http, grpc, and log`
11+
public class ExporterMetrics {
12+
public enum TransporterType: String {
13+
case grpc = "grpc"
14+
case protoBuf = "http"
15+
case httpJson = "http-json"
16+
}
17+
18+
public static let ATTRIBUTE_KEY_TYPE: String = "type"
19+
public static let ATTRIBUTE_KEY_SUCCESS: String = "success"
20+
21+
private let meterProvider: StableMeterProvider
22+
private let exporterName: String
23+
private let transportName: String
24+
private var seenAttrs: [String: AttributeValue] = [:]
25+
private var successAttrs: [String: AttributeValue] = [:]
26+
private var failedAttrs: [String: AttributeValue] = [:]
27+
28+
private var seen: LongCounter?
29+
private var exported: LongCounter?
30+
31+
/// - Parameters:
32+
/// - type: That represent what type of exporter it is. `otlp`
33+
/// - meterProvider: Injected `StableMeterProvider` for metric
34+
/// - exporterName: Could be `span`, `log` etc
35+
/// - transportName: Kind of exporter defined by type `TransporterType`
36+
public init(
37+
type: String,
38+
meterProvider: StableMeterProvider,
39+
exporterName: String,
40+
transportName: TransporterType
41+
) {
42+
self.meterProvider = meterProvider
43+
self.exporterName = exporterName
44+
self.transportName = transportName.rawValue
45+
self.seenAttrs = [
46+
ExporterMetrics.ATTRIBUTE_KEY_TYPE: .string(type)
47+
]
48+
self.successAttrs = [
49+
ExporterMetrics.ATTRIBUTE_KEY_SUCCESS: .bool(true)
50+
]
51+
self.failedAttrs = [
52+
ExporterMetrics.ATTRIBUTE_KEY_SUCCESS: .bool(false)
53+
]
54+
55+
self.seen = meter.counterBuilder(name: "\(exporterName).exporter.seen").build()
56+
self.exported = meter.counterBuilder(name: "\(exporterName).exporter.exported").build()
57+
58+
}
59+
60+
public func addSeen(value: Int) -> Void {
61+
seen?.add(value: value, attribute: seenAttrs)
62+
}
63+
64+
public func addSuccess(value: Int) -> Void {
65+
exported?.add(value: value, attribute: successAttrs)
66+
}
67+
68+
public func addFailed(value: Int) -> Void {
69+
exported?.add(value: value, attribute: failedAttrs)
70+
}
71+
72+
// MARK: - Private functions
73+
74+
/***
75+
* Create an instance for recording exporter metrics under the meter
76+
* "io.opentelemetry.exporters." + exporterName + "-transporterType".
77+
**/
78+
private var meter: StableMeter {
79+
meterProvider.get(name: "io.opentelemetry.exporters.\(exporterName)-\(transportName)")
80+
}
81+
82+
// MARK: - Static function
83+
84+
public static func makeExporterMetric(
85+
type: String,
86+
meterProvider: StableMeterProvider,
87+
exporterName: String,
88+
transportName: TransporterType
89+
) -> ExporterMetrics {
90+
ExporterMetrics(
91+
type: type,
92+
meterProvider: meterProvider,
93+
exporterName: exporterName,
94+
transportName: transportName
95+
)
96+
}
97+
}

Sources/Exporters/OpenTelemetryProtocolCommon/common/OtlpConfiguration.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,16 @@ public struct OtlpConfiguration {
3030
public let headers : [(String,String)]?
3131
public let timeout : TimeInterval
3232
public let compression: CompressionType
33-
33+
public let exportAsJson: Bool
3434
public init(
3535
timeout : TimeInterval = OtlpConfiguration.DefaultTimeoutInterval,
3636
compression: CompressionType = .gzip,
37-
headers: [(String,String)]? = nil
37+
headers: [(String,String)]? = nil,
38+
exportAsJson: Bool = true
3839
) {
3940
self.headers = headers
4041
self.timeout = timeout
4142
self.compression = compression
43+
self.exportAsJson = exportAsJson
4244
}
4345
}

Sources/Exporters/OpenTelemetryProtocolHttp/OtlpHttpExporterBase.swift

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,25 @@ import FoundationNetworking
1414
#endif
1515

1616
public class OtlpHttpExporterBase {
17-
let endpoint: URL
18-
let httpClient: HTTPClient
19-
let envVarHeaders : [(String,String)]?
20-
21-
let config : OtlpConfiguration
22-
public init(endpoint: URL, config: OtlpConfiguration = OtlpConfiguration(), useSession: URLSession? = nil, envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes) {
23-
self.envVarHeaders = envVarHeaders
24-
25-
self.endpoint = endpoint
26-
self.config = config
27-
if let providedSession = useSession {
28-
self.httpClient = HTTPClient(session: providedSession)
29-
} else {
30-
self.httpClient = HTTPClient()
31-
}
17+
let endpoint: URL
18+
let httpClient: HTTPClient
19+
let envVarHeaders : [(String,String)]?
20+
let config : OtlpConfiguration
21+
22+
public init(
23+
endpoint: URL,
24+
config: OtlpConfiguration = OtlpConfiguration(),
25+
useSession: URLSession? = nil,
26+
envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes
27+
) {
28+
self.envVarHeaders = envVarHeaders
29+
self.endpoint = endpoint
30+
self.config = config
31+
if let providedSession = useSession {
32+
self.httpClient = HTTPClient(session: providedSession)
33+
} else {
34+
self.httpClient = HTTPClient()
35+
}
3236
}
3337

3438
public func createRequest(body: Message, endpoint: URL) -> URLRequest {

Sources/Exporters/OpenTelemetryProtocolHttp/logs/OtlpHttpLogExporter.swift

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import Foundation
77
import OpenTelemetryProtocolExporterCommon
88
import OpenTelemetrySdk
9+
import OpenTelemetryApi
910
#if canImport(FoundationNetworking)
1011
import FoundationNetworking
1112
#endif
@@ -17,13 +18,47 @@ public func defaultOltpHttpLoggingEndpoint() -> URL {
1718
public class OtlpHttpLogExporter: OtlpHttpExporterBase, LogRecordExporter {
1819
var pendingLogRecords: [ReadableLogRecord] = []
1920
private let exporterLock = Lock()
20-
override public init(endpoint: URL = defaultOltpHttpLoggingEndpoint(),
21-
config: OtlpConfiguration = OtlpConfiguration(),
22-
useSession: URLSession? = nil,
23-
envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes) {
24-
super.init(endpoint: endpoint, config: config, useSession: useSession, envVarHeaders: envVarHeaders)
21+
private var exporterMetrics: ExporterMetrics?
22+
23+
override public init(
24+
endpoint: URL = defaultOltpHttpLoggingEndpoint(),
25+
config: OtlpConfiguration = OtlpConfiguration(),
26+
useSession: URLSession? = nil,
27+
envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes
28+
) {
29+
super.init(
30+
endpoint: endpoint,
31+
config: config,
32+
useSession: useSession,
33+
envVarHeaders: envVarHeaders
34+
)
2535
}
2636

37+
/// A `convenience` constructor to provide support for exporter metric using`StableMeterProvider` type
38+
/// - Parameters:
39+
/// - endpoint: Exporter endpoint injected as dependency
40+
/// - config: Exporter configuration including type of exporter
41+
/// - meterProvider: Injected `StableMeterProvider` for metric
42+
/// - useSession: Overridden `URLSession` if any
43+
/// - envVarHeaders: Extra header key-values
44+
convenience public init(
45+
endpoint: URL = defaultOltpHttpLoggingEndpoint(),
46+
config: OtlpConfiguration = OtlpConfiguration(),
47+
meterProvider: StableMeterProvider,
48+
useSession: URLSession? = nil,
49+
envVarHeaders: [(String, String)]? = EnvVarHeaders.attributes
50+
) {
51+
self.init(endpoint: endpoint, config: config, useSession: useSession, envVarHeaders: envVarHeaders)
52+
exporterMetrics = ExporterMetrics(
53+
type: "otlp",
54+
meterProvider: meterProvider,
55+
exporterName: "log",
56+
transportName: config.exportAsJson ?
57+
ExporterMetrics.TransporterType.httpJson :
58+
ExporterMetrics.TransporterType.grpc
59+
)
60+
}
61+
2762
public func export(logRecords: [OpenTelemetrySdk.ReadableLogRecord], explicitTimeout: TimeInterval? = nil) -> OpenTelemetrySdk.ExportResult {
2863
var sendingLogRecords: [ReadableLogRecord] = []
2964
exporterLock.withLockVoid {
@@ -47,12 +82,15 @@ public class OtlpHttpLogExporter: OtlpHttpExporterBase, LogRecordExporter {
4782
request.addValue(value, forHTTPHeaderField: key)
4883
}
4984
}
85+
exporterMetrics?.addSeen(value: sendingLogRecords.count)
5086
request.timeoutInterval = min(explicitTimeout ?? TimeInterval.greatestFiniteMagnitude, config.timeout)
5187
httpClient.send(request: request) { [weak self] result in
5288
switch result {
5389
case .success:
90+
self?.exporterMetrics?.addSuccess(value: sendingLogRecords.count)
5491
break
5592
case let .failure(error):
93+
self?.exporterMetrics?.addFailed(value: sendingLogRecords.count)
5694
self?.exporterLock.withLockVoid {
5795
self?.pendingLogRecords.append(contentsOf: sendingLogRecords)
5896
}
@@ -90,11 +128,13 @@ public class OtlpHttpLogExporter: OtlpHttpExporterBase, LogRecordExporter {
90128
request.addValue(value, forHTTPHeaderField: key)
91129
}
92130
}
93-
httpClient.send(request: request) { result in
131+
httpClient.send(request: request) { [weak self] result in
94132
switch result {
95133
case .success:
134+
self?.exporterMetrics?.addSuccess(value: pendingLogRecords.count)
96135
exporterResult = ExportResult.success
97136
case let .failure(error):
137+
self?.exporterMetrics?.addFailed(value: pendingLogRecords.count)
98138
print(error)
99139
exporterResult = ExportResult.failure
100140
}

Sources/Exporters/OpenTelemetryProtocolHttp/metric/OltpHTTPMetricExporter.swift

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import OpenTelemetrySdk
77
import OpenTelemetryProtocolExporterCommon
88
import Foundation
9+
import OpenTelemetryApi
910
#if canImport(FoundationNetworking)
1011
import FoundationNetworking
1112
#endif
@@ -16,14 +17,50 @@ public func defaultOltpHTTPMetricsEndpoint() -> URL {
1617

1718
@available(*, deprecated, renamed: "StableOtlpHTTPMetricExporter")
1819
public class OtlpHttpMetricExporter: OtlpHttpExporterBase, MetricExporter {
19-
var pendingMetrics: [Metric] = []
20-
private let exporterLock = Lock()
21-
20+
var pendingMetrics: [Metric] = []
21+
private let exporterLock = Lock()
22+
private var exporterMetrics: ExporterMetrics?
23+
2224
override
23-
public init(endpoint: URL = defaultOltpHTTPMetricsEndpoint(), config : OtlpConfiguration = OtlpConfiguration(), useSession: URLSession? = nil, envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes) {
24-
super.init(endpoint: endpoint, config: config, useSession: useSession, envVarHeaders: envVarHeaders)
25+
public init(
26+
endpoint: URL = defaultOltpHTTPMetricsEndpoint(),
27+
config : OtlpConfiguration = OtlpConfiguration(),
28+
useSession: URLSession? = nil,
29+
envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes
30+
) {
31+
super.init(
32+
endpoint: endpoint,
33+
config: config,
34+
useSession: useSession,
35+
envVarHeaders: envVarHeaders
36+
)
2537
}
26-
38+
39+
/// A `convenience` constructor to provide support for exporter metric using`StableMeterProvider` type
40+
/// - Parameters:
41+
/// - endpoint: Exporter endpoint injected as dependency
42+
/// - config: Exporter configuration including type of exporter
43+
/// - meterProvider: Injected `StableMeterProvider` for metric
44+
/// - useSession: Overridden `URLSession` if any
45+
/// - envVarHeaders: Extra header key-values
46+
convenience public init(
47+
endpoint: URL = defaultOltpHTTPMetricsEndpoint(),
48+
config : OtlpConfiguration = OtlpConfiguration(),
49+
meterProvider: StableMeterProvider,
50+
useSession: URLSession? = nil,
51+
envVarHeaders: [(String,String)]? = EnvVarHeaders.attributes
52+
) {
53+
self.init(endpoint: endpoint, config: config, useSession: useSession, envVarHeaders: envVarHeaders)
54+
exporterMetrics = ExporterMetrics(
55+
type: "otlp",
56+
meterProvider: meterProvider,
57+
exporterName: "metric",
58+
transportName: config.exportAsJson ?
59+
ExporterMetrics.TransporterType.httpJson :
60+
ExporterMetrics.TransporterType.grpc
61+
)
62+
}
63+
2764
public func export(metrics: [Metric], shouldCancel: (() -> Bool)?) -> MetricExporterResultCode {
2865
var sendingMetrics: [Metric] = []
2966
exporterLock.withLockVoid {
@@ -46,11 +83,14 @@ public class OtlpHttpMetricExporter: OtlpHttpExporterBase, MetricExporter {
4683
request.addValue(value, forHTTPHeaderField: key)
4784
}
4885
}
86+
exporterMetrics?.addSeen(value: sendingMetrics.count)
4987
httpClient.send(request: request) { [weak self] result in
5088
switch result {
5189
case .success(_):
90+
self?.exporterMetrics?.addSuccess(value: sendingMetrics.count)
5291
break
5392
case .failure(let error):
93+
self?.exporterMetrics?.addFailed(value: sendingMetrics.count)
5494
self?.exporterLock.withLockVoid {
5595
self?.pendingMetrics.append(contentsOf: sendingMetrics)
5696
}
@@ -64,25 +104,27 @@ public class OtlpHttpMetricExporter: OtlpHttpExporterBase, MetricExporter {
64104
public func flush() -> MetricExporterResultCode {
65105
var exporterResult: MetricExporterResultCode = .success
66106

67-
if !pendingMetrics.isEmpty {
68-
let body = Opentelemetry_Proto_Collector_Metrics_V1_ExportMetricsServiceRequest.with {
69-
$0.resourceMetrics = MetricsAdapter.toProtoResourceMetrics(metricDataList: pendingMetrics)
107+
if !pendingMetrics.isEmpty {
108+
let body = Opentelemetry_Proto_Collector_Metrics_V1_ExportMetricsServiceRequest.with {
109+
$0.resourceMetrics = MetricsAdapter.toProtoResourceMetrics(metricDataList: pendingMetrics)
110+
}
111+
112+
let semaphore = DispatchSemaphore(value: 0)
113+
let request = createRequest(body: body, endpoint: endpoint)
114+
httpClient.send(request: request) { [weak self, count = pendingMetrics.count] result in
115+
switch result {
116+
case .success(_):
117+
self?.exporterMetrics?.addSuccess(value: count)
118+
break
119+
case .failure(let error):
120+
self?.exporterMetrics?.addFailed(value: count)
121+
print(error)
122+
exporterResult = MetricExporterResultCode.failureNotRetryable
123+
}
124+
semaphore.signal()
125+
}
126+
semaphore.wait()
70127
}
71-
72-
let semaphore = DispatchSemaphore(value: 0)
73-
let request = createRequest(body: body, endpoint: endpoint)
74-
httpClient.send(request: request) { result in
75-
switch result {
76-
case .success(_):
77-
break
78-
case .failure(let error):
79-
print(error)
80-
exporterResult = MetricExporterResultCode.failureNotRetryable
81-
}
82-
semaphore.signal()
83-
}
84-
semaphore.wait()
85-
}
86-
return exporterResult
128+
return exporterResult
87129
}
88130
}

0 commit comments

Comments
 (0)