llm logs backup command, closes #879

This commit is contained in:
Simon Willison 2025-04-07 21:45:52 -07:00
parent 63be6ef51d
commit 0fbbe6a054
4 changed files with 56 additions and 0 deletions

View file

@ -250,6 +250,7 @@ Options:
Commands:
list* Show logged prompts and their responses
backup Backup your logs database to this file
off Turn off logging for all prompts
on Turn on logging for all prompts
path Output the path to the logs.db file
@ -278,6 +279,17 @@ Options:
--help Show this message and exit.
```
(help-logs-backup)=
#### llm logs backup --help
```
Usage: llm logs backup [OPTIONS] PATH
Backup your logs database to this file
Options:
--help Show this message and exit.
```
(help-logs-on)=
#### llm logs on --help
```

View file

@ -234,6 +234,17 @@ You can also use [Datasette](https://datasette.io/) to browse your logs like thi
datasette "$(llm logs path)"
```
(logging-backup)=
### Backing up your database
You can backup your logs to another file using the `llm logs backup` command:
```bash
llm logs backup /tmp/backup.db
```
This uses SQLite [VACCUM INTO](https://sqlite.org/lang_vacuum.html#vacuum_with_an_into_clause) under the hood.
(logging-sql-schema)=
## SQL schema

View file

@ -1085,6 +1085,22 @@ def logs_status():
)
@logs.command(name="backup")
@click.argument("path", type=click.Path(dir_okay=True, writable=True))
def backup(path):
"Backup your logs database to this file"
logs_path = logs_db_path()
path = pathlib.Path(path)
db = sqlite_utils.Database(logs_path)
try:
db.execute("vacuum into ?", [str(path)])
except Exception as ex:
raise click.ClickException(str(ex))
click.echo(
"Backed up {} to {}".format(_human_readable_size(path.stat().st_size), path)
)
@logs.command(name="on")
def logs_turn_on():
"Turn on logging for all prompts"

View file

@ -5,6 +5,7 @@ from llm import Fragment
from ulid import ULID
import datetime
import json
import pathlib
import pytest
import re
import sqlite_utils
@ -842,3 +843,19 @@ def test_expand_fragment_markdown(fragments_fixture):
expected_prefix = f"### Prompt fragments\n\n<details><summary>{hash}</summary>\nThis is fragment 5"
assert interesting_bit.startswith(expected_prefix)
assert interesting_bit.endswith("</details>")
def test_logs_backup(logs_db):
assert not logs_db.tables
runner = CliRunner()
with runner.isolated_filesystem():
runner.invoke(cli, ["-m", "echo", "simple prompt"])
assert logs_db.tables
expected_path = pathlib.Path("backup.db")
assert not expected_path.exists()
# Now back it up
result = runner.invoke(cli, ["logs", "backup", "backup.db"])
assert result.exit_code == 0
assert result.output.startswith("Backed up ")
assert result.output.endswith("to backup.db\n")
assert expected_path.exists()