Skip to content

Commit 5e02eda

Browse files
author
Ignacio Bonafonte
authored
Merge pull request #279 from shelefg/codable_signals
Make Metric and SpanData conform to Codable to support persisting them. This submission is making Metric and SpanData conform to Codable in order to allow them to be persisted. Being able to persist signals' data is required to support use cases where signals may collected when there is no network connectivity, and so they have to be exported later - possibly after the process was terminated and relaunched - when network connectivity is back. The core of the change is making Metric and SpanData conform to the Codable protocol. To that end all the types Metric and SpanData depend on were made Codable as well. Most of the types could simply have the Codable protocol implementation synthesized. However there were several cases where an explicit implementation was required: Metric - explicit encoding & decoding logic was required to deduce the concrete type of Metric.data by Metric.aggregationType AttributeValue - enum with associated values Status - enum with associated values HistogramData - has a tuple type member Conflict with existing Encodable implementation used specifically in StdoutExporter. In this case an explicit Encodable implementation was written for SpanExporterData. Tests were added to cover the encoding & decoding code, and corresponding Equatable conformance for Metric that was required for tests verification was added. AttributeValue and Status use synthesized Codable implementation for swift >= 5.5
2 parents 2c903eb + b70a9ef commit 5e02eda

File tree

24 files changed

+969
-45
lines changed

24 files changed

+969
-45
lines changed

Sources/Exporters/Stdout/StdoutExporter.swift

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public class StdoutExporter: SpanExporter {
5858
public func shutdown() {}
5959
}
6060

61-
private struct SpanExporterData: Encodable {
61+
private struct SpanExporterData {
6262
private let span: String
6363
private let traceId: String
6464
private let spanId: String
@@ -83,3 +83,92 @@ private struct SpanExporterData: Encodable {
8383
self.attributes = span.attributes
8484
}
8585
}
86+
87+
extension SpanExporterData: Encodable {
88+
enum CodingKeys: String, CodingKey {
89+
case span
90+
case traceId
91+
case spanId
92+
case spanKind
93+
case traceFlags
94+
case traceState
95+
case parentSpanId
96+
case start
97+
case duration
98+
case attributes
99+
}
100+
101+
enum TraceFlagsCodingKeys: String, CodingKey {
102+
case sampled
103+
}
104+
105+
enum TraceStateCodingKeys: String, CodingKey {
106+
case entries
107+
}
108+
109+
enum TraceStateEntryCodingKeys: String, CodingKey {
110+
case key
111+
case value
112+
}
113+
114+
struct AttributesCodingKeys: CodingKey {
115+
var stringValue: String
116+
var intValue: Int?
117+
118+
init?(intValue: Int) {
119+
self.stringValue = "\(intValue)"
120+
self.intValue = intValue
121+
}
122+
123+
init?(stringValue: String) {
124+
self.stringValue = stringValue
125+
}
126+
}
127+
128+
enum AttributeValueCodingKeys: String, CodingKey {
129+
case description
130+
}
131+
132+
func encode(to encoder: Encoder) throws {
133+
var container = encoder.container(keyedBy: CodingKeys.self)
134+
135+
try container.encode(span, forKey: .span)
136+
try container.encode(traceId, forKey: .traceId)
137+
try container.encode(spanId, forKey: .spanId)
138+
try container.encode(spanKind, forKey: .spanKind)
139+
140+
var traceFlagsContainer = container.nestedContainer(keyedBy: TraceFlagsCodingKeys.self, forKey: .traceFlags)
141+
try traceFlagsContainer.encode(traceFlags.sampled, forKey: .sampled)
142+
143+
var traceStateContainer = container.nestedContainer(keyedBy: TraceStateCodingKeys.self, forKey: .traceState)
144+
var traceStateEntriesContainer = traceStateContainer.nestedUnkeyedContainer(forKey: .entries)
145+
146+
try traceState.entries.forEach { entry in
147+
var traceStateEntryContainer = traceStateEntriesContainer.nestedContainer(keyedBy: TraceStateEntryCodingKeys.self)
148+
149+
try traceStateEntryContainer.encode(entry.key, forKey: .key)
150+
try traceStateEntryContainer.encode(entry.value, forKey: .value)
151+
}
152+
153+
try container.encodeIfPresent(parentSpanId, forKey: .parentSpanId)
154+
try container.encode(start, forKey: .start)
155+
try container.encode(duration, forKey: .duration)
156+
157+
var attributesContainer = container.nestedContainer(keyedBy: AttributesCodingKeys.self, forKey: .attributes)
158+
159+
try attributes.forEach { attribute in
160+
161+
if let attributeValueCodingKey = AttributesCodingKeys(stringValue: attribute.key) {
162+
var attributeValueContainer = attributesContainer.nestedContainer(keyedBy: AttributeValueCodingKeys.self, forKey: attributeValueCodingKey)
163+
164+
try attributeValueContainer.encode(attribute.value.description, forKey: .description)
165+
} else {
166+
// this should never happen
167+
let encodingContext = EncodingError.Context(codingPath: attributesContainer.codingPath,
168+
debugDescription: "Failed to create coding key")
169+
170+
throw EncodingError.invalidValue(attribute, encodingContext)
171+
}
172+
}
173+
}
174+
}

Sources/OpenTelemetryApi/Common/AttributeValue.swift

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,117 @@ public enum AttributeValue: Equatable, CustomStringConvertible, Hashable {
6161
}
6262
}
6363

64-
extension AttributeValue: Encodable {
64+
internal struct AttributeValueExplicitCodable : Codable {
65+
let attributeValue: AttributeValue
66+
6567
enum CodingKeys: String, CodingKey {
66-
case description
68+
case string
69+
case bool
70+
case int
71+
case double
72+
case stringArray
73+
case boolArray
74+
case intArray
75+
case doubleArray
76+
}
77+
78+
enum AssociatedValueCodingKeys: String, CodingKey {
79+
case associatedValue = "_0"
80+
}
81+
82+
internal init(attributeValue: AttributeValue) {
83+
self.attributeValue = attributeValue
84+
}
85+
86+
public init(from decoder: Decoder) throws {
87+
let container = try decoder.container(keyedBy: CodingKeys.self)
88+
89+
guard container.allKeys.count == 1 else {
90+
let context = DecodingError.Context(
91+
codingPath: container.codingPath,
92+
debugDescription: "Invalid number of keys found, expected one.")
93+
throw DecodingError.typeMismatch(Status.self, context)
94+
}
95+
96+
switch container.allKeys.first.unsafelyUnwrapped {
97+
case .string:
98+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .string)
99+
self.attributeValue = .string(try nestedContainer.decode(String.self, forKey: .associatedValue))
100+
case .bool:
101+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .bool)
102+
self.attributeValue = .bool(try nestedContainer.decode(Bool.self, forKey: .associatedValue))
103+
case .int:
104+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .int)
105+
self.attributeValue = .int(try nestedContainer.decode(Int.self, forKey: .associatedValue))
106+
case .double:
107+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .double)
108+
self.attributeValue = .double(try nestedContainer.decode(Double.self, forKey: .associatedValue))
109+
case .stringArray:
110+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .stringArray)
111+
self.attributeValue = .stringArray(try nestedContainer.decode([String].self, forKey: .associatedValue))
112+
case .boolArray:
113+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .boolArray)
114+
self.attributeValue = .boolArray(try nestedContainer.decode([Bool].self, forKey: .associatedValue))
115+
case .intArray:
116+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .intArray)
117+
self.attributeValue = .intArray(try nestedContainer.decode([Int].self, forKey: .associatedValue))
118+
case .doubleArray:
119+
let nestedContainer = try container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .doubleArray)
120+
self.attributeValue = .doubleArray(try nestedContainer.decode([Double].self, forKey: .associatedValue))
121+
}
67122
}
68123

69124
public func encode(to encoder: Encoder) throws {
125+
70126
var container = encoder.container(keyedBy: CodingKeys.self)
71-
try container.encode(description, forKey: .description)
127+
128+
switch self.attributeValue {
129+
case .string(let value):
130+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .string)
131+
try nestedContainer.encode(value, forKey: .associatedValue)
132+
case .bool(let value):
133+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .bool)
134+
try nestedContainer.encode(value, forKey: .associatedValue)
135+
case .int(let value):
136+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .int)
137+
try nestedContainer.encode(value, forKey: .associatedValue)
138+
case .double(let value):
139+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .double)
140+
try nestedContainer.encode(value, forKey: .associatedValue)
141+
case .stringArray(let value):
142+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .stringArray)
143+
try nestedContainer.encode(value, forKey: .associatedValue)
144+
case .boolArray(let value):
145+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .boolArray)
146+
try nestedContainer.encode(value, forKey: .associatedValue)
147+
case .intArray(let value):
148+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .intArray)
149+
try nestedContainer.encode(value, forKey: .associatedValue)
150+
case .doubleArray(let value):
151+
var nestedContainer = container.nestedContainer(keyedBy: AssociatedValueCodingKeys.self, forKey: .doubleArray)
152+
try nestedContainer.encode(value, forKey: .associatedValue)
153+
}
154+
}
155+
}
156+
157+
#if swift(>=5.5)
158+
// swift 5.5 supports synthesizing Codable for enums with associated values
159+
// see https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
160+
extension AttributeValue: Codable { }
161+
#else
162+
// for older swift versions use a forward compatible explicit Codable implementation
163+
extension AttributeValue: Codable {
164+
165+
public init(from decoder: Decoder) throws {
166+
let explicitDecoded = try AttributeValueExplicitCodable(from: decoder)
167+
168+
self = explicitDecoded.attributeValue
169+
}
170+
171+
public func encode(to encoder: Encoder) throws {
172+
let explicitEncoded = AttributeValueExplicitCodable(attributeValue: self)
173+
174+
try explicitEncoded.encode(to: encoder)
72175
}
73176
}
177+
#endif

Sources/OpenTelemetryApi/Trace/SpanContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Foundation
88
/// A class that represents a span context. A span context contains the state that must propagate to
99
/// child Spans and across process boundaries. It contains the identifiers race_id and span_id
1010
/// associated with the Span and a set of options.
11-
public struct SpanContext: Equatable, CustomStringConvertible, Hashable {
11+
public struct SpanContext: Equatable, CustomStringConvertible, Hashable, Codable {
1212
/// The trace identifier associated with this SpanContext
1313
public private(set) var traceId: TraceId
1414

Sources/OpenTelemetryApi/Trace/SpanId.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Foundation
77

88
/// A struct that represents a span identifier. A valid span identifier is an 8-byte array with at
99
/// least one non-zero byte.
10-
public struct SpanId: Equatable, Comparable, Hashable, CustomStringConvertible {
10+
public struct SpanId: Equatable, Comparable, Hashable, CustomStringConvertible, Codable {
1111
public static let size = 8
1212
public static let invalidId: UInt64 = 0
1313
public static let invalid = SpanId(id: invalidId)

Sources/OpenTelemetryApi/Trace/SpanKind.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Foundation
77

88
/// Type of span. Can be used to specify additional relationships between spans in addition to a
99
/// parent/child relationship
10-
public enum SpanKind: String, Equatable {
10+
public enum SpanKind: String, Equatable, Codable {
1111
/// Default value. Indicates that the span is used internally.
1212
case `internal`
1313
/// ndicates that the span covers server-side handling of an RPC or other remote request.

Sources/OpenTelemetryApi/Trace/Status.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,85 @@ extension Status: CustomStringConvertible {
6060
}
6161
}
6262
}
63+
64+
internal struct StatusExplicitCodable : Codable {
65+
let status: Status
66+
67+
enum CodingKeys: String, CodingKey {
68+
case ok
69+
case unset
70+
case error
71+
}
72+
73+
enum EmptyCodingKeys: CodingKey {
74+
75+
}
76+
77+
enum ErrorCodingKeys: String, CodingKey {
78+
case description
79+
}
80+
81+
internal init(status: Status) {
82+
self.status = status
83+
}
84+
85+
public init(from decoder: Decoder) throws {
86+
let container = try decoder.container(keyedBy: CodingKeys.self)
87+
88+
guard container.allKeys.count == 1 else {
89+
let context = DecodingError.Context(
90+
codingPath: container.codingPath,
91+
debugDescription: "Invalid number of keys found, expected one.")
92+
throw DecodingError.typeMismatch(Status.self, context)
93+
}
94+
95+
switch container.allKeys.first.unsafelyUnwrapped {
96+
case .ok:
97+
_ = try container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .ok)
98+
self.status = .ok
99+
case .unset:
100+
_ = try container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .unset)
101+
self.status = .unset
102+
case .error:
103+
let nestedContainer = try container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .error)
104+
self.status = .error(description: try nestedContainer.decode(String.self, forKey: .description))
105+
}
106+
}
107+
108+
public func encode(to encoder: Encoder) throws {
109+
110+
var container = encoder.container(keyedBy: CodingKeys.self)
111+
112+
switch self.status {
113+
case .ok:
114+
_ = container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .ok)
115+
case .unset:
116+
_ = container.nestedContainer(keyedBy: EmptyCodingKeys.self, forKey: .unset)
117+
case .error(let description):
118+
var nestedContainer = container.nestedContainer(keyedBy: ErrorCodingKeys.self, forKey: .error)
119+
try nestedContainer.encode(description, forKey: .description)
120+
}
121+
}
122+
}
123+
124+
#if swift(>=5.5)
125+
// swift 5.5 supports synthesizing Codable for enums with associated values
126+
// see https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md
127+
extension Status: Codable { }
128+
#else
129+
// for older swift versions use a forward compatible explicit Codable implementation
130+
extension Status: Codable {
131+
132+
public init(from decoder: Decoder) throws {
133+
let explicitDecoded = try StatusExplicitCodable(from: decoder)
134+
135+
self = explicitDecoded.status
136+
}
137+
138+
public func encode(to encoder: Encoder) throws {
139+
let explicitEncoded = StatusExplicitCodable(status: self)
140+
141+
try explicitEncoded.encode(to: encoder)
142+
}
143+
}
144+
#endif

Sources/OpenTelemetryApi/Trace/TraceFlags.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Foundation
88
/// A struct that represents global trace options. These options are propagated to all child spans.
99
/// These determine features such as whether a Span should be traced. It is
1010
/// implemented as a bitmask.
11-
public struct TraceFlags: Equatable, CustomStringConvertible {
11+
public struct TraceFlags: Equatable, CustomStringConvertible, Codable {
1212
/// Default options. Nothing set.
1313
private static let defaultOptions: UInt8 = 0
1414
/// Bit to represent whether trace is sampled or not.
@@ -88,14 +88,3 @@ public struct TraceFlags: Equatable, CustomStringConvertible {
8888
"TraceFlags{sampled=\(sampled)}"
8989
}
9090
}
91-
92-
extension TraceFlags: Encodable {
93-
enum CodingKeys: String, CodingKey {
94-
case sampled
95-
}
96-
97-
public func encode(to encoder: Encoder) throws {
98-
var container = encoder.container(keyedBy: CodingKeys.self)
99-
try container.encode(sampled, forKey: .sampled)
100-
}
101-
}

Sources/OpenTelemetryApi/Trace/TraceId.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Foundation
77

88
/// A struct that represents a trace identifier. A valid trace identifier is a 16-byte array with at
99
/// least one non-zero byte.
10-
public struct TraceId: Comparable, Hashable, CustomStringConvertible, Equatable {
10+
public struct TraceId: Comparable, Hashable, CustomStringConvertible, Equatable, Codable {
1111
public static let size = 16
1212
public static let invalidId: UInt64 = 0
1313
public static let invalid = TraceId()

0 commit comments

Comments
 (0)