iOS (OSLog)

Forward logs from Apple's built-in OSLog system to BunnyLogs — no third-party dependencies, using only URLSession and a thin wrapper around Logger.

How it works

OSLog doesn't support pluggable destinations the way CocoaLumberjack does, so the approach is a thin BunnyLogger wrapper that calls both os.Logger (for the system log / Xcode console) and your BunnyLogs endpoint in one call. No interception or swizzling required.

BunnyLogger
import Foundation
import os

struct BunnyLogger {
    private let oslog: Logger
    private let uuid: String
    private let program: String
    private let session: URLSession
    private let url: URL

    init(subsystem: String, category: String, uuid: String, program: String = "ios") {
        self.oslog   = Logger(subsystem: subsystem, category: category)
        self.uuid    = uuid
        self.program = program
        self.session = URLSession(configuration: .default)
        self.url     = URL(string: "https://bunnylogs.com/live/\(uuid)")!
    }

    func debug(_ message: String)   { oslog.debug("\(message)");   ship(message, level: "DEBUG") }
    func info(_ message: String)    { oslog.info("\(message)");    ship(message, level: "INFO") }
    func warning(_ message: String) { oslog.warning("\(message)"); ship(message, level: "WARN") }
    func error(_ message: String)   { oslog.error("\(message)");   ship(message, level: "ERROR") }
    func fault(_ message: String)   { oslog.fault("\(message)");   ship(message, level: "ERROR") }

    private func ship(_ message: String, level: String) {
        let body: [String: String] = [
            "message": message,
            "level":   level,
            "program": program,
        ]
        guard let data = try? JSONSerialization.data(withJSONObject: body) else { return }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = data

        session.dataTask(with: request).resume()
    }
}
Setup

Create a shared instance, typically as a global or in your dependency container:

// Shared across the app
let logger = BunnyLogger(
    subsystem: "com.example.myapp",
    category:  "general",
    uuid:      "<your-uuid>"
)

Or one per module:

enum Log {
    static let network = BunnyLogger(subsystem: "com.example.myapp", category: "network", uuid: "<your-uuid>", program: "ios/network")
    static let auth    = BunnyLogger(subsystem: "com.example.myapp", category: "auth",    uuid: "<your-uuid>", program: "ios/auth")
    static let ui      = BunnyLogger(subsystem: "com.example.myapp", category: "ui",      uuid: "<your-uuid>", program: "ios/ui")
}

Replace <your-uuid> with the UUID from your stream URL (https://bunnylogs.com/live/<uuid>).

Usage
logger.info("User signed in: \(userId)")
logger.warning("Cache miss — fetching from network")
logger.error("Payment failed: \(error.localizedDescription)")

// Module-scoped loggers
Log.network.info("Request completed in \(ms)ms")
Log.auth.error("Token refresh failed")
Sending only in production

Wrap the ship call in a debug guard to keep local development free of network traffic:

private func ship(_ message: String, level: String) {
    #if DEBUG
    return
    #endif
    // ... rest of ship()
}

Or gate on minimum level — only send warnings and above:

private func ship(_ message: String, level: String) {
    guard level == "WARN" || level == "ERROR" else { return }
    // ...
}
Field mapping
BunnyLogs fieldSource
messageString passed to info() / error() / etc.
levelMethod name mapped to DEBUG, INFO, WARN, or ERROR
programprogram constructor parameter (default: ios)
timestampServer receipt time (no client timestamp sent)
Notes
  • session.dataTask(with:).resume() is non-blocking — the network call dispatches to URLSession's own thread pool immediately.
  • OSLog redacts dynamic string content in the system log by default (shown as <private> in Console.app). The BunnyLogs side receives the full string because it is handled before OSLog's privacy filtering applies.
  • Store the UUID in your Info.plist or a secrets file rather than hardcoding it in source.
  • One URLSession per BunnyLogger instance is fine for low-to-medium volume. For a single high-throughput logger, share one session across instances.
  • Set up an Alert in BunnyLogs matching level=ERROR and program=ios to get notified on errors via Slack, Telegram, or Discord.