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
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.