Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DatadogSDKTesting.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
A77C12392D8DD2A9009C2BA1 /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C12382D8DD2A9009C2BA1 /* Feature.swift */; };
A77C123C2D9425C7009C2BA1 /* KnownTestsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C123B2D9425C7009C2BA1 /* KnownTestsTests.swift */; };
A77C123E2D9474E6009C2BA1 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C123D2D9474E6009C2BA1 /* Models.swift */; };
A7926AEF2FCDCBFE007834F2 /* TelemetryLogExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */; };
A794A8FF2E7990D20044FE3C /* TestManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A794A8FE2E7990C40044FE3C /* TestManagementTests.swift */; };
A79DA5B42F928F87004BCA8C /* SwiftTestingSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79DA5B32F928F87004BCA8C /* SwiftTestingSmokeTests.swift */; };
A79DA5B72F929812004BCA8C /* DatadogSDKTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7FC25642BA1E83500067E26 /* DatadogSDKTesting.framework */; };
Expand Down Expand Up @@ -489,6 +490,7 @@
A77C12382D8DD2A9009C2BA1 /* Feature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feature.swift; sourceTree = "<group>"; };
A77C123B2D9425C7009C2BA1 /* KnownTestsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownTestsTests.swift; sourceTree = "<group>"; };
A77C123D2D9474E6009C2BA1 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryLogExporter.swift; sourceTree = "<group>"; };
A794A8FE2E7990C40044FE3C /* TestManagementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestManagementTests.swift; sourceTree = "<group>"; };
A79DA5B22F928F71004BCA8C /* IntegrationTests-UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "IntegrationTests-UnitTests.xctestplan"; sourceTree = "<group>"; };
A79DA5B32F928F87004BCA8C /* SwiftTestingSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingSmokeTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -983,6 +985,7 @@
isa = PBXGroup;
children = (
BC161A86B91745F4BDC88FF1 /* TelemetryExporter.swift */,
A7926AEE2FCDCBFE007834F2 /* TelemetryLogExporter.swift */,
);
path = Telemetry;
sourceTree = "<group>";
Expand Down Expand Up @@ -2138,6 +2141,7 @@
A7FC261B2BA1EC5A00067E26 /* DataUploadWorker.swift in Sources */,
A7FC26372BA1ECCF00067E26 /* JSONGeneric.swift in Sources */,
A79DA6442FB62618004BCA8C /* HTTPClient.swift in Sources */,
A7926AEF2FCDCBFE007834F2 /* TelemetryLogExporter.swift in Sources */,
A7AB7C2026E7EB1100000001 /* OpenTelemetry+Extensions.swift in Sources */,
A7AB7C2026E7EB1100000003 /* SpanEventsLog.swift in Sources */,
A7AB7C2026E7EB110000000D /* TestSpan.swift in Sources */,
Expand Down
76 changes: 41 additions & 35 deletions Sources/EventsExporter/Logs/LogEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,38 +132,47 @@ internal struct DDLog: Encodable {

/// Encodes `Log` to given encoder.
internal struct LogEncoder {
/// Coding keys for permanent `Log` attributes.
enum StaticCodingKeys: String, CodingKey {
case date
case status
case message
case serviceName = "service"
case tags = "ddtags"
case hostname
case product = "datadog.product"
/// Coding keys for `Log` attributes.
struct CodingKeys: CodingKey, ExpressibleByStringLiteral {
typealias StringLiteralType = String
let stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

init(stringLiteral value: String) {
stringValue = value
}

init(_ string: String) {
self.init(stringLiteral: string)
}

var intValue: Int? { nil }
init?(intValue: Int) { nil }

static var date: Self { "date" }
static var status: Self { "status" }
static var message: Self { "message" }
static var serviceName: Self { "service" }
static var tags: Self { "ddtags" }
static var hostname: Self { "hostname" }
static var product: Self { "datadog.product" }

// MARK: - Application info

case applicationVersion = "version"
static var applicationVersion: Self { "version" }

// MARK: - Logger info

case loggerName = "logger.name"
case loggerVersion = "logger.version"
case threadName = "logger.thread_name"
}

/// Coding keys for dynamic `Log` attributes specified by user.
private struct DynamicCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
init(_ string: String) { self.stringValue = string }
static var loggerName: Self { "logger.name" }
static var loggerVersion: Self { "logger.version" }
static var threadName: Self { "logger.thread_name" }
}

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

// Encode application info
try container.encode(log.applicationVersion, forKey: .applicationVersion)

// Encode attributes...
var attributesContainer = encoder.container(keyedBy: DynamicCodingKey.self)


// ... first, user attributes ...
let encodableUserAttributes = Dictionary(
uniqueKeysWithValues: log.attributes.userAttributes.map { name, value in (name, EncodableValue(value)) }
)
try encodableUserAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) }

for (key, value) in log.attributes.userAttributes {
try container.encode(value, forKey: .init(key))
}

// ... then, internal attributes:
if let internalAttributes = log.attributes.internalAttributes {
let encodableInternalAttributes = Dictionary(
uniqueKeysWithValues: internalAttributes.map { name, value in (name, EncodableValue(value)) }
)
try encodableInternalAttributes.forEach { try attributesContainer.encode($0.value, forKey: DynamicCodingKey($0.key)) }
for (key, value) in internalAttributes {
try container.encode(value, forKey: .init(key))
}
}

// Encode tags
Expand Down
69 changes: 69 additions & 0 deletions Sources/EventsExporter/Telemetry/TelemetryLogExporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2020-Present Datadog, Inc.
*/

import Foundation
import OpenTelemetryApi
import OpenTelemetrySdk

internal final class TelemetryLogExporter: LogRecordExporter {
let telemetryExporter: TelemetryExporter

init(telemetryExporter: TelemetryExporter) {
self.telemetryExporter = telemetryExporter
}

func export(logRecords: [ReadableLogRecord], explicitTimeout: TimeInterval?) -> ExportResult {
guard !logRecords.isEmpty else { return .success }
telemetryExporter.export(item: TelemetryLog.Logs(logRecords.map(TelemetryLog.init(_:))))
return .success
}

func forceFlush(explicitTimeout: TimeInterval?) -> ExportResult {
telemetryExporter.flush() ? .success : .failure
}

func shutdown(explicitTimeout: TimeInterval?) {
telemetryExporter.shutdown()
}
}

private extension TelemetryLog {
init(_ record: ReadableLogRecord) {
var attributes = record.attributes
let message = record.body?.description
?? attributes.removeValue(forKey: "message")?.description
?? record.eventName
?? "Log event"
let stackTrace = attributes.removeValue(forKey: "exception.stacktrace")?.description
let tracerTime = Int64((record.observedTimestamp ?? record.timestamp).timeIntervalSince1970)
let tags = attributes.isEmpty ? nil : attributes
.map { "\($0.key):\($0.value.description)" }
.sorted()
.joined(separator: ",")

self.init(
message: message,
level: TelemetryLog.Level(severity: record.severity),
tags: tags,
stackTrace: stackTrace,
tracerTime: tracerTime
)
}
}

private extension TelemetryLog.Level {
init(severity: Severity?) {
switch severity {
case .error, .error2, .error3, .error4,
.fatal, .fatal2, .fatal3, .fatal4:
self = .error
case .warn, .warn2, .warn3, .warn4:
self = .warn
default:
self = .debug
}
}
}
Loading