Skip to content

Commit 003d90c

Browse files
ypopovychclaude
andauthored
[SDTEST-3773] Add TelemetryLogExporter bridging OTel logs to TelemetryExporter (#267)
Implements LogRecordExporter that adapts ReadableLogRecord to TelemetryLog and forwards batches to TelemetryExporter for storage and upload. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4c0c7fd commit 003d90c

3 files changed

Lines changed: 114 additions & 35 deletions

File tree

DatadogSDKTesting.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
A77C12392D8DD2A9009C2BA1 /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C12382D8DD2A9009C2BA1 /* Feature.swift */; };
7878
A77C123C2D9425C7009C2BA1 /* KnownTestsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C123B2D9425C7009C2BA1 /* KnownTestsTests.swift */; };
7979
A77C123E2D9474E6009C2BA1 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C123D2D9474E6009C2BA1 /* Models.swift */; };
80+
A7926AEF2FCDCBFE007834F2 /* TelemetryLogExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */; };
8081
A794A8FF2E7990D20044FE3C /* TestManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A794A8FE2E7990C40044FE3C /* TestManagementTests.swift */; };
8182
A79DA5B42F928F87004BCA8C /* SwiftTestingSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79DA5B32F928F87004BCA8C /* SwiftTestingSmokeTests.swift */; };
8283
A79DA5B72F929812004BCA8C /* DatadogSDKTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7FC25642BA1E83500067E26 /* DatadogSDKTesting.framework */; };
@@ -489,6 +490,7 @@
489490
A77C12382D8DD2A9009C2BA1 /* Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = "<group>"; };
490491
A77C123B2D9425C7009C2BA1 /* KnownTestsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownTestsTests.swift; sourceTree = "<group>"; };
491492
A77C123D2D9474E6009C2BA1 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
493+
A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryLogExporter.swift; sourceTree = "<group>"; };
492494
A794A8FE2E7990C40044FE3C /* TestManagementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestManagementTests.swift; sourceTree = "<group>"; };
493495
A79DA5B22F928F71004BCA8C /* IntegrationTests-UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "IntegrationTests-UnitTests.xctestplan"; sourceTree = "<group>"; };
494496
A79DA5B32F928F87004BCA8C /* SwiftTestingSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingSmokeTests.swift; sourceTree = "<group>"; };
@@ -983,6 +985,7 @@
983985
isa = PBXGroup;
984986
children = (
985987
BC161A86B91745F4BDC88FF1 /* TelemetryExporter.swift */,
988+
A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */,
986989
);
987990
path = Telemetry;
988991
sourceTree = "<group>";
@@ -2138,6 +2141,7 @@
21382141
A7FC261B2BA1EC5A00067E26 /* DataUploadWorker.swift in Sources */,
21392142
A7FC26372BA1ECCF00067E26 /* JSONGeneric.swift in Sources */,
21402143
A79DA6442FB62618004BCA8C /* HTTPClient.swift in Sources */,
2144+
A7926AEF2FCDCBFE007834F2 /* TelemetryLogExporter.swift in Sources */,
21412145
A7AB7C2026E7EB1100000001 /* OpenTelemetry+Extensions.swift in Sources */,
21422146
A7AB7C2026E7EB1100000003 /* SpanEventsLog.swift in Sources */,
21432147
A7AB7C2026E7EB110000000D /* TestSpan.swift in Sources */,

Sources/EventsExporter/Logs/LogEncoder.swift

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -132,38 +132,47 @@ internal struct DDLog: Encodable {
132132

133133
/// Encodes `Log` to given encoder.
134134
internal struct LogEncoder {
135-
/// Coding keys for permanent `Log` attributes.
136-
enum StaticCodingKeys: String, CodingKey {
137-
case date
138-
case status
139-
case message
140-
case serviceName = "service"
141-
case tags = "ddtags"
142-
case hostname
143-
case product = "datadog.product"
135+
/// Coding keys for `Log` attributes.
136+
struct CodingKeys: CodingKey, ExpressibleByStringLiteral {
137+
typealias StringLiteralType = String
138+
let stringValue: String
139+
140+
init?(stringValue: String) {
141+
self.stringValue = stringValue
142+
}
143+
144+
init(stringLiteral value: String) {
145+
stringValue = value
146+
}
147+
148+
init(_ string: String) {
149+
self.init(stringLiteral: string)
150+
}
151+
152+
var intValue: Int? { nil }
153+
init?(intValue: Int) { nil }
154+
155+
static var date: Self { "date" }
156+
static var status: Self { "status" }
157+
static var message: Self { "message" }
158+
static var serviceName: Self { "service" }
159+
static var tags: Self { "ddtags" }
160+
static var hostname: Self { "hostname" }
161+
static var product: Self { "datadog.product" }
144162

145163
// MARK: - Application info
146164

147-
case applicationVersion = "version"
165+
static var applicationVersion: Self { "version" }
148166

149167
// MARK: - Logger info
150168

151-
case loggerName = "logger.name"
152-
case loggerVersion = "logger.version"
153-
case threadName = "logger.thread_name"
154-
}
155-
156-
/// Coding keys for dynamic `Log` attributes specified by user.
157-
private struct DynamicCodingKey: CodingKey {
158-
var stringValue: String
159-
var intValue: Int?
160-
init?(stringValue: String) { self.stringValue = stringValue }
161-
init?(intValue: Int) { return nil }
162-
init(_ string: String) { self.stringValue = string }
169+
static var loggerName: Self { "logger.name" }
170+
static var loggerVersion: Self { "logger.version" }
171+
static var threadName: Self { "logger.thread_name" }
163172
}
164173

165174
func encode(_ log: DDLog, to encoder: Encoder) throws {
166-
var container = encoder.container(keyedBy: StaticCodingKeys.self)
175+
var container = encoder.container(keyedBy: CodingKeys.self)
167176
try container.encode(log.date, forKey: .date)
168177
try container.encode(log.status, forKey: .status)
169178
try container.encode(log.message, forKey: .message)
@@ -177,22 +186,19 @@ internal struct LogEncoder {
177186

178187
// Encode application info
179188
try container.encode(log.applicationVersion, forKey: .applicationVersion)
180-
189+
181190
// Encode attributes...
182-
var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self)
183-
191+
184192
// ... first, user attributes ...
185-
let encodableUserAttributes = Dictionary(
186-
uniqueKeysWithValues: log.attributes.userAttributes.map { name, value in (name, EncodableValue(value)) }
187-
)
188-
try encodableUserAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) }
189-
193+
for (key, value) in log.attributes.userAttributes {
194+
try container.encode(value, forKey: .init(key))
195+
}
196+
190197
// ... then, internal attributes:
191198
if let internalAttributes = log.attributes.internalAttributes {
192-
let encodableInternalAttributes = Dictionary(
193-
uniqueKeysWithValues: internalAttributes.map { name, value in (name, EncodableValue(value)) }
194-
)
195-
try encodableInternalAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) }
199+
for (key, value) in internalAttributes {
200+
try container.encode(value, forKey: .init(key))
201+
}
196202
}
197203

198204
// Encode tags
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2020-Present Datadog, Inc.
5+
*/
6+
7+
import Foundation
8+
import OpenTelemetryApi
9+
import OpenTelemetrySdk
10+
11+
internal final class TelemetryLogExporter: LogRecordExporter {
12+
let telemetryExporter: TelemetryExporter
13+
14+
init(telemetryExporter: TelemetryExporter) {
15+
self.telemetryExporter = telemetryExporter
16+
}
17+
18+
func export(logRecords: [ReadableLogRecord], explicitTimeout: TimeInterval?) -> ExportResult {
19+
guard !logRecords.isEmpty else { return .success }
20+
telemetryExporter.export(item: TelemetryLog.Logs(logRecords.map(TelemetryLog.init(_:))))
21+
return .success
22+
}
23+
24+
func forceFlush(explicitTimeout: TimeInterval?) -> ExportResult {
25+
telemetryExporter.flush() ? .success : .failure
26+
}
27+
28+
func shutdown(explicitTimeout: TimeInterval?) {
29+
telemetryExporter.shutdown()
30+
}
31+
}
32+
33+
private extension TelemetryLog {
34+
init(_ record: ReadableLogRecord) {
35+
var attributes = record.attributes
36+
let message = record.body?.description
37+
?? attributes.removeValue(forKey: "message")?.description
38+
?? record.eventName
39+
?? "Log event"
40+
let stackTrace = attributes.removeValue(forKey: "exception.stacktrace")?.description
41+
let tracerTime = Int64((record.observedTimestamp ?? record.timestamp).timeIntervalSince1970)
42+
let tags = attributes.isEmpty ? nil : attributes
43+
.map { "\($0.key):\($0.value.description)" }
44+
.sorted()
45+
.joined(separator: ",")
46+
47+
self.init(
48+
message: message,
49+
level: TelemetryLog.Level(severity: record.severity),
50+
tags: tags,
51+
stackTrace: stackTrace,
52+
tracerTime: tracerTime
53+
)
54+
}
55+
}
56+
57+
private extension TelemetryLog.Level {
58+
init(severity: Severity?) {
59+
switch severity {
60+
case .error, .error2, .error3, .error4,
61+
.fatal, .fatal2, .fatal3, .fatal4:
62+
self = .error
63+
case .warn, .warn2, .warn3, .warn4:
64+
self = .warn
65+
default:
66+
self = .debug
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)