Skip to content

Repository files navigation

SwiftMoLogger

The logging package iOS teams wish they'd written. Structured, multi-engine, Swift-Concurrency-native β€” with an in-app Instruments dashboard, zero-config live tail to your Mac, automatic PII redaction, Sentry/Datadog/Loki shippers, and Swift Macros. All in one package, all opt-in.

Swift Platforms SPM License

by Mohammed Elnaggar (@MoElnaggar14)


30-second pitch

import SwiftMoLogger

// Day 1: it just works.
SwiftMoLogger.info("App started")
SwiftMoLogger.error("Payment failed", tag: .api, metadata: [
    "order_id": "ord_4291",
    "amount": 49.99
])
import SwiftMoLoggerUI

// Day 2: drop one view, get Instruments inside your app.
DiagnosticsHubView()
# Day 3: tail every device on your Wi-Fi from the terminal.
swift run swiftmologger-inspector

That's it. No configure(…), no singletons to wire, no protocol gymnastics.


Table of contents


Why SwiftMoLogger?

What it solves
🎯 Zero ceremony. SwiftMoLogger.info("hi") works the moment you import. No configuration step.
🧩 Structured everywhere. Every call materialises a LogEntry with level + tag + metadata + source location + thread. No more parsing strings downstream.
πŸš€ Sub-Β΅s hot path. ~140 ns when no engines are attached, ~310 ns with a memory engine. See PERFORMANCE.md.
πŸ›‘ Production-safe by default. Built-in PII / token / credit-card redaction. Rate limiting. Sampling. Privacy manifest.
πŸ”­ Self-hosted observability. DiagnosticsHubView() is Instruments + Charles + Console inside your app. No cable, no Mac required.
πŸ“‘ Zero-config live tail. Bonjour-advertised devices, terminal CLI on your Mac auto-discovers them all.
πŸ›° W3C distributed tracing. Stamp every URLSession request with traceparent so iOS spans show up next to your backend trace.
πŸ“Ό Flight recorder. Rolling 2-minute black box persisted to disk; replay the seconds before a crash on next launch.
πŸͺž Smart error grouping. Spammy retries collapse into one card with a count, not 1 000 noise lines.
πŸ§ͺ First-class testing. Drop-in XCTest assertions over what was logged.
πŸͺΆ Opt-in everything. 7 separate library products. Pay only for what you import.

Install

// Package.swift
dependencies: [
    .package(url: "https://github.com/MoElnaggar14/SwiftMoLogger.git", from: "3.0.0")
],
targets: [
    .target(name: "App", dependencies: [
        .product(name: "SwiftMoLogger", package: "SwiftMoLogger"),
        // …add only what you need:
        .product(name: "SwiftMoLoggerUI", package: "SwiftMoLogger"),
        .product(name: "SwiftMoLoggerNetwork", package: "SwiftMoLogger"),
        .product(name: "SwiftMoLoggerRemote", package: "SwiftMoLogger"),
        .product(name: "SwiftMoLoggerDiagnostics", package: "SwiftMoLogger"),
        .product(name: "SwiftMoLoggerTesting", package: "SwiftMoLogger"),
        .product(name: "SwiftMoLoggerSugar", package: "SwiftMoLogger"),
    ])
]

Architecture at a glance

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     YOUR APP                                β”‚
β”‚       SwiftMoLogger.info("...", tag: .api, metadata: ...)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚      EngineRegistry         β”‚  os_unfair_lock
                β”‚   (level filter + fan-out)  β”‚  ~140 ns hot path
                β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚     β”‚     └──────────────┐
       β”‚                    β”‚     β”‚                    β”‚
   β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β–Όβ”€β” β”Œβ”€β–Όβ”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
   β”‚System β”‚         β”‚Memory β”‚ β”‚ File   β”‚   …     β”‚ Custom  β”‚
   β”‚Logger β”‚         β”‚Engine β”‚ β”‚Engine  β”‚         β”‚ Engine  β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚     β”‚                    β”‚
   os.Logger              ring   JSONL              Sentry /
                          buffer rotation           Datadog /
                                                    Loki / WS / …

Decorators (Redacting, Sampling, RateLimiting) wrap any engine. Streams (AsyncStream<LogEntry>, Combine Publisher) tap the registry. The SwiftUI Hub reads from shared NetworkEventStore / SignpostEventStore / VitalsHistoryStore / BreadcrumbStore.

Products

Product What you get
SwiftMoLogger Core: levels, tags, metadata, engines, registry, MetricKit, breadcrumbs, redaction, sampling, rate-limiting, Combine, signposts
SwiftMoLoggerUI SwiftUI console (LogConsoleView) + DiagnosticsHubView (the headline)
SwiftMoLoggerNetwork URLProtocol that auto-logs every URLSession request
SwiftMoLoggerRemote HTTPLogShipper + ready-made SentryLogEngine / DatadogLogEngine / LokiLogEngine
SwiftMoLoggerDiagnostics LiveSink (Bonjour), AppVitalsMonitor, BugReporter, WebSocketTailEngine
SwiftMoLoggerTesting XCTAssertLogged + RecordingLogEngine
SwiftMoLoggerSugar #log / #measure / @AutoLog Swift Macros
swiftmologger-inspector Mac CLI executable for live tail

The headline features

1. Diagnostics Hub β€” Instruments inside your app

One SwiftUI view that turns any build into a self-hosted observability cockpit. No cable, no Mac, no Xcode β€” just open the app.

import SwiftMoLoggerUI

struct DebugTab: View {
    var body: some View { DiagnosticsHubView() }
}
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ πŸ” Diagnostics Hub      πŸ“„ 421   🌐 38   γ€° 12   [πŸ—‘ clear]       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 14:22:01 β”ƒβ–Œβ–Œβ–Œβ–Žβ–Žβ–β– β–ˆβ–Œβ–Œβ–Žβ–Žβ–β–β–  β–Žβ–Œβ–ˆβ–Œβ–Œβ–Žβ–β–  β–Œβ–Žβ–β–Žβ–β– ┃ 14:23:01          β”‚
β”‚           ━━━━━━━━━━━━━━━━━━━━●━━━━━━━━                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  [πŸ“„ Logs] [🌐 Network] [γ€° Signposts] [πŸ’— Vitals] [🐚 Crumbs]   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β–Ά GET /v1/users      β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘  142ms  [200]                    β”‚
β”‚ β–Ά POST /v1/checkout  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘  423ms  [201]                    β”‚
β”‚ β–Ά GET /v1/products   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 891ms  [500]                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

You get:

  • Timeline scrubber with log-density bar β€” rewind up to 10 minutes
  • Network waterfall of every URLSession request (colour-coded by status)
  • Signpost flame graph with automatic lane assignment
  • Vitals charts (memory / CPU / FPS / thermal) via Swift Charts
  • Breadcrumb trail with category-coloured pins

2. Bonjour live tail β€” zero-config Mac companion

The on-device LiveSink advertises a Bonjour service. The bundled Mac CLI discovers every device on the network and pretty-prints every log line.

#if DEBUG
import SwiftMoLoggerDiagnostics
let sink = LiveSink()
try sink.start()
SwiftMoLogger.addEngine(sink)
#endif
$ swift run swiftmologger-inspector
SwiftMoLogger Inspector β€” discovering _swiftmologger._tcp on local network…
β—‰ discovered MyApp-iPhone-15
β—‰ discovered MyApp-iPad-Pro
● connected MyApp-iPhone-15
● connected MyApp-iPad-Pro

14:22:01.124 INFO  MyApp-iPhone-15 [API]      HTTP response status=200 duration_ms=132
14:22:01.221 WARN  MyApp-iPad-Pro  [Layout]   Auto-layout broke 3 constraints
14:22:01.337 ERROR MyApp-iPhone-15 [Database] Migration v4 β†’ v5 timed out

Multiple devices, one terminal, no Xcode needed.

3. Swift Macros β€” zero-boilerplate call sites

import SwiftMoLoggerSugar

#log("user signed in", level: .info, tag: .api)
// Captures #fileID / #function / #line at the call site.

let users = #measure("loadUsers") {
    try repo.all()
}
// Lowers to LogSignpost.measure("loadUsers") { … }

@AutoLog
final class CheckoutService {
    func purchase(_ id: String) throws { … }
}

Macros live in a separate SwiftMoLoggerSugar product so the swift-syntax build cost is opt-in.


Core logging

import SwiftMoLogger

// 8 levels mapped to OSLogType
SwiftMoLogger.trace("internals")
SwiftMoLogger.debug("only in DEBUG builds")
SwiftMoLogger.info("happy path")
SwiftMoLogger.notice("worth noticing")
SwiftMoLogger.warn("looks off")
SwiftMoLogger.error("broke")
SwiftMoLogger.critical("badly broke")
SwiftMoLogger.fault("unrecoverable")

// Errors with auto-metadata
SwiftMoLogger.error(error, tag: .api)
// β†’ metadata.error_type, metadata.error captured automatically

// Tagged with namespaces β€” code completion friendly
SwiftMoLogger.info("hit cache", tag: .Data.cache)
SwiftMoLogger.warn("slow query", tag: .Data.database)
SwiftMoLogger.info("custom", tag: .custom("Checkout", domain: "checkout"))

// Global level filter β€” short-circuits before any allocation
SwiftMoLogger.minimumLevel = .info  // drops trace + debug everywhere

Engines

SwiftMoLogger.addEngine(MemoryLogEngine(capacity: 1_000))
SwiftMoLogger.addEngine(try FileLogEngine(
    fileURL: URL.documentsDirectory.appending(path: "app.log"),
    maxFileSizeBytes: 2 * 1_048_576,
    maxRotatedFiles: 3
))

Write your own in 3 lines:

struct AnalyticsEngine: LogEngine {
    func log(_ entry: LogEntry) {
        guard entry.level >= .warning else { return }
        Analytics.track(entry.message, properties: entry.metadata.storage)
    }
}
SwiftMoLogger.addEngine(AnalyticsEngine())

LogTagged β€” automatic per-object tagging

struct APIService: LogTagged {
    var logTag: LogTag { .api }
}

let service = APIService()
service.logInfo("hit")           // β†’ automatically tagged [API]
service.logError(networkError)   // β†’ tag + structured error metadata

Production hardening

PII redaction

Every log line passes through a regex-based scrubber before it leaves your process.

SwiftMoLogger.enableRedaction()  // one-line install over SystemLogger

Default rules: JWT, Bearer / Basic tokens, AWS / GCP keys, emails, credit cards, phone numbers, IPv4, UUIDs. Walks metadata recursively. Custom rules:

var redactor = Redactor()
try redactor.add(Redactor.Rule(name: "ssn", pattern: #"\d{3}-\d{2}-\d{4}"#))
SwiftMoLogger.addEngine(RedactingLogEngine(wrapping: networkEngine, redactor: redactor))

Breadcrumbs

SwiftMoLogger.breadcrumb("user tapped Buy", category: .userAction)
SwiftMoLogger.breadcrumb("nav β†’ checkout", category: .navigation)

// Attach to a crash report / bug report
let crumbs: [Breadcrumb] = SwiftMoLogger.breadcrumbs()

Bounded ring buffer (default 100), O(1) append, Sendable value type matching the Sentry / Bugsnag shape so shipping is a 1:1 mapping.

Sampling + rate limiting

// Keep 1% of trace logs in production
SwiftMoLogger.addEngine(SamplingLogEngine(
    wrapping: fileEngine,
    strategy: .perLevel(rates: [.trace: 0.01, .debug: 0.1])
))

// Cap any sink at 50 logs/sec with a 100-event burst
SwiftMoLogger.addEngine(RateLimitingLogEngine(
    wrapping: networkEngine,
    permitsPerSecond: 50,
    burst: 100
))

Token-bucket rate limiter, thread-local PRNG for sampling β€” both ~ns-class overhead.

Remote shipping (Sentry / Datadog / Loki)

import SwiftMoLoggerRemote

SwiftMoLogger.addEngine(SentryLogEngine(
    dsn: URL(string: "https://[email protected]/456")!,
    release: "1.4.2",
    environment: "production"
))

SwiftMoLogger.addEngine(DatadogLogEngine(
    apiKey: "<DD_API_KEY>",
    site: .eu1,
    service: "checkout"
))

SwiftMoLogger.addEngine(LokiLogEngine(
    endpoint: URL(string: "https://loki.example.com/loki/api/v1/push")!,
    labels: ["job": "ios", "env": "prod"]
))

All shippers: batch (50–100), debounce (5 s), retry with exponential backoff, cap buffered entries on long offline spells. log() is O(1) β€” network happens off the caller's thread.

Auto network logging

import SwiftMoLoggerNetwork

let config = URLSessionConfiguration.default
NetworkLogger.install(on: config)
let session = URLSession(configuration: config)
// Every request is now logged with method/URL/status/duration_ms
// + breadcrumbs are recorded. Sensitive headers stripped automatically.

Authorization, Cookie, X-API-Key, X-Auth-Token and friends are stripped by default β€” extend NetworkLoggingProtocol.sensitiveHeaders to add more.

Privacy manifest

PrivacyInfo.xcprivacy ships in the package. It declares:

  • NSPrivacyTracking = false (no tracking)
  • No collected data types
  • Approved API reasons: UserDefaults (CA92.1), FileTimestamp (C617.1), SystemBootTime (35F9.1)

App Store submissions pass without further work.


Distributed tracing (W3C)

Stamp every log entry and every outbound URLSession request with a W3C traceparent header. Tie the iOS-side operation directly to the downstream backend trace in Datadog, Honeycomb, OpenTelemetry, etc.

SwiftMoLogger.withTrace {
    SwiftMoLogger.info("starting checkout")
    try await api.charge(order)   // outgoing request gets traceparent: 00-<traceID>-<spanID>-01
    try await api.confirm(order)  // same trace, new child span
}

TraceContext is a value type β€” generate fresh roots, spawn child spans, parse inbound headers:

let ctx = TraceContext.generate()
let parsed = TraceContext.parse(traceparent: incomingHeader)
let child = ctx.childSpan()

Backed by @TaskLocal, so concurrent tasks see their own trace.


Flight recorder β€” black box for crashes

let recorder = FlightRecorder(window: 120, flushInterval: 2)
recorder.start()

Persists a rolling 2-minute window of every signal (logs, breadcrumbs, network, signposts, vitals) to disk every 2 seconds. On next launch:

if let session = FlightRecorder.recoverLastSession() {
    SwiftMoLogger.warn("Recovered crashed session: \(session.entries.count) entries")
    uploader.attach(session)
}

Returns non-nil only when the previous run never had a clean stop() β€” almost always a crash, OOM, or watchdog kill. The exact signals you wish you'd had, after the fact.


Error grouping

A 1000-occurrence retry-spam logged once with count = 1000:

SwiftMoLogger.addEngine(ErrorGroupingEngine(
    wrapping: sentryShipper,
    fingerprintMinLevel: .warning,
    emitThreshold: 1   // emit first occurrence per fingerprint
))

Fingerprints by normalising the message (UUIDs β†’ <uuid>, hex blobs β†’ <hex>, digit runs β†’ #, quoted strings β†’ "…"), then SHA-256 of the result. Different shapes stay distinct; identical-shape noise collapses.

let groups = grouper.snapshot()
// β†’ ErrorGroup(count: 1024, exemplar: "User # timed out", firstSeen: …, lastSeen: …)

Swift Concurrency

Task-local ambient context

SwiftMoLogger.withContext(["request_id": "req-42", "user_id": "u-123"]) {
    SwiftMoLogger.info("fetching profile")   // ← inherits both keys
    try await api.fetchProfile()
    SwiftMoLogger.info("profile cached")     // ← still inherits
}
SwiftMoLogger.info("outside scope")          // ← clean

Backed by @TaskLocal β€” concurrent Tasks see their own scope without interfering.

AsyncStream of entries

Task {
    for await entry in SwiftMoLogger.stream() where entry.level >= .error {
        await reportToBackend(entry)
    }
}

Combine publisher (alternative)

SwiftMoLogger.publisher()
    .filter { $0.level >= .error }
    .sink { entry in /* … */ }
    .store(in: &cancellables)

Signposts for Instruments

let users = try LogSignpost.measure("loadUsers", tag: .database) {
    try userRepo.all()
}

let response = try await LogSignpost.measureAsync("uploadAvatar") {
    try await uploader.send(image)
}

One call emits both an os_signpost interval (visible in Instruments' Points of Interest) and a log entry with metadata.elapsed_ms.


Performance

Measured on M1 MacBook Pro, iOS 17 simulator, release build:

Scenario per-call median
info("…") β€” no engines ~140 ns
info("…") β€” MemoryLogEngine only ~310 ns
info("…") filtered out by minimumLevel ~35 ns
info("…") β€” SystemLogger (os.log) ~820 ns
Concurrent 8 threads Γ— 2 000 calls linear scaling, ~22 ms total

Memory: LogEntry is 200 B on the stack with zero heap unless metadata is non-empty. MemoryLogEngine pre-allocates its ring buffer β€” zero growth, zero GC churn.

Full benchmarks + design rationale β†’ PERFORMANCE.md.


Testing

import SwiftMoLoggerTesting

final class CheckoutTests: XCTestCase {
    var logs: RecordingLogEngine!

    override func setUp() {
        logs = SwiftMoLogger.installRecorder()
    }

    func testFailureIsLogged() async throws {
        try await service.purchase(invalid: true)
        XCTAssertLogged(.error, contains: "declined", tag: .api, in: logs)
        XCTAssertLogCount(0, atLevel: .fault, in: logs)
    }
}

RecordingLogEngine captures everything; assertions are simple, scoped to a single test, and zero-config.


Comparison

SwiftMoLogger os.Logger SwiftyBeaver CocoaLumberjack
Structured LogEntry βœ… ❌ (string) ⚠️ ⚠️
Multi-engine fan-out βœ… ❌ βœ… βœ…
AsyncStream<LogEntry> βœ… ❌ ❌ ❌
Combine publisher βœ… ❌ ❌ ❌
In-app Instruments view βœ… ❌ ❌ ❌
Bonjour live tail βœ… ❌ ❌ ❌
Built-in PII redaction βœ… ❌ ❌ ❌
Breadcrumbs βœ… ❌ ❌ ❌
Auto URLSession capture βœ… ❌ ❌ ❌
Sentry / Datadog / Loki βœ… ❌ ⚠️ ❌
Sampling + rate limit βœ… ❌ ❌ ❌
App vitals (CPU/FPS/mem) βœ… ❌ ❌ ❌
Swift Macros βœ… ❌ ❌ ❌
XCTAssertLogged βœ… ❌ ❌ ❌
Privacy manifest βœ… n/a ❌ ❌
Task-local context βœ… ❌ ❌ ❌
W3C traceparent propagation βœ… ❌ ❌ ❌
Flight recorder βœ… ❌ ❌ ❌
Smart error grouping βœ… ❌ ❌ ❌
Xcode code snippets bundled βœ… ❌ ❌ ❌
Hot path (no engines) ~140 ns ~120 ns ~3 Β΅s ~2 Β΅s

Migration from v2

v2 v3 Notes
LogEngine.info(message:) LogEngine.log(_:) v2 methods kept as default-impls
LogTag is enum LogTag is struct + namespaces All .api shorthands preserved
getAllEngines() allEngines() Old name kept as deprecated alias
info/warn silently dropped in release always shipped Real bug fix
no metadata metadata: [:] on every call
no source location captured via #fileID / #line automatic
no AsyncStream SwiftMoLogger.stream()
no signpost integration LogSignpost.measure
no SwiftUI console LogConsoleView, DiagnosticsHubView

Development model (GitFlow)

Branch Purpose Direct push?
main tagged releases only ❌ release PR
develop integration ❌ via PR
feature/* new features β†’ develop merge to develop
bugfix/* bug fixes β†’ develop merge to develop
release/* release prep β†’ main + develop merge both
hotfix/* emergency from main merge both

Branch policy is enforced by .github/workflows/gitflow.yml. Full procedure β†’ GITFLOW.md.


πŸ“š Article series

A 5-part deep-dive on the rewrite, the design choices, and the production playbook. Read in order or jump to whichever is on fire for you today.

# Title What you'll learn
1 Why I rewrote iOS logging from scratch The shortcomings of print / os.Logger / SwiftyBeaver, and the design principles behind v3
2 Sub-Β΅s logging: the performance design Why the hot path is ~140 ns β€” locking choices, autoclosure tricks, allocation budgets
3 Instruments in your app: building Diagnostics Hub How the timeline + waterfall + flame graph + vitals charts compose
4 Zero-config debugging with Bonjour and Swift Macros The Mac CLI live tail, the macros target, and the dev-experience wins
5 The production playbook: tracing, redaction, flight recorder The features that save you on the 3 AM call

Series index: Articles/README.md.


Xcode code snippets

Five .codesnippet files in Extras/Snippets/ for the calls you'll type most often:

Prefix Expands to
smlinfo SwiftMoLogger.info(…, tag:, metadata:)
smlerror SwiftMoLogger.error(error:, tag:, metadata:)
smlmeasure LogSignpost.measure("name", tag: .performance) { … }
smlcontext SwiftMoLogger.withContext(…) { … }
smlcrumb SwiftMoLogger.breadcrumb(…, category: .userAction)

Install:

cp Extras/Snippets/*.codesnippet ~/Library/Developer/Xcode/UserData/CodeSnippets/

Restart Xcode. The snippets show up in the Snippets Library (βŒ˜β‡§L) and autocomplete by prefix.


License

MIT. See LICENSE.


Built with care by @MoElnaggar14. If it helped you ship faster, drop a ⭐.

About

πŸš€ A comprehensive, scalable logging framework for iOS applications with built-in MetricKit crash reporting. Features 40+ organized log tags, protocol-based automatic context, and zero dependencies. Perfect for production apps! πŸ“±πŸͺ΅

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages