Skip to content

Commit 2b1f6a1

Browse files
monitoring: publish reports for delete, undelete and transfer
borg delete, borg undelete and borg transfer now publish a monitoring report as the last action while the store is open (skipped on --dry-run), with host/user defaulting to the local machine like prune. Stats record the number of archives deleted / undeleted / transferred (plus considered/skipped and transferred bytes). These appear as their own units in borg monitor (command=delete / undelete / transfer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b1a70fe commit 2b1f6a1

4 files changed

Lines changed: 77 additions & 6 deletions

File tree

src/borg/archiver/delete_cmd.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from ._common import with_repository
4+
from .. import monitoring
45
from ..constants import * # NOQA
56
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
67
from ..helpers.argparsing import ArgumentParser
@@ -32,7 +33,7 @@ def do_delete(self, args, repository):
3233
"or just delete the whole repository (might be much faster)."
3334
)
3435

35-
deleted = False
36+
deleted_count = 0
3637
logger_list = logging.getLogger("borg.output.list")
3738
for i, archive_info in enumerate(archive_infos, 1):
3839
name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)
@@ -46,17 +47,24 @@ def do_delete(self, args, repository):
4647
except KeyError:
4748
self.print_warning(f"Archive {name} {hex_id} not found ({i}/{count}).")
4849
else:
49-
deleted = True
50+
deleted_count += 1
5051
if self.output_list:
5152
msg = "Would delete: {} ({}/{})" if dry_run else "Deleted archive: {} ({}/{})"
5253
logger_list.info(msg.format(archive_formatted, i, count))
5354
if dry_run:
5455
logger.info("Finished dry-run.")
55-
elif deleted:
56+
elif deleted_count:
5657
manifest.write()
5758
self.print_warning('Done. Run "borg compact" to free space.', wc=None)
5859
else:
5960
self.print_warning("Aborted.", wc=None)
61+
if not dry_run:
62+
monitoring.publish_command_report(
63+
repository,
64+
manifest.key,
65+
"delete",
66+
stats={"archives_deleted": deleted_count, "archives_considered": count},
67+
)
6068
return
6169

6270
def build_parser_delete(self, subparsers, common_parser, mid_common_parser):

src/borg/archiver/transfer_cmd.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ._common import with_repository, with_other_repository, Highlander
2+
from .. import monitoring
23
from ..archive import Archive, cached_hash, DownloadPipeline
34
from ..chunkers import get_chunker
45
from ..constants import * # NOQA
@@ -186,6 +187,9 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non
186187

187188
upgrader = UpgraderCls(cache=cache, args=args)
188189

190+
transferred_count = 0
191+
skipped_count = 0
192+
transferred_size = 0
189193
for archive_info in archive_infos:
190194
name, id, ts = archive_info.name, archive_info.id, archive_info.ts
191195
id_hex, ts_str = bin_to_hex(id), ts.isoformat()
@@ -201,9 +205,11 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non
201205
# Useful for Borg 1.x -> 2 transfers; we have unique names in Borg 1.x.
202206
# Also useful for Borg 2 -> 2 transfers with metadata changes (ID changes).
203207
print(f"{name} {ts_str}: archive is already present in destination repo, skipping.")
208+
skipped_count += 1
204209
elif not dry_run and manifest.archives.exists_name_and_id(name, id):
205210
# Useful for Borg 2 -> 2 transfers without changes (ID stays the same)
206211
print(f"{name} {id_hex}: archive is already present in destination repo, skipping.")
212+
skipped_count += 1
207213
else:
208214
if not dry_run:
209215
print(f"{name} {ts_str} {id_hex}: copying archive to destination repo...")
@@ -253,6 +259,8 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non
253259
archive.stats.show_progress(final=True)
254260
additional_metadata = upgrader.upgrade_archive_metadata(metadata=other_archive.metadata)
255261
archive.save(additional_metadata=additional_metadata)
262+
transferred_count += 1
263+
transferred_size += transfer_size
256264
print(
257265
f"{name} {ts_str} {id_hex}: finished. "
258266
f"transfer_size: {format_file_size(transfer_size)} "
@@ -267,6 +275,19 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non
267275
f"present_size: {format_file_size(present_size)}"
268276
)
269277

278+
if not dry_run:
279+
monitoring.publish_command_report(
280+
repository,
281+
manifest.key,
282+
"transfer",
283+
stats={
284+
"archives_transferred": transferred_count,
285+
"archives_skipped": skipped_count,
286+
"archives_considered": count,
287+
"transferred_size": transferred_size,
288+
},
289+
)
290+
270291
def build_parser_transfer(self, subparsers, common_parser, mid_common_parser):
271292
from ._common import process_epilog
272293
from ._common import define_archive_filters_group

src/borg/archiver/undelete_cmd.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from ._common import with_repository
4+
from .. import monitoring
45
from ..constants import * # NOQA
56
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
67
from ..helpers.argparsing import ArgumentParser
@@ -29,7 +30,7 @@ def do_undelete(self, args, repository):
2930
if not args.name and not args.match_archives and args.first == 0 and args.last == 0:
3031
raise CommandError("Aborting: if you really want to undelete all archives, please use -a 'sh:*'.")
3132

32-
undeleted = False
33+
undeleted_count = 0
3334
logger_list = logging.getLogger("borg.output.list")
3435
for i, archive_info in enumerate(archive_infos, 1):
3536
name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)
@@ -39,17 +40,24 @@ def do_undelete(self, args, repository):
3940
except KeyError:
4041
self.print_warning(f"Archive {name} {hex_id} not found ({i}/{count}).")
4142
else:
42-
undeleted = True
43+
undeleted_count += 1
4344
if self.output_list:
4445
msg = "Would undelete: {} ({}/{})" if dry_run else "Undeleted archive: {} ({}/{})"
4546
logger_list.info(msg.format(format_archive(archive_info), i, count))
4647
if dry_run:
4748
logger.info("Finished dry-run.")
48-
elif undeleted:
49+
elif undeleted_count:
4950
manifest.write()
5051
self.print_warning("Done.", wc=None)
5152
else:
5253
self.print_warning("Aborted.", wc=None)
54+
if not dry_run:
55+
monitoring.publish_command_report(
56+
repository,
57+
manifest.key,
58+
"undelete",
59+
stats={"archives_undeleted": undeleted_count, "archives_considered": count},
60+
)
5361
return
5462

5563
def build_parser_undelete(self, subparsers, common_parser, mid_common_parser):

src/borg/testsuite/archiver/monitor_cmd_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,40 @@ def test_prune_publishes_its_own_report(archivers, request, monkeypatch):
108108
assert prune["stats"]["archives_kept"] == 1
109109

110110

111+
def test_delete_and_undelete_publish_reports(archivers, request, monkeypatch):
112+
archiver = request.getfixturevalue(archivers)
113+
create_regular_file(archiver.input_path, "file1", contents=b"some data")
114+
cmd(archiver, "repo-create", RK_ENCRYPTION)
115+
cmd(archiver, "create", "a1", "input")
116+
cmd(archiver, "create", "a2", "input")
117+
cmd(archiver, "delete", "a2")
118+
cmd(archiver, "undelete", "a2")
119+
monkeypatch.setenv("BORG_MONITORING_KEY", _monitoring_key(archiver))
120+
121+
entries = _entries(archiver)
122+
assert {"delete", "undelete"} <= set(entries)
123+
assert entries["delete"]["report"]["stats"]["archives_deleted"] == 1
124+
assert entries["undelete"]["report"]["stats"]["archives_undeleted"] == 1
125+
126+
127+
def test_transfer_publishes_report(archivers, request, monkeypatch):
128+
from .transfer_cmd_test import setup_repos
129+
130+
archiver = request.getfixturevalue(archivers)
131+
with setup_repos(archiver, monkeypatch) as other_repo1:
132+
create_regular_file(archiver.input_path, "file1", contents=b"some data")
133+
cmd(archiver, "create", "arch1", "input")
134+
cmd(archiver, "create", "arch2", "input")
135+
cmd(archiver, "transfer", other_repo1)
136+
monkeypatch.setenv("BORG_MONITORING_KEY", _monitoring_key(archiver))
137+
138+
entries = _entries(archiver)
139+
assert "transfer" in entries
140+
stats = entries["transfer"]["report"]["stats"]
141+
assert stats["archives_transferred"] == 2
142+
assert stats["archives_considered"] == 2
143+
144+
111145
def test_keep_evicts_old_objects(archivers, request, monkeypatch):
112146
archiver = request.getfixturevalue(archivers)
113147
create_regular_file(archiver.input_path, "file1", contents=b"some data")

0 commit comments

Comments
 (0)