Skip to content

anthony1810/SendLogsKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SendLogsKit

A reusable iOS package that gives any app:

  • A persistent logger (swift-log + GRDB-backed SQLite) that survives app relaunches.
  • A drop-in SwiftUI sheet that lets users share, mail, copy, or clear the captured logs.

Built so any client app can wire it up with one config struct, no further glue code, and no host-specific assumptions.

Requirements

  • iOS 17+ (or macOS 14+ for the core module)
  • Swift 5.9+
  • Xcode 15+
  • The app's Info.plist must declare NSAppTransportSecurity / mail entitlements as usual for MFMailComposeViewController.

Installation

Xcode

File → Add Package Dependencies…

git@github.com:anthony1810/SendLogsKit.git

Pick the version (e.g. 1.0.0, exact). Add the products you need:

  • SendLogsCore — the logger + SQLite store.
  • SendLogsUI — the "Send Logs" SwiftUI sheet (depends on SendLogsCore).

Package.swift

dependencies: [
    .package(url: "git@github.com:anthony1810/SendLogsKit.git", exact: "1.0.0"),
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "SendLogsCore", package: "SendLogsKit"),
            .product(name: "SendLogsUI",   package: "SendLogsKit"),
        ]
    ),
]

Quick Start

1. Configure the logger once at app launch

import SwiftUI
import SendLogsCore

@main
struct MyApp: App {

    init() {
        let config = SendLogsConfig(
            recipients: ["support@yourdomain.com"],
            subjectPrefix: "[Logs] MyApp Report",
            appName: "MyApp"
        )
        let sqliteService = try! SQLiteLoggingService(config: config)
        UnifiedLogger.shared.configure(services: [sqliteService], config: config)
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

Call configure exactly once. It bootstraps swift-log's LoggingSystem, opens the SQLite store, and schedules a background sweep of expired logs.

2. Log from anywhere

import Logging
import SendLogsCore

Logger.info.log(category: .general, "User signed in")
Logger.error.log(category: .networking, "Request failed: \(error)")

The convenience accessors Logger.debug / .info / .error / .critical route through UnifiedLogger to every registered service. Five sensible defaults ship out of the box (.general, .networking, .event, .database, .ui) — see Categories below for how to add your own.

You can also use plain swift-log directly — LoggingSystem is bootstrapped, so Logger(label: "anything").info("…") writes to the same store.

Categories

A category is just a string label that gets attached to each log record. Built-in categories live on LogCategory:

public extension LogCategory {
    static let general:    LogCategory = "general"
    static let networking: LogCategory = "networking"
    static let event:      LogCategory = "event"
    static let database:   LogCategory = "database"
    static let ui:         LogCategory = "ui"
}

Add your own by extending LogCategory — works because it conforms to ExpressibleByStringLiteral:

extension LogCategory {
    static let ditto:   LogCategory = "ditto"
    static let gameday: LogCategory = "gameday"
}

Logger.info.log(category: .ditto, "subscription started")

Or define a typed enum for your app, conforming to LogCategoryRepresentable:

enum AppCategory: String, LogCategoryRepresentable {
    case ditto, gameday, sync
}

Logger.info.log(category: AppCategory.gameday, "period changed")

Both approaches reach the same LoggingSystem and end up in the same SQLite store. Categories don't need to be pre-registered — swift-log handlers are looked up by label on demand.

3. Present the Send Logs sheet

import SendLogsUI

struct SettingsView: View {

    let config: SendLogsConfig
    @State private var showSendLogs = false

    var body: some View {
        Button("Send Logs") { showSendLogs = true }
            .sheet(isPresented: $showSendLogs) {
                SendLogsView(config: config)
            }
    }
}

The sheet exposes four actions:

  • Share Logs — opens UIActivityViewController with the SQLite file.
  • Send Mail — opens MFMailComposeViewController pre-populated with the configured recipients, subject and a body containing device + version info.
  • Copy File URL — copies the on-disk path to the pasteboard (useful in dev).
  • Clear Logs — wipes every row from the store.

Configuration

SendLogsConfig is a Sendable value type — share one instance between the logger setup and the UI:

Field Required Default Purpose
recipients yes MFMailComposeViewController to: list. Pass [] to disable the mail button effectively.
subjectPrefix yes Mail subject. Date is appended as "\(prefix): \(date)".
appName yes Used in the mail body template ("Please find the diagnostic logs for \(appName) attached.").
folderName no "SendLogsKit" Subfolder under Application Support where logs.sqlite lives. Change this when shipping multiple apps that share an app group.
rollingFrequencyDays no 3 Number of days to retain logs. Anything older is deleted on configure.
contextProvider no nil @Sendable () -> [String: String] evaluated on every log insert. Returned key/value pairs are merged into the log's metadata column. Use this to attach userId / teamId / build flavour / anything else you'd want to grep on later.

contextProvider example

let config = SendLogsConfig(
    recipients: ["support@yourdomain.com"],
    subjectPrefix: "[Logs] MyApp",
    appName: "MyApp",
    contextProvider: { [
        "userId": Session.current?.userId ?? "",
        "teamId": Session.current?.teamId ?? "",
    ] }
)

Every log row's metadata JSON now carries those fields automatically — no per-call wrapping needed.

Advanced: custom logger services

SQLiteLoggingService is one implementation of LoggerServiceProtocol. You can register additional services (e.g. forward errors to Mixpanel/Sentry, mirror to OSLog) by conforming to the protocol:

actor MixpanelLoggingService: LoggerServiceProtocol {
    func insert(_ log: LogModel) throws {
        guard log.logLevel == .error || log.logLevel == .critical else { return }
        Mixpanel.mainInstance().track(event: "Log", properties: ["message": log.message])
    }
}

UnifiedLogger.shared.configure(
    services: [
        try SQLiteLoggingService(config: config),
        MixpanelLoggingService(),
    ],
    config: config
)

Default protocol methods make configure / exportLogs / clearExpiredLogs / clearAllLogs optional — implement only what your service needs.

File location

Logs live at:

{Application Support}/{config.folderName}/logs.sqlite

The directory is created on first launch with extensionHidden: true and protectionKey: .complete. Both the directory and the file are excluded from iCloud backups by virtue of being in Application Support and using complete file protection.

Notes & caveats

  • Mail entitlementMFMailComposeViewController is unavailable in the Simulator without an account configured. The sheet falls back to a friendly message in that case.
  • SharingUIActivityViewController will pass the raw .sqlite file. Most modern email clients (Gmail, Outlook) will attach it. AirDrop works to a Mac for local inspection.
  • Schema version — the store erases itself on schema upgrades (SQLiteLoggingService.schemaVersion). Stored in UserDefaults under sendlogskit.schema.version.
  • SwiftUI preview safetyUnifiedLogger.configure(...) is a no-op inside Xcode SwiftUI previews so previews don't write to disk.
  • configure is one-shot — calling it twice triggers an assertionFailure in DEBUG (silent in release). Wire it from your App.init or app delegate's didFinishLaunching.

Versioning

Semantic versioning. Pin to an exact version in your Package.swift and bump intentionally.

License

MIT. See LICENSE.

About

Reusable iOS logging + Send-Logs UI: swift-log + GRDB SQLite store + SwiftUI sheet for sharing / mailing logs.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages