Consolidate telemetry init into quire-core
Move SentryConfig into telemetry.rs and add init_telemetry() that
does both tracing and sentry setup in one call. Telemetry::Error
covers both filter and sentry/secret errors with thiserror + Diagnostic
derives. Both quire-server and quire-ci now use the same init_telemetry
entry point. Removed separate init_sentry functions from both crates.

Assisted-by: Owl Alpha via pi
change llrkpkoxmmpzusulnvsmuzqxsuksxqlu
commit f6285728af0659603752bc0ebf51432f87743d48
author Alpha Chen <alpha@kejadlen.dev>
date
parent monnoslm
diff --git a/quire-ci/src/quire.rs b/quire-ci/src/quire.rs
index 4b33fa4..83b27f1 100644
--- a/quire-ci/src/quire.rs
+++ b/quire-ci/src/quire.rs
@@ -19,10 +19,7 @@ fn default_port() -> u16 {
     3000
 }
 
-#[derive(serde::Deserialize, Debug, Clone)]
-pub struct SentryConfig {
-    pub dsn: quire_core::secret::SecretString,
-}
+pub use quire_core::telemetry::SentryConfig;
 
 /// Application runtime context.
 ///
diff --git a/quire-ci/src/server.rs b/quire-ci/src/server.rs
index 6df6f2a..edfe0ae 100644
--- a/quire-ci/src/server.rs
+++ b/quire-ci/src/server.rs
@@ -2,6 +2,7 @@ use std::net::SocketAddr;
 
 use axum::Router;
 use axum::routing::get;
+use quire_core::telemetry::{self, FmtMode};
 
 use crate::quire::QuireCi;
 
@@ -20,11 +21,11 @@ pub enum Error {
     #[error("io error: {0}")]
     Io(#[from] std::io::Error),
 
-    #[error("telemetry init error: {0}")]
-    Telemetry(#[from] quire_core::telemetry::Error),
-
-    #[error("secret error: {0}")]
+    #[error(transparent)]
     Secret(#[from] quire_core::secret::Error),
+
+    #[error(transparent)]
+    Telemetry(#[from] quire_core::telemetry::Error),
 }
 
 pub type Result<T> = std::result::Result<T, Error>;
@@ -32,11 +33,12 @@ pub type Result<T> = std::result::Result<T, Error>;
 pub async fn run(quire: QuireCi) -> Result<()> {
     let port = quire.config().port;
 
-    let _sentry = init_sentry(&quire)?;
-    let miette_layer = quire_core::telemetry::MietteLayer::new();
-    let _tracing_guard = quire_core::telemetry::init_tracing(
+    let miette_layer = telemetry::MietteLayer::new();
+    let (_tracing_guard, _sentry_guard) = telemetry::init_telemetry(
         miette_layer,
-        quire_core::telemetry::FmtMode::AutoJson,
+        FmtMode::AutoJson,
+        quire.config().sentry.as_ref(),
+        VERSION,
     )?;
 
     let app = Router::new()
@@ -52,15 +54,3 @@ pub async fn run(quire: QuireCi) -> Result<()> {
 
     Ok(())
 }
-
-/// Initialize Sentry if the global config provides a DSN.
-fn init_sentry(quire: &QuireCi) -> Result<Option<sentry::ClientInitGuard>> {
-    let Some(sentry_config) = quire.config().sentry.as_ref() else {
-        return Ok(None);
-    };
-    let dsn = sentry_config.dsn.reveal()?;
-    Ok(Some(sentry::init((
-        dsn,
-        quire_core::telemetry::sentry_client_options(VERSION),
-    ))))
-}
diff --git a/quire-core/src/secret.rs b/quire-core/src/secret.rs
index 73ffc33..827c807 100644
--- a/quire-core/src/secret.rs
+++ b/quire-core/src/secret.rs
@@ -3,7 +3,7 @@ use std::path::PathBuf;
 use std::sync::{Arc, OnceLock};
 
 /// Errors produced by secret resolution.
-#[derive(Debug, Clone, thiserror::Error)]
+#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
 pub enum Error {
     /// Secret could not be resolved. The source error is preserved for
     /// diagnostics. `Arc` provides `Clone` without requiring the inner
diff --git a/quire-core/src/telemetry.rs b/quire-core/src/telemetry.rs
index bec791f..f9d7a36 100644
--- a/quire-core/src/telemetry.rs
+++ b/quire-core/src/telemetry.rs
@@ -1,16 +1,24 @@
 use std::cell::RefCell;
 use std::io::IsTerminal;
-use std::result::Result;
 use std::sync::Arc;
 
 use opentelemetry::trace::TracerProvider as _;
 use tracing_subscriber::EnvFilter;
 
-/// Errors that can occur during tracing initialization.
+/// Errors that can occur during telemetry initialization.
 #[derive(Debug, thiserror::Error, miette::Diagnostic)]
 pub enum Error {
-    #[error("invalid log filter: {0}")]
+    #[error("invalid log filter")]
     Filter(#[from] tracing_subscriber::filter::FromEnvError),
+
+    #[error(transparent)]
+    Secret(#[from] crate::secret::Error),
+}
+
+/// Sentry configuration extracted from the global config.
+#[derive(serde::Deserialize, Debug, Clone)]
+pub struct SentryConfig {
+    pub dsn: crate::secret::SecretString,
 }
 
 use tracing_subscriber::Layer;
@@ -217,7 +225,10 @@ impl Drop for TracingGuard {
 /// Layer ordering is baked in: `miette_layer` registers before
 /// `sentry_tracing::layer()` so its thread-local is populated when
 /// sentry-tracing's `on_event` calls `capture_event`.
-pub fn init_tracing(miette_layer: MietteLayer, fmt_mode: FmtMode) -> Result<TracingGuard, Error> {
+pub fn init_tracing(
+    miette_layer: MietteLayer,
+    fmt_mode: FmtMode,
+) -> std::result::Result<TracingGuard, Error> {
     let filter = EnvFilter::builder().with_env_var("QUIRE_LOG").from_env()?;
 
     let layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
@@ -252,6 +263,30 @@ pub fn init_tracing(miette_layer: MietteLayer, fmt_mode: FmtMode) -> Result<Trac
     Ok(TracingGuard { provider })
 }
 
+/// Initialize both tracing and Sentry.
+///
+/// Sets up the global tracing subscriber (OTEL + stderr fmt) and, if a
+/// Sentry config is provided, initializes the Sentry client. Returns both
+/// guards so the caller can control drop order.
+pub fn init_telemetry(
+    miette_layer: MietteLayer,
+    fmt_mode: FmtMode,
+    sentry_config: Option<&SentryConfig>,
+    release: &'static str,
+) -> std::result::Result<(TracingGuard, Option<sentry::ClientInitGuard>), Error> {
+    let tracing_guard = init_tracing(miette_layer, fmt_mode)?;
+
+    let sentry_guard = match sentry_config {
+        Some(config) => {
+            let dsn = config.dsn.reveal()?;
+            Some(sentry::init((dsn, sentry_client_options(release))))
+        }
+        None => None,
+    };
+
+    Ok((tracing_guard, sentry_guard))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/quire-server/src/bin/quire/main.rs b/quire-server/src/bin/quire/main.rs
index e8fb724..61c7105 100644
--- a/quire-server/src/bin/quire/main.rs
+++ b/quire-server/src/bin/quire/main.rs
@@ -7,7 +7,6 @@ use miette::IntoDiagnostic;
 use miette::Result;
 use quire::Quire;
 use quire_core::telemetry::{self, FmtMode, MietteLayer};
-use sentry::ClientInitGuard;
 
 const VERSION: &str = env!("QUIRE_VERSION");
 
@@ -101,40 +100,6 @@ enum CiCommands {
     },
 }
 
-/// Initialize Sentry if the global config provides a DSN.
-///
-/// Returns the guard if initialized, or None if Sentry is not configured.
-/// Logs a warning on failure but does not abort.
-fn init_sentry(quire: &Quire) -> Option<ClientInitGuard> {
-    let config = match quire.global_config() {
-        Ok(config) => config,
-        Err(e) => {
-            tracing::warn!(
-                error = &e as &(dyn std::error::Error + 'static),
-                "failed to load global config, skipping Sentry init"
-            );
-            return None;
-        }
-    };
-
-    let sentry_config = config.sentry.as_ref()?;
-    let dsn = match sentry_config.dsn.reveal() {
-        Ok(dsn) => dsn,
-        Err(e) => {
-            tracing::warn!(
-                error = &e as &(dyn std::error::Error + 'static),
-                "failed to resolve Sentry DSN, skipping Sentry init"
-            );
-            return None;
-        }
-    };
-
-    Some(sentry::init((
-        dsn,
-        telemetry::sentry_client_options(VERSION),
-    )))
-}
-
 #[tokio::main]
 async fn main() -> Result<()> {
     let cli = Cli::parse();
@@ -143,12 +108,18 @@ async fn main() -> Result<()> {
         Some(ref dir) => Quire::new(dir.into()),
         None => Quire::default(),
     };
-    let _sentry = init_sentry(&quire);
+
+    let sentry_config = quire.global_config().ok().and_then(|c| c.sentry);
     let miette_layer = MietteLayer::new()
         .with_type::<quire::Error>()
         .with_type::<quire::ci::Error>()
         .with_type::<quire_core::fennel::FennelError>();
-    telemetry::init_tracing(miette_layer, FmtMode::AutoJson)?;
+    let (_tracing_guard, _sentry_guard) = telemetry::init_telemetry(
+        miette_layer,
+        FmtMode::AutoJson,
+        sentry_config.as_ref(),
+        VERSION,
+    )?;
 
     if let Some(shell) = cli.completions {
         clap_complete::generate(shell, &mut Cli::command(), "quire", &mut std::io::stdout());
diff --git a/quire-server/src/lib.rs b/quire-server/src/lib.rs
index 2f268e2..781c637 100644
--- a/quire-server/src/lib.rs
+++ b/quire-server/src/lib.rs
@@ -4,6 +4,8 @@ mod error;
 pub mod event;
 pub mod quire;
 
+pub use quire_core::telemetry::SentryConfig;
+
 pub use error::Error;
 pub use error::Result;
 pub use quire::Quire;
diff --git a/quire-server/src/quire/mod.rs b/quire-server/src/quire/mod.rs
index 525b8ad..d71e66d 100644
--- a/quire-server/src/quire/mod.rs
+++ b/quire-server/src/quire/mod.rs
@@ -8,6 +8,8 @@ pub mod web;
 
 use crate::ci::{Ci, Executor, Runs};
 use crate::{Error, Result as AppResult};
+pub use quire_core::telemetry::SentryConfig;
+
 use quire_core::fennel::Fennel;
 use quire_core::secret::SecretString;
 
@@ -45,11 +47,6 @@ pub struct CiConfig {
     pub executor: Executor,
 }
 
-#[derive(serde::Deserialize, Debug)]
-pub struct SentryConfig {
-    pub dsn: SecretString,
-}
-
 /// A resolved repository path.
 ///
 /// Created by `Quire::repo` after validating the name.