{"id":50089431,"url":"https://github.com/olcayertas/network-logger","last_synced_at":"2026-05-22T22:07:03.758Z","repository":{"id":359568259,"uuid":"1246660463","full_name":"olcayertas/network-logger","owner":"olcayertas","description":"Modern Swift network-debugging library — a SwiftUI rewrite of Wormholy with Swift 6 strict concurrency, actors, and URLSession delegate proxying.","archived":false,"fork":false,"pushed_at":"2026-05-22T12:38:36.000Z","size":48,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T17:59:36.305Z","etag":null,"topics":["debugging","ios","network-debugging","networking","swift","swift-concurrency","swift6","swiftui","urlsession","wormholy"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/olcayertas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T12:21:34.000Z","updated_at":"2026-05-22T12:38:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/olcayertas/network-logger","commit_stats":null,"previous_names":["olcayertas/network-logger"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/olcayertas/network-logger","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olcayertas%2Fnetwork-logger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olcayertas%2Fnetwork-logger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olcayertas%2Fnetwork-logger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olcayertas%2Fnetwork-logger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/olcayertas","download_url":"https://codeload.github.com/olcayertas/network-logger/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/olcayertas%2Fnetwork-logger/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33372739,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-22T21:56:13.512Z","status":"ssl_error","status_checked_at":"2026-05-22T21:56:10.769Z","response_time":265,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["debugging","ios","network-debugging","networking","swift","swift-concurrency","swift6","swiftui","urlsession","wormholy"],"created_at":"2026-05-22T22:07:02.982Z","updated_at":"2026-05-22T22:07:03.753Z","avatar_url":"https://github.com/olcayertas.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NetworkLogger\n\n[![Release](https://img.shields.io/github/v/release/olcayertas/network-logger?display_name=tag\u0026sort=semver\u0026color=brightgreen)](https://github.com/olcayertas/network-logger/releases/latest)\n[![Swift](https://img.shields.io/badge/Swift-6.0-orange.svg?logo=swift)](https://swift.org)\n[![Platforms](https://img.shields.io/badge/platforms-iOS%2016%20%7C%20macOS%2013-lightgrey.svg)](https://swift.org)\n[![SPM](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/)\n[![License](https://img.shields.io/github/license/olcayertas/network-logger?color=blue)](LICENSE)\n\nA modern Swift network-debugging library for iOS — a from-scratch rewrite of [Wormholy](https://github.com/pmusolino/Wormholy) using SwiftUI, Swift 6 strict concurrency, actors, and `URLSession` delegate proxying.\n\nIt captures HTTP requests, responses, headers, bodies, and metrics; lets you filter, share, and export as cURL / Postman / HAR / plain text; and lives behind a `NetworkLoggerView` that **you** present wherever you want.\n\n## Screenshots\n\n| Request list | Request detail | Response body |\n|:-:|:-:|:-:|\n| ![Request list with status-code chip filters](screenshots/request-list.png) | ![Request detail with headers and body summary](screenshots/request-detail.png) | ![Syntax-highlighted JSON response body](screenshots/response-body.png) |\n\n| Appearance | Share / export |\n|:-:|:-:|\n| ![Appearance sheet — color scheme, accent, body font size](screenshots/appearance-settings.png) | ![Share menu — text, cURL, Postman, HAR](screenshots/share-menu.png) |\n\n## Why a rewrite\n\nWormholy is great. NetworkLogger keeps every feature you actually use and drops two architectural pillars that fight modern apps:\n\n| Wormholy | NetworkLogger |\n|---|---|\n| Walks `UIApplication.shared.connectedScenes → keyWindow → rootViewController` to push its UI | You present `NetworkLoggerView(logger:)` like any SwiftUI view — `.sheet`, `NavigationLink`, fullscreen, tab, inline |\n| Shake-to-trigger NSNotification (`wormholy_fire`) is hardwired | No notifications, no shake handler, no global state — wire up whatever trigger you want |\n| Method-swizzles `NSURLSessionConfiguration` from an ObjC constructor | Pure-Swift `URLSession` delegate proxy you opt into per session |\n| Singleton `Storage.shared`, `@Published` reference types, `DispatchQueue.main.async` updates | `actor EventStore`, `Sendable` value types, Swift 6 strict concurrency |\n| `Wormholy.limit` getter that returns `nil` synchronously while doing a `Task { @MainActor in return ... }` | Real async API |\n\n## Installation\n\nSwift Package Manager:\n\n```swift\n.package(url: \"https://github.com/olcayertas/network-logger\", from: \"0.1.0\")\n```\n\nAdd one (or both) of the products to your target:\n\n| Product | Pulls in | When to use |\n|---|---|---|\n| `NetworkLogger` | swift-perception | Always — the core library. |\n| `NetworkLoggerDependencies` | the above + swift-dependencies | If you want `@Dependency(\\.networkLogger)` everywhere. |\n\niOS 16+ (uses the [Perception](https://github.com/pointfreeco/swift-perception) library to back-port `@Observable`). The Swift package targets iOS 16 and macOS 13; UI is iOS-only.\n\n### Excluding from Release builds\n\nAdd to your `Release.xcconfig` (or each non-debug target's build settings):\n\n```\nEXCLUDED_SOURCE_FILE_NAMES = NetworkLogger*\n```\n\nNo constructor-time side effects, so the library is inert if you just skip wiring it up.\n\n## Quick start\n\n```swift\nimport NetworkLogger\n\n// 1. Create an instance — no singleton.\nlet logger = NetworkLogger(configuration: .init(\n    limit: 500,\n    ignoredHosts: [\"analytics.example.com\"]\n))\n\n// 2. Build a pre-wired session and route requests through it.\nlet session = logger.makeLoggingURLSession()\nlet (data, response) = try await session.data(for: request)\n\n// 3. Present the inspector wherever you want.\n.sheet(isPresented: $showInspector) {\n    NetworkLoggerView(logger: logger)\n}\n```\n\n`makeLoggingURLSession()` is the shortest path — one line, full body / header / metrics capture, cancellation forwarded. If you need to keep your own `URLSession` (custom configuration, your own delegate, etc.) use the manual delegate-proxy path described under **Recording modes** below.\n\n## Recording modes\n\nThree complementary ways to feed events into a logger. Pick one or combine them.\n\n### 1. `LoggingURLSession` (recommended)\n\n```swift\nlet session = logger.makeLoggingURLSession()\nlet (data, response) = try await session.data(for: request)\n```\n\n`makeLoggingURLSession()` returns a `LoggingURLSession` actor that:\n\n- Owns a `URLSession` whose delegate is `LoggingURLSessionDelegate` (full callback forwarding).\n- Drives requests via `dataTask(with:).resume()` + a `CheckedContinuation` glue (`TaskResultSink`) so the data-delegate callbacks fire reliably — even on iOS 26, where `URLSession.data(for:delegate:)` no longer delivers `URLSessionDataDelegate` events to the per-task delegate.\n- Forwards Swift `Task` cancellation to the underlying `URLSessionDataTask`.\n- Bounds memory with a configurable `bodyCaptureLimit` (1 MiB default).\n\nCaptures everything the inspector needs: request body, response body + headers, metrics, redirects, auth challenges.\n\n### 1b. Manual delegate proxy (advanced)\n\nIf you already have a custom `URLSession` (your own configuration, pinned transport, existing delegate) and want to keep it, install the logger as a forwardee on your own delegate:\n\n```swift\nlet delegate = await logger.makeSessionDelegate(forwardingTo: myDelegate)\nlet session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)\n```\n\n`logger.makeSessionDelegate(forwardingTo:)` returns a `URLSessionDelegate` that records every callback then forwards to your real delegate via Objective-C message forwarding (`responds(to:)` + `forwardingTarget(for:)`). Works with:\n\n- Upload progress (`didSendBodyData:`)\n- SSL pinning / server-trust (`didReceive challenge:`)\n- HTTP redirects (`willPerformHTTPRedirection:`)\n- Multipart bodies (combined with `BodyStreamTee`)\n- `URLSessionStreamDelegate` / `URLSessionWebSocketDelegate` — forwarded without any code from us\n\nLimitation: the URLSession **async/await convenience APIs** (`session.data(from:)`, `session.data(for:)` without a per-task delegate) consume the data delegate methods internally on iOS 26, so the session delegate only sees `task` and `auth` events. Use `dataTask(with:).resume()` + a delegate-driven completion, or just use `makeLoggingURLSession()` which already does this for you.\n\n### 2. Manual recording\n\nFor gRPC, WebSocket, or any traffic that doesn't go through `URLSession`:\n\n```swift\nlet event = NetworkEvent(\n    request: .init(\n        url: URL(string: \"grpc://api.example.com/GetUser\")!,\n        httpMethod: \"POST\",\n        headers: [\"Content-Type\": \"application/grpc+proto\"],\n        body: BodyData(data: payload)\n    ),\n    response: .init(statusCode: 0, body: BodyData(data: reply)),\n    metrics: .init(duration: 0.085),\n    state: .completed\n)\nawait logger.record(event)\n```\n\n### 3. URLProtocol fallback (third-party SDKs)\n\nWhen you can't reach the URLSession (a closed-source SDK owns it), opt into the global URLProtocol:\n\n```swift\nlet configuration: URLSessionConfiguration = .default\nlogger.attach(to: configuration)            // installs LoggingURLProtocol on this config\nURLProtocol.registerClass(logger.urlProtocolClass)  // or globally, if you must\n```\n\nLimitations baked into Apple's URLProtocol design:\n\n- Cannot forward `didSendBodyData:` for streamed uploads (Wormholy bug #77 — an Apple-level limitation, not ours).\n- Background sessions are unsupported.\n- The challenge handler always calls its completion handler with `.performDefaultHandling` (fixes Wormholy #157's \"API misuse\" warning by construction).\n\n## Presenting the UI\n\n`NetworkLoggerView(logger:)` is a plain SwiftUI view that builds its own `NavigationStack`. Drop it wherever:\n\n```swift\n// Sheet\n.sheet(isPresented: $show) { NetworkLoggerView(logger: logger) }\n\n// Push\nNavigationLink(\"Network Logs\") {\n    NetworkLoggerView(logger: logger)\n}\n\n// Full-screen cover\n.fullScreenCover(isPresented: $show) { NetworkLoggerView(logger: logger) }\n\n// Tab in a debug menu\nTabView { NetworkLoggerView(logger: logger).tabItem { ... } }\n```\n\nThe library ships **no** shake-to-present helper, **no** floating overlay button, and **no** NSNotification trigger. Wire up your own trigger — see `Examples/NetworkLoggerDemo` for several patterns.\n\n## Customizing the UI\n\n`NetworkLoggerView` carries a slider icon in its leading nav-bar position. Tap it to open the **Appearance** sheet — settings persist to `UserDefaults` so they survive across launches.\n\n| Setting | What it does |\n|---|---|\n| **Color scheme** | Forces `System` / `Light` / `Dark` on the inspector independent of the host app, via `.preferredColorScheme(...)` at the root. |\n| **Accent** | Picks the inspector's tint (Blue / Indigo / Purple / Teal / Green / Orange / Pink). Applied via `.tint(...)` so the host app's accent doesn't bleed into chips and buttons. |\n| **Body font size** | Slider 9 – 22 pt (default 12 pt). Live preview in the sheet. Applied to the request and response body screens. |\n| **Reset to defaults** | One-tap restore. |\n\n### Programmatic access\n\n`AppearanceSettings.shared` is the singleton the UI reads from. You can pre-seed it at app startup (e.g. to force dark mode on internal builds):\n\n```swift\nimport NetworkLogger\n\nAppearanceSettings.shared.colorScheme = .dark\nAppearanceSettings.shared.accent = .indigo\nAppearanceSettings.shared.bodyFontSize = 11\n```\n\n### Syntax highlighting\n\nPretty-printed JSON bodies are token-coloured (keys, strings, numbers, booleans, `null`, punctuation) in both the request and response body views. Two palettes ship out of the box and are picked automatically based on the active color scheme:\n\n```swift\nSyntaxTheme.lightDefault    // Xcode-ish, tuned for paper-white surfaces\nSyntaxTheme.darkDefault     // higher saturation, tuned for dark surfaces\n```\n\nBuild a custom palette with the `SyntaxTheme(key:string:number:boolean:null:punctuation:)` initialiser. Non-JSON bodies (XML, plain text, binary placeholders) skip the highlighter and render unstyled — the scanner never throws on malformed input.\n\nSearch highlighting (yellow background from the search field) composes on top of syntax colours, so finding text inside a coloured body does not erase the syntax run for the rest of the response.\n\n## Using with Point-Free's Dependencies (optional)\n\nThe optional `NetworkLoggerDependencies` product registers `NetworkLogger` as a [swift-dependencies](https://github.com/pointfreeco/swift-dependencies) value, so you can configure once at app startup and read the configured instance from anywhere with `@Dependency(\\.networkLogger)` — no prop drilling, no global singleton.\n\n**Add the product** to your target:\n\n```swift\n.product(name: \"NetworkLoggerDependencies\", package: \"network-logger\")\n```\n\n**Configure once** at app startup:\n\n```swift\nimport NetworkLogger\nimport NetworkLoggerDependencies\nimport Dependencies\nimport SwiftUI\n\n@main\nstruct MyApp: App {\n    init() {\n        prepareDependencies {\n            $0.networkLogger = NetworkLogger(configuration: .init(\n                limit: 500,\n                ignoredHosts: [\"analytics.example.com\"]\n            ))\n        }\n    }\n\n    var body: some Scene {\n        WindowGroup { ContentView() }\n    }\n}\n```\n\n**Read it anywhere**:\n\n```swift\nstruct DebugMenuView: View {\n    @Dependency(\\.networkLogger) var logger\n    @State private var showLogs = false\n\n    var body: some View {\n        Button(\"Network logs\") { showLogs = true }\n            .sheet(isPresented: $showLogs) {\n                NetworkLoggerView(logger: logger)\n            }\n    }\n}\n```\n\nOr use the bundled `NetworkLoggerInspector()` view, which pulls the dependency for you:\n\n```swift\n.sheet(isPresented: $showLogs) {\n    NetworkLoggerInspector()\n}\n```\n\n**Override for tests / previews** like any Dependencies-managed value:\n\n```swift\nwithDependencies {\n    $0.networkLogger = NetworkLogger()\n} operation: {\n    // ... test or preview code that consumes @Dependency(\\.networkLogger)\n}\n```\n\nThis module is opt-in. If you don't import `NetworkLoggerDependencies`, swift-dependencies is never pulled into your dependency graph.\n\n## Configuration\n\n```swift\nNetworkLoggerConfiguration(\n    limit: 500,                          // ring buffer size\n    ignoredHosts: [\"analytics.example.com\"],\n    defaultFilter: \"/api/v2\",            // pre-fill the search box\n    bodyCaptureLimit: 1_048_576,         // 1 MiB body cap with truncation flag\n    headerRedactor: Redaction.makeHeaderRedactor(),  // redacts Authorization, Cookie, ...\n    responseTransformer: { data, request in\n        // decrypt, decompress, anything you want\n        return MyCrypto.decrypt(data)\n    }\n)\n```\n\nRuntime mutation:\n\n```swift\nawait logger.setLimit(1000)\nawait logger.setIgnoredHosts([\"new.host\"])\nawait logger.setDefaultFilter(\"/v3/\")\nawait logger.replaceConfiguration(newConfig)\n```\n\n## Exporters\n\n```swift\nlet events = await logger.snapshot()\n\nCurlExporter.string(for: events)                       // multiline cURL commands\nPostmanExporter.collection(name: \"MyApp\", from: events) // Postman 2.1 JSON\nPlainTextExporter.text(for: events)                    // human-readable text\nHARExporter.har(from: events)                          // HAR 1.2 JSON\n```\n\nThe built-in UI wires these into `ShareLink` automatically (Save to Files, Mail, Messages, etc.).\n\n## Filters\n\n```swift\nlet combined = CompositeFilter(\n    URLSubstringFilter(\"/users\"),\n    MethodFilter(\"POST\"),\n    StatusCodeRangeFilter.clientError,\n    HostFilter(blocking: [\"staging.example.com\"])\n)\nlet problems = (await logger.snapshot()).filtered(by: combined)\n```\n\nBuilt-ins: `URLSubstringFilter`, `MethodFilter`, `HostFilter`, `StatusCodeRangeFilter`, `CompositeFilter` (AND), `AnyOfFilter` (OR), `NotFilter`. All `Sendable`. Implement your own by conforming to `EventFilter`.\n\n## Wormholy issue map\n\n| Wormholy issue | NetworkLogger resolution |\n|---|---|\n| #157 SSL pinning \"API MISUSE\" | Delegate proxy forwards `didReceive challenge:` to your delegate; URLProtocol mode always calls the completion handler. |\n| #147 Empty request body | `BodyStreamTee` clones `httpBodyStream` for the URLProtocol path. The delegate proxy captures from `task.originalRequest.httpBody`, which URLSession preserves. |\n| #146 Custom logs (gRPC) | Public `record(_:)` API. |\n| #144 Debug-only SPM | `EXCLUDED_SOURCE_FILE_NAMES = NetworkLogger*` per Release config — no library code change needed since there's no constructor side effect. |\n| #132 Custom cell content | (Roadmap) cell row override via `RequestListView` initializer. |\n| #130 Decrypt response | `Configuration.responseTransformer`. |\n| #125 Response mocking (Proxyman-style) | Roadmap (stretch goal). |\n| #114 Static linking | ObjC constructor + method swizzling removed entirely. Works by construction. |\n| #93 Multipart bodies | Same `BodyStreamTee` path as #147. |\n| #77 Alamofire/upload progress | Delegate-proxy mode forwards `didSendBodyData:` to your delegate. URLProtocol mode preserves Apple's limitation but documents it. |\n\n## Concurrency model\n\n- All public mutable state is in an `actor` (`EventStore`, `ConfigurationStore`, per-task `SessionRecorder`).\n- `NetworkEvent` and its components are `struct, Sendable` value types.\n- `LoggingURLSessionDelegate` is `NSObject, @unchecked Sendable` (URLSession requires NSObject; isolation is enforced by routing all state mutations through the recorder actor).\n- UI uses `@Perceptible` (Perception library) `@MainActor` view models subscribed to `logger.eventStream()`.\n- No `DispatchQueue.main.async`, no `@Published` on reference types, no implicit globals.\n\nBuilds clean under `-strict-concurrency=complete` on Swift 6.\n\n## License\n\nMIT — same as Wormholy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folcayertas%2Fnetwork-logger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Folcayertas%2Fnetwork-logger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Folcayertas%2Fnetwork-logger/lists"}