commit 0feaf1efb7b4c3ceccda83fdb6e3c3635d8bc32e
parent 3353c52c82cff9c50c6a44fdfc01f3131d107130
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 14:19:06 -0700
telemetry: route app logging through AppleKit
- add a FieldTelemetry runtime facade backed by RadrootsKit telemetry
- configure UI-test recording telemetry and logging-filter level mapping
- keep Rust runtime logging initialization in LoggingSettings
- remove direct app OSLog and generated Rust helper logging calls
Diffstat:
7 files changed, 151 insertions(+), 118 deletions(-)
diff --git a/Radroots.xcodeproj/project.pbxproj b/Radroots.xcodeproj/project.pbxproj
@@ -23,7 +23,6 @@
3A7FA9E5BCC7590B2EAC5349 /* RelaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBB081610305940C7849C7C /* RelaySettings.swift */; };
3B6020E24A2DAD8ADFC2F155 /* BuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71EFADBC7D54AF5B9314773 /* BuildConfig.swift */; };
3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */; };
- 4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F9BFC3C8CE2F86FB7DB74B /* DebugDump.swift */; };
4B44B723FF06ECC363A486BA /* TradeListingDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */; };
505A5731ACDBBB0296134340 /* TradeListingCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947ADA6D32E42ED2B40A5351 /* TradeListingCreateView.swift */; };
5AECD474FB2F91855BDD79C0 /* PostFeedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */; };
@@ -37,11 +36,11 @@
8B923F78BF5B680C7F6A7CE3 /* PostFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AE0EB327C10171444553378 /* PostFeedView.swift */; };
8F6D0970610DF68816DE1A98 /* Radroots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F0F21496E7A8490EB14AC5B /* Radroots.swift */; };
9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C7DE4207398DE242519F9C /* CopyButton.swift */; };
+ 9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */; };
A1B921027DA7ACD7343BE250 /* SectionWideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17CA8F5611075F60F214A00 /* SectionWideButton.swift */; };
B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */; };
B971351ABE8E79A472B4DC7D /* View+Nav.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B0C9861CD86EAD3CAD549E /* View+Nav.swift */; };
C22DB0F3EB2E69A34DF941E0 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2818363B157125491FB84A1E /* App.swift */; };
- C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE790CA1CD31208947913B9 /* Logger.swift */; };
C8AB3389F7430A5C79AD7DF8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */; };
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */; };
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4289F43625DD65E6C4B25 /* CopyRow.swift */; };
@@ -70,7 +69,7 @@
26BAE32CD1D46033DDA1A5BB /* TradeListingDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeListingDetailView.swift; sourceTree = "<group>"; };
2818363B157125491FB84A1E /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldLocationCheckIn.swift; sourceTree = "<group>"; };
- 2FE790CA1CD31208947913B9 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
+ 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldTelemetry.swift; sourceTree = "<group>"; };
3E6187FA7C4786EC662718B2 /* FieldExternalActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldExternalActions.swift; sourceTree = "<group>"; };
41A4289F43625DD65E6C4B25 /* CopyRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRow.swift; sourceTree = "<group>"; };
466BFA2F60BE3113EDD1BA3B /* TradeOrderRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TradeOrderRequestView.swift; sourceTree = "<group>"; };
@@ -110,7 +109,6 @@
DC881872F750120A184F45E6 /* RadrootsKitBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadrootsKitBindings.swift; sourceTree = "<group>"; };
E1D12A016D1377CDFBFB0F9B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
E4F2BDC19EF9435162BFF5EC /* FieldDocumentInterchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchange.swift; sourceTree = "<group>"; };
- E7F9BFC3C8CE2F86FB7DB74B /* DebugDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugDump.swift; sourceTree = "<group>"; };
E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldRuntimeService.swift; sourceTree = "<group>"; };
EF05B944D348DAA4EF28B463 /* FieldDocumentInterchangeUITestProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldDocumentInterchangeUITestProbe.swift; sourceTree = "<group>"; };
F21554DA87EEC1E5C5F38365 /* PostFeedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedViewModel.swift; sourceTree = "<group>"; };
@@ -202,6 +200,7 @@
2F3541389124A31F5D701A45 /* FieldLocationCheckIn.swift */,
E883FDB7004A210C9D7BE27A /* FieldRuntimeService.swift */,
8246B707FA9D218414EC4038 /* FieldSecureIdentityStore.swift */,
+ 3B4E53FD4C4AADF63855888A /* FieldTelemetry.swift */,
D65835E1C633C7B946C64D11 /* FieldUserPresenceGate.swift */,
D4DE3DD8C3BB2F63676F463E /* LoggingSettings.swift */,
63189EB90A86A9929BECD9ED /* Nostr.swift */,
@@ -227,7 +226,6 @@
D46F444AD1818932F03AC6B6 /* Components */,
9D22575D1FAD99FE8B6FCE6C /* Extensions */,
65EC1C4AF7DC676E78603D52 /* Localisation */,
- F16A19713274742D956C3A4D /* Logging */,
C5BAA3C6E2D410F0C8475D89 /* Modifiers */,
);
path = Shared;
@@ -323,15 +321,6 @@
path = Resources;
sourceTree = "<group>";
};
- F16A19713274742D956C3A4D /* Logging */ = {
- isa = PBXGroup;
- children = (
- E7F9BFC3C8CE2F86FB7DB74B /* DebugDump.swift */,
- 2FE790CA1CD31208947913B9 /* Logger.swift */,
- );
- path = Logging;
- sourceTree = "<group>";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -436,7 +425,6 @@
E3864E34D67BAD0744B93180 /* Bundle+Build.swift in Sources */,
9121BD4A3E7C6EF2B21F540F /* CopyButton.swift in Sources */,
D3834AF9A4E1327B7DA557F3 /* CopyRow.swift in Sources */,
- 4025E63F6603011431B8A0E1 /* DebugDump.swift in Sources */,
299A6111507F657670856F36 /* FieldCaptureIntake.swift in Sources */,
3FC570AC038C3DC575E5A3E7 /* FieldDocumentInterchange.swift in Sources */,
E432FD39ECC8F03764EEED81 /* FieldDocumentInterchangeUITestProbe.swift in Sources */,
@@ -447,9 +435,9 @@
1C6EA551530A46CA77BD9E1C /* FieldLocationCheckIn.swift in Sources */,
D25C1E1DC99F5CF8E99AE970 /* FieldRuntimeService.swift in Sources */,
D9BF5BE7E4AB5EACBF342539 /* FieldSecureIdentityStore.swift in Sources */,
+ 9346DA48630668A65D37E14A /* FieldTelemetry.swift in Sources */,
25654E50F9519809A237759D /* FieldUserPresenceGate.swift in Sources */,
1E5B41A3E1F9A7D68F63B079 /* HomeView.swift in Sources */,
- C512BD267A3E2B8F10FABB3B /* Logger.swift in Sources */,
B8A3BBDE3A1FC0248512BF76 /* LoggingSettings.swift in Sources */,
33A800AA701C354099623B24 /* MarketView.swift in Sources */,
657BEA5AAFF129E10177FE63 /* Nostr.swift in Sources */,
diff --git a/Radroots/App/AppState.swift b/Radroots/App/AppState.swift
@@ -71,6 +71,7 @@ public final class AppState: ObservableObject {
}
public let radroots: Radroots
+ private let telemetry: FieldTelemetry
public var runtimeService: FieldRuntimeService? {
radroots.runtimeService
@@ -85,8 +86,9 @@ public final class AppState: ObservableObject {
private let externalActions = FieldExternalActions.configured()
private let userPresenceGate = FieldUserPresenceGate.configured()
- public init(radroots: Radroots = Radroots()) {
+ init(radroots: Radroots = Radroots(), telemetry: FieldTelemetry = .shared) {
self.radroots = radroots
+ self.telemetry = telemetry
self.isLocked = UserDefaults.standard.bool(forKey: lockKey)
}
@@ -102,7 +104,7 @@ public final class AppState: ObservableObject {
if startupFailureWasRequested {
throw FieldAppRuntimeError.forcedStartupFailure
}
- let service = try radroots.start()
+ let service = try radroots.start(telemetry: telemetry)
let secureStore = try FieldSecureIdentityStore.configured()
let metadataStore = try FieldIdentityPublicMetadataStore.configured()
let appBundleIdentifier = try bundleIdentifier()
diff --git a/Radroots/Runtime/FieldTelemetry.swift b/Radroots/Runtime/FieldTelemetry.swift
@@ -0,0 +1,137 @@
+import Foundation
+import RadrootsKit
+import RadrootsKitTesting
+
+final class FieldTelemetry: @unchecked Sendable {
+ static let shared = FieldTelemetry.configured()
+
+ private let sink: any RadrootsTelemetry
+ private let minimumLevel: RadrootsTelemetryLevel
+ private let recordingTelemetry: RadrootsRecordingTelemetry?
+
+ init(
+ sink: any RadrootsTelemetry,
+ minimumLevel: RadrootsTelemetryLevel = .info,
+ recordingTelemetry: RadrootsRecordingTelemetry? = nil
+ ) {
+ self.sink = sink
+ self.minimumLevel = minimumLevel
+ self.recordingTelemetry = recordingTelemetry
+ }
+
+ static func configured(
+ bundleIdentifier: String = Bundle.main.bundleIdentifier ?? "dev.local.radroots",
+ loggingSettings: LoggingSettings = .load()
+ ) -> FieldTelemetry {
+ let minimumLevel = telemetryMinimumLevel(from: loggingSettings.level)
+ let appleTelemetry = RadrootsAppleLoggerTelemetry(subsystem: bundleIdentifier)
+ if uiTestWasRequested {
+ let recorder = RadrootsRecordingTelemetry()
+ return FieldTelemetry(
+ sink: RadrootsMultiplexTelemetry([appleTelemetry, recorder]),
+ minimumLevel: minimumLevel,
+ recordingTelemetry: recorder
+ )
+ }
+ return FieldTelemetry(sink: appleTelemetry, minimumLevel: minimumLevel)
+ }
+
+ func record(
+ name: String,
+ category: String = "field_ios",
+ level: RadrootsTelemetryLevel = .info,
+ message: String? = nil,
+ fields: [RadrootsTelemetryField] = []
+ ) {
+ Task {
+ await recordAsync(
+ name: name,
+ category: category,
+ level: level,
+ message: message,
+ fields: fields
+ )
+ }
+ }
+
+ func recordAsync(
+ name: String,
+ category: String = "field_ios",
+ level: RadrootsTelemetryLevel = .info,
+ message: String? = nil,
+ fields: [RadrootsTelemetryField] = []
+ ) async {
+ guard level >= minimumLevel else {
+ return
+ }
+ guard let event = try? RadrootsTelemetryEvent(
+ name: name,
+ category: category,
+ level: level,
+ message: message,
+ fields: fields
+ ) else {
+ return
+ }
+ await sink.record(event)
+ }
+
+ func runtimeLoggingInitialized(
+ settings: LoggingSettings,
+ fallbackUsed: Bool
+ ) {
+ record(
+ name: "field_ios.runtime.logging_initialized",
+ level: fallbackUsed ? .warning : .info,
+ fields: [
+ try? .bool("stdout_enabled", settings.stdout),
+ try? .bool("file_enabled", settings.fileEnabled),
+ try? .string("logging_filter", settings.level ?? "unset"),
+ try? .bool("fallback_used", fallbackUsed)
+ ].compactMap { $0 }
+ )
+ }
+
+ func recordedEventsForUITest() async -> [RadrootsTelemetryEvent] {
+ guard let recordingTelemetry else {
+ return []
+ }
+ return await recordingTelemetry.recordedEvents
+ }
+
+ private static var uiTestWasRequested: Bool {
+ let environment = ProcessInfo.processInfo.environment
+ let arguments = ProcessInfo.processInfo.arguments
+ return environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" ||
+ arguments.contains("--radroots-field-ios-ui-test")
+ }
+
+ private static func telemetryMinimumLevel(from filter: String?) -> RadrootsTelemetryLevel {
+ guard let filter else {
+ return .info
+ }
+ let lowered = filter.lowercased()
+ if lowered.contains("trace") {
+ return .trace
+ }
+ if lowered.contains("debug") {
+ return .debug
+ }
+ if lowered.contains("info") {
+ return .info
+ }
+ if lowered.contains("notice") {
+ return .notice
+ }
+ if lowered.contains("warn") {
+ return .warning
+ }
+ if lowered.contains("error") {
+ return .error
+ }
+ if lowered.contains("critical") || lowered.contains("fault") {
+ return .critical
+ }
+ return .info
+ }
+}
diff --git a/Radroots/Runtime/LoggingSettings.swift b/Radroots/Runtime/LoggingSettings.swift
@@ -29,23 +29,4 @@ struct LoggingSettings: Equatable {
try initLogging(dir: nil, fileName: fileName, isStdout: stdout)
}
}
-
- func logEffectiveConfigs() {
- let keys: [BuildConfigKey] = [
- .envFile,
- .runtimeMode,
- .loggingStdout,
- .loggingFilter,
- .loggingFileEnabled,
- .loggingFileName,
- .nostrRelayUrls,
- .keychainServicePrefix,
- .resetLocalState,
- .tradeRhiPubkey,
- ]
- let dict = BuildConfig.effectiveDictionary(keys: keys)
- let json = (try? JSONSerialization.data(withJSONObject: dict, options: [.sortedKeys])) ?? Data()
- let text = String(data: json, encoding: .utf8) ?? String(describing: dict)
- try? logInfo(msg: "radroots.config \(text)")
- }
}
diff --git a/Radroots/Runtime/Radroots.swift b/Radroots/Runtime/Radroots.swift
@@ -7,19 +7,22 @@ public final class Radroots: ObservableObject {
public init() {}
- public func start(
+ func start(
bundleId: String = Bundle.main.bundleIdentifier ?? "unknown",
version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "0",
build: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "0",
- buildSha: String? = nil
+ buildSha: String? = nil,
+ telemetry: FieldTelemetry = .shared
) throws -> FieldRuntimeService {
let settings = LoggingSettings.load()
+ var loggingFallbackUsed = false
do {
try settings.apply(bundleIdentifier: bundleId)
} catch {
try? initLoggingStdout()
+ loggingFallbackUsed = true
}
- settings.logEffectiveConfigs()
+ telemetry.runtimeLoggingInitialized(settings: settings, fallbackUsed: loggingFallbackUsed)
let rt = try RadrootsRuntime()
let resolvedSha = buildSha ?? (Bundle.main.object(forInfoDictionaryKey: "GIT_SHA") as? String)
diff --git a/Radroots/Shared/Logging/DebugDump.swift b/Radroots/Shared/Logging/DebugDump.swift
@@ -1,43 +0,0 @@
-import Foundation
-
-enum DebugDump {
- static func posts(_ items: [NostrPostEventMetadata], label: String = "PostFeed.kind1") {
- let mapped = items.map {
- DumpPost(id: $0.id, author: $0.author, publishedAt: $0.publishedAt, content: $0.post.content)
- }
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- let jsonString = (try? encoder.encode(mapped)).flatMap { String(data: $0, encoding: .utf8) }
- let dumpString: String = jsonString ?? {
- var s = ""
- dump(mapped, to: &s, maxDepth: Int.max, maxItems: Int.max)
- return s
- }()
- RadrootsLogger.debug("\(label) count=\(items.count)")
- let chunks = dumpString.chunked(into: 900)
- for (i, chunk) in chunks.enumerated() {
- RadrootsLogger.debug("\(label) \(i + 1)/\(chunks.count):\n\(chunk)")
- }
- }
-
- private struct DumpPost: Codable {
- let id: String
- let author: String
- let publishedAt: UInt64
- let content: String
- }
-}
-
-private extension String {
- func chunked(into size: Int) -> [String] {
- guard size > 0, !isEmpty else { return [self] }
- var result: [String] = []
- var idx = startIndex
- while idx < endIndex {
- let end = index(idx, offsetBy: size, limitedBy: endIndex) ?? endIndex
- result.append(String(self[idx..<end]))
- idx = end
- }
- return result
- }
-}
diff --git a/Radroots/Shared/Logging/Logger.swift b/Radroots/Shared/Logging/Logger.swift
@@ -1,35 +0,0 @@
-import Foundation
-import os
-
-private let oslog = os.Logger(subsystem: Bundle.main.bundleIdentifier ?? "Radroots", category: "App")
-
-enum RadrootsLogger {
- static func info(_ message: String) {
- oslog.info("\(message, privacy: .public)")
- do {
- try logInfo(msg: message)
- } catch {
- oslog.error("logInfo failed: \(error.localizedDescription, privacy: .public)")
- }
- }
-
- static func error(_ message: String) {
- oslog.error("\(message, privacy: .public)")
- do {
- try logError(msg: message)
- } catch {
- oslog.error("logError failed: \(error.localizedDescription, privacy: .public)")
- }
- }
-
- static func debug(_ message: String) {
- #if DEBUG
- oslog.debug("\(message, privacy: .public)")
- do {
- try logDebug(msg: message)
- } catch {
- oslog.error("logDebug failed: \(error.localizedDescription, privacy: .public)")
- }
- #endif
- }
-}