Skip to content

Commit f0bdf86

Browse files
author
Ignacio Bonafonte
authored
Merge pull request #412 from open-telemetry/Persistent-LogRecords
added persistent store decorator for log records
2 parents 7a41072 + 76a622a commit f0bdf86

File tree

4 files changed

+170
-101
lines changed

4 files changed

+170
-101
lines changed

Sources/Exporters/Persistence/PersistenceExporterDecorator.swift

Lines changed: 102 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -8,118 +8,121 @@ import OpenTelemetrySdk
88

99
// protocol for exporters that can be decorated with `PersistenceExporterDecorator`
1010
protocol DecoratedExporter {
11-
associatedtype SignalType
11+
associatedtype SignalType
1212

13-
func export(values: [SignalType]) -> DataExportStatus
13+
func export(values: [SignalType]) -> DataExportStatus
1414
}
1515

1616
// a generic decorator of `DecoratedExporter` adding filesystem persistence of batches of `[T.SignalType]`.
1717
// `T.SignalType` must conform to `Codable`.
1818
internal class PersistenceExporterDecorator<T> where T: DecoratedExporter, T.SignalType: Codable {
19-
// a wrapper of `DecoratedExporter` (T) to add conformance to `DataExporter` that can be
20-
// used with `DataExportWorker`.
21-
private class DecoratedDataExporter: DataExporter {
22-
private let decoratedExporter: T
23-
24-
init(decoratedExporter: T) {
25-
self.decoratedExporter = decoratedExporter
26-
}
27-
28-
func export(data: Data) -> DataExportStatus {
29-
// decode batches of `[T.SignalType]` from the raw data.
30-
// the data is made of batches of comma-suffixed JSON arrays, so in order to utilize
31-
// `JSONDecoder`, add a "[" prefix and "null]" suffix making the data a valid
32-
// JSON array of `[T.SignalType]`.
33-
var arrayData: Data = JSONDataConstants.arrayPrefix
34-
arrayData.append(data)
35-
arrayData.append(JSONDataConstants.arraySuffix)
36-
37-
do {
38-
let decoder = JSONDecoder()
39-
let exportables = try decoder.decode(
40-
[[T.SignalType]?].self,
41-
from: arrayData
42-
).compactMap { $0 }.flatMap { $0 }
43-
44-
return decoratedExporter.export(values: exportables)
45-
} catch {
46-
return DataExportStatus(needsRetry: false)
47-
}
48-
}
49-
}
50-
51-
private let performancePreset: PersistencePerformancePreset
52-
53-
private let fileWriter: FileWriter
54-
55-
private let worker: DataExportWorkerProtocol
56-
57-
public convenience init(decoratedExporter: T,
58-
storageURL: URL,
59-
exportCondition: @escaping () -> Bool = { true },
60-
performancePreset: PersistencePerformancePreset = .default)
61-
{
62-
// orchestrate writes and reads over the folder given by `storageURL`
63-
let filesOrchestrator = FilesOrchestrator(
64-
directory: Directory(url: storageURL),
65-
performance: performancePreset,
66-
dateProvider: SystemDateProvider()
67-
)
68-
69-
let fileWriter = OrchestratedFileWriter(
70-
orchestrator: filesOrchestrator
71-
)
19+
// a wrapper of `DecoratedExporter` (T) to add conformance to `DataExporter` that can be
20+
// used with `DataExportWorker`.
21+
private class DecoratedDataExporter: DataExporter {
22+
private let decoratedExporter: T
7223

73-
let fileReader = OrchestratedFileReader(
74-
orchestrator: filesOrchestrator
75-
)
76-
77-
self.init(decoratedExporter: decoratedExporter,
78-
fileWriter: fileWriter,
79-
workerFactory: {
80-
DataExportWorker(
81-
fileReader: fileReader,
82-
dataExporter: $0,
83-
exportCondition: exportCondition,
84-
delay: DataExportDelay(performance: performancePreset)
85-
)
86-
},
87-
performancePreset: performancePreset)
24+
init(decoratedExporter: T) {
25+
self.decoratedExporter = decoratedExporter
8826
}
8927

90-
// internal initializer for testing that accepts a worker factory that allows mocking the worker
91-
internal init(decoratedExporter: T,
92-
fileWriter: FileWriter,
93-
workerFactory createWorker: (DataExporter) -> DataExportWorkerProtocol,
94-
performancePreset: PersistencePerformancePreset)
95-
{
96-
self.performancePreset = performancePreset
97-
98-
self.fileWriter = fileWriter
99-
100-
self.worker = createWorker(DecoratedDataExporter(decoratedExporter: decoratedExporter))
28+
func export(data: Data) -> DataExportStatus {
29+
// decode batches of `[T.SignalType]` from the raw data.
30+
// the data is made of batches of comma-suffixed JSON arrays, so in order to utilize
31+
// `JSONDecoder`, add a "[" prefix and "null]" suffix making the data a valid
32+
// JSON array of `[T.SignalType]`.
33+
var arrayData: Data = JSONDataConstants.arrayPrefix
34+
arrayData.append(data)
35+
arrayData.append(JSONDataConstants.arraySuffix)
36+
37+
do {
38+
let decoder = JSONDecoder()
39+
let exportables = try decoder.decode(
40+
[[T.SignalType]?].self,
41+
from: arrayData
42+
).compactMap { $0 }.flatMap { $0 }
43+
44+
return decoratedExporter.export(values: exportables)
45+
} catch {
46+
return DataExportStatus(needsRetry: false)
47+
}
10148
}
102-
103-
public func export(values: [T.SignalType]) throws {
104-
let encoder = JSONEncoder()
105-
var data = try encoder.encode(values)
106-
data.append(JSONDataConstants.arraySeparator)
107-
108-
if performancePreset.synchronousWrite {
109-
fileWriter.writeSync(data: data)
110-
} else {
111-
fileWriter.write(data: data)
112-
}
49+
}
50+
51+
private let performancePreset: PersistencePerformancePreset
52+
53+
private let fileWriter: FileWriter
54+
55+
private let worker: DataExportWorkerProtocol
56+
57+
public convenience init(
58+
decoratedExporter: T,
59+
storageURL: URL,
60+
exportCondition: @escaping () -> Bool = { true },
61+
performancePreset: PersistencePerformancePreset = .default
62+
) {
63+
// orchestrate writes and reads over the folder given by `storageURL`
64+
let filesOrchestrator = FilesOrchestrator(
65+
directory: Directory(url: storageURL),
66+
performance: performancePreset,
67+
dateProvider: SystemDateProvider()
68+
)
69+
70+
let fileWriter = OrchestratedFileWriter(
71+
orchestrator: filesOrchestrator
72+
)
73+
74+
let fileReader = OrchestratedFileReader(
75+
orchestrator: filesOrchestrator
76+
)
77+
78+
self.init(
79+
decoratedExporter: decoratedExporter,
80+
fileWriter: fileWriter,
81+
workerFactory: {
82+
DataExportWorker(
83+
fileReader: fileReader,
84+
dataExporter: $0,
85+
exportCondition: exportCondition,
86+
delay: DataExportDelay(performance: performancePreset)
87+
)
88+
},
89+
performancePreset: performancePreset)
90+
}
91+
92+
// internal initializer for testing that accepts a worker factory that allows mocking the worker
93+
internal init(
94+
decoratedExporter: T,
95+
fileWriter: FileWriter,
96+
workerFactory createWorker: (DataExporter) -> DataExportWorkerProtocol,
97+
performancePreset: PersistencePerformancePreset
98+
) {
99+
self.performancePreset = performancePreset
100+
101+
self.fileWriter = fileWriter
102+
103+
self.worker = createWorker(DecoratedDataExporter(decoratedExporter: decoratedExporter))
104+
}
105+
106+
public func export(values: [T.SignalType]) throws {
107+
let encoder = JSONEncoder()
108+
var data = try encoder.encode(values)
109+
data.append(JSONDataConstants.arraySeparator)
110+
111+
if performancePreset.synchronousWrite {
112+
fileWriter.writeSync(data: data)
113+
} else {
114+
fileWriter.write(data: data)
113115
}
116+
}
114117

115-
public func flush() {
116-
fileWriter.flush()
117-
_ = worker.flush()
118-
}
118+
public func flush() {
119+
fileWriter.flush()
120+
_ = worker.flush()
121+
}
119122
}
120123

121124
private enum JSONDataConstants {
122-
static let arrayPrefix = "[".data(using: .utf8)!
123-
static let arraySuffix = "null]".data(using: .utf8)!
124-
static let arraySeparator = ",".data(using: .utf8)!
125+
static let arrayPrefix = "[".data(using: .utf8)!
126+
static let arraySuffix = "null]".data(using: .utf8)!
127+
static let arraySeparator = ",".data(using: .utf8)!
125128
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// Copyright The OpenTelemetry Authors
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
6+
import Foundation
7+
import OpenTelemetrySdk
8+
9+
// a persistence exporter decorator for `LogRecords`.
10+
// specialization of `PersistenceExporterDecorator` for `MetricExporter`.
11+
public class PersistenceLogExporterDecorator: LogRecordExporter {
12+
13+
struct LogRecordDecoratedExporter: DecoratedExporter {
14+
typealias SignalType = ReadableLogRecord
15+
16+
private let logRecordExporter: LogRecordExporter
17+
18+
init(logRecordExporter: LogRecordExporter) {
19+
self.logRecordExporter = logRecordExporter
20+
}
21+
22+
func export(values: [ReadableLogRecord]) -> DataExportStatus {
23+
let result = logRecordExporter.export(logRecords: values)
24+
return DataExportStatus(needsRetry: result == .failure)
25+
}
26+
}
27+
private let logRecordExporter: LogRecordExporter
28+
private let persistenceExporter: PersistenceExporterDecorator<LogRecordDecoratedExporter>
29+
30+
public init(
31+
logRecordExporter: LogRecordExporter,
32+
storageURL: URL,
33+
exportCondition: @escaping () -> Bool = { true },
34+
performancePreset: PersistencePerformancePreset = .default
35+
) throws {
36+
self.persistenceExporter =
37+
PersistenceExporterDecorator<LogRecordDecoratedExporter>(
38+
decoratedExporter: LogRecordDecoratedExporter(logRecordExporter: logRecordExporter),
39+
storageURL: storageURL,
40+
exportCondition: exportCondition,
41+
performancePreset: performancePreset)
42+
self.logRecordExporter = logRecordExporter
43+
}
44+
45+
public func export(logRecords: [ReadableLogRecord]) -> ExportResult {
46+
do {
47+
try persistenceExporter.export(values: logRecords)
48+
return .success
49+
} catch {
50+
return .failure
51+
}
52+
}
53+
54+
public func shutdown() {
55+
persistenceExporter.flush()
56+
logRecordExporter.shutdown()
57+
}
58+
59+
public func forceFlush() -> ExportResult {
60+
persistenceExporter.flush()
61+
return logRecordExporter.forceFlush()
62+
63+
}
64+
}

Sources/OpenTelemetryApi/Logs/Severity.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import Foundation
77

8-
public enum Severity : Int, Comparable, CustomStringConvertible {
8+
public enum Severity : Int, Comparable, CustomStringConvertible, Codable {
99

1010
case trace=1,
1111
trace2,

Sources/OpenTelemetrySdk/Logs/Data/ReadableLogRecord.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import Foundation
77
import OpenTelemetryApi
88

9-
public struct ReadableLogRecord {
9+
public struct ReadableLogRecord : Codable {
1010
public init(resource: Resource, instrumentationScopeInfo: InstrumentationScopeInfo, timestamp: Date, observedTimestamp: Date? = nil, spanContext: SpanContext? = nil, severity: Severity? = nil, body: String? = nil, attributes: [String : AttributeValue]) {
1111
self.resource = resource
1212
self.instrumentationScopeInfo = instrumentationScopeInfo
@@ -27,3 +27,5 @@ public struct ReadableLogRecord {
2727
public private(set) var body: String?
2828
public private(set) var attributes : [String: AttributeValue]
2929
}
30+
31+

0 commit comments

Comments
 (0)