From 3f5710e2b77ba5889879b682af60dba84a664dd8 Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 13:49:46 +0200 Subject: [PATCH 1/6] [WIP] Redacted + Patterns --- .../LoggerStore+Configuration.swift | 2 + Sources/Pulse/LoggerStore/LoggerStore.swift | 19 ++- .../Pulse/NetworkLogger/NetworkLogger.swift | 122 +----------------- .../Redacted+Patterns.swift} | 54 +++++++- Sources/Pulse/Redacted/Redacted.swift | 85 ++++++++++++ 5 files changed, 160 insertions(+), 122 deletions(-) rename Sources/Pulse/{NetworkLogger/NetworkLogger+Redacting.swift => Redacted/Redacted+Patterns.swift} (80%) create mode 100644 Sources/Pulse/Redacted/Redacted.swift diff --git a/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift b/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift index a0ea2c8b3..c5ea4e04b 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift @@ -90,6 +90,8 @@ extension LoggerStore { /// ``LoggerStore/Options-swift.struct/sweep`` option. The default store supports sweeps. public var maxAge: TimeInterval = 14 * 86400 + public var redacted: Redacted = Redacted() + /// Gets called when the store receives an event. You can use it to /// modify the event before it is stored in order, for example, filter /// out some sensitive information. If you return `nil`, the event diff --git a/Sources/Pulse/LoggerStore/LoggerStore.swift b/Sources/Pulse/LoggerStore/LoggerStore.swift index 84c286235..e21f73475 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore.swift @@ -57,6 +57,8 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { private var requestsCache: [NetworkLogger.Request: NetworkRequestEntity] = [:] private var responsesCache: [NetworkLogger.Response: NetworkResponseEntity] = [:] + private let patterns: Redacted.Patterns + /// For testing purposes. var makeCurrentDate: () -> Date = { Date() } @@ -132,6 +134,7 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { self.options = options self.configuration = configuration + self.patterns = configuration.redacted.patterns() if options.contains(.inMemory) { // Do nothing @@ -240,7 +243,11 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { self.backgroundContext = container.newBackgroundContext() self.manifest = .init(storeId: UUID(), version: .currentStoreVersion) self.options = [] - self.configuration = .init() + + let configuration = Configuration() + + self.configuration = configuration + self.patterns = configuration.redacted.patterns() } private static func makeContainer(databaseURL: URL, options: Options) -> NSPersistentContainer { @@ -314,7 +321,7 @@ extension LoggerStore { label: String? = nil, taskDescription: String? = nil ) { - handle(.networkTaskCompleted(.init( + let event: Event = .networkTaskCompleted(.init( taskId: UUID(), taskType: .dataTask, createdAt: makeCurrentDate(), @@ -327,7 +334,13 @@ extension LoggerStore { metrics: metrics.map(NetworkLogger.Metrics.init), label: label, taskDescription: taskDescription - ))) + )) + + guard !patterns.isFilteringNeeded || patterns.filter(event) else { + return + } + + handle(patterns.preprocess(event)) } /// Handles event created by the current store and dispatches it to observers. diff --git a/Sources/Pulse/NetworkLogger/NetworkLogger.swift b/Sources/Pulse/NetworkLogger/NetworkLogger.swift index 91c423cf4..2ddb86eab 100644 --- a/Sources/Pulse/NetworkLogger/NetworkLogger.swift +++ b/Sources/Pulse/NetworkLogger/NetworkLogger.swift @@ -13,16 +13,8 @@ public final class NetworkLogger: @unchecked Sendable { private var store: LoggerStore { _store ?? .shared } private let _store: LoggerStore? - private var includedHosts: [Regex] = [] - private var includedURLs: [Regex] = [] - private var excludedHosts: [Regex] = [] - private var excludedURLs: [Regex] = [] + private let patterns: Redacted.Patterns - private var sensitiveHeaders: [Regex] = [] - private var sensitiveQueryItems: Set = [] - private var sensitiveDataFields: Set = [] - - private var isFilteringNeeded = false private let lock = NSLock() /// A shared network logger. @@ -53,49 +45,7 @@ public final class NetworkLogger: @unchecked Sendable { /// If the request itself fails, the task completes immediately. public var isWaitingForDecoding = false - /// Store logs only for the included hosts. - /// - /// - note: Supports wildcards, e.g. `*.example.com`, and full regex - /// when ``isRegexEnabled`` option is enabled. - public var includedHosts: Set = [] - - /// Store logs only for the included URLs. - /// - /// - note: Supports wildcards, e.g. `*.example.com`, and full regex - /// when ``isRegexEnabled`` option is enabled. - public var includedURLs: Set = [] - - /// Exclude the given hosts from the logs. - /// - /// - note: Supports wildcards, e.g. `*.example.com`, and full regex - /// when ``isRegexEnabled`` option is enabled. - public var excludedHosts: Set = [] - - /// Exclude the given URLs from the logs. - /// - /// - note: Supports wildcards, e.g. `*.example.com`, and full regex - /// when ``isRegexEnabled`` option is enabled. - public var excludedURLs: Set = [] - - /// Redact the given HTTP headers from the logged requests and responses. - /// - /// - note: Supports wildcards, e.g. `X-*`, and full regex - /// when ``isRegexEnabled`` option is enabled. - public var sensitiveHeaders: Set = [] - - /// Redact the given query items from the URLs. - /// - /// - note: Supports only plain strings. Case-sensitive. - public var sensitiveQueryItems: Set = [] - - /// Redact the given JSON fields from the logged requests and responses bodies. - /// - /// - note: Supports only plain strings. Case-sensitive. - public var sensitiveDataFields: Set = [] - - /// If enabled, processes `include` and `exclude` patterns using regex. - /// By default, patterns support only basic wildcard syntax: `*.example.com`. - public var isRegexEnabled = false + public var redacted = Redacted() /// Gets called when the logger receives an event. You can use it to /// modify the event before it is stored in order, for example, filter @@ -115,7 +65,7 @@ public final class NetworkLogger: @unchecked Sendable { public init(store: LoggerStore? = nil, configuration: Configuration = .init()) { self._store = store self.configuration = configuration - self.processPatterns() + self.patterns = configuration.redacted.patterns() } /// Initializes and configures the network logger. @@ -125,36 +75,6 @@ public final class NetworkLogger: @unchecked Sendable { self.init(store: store, configuration: configuration) } - // MARK: Patterns - - private func processPatterns() { - func process(_ pattern: String) -> Regex? { - process(pattern, options: []) - } - - func process(_ pattern: String, options: [Regex.Options]) -> Regex? { - do { - let pattern = configuration.isRegexEnabled ? pattern : expandingWildcards(pattern) - return try Regex(pattern) - } catch { - debugPrint("Failed to parse pattern: \(pattern) \(error)") - return nil - } - } - - self.includedHosts = configuration.includedHosts.compactMap(process) - self.includedURLs = configuration.includedURLs.compactMap(process) - self.excludedHosts = configuration.excludedHosts.compactMap(process) - self.excludedURLs = configuration.excludedURLs.compactMap(process) - self.sensitiveHeaders = configuration.sensitiveHeaders.compactMap { - process($0, options: [.caseInsensitive]) - } - self.sensitiveQueryItems = configuration.sensitiveQueryItems - self.sensitiveDataFields = configuration.sensitiveDataFields - - self.isFilteringNeeded = !includedHosts.isEmpty || !excludedHosts.isEmpty || !includedURLs.isEmpty || !excludedURLs.isEmpty - } - // MARK: Logging /// Logs the task creation (optional). @@ -255,47 +175,15 @@ public final class NetworkLogger: @unchecked Sendable { } private func send(_ event: LoggerStore.Event) { - guard !isFilteringNeeded || filter(event) else { + guard !patterns.isFilteringNeeded || patterns.filter(event) else { return } - guard let event = configuration.willHandleEvent(preprocess(event)) else { + guard let event = configuration.willHandleEvent(patterns.preprocess(event)) else { return } store.handle(event) } - /// Check if the events can be stored (included and not excluded). - private func filter(_ event: LoggerStore.Event) -> Bool { - guard let url = event.url else { - return false // Should never happen - } - var host = url.host ?? "" - if url.scheme == nil, let url = URL(string: "https://" + url.absoluteString) { - host = url.host ?? "" // URL(string: "example.com")?.host with not scheme returns host: "" - } - let absoluteString = url.absoluteString - if !includedHosts.isEmpty || !includedURLs.isEmpty { - guard includedHosts.contains(where: { $0.isMatch(host) }) || - includedURLs.contains(where: { $0.isMatch(absoluteString) }) else { - return false - } - } - if !excludedHosts.isEmpty && excludedHosts.contains(where: { $0.isMatch(host) }) { - return false - } - if !excludedURLs.isEmpty && excludedURLs.contains(where: { $0.isMatch(absoluteString) }) { - return false - } - return true - } - - private func preprocess(_ event: LoggerStore.Event) -> LoggerStore.Event { - event - .redactingSensitiveHeaders(sensitiveHeaders) - .redactingSensitiveQueryItems(sensitiveQueryItems) - .redactingSensitiveResponseDataFields(sensitiveDataFields) - } - // MARK: - Private private var tasks: [TaskKey: TaskContext] = [:] diff --git a/Sources/Pulse/NetworkLogger/NetworkLogger+Redacting.swift b/Sources/Pulse/Redacted/Redacted+Patterns.swift similarity index 80% rename from Sources/Pulse/NetworkLogger/NetworkLogger+Redacting.swift rename to Sources/Pulse/Redacted/Redacted+Patterns.swift index 562aa253a..96d26cea0 100644 --- a/Sources/Pulse/NetworkLogger/NetworkLogger+Redacting.swift +++ b/Sources/Pulse/Redacted/Redacted+Patterns.swift @@ -1,9 +1,59 @@ -// The MIT License (MIT) // -// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). +// File.swift +// Pulse +// +// Created by Taras Nikulin on 02/04/2025. +// import Foundation +extension Redacted { + struct Patterns { + var includedHosts: [Regex] = [] + var includedURLs: [Regex] = [] + var excludedHosts: [Regex] = [] + var excludedURLs: [Regex] = [] + + var sensitiveHeaders: [Regex] = [] + var sensitiveQueryItems: Set = [] + var sensitiveDataFields: Set = [] + + var isFilteringNeeded = false + + /// Check if the events can be stored (included and not excluded). + func filter(_ event: LoggerStore.Event) -> Bool { + guard let url = event.url else { + return false // Should never happen + } + var host = url.host ?? "" + if url.scheme == nil, let url = URL(string: "https://" + url.absoluteString) { + host = url.host ?? "" // URL(string: "example.com")?.host with not scheme returns host: "" + } + let absoluteString = url.absoluteString + if !includedHosts.isEmpty || !includedURLs.isEmpty { + guard includedHosts.contains(where: { $0.isMatch(host) }) || + includedURLs.contains(where: { $0.isMatch(absoluteString) }) else { + return false + } + } + if !excludedHosts.isEmpty && excludedHosts.contains(where: { $0.isMatch(host) }) { + return false + } + if !excludedURLs.isEmpty && excludedURLs.contains(where: { $0.isMatch(absoluteString) }) { + return false + } + return true + } + + func preprocess(_ event: LoggerStore.Event) -> LoggerStore.Event { + event + .redactingSensitiveHeaders(sensitiveHeaders) + .redactingSensitiveQueryItems(sensitiveQueryItems) + .redactingSensitiveResponseDataFields(sensitiveDataFields) + } + } +} + // MARK: - Redacting Sensitive Headers extension LoggerStore.Event { diff --git a/Sources/Pulse/Redacted/Redacted.swift b/Sources/Pulse/Redacted/Redacted.swift new file mode 100644 index 000000000..3f933495a --- /dev/null +++ b/Sources/Pulse/Redacted/Redacted.swift @@ -0,0 +1,85 @@ +import Foundation + +public struct Redacted: Sendable { + /// Store logs only for the included hosts. + /// + /// - note: Supports wildcards, e.g. `*.example.com`, and full regex + /// when ``isRegexEnabled`` option is enabled. + public var includedHosts: Set = [] + + /// Store logs only for the included URLs. + /// + /// - note: Supports wildcards, e.g. `*.example.com`, and full regex + /// when ``isRegexEnabled`` option is enabled. + public var includedURLs: Set = [] + + /// Exclude the given hosts from the logs. + /// + /// - note: Supports wildcards, e.g. `*.example.com`, and full regex + /// when ``isRegexEnabled`` option is enabled. + public var excludedHosts: Set = [] + + /// Exclude the given URLs from the logs. + /// + /// - note: Supports wildcards, e.g. `*.example.com`, and full regex + /// when ``isRegexEnabled`` option is enabled. + public var excludedURLs: Set = [] + + /// Redact the given HTTP headers from the logged requests and responses. + /// + /// - note: Supports wildcards, e.g. `X-*`, and full regex + /// when ``isRegexEnabled`` option is enabled. + public var sensitiveHeaders: Set = [] + + /// Redact the given query items from the URLs. + /// + /// - note: Supports only plain strings. Case-sensitive. + public var sensitiveQueryItems: Set = [] + + /// Redact the given JSON fields from the logged requests and responses bodies. + /// + /// - note: Supports only plain strings. Case-sensitive. + public var sensitiveDataFields: Set = [] + + /// If enabled, processes `include` and `exclude` patterns using regex. + /// By default, patterns support only basic wildcard syntax: `*.example.com`. + public var isRegexEnabled = false + + public init() {} + + func patterns() -> Patterns { + func process(_ pattern: String) -> Regex? { + process(pattern, options: []) + } + + func process(_ pattern: String, options: [Regex.Options]) -> Regex? { + do { + let pattern = isRegexEnabled ? pattern : expandingWildcards(pattern) + return try Regex(pattern) + } catch { + debugPrint("Failed to parse pattern: \(pattern) \(error)") + return nil + } + } + + func expandingWildcards(_ pattern: String) -> String { + let pattern = NSRegularExpression.escapedPattern(for: pattern) + .replacingOccurrences(of: "\\?", with: ".") + .replacingOccurrences(of: "\\*", with: "[^\\s]*") + return "^\(pattern)$" + } + + return Patterns( + includedHosts: includedHosts.compactMap(process), + includedURLs: includedURLs.compactMap(process), + excludedHosts: excludedHosts.compactMap(process), + excludedURLs: excludedURLs.compactMap(process), + sensitiveHeaders: sensitiveHeaders.compactMap { + process($0, options: [.caseInsensitive]) + }, + sensitiveQueryItems: sensitiveQueryItems, + sensitiveDataFields: sensitiveDataFields, + isFilteringNeeded: !includedHosts.isEmpty || !excludedHosts.isEmpty || !includedURLs.isEmpty || !excludedURLs.isEmpty + ) + } +} From 5b5651b30c4cba2d2cd6a415a430fbd4727bc601 Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 14:33:30 +0200 Subject: [PATCH 2/6] [WIP] RequestsLogger --- Sources/Pulse/LoggerStore/LoggerStore.swift | 44 +-------- .../Pulse/RequestsLogger/RequestsLogger.swift | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+), 43 deletions(-) create mode 100644 Sources/Pulse/RequestsLogger/RequestsLogger.swift diff --git a/Sources/Pulse/LoggerStore/LoggerStore.swift b/Sources/Pulse/LoggerStore/LoggerStore.swift index e21f73475..3d5b889d4 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore.swift @@ -57,8 +57,6 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { private var requestsCache: [NetworkLogger.Request: NetworkRequestEntity] = [:] private var responsesCache: [NetworkLogger.Response: NetworkResponseEntity] = [:] - private let patterns: Redacted.Patterns - /// For testing purposes. var makeCurrentDate: () -> Date = { Date() } @@ -134,7 +132,6 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { self.options = options self.configuration = configuration - self.patterns = configuration.redacted.patterns() if options.contains(.inMemory) { // Do nothing @@ -243,11 +240,7 @@ public final class LoggerStore: @unchecked Sendable, Identifiable { self.backgroundContext = container.newBackgroundContext() self.manifest = .init(storeId: UUID(), version: .currentStoreVersion) self.options = [] - - let configuration = Configuration() - - self.configuration = configuration - self.patterns = configuration.redacted.patterns() + self.configuration = .init() } private static func makeContainer(databaseURL: URL, options: Options) -> NSPersistentContainer { @@ -308,41 +301,6 @@ extension LoggerStore { ))) } - /// Stores the network request. - /// - /// - note: If you want to store incremental updates to the task, use - /// `NetworkLogger` instead. - public func storeRequest( - _ request: URLRequest, - response: URLResponse?, - error: Swift.Error?, - data: Data?, - metrics: URLSessionTaskMetrics? = nil, - label: String? = nil, - taskDescription: String? = nil - ) { - let event: Event = .networkTaskCompleted(.init( - taskId: UUID(), - taskType: .dataTask, - createdAt: makeCurrentDate(), - originalRequest: NetworkLogger.Request(request), - currentRequest: NetworkLogger.Request(request), - response: response.map(NetworkLogger.Response.init), - error: error.map(NetworkLogger.ResponseError.init), - requestBody: request.httpBody ?? request.httpBodyStreamData(), - responseBody: data, - metrics: metrics.map(NetworkLogger.Metrics.init), - label: label, - taskDescription: taskDescription - )) - - guard !patterns.isFilteringNeeded || patterns.filter(event) else { - return - } - - handle(patterns.preprocess(event)) - } - /// Handles event created by the current store and dispatches it to observers. func handle(_ event: Event) { guard let event = configuration.willHandleEvent(event) else { diff --git a/Sources/Pulse/RequestsLogger/RequestsLogger.swift b/Sources/Pulse/RequestsLogger/RequestsLogger.swift new file mode 100644 index 000000000..c9b97176e --- /dev/null +++ b/Sources/Pulse/RequestsLogger/RequestsLogger.swift @@ -0,0 +1,97 @@ +import Foundation + +public final class RequestsLogger: @unchecked Sendable { + public struct Configuration { + public var redacted: Redacted = Redacted() + + /// Gets called when the logger receives an event. You can use it to + /// modify the event before it is stored in order, for example, filter + /// out some sensitive information. If you return `nil`, the event + /// is ignored completely. + public var willHandleEvent: @Sendable (LoggerStore.Event) -> LoggerStore.Event? = { $0 } + + /// Initializes the default configuration. + public init() {} + } + + private var store: LoggerStore { _store ?? .shared } + private let _store: LoggerStore? + + private let configuration: Configuration + private let patterns: Redacted.Patterns + + /// A shared requests logger. + /// + /// You can configure a logger by creating a new instance and setting it as + /// a shared logger: + /// + /// ```swift + /// RequestsLogger.shared = RequestsLogger { + /// $0.excludedHosts = ["github.com"] + /// } + /// ``` + /// + /// The best place to do it is at the app launch. + public static var shared: RequestsLogger { + get { _shared.value } + set { _shared.value = newValue } + } + private static let _shared = Mutex(RequestsLogger()) + + /// Initializes the requests logger. + /// + /// - parameters: + /// - store: The target store for network requests. + /// - configuration: The store configuration. + public init(store: LoggerStore? = nil, configuration: Configuration = .init()) { + self._store = store + self.configuration = configuration + self.patterns = configuration.redacted.patterns() + } + + /// Initializes and configures the requests logger. + public convenience init(store: LoggerStore? = nil, _ configure: (inout Configuration) -> Void) { + var configuration = Configuration() + configure(&configuration) + self.init(store: store, configuration: configuration) + } + + /// Stores the network request. + /// + /// - note: If you want to store incremental updates to the task, use + /// `NetworkLogger` instead. + public func storeRequest( + _ request: URLRequest, + response: URLResponse?, + error: Swift.Error?, + data: Data?, + metrics: URLSessionTaskMetrics? = nil, + label: String? = nil, + taskDescription: String? = nil + ) { + send(.networkTaskCompleted(.init( + taskId: UUID(), + taskType: .dataTask, + createdAt: Date(), + originalRequest: NetworkLogger.Request(request), + currentRequest: NetworkLogger.Request(request), + response: response.map(NetworkLogger.Response.init), + error: error.map(NetworkLogger.ResponseError.init), + requestBody: request.httpBody ?? request.httpBodyStreamData(), + responseBody: data, + metrics: metrics.map(NetworkLogger.Metrics.init), + label: label, + taskDescription: taskDescription + ))) + } + + private func send(_ event: LoggerStore.Event) { + guard !patterns.isFilteringNeeded || patterns.filter(event) else { + return + } + guard let event = configuration.willHandleEvent(patterns.preprocess(event)) else { + return + } + store.handle(event) + } +} From e49a64e377d3a11357109283440e9a88c5cf5ee5 Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 14:35:32 +0200 Subject: [PATCH 3/6] [WIP] RequestsLogger --- Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift b/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift index c5ea4e04b..a0ea2c8b3 100644 --- a/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift +++ b/Sources/Pulse/LoggerStore/LoggerStore+Configuration.swift @@ -90,8 +90,6 @@ extension LoggerStore { /// ``LoggerStore/Options-swift.struct/sweep`` option. The default store supports sweeps. public var maxAge: TimeInterval = 14 * 86400 - public var redacted: Redacted = Redacted() - /// Gets called when the store receives an event. You can use it to /// modify the event before it is stored in order, for example, filter /// out some sensitive information. If you return `nil`, the event From a398f260d7ebd7512595331231750df96d5051cc Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 14:36:41 +0200 Subject: [PATCH 4/6] [WIP] File Headers --- Sources/Pulse/Redacted/Redacted+Patterns.swift | 7 ++----- Sources/Pulse/Redacted/Redacted.swift | 4 ++++ Sources/Pulse/RequestsLogger/RequestsLogger.swift | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/Pulse/Redacted/Redacted+Patterns.swift b/Sources/Pulse/Redacted/Redacted+Patterns.swift index 96d26cea0..909843fb7 100644 --- a/Sources/Pulse/Redacted/Redacted+Patterns.swift +++ b/Sources/Pulse/Redacted/Redacted+Patterns.swift @@ -1,9 +1,6 @@ +// The MIT License (MIT) // -// File.swift -// Pulse -// -// Created by Taras Nikulin on 02/04/2025. -// +// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). import Foundation diff --git a/Sources/Pulse/Redacted/Redacted.swift b/Sources/Pulse/Redacted/Redacted.swift index 3f933495a..59e27a3d9 100644 --- a/Sources/Pulse/Redacted/Redacted.swift +++ b/Sources/Pulse/Redacted/Redacted.swift @@ -1,3 +1,7 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). + import Foundation public struct Redacted: Sendable { diff --git a/Sources/Pulse/RequestsLogger/RequestsLogger.swift b/Sources/Pulse/RequestsLogger/RequestsLogger.swift index c9b97176e..f36ed571e 100644 --- a/Sources/Pulse/RequestsLogger/RequestsLogger.swift +++ b/Sources/Pulse/RequestsLogger/RequestsLogger.swift @@ -1,3 +1,7 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). + import Foundation public final class RequestsLogger: @unchecked Sendable { From 5e250ce2f574c06238ce3dd3698c20b01812db17 Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 14:39:30 +0200 Subject: [PATCH 5/6] [WIP] Patterns + Init --- .../Pulse/Redacted/Redacted+Patterns.swift | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/Sources/Pulse/Redacted/Redacted+Patterns.swift b/Sources/Pulse/Redacted/Redacted+Patterns.swift index 909843fb7..f6ab39b14 100644 --- a/Sources/Pulse/Redacted/Redacted+Patterns.swift +++ b/Sources/Pulse/Redacted/Redacted+Patterns.swift @@ -6,16 +6,36 @@ import Foundation extension Redacted { struct Patterns { - var includedHosts: [Regex] = [] - var includedURLs: [Regex] = [] - var excludedHosts: [Regex] = [] - var excludedURLs: [Regex] = [] + let includedHosts: [Regex] + let includedURLs: [Regex] + let excludedHosts: [Regex] + let excludedURLs: [Regex] - var sensitiveHeaders: [Regex] = [] - var sensitiveQueryItems: Set = [] - var sensitiveDataFields: Set = [] + let sensitiveHeaders: [Regex] + let sensitiveQueryItems: Set + let sensitiveDataFields: Set - var isFilteringNeeded = false + let isFilteringNeeded: Bool + + init( + includedHosts: [Regex] = [], + includedURLs: [Regex] = [], + excludedHosts: [Regex] = [], + excludedURLs: [Regex] = [], + sensitiveHeaders: [Regex] = [], + sensitiveQueryItems: Set = [], + sensitiveDataFields: Set = [], + isFilteringNeeded: Bool = false + ) { + self.includedHosts = includedHosts + self.includedURLs = includedURLs + self.excludedHosts = excludedHosts + self.excludedURLs = excludedURLs + self.sensitiveHeaders = sensitiveHeaders + self.sensitiveQueryItems = sensitiveQueryItems + self.sensitiveDataFields = sensitiveDataFields + self.isFilteringNeeded = isFilteringNeeded + } /// Check if the events can be stored (included and not excluded). func filter(_ event: LoggerStore.Event) -> Bool { From cc075154435a711496b3a54f8b5bd25b11fd9a7e Mon Sep 17 00:00:00 2001 From: Taras Nikulin Date: Wed, 2 Apr 2025 14:54:33 +0200 Subject: [PATCH 6/6] [WIP] Docs --- .../Articles/NetworkLogging-Article.md | 34 ++++++++++++------- .../Extensions/LoggerStore-Extension.md | 1 - Sources/Pulse/Pulse.docc/Pulse.md | 1 + 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Sources/Pulse/Pulse.docc/Articles/NetworkLogging-Article.md b/Sources/Pulse/Pulse.docc/Articles/NetworkLogging-Article.md index 811d86630..0fec31531 100644 --- a/Sources/Pulse/Pulse.docc/Articles/NetworkLogging-Article.md +++ b/Sources/Pulse/Pulse.docc/Articles/NetworkLogging-Article.md @@ -84,10 +84,10 @@ struct NetworkLoggerEventMonitor: EventMonitor { } ``` -Alternatively, if you don't have access to `URLSessionTask`, you can store the request/response directly in ``LoggerStore``: +Alternatively, if you don't have access to `URLSessionTask`, you can store the request/response directly using ``RequestsLogger``: ```swift -LoggerStore.shared.storeRequest(urlRequest, response: urlResponse, ...) +RequestsLogger.shared.storeRequest(urlRequest, response: urlResponse, ...) ``` ## Configure Logging @@ -113,36 +113,44 @@ logger.logTask(task, didFinishDecodingWithError: decodingError) ``NetworkLogger`` captures data safely in a local database, and it never leaves your device. Logs are never written to the system's logging system. But, of course, logs are meant to be viewed and shared, which is why PulseUI provides sharing options. In case the logs do leave your device, it's best to redact any sensitive information.  -``NetworkLogger/Configuration`` has a set of convenience APIs for managing what information is included or excluded from the logs. +It's possible to manage that information using ``Redacted`` component. The same type is used in ``NetworkLogger.Configuration`` and ``RequestsLogger.Configuration``. So you can either use different ones or the same one in both. ```swift -var configuration = NetworkLogger.Configuration() +var redacted = Redacted() // Includes only requests with the given domain. -configuration.includedHosts = ["*.example.com"] +redacted.includedHosts = ["*.example.com"] // Exclude some subdomains. -configuration.excludedHosts = ["logging.example.com"] +redacted.excludedHosts = ["logging.example.com"] // Exclude specific URLs. -configuration.excludedURLs = ["*/log/event"] +redacted.excludedURLs = ["*/log/event"] // Replaces values for the given HTTP headers with "" -configuration.sensitiveHeaders = ["Authorization", "Access-Token"] +redacted.sensitiveHeaders = ["Authorization", "Access-Token"] // Redacts sensitive query items. -configuration.sensitiveQueryItems = ["password"] +redacted.sensitiveQueryItems = ["password"] // Replaces values for the given response and request JSON fields with "" -configuration.sensitiveDataFields = ["password"] - -let logger = NetworkLogger(configuration: configuration) +redacted.sensitiveDataFields = ["password"] ``` You can then replace the default decoder with your custom instance: ```swift -NetworkLogger.shared = logger +NetworkLogger.shared = NetworkLogger { + $0.redacted = redacted +} +``` + +And do the same with ``RequestsLogger`` + +```swift +RequestsLogger.shared = RequestsLogger { + $0.redacted = redacted +} ``` > Tip: "Include" and "exclude" patterns support basic wildcards (`*`), but you can also turn them into full-featured regex patterns using ``NetworkLogger/Configuration/isRegexEnabled``.  diff --git a/Sources/Pulse/Pulse.docc/Extensions/LoggerStore-Extension.md b/Sources/Pulse/Pulse.docc/Extensions/LoggerStore-Extension.md index 517ffc4de..ca2d6b8c0 100644 --- a/Sources/Pulse/Pulse.docc/Extensions/LoggerStore-Extension.md +++ b/Sources/Pulse/Pulse.docc/Extensions/LoggerStore-Extension.md @@ -12,7 +12,6 @@ ### Storing Logs - ``storeMessage(createdAt:label:level:message:metadata:file:function:line:)`` -- ``storeRequest(_:response:error:data:metrics:label:taskDescription:)`` ### Accessing Logs diff --git a/Sources/Pulse/Pulse.docc/Pulse.md b/Sources/Pulse/Pulse.docc/Pulse.md index 776342dac..0ef0e972e 100644 --- a/Sources/Pulse/Pulse.docc/Pulse.md +++ b/Sources/Pulse/Pulse.docc/Pulse.md @@ -13,6 +13,7 @@ Logger and network inspector for Apple platforms. ### Network Logging & Debugging - +- ``RequestsLogger`` - ``NetworkLogger`` - ``URLSessionProxy`` - ``URLSessionProtocol``