Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log improvements #61

Merged
merged 14 commits into from
Jul 6, 2024
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,27 +337,33 @@ The default value is always `en`. With other languages, we use values compliant

## Logging

For logging purposes `WPNLogger` that prints to the console is used.

<!-- begin box info -->
Note that logging to the console is available only when the library is compiled with the `DEBUG` or `WPN_ENABLE_LOGGING` Swift compile condition.
<!-- end -->
You can set up logging for the library using the `WPNLogger` class.

### Verbosity Level

You can limit the amount of logged information via `verboseLevel` property.
You can limit the amount of logged information via the `verboseLevel` property.

| Level | Description |
| --- | --- |
| `off` | Silences all messages. |
| `errors` | Only errors will be printed to the debug console. |
| `warnings` _(default)_ | Errors and warnings will be printed to the debug console. |
| `all` | All messages will be printed to the debug console. |
| Level | Description |
| ---------------------- | ------------------------------------------------- |
| `off` | Silences all messages. |
| `errors` | Only errors will be logged. |
| `warnings` _(default)_ | Errors and warnings will be logged. |
| `info` | Error, warning and info messages will be logged. |
| `all` | All messages will be logged. |

### Character limit

To prevent huge logs from being printed out, there is a default limit of 12,000 characters per log in place. You can change this via `WPNLogger.characterLimit`.

### HTTP traffic logs

- You can turn on or off logging of the requests and responses with the `WPNLogger.logHttpTraffic` property.
- You can filter which headers will be logged with the `WPNLogger.httpHeadersToSkip` property.

### Logger Delegate

In case you want to process logs on your own (for example log into a file or some cloud service), you can set `WPNLogger.delegate`.

<!-- begin remove -->
## Web Documentation

Expand Down
33 changes: 16 additions & 17 deletions Sources/WultraPowerauthNetworking/WPNHttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,27 @@ class WPNHttpClient: NSObject, URLSessionDelegate {

private extension URLRequest {
func printToConsole() {
D.print("WPNHttpClient Request")
D.print("- URL: POST - \(url?.absoluteString ?? "no URL")")
D.print("- Headers: \(allHTTPHeaderFields?.betterDescription ?? "no headers")")
D.print("- Body: \(httpBody?.utf8string ?? "empty body")")
if D.logHttpTraffic {
D.info("WPNHttpClient Request")
D.info("- URL: POST - \(url?.absoluteString ?? "no URL")")
D.info("- Headers: \(D.httpHeadersToSkip.filterHeaders(headers: allHTTPHeaderFields))")
D.debug("- Body: \(httpBody?.utf8string ?? "empty body")")
}
}
}

private extension HTTPURLResponse {
func printToConsole(withData data: Data?, andError error: Error?) {
D.print("WPNHttpClient Response")
D.print("- URL: POST - \(url?.absoluteString ?? "no URL")")
D.print("- Status code: \(statusCode)")
D.print("- Headers: \(allHeaderFields.betterDescription)")
D.print("- Body: \(data?.utf8string ?? "empty body")")
if let error = error {
D.print("- Error: \(error.localizedDescription)")
if D.logHttpTraffic {
D.info("WPNHttpClient Response")
D.info("- URL: POST - \(url?.absoluteString ?? "no URL")")
D.info("- Status code: \(statusCode)")
D.info("- Headers: \(D.httpHeadersToSkip.filterHeaders(headers: Dictionary(uniqueKeysWithValues: allHeaderFields.map { ($0.key.description, "\($0.value)") })))")
D.debug("- Body: \(data?.utf8string ?? "empty body")")

if let error = error {
D.error("- Error: \(error.localizedDescription)")
}
}
}
}
Expand All @@ -105,9 +110,3 @@ private extension Data {
return String(bytes: self, encoding: .utf8)
}
}

private extension Dictionary where Key: CustomStringConvertible, Value: Any {
var betterDescription: String {
return map({($0.key.description, $0.value)}).description
}
}
209 changes: 183 additions & 26 deletions Sources/WultraPowerauthNetworking/WPNLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,52 +16,133 @@

import Foundation

/// WPNLogger provides simple logging facility available for DEBUG build of the library.
/// Level of the log
public enum WPNLogLevel {
/// Debug logs. Might contain sensitive data like body of the request etc.
/// You should only use this level during development.
case debug
/// Regular library logic logs
case info
/// Non-critical warning
case warning
/// Error happened
case error

fileprivate var minVerboseLevel: WPNLogger.VerboseLevel {
return switch self {
case .debug: .debug
case .info: .info
case .warning: .warnings
case .error: .errors
}
}

fileprivate var logName: String {
return switch self {
case .debug: "DEBUG"
case .info: "INFO"
case .warning: "WARNING"
case .error: "ERROR"
}
}
}

/// Delegate that can further process logs from the library
public protocol WPNLoggerDelegate: AnyObject {

/// If the delegate should follow selected verbosity level.
///
/// When set to true, then (for example) if `errors` is selected as a `verboseLevel`, only `error` logLevel will be called.
/// When set to false, all methods might be called no matter the selected `verboseLevel`.
var wpnFollowVerboseLevel: Bool { get }

/// Log was recorded
/// - Parameters:
/// - message: Message of the log
/// - logLevel: Log level
func wpnLog(message: String, logLevel: WPNLogLevel)
}

/// WPNLogger provides simple logging facility.
public class WPNLogger {

/// Defines verbose level for this simple debugging facility.
/// Verbose level of the logger.
public enum VerboseLevel: Int {
/// Silences all messages.
case off = 0
/// Only errors will be printed to the debug console.
/// Only errors will be printed to the system console.
case errors = 1
/// Errors and warnings will be printed to the debug console.
/// Errors and warnings will be printed to the system console.
case warnings = 2
/// All messages will be printed to the debug console.
case all = 3
/// Error ,warning and info messages will be printed to the system console.
case info = 3
/// All messages will be printed to the system console - including debug messages
case debug = 4
}

/// Current verbose level. Note that value is ignored for non-DEBUG builds.
/// Logger delegate
public static weak var delegate: WPNLoggerDelegate?

/// Current verbose level. `warnings` by default
public static var verboseLevel: VerboseLevel = .warnings

/// Character limit for single log message. Default is 12 000. Unlimited when nil
/// If HTTP traffic should be reported by this logger. `true` by default
///
/// You can use this option to stop log from the HTTP traffic when you setup your own logging logic
/// via the `responseDelegate` and `requestDelegate` in the `WPNNetworkingService`.
public static var logHttpTraffic = true

/// Headers that won't be logged.
///
/// Default headers to skip are:
/// ```
/// "accept-language", "content-type", "content-length",
/// "accept-language", "transfer-encoding", "date",
/// "server", "user-agent", "connection", "x-content-type-options",
/// "x-xss-protection", "cache-control", "pragma", "expires",
/// "x-frame-options", "vary"
/// ```
public static let httpHeadersToSkip = HeaderBlockList()

/// Character limit for single log message. Default is `12 000`. Unlimited when nil
public static var characterLimit: Int? = 12_000

/// Prints simple message to the debug console.
static func print(_ message: @autoclosure () -> String) {
#if DEBUG || WPN_ENABLE_LOGGING
if verboseLevel == .all {
Swift.print("[WPN] \(message().limit(characterLimit))")
}
#endif
/// Prints simple message to the system console.
static func debug(_ message: @autoclosure () -> String) {
log(message(), level: .debug)
}

/// Prints simple message to the system console.
static func info(_ message: @autoclosure () -> String) {
log(message(), level: .info)
}

/// Prints warning message to the debug console.
/// Prints warning message to the system console.
static func warning(_ message: @autoclosure () -> String) {
#if DEBUG || WPN_ENABLE_LOGGING
if verboseLevel.rawValue >= VerboseLevel.warnings.rawValue {
Swift.print("[WPN] WARNING: \(message().limit(characterLimit))")
}
#endif
log(message(), level: .warning)
}

/// Prints error message to the debug console.
/// Prints error message to the system console.
static func error(_ message: @autoclosure () -> String) {
#if DEBUG || WPN_ENABLE_LOGGING
if verboseLevel != .off {
Swift.print("[WPN] ERROR: \(message().limit(characterLimit))")
log(message(), level: .error)
}

private static func log(_ message: @autoclosure () -> String, level: WPNLogLevel) {
let levelAllowed = level.minVerboseLevel.rawValue <= verboseLevel.rawValue
let forceReport = delegate?.wpnFollowVerboseLevel == false
guard levelAllowed || forceReport else {
// not logging
return
}

let msg = message().limit(characterLimit)

if levelAllowed {
print("[WPN:\(level.logName)] \(msg)")
}
if levelAllowed || forceReport {
delegate?.wpnLog(message: msg, logLevel: level)
}
#endif
}

#if DEBUG
Expand All @@ -87,6 +168,82 @@ public class WPNLogger {
#endif
}

/// Headers to skip when logging.
///
/// Note that all headers are transformed to lowercase variant when added.
///
/// Default headers to skip are:
/// ```
/// "accept-language", "content-type", "content-length", "accept-language", "transfer-encoding", "date", "server", "user-agent",
/// "connection", "x-content-type-options", "x-xss-protection", "cache-control", "pragma", "expires", "x-frame-options", "vary"
/// ```
///
public class HeaderBlockList {

private var headersToSkp = [
"accept-language", "content-type", "content-length", "accept-language", "transfer-encoding", "date", "server", "user-agent",
"connection", "x-content-type-options", "x-xss-protection", "cache-control", "pragma", "expires", "x-frame-options", "vary"
]

/// Adds element to the block list.
/// - Parameter element: HTTP header key to block.
public func add(element: String) {
headersToSkp.append(element.lowercased())
}

/// Adds elements to the block list.
/// - Parameter element: HTTP header keys to block.
public func add(elements: [String]) {
headersToSkp.append(contentsOf: elements.map { $0.lowercased() })
}

/// Removes element from the block list.
/// - Parameter element: HTTP header key to remove.
public func remove(element: String) {
headersToSkp.removeAll { $0 == element.lowercased() }
}

/// Removes elements from the block list.
/// - Parameter element: HTTP header keys to remove.
public func removeAll(elements: [String]) {
elements.map { $0.lowercased() }.forEach {
if let idx = headersToSkp.firstIndex(of: $0) {
headersToSkp.remove(at: idx)
}
}
}

/// Remove all
public func removeAll() {
headersToSkp.removeAll()
}

/// Returns array of headers to skip
/// - Returns: Headers to skip
public func headersToSkip() -> [String] {
return Array(headersToSkp)
}

func filterHeaders(headers: [String: String]?) -> String {

guard let headers else {
return "no headers"
}

var result = ""
var skipped = 0

for header in headers {
if headersToSkp.contains(where: { $0 == header.key.lowercased() }) {
skipped += 1
} else {
result += "\n - \(header.key): \(header.value)"
}
}
return "\(skipped) filtered out" + result
}
}

private extension String {
func limit(_ characterLimit: Int?) -> String {
guard let cl = characterLimit else {
Expand Down
3 changes: 3 additions & 0 deletions Sources/WultraPowerauthNetworking/WPNNetworkingService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ public class WPNNetworkingService {
self.responseDelegate?.responseReceived(from: request.url, statusCode: urlResponse?.statusCode, body: receivedData)
resp = envelope
case .encrypted(let envelope, let decryptedData):
if D.logHttpTraffic {
D.debug("Decrypted response from \(request.url.absoluteString):\n\(String(decoding: decryptedData, as: UTF8.self) ?? "empty")")
}
self.responseDelegate?.encryptedResponseReceived(from: request.url, statusCode: urlResponse?.statusCode, body: receivedData, decrypted: decryptedData)
resp = envelope
case .failed:
Expand Down
Loading