diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1810e36..943069c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -25,12 +25,36 @@ "command": "./target/debug/secrets help add", "dependsOn": "build" }, + { + "label": "cli: help config", + "type": "shell", + "command": "./target/debug/secrets help config", + "dependsOn": "build" + }, + { + "label": "cli: config path", + "type": "shell", + "command": "./target/debug/secrets config path", + "dependsOn": "build" + }, + { + "label": "cli: config show", + "type": "shell", + "command": "./target/debug/secrets config show", + "dependsOn": "build" + }, { "label": "test: search all", "type": "shell", "command": "./target/debug/secrets search", "dependsOn": "build" }, + { + "label": "test: search all (verbose)", + "type": "shell", + "command": "./target/debug/secrets --verbose search", + "dependsOn": "build" + }, { "label": "test: search by namespace (refining)", "type": "shell", @@ -82,7 +106,7 @@ { "label": "test: search with secrets revealed", "type": "shell", - "command": "./target/debug/secrets search -n refining --kind service --name gitea --show-secrets", + "command": "./target/debug/secrets search -n refining --kind service --show-secrets", "dependsOn": "build" }, { @@ -97,6 +121,24 @@ "command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search ---' && ./target/debug/secrets search -n test --show-secrets && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test", "dependsOn": "build" }, + { + "label": "test: add + delete roundtrip (verbose)", + "type": "shell", + "command": "echo '--- add (verbose) ---' && ./target/debug/secrets --verbose add -n test --kind demo --name roundtrip-verbose --tag test -m foo=bar -s password=secret123 && echo '--- delete (verbose) ---' && ./target/debug/secrets --verbose delete -n test --kind demo --name roundtrip-verbose", + "dependsOn": "build" + }, + { + "label": "test: update roundtrip", + "type": "shell", + "command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name update-test --tag v1 -m env=staging && echo '--- update ---' && ./target/debug/secrets update -n test --kind demo --name update-test --add-tag v2 --remove-tag v1 -m env=production && echo '--- verify ---' && ./target/debug/secrets search -n test --kind demo && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind demo --name update-test", + "dependsOn": "build" + }, + { + "label": "test: audit log", + "type": "shell", + "command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name audit-test -m foo=bar -s key=val && echo '--- update ---' && ./target/debug/secrets update -n test --kind demo --name audit-test -m foo=baz && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name audit-test && echo '--- audit log (last 5) ---' && psql $DATABASE_URL -c \"SELECT action, namespace, kind, name, actor, detail, created_at FROM audit_log ORDER BY created_at DESC LIMIT 5;\"", + "dependsOn": "build" + }, { "label": "test: add with file secret", "type": "shell", diff --git a/AGENTS.md b/AGENTS.md index 7d1f8c9..e70ae54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,19 +7,22 @@ ``` secrets/ src/ - main.rs # CLI 入口,clap 命令定义,auto-migrate - db.rs # PgPool 创建 + 建表/索引(幂等) + main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数 + config.rs # 配置读写:~/.config/secrets/config.toml(database_url) + db.rs # PgPool 创建 + 建表/索引(幂等,含 audit_log) models.rs # Secret 结构体(sqlx::FromRow + serde) + audit.rs # 审计写入:向 audit_log 表记录所有写操作 commands/ add.rs # add 命令:upsert,支持 --meta key=value / --secret key=@file + config.rs # config 命令:set-db / show / path(持久化 database_url) search.rs # search 命令:多条件动态查询 delete.rs # delete 命令 + update.rs # update 命令:增量更新(合并 tags/metadata/encrypted) scripts/ seed-data.sh # 从 refining/ricnsmart config.toml 导入全量数据 .gitea/workflows/ secrets.yml # CI:fmt + clippy + musl 构建 + Release 上传 + 飞书通知 - .vscode/tasks.json # 本地测试任务(build / search / add+delete roundtrip 等) - .env # DATABASE_URL(gitignore,不提交) + .vscode/tasks.json # 本地测试任务(build / config / search / add+delete / update / audit 等) ``` ## 数据库 @@ -27,7 +30,7 @@ secrets/ - **Host**: `47.117.131.22:5432`(阿里云上海 ECS,PostgreSQL 18 with io_uring) - **Database**: `secrets` - **连接串**: `postgres://postgres:@47.117.131.22:5432/secrets` -- **表**: 单张 `secrets` 表,首次连接自动建表(auto-migrate) +- **表**: `secrets`(主表)+ `audit_log`(审计表),首次连接自动建表(auto-migrate) ### 表结构 @@ -46,6 +49,21 @@ secrets ( ) ``` +### audit_log 表结构 + +```sql +audit_log ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + action VARCHAR(32) NOT NULL, -- 'add' | 'update' | 'delete' + namespace VARCHAR(64) NOT NULL, + kind VARCHAR(64) NOT NULL, + name VARCHAR(256) NOT NULL, + detail JSONB NOT NULL DEFAULT '{}', -- 变更摘要(tags/meta keys/secret keys,不含 value) + actor VARCHAR(128) NOT NULL DEFAULT '', -- 操作者($USER 环境变量) + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) +``` + ### 字段职责划分 | 字段 | 存什么 | 示例 | @@ -57,6 +75,18 @@ secrets ( | `metadata` | 明文非敏感信息 | `{"ip":"47.243.154.187","desc":"Grafana","domains":["..."]}` | | `encrypted` | 敏感凭据(MVP 阶段明文存储,后续对 value 加密) | `{"ssh_key":"-----BEGIN...","password":"..."}` | +## 数据库配置 + +首次使用需显式配置数据库连接,设置一次后在该设备上持久生效: + +```bash +secrets config set-db "postgres://postgres:@47.117.131.22:5432/secrets" +secrets config show # 查看当前配置(密码脱敏) +secrets config path # 打印配置文件路径 +``` + +配置文件:`~/.config/secrets/config.toml`,权限 0600。`--db-url` 参数可一次性覆盖。 + ## CLI 命令 ```bash @@ -77,6 +107,11 @@ secrets add -n --kind --name \ secrets search [-n ] [--kind ] [--tag ] [-q ] [--show-secrets] # -q 匹配范围:name、namespace、kind、metadata 全文内容、tags +# 开启 debug 级别日志(全局参数,位于子命令之前) +secrets --verbose +secrets -v +# 或通过环境变量控制:RUST_LOG=secrets=trace secrets search + # 增量更新已有记录(合并语义,记录不存在则报错) secrets update -n --kind --name \ [--add-tag ]... # 添加标签(不影响已有标签) @@ -88,6 +123,11 @@ secrets update -n --kind --name \ # 删除 secrets delete -n --kind --name + +# 配置(持久化 database_url,设置一次即可) +secrets config set-db +secrets config show +secrets config path ``` ### 示例 @@ -134,7 +174,9 @@ secrets update -n refining --kind service --name mqtt \ - 异步:全程 `tokio`,数据库操作 `sqlx` async - SQL:使用 `sqlx::query` / `sqlx::query_as` 绑定参数,禁止字符串拼接(搜索的动态 WHERE 子句除外,需使用参数绑定 `$1/$2`) - 新增 `kind` 类型时:只需在 `add` 调用时传入,无需改代码 -- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query +- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query,`-v`=verbose +- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!` +- 审计:`add`/`update`/`delete` 成功后调用 `audit::log()`,写入 `audit_log` 表;失败只 warn 不中断 ## 提交前检查(必须全部通过) @@ -181,4 +223,7 @@ cargo fmt -- --check && cargo clippy -- -D warnings && cargo test | 变量 | 说明 | |------|------| -| `DATABASE_URL` | PostgreSQL 连接串,优先级高于 `--db-url` 参数 | +| `RUST_LOG` | 日志级别,如 `secrets=debug`、`secrets=trace`(默认 warn) | +| `USER` | 审计日志 actor 字段来源,Shell 自动设置,通常无需手动配置 | + +数据库连接通过 `secrets config set-db` 持久化到 `~/.config/secrets/config.toml`,不支持环境变量。 diff --git a/Cargo.lock b/Cargo.lock index 45964cd..05b300c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -828,6 +837,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "md-5" version = "0.10.6" @@ -855,6 +873,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -1113,6 +1140,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -1212,6 +1256,8 @@ dependencies = [ "sqlx", "tokio", "toml", + "tracing", + "tracing-subscriber", "uuid", ] @@ -1307,6 +1353,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1646,6 +1701,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1779,6 +1843,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1862,6 +1956,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index c8863eb..7089151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,6 @@ serde_json = "1.0.149" sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] } tokio = { version = "1.50.0", features = ["full"] } toml = "1.0.7" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.22.0", features = ["serde", "v4"] } diff --git a/README.md b/README.md index 756d0c8..06feea7 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,10 @@ cargo build --release # 或从 Release 页面下载预编译二进制 ``` -配置数据库连接: +配置数据库连接(首次使用需执行一次,之后在该设备上持久生效): ```bash -export DATABASE_URL=postgres://postgres:@:5432/secrets -# 或在项目根目录创建 .env 文件写入上述变量 +secrets config set-db "postgres://postgres:@:5432/secrets" ``` ## 使用 @@ -30,6 +29,7 @@ secrets --help secrets -h # 查看子命令帮助 +secrets help config secrets help add secrets help search secrets help delete @@ -54,6 +54,13 @@ secrets search --tag hongkong secrets search -q mqtt # 关键词匹配 name / metadata / tags secrets search -n refining --kind service --name gitea --show-secrets +# 开启 debug 级别日志(--verbose / -v,全局参数) +secrets --verbose search -q mqtt +secrets -v add -n refining --kind service --name gitea -m url=xxx -s token=yyy + +# 或通过环境变量精细控制日志级别 +RUST_LOG=secrets=trace secrets search + # 增量更新已有记录(合并语义,记录不存在则报错) secrets update -n refining --kind server --name my-server -m ip=10.0.0.1 secrets update -n refining --kind service --name gitea --add-tag production -s token= @@ -65,7 +72,7 @@ secrets delete -n refining --kind server --name my-server ## 数据模型 -单张 `secrets` 表,首次连接自动建表。 +单张 `secrets` 表,首次连接自动建表;同时自动创建 `audit_log` 表,记录所有写操作。 | 字段 | 说明 | |------|------| @@ -78,15 +85,30 @@ secrets delete -n refining --kind server --name my-server `-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `encrypted`,`value=@file` 从文件读取内容。 +## 审计日志 + +`add`、`update`、`delete` 操作成功后自动向 `audit_log` 表写入一条记录,包含操作类型、操作对象和变更摘要(不含 secret 值)。操作者取自 `$USER` 环境变量。 + +```sql +-- 查看最近 20 条审计记录 +SELECT action, namespace, kind, name, actor, detail, created_at +FROM audit_log +ORDER BY created_at DESC +LIMIT 20; +``` + ## 项目结构 ``` src/ main.rs # CLI 入口(clap) - db.rs # 连接池 + auto-migrate + config.rs # 配置读写(~/.config/secrets/config.toml) + db.rs # 连接池 + auto-migrate(secrets + audit_log) models.rs # Secret 结构体 + audit.rs # 审计日志写入(audit_log 表) commands/ add.rs # upsert + config.rs # config set-db/show/path search.rs # 多条件查询 delete.rs # 删除 update.rs # 增量更新(合并 tags/metadata/encrypted) diff --git a/src/audit.rs b/src/audit.rs new file mode 100644 index 0000000..fab1008 --- /dev/null +++ b/src/audit.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use serde_json::Value; +use sqlx::PgPool; + +/// Write an audit entry for a write operation. Failures are logged as warnings +/// and do not interrupt the main flow. +pub async fn log( + pool: &PgPool, + action: &str, + namespace: &str, + kind: &str, + name: &str, + detail: Value, +) { + let actor = std::env::var("USER").unwrap_or_default(); + let result: Result<_, sqlx::Error> = sqlx::query( + "INSERT INTO audit_log (action, namespace, kind, name, detail, actor) \ + VALUES ($1, $2, $3, $4, $5, $6)", + ) + .bind(action) + .bind(namespace) + .bind(kind) + .bind(name) + .bind(&detail) + .bind(&actor) + .execute(pool) + .await; + + if let Err(e) = result { + tracing::warn!(error = %e, "failed to write audit log"); + } else { + tracing::debug!(action, namespace, kind, name, actor, "audit logged"); + } +} diff --git a/src/commands/add.rs b/src/commands/add.rs index 1a7971c..57cc681 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use serde_json::{Map, Value}; +use serde_json::{Map, Value, json}; use sqlx::PgPool; use std::fs; @@ -43,6 +43,8 @@ pub async fn run( let metadata = build_json(meta_entries)?; let encrypted = build_json(secret_entries)?; + tracing::debug!(namespace, kind, name, "upserting record"); + sqlx::query( r#" INSERT INTO secrets (namespace, kind, name, tags, metadata, encrypted, updated_at) @@ -64,23 +66,38 @@ pub async fn run( .execute(pool) .await?; + let meta_keys: Vec<&str> = meta_entries + .iter() + .filter_map(|s| s.split_once('=').map(|(k, _)| k)) + .collect(); + let secret_keys: Vec<&str> = secret_entries + .iter() + .filter_map(|s| s.split_once('=').map(|(k, _)| k)) + .collect(); + + crate::audit::log( + pool, + "add", + namespace, + kind, + name, + json!({ + "tags": tags, + "meta_keys": meta_keys, + "secret_keys": secret_keys, + }), + ) + .await; + println!("Added: [{}/{}] {}", namespace, kind, name); if !tags.is_empty() { println!(" tags: {}", tags.join(", ")); } if !meta_entries.is_empty() { - let keys: Vec<&str> = meta_entries - .iter() - .filter_map(|s| s.split_once('=').map(|(k, _)| k)) - .collect(); - println!(" metadata: {}", keys.join(", ")); + println!(" metadata: {}", meta_keys.join(", ")); } if !secret_entries.is_empty() { - let keys: Vec<&str> = secret_entries - .iter() - .filter_map(|s| s.split_once('=').map(|(k, _)| k)) - .collect(); - println!(" secrets: {}", keys.join(", ")); + println!(" secrets: {}", secret_keys.join(", ")); } Ok(()) diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 4ff049e..9d7c8ba 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,7 +1,10 @@ use anyhow::Result; +use serde_json::json; use sqlx::PgPool; pub async fn run(pool: &PgPool, namespace: &str, kind: &str, name: &str) -> Result<()> { + tracing::debug!(namespace, kind, name, "deleting record"); + let result = sqlx::query("DELETE FROM secrets WHERE namespace = $1 AND kind = $2 AND name = $3") .bind(namespace) @@ -11,8 +14,10 @@ pub async fn run(pool: &PgPool, namespace: &str, kind: &str, name: &str) -> Resu .await?; if result.rows_affected() == 0 { + tracing::warn!(namespace, kind, name, "record not found for deletion"); println!("Not found: [{}/{}] {}", namespace, kind, name); } else { + crate::audit::log(pool, "delete", namespace, kind, name, json!({})).await; println!("Deleted: [{}/{}] {}", namespace, kind, name); } Ok(()) diff --git a/src/commands/search.rs b/src/commands/search.rs index 42f56c1..d764361 100644 --- a/src/commands/search.rs +++ b/src/commands/search.rs @@ -44,6 +44,8 @@ pub async fn run( where_clause ); + tracing::debug!(sql, "executing search query"); + let mut q = sqlx::query_as::<_, Secret>(&sql); if let Some(v) = namespace { q = q.bind(v); diff --git a/src/commands/update.rs b/src/commands/update.rs index 94c02d6..fe216e3 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use serde_json::{Map, Value}; +use serde_json::{Map, Value, json}; use sqlx::{FromRow, PgPool}; use uuid::Uuid; @@ -85,6 +85,13 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> { } let encrypted = Value::Object(enc_map); + tracing::debug!( + namespace = args.namespace, + kind = args.kind, + name = args.name, + "updating record" + ); + sqlx::query( r#" UPDATE secrets @@ -99,6 +106,34 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> { .execute(pool) .await?; + let meta_keys: Vec<&str> = args + .meta_entries + .iter() + .filter_map(|s| s.split_once('=').map(|(k, _)| k)) + .collect(); + let secret_keys: Vec<&str> = args + .secret_entries + .iter() + .filter_map(|s| s.split_once('=').map(|(k, _)| k)) + .collect(); + + crate::audit::log( + pool, + "update", + args.namespace, + args.kind, + args.name, + json!({ + "add_tags": args.add_tags, + "remove_tags": args.remove_tags, + "meta_keys": meta_keys, + "remove_meta": args.remove_meta, + "secret_keys": secret_keys, + "remove_secrets": args.remove_secrets, + }), + ) + .await; + println!("Updated: [{}/{}] {}", args.namespace, args.kind, args.name); if !args.add_tags.is_empty() { diff --git a/src/db.rs b/src/db.rs index a8a7242..8f96570 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,14 +3,17 @@ use sqlx::PgPool; use sqlx::postgres::PgPoolOptions; pub async fn create_pool(database_url: &str) -> Result { + tracing::debug!("connecting to database"); let pool = PgPoolOptions::new() .max_connections(5) .connect(database_url) .await?; + tracing::debug!("database connection established"); Ok(pool) } pub async fn migrate(pool: &PgPool) -> Result<()> { + tracing::debug!("running migrations"); sqlx::raw_sql( r#" CREATE TABLE IF NOT EXISTS secrets ( @@ -36,9 +39,24 @@ pub async fn migrate(pool: &PgPool) -> Result<()> { CREATE INDEX IF NOT EXISTS idx_secrets_kind ON secrets(kind); CREATE INDEX IF NOT EXISTS idx_secrets_tags ON secrets USING GIN(tags); CREATE INDEX IF NOT EXISTS idx_secrets_metadata ON secrets USING GIN(metadata jsonb_path_ops); + + CREATE TABLE IF NOT EXISTS audit_log ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + action VARCHAR(32) NOT NULL, + namespace VARCHAR(64) NOT NULL, + kind VARCHAR(64) NOT NULL, + name VARCHAR(256) NOT NULL, + detail JSONB NOT NULL DEFAULT '{}', + actor VARCHAR(128) NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + + CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_audit_log_ns_kind ON audit_log(namespace, kind); "#, ) .execute(pool) .await?; + tracing::debug!("migrations complete"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index b0688a9..e860f25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod audit; mod commands; mod config; mod db; @@ -5,6 +6,7 @@ mod models; use anyhow::Result; use clap::{Parser, Subcommand}; +use tracing_subscriber::EnvFilter; #[derive(Parser)] #[command( @@ -17,6 +19,10 @@ struct Cli { #[arg(long, global = true, default_value = "")] db_url: String, + /// Enable verbose debug output + #[arg(long, short, global = true)] + verbose: bool, + #[command(subcommand)] command: Commands, } @@ -132,6 +138,16 @@ enum ConfigAction { async fn main() -> Result<()> { let cli = Cli::parse(); + let filter = if cli.verbose { + EnvFilter::new("secrets=debug") + } else { + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("secrets=warn")) + }; + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_target(false) + .init(); + // config 子命令不需要数据库连接,提前处理 if let Commands::Config { action } = &cli.command { let cmd_action = match action { @@ -157,6 +173,8 @@ async fn main() -> Result<()> { meta, secrets, } => { + let _span = + tracing::info_span!("cmd", command = "add", %namespace, %kind, %name).entered(); commands::add::run(&pool, namespace, kind, name, tags, meta, secrets).await?; } Commands::Search { @@ -166,6 +184,7 @@ async fn main() -> Result<()> { query, show_secrets, } => { + let _span = tracing::info_span!("cmd", command = "search").entered(); commands::search::run( &pool, namespace.as_deref(), @@ -181,6 +200,8 @@ async fn main() -> Result<()> { kind, name, } => { + let _span = + tracing::info_span!("cmd", command = "delete", %namespace, %kind, %name).entered(); commands::delete::run(&pool, namespace, kind, name).await?; } Commands::Update { @@ -194,6 +215,8 @@ async fn main() -> Result<()> { secrets, remove_secrets, } => { + let _span = + tracing::info_span!("cmd", command = "update", %namespace, %kind, %name).entered(); commands::update::run( &pool, commands::update::UpdateArgs {