commit 13c9b4ab6973f28e13fefcf4bd5e23f2a6fdf211
parent b0318dafece6648b8986afefe9a91ed629a93c44
Author: triesap <tyson@radroots.org>
Date: Sat, 6 Jun 2026 12:30:13 -0700
observability: add tracing setup
- add runtime tracing config with compact and JSON formats
- initialize CLI tracing subscribers from runtime config
- emit command and server lifecycle trace events
- assert relay trace output in the integration run path
Diffstat:
6 files changed, 428 insertions(+), 16 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -359,7 +359,7 @@ dependencies = [
"bitflags",
"cexpr",
"clang-sys",
- "itertools 0.11.0",
+ "itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
@@ -2208,6 +2208,15 @@ dependencies = [
]
[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2336,6 +2345,15 @@ dependencies = [
]
[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3523,6 +3541,15 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4037,6 +4064,8 @@ dependencies = [
"tangle_test_support",
"tokio",
"tokio-tungstenite 0.29.0",
+ "tracing",
+ "tracing-subscriber",
]
[[package]]
@@ -4106,6 +4135,7 @@ dependencies = [
"tangle_test_support",
"tokio",
"tower",
+ "tracing",
"url",
]
@@ -4485,6 +4515,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
]
[[package]]
@@ -4633,6 +4706,12 @@ dependencies = [
]
[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
name = "vart"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/tangle/Cargo.toml b/crates/tangle/Cargo.toml
@@ -11,6 +11,8 @@ readme = "../../README"
[dependencies]
tangle_runtime = { path = "../tangle_runtime" }
tokio = { version = "1", features = ["rt", "signal"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
[dev-dependencies]
futures-util = "0.3"
diff --git a/crates/tangle/src/lib.rs b/crates/tangle/src/lib.rs
@@ -296,6 +296,7 @@ pub fn projection_rebuild_output(report: tangle_runtime::RuntimeProjectionRebuil
pub async fn migrate_with_config(path: &str) -> Result<String, String> {
let config = tangle_runtime::load_runtime_config(path).map_err(|error| error.to_string())?;
+ initialize_tracing(config.tracing_config())?;
let report = tangle_runtime::migrate_runtime_database(&config)
.await
.map_err(|error| error.to_string())?;
@@ -308,6 +309,7 @@ pub async fn event_import_with_config(
) -> Result<String, String> {
let config =
tangle_runtime::load_runtime_config(config_path).map_err(|error| error.to_string())?;
+ initialize_tracing(config.tracing_config())?;
let report = tangle_runtime::import_events_from_path(&config, input_path)
.await
.map_err(|error| error.to_string())?;
@@ -320,6 +322,7 @@ pub async fn event_export_with_config(
) -> Result<String, String> {
let config =
tangle_runtime::load_runtime_config(config_path).map_err(|error| error.to_string())?;
+ initialize_tracing(config.tracing_config())?;
let report = tangle_runtime::export_events_to_path(&config, output_path)
.await
.map_err(|error| error.to_string())?;
@@ -329,6 +332,7 @@ pub async fn event_export_with_config(
pub async fn projection_rebuild_with_config(config_path: &str) -> Result<String, String> {
let config =
tangle_runtime::load_runtime_config(config_path).map_err(|error| error.to_string())?;
+ initialize_tracing(config.tracing_config())?;
let report = tangle_runtime::rebuild_projections(&config)
.await
.map_err(|error| error.to_string())?;
@@ -337,6 +341,7 @@ pub async fn projection_rebuild_with_config(config_path: &str) -> Result<String,
pub async fn run_with_config(path: &str) -> Result<(), String> {
let config = tangle_runtime::load_runtime_config(path).map_err(|error| error.to_string())?;
+ initialize_tracing(config.tracing_config())?;
let (shutdown, _) = tangle_runtime::GracefulShutdownSignal::new();
let signal = shutdown.clone();
tokio::spawn(async move {
@@ -350,17 +355,46 @@ pub async fn run_with_config(path: &str) -> Result<(), String> {
.map_err(|error| error.to_string())
}
+fn initialize_tracing(config: &tangle_runtime::RuntimeTracingConfig) -> Result<(), String> {
+ if !config.enabled() {
+ return Ok(());
+ }
+ let filter = tracing_subscriber::EnvFilter::try_new(config.filter())
+ .map_err(|error| format!("tracing filter is invalid: {error}"))?;
+ match config.format() {
+ tangle_runtime::RuntimeTracingFormat::Compact => {
+ let _ = tracing_subscriber::fmt()
+ .with_env_filter(filter)
+ .with_writer(std::io::stderr)
+ .try_init();
+ }
+ tangle_runtime::RuntimeTracingFormat::Json => {
+ let _ = tracing_subscriber::fmt()
+ .with_env_filter(filter)
+ .with_writer(std::io::stderr)
+ .json()
+ .try_init();
+ }
+ }
+ tracing::info!(
+ filter = config.filter(),
+ format = config.format().as_str(),
+ "tracing initialized"
+ );
+ Ok(())
+}
+
#[cfg(test)]
mod tests {
use super::{
PACKAGE_NAME, PACKAGE_VERSION, TangleCliError, TangleCommand, TangleInvocation,
- event_export_output, event_import_output, migrate_output, parse_tangle_command,
- parse_tangle_invocation, projection_rebuild_output, require_config_path,
- require_input_path, require_output_path, usage_output, version_output,
+ event_export_output, event_import_output, initialize_tracing, migrate_output,
+ parse_tangle_command, parse_tangle_invocation, projection_rebuild_output,
+ require_config_path, require_input_path, require_output_path, usage_output, version_output,
};
use tangle_runtime::{
RuntimeEventExportReport, RuntimeEventImportReport, RuntimeMigrationReport,
- RuntimeProjectionRebuildReport,
+ RuntimeProjectionRebuildReport, RuntimeTracingConfig, RuntimeTracingFormat,
};
#[test]
@@ -379,6 +413,21 @@ mod tests {
}
#[test]
+ fn tracing_setup_ignores_disabled_config_and_rejects_bad_filters() {
+ assert_eq!(
+ initialize_tracing(&RuntimeTracingConfig::disabled()),
+ Ok(())
+ );
+ let invalid =
+ RuntimeTracingConfig::new(true, "bad[", RuntimeTracingFormat::Compact).expect("config");
+ assert!(
+ initialize_tracing(&invalid)
+ .expect_err("invalid filter")
+ .starts_with("tracing filter is invalid:")
+ );
+ }
+
+ #[test]
fn usage_output_lists_supported_command_model() {
assert_eq!(
usage_output(),
diff --git a/crates/tangle/tests/run_integration.rs b/crates/tangle/tests/run_integration.rs
@@ -41,6 +41,13 @@ async fn tangle_run_serves_relay_clients_and_persists_surreal_state() {
"window_seconds": 60
}
}),
+ Some(serde_json::json!({
+ "tracing": {
+ "enabled": true,
+ "filter": "info,tangle=info,tangle_runtime=info",
+ "format": "json"
+ }
+ })),
);
let mut relay = Command::new(env!("CARGO_BIN_EXE_tangle"))
@@ -404,7 +411,10 @@ async fn tangle_run_serves_relay_clients_and_persists_surreal_state() {
assert!(seller_detail.contains("\"display_name\":\"Radroots Market\""));
assert!(seller_detail.contains("\"regions\":[\"cascadia\",\"pnw\"]"));
- stop_relay(relay);
+ let trace_output = stop_relay_with_stderr(relay);
+ assert!(trace_output.contains("tracing initialized"));
+ assert!(trace_output.contains("starting runtime server"));
+ assert!(trace_output.contains("\"format\":\"json\""));
let store_config =
SurrealConnectionConfig::rocksdb(db_path.to_str().expect("db path"), "tangle_it", "relay")
@@ -638,6 +648,7 @@ async fn tangle_run_persists_durable_write_rate_limits() {
"window_seconds": 60
}
}),
+ None,
);
let mut relay = spawn_relay(&config_path);
wait_for_http(port, &mut relay);
@@ -735,6 +746,7 @@ async fn tangle_run_serves_admin_policy_api() {
serde_json::json!({
"admin_pubkeys": [admin.as_str()]
}),
+ None,
);
let mut relay = spawn_relay(&config_path);
wait_for_http(port, &mut relay);
@@ -887,7 +899,7 @@ async fn run_policy_write_scenario(
let db_path = root.join("surrealdb");
let config_path = root.join("runtime.json");
fs::create_dir_all(&root).expect("runtime root");
- write_runtime_config(&config_path, &db_path, port, namespace, policy);
+ write_runtime_config(&config_path, &db_path, port, namespace, policy, None);
let mut relay = spawn_relay(&config_path);
wait_for_http(port, &mut relay);
let (mut client, _) = connect_async(format!("ws://127.0.0.1:{port}/ws"))
@@ -935,8 +947,15 @@ fn spawn_relay(config_path: &Path) -> Child {
.expect("spawn tangle run")
}
-fn write_runtime_config(path: &Path, db_path: &Path, port: u16, namespace: &str, policy: Value) {
- let config = serde_json::json!({
+fn write_runtime_config(
+ path: &Path,
+ db_path: &Path,
+ port: u16,
+ namespace: &str,
+ policy: Value,
+ observability: Option<Value>,
+) {
+ let mut config = serde_json::json!({
"server": {
"listen_addr": format!("127.0.0.1:{port}"),
"relay_url": "wss://relay.radroots.test"
@@ -958,6 +977,9 @@ fn write_runtime_config(path: &Path, db_path: &Path, port: u16, namespace: &str,
},
"policy": policy
});
+ if let Some(observability) = observability {
+ config["observability"] = observability;
+ }
fs::write(
path,
serde_json::to_string_pretty(&config).expect("config JSON"),
@@ -1292,10 +1314,15 @@ fn forum_thread_comment(
.expect("forum comment event")
}
-fn stop_relay(mut relay: Child) {
+fn stop_relay(relay: Child) {
+ let _ = stop_relay_with_stderr(relay);
+}
+
+fn stop_relay_with_stderr(mut relay: Child) -> String {
stop_child(&mut relay);
- let status = relay.wait().expect("relay exit");
- assert!(status.success());
+ let output = relay.wait_with_output().expect("relay exit");
+ assert!(output.status.success());
+ String::from_utf8_lossy(&output.stderr).to_string()
}
#[cfg(unix)]
diff --git a/crates/tangle_runtime/Cargo.toml b/crates/tangle_runtime/Cargo.toml
@@ -18,6 +18,7 @@ tangle_protocol = { path = "../tangle_protocol" }
tangle_store = { path = "../tangle_store" }
tangle_store_surreal = { path = "../tangle_store_surreal" }
tokio = { version = "1", features = ["net", "sync"] }
+tracing = "0.1"
url = "2"
[dev-dependencies]
diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs
@@ -243,6 +243,7 @@ pub struct TangleRuntimeConfig {
durable_write_rate_limit: Option<RateLimitConfig>,
admin_pubkeys: BTreeSet<PublicKeyHex>,
limits: RuntimeLimits,
+ tracing: RuntimeTracingConfig,
}
impl TangleRuntimeConfig {
@@ -274,6 +275,10 @@ impl TangleRuntimeConfig {
self.limits
}
+ pub fn tracing_config(&self) -> &RuntimeTracingConfig {
+ &self.tracing
+ }
+
pub fn websocket_state(&self, shutdown_signal: GracefulShutdownSignal) -> WebSocketHttpState {
WebSocketHttpState::with_shutdown(self.relay_connection.clone(), shutdown_signal)
}
@@ -284,6 +289,68 @@ impl TangleRuntimeConfig {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum RuntimeTracingFormat {
+ Compact,
+ Json,
+}
+
+impl RuntimeTracingFormat {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Compact => "compact",
+ Self::Json => "json",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RuntimeTracingConfig {
+ enabled: bool,
+ filter: String,
+ format: RuntimeTracingFormat,
+}
+
+impl RuntimeTracingConfig {
+ pub fn new(
+ enabled: bool,
+ filter: impl Into<String>,
+ format: RuntimeTracingFormat,
+ ) -> Result<Self, RuntimeConfigError> {
+ let filter = filter.into();
+ if filter.trim().is_empty() {
+ return Err(RuntimeConfigError::invalid(
+ "observability.tracing.filter must not be empty",
+ ));
+ }
+ Ok(Self {
+ enabled,
+ filter: filter.trim().to_owned(),
+ format,
+ })
+ }
+
+ pub fn disabled() -> Self {
+ Self {
+ enabled: false,
+ filter: "info,tangle=info,tangle_runtime=info".to_owned(),
+ format: RuntimeTracingFormat::Compact,
+ }
+ }
+
+ pub fn enabled(&self) -> bool {
+ self.enabled
+ }
+
+ pub fn filter(&self) -> &str {
+ &self.filter
+ }
+
+ pub fn format(&self) -> RuntimeTracingFormat {
+ self.format
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeConfigErrorKind {
Read,
Parse,
@@ -435,6 +502,12 @@ impl std::error::Error for RuntimeCommandError {}
pub async fn migrate_runtime_database(
config: &TangleRuntimeConfig,
) -> Result<RuntimeMigrationReport, RuntimeCommandError> {
+ tracing::info!(
+ command = "migrate",
+ namespace = config.database_config().namespace(),
+ database = config.database_config().database(),
+ "starting runtime database migration"
+ );
let store = connect_runtime_store(config).await?;
let outcomes = store
.apply_plan(&base_migration_plan())
@@ -448,6 +521,13 @@ pub async fn migrate_runtime_database(
.iter()
.filter(|outcome| **outcome == MigrationApplyOutcome::AlreadyApplied)
.count() as u64;
+ tracing::info!(
+ command = "migrate",
+ applied,
+ already_applied,
+ total = outcomes.len() as u64,
+ "finished runtime database migration"
+ );
Ok(RuntimeMigrationReport::new(
applied,
already_applied,
@@ -526,6 +606,11 @@ pub async fn import_events_from_path(
path: impl AsRef<FsPath>,
) -> Result<RuntimeEventImportReport, RuntimeCommandError> {
let path = path.as_ref();
+ tracing::info!(
+ command = "event import",
+ input_path = path.display().to_string(),
+ "starting event import"
+ );
let raw = fs::read_to_string(path).map_err(|error| {
RuntimeCommandError::input(format!(
"failed to read event import file `{}`: {error}",
@@ -551,6 +636,15 @@ pub async fn import_events_from_path(
let outcome = import_single_event(&store, &validator, event, now).await?;
report.record(outcome);
}
+ tracing::info!(
+ command = "event import",
+ total = report.total(),
+ inserted = report.inserted(),
+ duplicate = report.duplicate(),
+ projected = report.projected(),
+ skipped = report.skipped(),
+ "finished event import"
+ );
Ok(report)
}
@@ -724,6 +818,12 @@ pub async fn export_events_to_path(
config: &TangleRuntimeConfig,
path: impl AsRef<FsPath>,
) -> Result<RuntimeEventExportReport, RuntimeCommandError> {
+ let path = path.as_ref();
+ tracing::info!(
+ command = "event export",
+ output_path = path.display().to_string(),
+ "starting event export"
+ );
let store = connect_runtime_store(config).await?;
store
.apply_plan(&base_migration_plan())
@@ -738,13 +838,17 @@ pub async fn export_events_to_path(
output.push_str(&runtime_row_string(row, "raw_json")?);
output.push('\n');
}
- let path = path.as_ref();
fs::write(path, output).map_err(|error| {
RuntimeCommandError::input(format!(
"failed to write event export file `{}`: {error}",
path.display()
))
})?;
+ tracing::info!(
+ command = "event export",
+ exported = rows.len() as u64,
+ "finished event export"
+ );
Ok(RuntimeEventExportReport::new(rows.len() as u64))
}
@@ -823,6 +927,10 @@ enum RuntimeProjectionRebuildOutcome {
pub async fn rebuild_projections(
config: &TangleRuntimeConfig,
) -> Result<RuntimeProjectionRebuildReport, RuntimeCommandError> {
+ tracing::info!(
+ command = "projection rebuild",
+ "starting projection rebuild"
+ );
let store = connect_runtime_store(config).await?;
store
.apply_plan(&base_migration_plan())
@@ -849,6 +957,14 @@ pub async fn rebuild_projections(
let outcome = rebuild_single_event_projection(&store, &validator, event, now).await?;
report.record(outcome);
}
+ tracing::info!(
+ command = "projection rebuild",
+ scanned = report.scanned(),
+ rebuilt = report.rebuilt(),
+ projected = report.projected(),
+ skipped = report.skipped(),
+ "finished projection rebuild"
+ );
Ok(report)
}
@@ -903,6 +1019,13 @@ impl RuntimeServer {
}
pub async fn run(&self) -> Result<RuntimeServerReport, RuntimeCommandError> {
+ tracing::info!(
+ command = "run",
+ listen_addr = self.config.listen_addr().to_string(),
+ namespace = self.config.database_config().namespace(),
+ database = self.config.database_config().database(),
+ "starting runtime server"
+ );
let store = connect_runtime_store(&self.config).await?;
store
.apply_plan(&base_migration_plan())
@@ -922,6 +1045,11 @@ impl RuntimeServer {
})
.await
.map_err(|error| RuntimeCommandError::store(format!("server failed: {error}")))?;
+ tracing::info!(
+ command = "run",
+ listen_addr = listen_addr.to_string(),
+ "runtime server stopped"
+ );
Ok(RuntimeServerReport::new(listen_addr))
}
}
@@ -1522,6 +1650,8 @@ struct RuntimeConfigDocument {
limits: RuntimeLimitsConfigDocument,
#[serde(default)]
policy: RuntimePolicyConfigDocument,
+ #[serde(default)]
+ observability: RuntimeObservabilityConfigDocument,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -1597,6 +1727,26 @@ struct RuntimePolicyConfigDocument {
blocked_pubkeys: Vec<String>,
}
+#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
+struct RuntimeObservabilityConfigDocument {
+ #[serde(default)]
+ tracing: RuntimeTracingConfigDocument,
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
+struct RuntimeTracingConfigDocument {
+ enabled: Option<bool>,
+ filter: Option<String>,
+ format: Option<RuntimeTracingFormatDocument>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "snake_case")]
+enum RuntimeTracingFormatDocument {
+ Compact,
+ Json,
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
enum RuntimeUnapprovedSellerActionDocument {
@@ -1626,6 +1776,7 @@ fn runtime_config_from_document(
let durable_write_rate_limit = durable_write_rate_limit_from_document(&document.policy)?;
let admin_pubkeys = admin_pubkeys_from_document(&document.policy)?;
let admission_policy = admission_policy_from_document(&document.policy)?;
+ let tracing = tracing_config_from_document(document.observability.tracing)?;
Ok(TangleRuntimeConfig {
listen_addr,
relay_connection,
@@ -1634,6 +1785,7 @@ fn runtime_config_from_document(
durable_write_rate_limit,
admin_pubkeys,
limits: limits.runtime,
+ tracing,
})
}
@@ -1727,6 +1879,24 @@ fn durable_write_rate_limit_from_document(
.transpose()
}
+fn tracing_config_from_document(
+ document: RuntimeTracingConfigDocument,
+) -> Result<RuntimeTracingConfig, RuntimeConfigError> {
+ let default = RuntimeTracingConfig::disabled();
+ let format = match document.format {
+ Some(RuntimeTracingFormatDocument::Compact) => RuntimeTracingFormat::Compact,
+ Some(RuntimeTracingFormatDocument::Json) => RuntimeTracingFormat::Json,
+ None => default.format(),
+ };
+ RuntimeTracingConfig::new(
+ document.enabled.unwrap_or(default.enabled()),
+ document
+ .filter
+ .unwrap_or_else(|| default.filter().to_owned()),
+ format,
+ )
+}
+
fn admin_pubkeys_from_document(
document: &RuntimePolicyConfigDocument,
) -> Result<BTreeSet<PublicKeyHex>, RuntimeConfigError> {
@@ -4201,9 +4371,9 @@ mod tests {
EventMessageHandler, GracefulShutdownSignal, ListingsHttpState, LiveEventFanout,
ReadinessCheckStatus, ReadinessState, RelayConnection, RelayConnectionConfig,
RelayConnectionId, RelayInfoDocument, ReqMessageHandler, RuntimeCommandErrorKind,
- RuntimeConfigErrorKind, TANGLE_RELAY_SOFTWARE, TANGLE_SUPPORTED_NIPS, WebSocketHttpState,
- health_router, listing_item_document, listing_projection_query, listings_router,
- load_runtime_config, migrate_runtime_database, parse_listing_query,
+ RuntimeConfigErrorKind, RuntimeTracingFormat, TANGLE_RELAY_SOFTWARE, TANGLE_SUPPORTED_NIPS,
+ WebSocketHttpState, health_router, listing_item_document, listing_projection_query,
+ listings_router, load_runtime_config, migrate_runtime_database, parse_listing_query,
parse_marketplace_search_query, parse_runtime_config_json, relay_info_router,
search_document_query, websocket_router,
};
@@ -4506,6 +4676,15 @@ mod tests {
websocket_state.connection_config(),
config.relay_connection_config()
);
+ assert!(!config.tracing_config().enabled());
+ assert_eq!(
+ config.tracing_config().filter(),
+ "info,tangle=info,tangle_runtime=info"
+ );
+ assert_eq!(
+ config.tracing_config().format(),
+ RuntimeTracingFormat::Compact
+ );
}
#[test]
@@ -4545,6 +4724,47 @@ mod tests {
}
#[test]
+ fn runtime_config_loader_parses_observability_tracing_config() {
+ let config = parse_runtime_config_json(
+ r#"{
+ "server": {
+ "listen_addr": "127.0.0.1:7101",
+ "relay_url": "wss://relay.radroots.test"
+ },
+ "database": {
+ "mode": "memory",
+ "namespace": "tangle",
+ "database": "relay"
+ },
+ "auth": {
+ "challenge_ttl_seconds": 300
+ },
+ "limits": {
+ "message_rate_limit": {
+ "limit": 120,
+ "window_seconds": 60
+ }
+ },
+ "observability": {
+ "tracing": {
+ "enabled": true,
+ "filter": "info,tangle=debug,tangle_runtime=debug",
+ "format": "json"
+ }
+ }
+ }"#,
+ )
+ .expect("runtime config");
+
+ assert!(config.tracing_config().enabled());
+ assert_eq!(
+ config.tracing_config().filter(),
+ "info,tangle=debug,tangle_runtime=debug"
+ );
+ assert_eq!(config.tracing_config().format(), RuntimeTracingFormat::Json);
+ }
+
+ #[test]
fn runtime_config_loader_rejects_invalid_documents() {
let parse_error = parse_runtime_config_json("{").expect_err("parse");
let invalid_listen = parse_runtime_config_json(
@@ -4593,6 +4813,35 @@ mod tests {
}"#,
)
.expect_err("endpoint");
+ let empty_tracing_filter = parse_runtime_config_json(
+ r#"{
+ "server": {
+ "listen_addr": "127.0.0.1:7000",
+ "relay_url": "ws://127.0.0.1:7000"
+ },
+ "database": {
+ "mode": "memory",
+ "namespace": "tangle",
+ "database": "relay"
+ },
+ "auth": {
+ "challenge_ttl_seconds": 300
+ },
+ "limits": {
+ "message_rate_limit": {
+ "limit": 120,
+ "window_seconds": 60
+ }
+ },
+ "observability": {
+ "tracing": {
+ "enabled": true,
+ "filter": " "
+ }
+ }
+ }"#,
+ )
+ .expect_err("tracing filter");
assert_eq!(parse_error.kind(), RuntimeConfigErrorKind::Parse);
assert!(
@@ -4611,6 +4860,11 @@ mod tests {
missing_endpoint.message(),
"database.endpoint is required for http mode"
);
+ assert_eq!(empty_tracing_filter.kind(), RuntimeConfigErrorKind::Invalid);
+ assert_eq!(
+ empty_tracing_filter.message(),
+ "observability.tracing.filter must not be empty"
+ );
}
#[test]