Skip to main content

rustic_rs/commands/
backup.rs

1//! `backup` subcommand
2
3use std::collections::HashMap;
4use std::fmt::Display;
5use std::path::PathBuf;
6use std::{collections::BTreeMap, env};
7
8use crate::commands::ls::LsCmd;
9use crate::commands::program_version;
10use crate::repository::IndexedIdsRepo;
11use crate::{
12    Application, RUSTIC_APP,
13    commands::{init::init, snapshots::fill_table},
14    config::{hooks::Hooks, parse_labels},
15    helpers::{bold_cell, bytes_size_to_string, table},
16    repository::Repo,
17    status_err,
18};
19
20use abscissa_core::{Command, Runnable, Shutdown};
21use anyhow::{Context, Result, anyhow, bail};
22use clap::ValueHint;
23use comfy_table::Cell;
24use conflate::{Merge, MergeFrom};
25use log::{debug, error, info, warn};
26use rustic_backend::OpenDALBackend;
27use rustic_core::{ChildStdoutSource, Excludes, LocalSource, ReadSource, StdinSource, StringList};
28use serde::{Deserialize, Serialize};
29use serde_with::serde_as;
30
31use rustic_core::{
32    BackupOptions, CommandInput, ConfigOptions, KeyOptions, LocalSourceFilterOptions,
33    LocalSourceSaveOptions, ParentOptions, PathList, SnapshotOptions,
34    repofile::{SnapshotFile, SnapshotId},
35};
36
37/// `backup` subcommand
38#[serde_as]
39#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
40#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
41// Note: using cli_sources, sources and snapshots within this struct is a hack to support serde(deny_unknown_fields)
42// for deserializing the backup options from TOML
43// Unfortunately we cannot work with nested flattened structures, see
44// https://github.com/serde-rs/serde/issues/1547
45// A drawback is that a wrongly set "snapshots = ..." won't get correct error handling and need to be manually checked, see below.
46#[allow(clippy::struct_excessive_bools)]
47pub struct BackupCmd {
48    /// Backup source (can be specified multiple times), use - for stdin. If no source is given, uses all
49    /// sources defined in the config file
50    #[clap(value_name = "SOURCE", value_hint = ValueHint::AnyPath)]
51    #[merge(skip)]
52    #[serde(skip)]
53    cli_sources: Vec<String>,
54
55    /// Backup sources defined in the config profile by the given name (can be specified multiple times)
56    #[clap(long = "name", value_name = "NAME", conflicts_with = "cli_sources")]
57    #[merge(skip)]
58    #[serde(skip)]
59    cli_name: Vec<String>,
60
61    /// Don't run the backup, but only list files which would be backup'ed.
62    #[clap(long)]
63    #[merge(skip)]
64    #[serde(skip)]
65    ls: bool,
66
67    #[clap(skip)]
68    #[merge(skip)]
69    name: Option<String>,
70
71    /// Set filename to be used when backing up from stdin (default: "stdin")
72    #[clap(long, value_name = "FILENAME", value_hint = ValueHint::FilePath)]
73    #[merge(strategy=conflate::option::overwrite_none)]
74    stdin_filename: Option<String>,
75
76    /// Start the given command and use its output as stdin
77    #[clap(long, value_name = "COMMAND")]
78    #[merge(strategy=conflate::option::overwrite_none)]
79    stdin_command: Option<CommandInput>,
80
81    /// Manually set backup path in snapshot
82    #[clap(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
83    #[merge(strategy=conflate::option::overwrite_none)]
84    as_path: Option<PathBuf>,
85
86    /// Don't scan the backup source for its size - this disables ETA estimation for backup.
87    #[clap(long)]
88    #[merge(strategy=conflate::bool::overwrite_false)]
89    pub no_scan: bool,
90
91    /// Output generated snapshot in json format
92    #[clap(long)]
93    #[merge(strategy=conflate::bool::overwrite_false)]
94    json: bool,
95
96    /// Show detailed information about generated snapshot
97    #[clap(long, conflicts_with = "json")]
98    #[merge(strategy=conflate::bool::overwrite_false)]
99    long: bool,
100
101    /// Initialize repository, if it doesn't exist yet
102    #[clap(long)]
103    #[merge(strategy=conflate::bool::overwrite_false)]
104    init: bool,
105
106    /// Node save options
107    #[clap(flatten, next_help_heading = "Node modification options")]
108    #[serde(flatten)]
109    ignore_save_opts: LocalSourceSaveOptions,
110
111    /// Parent processing options
112    #[clap(flatten, next_help_heading = "Options for parent processing")]
113    #[serde(flatten)]
114    parent_opts: ParentOptions,
115
116    /// Exclude options
117    #[clap(flatten, next_help_heading = "Exclude options")]
118    #[serde(flatten)]
119    excludes: Excludes,
120
121    /// Exclude options for local source
122    #[clap(flatten, next_help_heading = "Exclude options for local source")]
123    #[serde(flatten)]
124    ignore_filter_opts: LocalSourceFilterOptions,
125
126    /// Snapshot options
127    #[clap(flatten, next_help_heading = "Snapshot options")]
128    #[serde(flatten)]
129    snap_opts: SnapshotOptions,
130
131    /// Key options (when using --init)
132    #[clap(flatten, next_help_heading = "Key options (when using --init)")]
133    #[serde(skip)]
134    #[merge(skip)]
135    key_opts: KeyOptions,
136
137    /// Config options (when using --init)
138    #[clap(flatten, next_help_heading = "Config options (when using --init)")]
139    #[serde(skip)]
140    #[merge(skip)]
141    config_opts: ConfigOptions,
142
143    /// Hooks to use
144    #[clap(skip)]
145    hooks: Hooks,
146
147    /// Backup snapshots to generate
148    #[clap(skip)]
149    #[merge(strategy = merge_snapshots)]
150    snapshots: Vec<Self>,
151
152    /// Backup source, used within config file
153    #[clap(skip)]
154    #[merge(skip)]
155    sources: Vec<String>,
156
157    /// Other options for this source
158    #[clap(skip)]
159    #[merge(strategy = conflate::btreemap::append_or_ignore)]
160    options: BTreeMap<String, String>,
161
162    /// Job name for the metrics. Default: rustic-backup
163    #[clap(long, value_name = "JOB_NAME", env = "RUSTIC_METRICS_JOB")]
164    #[merge(strategy=conflate::option::overwrite_none)]
165    pub metrics_job: Option<String>,
166
167    /// Additional labels to set to generated metrics
168    #[clap(long, value_name = "NAME=VALUE", value_parser = parse_labels, default_value = "")]
169    #[merge(strategy=conflate::btreemap::append_or_ignore)]
170    metrics_labels: BTreeMap<String, String>,
171}
172
173impl BackupCmd {
174    fn validate(&self) -> Result<(), &str> {
175        // manually check for a "source" field, check is not done by serde, see above.
176        if !self.sources.is_empty() {
177            return Err("key \"sources\" is not valid in the [backup] section!");
178        }
179
180        // manually check for a "name" field, check is not done by serde, see above.
181        if self.name.is_some() {
182            return Err("key \"name\" is not valid in the [backup] section!");
183        }
184
185        let snapshot_opts = &self.snapshots;
186        // manually check for a "sources" field, check is not done by serde, see above.
187        if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) {
188            return Err("key \"snapshots\" is not valid in a [[backup.snapshots]] section!");
189        }
190        Ok(())
191    }
192}
193
194/// Merge backup snapshots to generate
195///
196/// If a snapshot is already defined on left, use that. Else add it.
197///
198/// # Arguments
199///
200/// * `left` - Vector of backup sources
201pub(crate) fn merge_snapshots(left: &mut Vec<BackupCmd>, mut right: Vec<BackupCmd>) {
202    let order = |opt1: &BackupCmd, opt2: &BackupCmd| {
203        opt1.name
204            .cmp(&opt2.name)
205            .then(opt1.sources.cmp(&opt2.sources))
206    };
207
208    left.append(&mut right);
209    left.sort_by(order);
210    left.dedup_by(|opt1, opt2| order(opt1, opt2).is_eq());
211}
212
213impl Runnable for BackupCmd {
214    fn run(&self) {
215        let config = RUSTIC_APP.config();
216        if let Err(err) = config.backup.validate() {
217            status_err!("{}", err);
218            RUSTIC_APP.shutdown(Shutdown::Crash);
219        }
220
221        if let Err(err) = config.repository.run(|repo| self.inner_run(repo)) {
222            status_err!("{}", err);
223            RUSTIC_APP.shutdown(Shutdown::Crash);
224        };
225    }
226}
227
228impl BackupCmd {
229    fn inner_run(&self, repo: Repo) -> Result<()> {
230        let config = RUSTIC_APP.config();
231        let snapshots = self.get_snapshots_to_backup()?;
232
233        // Initialize repository if init is set and it is not yet initialized
234        let do_init =
235            self.init || config.backup.init || snapshots.iter().any(|(opts, _)| opts.init);
236        let repo = if do_init && repo.config_id()?.is_none() {
237            if config.global.dry_run {
238                bail!(
239                    "cannot initialize repository {} in dry-run mode!",
240                    repo.name
241                );
242            }
243            init(
244                repo,
245                &config.repository.credential_opts,
246                &self.key_opts,
247                &self.config_opts,
248            )?
249        } else {
250            repo.open(&config.repository.credential_opts)?
251        }
252        .to_indexed_ids()?;
253
254        let hooks = self.hooks(
255            &config.backup.hooks,
256            "backup",
257            itertools::join(&config.backup.sources, ","),
258        );
259
260        hooks.use_with(|| -> Result<_> {
261            let mut is_err = false;
262            for (opts, sources) in snapshots {
263                if let Err(err) = opts.backup_snapshot(sources.clone(), &repo) {
264                    error!("error backing up {sources}: {err}");
265                    is_err = true;
266                }
267            }
268            if is_err {
269                Err(anyhow!("Not all snapshots were generated successfully!"))
270            } else {
271                Ok(())
272            }
273        })
274    }
275
276    fn get_snapshots_to_backup(&self) -> Result<Vec<(Self, PathList)>> {
277        let config = RUSTIC_APP.config();
278        let mut config_snapshots = config
279            .backup
280            .snapshots
281            .iter()
282            .map(|opt| (opt.clone(), PathList::from_iter(&opt.sources)));
283
284        if !self.cli_sources.is_empty() {
285            let sources = PathList::from_iter(&self.cli_sources);
286            let mut opts = self.clone();
287            // merge Options from config file, if given
288            if let Some((config_opts, _)) = config_snapshots.find(|(_, s)| s == &sources) {
289                info!("merging sources={sources} section from config file");
290                opts.merge(config_opts);
291            }
292            return Ok(vec![(opts, sources)]);
293        }
294
295        let config_snapshots: Vec<_> = config_snapshots
296            // filter out using cli_name, if given
297            .filter(|(opt, _)| {
298                self.cli_name.is_empty()
299                    || opt
300                        .name
301                        .as_ref()
302                        .is_some_and(|name| self.cli_name.contains(name))
303            })
304            .map(|(opt, sources)| (self.clone().merge_from(opt), sources))
305            .collect();
306
307        if config_snapshots.is_empty() {
308            bail!("no backup source given.");
309        }
310
311        info!("using backup sources from config file.");
312        Ok(config_snapshots)
313    }
314
315    fn hooks(&self, hooks: &Hooks, action: &str, source: impl Display) -> Hooks {
316        let mut hooks_variables =
317            HashMap::from([("RUSTIC_ACTION".to_string(), action.to_string())]);
318
319        if let Some(label) = &self.snap_opts.label {
320            let _ = hooks_variables.insert("RUSTIC_BACKUP_LABEL".to_string(), label.to_string());
321        }
322
323        let source = source.to_string();
324        if !source.is_empty() {
325            let _ = hooks_variables.insert("RUSTIC_BACKUP_SOURCES".to_string(), source.clone());
326        }
327
328        let mut tags = StringList::default();
329        tags.add_all(self.snap_opts.tags.clone());
330        let tags = tags.to_string();
331        if !tags.is_empty() {
332            let _ = hooks_variables.insert("RUSTIC_BACKUP_TAGS".to_string(), tags);
333        }
334
335        let hooks = if action == "backup" {
336            hooks.with_context("backup")
337        } else {
338            hooks.with_context(&format!("backup {source}"))
339        };
340
341        hooks.with_env(&hooks_variables)
342    }
343
344    fn backup_source(
345        source: &PathList,
346        options: BTreeMap<String, String>,
347        ls: bool,
348        backup_opts: BackupOptions,
349        snap: &mut SnapshotFile,
350        repo: &IndexedIdsRepo,
351    ) -> Result<()> {
352        let backup_stdin = PathList::from_string("-")?;
353        let source = source
354            .clone()
355            .sanitize()
356            .with_context(|| format!("error sanitizing source=s\"{:?}\"", source))?
357            .merge();
358
359        if source.len() == 1
360                // TODO: This check should not be done on PathList, but in the sources list directly
361                && let Some(path) = source[0].to_string_lossy().strip_prefix("opendal:")
362        {
363            let source = OpenDALBackend::new(path, options)?.as_source(&backup_opts.excludes)?;
364            Self::archive(repo, &backup_opts, ls, &source, snap, &[PathBuf::new()])?;
365        } else if source == backup_stdin {
366            let path = PathBuf::from(&backup_opts.stdin_filename);
367            let backup_paths = vec![path.clone()];
368            if let Some(command) = &backup_opts.stdin_command {
369                let src = ChildStdoutSource::new(command, path)?;
370                Self::archive(repo, &backup_opts, ls, &src, snap, &backup_paths)?;
371                src.finish()?;
372            } else {
373                let src = StdinSource::new(path);
374                Self::archive(repo, &backup_opts, ls, &src, snap, &backup_paths)?;
375            }
376        } else {
377            let backup_path = source.paths();
378            let src = LocalSource::new(
379                backup_opts.ignore_save_opts,
380                &backup_opts.excludes,
381                &backup_opts.ignore_filter_opts,
382                &backup_path,
383            )?;
384            Self::archive(repo, &backup_opts, ls, &src, snap, &backup_path)?;
385        };
386        Ok(())
387    }
388
389    pub fn archive<R>(
390        repo: &IndexedIdsRepo,
391        opts: &BackupOptions,
392        ls: bool,
393        src: &R,
394        snap: &mut SnapshotFile,
395        backup_paths: &[PathBuf],
396    ) -> Result<()>
397    where
398        R: ReadSource + 'static,
399        <R as ReadSource>::Open: Send,
400        <R as ReadSource>::Iter: Send,
401    {
402        if ls {
403            let lister = LsCmd {
404                long: true,
405                ..Default::default()
406            };
407            lister.display(src.entries().map(|e| Ok(e?.as_tree_entry())))?;
408        } else {
409            let snapshot = std::mem::take(snap);
410            let snapshot = repo.archive(opts, src, snapshot, backup_paths)?;
411            *snap = snapshot;
412        }
413        Ok(())
414    }
415
416    fn backup_snapshot(mut self, source: PathList, repo: &IndexedIdsRepo) -> Result<()> {
417        let config = RUSTIC_APP.config();
418        let snapshot_opts = &config.backup.snapshots;
419        if let Some(path) = &self.as_path {
420            // as_path only works in combination with a single target
421            if source.len() > 1 {
422                bail!("as-path only works with a single source!");
423            }
424            // merge Options from config file using as_path, if given
425            if let Some(path) = path.as_os_str().to_str()
426                && let Some(idx) = snapshot_opts
427                    .iter()
428                    .position(|opt| opt.sources == vec![path])
429            {
430                info!("merging snapshot=\"{path}\" section from config file");
431                self.merge(snapshot_opts[idx].clone());
432            }
433        }
434
435        // use hooks definition before merging "backup" section
436        let hooks = self.hooks.clone();
437
438        // merge "backup" section from config file, if given
439        self.merge(config.backup.clone());
440
441        let hooks = self.hooks(&hooks, "source-specific-backup", &source);
442
443        // use global group-by if not set
444        let mut parent_opts = self.parent_opts;
445        parent_opts.group_by = parent_opts.group_by.or(config.global.group_by);
446
447        let backup_opts = BackupOptions::default()
448            .stdin_filename(self.stdin_filename.unwrap_or_else(|| "stdin".to_string()))
449            .stdin_command(self.stdin_command)
450            .as_path(self.as_path)
451            .parent_opts(parent_opts)
452            .ignore_save_opts(self.ignore_save_opts)
453            .excludes(self.excludes)
454            .ignore_filter_opts(self.ignore_filter_opts)
455            .no_scan(self.no_scan)
456            .dry_run(config.global.dry_run);
457
458        let mut snap = self.snap_opts.to_snapshot()?;
459        snap.program_version = program_version();
460        hooks.use_with(|| {
461            Self::backup_source(&source, self.options, self.ls, backup_opts, &mut snap, repo)
462        })?;
463
464        if self.ls {
465            // no output here
466        } else if config.global.progress_options.json_progress {
467            write_json_progress_summary(&snap)?;
468        } else if self.json {
469            let mut stdout = std::io::stdout();
470            serde_json::to_writer_pretty(&mut stdout, &snap)?;
471        } else if self.long {
472            let mut table = table();
473
474            let add_entry = |title: &str, value: String| {
475                _ = table.add_row([bold_cell(title), Cell::new(value)]);
476            };
477            fill_table(&snap, add_entry);
478
479            println!("{table}");
480        } else {
481            let summary = snap.summary.as_ref().unwrap();
482            info!(
483                "Files:       {} new, {} changed, {} unchanged",
484                summary.files_new, summary.files_changed, summary.files_unmodified
485            );
486            info!(
487                "Dirs:        {} new, {} changed, {} unchanged",
488                summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified
489            );
490            debug!("Data Blobs:  {} new", summary.data_blobs);
491            debug!("Tree Blobs:  {} new", summary.tree_blobs);
492            info!(
493                "Added to the repo: {} (raw: {})",
494                bytes_size_to_string(summary.data_added_packed),
495                bytes_size_to_string(summary.data_added)
496            );
497
498            info!(
499                "processed {} files, {}",
500                summary.total_files_processed,
501                bytes_size_to_string(summary.total_bytes_processed)
502            );
503            info!("snapshot {} successfully saved.", snap.id);
504        }
505
506        if config.global.is_metrics_configured() {
507            // Merge global metrics labels
508            conflate::btreemap::append_or_ignore(
509                &mut self.metrics_labels,
510                config.global.metrics_labels.clone(),
511            );
512            if let Err(err) = publish_metrics(&snap, self.metrics_job, self.metrics_labels) {
513                warn!("error pushing metrics: {err}");
514            }
515        }
516
517        info!("backup of {source} done.");
518        Ok(())
519    }
520}
521
522#[derive(Serialize)]
523struct JsonProgressSummary {
524    message_type: &'static str,
525    files_new: u64,
526    files_changed: u64,
527    files_unmodified: u64,
528    dirs_new: u64,
529    dirs_changed: u64,
530    dirs_unmodified: u64,
531    data_blobs: u64,
532    tree_blobs: u64,
533    data_added: u64,
534    data_added_packed: u64,
535    total_files_processed: u64,
536    total_bytes_processed: u64,
537    total_duration: f64,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    snapshot_id: Option<SnapshotId>,
540}
541
542fn write_json_progress_summary(snap: &SnapshotFile) -> Result<()> {
543    if let Some(summary) = snap.summary.as_ref() {
544        let snapshot_id = (snap.id != SnapshotId::default()).then_some(snap.id);
545        let json_rogress = JsonProgressSummary {
546            message_type: "summary",
547            files_new: summary.files_new,
548            files_changed: summary.files_changed,
549            files_unmodified: summary.files_unmodified,
550            dirs_new: summary.dirs_new,
551            dirs_changed: summary.dirs_changed,
552            dirs_unmodified: summary.dirs_unmodified,
553            data_blobs: summary.data_blobs,
554            tree_blobs: summary.tree_blobs,
555            data_added: summary.data_added,
556            data_added_packed: summary.data_added_packed,
557            total_files_processed: summary.total_files_processed,
558            total_bytes_processed: summary.total_bytes_processed,
559            total_duration: summary.total_duration,
560            snapshot_id,
561        };
562        let mut stdout = std::io::stdout();
563        serde_json::to_writer(&mut stdout, &json_rogress)?;
564        println!();
565    }
566    Ok(())
567}
568
569#[cfg(not(any(feature = "prometheus", feature = "opentelemetry")))]
570fn publish_metrics(
571    snap: &SnapshotFile,
572    job_name: Option<String>,
573    mut labels: BTreeMap<String, String>,
574) -> Result<()> {
575    Err(anyhow!("metrics support is not compiled-in!"))
576}
577
578#[cfg(any(feature = "prometheus", feature = "opentelemetry"))]
579fn publish_metrics(
580    snap: &SnapshotFile,
581    job_name: Option<String>,
582    mut labels: BTreeMap<String, String>,
583) -> Result<()> {
584    use crate::metrics::MetricValue::*;
585    use crate::metrics::{Metric, MetricsExporter};
586
587    let summary = snap.summary.as_ref().expect("Reaching the 'push to prometheus' point should only happen for successful backups, which must have a summary set.");
588    let metrics = [
589        Metric {
590            name: "rustic_backup_time",
591            description: "Timestamp of this snapshot",
592            value: Float(snap.time.timestamp().as_millisecond() as f64 / 1000.),
593        },
594        Metric {
595            name: "rustic_backup_files_new",
596            description: "New files compared to the last (i.e. parent) snapshot",
597            value: Int(summary.files_new),
598        },
599        Metric {
600            name: "rustic_backup_files_changed",
601            description: "Changed files compared to the last (i.e. parent) snapshot",
602            value: Int(summary.files_changed),
603        },
604        Metric {
605            name: "rustic_backup_files_unmodified",
606            description: "Unchanged files compared to the last (i.e. parent) snapshot",
607            value: Int(summary.files_unmodified),
608        },
609        Metric {
610            name: "rustic_backup_total_files_processed",
611            description: "Total processed files",
612            value: Int(summary.total_files_processed),
613        },
614        Metric {
615            name: "rustic_backup_total_bytes_processed",
616            description: "Total size of all processed files",
617            value: Int(summary.total_bytes_processed),
618        },
619        Metric {
620            name: "rustic_backup_dirs_new",
621            description: "New directories compared to the last (i.e. parent) snapshot",
622            value: Int(summary.dirs_new),
623        },
624        Metric {
625            name: "rustic_backup_dirs_changed",
626            description: "Changed directories compared to the last (i.e. parent) snapshot",
627            value: Int(summary.dirs_changed),
628        },
629        Metric {
630            name: "rustic_backup_dirs_unmodified",
631            description: "Unchanged directories compared to the last (i.e. parent) snapshot",
632            value: Int(summary.dirs_unmodified),
633        },
634        Metric {
635            name: "rustic_backup_total_dirs_processed",
636            description: "Total processed directories",
637            value: Int(summary.total_dirs_processed),
638        },
639        Metric {
640            name: "rustic_backup_total_dirsize_processed",
641            description: "Total size of all processed dirs",
642            value: Int(summary.total_dirsize_processed),
643        },
644        Metric {
645            name: "rustic_backup_data_blobs",
646            description: "Total number of data blobs added by this snapshot",
647            value: Int(summary.data_blobs),
648        },
649        Metric {
650            name: "rustic_backup_tree_blobs",
651            description: "Total number of tree blobs added by this snapshot",
652            value: Int(summary.tree_blobs),
653        },
654        Metric {
655            name: "rustic_backup_data_added",
656            description: "Total uncompressed bytes added by this snapshot",
657            value: Int(summary.data_added),
658        },
659        Metric {
660            name: "rustic_backup_data_added_packed",
661            description: "Total bytes added to the repository by this snapshot",
662            value: Int(summary.data_added_packed),
663        },
664        Metric {
665            name: "rustic_backup_data_added_files",
666            description: "Total uncompressed bytes (new/changed files) added by this snapshot",
667            value: Int(summary.data_added_files),
668        },
669        Metric {
670            name: "rustic_backup_data_added_files_packed",
671            description: "Total bytes for new/changed files added to the repository by this snapshot",
672            value: Int(summary.data_added_files_packed),
673        },
674        Metric {
675            name: "rustic_backup_data_added_trees",
676            description: "Total uncompressed bytes (new/changed directories) added by this snapshot",
677            value: Int(summary.data_added_trees),
678        },
679        Metric {
680            name: "rustic_backup_data_added_trees_packed",
681            description: "Total bytes (new/changed directories) added to the repository by this snapshot",
682            value: Int(summary.data_added_trees_packed),
683        },
684        Metric {
685            name: "rustic_backup_backup_start",
686            description: "Start time of the backup. This may differ from the snapshot `time`.",
687            value: Float(summary.backup_start.timestamp().as_millisecond() as f64 / 1000.),
688        },
689        Metric {
690            name: "rustic_backup_backup_end",
691            description: "The time that the backup has been finished.",
692            value: Float(summary.backup_end.timestamp().as_millisecond() as f64 / 1000.),
693        },
694        Metric {
695            name: "rustic_backup_backup_duration",
696            description: "Total duration of the backup in seconds, i.e. the time between `backup_start` and `backup_end`",
697            value: Float(summary.backup_duration),
698        },
699        Metric {
700            name: "rustic_backup_total_duration",
701            description: "Total duration that the rustic command ran in seconds",
702            value: Float(summary.total_duration),
703        },
704    ];
705
706    _ = labels
707        .entry("paths".to_string())
708        .or_insert_with(|| format!("{}", snap.paths));
709    _ = labels
710        .entry("hostname".to_owned())
711        .or_insert_with(|| snap.hostname.clone());
712    _ = labels
713        .entry("snapshot_label".to_string())
714        .or_insert_with(|| snap.label.clone());
715    _ = labels
716        .entry("tags".to_string())
717        .or_insert_with(|| format!("{}", snap.tags));
718
719    let job_name = job_name.as_deref().unwrap_or("rustic_backup");
720    let global_config = &RUSTIC_APP.config().global;
721
722    #[cfg(feature = "prometheus")]
723    if let Some(prometheus_endpoint) = &global_config.prometheus {
724        use crate::metrics::prometheus::PrometheusExporter;
725
726        let metrics_exporter = PrometheusExporter {
727            endpoint: prometheus_endpoint.clone(),
728            job_name: job_name.to_string(),
729            grouping: labels.clone(),
730            prometheus_user: global_config.prometheus_user.clone(),
731            prometheus_pass: global_config.prometheus_pass.clone(),
732        };
733
734        metrics_exporter
735            .push_metrics(metrics.as_slice())
736            .context("pushing prometheus metrics")?;
737    }
738
739    #[cfg(not(feature = "prometheus"))]
740    if global_config.prometheus.is_some() {
741        bail!("prometheus metrics support is not compiled-in!");
742    }
743
744    #[cfg(feature = "opentelemetry")]
745    if let Some(otlp_endpoint) = &global_config.opentelemetry {
746        use crate::metrics::opentelemetry::OpentelemetryExporter;
747
748        let metrics_exporter = OpentelemetryExporter {
749            endpoint: otlp_endpoint.clone(),
750            service_name: job_name.to_string(),
751            labels: global_config.metrics_labels.clone(),
752        };
753
754        metrics_exporter
755            .push_metrics(metrics.as_slice())
756            .context("pushing opentelemetry metrics")?;
757    }
758
759    #[cfg(not(feature = "opentelemetry"))]
760    if global_config.opentelemetry.is_some() {
761        bail!("opentelemetry metrics support is not compiled-in!");
762    }
763
764    Ok(())
765}