Skip to content
Open
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
29 changes: 0 additions & 29 deletions Sources/Pulse/LoggerStore/LoggerStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,35 +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
) {
handle(.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
)))
}

/// Handles event created by the current store and dispatches it to observers.
func handle(_ event: Event) {
guard let event = configuration.willHandleEvent(event) else {
Expand Down
122 changes: 5 additions & 117 deletions Sources/Pulse/NetworkLogger/NetworkLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = []
private var sensitiveDataFields: Set<String> = []

private var isFilteringNeeded = false
private let lock = NSLock()

/// A shared network logger.
Expand Down Expand Up @@ -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<String> = []

/// 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<String> = []

/// 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<String> = []

/// 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<String> = []

/// 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<String> = []

/// Redact the given query items from the URLs.
///
/// - note: Supports only plain strings. Case-sensitive.
public var sensitiveQueryItems: Set<String> = []

/// Redact the given JSON fields from the logged requests and responses bodies.
///
/// - note: Supports only plain strings. Case-sensitive.
public var sensitiveDataFields: Set<String> = []

/// 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
Expand All @@ -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.
Expand All @@ -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).
Expand Down Expand Up @@ -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] = [:]
Expand Down
34 changes: 21 additions & 13 deletions Sources/Pulse/Pulse.docc/Articles/NetworkLogging-Article.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 "<private>"
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 "<private>"
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``. 
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
### Storing Logs

- ``storeMessage(createdAt:label:level:message:metadata:file:function:line:)``
- ``storeRequest(_:response:error:data:metrics:label:taskDescription:)``

### Accessing Logs

Expand Down
1 change: 1 addition & 0 deletions Sources/Pulse/Pulse.docc/Pulse.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Logger and network inspector for Apple platforms.
### Network Logging & Debugging

- <doc:NetworkLogging-Article>
- ``RequestsLogger``
- ``NetworkLogger``
- ``URLSessionProxy``
- ``URLSessionProtocol``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,73 @@

import Foundation

extension Redacted {
struct Patterns {
let includedHosts: [Regex]
let includedURLs: [Regex]
let excludedHosts: [Regex]
let excludedURLs: [Regex]

let sensitiveHeaders: [Regex]
let sensitiveQueryItems: Set<String>
let sensitiveDataFields: Set<String>

let isFilteringNeeded: Bool

init(
includedHosts: [Regex] = [],
includedURLs: [Regex] = [],
excludedHosts: [Regex] = [],
excludedURLs: [Regex] = [],
sensitiveHeaders: [Regex] = [],
sensitiveQueryItems: Set<String> = [],
sensitiveDataFields: Set<String> = [],
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 {
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 {
Expand Down
Loading
Loading