Skip to content

Commit 072c69b

Browse files
gaojunranjunrangao
andauthored
feat(logs): add log rotate (jdx#470)
Co-authored-by: junrangao <junrangao@tencent.com>
1 parent 002b712 commit 072c69b

26 files changed

Lines changed: 1545 additions & 1603 deletions

Cargo.lock

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ reqwest = { version = "0.13", default-features = false, features = [
5757
"rustls-native-certs",
5858
"rustls-no-provider",
5959
] }
60+
rusqlite = { version = "0.34", features = ["bundled", "chrono"] }
6061
rev_lines = "0.3"
6162
rust-embed = { version = "8", features = ["axum"] }
6263
axum = { version = "0.8" }
@@ -123,6 +124,7 @@ indexmap = "2"
123124

124125
[dev-dependencies]
125126
tempfile = "3"
127+
rusqlite = { version = "0.34", features = ["bundled", "chrono"] }
126128

127129
[profile.dev]
128130
debug = 1

docs/guides/logs.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Log Management
22

3-
View, filter, and manage daemon logs.
3+
View, filter, and manage daemon logs. Pitchfork stores all daemon logs in an SQLite database (`~/.local/state/pitchfork/logs/logs.db`) with full timestamp indexing, making filtering by time fast and reliable.
44

55
## View Logs
66

@@ -125,6 +125,38 @@ Delete all logs for a daemon:
125125
pitchfork logs api --clear
126126
```
127127

128+
## Log Rotation
129+
130+
Pitchfork supports automatic log rotation via `time_retention` and `line_retention` settings. Old entries are pruned periodically by the supervisor so the database does not grow unbounded.
131+
132+
### Automatic Rotation
133+
134+
Configure in any `pitchfork.toml` under `[settings.logs]`:
135+
136+
```toml
137+
[settings.logs]
138+
# Keep only the last 7 days of logs
139+
time_retention = "7d"
140+
141+
# Or keep only the most recent 10,000 entries
142+
line_retention = 10000
143+
144+
# You can also combine both (entries older than 7d OR exceeding 10,000 lines are pruned)
145+
# time_retention = "7d"
146+
# line_retention = 10000
147+
```
148+
149+
Supported formats:
150+
- **Time-based (`time_retention`):** `"7d"`, `"30d"`, `"1h"` — delete entries older than this duration
151+
- **Count-based (`line_retention`):** `10000`, `5000` — keep only the most recent N entries per daemon
152+
- **Unset (default):** no automatic pruning
153+
154+
The supervisor evaluates this policy during its regular interval watcher cycle.
155+
156+
## Migrate Legacy Logs
157+
158+
If you were using pitchfork before the SQLite log store was introduced, legacy text log files may still exist under the logs directory. They are automatically imported into the SQLite database on the first access to the log store, so no manual action is required.
159+
128160
## Supervisor Logs
129161

130162
View pitchfork's own logs:
@@ -133,12 +165,10 @@ View pitchfork's own logs:
133165
pitchfork logs pitchfork
134166
```
135167

136-
## Log Location
137-
138-
Logs are stored in `~/.local/state/pitchfork/logs/<namespace>--<name>/`. See [File Locations](/reference/file-locations#logs) for details on the path format.
139-
140-
Each daemon has its own log file that persists across restarts.
141-
142168
## TUI and Web UI
143169

144170
You can also view logs in real-time through the [TUI](/guides/tui) (`pitchfork tui`) or [Web UI](/guides/web-ui) (if enabled).
171+
172+
## Log Storage Location
173+
174+
Logs are stored in a single SQLite database at `~/.local/state/pitchfork/logs/logs.db`. Each daemon has its own table partition identified by its qualified ID (`namespace/name`). See [File Locations](/reference/file-locations#logs) for details on the state directory resolution.

docs/public/schema.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"api": {},
2929
"general": {},
3030
"ipc": {},
31+
"logs": {},
3132
"proxy": {},
3233
"supervisor": {},
3334
"tui": {},
@@ -225,6 +226,14 @@
225226
}
226227
]
227228
},
229+
"line_retention": {
230+
"description": "Maximum number of log entries to keep per daemon.\nOverrides the global `settings.logs.line_retention` when set.",
231+
"type": [
232+
"integer",
233+
"null"
234+
],
235+
"format": "int64"
236+
},
228237
"memory_limit": {
229238
"description": "Memory limit for the daemon process (e.g. \"50MB\", \"1GiB\").\nThe supervisor periodically monitors RSS and kills the process if it exceeds the limit.",
230239
"anyOf": [
@@ -328,6 +337,13 @@
328337
}
329338
]
330339
},
340+
"time_retention": {
341+
"description": "Maximum age of log entries to keep (e.g. \"7d\", \"30d\").\nOverrides the global `settings.logs.time_retention` when set.",
342+
"type": [
343+
"string",
344+
"null"
345+
]
346+
},
331347
"user": {
332348
"description": "Unix user to run this daemon as. Overrides `settings.supervisor.user` when set.",
333349
"type": [
@@ -638,6 +654,26 @@
638654
}
639655
}
640656
},
657+
"SettingsLogsPartial": {
658+
"type": "object",
659+
"properties": {
660+
"line_retention": {
661+
"description": "Count-based log retention (e.g. 10000)",
662+
"type": [
663+
"integer",
664+
"null"
665+
],
666+
"format": "int64"
667+
},
668+
"time_retention": {
669+
"description": "Time-based log retention duration (e.g. '7d', '30d')",
670+
"type": [
671+
"string",
672+
"null"
673+
]
674+
}
675+
}
676+
},
641677
"SettingsPartial": {
642678
"type": "object",
643679
"properties": {
@@ -653,6 +689,10 @@
653689
"$ref": "#/$defs/SettingsIpcPartial",
654690
"default": {}
655691
},
692+
"logs": {
693+
"$ref": "#/$defs/SettingsLogsPartial",
694+
"default": {}
695+
},
656696
"proxy": {
657697
"$ref": "#/$defs/SettingsProxyPartial",
658698
"default": {}

docs/reference/file-locations.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,21 @@ Within a given project directory, files take precedence in this order:
7373

7474
### Logs
7575

76-
Each daemon has its own log directory and file. The log path is determined by the daemon's qualified ID (namespace + name):
76+
Logs are stored in a single SQLite database (`logs.db`) for efficient querying, filtering, and rotation. The database uses WAL mode for concurrent readers so the CLI, TUI, and Web UI can all read logs at the same time without blocking the supervisor's writes.
7777

78-
```
79-
~/.local/state/pitchfork/logs/<namespace>--<daemon-name>/<namespace>--<daemon-name>.log
78+
```text
79+
~/.local/state/pitchfork/logs/logs.db
8080
```
8181

82-
The namespace is derived from top-level `namespace` in the config when present, otherwise from the project directory name (or `global` for global config files). For example:
83-
- Daemon `api` in project `myapp``logs/myapp--api/myapp--api.log`
84-
- Daemon `api` in project `yourapp``logs/yourapp--api/yourapp--api.log`
85-
- Daemon `postgres` in global config → `logs/global--postgres/global--postgres.log`
82+
Inside the database, each daemon is identified by its qualified ID (`namespace/name`). Log entries include a timestamp (millisecond precision) and the raw message text, so time-based filtering is fast and reliable.
8683

87-
The `--` separator is used to convert the `/` in qualified daemon IDs (e.g., `myapp/api`) to a filesystem-safe format.
84+
For backwards compatibility, the legacy log directory structure still exists but is no longer written to by the supervisor:
8885

89-
Because `--` is reserved for this encoding, project directory names containing `--` (or other invalid namespace characters) require a top-level `namespace` override in `pitchfork.toml`.
86+
```text
87+
~/.local/state/pitchfork/logs/<namespace>--<daemon-name>/
88+
```
9089

91-
See [Namespaces](/concepts/namespaces) for more details on how daemon IDs work across projects.
90+
Legacy text log files from older pitchfork versions are automatically imported into the SQLite store on first access, so no manual migration is needed.
9291

9392
### IPC Socket
9493

docs/reference/settings.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ run = "node server.js"
3030
autostop_delay = "5m"
3131
log_level = "debug"
3232

33+
[settings.logs]
34+
time_retention = "7d"
35+
3336
[settings.tui]
3437
refresh_rate = "1s"
3538

settings.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,48 @@ When disabled (default), a dim bullet (`•`) is used instead to keep
125125
the output compact and aligned with the spinner / status icons.
126126
"""
127127

128+
# =============================================================================
129+
# Log Settings
130+
# =============================================================================
131+
132+
[logs]
133+
134+
[logs.time_retention]
135+
type = "Duration"
136+
env = "PITCHFORK_LOG_TIME_RETENTION"
137+
default = ""
138+
description = "Time-based log retention duration (e.g. '7d', '30d')"
139+
docs = """
140+
Maximum age of log entries to keep in the SQLite log store.
141+
142+
When set, log entries older than this duration are automatically pruned.
143+
Examples: `"7d"` for 7 days, `"30d"` for 30 days, `"1h"` for 1 hour.
144+
145+
When empty (default), no time-based pruning is performed. You can combine
146+
this with `line_retention` to enforce both a time limit and a line count limit.
147+
148+
Logs are pruned automatically by the supervisor during its interval watcher
149+
cycle (no more than once per hour). No manual rotation command is needed.
150+
"""
151+
152+
[logs.line_retention]
153+
type = "Integer"
154+
env = "PITCHFORK_LOG_LINE_RETENTION"
155+
default = "0"
156+
description = "Count-based log retention (e.g. 10000)"
157+
docs = """
158+
Maximum number of log entries to keep per daemon in the SQLite log store.
159+
160+
When set, only the most recent N log entries are retained. Older entries are
161+
automatically pruned.
162+
163+
When set to `0` (default), no count-based pruning is performed. You can combine
164+
this with `time_retention` to enforce both a time limit and a line count limit.
165+
166+
Logs are pruned automatically by the supervisor during its interval watcher
167+
cycle (no more than once per hour). No manual rotation command is needed.
168+
"""
169+
128170
# =============================================================================
129171
# IPC (Inter-Process Communication) Settings
130172
# =============================================================================

0 commit comments

Comments
 (0)