☰
Pulse 的 LoggerStore 全面解析:Core Data 存储引擎、日志写入与导出实战指南
2026/9/29 3:09:41 网站建设 项目流程
  • 开发工具
  • 可观测性

【免费下载链接】Pulse

Network logger for Apple platforms

项目地址:https://gitcode.com/gh_mirrors/pul/Pulse
点击查看免费下载

LoggerStore是 Pulse 网络日志框架(Network logger for Apple platforms)的持久化存储核心:它负责把日志消息、网络请求/响应与响应体(blob)写入以 SQLite 为底层、Core Data 为模型的本地存储中,并提供查询、导出、清理与直接数据库访问能力。本文以 LoggerStore 的 DocC 扩展文档 为骨架,结合 LoggerStore.swift、LoggerStore+Configuration.swift 等源码,完整讲解 LoggerStore 的初始化选项、存储与访问 API、导出流程、实体模型以及底层实现原理,帮助你掌握在自有 App 中集成与二次开发 Pulse 存储层的完整方案。

LoggerStore 是什么

LoggerStore是 Pulse 中最核心的持久化组件,源码注释将其定义为"Persistently stores logs, network requests, and response blobs"(LoggerStore.swift 第 9 行)。它是一个以 Swift Package 形式分发的类:

  • 底层存储:Core Data + SQLite(logs.sqlite),响应体等大块二进制数据存储在blobs/目录;
  • 能力边界:负责日志的写入、查询、导出、清理与销毁,同时暴露 Core Data 容器供上层 UI(PulseUI)和开发者直接访问;
  • 线程模型:LoggerStore被标记为@unchecked Sendable,写入全部在后台backgroundContext中进行,主线程通过viewContext读取;
  • 事件机制:内部通过PassthroughSubject<Event, Never>重传事件(LoggerStore.swift 第 38 行),RemoteLogger正是借此实现远程日志同步。

从 DocC 索引文档(LoggerStore-Extension.md)可以看到其 API 被分为 Initializers、Storing Logs、Accessing Logs、Exporting Logs、Managing the Store、Direct Database Access、Core Data Entities 与 Nested Types 等若干主题,本文即沿此脉络展开。

初始化 LoggerStore

构造函数签名与默认参数

LoggerStore通过以下指定构造器初始化:

public init(storeURL: URL, options: Options = [.create, .sweep], configuration: Configuration = .init()) throws

参数含义(LoggerStore.swift 第 107-168 行):

参数说明
storeURL指向一个目录(package),内部包含logs.sqlite数据库、blobs/二进制目录和manifest.json清单文件
options存储打开选项,默认[.create, .sweep]
configuration存储配置,默认Configuration()(256 MB 大小上限等)

初始化流程中值得注意的几个行为:

  1. 目录校验:如果未开启.create,则要求storeURL目录必须已存在,否则抛出LoggerStore.Error.fileDoesntExist;
  2. 版本迁移:读取manifest.json后若发现内部版本与当前版本(currentStoreVersion,见下文)不一致,会直接移除旧存储目录重建(源码注释说明"对日志数据而言,删除旧数据是最可靠安全的迁移方案");
  3. 会话自动创建:当开启.create且未开启.readonly、且configuration.isAutoStartingSession为真时,会自动写入当前Session实体(LoggerStore.swift 第 183-189 行);
  4. 自动清扫调度:开启.sweep且距离上次清扫超过sweepInterval(默认 1 小时)时,会在 10 秒后于后台执行sweep()(LoggerStore.swift 第 199-206 行)。

共享实例 shared

LoggerStore.shared是框架提供的默认单例(LoggerStore.swift 第 70-103 行):

public static var shared: LoggerStore { get set }
  • 默认实现位于URL.logs.appending(directory: "current.pulse"),以[.create, .sweep]打开;
  • 你可以替换它为自定义 store:替换时会自动注册为RemoteLogger的默认存储(若其尚未初始化);
  • 它同时也是NetworkLogger、URLSessionProxy等组件的默认落库目标。

Options 选项详解

Options是一个OptionSet(LoggerStore+Configuration.swift 第 12-55 行):

选项位值作用与注意事项
.create1 << 0目录不存在时创建 store;中间目录必须已存在
.sweep1 << 1达到大小上限时通过删除最旧的 message/blob 来压缩存储
.synchronous1 << 2所有写入立即同步落盘;会显著降低批量写入效率,官方不推荐常规使用,也不会加速远程日志
.readonly1 << 3只读打开;不执行清扫、禁止任何修改
.inMemory1 << 4完全不写盘,storeURL可以是任意路径(甚至/dev/null);.sweep仍生效
.unsafe1 << 5关闭 SQLite 持久化特性(WAL、fsync、共享锁)换取原始写入速度,面向一次性批量导入/模拟数据生成等场景;中途崩溃可能导致存储损坏

.unsafe在源码中会设置四条 pragma(LoggerStore.swift 第 251-260 行):journal_mode=OFF、synchronous=OFF、temp_store=MEMORY、locking_mode=EXCLUSIVE,这正是"牺牲崩溃安全换取吞吐"的底层依据。

Configuration 配置详解

Configuration决定存储的大小上限、写入频率与数据保留策略(LoggerStore+Configuration.swift 第 57-115 行):

public struct Configuration { public var sizeLimit: Int64 // 默认 256 MB(含数据库与 blobs) public var saveInterval: DispatchTimeInterval // 默认 300ms,消息落库的合并写入窗口 public var isStoringOnlyImageThumbnails: Bool // 默认 true,网络响应图片仅存缩略图 public var imageThumbnailOptions = ThumbnailOptions()// 缩略图参数 public var responseBodySizeLimit: Int = 8 * 1048576 // 默认 8 MB,请求/响应体上限 public var maxAge: TimeInterval = 14 * 86400 // 默认两周,过期消息自动删除 public var willHandleEvent: @Sendable (Event) -> Event? // 事件预处理钩子,返回 nil 则忽略 }
  • Configuration(sizeLimit:)是唯一对外公开的指定构造器,默认256 * 1_000_000字节;
  • 内部私有字段还包括:blobSizeLimit(大小上限的 70% 用于 blobs)、trimRatio = 0.7(清扫时裁剪比例)、sweepInterval = 3600秒、isBlobCompressionEnabled = true(blob 默认压缩存储)、inlineLimit = 16384(16 KB 以下 blob 直接内联进数据库);
  • willHandleEvent是一个强大的脱敏入口:你可以在事件落库前修改或丢弃它(例如过滤敏感信息)。注意configuration属性不是线程安全的,官方警告需在应用启动、发送任何日志之前完成修改(LoggerStore.swift 第 19-23 行)。

实际初始化示例

// 使用默认共享实例(绝大多数场景) let store = LoggerStore.shared // 自定义 store let url = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent("my-logs.pulse") var configuration = LoggerStore.Configuration() configuration.sizeLimit = 512 * 1_000_000 // 512 MB configuration.maxAge = 30 * 86400 // 保留 30 天 let custom = try LoggerStore(storeURL: url, options: [.create, .sweep], configuration: configuration)

写入日志:Storing Logs

存储消息 storeMessage

public func storeMessage( createdAt: Date? = nil, label: String, level: Level, message: String, metadata: [String: MetadataValue]? = nil, file: String = #file, function: String = #function, line: UInt = #line )

(签名见 LoggerStore.swift 第 292-312 行)

  • createdAt缺省时取当前时间;file/function/line默认自动捕获调用点,其中文件名在落库时只保留lastPathComponent;
  • metadata使用[String: MetadataValue],MetadataValue是枚举:.string(String)或.stringConvertible(CustomStringConvertible)(LoggerStore+Level.swift 第 6-11 行),内部通过unpack()转成[String: String]后以键值对编码存储;
  • Level与 SwiftLog 的Logger.Level兼容,共 7 级:trace(1) < debug(2) < info(3) < notice(4) < warning(5) < error(6) < critical(7)(LoggerStore+Level.swift 第 14-40 行)。

底层实现:storeMessage构造Event.messageStored事件,经handle(_:)处理后落入LoggerMessageEntity(LoggerStore.swift 第 368-381 行),并同步通过events发布给远程日志等观察者。

存储网络请求 storeRequest

public func storeRequest( _ request: URLRequest, response: URLResponse?, error: Swift.Error?, data: Data?, metrics: URLSessionTaskMetrics? = nil, label: String? = nil, taskDescription: String? = nil )

(LoggerStore.swift 第 318-341 行)

  • 一次性记录完整请求生命周期:请求体取request.httpBody ?? request.httpBodyStreamData(),响应体即data;
  • 源码注释提醒:如果需要增量更新任务(进度、分阶段指标),应使用NetworkLogger而非此方法——storeRequest是"一次完成"式的快捷 API;
  • 落库时handle(.networkTaskCompleted(...))会创建/复用NetworkTaskEntity,同步写入状态码、内容类型、请求/响应体 blob、URLSessionTaskMetrics转换后的交易记录与错误信息(LoggerStore.swift 第 410-507 行)。

查询日志:Accessing Logs

查询消息与任务

public func messages(sortDescriptors: [SortDescriptor<LoggerMessageEntity>] = [SortDescriptor(\.createdAt, order: .forward)], predicate: NSPredicate? = nil, context: NSManagedObjectContext? = nil) throws -> [LoggerMessageEntity] public func tasks(sortDescriptors: [SortDescriptor<NetworkTaskEntity>] = [SortDescriptor(\.createdAt, order: .forward)], predicate: NSPredicate? = nil, context: NSManagedObjectContext? = nil) throws -> [NetworkTaskEntity]

(LoggerStore.swift 第 779-804 行)

两个 API 默认按createdAt正序排列,默认在viewContext上执行(可通过context参数指定其他 context):

// 仅取普通日志消息(排除与网络任务关联的技术性消息) let messages = try store.messages(predicate: NSPredicate(format: "task == NULL")) // 取最近的网络请求任务 let tasks = try store.tasks(sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)], predicate: NSPredicate(format: "requestState == %d", NetworkTaskEntity.State.success.rawValue))

已废弃的旧 API

allMessages()与allTasks()已在 Pulse 5.1 中废弃(LoggerStore.swift 第 806-816 行),由messages(sortDescriptors:predicate:)/tasks(sortDescriptors:predicate:)取代,新代码请勿再使用。

导出与分享:Exporting Logs

export(to:options:)

public func export(to targetURL: URL, options: ExportOptions = .init()) async throws

(LoggerStore.swift 第 913-929 行)

  • 生成的副本具有.pulse扩展名,是一种归档格式(PulseDocument),将 SQLite 数据库与所有 blob 一并压缩进单个文档,便于通过 AirDrop、邮件等方式分享给开发者用 Pulse 桌面工具/控制台查看;
  • 目标目录必须已存在,且目标文件已存在时抛出LoggerStore.Error.fileAlreadyExists;
  • 归档内部结构:databaseblob(压缩后的 SQLite 副本)、blobs批量插入(每 100 个文件一组)、infoblob(存储Info元数据),见 LoggerStore.swift 第 1013-1067 行。

ExportOptions

public struct ExportOptions { public var predicate: NSPredicate? // 导出满足条件的 LoggerMessageEntity public var sessions: Set<UUID>? // 仅导出指定会话 public init(predicate: NSPredicate? = nil, sessions: Set<UUID>? = nil) }

(LoggerStore.swift 第 900-911 行)

当指定筛选条件时,导出会先在临时目录生成一份"包格式"副本,剔除不匹配的会话与消息,再打包为归档(LoggerStore.swift 第 996-1011 行)。注意导出副本会获得全新的storeId。

let url = FileManager.default.temporaryDirectory.appendingPathComponent("logs.pulse") try await store.export(to: url, options: ExportOptions(sessions: [sessionID]))

管理存储:Managing the Store

info():读取统计信息

public func info() async throws -> Info

(LoggerStore.swift 第 1208-1236 行)

线程安全,但严禁在backgroundContext队列内调用。返回的Info结构(LoggerStore+Info.swift 第 10-72 行)包含:

  • 标识:storeId、storeVersion(内部版本,非框架版本);
  • 时间:creationDate、modifiedDate(取自数据库文件属性);
  • 统计:messageCount(已剔除与网络任务关联的技术消息)、taskCount、blobCount、totalStoreSize、blobsSize、blobsDecompressedSize(blob 默认压缩存储,因此解压后体积更大);
  • 上下文:appInfo(bundle id、名称、版本、build、base64 应用图标)与deviceInfo(设备名、型号、系统名称与版本),平台差异见 LoggerStore+Info.swift 第 107-168 行。

removeAll / removeSessions / close / destroy

public func removeAll() // 删除全部消息、blob 与会话,重建当前会话 public func removeSessions(withIDs: Set<UUID>)// 删除指定会话及其关联消息 public func close() throws // 安全关闭数据库(移除 persistent store) public func destroy() throws // 关闭并删除 store 目录全部数据,之后不可再写入

(LoggerStore.swift 第 818-893 行)

removeAll通过NSBatchDeleteRequest批量删除三类实体后重建会话目录(LoggerStore.swift 第 857-867 行);destroy先destroyPersistentStore再删除整个storeURL。

getBlobData(forKey:)

public func getBlobData(forKey key: String) -> Data?

(LoggerStore.swift 第 710-713 行)

按 blob 的 SHA1 十六进制 key 读取原始二进制数据。由于 blob 默认压缩存储,此方法会自动解压;源码注释提醒在个别场景(如直接手改配置文件关闭压缩)下可能失效,最稳妥的方式是始终通过实体关系(如task.responseBody?.data)访问数据。

直接数据库访问:Direct Database Access

LoggerStore将 Core Data 容器与上下文直接暴露给调用方:

public let container: NSPersistentContainer // 底层容器 public var viewContext: NSManagedObjectContext // 主线程读上下文 public let backgroundContext: NSManagedObjectContext // 全部写操作的后台上下文 public func newBackgroundContext() -> NSManagedObjectContext // 额外后台上下文

(LoggerStore.swift 第 29-36、226-232 行)

  • viewContext设置了automaticallyMergesChangesFromParent = true与mergeByPropertyObjectTrump合并策略(LoggerStore.swift 第 210-213 行),因此后台写入会自动合并到主线程上下文;
  • 所有写操作统一走backgroundContext,配合默认 300ms 的saveInterval做合并写入:setNeedsSave()调度、flush()落库(LoggerStore.swift 第 725-765 行);开启.synchronous则每次写入立即performAndWait+ 保存;
  • 每个 context 的userInfo中都会挂载LoggerBlogDataStore,LoggerBlobHandleEntity.data依赖它才能解出 blob 数据(LoggerStore+Entities.swift 第 335-340 行)。
// 在后台线程做只读查询 let context = store.newBackgroundContext() let tasks = try context.performAndWait { try store.tasks(sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)], context: context) }

Core Data 实体模型

DocC 文档列出的 7 个实体全部在 LoggerStore+Entities.swift 中定义,其属性与关系在 LoggerStore+Model.swift 中程序化构建(8 个实体:除文档列出的 7 个外,progress实体对应NetworkTaskProgressEntity):

实体职责关键属性
LoggerSessionEntity会话id、createdAt、version、build
LoggerMessageEntity日志消息createdAt、level、text、label、file/function/line、rawMetadata、isPinned、session,task一对一关联
NetworkTaskEntity网络任务taskId、taskType、url/host/path、httpMethod、statusCode、requestState、startDate/duration、isFromCache/isMocked、errorCode/errorDomain、requestBody/responseBody(关联 blob)等
NetworkTaskProgressEntity任务进度completedUnitCount、totalUnitCount(惰性创建)
NetworkRequestEntity请求详情url、httpMethod、httpHeaders、超时与缓存策略、网络访问选项等
NetworkResponseEntity响应详情statusCode、httpHeaders
NetworkTransactionMetricsEntity单次交易指标完整时间线(fetch/domainLookup/connect/secure/request/response 各阶段起止)、字节数、TLS 协议与套件、地址端口、网络条件标记等
LoggerBlobHandleEntity请求/响应体句柄key(SHA1)、size、decompressedSize、inlineData、linkCount、contentType

几个值得注意的实现细节:

  1. Blob 的去重与内联策略:storeBlob以 SHA1 为 key 去重,重复数据仅递增linkCount;16 KB 以内的小 blob 内联进数据库(inlineData),更大的写入blobs/目录按 key 命名文件(LoggerStore.swift 第 643-684 行);
  2. 图片缩略图:超过 15 KB 的图片响应体在isStoringOnlyImageThumbnails开启时会被压缩为最大 512 px、质量 0.5 的缩略图再存储(LoggerStore.swift 第 509-522 行);
  3. 消息与任务合一:每个网络任务都自动关联一条技术性LoggerMessageEntity(默认label == "network"、line字段复用为任务状态存储以节省空间),因此messageCount需要减去taskCount才是纯日志消息数(LoggerStore.swift 第 554-565 行)。

嵌套类型与辅助结构

DocC 文档将以下嵌套类型列为独立主题(均位于LoggerStore命名空间下):

  • Error:fileDoesntExist、storeInvalid、unsupportedVersion(version:minimumSupportedVersion:)、fileAlreadyExists、unknownError(LoggerStore.swift 第 1265-1281 行);
  • Event:messageStored、networkTaskCreated、networkTaskProgressUpdated、networkTaskCompleted四类,全部Codable & Sendable,用于 store 间数据同步与远程日志(LoggerStore+Event.swift);
  • Info:见上文info()一节;
  • Level / MetadataValue / Metadata:日志级别与元数据值类型;
  • Session:Session(id:startDate:)结构,Session.current即当前会话(LoggerSession.swift);
  • Version:语义化版本号Version(major:minor:patch),可Codable、Comparable、LosslessStringConvertible,解析失败的字符串返回 nil(LoggerStore+Version.swift)。

内部版本常量(LoggerStore.swift 第 1307-1311 行):minimumSupportedVersion = 3.1.0、currentStoreVersion = 3.7.0、currentProtocolVersion = 4.0.0——它们决定了旧 store 的迁移与远程日志协议版本。

存储布局与数据流总览

一个.pulsestore 目录包含三个组成部分(常量定义于 LoggerStore.swift 第 1315-1318 行):

my-logs.pulse/ ├── logs.sqlite # Core Data 数据库(实体、内联 blob) ├── manifest.json # 清单:storeId、内部版本、上次清扫时间 └── blobs/ # 大于 16 KB 的二进制响应/请求体(以 SHA1 命名)

完整数据流(写入侧):

  1. 调用方(NetworkLogger、storeMessage等)构造Event;
  2. handle(_:)先经configuration.willHandleEvent过滤,再调度到backgroundContext执行_handle(LoggerStore.swift 第 344-366 行),同时通过events发布事件供 RemoteLogger 转发;
  3. 实体在backgroundContext中创建/更新,默认延迟 300ms 合并保存;
  4. 大 blob 写入blobs/目录,小 blob 内联;图片自动生成缩略图;
  5. 达到sizeLimit/maxAge时由sweep()在后台裁剪(LoggerStore.swift 第 1091-1099 行)。

结语

LoggerStore以"Core Data 实体 + SQLite 数据库 + blobs 文件目录 + manifest 清单"的复合结构,为 Pulse 提供了高性能、可压缩、可导出、可清理的日志持久化方案。理解它的初始化选项、写入/查询 API、导出能力与底层实体模型,是你在自有 App 中嵌入 Pulse、定制存储策略(大小上限、保留周期、图片缩略图、事件脱敏)以及对接远程日志功能的前提。需要进一步深入时,可以继续阅读:

  • NetworkLogger.swift:增量式记录网络请求的完整生命周期;
  • RemoteLogger.swift:基于Event的远程日志同步;
  • MockStore.swift:使用Options.unsafe进行批量模拟数据写入的参考实现。
  • 开发工具
  • 可观测性

【免费下载链接】Pulse

Network logger for Apple platforms

项目地址:https://gitcode.com/gh_mirrors/pul/Pulse
点击查看免费下载

相关推荐

上一篇:告别手动刷歌!网易云音乐自动打卡工具让你轻松冲击LV10等级
下一篇:5分钟快速上手TrollInstallerX:iOS设备终极安装指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询