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