Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
52e6976
Feat/log command improvements: add support for line count, follow mod…
kooksee May 31, 2026
2a7c413
Feat/log command improvements: remove grep filtering support and upda…
kooksee May 31, 2026
a571a52
Feat: add GitHub Actions workflow for ziginit release process
kooksee May 31, 2026
aca5d93
Feat: 修复 sleepNs 函数中的类型转换问题
kooksee May 31, 2026
0d3a726
Feat/log: 保留空行并增强行计数参数的错误处理
kooksee May 31, 2026
07d0bac
Feat/log: 实现 follow 模式的跨 chunk 行拼接,增强日志输出完整性
kooksee May 31, 2026
e8af566
Feat/log: 添加路径长度检查,防止越界写入
kooksee May 31, 2026
ff42f6f
Feat/log: 优化路径构建,增加路径长度检查并改进文件跟踪逻辑
kooksee May 31, 2026
37bc55f
Feat/log: 增强 sockaddrUn 和 sendCommand 函数的路径长度检查,防止缓冲区溢出
kooksee May 31, 2026
856374c
Feat/log: 增强命令处理和响应格式,确保 CLI 命令失败时正确退出并记录错误信息
kooksee Jun 1, 2026
91d88f1
Feat/log: 增强 CLI 错误处理,确保命令失败时返回详细错误信息并正确退出
kooksee Jun 1, 2026
990d9a7
更新 README.md,修正服务日志托管描述,调整模块行数信息,优化命令启动说明
kooksee Jun 1, 2026
9a453aa
Feat/log: 添加日志轮转功能,支持备份文件管理和流式复制
kooksee Jun 1, 2026
6852115
Feat/log: 将多个函数参数类型从 [*:0]const u8 修改为 c.CStr,以增强 C 字符串处理的安全性和一致性
kooksee Jun 2, 2026
67e22f5
Feat/log: 添加 gzip 可用性检查,确保日志轮转时备份安全有效
kooksee Jun 2, 2026
2920e6a
Feat/log: 优化日志输出处理,确保数据完整性并提升性能
kooksee Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions .github/workflows/release-ziginit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Release ziginit

on:
push:
tags:
- "ziginit/v*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: 0.16.0

- name: Cache Zig build artifacts
uses: actions/cache@v4
with:
path: |
~/.cache/zig
tools/ziginit/.zig-cache
key: ziginit-${{ runner.os }}-zig-${{ hashFiles('tools/ziginit/main.zig', 'tools/ziginit/build.zig') }}
restore-keys: |
ziginit-${{ runner.os }}-zig-

- name: Parse version and channel from tag
id: version
run: |
version="${GITHUB_REF#refs/tags/ziginit/}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
if [[ "$version" =~ -alpha ]]; then
echo "channel=dev" >> "$GITHUB_OUTPUT"
elif [[ "$version" =~ -beta ]]; then
echo "channel=beta" >> "$GITHUB_OUTPUT"
else
echo "channel=prod" >> "$GITHUB_OUTPUT"
fi

- name: Install UPX (production only)
if: steps.version.outputs.channel == 'prod'
run: sudo apt-get update && sudo apt-get install -y upx

- name: Cross-compile ziginit
run: |
cd tools/ziginit
channel="${{ steps.version.outputs.channel }}"
if [[ "$channel" == "prod" ]]; then
optimize="ReleaseSmall"
else
optimize="ReleaseSafe"
fi
echo "Channel: ${channel}, Optimize: ${optimize}"

declare -A targets=(
["x86_64-linux"]="linux_amd64"
["aarch64-linux"]="linux_arm64"
["arm-linux"]="linux_armv7"
["x86_64-macos"]="darwin_amd64"
["aarch64-macos"]="darwin_arm64"
)
mkdir -p ../../release-artifacts
for zig_target in "${!targets[@]}"; do
label="${targets[$zig_target]}"
echo "=== Building ziginit for ${zig_target} (${optimize}) ==="
zig build -Doptimize="${optimize}" -Dtarget="${zig_target}"
if [[ "$channel" == "prod" && "$zig_target" == *-linux ]]; then
upx --best --lzma zig-out/bin/ziginit || true
fi
archive="ziginit_${{ steps.version.outputs.version }}_${label}.tar.gz"
tar -czf "../../release-artifacts/${archive}" -C zig-out/bin ziginit
rm -rf zig-out
done

- name: Generate checksums
run: |
cd release-artifacts
sha256sum *.tar.gz > checksums.txt
cat checksums.txt

- name: Generate changelog (production only)
if: steps.version.outputs.channel == 'prod'
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: CHANGES.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.RELEASER_TOKEN }}
run: |
prerelease=""
notes_arg="--generate-notes"
if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then
prerelease="--prerelease"
else
notes_arg="--notes-file CHANGES.md"
fi
gh release create "${{ github.ref_name }}" \
--title "ziginit ${{ steps.version.outputs.version }}" \
$notes_arg \
$prerelease \
release-artifacts/*
21 changes: 21 additions & 0 deletions tools/ziginit/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub const MAX_LOG_TAIL_SIZE: usize = 4 * 1024 * 1024;
pub const LOG_MAX_SIZE: usize = 10 * 1024 * 1024;
// ZIGINIT_LOG_MAX_KB 环境变量允许的最大值
pub const LOG_MAX_SIZE_UPPER: usize = 100 * 1024 * 1024;
// `log -n` 默认显示行数
pub const DEFAULT_LOG_LINES: u32 = 100;
// `log -f` follow 模式轮询间隔
pub const LOG_FOLLOW_POLL_NS: i64 = 200_000_000; // 200ms

// ── 默认路径 ──

Expand Down Expand Up @@ -244,6 +248,15 @@ pub fn buildServicePaths(workdir: []const u8, name: []const u8) ServicePaths {
};
for (pairs) |p| {
const total = workdir.len + 1 + name.len + p.suffix.len;
if (total >= MAX_PATH) {
// 路径过长,截断到缓冲区上限,防止越界写入
const trunc = MAX_PATH - 1;
const wlen = @min(workdir.len, trunc);
@memcpy(p.dst[0..wlen], workdir[0..wlen]);
p.dst[wlen] = 0;
p.len.* = wlen;
continue;
}
Comment thread
kooksee marked this conversation as resolved.
@memcpy(p.dst[0..workdir.len], workdir);
p.dst[workdir.len] = '/';
@memcpy(p.dst[workdir.len + 1 ..][0..name.len], name);
Expand All @@ -265,6 +278,14 @@ pub fn buildDaemonPaths(workdir: []const u8) DaemonPaths {
};
for (pairs) |p| {
const total = workdir.len + p.suffix.len;
if (total >= MAX_PATH) {
const trunc = MAX_PATH - 1;
const wlen = @min(workdir.len, trunc);
@memcpy(p.dst[0..wlen], workdir[0..wlen]);
p.dst[wlen] = 0;
p.len.* = wlen;
continue;
}
Comment thread
kooksee marked this conversation as resolved.
@memcpy(p.dst[0..workdir.len], workdir);
@memcpy(p.dst[workdir.len..total], p.suffix);
p.dst[total] = 0;
Expand Down
131 changes: 131 additions & 0 deletions tools/ziginit/docs/log-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Log 子命令改进

## 1. 动机

原有 `log` 命令通过 Unix Socket 向 daemon 请求日志数据,daemon 读取日志文件后流式写回客户端 socket。这种架构存在以下限制:

- **无法 follow**:socket 是请求-响应模型,无法持续推送新数据
- **增加延迟**:CLI → daemon → 读文件 → 回传 socket → CLI,多一跳中转
- **daemon 耦合**:daemon 停机时无法查看日志
- **按字节截断**:尾部读 64KB 字节,不保证从完整行开始

## 2. 新架构

CLI 直接读取日志文件,绕过 daemon socket。

```mermaid
graph LR
subgraph "旧架构"
A1[CLI] -->|socket| B1[daemon]
B1 -->|read| C1[log files]
B1 -->|stream| A1
end

subgraph "新架构"
A2[CLI] -->|direct read| C2[log files]
end

style A1 fill:#fee2e2,stroke:#dc2626
style A2 fill:#dcfce7,stroke:#16a34a
```

日志文件位于 `{workdir}/{service}.log` 和 `{workdir}/daemon.log`,由 daemon 进程通过 `O_APPEND` 模式写入。CLI 直读不会与 daemon 写入冲突。

## 3. CLI 用法

```
ziginit log <name> # 显示最近 100 行
ziginit log -n <N> <name> # 显示最近 N 行
ziginit log -f <name> # 实时跟踪(类似 tail -f)
ziginit log daemon # 查看 daemon 日志
ziginit log --all # 所有服务 + daemon 日志(带 [name] 前缀)
ziginit log -f --all # follow 所有服务
```

过滤日志使用 Unix 管道:`ziginit log -f svc | grep error`

### 参数说明

| 参数 | 默认值 | 说明 |
| -------- | ------ | ------------------------------------- |
| `-n <N>` | 100 | 显示尾部 N 行 |
| `-f` | off | follow 模式,持续输出新行直到 Ctrl-C |
| `--all` | off | 显示所有服务 + daemon 日志 |
| `daemon` | — | 伪服务名,指向 `{workdir}/daemon.log` |

### `--all` 输出格式

每行加 `[name] ` 前缀(去掉了旧版的读取时间戳,因为它不是写入时间,会产生误导):

```
[daemon] daemon started, 2 services
[svc-a] svc-a-out
[svc-a] svc-a-err
[svc-b] listening on :8080
```

## 4. 实现细节

### 核心函数(journal.zig)

| 函数 | 职责 |
| -------------------------------- | ---------------------------------------------------------------------- |
| `tailLastLines(path, n, prefix)` | 从文件尾部读取,反向扫描定位最后 N 行,应用 prefix 后输出到 stdout |
| `followFiles(targets)` | 轮询文件变化(200ms 间隔),输出新增行,检测 `sigpending` 实现优雅退出 |
| `outputLines(data, prefix)` | 逐行处理:前缀添加 + stdout 写入(过滤交给外部 `grep` 管道) |

### FollowTarget 结构

```zig
pub const FollowTarget = struct {
path_buf: [MAX_PATH + 1]u8, // 自持路径数据,避免悬空指针
path_len: usize,
offset: usize, // 当前读取位置(= tailLastLines 返回的文件大小)
prefix: [MAX_NAME + 4]u8, // "[name] " 前缀
prefix_len: usize,
};
```

> **设计决策**:`FollowTarget` 拷贝路径到内部缓冲区而非存储外部指针。原因是 `ServicePaths` 是栈上临时值,循环迭代结束后失效。存裸指针会导致 Release 模式悬空指针崩溃(与 `ConfigDtoBuild` 同类问题)。

### Follow 模式实现

```mermaid
graph TD
A[tailLastLines 初始输出] --> B[blockTermInt 阻塞信号]
B --> C{sleep 200ms}
C --> D{sigpending?}
D -->|是| E[恢复信号 & 退出]
D -->|否| F[遍历 targets]
F --> G{fstat: 文件增长?}
G -->|否| C
G -->|文件缩小| H[offset = 0 重置]
H --> C
G -->|增长| I[seekTo + read 新数据]
I --> J[outputLines 过滤输出]
J --> K[更新 offset]
K --> C
```

- 使用 `fstat` 检测文件大小变化,而非 `poll()`(`poll` 对普通文件总是立即返回 `POLLIN`)
- 文件大小减小时判定为日志轮转,重置 offset 到 0 重新读取
- 信号检查使用 `sigpending`,不依赖信号 handler 全局状态

## 5. 删除的代码

| 位置 | 删除内容 |
| -------------- | -------------------------------------------------------------------- |
| `journal.zig` | `readServiceLogTail`、`readServiceLogTailPrefixed`(旧 socket 模式) |
| `server.zig` | `ACTION_LOG` 流式转发逻辑(serverLoop + handleCommand) |
| `protocol.zig` | `ACTION_LOG` 常量、`sendCommand` 中的 log 流式读取 |

## 6. 配置常量(config.zig)

```zig
pub const DEFAULT_LOG_LINES: u32 = 100; // -n 默认值
pub const LOG_FOLLOW_POLL_NS: i64 = 200_000_000; // follow 轮询间隔 200ms
```

沿用现有环境变量:
- `ZIGINIT_LOG_TAIL_KB`:控制 tail 读取的最大字节数(默认 64KB,上限 4MB)
- `ZIGINIT_LOG_MAX_KB`:控制日志轮转阈值(默认 10MB)
Loading