Skip to content

Commit 385e9c6

Browse files
authored
feat: add audit log retention cleanup with clean-audit-logs CLI command (#51)
Add operational support for audit log retention management via a new CLI command that enables safe deletion of old audit logs by age. The command supports dry-run preview mode to show affected row counts before deletion and provides both human-readable text and machine-friendly JSON output formats. Implementation: - Add clean-audit-logs CLI command with --days, --dry-run, and --format flags - Implement DeleteOlderThan() method across repository, use case, and interface layers - Support both PostgreSQL and MySQL with parameterized DELETE queries and COUNT-based dry-run - Calculate UTC-based cutoff timestamps (current time minus retention days) - Return affected row count for both dry-run (via SELECT COUNT) and execution (via RowsAffected) Testing: - Add comprehensive use case tests covering success, error, dry-run, and zero-result scenarios - Update mock interfaces (middleware test, generated mocks) with new DeleteOlderThan signature Documentation: - Add command reference to CLI guide with examples and Docker usage patterns - Add retention cleanup runbook to production operations guide - Clarify audit log retention is CLI-based (no HTTP DELETE endpoint) - Update version references from v0.1.0 to v0.2.0 across documentation - Add v0.2.0 release notes with operational guidance and upgrade notes Project files updated: - AGENTS.md: add clean-audit-logs to available commands list - README.md: update pinned Docker tag to v0.2.0 and release link - docs/CHANGELOG.md: add v0.2.0 documentation changes entry
1 parent 848343e commit 385e9c6

19 files changed

Lines changed: 644 additions & 67 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,9 @@ func main() {
473473
- `app create-kek [--algorithm aes-gcm|chacha20-poly1305]` - Create initial KEK
474474
- `app rotate-kek [--algorithm aes-gcm|chacha20-poly1305]` - Rotate existing KEK
475475

476+
**Audit Log Operations:**
477+
- `app clean-audit-logs --days <days> [--dry-run] [--format text|json]` - Delete old audit logs or preview count
478+
476479
### Command Testing
477480

478481
When adding new commands:

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Secrets is inspired by **HashiCorp Vault** ❤️, but it is intentionally **muc
1313
The default way to run Secrets is the published Docker image:
1414

1515
```bash
16-
docker pull allisson/secrets:v0.1.0
16+
docker pull allisson/secrets:v0.2.0
1717
```
1818

1919
Use pinned tags for reproducible setups. `latest` is also available for fast iteration.
@@ -36,7 +36,7 @@ Then follow the Docker setup guide in [docs/getting-started/docker.md](docs/gett
3636
- 🧰 **Troubleshooting**: [docs/getting-started/troubleshooting.md](docs/getting-started/troubleshooting.md)
3737
-**Smoke test script**: [docs/getting-started/smoke-test.md](docs/getting-started/smoke-test.md)
3838
- 🧪 **CLI commands reference**: [docs/cli/commands.md](docs/cli/commands.md)
39-
- 🚀 **v0.1.0 release notes**: [docs/releases/v0.1.0.md](docs/releases/v0.1.0.md)
39+
- 🚀 **v0.2.0 release notes**: [docs/releases/v0.2.0.md](docs/releases/v0.2.0.md)
4040

4141
- **By Topic**
4242
- ⚙️ **Environment variables**: [docs/configuration/environment-variables.md](docs/configuration/environment-variables.md)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log/slog"
8+
"os"
9+
10+
"github.com/allisson/secrets/internal/app"
11+
"github.com/allisson/secrets/internal/config"
12+
)
13+
14+
// RunCleanAuditLogs deletes audit logs older than the specified number of days.
15+
// Supports dry-run mode to preview deletion count and both text/JSON output formats.
16+
//
17+
// Requirements: Database must be migrated and accessible.
18+
func RunCleanAuditLogs(ctx context.Context, days int, dryRun bool, format string) error {
19+
// Validate days parameter
20+
if days < 0 {
21+
return fmt.Errorf("days must be a positive number, got: %d", days)
22+
}
23+
24+
// Load configuration
25+
cfg := config.Load()
26+
27+
// Create DI container
28+
container := app.NewContainer(cfg)
29+
30+
// Get logger from container
31+
logger := container.Logger()
32+
logger.Info("cleaning audit logs",
33+
slog.Int("days", days),
34+
slog.Bool("dry_run", dryRun),
35+
)
36+
37+
// Ensure cleanup on exit
38+
defer closeContainer(container, logger)
39+
40+
// Get audit log use case from container
41+
auditLogUseCase, err := container.AuditLogUseCase()
42+
if err != nil {
43+
return fmt.Errorf("failed to initialize audit log use case: %w", err)
44+
}
45+
46+
// Execute deletion or count operation
47+
count, err := auditLogUseCase.DeleteOlderThan(ctx, days, dryRun)
48+
if err != nil {
49+
return fmt.Errorf("failed to delete audit logs: %w", err)
50+
}
51+
52+
// Output result based on format
53+
if format == "json" {
54+
outputCleanJSON(count, days, dryRun)
55+
} else {
56+
outputCleanText(count, days, dryRun)
57+
}
58+
59+
logger.Info("cleanup completed",
60+
slog.Int64("count", count),
61+
slog.Int("days", days),
62+
slog.Bool("dry_run", dryRun),
63+
)
64+
65+
return nil
66+
}
67+
68+
// outputCleanText outputs the result in human-readable text format.
69+
func outputCleanText(count int64, days int, dryRun bool) {
70+
if dryRun {
71+
fmt.Printf("Dry-run mode: Would delete %d audit log(s) older than %d day(s)\n", count, days)
72+
} else {
73+
fmt.Printf("Successfully deleted %d audit log(s) older than %d day(s)\n", count, days)
74+
}
75+
}
76+
77+
// outputCleanJSON outputs the result in JSON format for machine consumption.
78+
func outputCleanJSON(count int64, days int, dryRun bool) {
79+
result := map[string]interface{}{
80+
"count": count,
81+
"days": days,
82+
"dry_run": dryRun,
83+
}
84+
85+
jsonBytes, err := json.MarshalIndent(result, "", " ")
86+
if err != nil {
87+
fmt.Fprintf(os.Stderr, "failed to marshal JSON: %v\n", err)
88+
return
89+
}
90+
91+
fmt.Println(string(jsonBytes))
92+
}

cmd/app/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,38 @@ func main() {
159159
)
160160
},
161161
},
162+
{
163+
Name: "clean-audit-logs",
164+
Usage: "Delete audit logs older than specified days",
165+
Flags: []cli.Flag{
166+
&cli.IntFlag{
167+
Name: "days",
168+
Aliases: []string{"d"},
169+
Required: true,
170+
Usage: "Delete audit logs older than this many days",
171+
},
172+
&cli.BoolFlag{
173+
Name: "dry-run",
174+
Aliases: []string{"n"},
175+
Value: false,
176+
Usage: "Show how many logs would be deleted without deleting",
177+
},
178+
&cli.StringFlag{
179+
Name: "format",
180+
Aliases: []string{"f"},
181+
Value: "text",
182+
Usage: "Output format: 'text' or 'json'",
183+
},
184+
},
185+
Action: func(ctx context.Context, cmd *cli.Command) error {
186+
return commands.RunCleanAuditLogs(
187+
ctx,
188+
cmd.Int("days"),
189+
cmd.Bool("dry-run"),
190+
cmd.String("format"),
191+
)
192+
},
193+
},
162194
},
163195
}
164196

docs/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
> Last updated: 2026-02-14
44
5+
## 2026-02-14 (docs v3 - v0.2.0 release prep)
6+
7+
- Added `clean-audit-logs` command documentation with dry-run and JSON/text output examples
8+
- Added audit-log retention cleanup runbook to production operations guide
9+
- Clarified audit log retention is a CLI cleanup workflow, while API remains list/query (`GET /v1/audit-logs`)
10+
- Updated pinned Docker image tags and release references from `v0.1.0` to `v0.2.0`
11+
- Added release notes page: `docs/releases/v0.2.0.md` and kept `v0.1.0` as historical
12+
513
## 2026-02-14 (docs v2 - v0.1.0 release prep)
614

715
- Added first-client bootstrap flow to Docker and local development guides using `create-client`

docs/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ Welcome to the full documentation for Secrets. Pick a path and dive in 🚀
5555

5656
OpenAPI scope note:
5757

58-
- `openapi.yaml` is a baseline subset for common API flows in `v0.1.0`
58+
- `openapi.yaml` is a baseline subset for common API flows in `v0.2.0`
5959
- Full endpoint behavior is documented in the endpoint pages under `docs/api/`
6060

6161
## 🚀 Releases
6262

63-
- 📦 [releases/v0.1.0.md](releases/v0.1.0.md)
63+
- 📦 [releases/v0.2.0.md](releases/v0.2.0.md)
64+
- 📦 [releases/v0.1.0.md](releases/v0.1.0.md) (historical)
6465

6566
## 🧠 ADRs
6667

docs/api/audit-logs.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ Example response (`200 OK`):
7878
- 🌐 Spot unusual source IP changes per client
7979
- 🧭 Correlate a request with app logs via `request_id`
8080

81+
## Retention and Cleanup
82+
83+
- Audit log cleanup is an operator workflow via CLI, not an HTTP delete endpoint
84+
- Use `clean-audit-logs` to delete old records by retention days
85+
- Start with `--dry-run` to preview affected rows before deletion
86+
87+
Example:
88+
89+
```bash
90+
./bin/app clean-audit-logs --days 90 --dry-run --format json
91+
```
92+
8193
## Common Errors
8294

8395
- `401 Unauthorized`: missing/invalid bearer token

docs/api/versioning-policy.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ This page defines compatibility expectations for HTTP API changes.
1111
- Existing endpoint paths and JSON field names are treated as stable unless explicitly deprecated
1212
- OpenAPI source of truth: `docs/openapi.yaml`
1313

14-
## OpenAPI Coverage (v0.1.0)
14+
## OpenAPI Coverage (v0.2.0)
1515

1616
- `docs/openapi.yaml` is a baseline subset focused on high-traffic/common integration flows
1717
- Endpoint pages in `docs/api/*.md` define full public behavior for covered operations
1818
- Endpoints may exist in runtime before they are expanded in OpenAPI detail
1919

2020
## App Version vs API Version
2121

22-
- Application release `v0.1.0` is pre-1.0 software and may evolve quickly
22+
- Application release `v0.2.0` is pre-1.0 software and may evolve quickly
2323
- API v1 path contract (`/v1/*`) remains the compatibility baseline for consumers
2424
- Breaking API behavior changes require explicit documentation and migration notes
2525

docs/cli/commands.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ Local binary:
1212
./bin/app <command> [flags]
1313
```
1414

15-
Docker image (v0.1.0):
15+
Docker image (v0.2.0):
1616

1717
```bash
18-
docker run --rm --env-file .env allisson/secrets:v0.1.0 <command> [flags]
18+
docker run --rm --env-file .env allisson/secrets:v0.2.0 <command> [flags]
1919
```
2020

2121
## Core Runtime
@@ -133,6 +133,53 @@ Flags:
133133
- Store client secrets in a secure secret manager
134134
- Use least-privilege policies per workload and path
135135

136+
## Audit Log Maintenance
137+
138+
### `clean-audit-logs`
139+
140+
Deletes audit logs older than a specified retention period.
141+
142+
Flags:
143+
144+
- `--days`, `-d` (required): delete logs older than this many days
145+
- `--dry-run`, `-n` (default `false`): preview count without deleting
146+
- `--format`, `-f`: `text` (default) or `json`
147+
148+
Examples:
149+
150+
```bash
151+
# Preview (no deletion)
152+
./bin/app clean-audit-logs --days 90 --dry-run
153+
154+
# Execute deletion
155+
./bin/app clean-audit-logs --days 90 --format text
156+
157+
# Docker form
158+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 \
159+
clean-audit-logs --days 90 --dry-run --format json
160+
```
161+
162+
Example text output:
163+
164+
```text
165+
Dry-run mode: Would delete 1234 audit log(s) older than 90 day(s)
166+
```
167+
168+
Example JSON output:
169+
170+
```json
171+
{
172+
"count": 1234,
173+
"days": 90,
174+
"dry_run": true
175+
}
176+
```
177+
178+
Requirements:
179+
180+
- Database must be reachable and migrated
181+
- Use `--dry-run` before deletion in production environments
182+
136183
## See also
137184

138185
- [Docker getting started](../getting-started/docker.md)

docs/getting-started/docker.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
55
This is the default way to run Secrets.
66

7-
For release reproducibility, this guide uses the pinned image tag `allisson/secrets:v0.1.0`.
7+
For release reproducibility, this guide uses the pinned image tag `allisson/secrets:v0.2.0`.
88
You can use `allisson/secrets:latest` for fast iteration.
99

1010
## ⚡ Quickstart Copy Block
1111

1212
Use this minimal flow when you just want to get a working instance quickly:
1313

1414
```bash
15-
docker pull allisson/secrets:v0.1.0
15+
docker pull allisson/secrets:v0.2.0
1616
docker network create secrets-net || true
1717

1818
docker run -d --name secrets-postgres --network secrets-net \
@@ -21,19 +21,19 @@ docker run -d --name secrets-postgres --network secrets-net \
2121
-e POSTGRES_DB=mydb \
2222
postgres:16-alpine
2323

24-
docker run --rm allisson/secrets:v0.1.0 create-master-key --id default
24+
docker run --rm allisson/secrets:v0.2.0 create-master-key --id default
2525
# copy generated MASTER_KEYS and ACTIVE_MASTER_KEY_ID into .env
2626

27-
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.1.0 migrate
28-
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.1.0 create-kek --algorithm aes-gcm
27+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 migrate
28+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 create-kek --algorithm aes-gcm
2929
docker run --rm --name secrets-api --network secrets-net --env-file .env -p 8080:8080 \
30-
allisson/secrets:v0.1.0 server
30+
allisson/secrets:v0.2.0 server
3131
```
3232

3333
## 1) Pull the image
3434

3535
```bash
36-
docker pull allisson/secrets:v0.1.0
36+
docker pull allisson/secrets:v0.2.0
3737
```
3838

3939
## 2) Start PostgreSQL
@@ -51,7 +51,7 @@ docker run -d --name secrets-postgres --network secrets-net \
5151
## 3) Generate a master key
5252

5353
```bash
54-
docker run --rm allisson/secrets:v0.1.0 create-master-key --id default
54+
docker run --rm allisson/secrets:v0.2.0 create-master-key --id default
5555
```
5656

5757
Copy the generated values into a local `.env` file.
@@ -80,15 +80,15 @@ EOF
8080
## 5) Run migrations and bootstrap KEK
8181

8282
```bash
83-
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.1.0 migrate
84-
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.1.0 create-kek --algorithm aes-gcm
83+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 migrate
84+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 create-kek --algorithm aes-gcm
8585
```
8686

8787
## 6) Start the API server
8888

8989
```bash
9090
docker run --rm --name secrets-api --network secrets-net --env-file .env -p 8080:8080 \
91-
allisson/secrets:v0.1.0 server
91+
allisson/secrets:v0.2.0 server
9292
```
9393

9494
## 7) Verify
@@ -108,7 +108,7 @@ Expected:
108108
Use the CLI command to create your first API client and policy set:
109109

110110
```bash
111-
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.1.0 create-client \
111+
docker run --rm --network secrets-net --env-file .env allisson/secrets:v0.2.0 create-client \
112112
--name bootstrap-admin \
113113
--active \
114114
--policies '[{"path":"*","capabilities":["read","write","delete","encrypt","decrypt","rotate"]}]' \

0 commit comments

Comments
 (0)