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::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#[serde_as]
39#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
40#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
41#[allow(clippy::struct_excessive_bools)]
47pub struct BackupCmd {
48 #[clap(value_name = "SOURCE", value_hint = ValueHint::AnyPath)]
51 #[merge(skip)]
52 #[serde(skip)]
53 cli_sources: Vec<String>,
54
55 #[clap(long = "name", value_name = "NAME", conflicts_with = "cli_sources")]
57 #[merge(skip)]
58 #[serde(skip)]
59 cli_name: Vec<String>,
60
61 #[clap(long)]
63 #[merge(skip)]
64 #[serde(skip)]
65 ls: bool,
66
67 #[clap(skip)]
68 #[merge(skip)]
69 name: Option<String>,
70
71 #[clap(long, value_name = "FILENAME", value_hint = ValueHint::FilePath)]
73 #[merge(strategy=conflate::option::overwrite_none)]
74 stdin_filename: Option<String>,
75
76 #[clap(long, value_name = "COMMAND")]
78 #[merge(strategy=conflate::option::overwrite_none)]
79 stdin_command: Option<CommandInput>,
80
81 #[clap(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
83 #[merge(strategy=conflate::option::overwrite_none)]
84 as_path: Option<PathBuf>,
85
86 #[clap(long)]
88 #[merge(strategy=conflate::bool::overwrite_false)]
89 pub no_scan: bool,
90
91 #[clap(long)]
93 #[merge(strategy=conflate::bool::overwrite_false)]
94 json: bool,
95
96 #[clap(long, conflicts_with = "json")]
98 #[merge(strategy=conflate::bool::overwrite_false)]
99 long: bool,
100
101 #[clap(long)]
103 #[merge(strategy=conflate::bool::overwrite_false)]
104 init: bool,
105
106 #[clap(flatten, next_help_heading = "Node modification options")]
108 #[serde(flatten)]
109 ignore_save_opts: LocalSourceSaveOptions,
110
111 #[clap(flatten, next_help_heading = "Options for parent processing")]
113 #[serde(flatten)]
114 parent_opts: ParentOptions,
115
116 #[clap(flatten, next_help_heading = "Exclude options")]
118 #[serde(flatten)]
119 excludes: Excludes,
120
121 #[clap(flatten, next_help_heading = "Exclude options for local source")]
123 #[serde(flatten)]
124 ignore_filter_opts: LocalSourceFilterOptions,
125
126 #[clap(flatten, next_help_heading = "Snapshot options")]
128 #[serde(flatten)]
129 snap_opts: SnapshotOptions,
130
131 #[clap(flatten, next_help_heading = "Key options (when using --init)")]
133 #[serde(skip)]
134 #[merge(skip)]
135 key_opts: KeyOptions,
136
137 #[clap(flatten, next_help_heading = "Config options (when using --init)")]
139 #[serde(skip)]
140 #[merge(skip)]
141 config_opts: ConfigOptions,
142
143 #[clap(skip)]
145 hooks: Hooks,
146
147 #[clap(skip)]
149 #[merge(strategy = merge_snapshots)]
150 snapshots: Vec<Self>,
151
152 #[clap(skip)]
154 #[merge(skip)]
155 sources: Vec<String>,
156
157 #[clap(skip)]
159 #[merge(strategy = conflate::btreemap::append_or_ignore)]
160 options: BTreeMap<String, String>,
161
162 #[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 #[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 if !self.sources.is_empty() {
177 return Err("key \"sources\" is not valid in the [backup] section!");
178 }
179
180 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 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
194pub(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 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 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(|(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 && 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 if source.len() > 1 {
422 bail!("as-path only works with a single source!");
423 }
424 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 let hooks = self.hooks.clone();
437
438 self.merge(config.backup.clone());
440
441 let hooks = self.hooks(&hooks, "source-specific-backup", &source);
442
443 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 } 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 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}