Skip to content

Commit 5352d75

Browse files
author
Ignacio Bonafonte
authored
Merge pull request #378 from open-telemetry/metric-refresh-2
otel-swift implementation of the current OpenTelemetry metrics specification. The existing otel-swift metric implementation is old and out-of-spec. While Stable Metrics is in an experimental phase it will maintain the "stable" prefix, and can be expected to be present on overlapping constructs in the implementation. Expected time line will be as follows: Phase 1: Provide access to Stable Metrics along side existing Metrics. Once Stable Metrics are considered stable we will move onto phase 2. Phase 2: Mark all existing Metric APIs as deprecated. This will maintained for a period TBD Phase 3: Remove deprecated metrics api and remove Stable prefix from Stable metrics.
2 parents f0bdf86 + cbf6d05 commit 5352d75

File tree

165 files changed

+5692
-23
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

165 files changed

+5692
-23
lines changed

.swift-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5.2

Examples/Metric Sample/main.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Copyright The OpenTelemetry Authors
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
import Foundation
7+
import OpenTelemetryApi
8+
import OpenTelemetrySdk
9+
10+
func configure() {
11+
let configuration = ClientConnection.Configuration.default(
12+
target: .hostAndPort("localhost", 4317),
13+
eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1)
14+
)
15+
let client = ClientConnection(configuration: configuration)
16+
17+
let resource = Resource.init(attributes: ["service.name": "StableMetricExample"]).merge(other: resource())
18+
19+
OpenTelemetry.registerMeterProvider(meterProvider: StableMeterProviderSdk.builder().
20+
registerMetricReader(reader: StablePeriodicMetricReaderBuilder(exporter: StableOtlpMetricExporter(channel: client))
21+
.setInterval(timeInterval: 60).build()).build())
22+
}
23+
24+
25+
configure()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
### Stable metrics
2+
3+
Stable metrics is the working name for the otel-swift implementation of the current OpenTelemetry metrics specification.
4+
The existing otel-swift metric implementation is old and out-of-spec. While Stable Metrics is in an experimental phase it will maintaion
5+
the "stable" prefix, and can be expected to be present on overlapping constructs in the implementation.
6+
Expected time line will be as follows:
7+
Phase 1:
8+
Provide access to Stable Metrics along side existing Metrics. Once Stable Metrics are considered stable we will move onto phase 2.
9+
Phase 2:
10+
Mark all existing Metric APIs as deprecated. This will maintained for a period TBD
11+
Phase 3:
12+
Remove deprecated metrics api and remove Stable prefix from Stable metrics.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// Copyright The OpenTelemetry Authors
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
7+
import Foundation
8+
import OpenTelemetryApi
9+
import OpenTelemetrySdk
10+
import OpenTelemetryProtocolExporter
11+
import GRPC
12+
import NIO
13+
import NIOHPACK
14+
15+
/*
16+
Stable metrics is the working name for the otel-swift implementation of the current OpenTelemetry metrics specification.
17+
The existing otel-swift metric implementation is old and out-of-spec. While Stable Metrics is in an experimental phase it will maintaion
18+
the "stable" prefix, and can be expected to be present on overlapping constructs in the implementation.
19+
Expected time line will be as follows:
20+
Phase 1:
21+
Provide access to Stable Metrics along side existing Metrics. Once Stable Metrics are considered stable we will move onto phase 2.
22+
Phase 2:
23+
Mark all existing Metric APIs as deprecated. This will maintained for a period TBD
24+
Phase 3:
25+
Remove deprecated metrics api and remove Stable prefix from Stable metrics.
26+
27+
28+
Below is an example used the Stable Metrics API
29+
30+
*/
31+
32+
33+
34+
35+
/*
36+
Basic configuration for metrics
37+
*/
38+
func basicConfiguration() {
39+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
40+
let exporterChannel = ClientConnection.insecure(group: group)
41+
.connect(host: "localhost", port: 8200)
42+
43+
44+
// register view will process all instruments using `.*` regex
45+
46+
OpenTelemetry.registerStableMeterProvider(meterProvider: StableMeterProviderBuilder()
47+
.registerView(selector: InstrumentSelector.builder().setInstrument(name: ".*").build(), view: StableView.builder().build())
48+
.registerMetricReader(reader:StablePeriodicMetricReaderBuilder(exporter: StableOtlpMetricExporter(channel: exporterChannel)).build())
49+
.build()
50+
)
51+
}
52+
53+
54+
func complexViewConfiguration() {
55+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
56+
let exporterChannel = ClientConnection.insecure(group: group)
57+
.connect(host: "localhost", port: 8200)
58+
59+
// The example registers a View that re-configures the Gauge instrument into a sum instrument named "GaugeSum"
60+
61+
OpenTelemetry.registerStableMeterProvider(meterProvider: StableMeterProviderBuilder()
62+
.registerView(selector: InstrumentSelector.builder().setInstrument(name: "Gauge").build(), view: StableView.builder().withName(name: "GaugeSum").withAggregation(aggregation: Aggregations.sum()).build())
63+
.registerMetricReader(reader:StablePeriodicMetricReaderBuilder(exporter: StableOtlpMetricExporter(channel: exporterChannel)).build())
64+
.build()
65+
)
66+
}
67+
68+
basicConfiguration()
69+
70+
// creating a new meter & instrument
71+
let meter = OpenTelemetry.instance.stableMeterProvider?.meterBuilder(name: "MyMeter").build()
72+
var gaugeBuilder = meter!.gaugeBuilder(name: "Gauge")
73+
var gauge = gaugeBuilder.buildWithCallback({ ObservableDoubleMeasurement in
74+
ObservableDoubleMeasurement.record(value: 1.0, attributes: ["test": AttributeValue.bool(true)])
75+
})

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ let package = Package(
192192
dependencies: ["DatadogExporter"],
193193
path: "Examples/Datadog Sample",
194194
exclude: ["README.md"]),
195+
.target(name: "StableMetricSample",
196+
dependencies: ["OpenTelemetrySdk", "OpenTelemetryApi", "OpenTelemetryProtocolExporter", .product(name: "GRPC", package: "grpc-swift")],
197+
path: "Examples/Stable Metric Sample",
198+
exclude: ["README.md"]),
195199
.target(name: "NetworkSample",
196200
dependencies: ["URLSessionInstrumentation", "StdoutExporter"],
197201
path: "Examples/Network Sample",

Sources/Exporters/OpenTelemetryProtocolCommon/metric/MetricsAdapter.swift

Lines changed: 185 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,30 @@ import Foundation
66
import OpenTelemetryApi
77
import OpenTelemetrySdk
88

9-
public struct MetricsAdapter {
9+
public enum MetricsAdapter {
10+
public static func toProtoResourceMetrics(stableMetricData: [StableMetricData]) -> [Opentelemetry_Proto_Metrics_V1_ResourceMetrics] {
11+
let resourceAndScopeMap = groupByResouceAndScope(stableMetricData: stableMetricData)
12+
13+
var resourceMetrics = [Opentelemetry_Proto_Metrics_V1_ResourceMetrics]()
14+
resourceAndScopeMap.forEach { resMap in
15+
var instrumentationScopeMetrics = [Opentelemetry_Proto_Metrics_V1_ScopeMetrics]()
16+
resMap.value.forEach { instScope in
17+
var protoInst = Opentelemetry_Proto_Metrics_V1_ScopeMetrics()
18+
protoInst.scope =
19+
CommonAdapter.toProtoInstrumentationScope(instrumentationScopeInfo: instScope.key)
20+
instScope.value.forEach {
21+
protoInst.metrics.append($0)
22+
}
23+
instrumentationScopeMetrics.append(protoInst)
24+
}
25+
var resourceMetric = Opentelemetry_Proto_Metrics_V1_ResourceMetrics()
26+
resourceMetric.resource = ResourceAdapter.toProtoResource(resource: resMap.key)
27+
resourceMetric.scopeMetrics.append(contentsOf: instrumentationScopeMetrics)
28+
resourceMetrics.append(resourceMetric)
29+
}
30+
return resourceMetrics
31+
}
32+
1033
public static func toProtoResourceMetrics(metricDataList: [Metric]) -> [Opentelemetry_Proto_Metrics_V1_ResourceMetrics] {
1134
let resourceAndScopeMap = groupByResouceAndScope(metricDataList: metricDataList)
1235
var resourceMetrics = [Opentelemetry_Proto_Metrics_V1_ResourceMetrics]()
@@ -15,7 +38,7 @@ public struct MetricsAdapter {
1538
var instrumentationScopeMetrics = [Opentelemetry_Proto_Metrics_V1_ScopeMetrics]()
1639
resMap.value.forEach { instScope in
1740
var protoInst =
18-
Opentelemetry_Proto_Metrics_V1_ScopeMetrics()
41+
Opentelemetry_Proto_Metrics_V1_ScopeMetrics()
1942
protoInst.scope =
2043
CommonAdapter.toProtoInstrumentationScope(instrumentationScopeInfo: instScope.key)
2144
instScope.value.forEach {
@@ -32,6 +55,17 @@ public struct MetricsAdapter {
3255
return resourceMetrics
3356
}
3457

58+
private static func groupByResouceAndScope(stableMetricData: [StableMetricData]) -> [Resource: [InstrumentationScopeInfo: [Opentelemetry_Proto_Metrics_V1_Metric]]] {
59+
var results = [Resource: [InstrumentationScopeInfo: [Opentelemetry_Proto_Metrics_V1_Metric]]]()
60+
61+
stableMetricData.forEach {
62+
if let metric = toProtoMetric(stableMetric: $0) {
63+
results[$0.resource, default: [InstrumentationScopeInfo: [Opentelemetry_Proto_Metrics_V1_Metric]]()][$0.instrumentationScopeInfo, default: [Opentelemetry_Proto_Metrics_V1_Metric]()].append(metric)
64+
}
65+
}
66+
return results
67+
}
68+
3569
private static func groupByResouceAndScope(metricDataList: [Metric]) -> [Resource: [InstrumentationScopeInfo: [Opentelemetry_Proto_Metrics_V1_Metric]]] {
3670
var results = [Resource: [InstrumentationScopeInfo: [Opentelemetry_Proto_Metrics_V1_Metric]]]()
3771

@@ -45,6 +79,151 @@ public struct MetricsAdapter {
4579
return results
4680
}
4781

82+
public static func toProtoMetric(stableMetric: StableMetricData) -> Opentelemetry_Proto_Metrics_V1_Metric? {
83+
var protoMetric = Opentelemetry_Proto_Metrics_V1_Metric()
84+
protoMetric.name = stableMetric.name
85+
protoMetric.unit = stableMetric.unit
86+
protoMetric.description_p = stableMetric.description
87+
if stableMetric.data.points.isEmpty { return nil }
88+
89+
stableMetric.data.points.forEach {
90+
switch stableMetric.type {
91+
case .LongGauge:
92+
guard let gaugeData = $0 as? LongPointData else {
93+
break
94+
}
95+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint()
96+
injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData)
97+
protoDataPoint.value = .asInt(Int64(gaugeData.value))
98+
protoMetric.gauge.dataPoints.append(protoDataPoint)
99+
case .LongSum:
100+
guard let gaugeData = $0 as? LongPointData else {
101+
break
102+
}
103+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint()
104+
injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData)
105+
protoDataPoint.value = .asInt(Int64(gaugeData.value))
106+
protoMetric.sum.aggregationTemporality = .cumulative
107+
protoMetric.sum.dataPoints.append(protoDataPoint)
108+
case .DoubleGauge:
109+
guard let gaugeData = $0 as? DoublePointData else {
110+
break
111+
}
112+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint()
113+
injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData)
114+
protoDataPoint.value = .asDouble(gaugeData.value)
115+
protoMetric.gauge.dataPoints.append(protoDataPoint)
116+
case .DoubleSum:
117+
guard let gaugeData = $0 as? DoublePointData else {
118+
break
119+
}
120+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_NumberDataPoint()
121+
injectPointData(protoNumberPoint: &protoDataPoint, pointData: gaugeData)
122+
protoDataPoint.value = .asDouble(gaugeData.value)
123+
protoMetric.sum.aggregationTemporality = .cumulative
124+
protoMetric.sum.dataPoints.append(protoDataPoint)
125+
case .Summary:
126+
guard let summaryData = $0 as? SummaryPointData else {
127+
break
128+
}
129+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_SummaryDataPoint()
130+
injectPointData(protoSummaryPoint: &protoDataPoint, pointData: summaryData)
131+
protoDataPoint.sum = summaryData.sum
132+
protoDataPoint.count = summaryData.count
133+
summaryData.values.forEach {
134+
var quantile = Opentelemetry_Proto_Metrics_V1_SummaryDataPoint.ValueAtQuantile()
135+
quantile.quantile = $0.quantile()
136+
quantile.value = $0.value()
137+
protoDataPoint.quantileValues.append(quantile)
138+
}
139+
protoMetric.summary.dataPoints.append(protoDataPoint)
140+
case .Histogram:
141+
guard let histogramData = $0 as? HistogramPointData else {
142+
break
143+
}
144+
var protoDataPoint = Opentelemetry_Proto_Metrics_V1_HistogramDataPoint()
145+
injectPointData(protoHistogramPoint: &protoDataPoint, pointData: histogramData)
146+
protoDataPoint.sum = Double(histogramData.sum)
147+
protoDataPoint.count = UInt64(histogramData.count)
148+
protoDataPoint.explicitBounds = histogramData.boundaries.map { Double($0) }
149+
protoDataPoint.bucketCounts = histogramData.counts.map { UInt64($0) }
150+
protoMetric.histogram.aggregationTemporality = .cumulative
151+
protoMetric.histogram.dataPoints.append(protoDataPoint)
152+
case .ExponentialHistogram:
153+
// TODO: implement
154+
break
155+
}
156+
}
157+
return protoMetric
158+
}
159+
160+
static func injectPointData(protoHistogramPoint protoPoint: inout Opentelemetry_Proto_Metrics_V1_HistogramDataPoint, pointData: PointData) {
161+
protoPoint.timeUnixNano = pointData.endEpochNanos
162+
protoPoint.startTimeUnixNano = pointData.startEpochNanos
163+
164+
pointData.attributes.forEach {
165+
protoPoint.attributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
166+
}
167+
168+
pointData.exemplars.forEach {
169+
var protoExemplar = Opentelemetry_Proto_Metrics_V1_Exemplar()
170+
protoExemplar.timeUnixNano = $0.epochNanos
171+
172+
$0.filteredAttributes.forEach {
173+
protoExemplar.filteredAttributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
174+
}
175+
if let spanContext = $0.spanContext {
176+
protoExemplar.spanID = TraceProtoUtils.toProtoSpanId(spanId: spanContext.spanId)
177+
protoExemplar.traceID = TraceProtoUtils.toProtoTraceId(traceId: spanContext.traceId)
178+
}
179+
}
180+
}
181+
182+
static func injectPointData(protoSummaryPoint protoPoint: inout Opentelemetry_Proto_Metrics_V1_SummaryDataPoint, pointData: PointData) {
183+
protoPoint.timeUnixNano = pointData.endEpochNanos
184+
protoPoint.startTimeUnixNano = pointData.startEpochNanos
185+
186+
pointData.attributes.forEach {
187+
protoPoint.attributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
188+
}
189+
190+
pointData.exemplars.forEach {
191+
var protoExemplar = Opentelemetry_Proto_Metrics_V1_Exemplar()
192+
protoExemplar.timeUnixNano = $0.epochNanos
193+
194+
$0.filteredAttributes.forEach {
195+
protoExemplar.filteredAttributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
196+
}
197+
if let spanContext = $0.spanContext {
198+
protoExemplar.spanID = TraceProtoUtils.toProtoSpanId(spanId: spanContext.spanId)
199+
protoExemplar.traceID = TraceProtoUtils.toProtoTraceId(traceId: spanContext.traceId)
200+
}
201+
}
202+
}
203+
204+
static func injectPointData(protoNumberPoint protoPoint: inout Opentelemetry_Proto_Metrics_V1_NumberDataPoint, pointData: PointData) {
205+
protoPoint.timeUnixNano = pointData.endEpochNanos
206+
protoPoint.startTimeUnixNano = pointData.startEpochNanos
207+
208+
pointData.attributes.forEach {
209+
protoPoint.attributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
210+
}
211+
212+
pointData.exemplars.forEach {
213+
var protoExemplar = Opentelemetry_Proto_Metrics_V1_Exemplar()
214+
protoExemplar.timeUnixNano = $0.epochNanos
215+
216+
$0.filteredAttributes.forEach {
217+
protoExemplar.filteredAttributes.append(CommonAdapter.toProtoAttribute(key: $0.key, attributeValue: $0.value))
218+
}
219+
if let spanContext = $0.spanContext {
220+
protoExemplar.spanID = TraceProtoUtils.toProtoSpanId(spanId: spanContext.spanId)
221+
protoExemplar.traceID = TraceProtoUtils.toProtoTraceId(traceId: spanContext.traceId)
222+
}
223+
protoPoint.exemplars.append(protoExemplar)
224+
}
225+
}
226+
48227
public static func toProtoMetric(metric: Metric) -> Opentelemetry_Proto_Metrics_V1_Metric? {
49228
var protoMetric = Opentelemetry_Proto_Metrics_V1_Metric()
50229
protoMetric.name = metric.name
@@ -179,14 +358,14 @@ public struct MetricsAdapter {
179358
protoDataPoint.timeUnixNano = histogramData.timestamp.timeIntervalSince1970.toNanoseconds
180359
protoDataPoint.explicitBounds = histogramData.buckets.boundaries.map { Double($0) }
181360
protoDataPoint.bucketCounts = histogramData.buckets.counts.map { UInt64($0) }
182-
361+
183362
histogramData.labels.forEach {
184363
var kvp = Opentelemetry_Proto_Common_V1_KeyValue()
185364
kvp.key = $0.key
186365
kvp.value.stringValue = $0.value
187366
protoDataPoint.attributes.append(kvp)
188367
}
189-
368+
190369
protoMetric.histogram.aggregationTemporality = .cumulative
191370
protoMetric.histogram.dataPoints.append(protoDataPoint)
192371
case .doubleHistogram:
@@ -200,14 +379,14 @@ public struct MetricsAdapter {
200379
protoDataPoint.timeUnixNano = histogramData.timestamp.timeIntervalSince1970.toNanoseconds
201380
protoDataPoint.explicitBounds = histogramData.buckets.boundaries.map { Double($0) }
202381
protoDataPoint.bucketCounts = histogramData.buckets.counts.map { UInt64($0) }
203-
382+
204383
histogramData.labels.forEach {
205384
var kvp = Opentelemetry_Proto_Common_V1_KeyValue()
206385
kvp.key = $0.key
207386
kvp.value.stringValue = $0.value
208387
protoDataPoint.attributes.append(kvp)
209388
}
210-
389+
211390
protoMetric.histogram.aggregationTemporality = .cumulative
212391
protoMetric.histogram.dataPoints.append(protoDataPoint)
213392
}

0 commit comments

Comments
 (0)