- 开发工具
- 可观测性
【免费下载链接】Pulse
Network logger for Apple platforms
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 大小上限等) |
初始化流程中值得注意的几个行为:
- 目录校验:如果未开启
.create,则要求storeURL目录必须已存在,否则抛出LoggerStore.Error.fileDoesntExist; - 版本迁移:读取
manifest.json后若发现内部版本与当前版本(currentStoreVersion,见下文)不一致,会直接移除旧存储目录重建(源码注释说明"对日志数据而言,删除旧数据是最可靠安全的迁移方案"); - 会话自动创建:当开启
.create且未开启.readonly、且configuration.isAutoStartingSession为真时,会自动写入当前Session实体(LoggerStore.swift 第 183-189 行); - 自动清扫调度:开启
.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 行):
| 选项 | 位值 | 作用与注意事项 |
|---|---|---|
.create | 1 << 0 | 目录不存在时创建 store;中间目录必须已存在 |
.sweep | 1 << 1 | 达到大小上限时通过删除最旧的 message/blob 来压缩存储 |
.synchronous | 1 << 2 | 所有写入立即同步落盘;会显著降低批量写入效率,官方不推荐常规使用,也不会加速远程日志 |
.readonly | 1 << 3 | 只读打开;不执行清扫、禁止任何修改 |
.inMemory | 1 << 4 | 完全不写盘,storeURL可以是任意路径(甚至/dev/null);.sweep仍生效 |
.unsafe | 1 << 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 |
几个值得注意的实现细节:
- Blob 的去重与内联策略:
storeBlob以 SHA1 为 key 去重,重复数据仅递增linkCount;16 KB 以内的小 blob 内联进数据库(inlineData),更大的写入blobs/目录按 key 命名文件(LoggerStore.swift 第 643-684 行); - 图片缩略图:超过 15 KB 的图片响应体在
isStoringOnlyImageThumbnails开启时会被压缩为最大 512 px、质量 0.5 的缩略图再存储(LoggerStore.swift 第 509-522 行); - 消息与任务合一:每个网络任务都自动关联一条技术性
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 命名)完整数据流(写入侧):
- 调用方(
NetworkLogger、storeMessage等)构造Event; handle(_:)先经configuration.willHandleEvent过滤,再调度到backgroundContext执行_handle(LoggerStore.swift 第 344-366 行),同时通过events发布事件供 RemoteLogger 转发;- 实体在
backgroundContext中创建/更新,默认延迟 300ms 合并保存; - 大 blob 写入
blobs/目录,小 blob 内联;图片自动生成缩略图; - 达到
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
相关推荐
Pulse 进阶配置指南:LoggerStore 存储调优、日志导出与网络调试
Pulse 进阶配置指南:LoggerStore 存储调优、日志导出与网络调试 本文是 Pulse 官方文档 NextSteps https://link.gi
开发工具可观测性highlight.io 日志存储引擎剖析:基于 ClickHouse 的结构化日志注入与键值检索实战
highlight.io 日志存储引擎剖析:基于 ClickHouse 的结构化日志注入与键值检索实战 本指南围绕 highlight.io 开源全栈监控平台在
可观测性后端5个理由告诉你为什么Montserrat字体是现代设计的完美选择
5个理由告诉你为什么Montserrat字体是现代设计的完美选择 Montserrat字体是一款完全免费开源的几何无衬线字体家族,以其优雅的现代设计和丰富的字重
设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考