{"id":24902918,"url":"https://github.com/tangentw/pinenut","last_synced_at":"2025-10-16T14:30:59.738Z","repository":{"id":209257013,"uuid":"723601457","full_name":"TangentW/Pinenut","owner":"TangentW","description":"An extremely high performance logging system for clients (iOS, Android, Desktop), written in Rust. ","archived":false,"fork":false,"pushed_at":"2024-05-15T14:53:45.000Z","size":41731,"stargazers_count":18,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-01T16:17:54.934Z","etag":null,"topics":["ios","log","logger","rust","swift"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TangentW.png","metadata":{"files":{"readme":"README-CN.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-11-26T07:18:20.000Z","updated_at":"2024-11-10T18:09:16.000Z","dependencies_parsed_at":"2023-12-02T20:37:58.447Z","dependency_job_id":null,"html_url":"https://github.com/TangentW/Pinenut","commit_stats":null,"previous_names":["tangentw/pinenut"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TangentW%2FPinenut","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TangentW%2FPinenut/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TangentW%2FPinenut/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TangentW%2FPinenut/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TangentW","download_url":"https://codeload.github.com/TangentW/Pinenut/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236726282,"owners_count":19194889,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","log","logger","rust","swift"],"created_at":"2025-02-01T22:17:49.678Z","updated_at":"2025-10-16T14:30:51.646Z","avatar_url":"https://github.com/TangentW.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"assets/logo.svg\" height=200\"\u003e\n\n# Pinenut (松子) Log\n\n\u003cdiv align=\"center\"\u003e\n\u003cstrong\u003e\n\n[中文文档](./README-CN.md) ・ [English](./README.md) \n\n\u003c/strong\u003e\n\u003c/div\u003e\n\n一款性能极高的跨平台端上日志系统 (将支持 iOS、Android 和桌面端)，采用 Rust 编写。\n\n[![codecov](https://codecov.io/gh/TangentW/Pinenut/graph/badge.svg)](https://codecov.io/gh/TangentW/Pinenut)\n[![Crates.io](https://img.shields.io/crates/v/pinenut-log)](https://crates.io/crates/pinenut-log)\n[![Cocoapods Version](https://img.shields.io/cocoapods/v/Pinenut)](https://cocoapods.org/pods/Pinenut)\n\n\u003c/div\u003e\n\n## 系统概述\n\n![架构设计](assets/architecture.svg)\n\n### 压缩\n\nPinenut 支持日志流式压缩（边记录日志边压缩），其使用了 `Zstandard (即 zstd)` 这款高性能的压缩算法，在压缩率和压缩速度上拥有比较好的平衡。\n\n### 加密\n\nPinenut 在日志记录过程中采用了 `AES 128` 算法进行对称加密。为了避免对称加密的密钥直接嵌入客户端代码中，Pinenut 使用 `ECDH` 进行对称加密密钥的协商 (不选用 RSA，因为其密钥过长)。所以在 Logger 初始化时，我们不用写死对称加密密钥，只需拟定 ECDH 公钥即可。这样客户端代码就不会暴露对称加密密钥。\n\nPinenut 采用了 `secp256r1` 椭圆曲线进行 ECDH 对称加密密钥协商，你可以自行根据此曲线生成加密私钥和公钥，或使用 Pinenut 内建的命令行工具生成：`pinenut-cli`（具体使用请看下文）。\n\n### 缓存\n\n为了降低 IO 频次，Pinenut 在进行日志文件写入前会对数据进行缓存。考虑到客户端程序可能会意外退出（如程序崩溃闪退），Pinenut 使用了 `mmap` 作为缓存支持。当程序意外退出时，操作系统仍能帮助持久化缓存数据，在下一次 Logger 初始化时，缓存数据将自动读取并回写到日志文件中。\n\n不仅如此，为了提升缓存的读写性能，Pinenut 还实现了`双缓冲 (double-buffering)`系统，也避免了异步的 IO 对当前日志记录所在线程造成性能影响。\n\n### 日志提取\n\n使用 Pinenut，我们不需要为了提取日志而自行遍历检索目录中所有的日志文件，其提供了便利的日志提取能力，并支持按分钟级别粒度的时间范围提取（如提取 13:45 - 14:13 期间的日志）。\n\n### 日志解析\n\nPinenut 日志文件的内容是经过编码、压缩和加密过后的特殊二进制序列，我们可以使用 Pinenut 提供的解析能力来对日志文件进行解析（解密、解压、解码）。\n\n## 依赖集成\n\nPinenut 目前提供了 `Swift` 和 `Rust` 语言的 API，在后续也会支持 `Kotlin` 语言。\n\n### Swift Package Manager\n\n```swift\n.package(url: \"https://github.com/TangentW/Pinenut.git\", from: \"0.0.1\")\n```\n\n### CocoaPods\n\n```ruby\npod 'Pinenut'\n```\n\n### Rust Cargo\n\n```toml\n[dependencies]\npinenut-log = 0.0.1\n```\n\n## 基本使用\n\n无论在哪种语言上，Pinenut 的 API 通常都是十分相似的。\n\n### 初始化 Logger\n\nPinenut 使用 `Logger` 实例进行日志记录。在初始化 `Logger` 前，我们需要先拟定其标识符 (identifier) 以及日志文件所存放的目录路径 (directory)，构造出 `Domain` 结构。\n\n`Logger` 支持若干的自定义配置，我们可以通过显式指定 `Config` 结构来进行定制。更多详情请查看 Pinenut 的 API 文档。\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eSwift Code\u003c/b\u003e\u003c/summary\u003e\n\n```swift\nlet domain = Domain(identifier: \"MyApp\", directory: \"/path/to/dir\")\nlet config = Config(key: \"Public Key Base64\", compressionLevel: 10)\nlet logger = Logger(domain: domain, config: config)\n```\n\n\u003c/details\u003e\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eRust Code\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nlet domain = Domain::new(\"MyApp\".into(), \"/path/to/dir\".into());\nlet config = Config::new().key_str(Some(\"Public Key Base64\")).compression_level(10);\nlet logger = Logger::new(domain, config);\n```\n\n\u003c/details\u003e\n\n### 记录日志\n\n在 Pinenut 中，`Record` 代表单条日志的数据模型，我们可以构造出 Record 实例然后调用 Logger 的 `log` 方法进行日志记录。\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eSwift Code\u003c/b\u003e\u003c/summary\u003e\n\n`Swift` API 也提供了更为便利的日志记录方式。\n\n```swift\nlogger.log(.info, \"Hello World\")\nlogger.log(.debug, tag: \"MyModule\", \"Debug Message\")\n\n// `Logger` 提供了针对不同日志等级的 API\nlogger.info(\"Hello World\")\nlogger.error(\"Error message\")\nlogger.debug(tag: \"MyModule\", \"Debug message\")\n\n// 将当前被缓存的全部 Record 异步 flush 出去\nlogger.flush()\n```\n\n\u003c/details\u003e\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eRust Code\u003c/b\u003e\u003c/summary\u003e\n\n在 `Rust` API 中，Record 可以通过构建器模式 (Builder pattern) 进行构造：\n\n```rust\n// 构造 `Meta` 和 `Record` 数据模型\nlet meta = Meta::builder().level(Level::Info).build();\nlet record = Record::builder().meta(meta).content(\"Hello World\").build();\nlogger.log(\u0026record);\n\n// 将当前被缓存的全部 Record 异步 flush 出去\nlogger.flush();\n```\n\n\u003c/details\u003e\n\n更多详情请查看 Pinenut 的 API 文档。\n\n### 提取日志\n\n我们可以通过调用 `extract` 方法来将特定时间范围内的日志提取出来，并自动写入到指定的文件中，而提取的时间是可以精确到分钟级别粒度的。\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eSwift Code\u003c/b\u003e\u003c/summary\u003e\n\n```swift\nlet domain = Domain(identifier: \"MyApp\", directory: \"/path/to/dir\")\nlet range = Date(timeIntervalSinceNow: -1800)...Date()\n\ndo {\n    try Logger.extract(domain: domain, timeRange: range, destPath: \"/path/to/destination\")\n} catch {\n    print(\"Error: \\(error)\")\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eRust Code\u003c/b\u003e\u003c/summary\u003e\n\n```rust\nlet domain = Domain::new(\"MyApp\".into(), \"/path/to/dir\".into());\nlet now = chrono::Utc::now();\nlet range = now.sub(Duration::from_secs(1800))..=now;\n\nif let Err(err) = pinenut_log::extract(domain, range, \"/path/to/destination\") {\n    println!(\"Error: {err}\");\n}\n```\n\n\u003c/details\u003e\n\n注意：提取出来的日志文件其内容仍然是经过编码、压缩和加密的二进制序列，我们需要进行解析才能看到易读的日志文本内容。\n\n### 解析日志\n\n使用带 `parse` 前缀的相关 API 可以进行日志的解析，得到易读的日志文本内容。你甚至可以自定义自己的文本格式化逻辑，详情请查看 Pinenut 的 API 文档。\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eSwift Code\u003c/b\u003e\u003c/summary\u003e\n\n```swift\ndo {\n    try Logger.parse(path: path, to: dest, secretKey: secretKey)\n} catch {\n    print(\"Error: \\(error)\")\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails open\u003e\n\u003csummary\u003e\u003cb\u003eRust Code\u003c/b\u003e\u003c/summary\u003e\n\n```rust\n// 指定 `DefaultFormater` 作为日志文本格式化器\nif let Err(err) = pinenut_log::parse_to_file(\u0026path, \u0026output, secret_key, DefaultFormatter) {\n    println!(\"Error: {err}\");\n}\n```\n\n\u003c/details\u003e\n\n或者可以直接使用内建的命令行工具 `pinenut-cli` 进行日志解析：\n\n```\n$ pinenut-cli parse ./my_log.pine \\\n    --output ./plain.log          \\\n    --secret-key XXXXXXXXXXX\n```\n\n### 生成加密密钥\n\n在记录日志或解析日志前，你需要准备好一对公钥和私钥。公钥用于初始化 Logger，而私钥则用于自己解析日志。或者你可以不指定公钥私钥，从而不进行日志的加密。\n\n你可以自行生成 `secp256r1` 椭圆曲线的公钥私钥对，或者可以直接使用内建的命令行工具 `pinenut-cli` 生成：\n\n```\n$ pinenut-cli gen-keys\n```\n\n## 性能验证\n\nPinenut 的部分设计灵感来源于 Xlog，这里也对其两者进行性能的比对。\n\n为了避免两者所使用的压缩库差异导致结果数据失去可比性，考虑到它们都支持 zstd，所以性能验证将在两者都使用同一版本的 zstd 库 (v1.5.5) 且压缩等级为 10 的前提下进行。\n\n*验证运行在 iPhone 12 上，系统为 iOS 15.5。*\n\n![Benchmark](assets/benchmark.svg)\n\n| 日志库  | 每秒能处理的日志条目数 (速度) |\n| :-----: | :---------------------------: |\n| Pinenut |            447460             |\n|  Xlog   |            317473             |\n\n## 待办项\n\n- [ ] 日志等级过滤\n- [ ] 提供 Kotlin API\n\n## 开源许可\n\nPinenut 基于 MIT 开源许可协议，更多详情请查看 LICENSE 文件。 \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftangentw%2Fpinenut","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftangentw%2Fpinenut","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftangentw%2Fpinenut/lists"}