packages/ak-common/tracing: get sentry config from API for outposts (#21625)

This commit is contained in:
Marc 'risson' Schmitt
2026-04-16 12:00:01 +00:00
committed by GitHub
parent b3e7a01f10
commit 1b53426e2c
7 changed files with 297 additions and 16 deletions

14
Cargo.lock generated
View File

@@ -178,6 +178,7 @@ name = "authentik-common"
version = "2026.5.0-rc1"
dependencies = [
"arc-swap",
"authentik-client",
"aws-lc-rs",
"axum-server",
"config",
@@ -189,6 +190,8 @@ dependencies = [
"nix 0.31.2",
"notify",
"pin-project-lite",
"reqwest",
"reqwest-middleware",
"rustls",
"sentry",
"serde",
@@ -198,6 +201,7 @@ dependencies = [
"thiserror 2.0.18",
"time",
"tokio",
"tokio-retry2",
"tokio-util",
"tracing",
"tracing-error",
@@ -3621,6 +3625,16 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-retry2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0266d56e57e6b29becbfce5daa6add8730941ca8192ddd7c24d25bf90c32a743"
dependencies = [
"pin-project",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.4"

View File

@@ -90,6 +90,7 @@ tempfile = "= 3.27.0"
thiserror = "= 2.0.18"
time = { version = "= 0.3.47", features = ["macros"] }
tokio = { version = "= 1.51.1", features = ["full", "tracing"] }
tokio-retry2 = "= 0.9.1"
tokio-rustls = "= 0.26.4"
tokio-util = { version = "= 0.7.18", features = ["full"] }
tower = "= 0.5.3"
@@ -105,6 +106,7 @@ tracing-subscriber = { version = "= 0.3.23", features = [
url = "= 2.5.8"
uuid = { version = "= 1.23.0", features = ["serde", "v4"] }
ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" }
ak-common = { package = "authentik-common", version = "2026.5.0-rc1", path = "./packages/ak-common", default-features = false }
[profile.dev.package.backtrace]

View File

@@ -15,6 +15,7 @@ core = ["dep:sqlx"]
proxy = []
[dependencies]
ak-client.workspace = true
arc-swap.workspace = true
aws-lc-rs.workspace = true
axum-server.workspace = true
@@ -26,6 +27,8 @@ ipnet.workspace = true
json-subscriber.workspace = true
notify.workspace = true
pin-project-lite.workspace = true
reqwest.workspace = true
reqwest-middleware.workspace = true
rustls.workspace = true
sentry.workspace = true
serde.workspace = true
@@ -33,6 +36,7 @@ serde_json.workspace = true
sqlx = { workspace = true, optional = true }
thiserror.workspace = true
time.workspace = true
tokio-retry2.workspace = true
tokio-util.workspace = true
tokio.workspace = true
tracing-error.workspace = true

View File

@@ -0,0 +1,193 @@
//! Utilities for working with the authentik API client.
use ak_client::apis::configuration::Configuration;
use eyre::{Result, eyre};
use url::Url;
use crate::{config, user_agent_outpost};
pub struct ServerConfig {
pub host: Url,
pub token: String,
pub insecure: bool,
}
impl ServerConfig {
pub fn new() -> Result<Self> {
let host = config::get()
.host
.clone()
.ok_or_else(|| eyre!("environment variable `AUTHENTIK_HOST` not set"))?;
let mut host: Url = host.parse()?;
let token = config::get()
.token
.clone()
.ok_or_else(|| eyre!("environment variable `AUTHENTIK_TOKEN` not set"))?;
let insecure = config::get().insecure.unwrap_or(false);
if !host.path().ends_with('/') {
host.path_segments_mut()
.map_err(|()| eyre!("URL cannot be a base"))?
.push("");
}
Ok(Self {
host,
token,
insecure,
})
}
}
/// Return a [`Configuration`] object based on external environment variables.
pub fn make_config() -> Result<Configuration> {
let server_config = ServerConfig::new()?;
let base_path = server_config.host.join("api/v3")?.into();
let client = reqwest::ClientBuilder::new()
.tls_danger_accept_invalid_hostnames(server_config.insecure)
.tls_danger_accept_invalid_certs(server_config.insecure)
.build()?;
let client = reqwest_middleware::ClientBuilder::new(client).build();
Ok(Configuration {
base_path,
client,
bearer_access_token: Some(server_config.token),
user_agent: Some(user_agent_outpost()),
..Default::default()
})
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{ServerConfig, make_config};
use crate::config;
#[test]
fn server_config_no_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000",
"token": "token",
}))
.expect("failed to set config");
let server_config = ServerConfig::new().expect("failed to create server config");
assert_eq!(server_config.host.as_str(), "http://localhost:9000/");
}
#[test]
fn server_config_with_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/",
"token": "token",
}))
.expect("failed to set config");
let server_config = ServerConfig::new().expect("failed to create server config");
assert_eq!(server_config.host.as_str(), "http://localhost:9000/");
}
#[test]
fn server_config_with_path_no_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/authentik",
"token": "token",
}))
.expect("failed to set config");
let server_config = ServerConfig::new().expect("failed to create server config");
assert_eq!(
server_config.host.as_str(),
"http://localhost:9000/authentik/"
);
}
#[test]
fn server_config_with_path_and_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/authentik/",
"token": "token",
}))
.expect("failed to set config");
let server_config = ServerConfig::new().expect("failed to create server config");
assert_eq!(
server_config.host.as_str(),
"http://localhost:9000/authentik/"
);
}
#[test]
fn make_config_no_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000",
"token": "token",
}))
.expect("failed to set config");
let api_config = make_config().expect("failed to make config");
assert_eq!(api_config.base_path, "http://localhost:9000/api/v3");
}
#[test]
fn make_config_with_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/",
"token": "token",
}))
.expect("failed to set config");
let api_config = make_config().expect("failed to make config");
assert_eq!(api_config.base_path, "http://localhost:9000/api/v3");
}
#[test]
fn make_config_with_path_no_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/authentik",
"token": "token",
}))
.expect("failed to set config");
let api_config = make_config().expect("failed to make config");
assert_eq!(
api_config.base_path,
"http://localhost:9000/authentik/api/v3"
);
}
#[test]
fn make_config_with_path_and_trailing_slash() {
config::init().expect("failed to init config");
config::set(json!({
"host": "http://localhost:9000/authentik/",
"token": "token",
}))
.expect("failed to set config");
let api_config = make_config().expect("failed to make config");
assert_eq!(
api_config.base_path,
"http://localhost:9000/authentik/api/v3"
);
}
}

View File

@@ -23,6 +23,11 @@ pub struct Config {
pub web: WebConfig,
pub worker: WorkerConfig,
// Outpost specific fields
pub host: Option<String>,
pub token: Option<String>,
pub insecure: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -1,5 +1,6 @@
//! Various utilities used by other crates
pub mod api;
pub mod arbiter;
pub use arbiter::{Arbiter, Event, Tasks};
pub mod config;
@@ -26,6 +27,10 @@ pub fn authentik_full_version() -> String {
}
}
pub fn user_agent_outpost() -> String {
format!("goauthentik.io/outpost/{}", authentik_full_version())
}
pub fn authentik_user_agent() -> String {
format!("authentik@{}", authentik_full_version())
}

View File

@@ -128,33 +128,91 @@ mod json {
/// Utilities for Sentry
pub mod sentry {
use std::str::FromStr as _;
use std::{str::FromStr as _, time::Duration};
use tracing::trace;
use ak_client::apis::root_api::root_config_retrieve;
use eyre::{Error, Result};
use tokio_retry2::{Retry, RetryError, strategy::FixedInterval};
use tracing::{error, trace};
use crate::{VERSION, authentik_user_agent, config};
use crate::{
Mode, VERSION, api, authentik_user_agent,
config::{self, schema::ErrorReportingConfig},
};
fn get_config() -> Result<ErrorReportingConfig> {
// In non-core mode, we are running an outpost and need to grab the error reporting
// configuration from the API.
if Mode::is_core() {
return Ok(config::get().error_reporting.clone());
}
let api_config = api::make_config()?;
let config = {
let retry_strategy = FixedInterval::new(Duration::from_secs(3));
let retrieve_config = async || {
root_config_retrieve(&api_config)
.await
.map_err(Error::new)
.map_err(RetryError::transient)
};
let retry_notify = |err: &Error, _duration| {
error!(
?err,
"Failed to fetch configuration from API, retrying in 3 seconds"
);
};
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(Retry::spawn_notify(
retry_strategy,
retrieve_config,
retry_notify,
))?
};
let config = config.error_reporting;
Ok(ErrorReportingConfig {
enabled: config.enabled,
sentry_dsn: Some(config.sentry_dsn),
environment: config.environment,
send_pii: config.send_pii,
#[expect(
clippy::cast_possible_truncation,
reason = "This is fine, we'll never get big values here."
)]
#[expect(
clippy::as_conversions,
reason = "This is fine, we'll never get big values here."
)]
sample_rate: config.traces_sample_rate as f32,
})
}
/// Install the sentry client. This must happen before [`super::install`] is called.
pub fn install() -> sentry::ClientInitGuard {
pub fn install() -> Result<Option<sentry::ClientInitGuard>> {
let config = get_config()?;
if !config.enabled {
return Ok(None);
}
trace!("setting up sentry");
let config = config::get();
sentry::init(sentry::ClientOptions {
dsn: config.error_reporting.sentry_dsn.clone().map(|dsn| {
let debug = config::get().debug;
Ok(Some(sentry::init(sentry::ClientOptions {
dsn: config.sentry_dsn.clone().map(|dsn| {
sentry::types::Dsn::from_str(&dsn).expect("Failed to create sentry DSN")
}),
release: Some(format!("authentik@{VERSION}").into()),
environment: Some(config.error_reporting.environment.clone().into()),
environment: Some(config.environment.clone().into()),
attach_stacktrace: true,
send_default_pii: config.error_reporting.send_pii,
sample_rate: config.error_reporting.sample_rate,
traces_sample_rate: if config.debug {
1.0
} else {
config.error_reporting.sample_rate
},
send_default_pii: config.send_pii,
sample_rate: config.sample_rate,
traces_sample_rate: if debug { 1.0 } else { config.sample_rate },
user_agent: authentik_user_agent().into(),
..sentry::ClientOptions::default()
})
})))
}
}