From 239baafd236c0e55bf8a86035fd1180061cd7845 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 26 Jun 2026 00:57:48 -0500 Subject: [PATCH 01/22] working --- raphtory-graphql/Cargo.toml | 1 + raphtory-graphql/src/config/app_config.rs | 4 + raphtory-graphql/src/config/otlp_config.rs | 157 +++++++++++++++++++++ raphtory-graphql/src/server.rs | 16 +++ 4 files changed, 178 insertions(+) diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index e17b90fbe2..5674ffcc41 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -69,6 +69,7 @@ rust-embed = { workspace = true } parking_lot = { workspace = true } tempfile = { workspace = true } pretty_assertions = { workspace = true } +opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } [features] python = ["dep:pyo3", "dep:pythonize", "raphtory/python"] diff --git a/raphtory-graphql/src/config/app_config.rs b/raphtory-graphql/src/config/app_config.rs index 03a520ca49..92aa26fe35 100644 --- a/raphtory-graphql/src/config/app_config.rs +++ b/raphtory-graphql/src/config/app_config.rs @@ -165,6 +165,10 @@ impl AppConfigBuilder { .map_err(|e| invalid_value([path, sub_path], e))?, ); } + #[cfg(test)] + TracingConfigFieldName::Exporters => { + // Exporter handles are runtime-only and should not be loaded from config. + } } } } diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index aff0d0d063..5fd9069a33 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -12,6 +12,10 @@ use opentelemetry_sdk::{ trace::{Sampler, SdkTracerProvider}, Resource, }; +#[cfg(test)] +use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; +#[cfg(test)] +use std::sync::{Arc, Mutex}; use raphtory_api::core::storage::arc_str::OptionAsStr; use reqwest::{blocking::ClientBuilder, Certificate}; use serde::Deserialize; @@ -19,6 +23,7 @@ use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf, time::Du use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; + pub const DEFAULT_TRACING_ENABLED: bool = false; #[derive( @@ -80,6 +85,8 @@ pub enum TracingProtocol { TONIC, HTTP, STDOUT, + #[cfg(test)] + IN_MEMORY } impl TryFrom for TracingProtocol { @@ -119,6 +126,8 @@ pub struct TracingConfig { /// Headers to use when transport_protocol is set to HTTP pub transport_headers: HashMap, pub transport_certificate: Option, + #[cfg(test)] + pub exporters: InMemoryExporterStore, } impl Default for TracingConfig { @@ -131,6 +140,8 @@ impl Default for TracingConfig { transport_protocol: DEFAULT_OTLP_TRANSPORT_PROTOCOL, transport_headers: Default::default(), transport_certificate: None, + #[cfg(test)] + exporters: InMemoryExporterStore::default(), } } } @@ -264,6 +275,24 @@ impl TracingConfig { opentelemetry_stdout::SpanExporter::default(), opentelemetry_stdout::LogExporter::default(), )) + }, + #[cfg(test)] + TracingProtocol::IN_MEMORY => { + eprintln!( + "Sending traces to in-memory exporter with tracing level `{}`", + self.level + ); + let span_exporter = InMemorySpanExporter::default(); + let log_exporter = InMemoryLogExporter::default(); + let exporters = InMemoryExporters { + span_exporter: span_exporter.clone(), + log_exporter: log_exporter.clone(), + }; + self.exporters.set(exporters); + Ok(self.with_exporter( + span_exporter, + log_exporter, + )) } }; @@ -278,4 +307,132 @@ impl TracingConfig { Ok(None) } } + + #[cfg(test)] + pub async fn tracer_provider_with_exporters( + &self, + ) -> Result< + ( + Option<(SdkTracerProvider, SdkLoggerProvider)>, + Option, + ), + ServerError, + > { + if self.enabled && matches!(self.transport_protocol, TracingProtocol::IN_MEMORY) { + eprintln!( + "Sending traces to in-memory exporter with tracing level `{}`", + self.level + ); + + let span_exporter = InMemorySpanExporter::default(); + let log_exporter = InMemoryLogExporter::default(); + let providers = self.with_exporter(span_exporter.clone(), log_exporter.clone()); + + Ok(( + Some(providers), + Some(InMemoryExporters { + span_exporter, + log_exporter, + }), + )) + } else { + Ok((self.tracer_provider().await?, None)) + } + } +} + +#[cfg(test)] +#[derive(Clone, Debug)] +pub struct InMemoryExporters { + pub span_exporter: InMemorySpanExporter, + pub log_exporter: InMemoryLogExporter, +} + +#[cfg(test)] +#[derive(Clone, Debug)] +pub struct InMemoryExporterStore { + pub inner: Arc>>, +} + +#[cfg(test)] +impl Default for InMemoryExporterStore { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::new(None)), + } + } +} + +#[cfg(test)] +impl InMemoryExporterStore { + pub fn set(&self, exporters: InMemoryExporters) { + let mut lock = self.inner.lock().expect("exporter store poisoned"); + lock.replace(exporters); + } + + pub fn get(&self) -> Option { + let lock = self.inner.lock().expect("exporter store poisoned"); + lock.clone() + } +} + +#[cfg(test)] +impl PartialEq for InMemoryExporterStore { + fn eq(&self, _other: &Self) -> bool { + // Runtime exporter handles are process-local and not value-comparable. + true + } +} + +#[cfg(test)] +impl serde::Serialize for InMemoryExporterStore { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_unit_struct("InMemoryExporterStore") + } +} + +#[cfg(test)] +impl<'de> serde::Deserialize<'de> for InMemoryExporterStore { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let _ = >::deserialize(deserializer)?; + Ok(Self::default()) + } +} + +#[cfg(test)] +impl PartialEq for InMemoryExporters { + fn eq(&self, _other: &Self) -> bool { + // Exporters are runtime handles; identity/equality semantics are not meaningful. + true + } +} + +#[cfg(test)] +impl serde::Serialize for InMemoryExporters { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_unit_struct("InMemoryExporters") + } +} + +#[cfg(test)] +impl<'de> serde::Deserialize<'de> for InMemoryExporters { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let _ = >::deserialize(deserializer)?; + Ok(Self { + span_exporter: InMemorySpanExporter::default(), + log_exporter: InMemoryLogExporter::default(), + }) + } } diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 53ff1cd46b..3b600400a7 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -540,4 +540,20 @@ mod server_tests { sleep(Duration::from_secs(5)).await; handler.await.unwrap().stop().await } + + #[tokio::test] + async fn test_open_telemetry() { + let tmp_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); + graph.encode(tmp_dir.path().join("g")).unwrap(); + + global_info_logger(); + let server = GraphServer::new(tmp_dir.path().to_path_buf(), None, Config::default()) + .await + .unwrap(); + let handler = server.start_with_port(0); + sleep(Duration::from_secs(5)).await; + handler.await.unwrap().stop().await; + } } From 59c3f41740c3c02d7727fe11e98cfaba02fad94d Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 26 Jun 2026 10:04:12 -0500 Subject: [PATCH 02/22] working --- raphtory-graphql/src/config/otlp_config.rs | 108 +-------------------- raphtory-graphql/src/server.rs | 67 ++++++++++++- 2 files changed, 67 insertions(+), 108 deletions(-) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index 5fd9069a33..c96572ab2e 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -14,8 +14,6 @@ use opentelemetry_sdk::{ }; #[cfg(test)] use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; -#[cfg(test)] -use std::sync::{Arc, Mutex}; use raphtory_api::core::storage::arc_str::OptionAsStr; use reqwest::{blocking::ClientBuilder, Certificate}; use serde::Deserialize; @@ -127,7 +125,7 @@ pub struct TracingConfig { pub transport_headers: HashMap, pub transport_certificate: Option, #[cfg(test)] - pub exporters: InMemoryExporterStore, + pub exporters: InMemoryExporters, } impl Default for TracingConfig { @@ -141,7 +139,7 @@ impl Default for TracingConfig { transport_headers: Default::default(), transport_certificate: None, #[cfg(test)] - exporters: InMemoryExporterStore::default(), + exporters: InMemoryExporters::default() } } } @@ -282,16 +280,9 @@ impl TracingConfig { "Sending traces to in-memory exporter with tracing level `{}`", self.level ); - let span_exporter = InMemorySpanExporter::default(); - let log_exporter = InMemoryLogExporter::default(); - let exporters = InMemoryExporters { - span_exporter: span_exporter.clone(), - log_exporter: log_exporter.clone(), - }; - self.exporters.set(exporters); Ok(self.with_exporter( - span_exporter, - log_exporter, + self.exporters.span_exporter.clone(), + self.exporters.log_exporter.clone(), )) } }; @@ -307,104 +298,15 @@ impl TracingConfig { Ok(None) } } - - #[cfg(test)] - pub async fn tracer_provider_with_exporters( - &self, - ) -> Result< - ( - Option<(SdkTracerProvider, SdkLoggerProvider)>, - Option, - ), - ServerError, - > { - if self.enabled && matches!(self.transport_protocol, TracingProtocol::IN_MEMORY) { - eprintln!( - "Sending traces to in-memory exporter with tracing level `{}`", - self.level - ); - - let span_exporter = InMemorySpanExporter::default(); - let log_exporter = InMemoryLogExporter::default(); - let providers = self.with_exporter(span_exporter.clone(), log_exporter.clone()); - - Ok(( - Some(providers), - Some(InMemoryExporters { - span_exporter, - log_exporter, - }), - )) - } else { - Ok((self.tracer_provider().await?, None)) - } - } } #[cfg(test)] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct InMemoryExporters { pub span_exporter: InMemorySpanExporter, pub log_exporter: InMemoryLogExporter, } -#[cfg(test)] -#[derive(Clone, Debug)] -pub struct InMemoryExporterStore { - pub inner: Arc>>, -} - -#[cfg(test)] -impl Default for InMemoryExporterStore { - fn default() -> Self { - Self { - inner: Arc::new(Mutex::new(None)), - } - } -} - -#[cfg(test)] -impl InMemoryExporterStore { - pub fn set(&self, exporters: InMemoryExporters) { - let mut lock = self.inner.lock().expect("exporter store poisoned"); - lock.replace(exporters); - } - - pub fn get(&self) -> Option { - let lock = self.inner.lock().expect("exporter store poisoned"); - lock.clone() - } -} - -#[cfg(test)] -impl PartialEq for InMemoryExporterStore { - fn eq(&self, _other: &Self) -> bool { - // Runtime exporter handles are process-local and not value-comparable. - true - } -} - -#[cfg(test)] -impl serde::Serialize for InMemoryExporterStore { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_unit_struct("InMemoryExporterStore") - } -} - -#[cfg(test)] -impl<'de> serde::Deserialize<'de> for InMemoryExporterStore { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let _ = >::deserialize(deserializer)?; - Ok(Self::default()) - } -} - #[cfg(test)] impl PartialEq for InMemoryExporters { fn eq(&self, _other: &Self) -> bool { diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 3b600400a7..aed287963e 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -492,7 +492,11 @@ async fn server_termination( #[cfg(test)] mod server_tests { - use crate::server::GraphServer; + use crate::{ + client::raphtory_client::RaphtoryGraphQLClient, + config::{app_config::AppConfigBuilder, otlp_config::TracingProtocol}, + server::GraphServer, + }; use chrono::prelude::*; use raphtory::{ db::api::storage::storage::Config, @@ -503,6 +507,8 @@ mod server_tests { use tempfile::tempdir; use tokio::time::{sleep, Duration}; use tracing::info; + use url::Url; + use std::collections::HashMap; #[tokio::test] async fn test_server_start_stop() { @@ -548,12 +554,63 @@ mod server_tests { graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); graph.encode(tmp_dir.path().join("g")).unwrap(); - global_info_logger(); - let server = GraphServer::new(tmp_dir.path().to_path_buf(), None, Config::default()) + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + let add_node_query = r#" + { + updateGraph(path: "g") { + addNode(time: 1, name: 1) { + success + } + } + } + "#; + + let result = client + .query(add_node_query, HashMap::new()) .await .unwrap(); - let handler = server.start_with_port(0); + let added = result + .get("updateGraph") + .and_then(|v| v.get("addNode")) + .and_then(|v| v.get("success")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + assert!(added, "addNode mutation returned unsuccessful response"); + sleep(Duration::from_secs(5)).await; - handler.await.unwrap().stop().await; + handler.stop().await; + assert!( + app_config + .tracing + .exporters + .span_exporter + .get_finished_spans() + .unwrap() + .len() > 0 + ); + assert!( + app_config + .tracing + .exporters + .log_exporter + .get_emitted_logs() + .unwrap() + .len() > 0 + ); } } From 6911fba53379c64a6f17f9632382fa0a0b8e25f2 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 26 Jun 2026 17:10:48 -0500 Subject: [PATCH 03/22] figuring out how spans work --- raphtory-graphql/src/server.rs | 131 +++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 39 deletions(-) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index aed287963e..89405f836a 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -494,10 +494,14 @@ async fn server_termination( mod server_tests { use crate::{ client::raphtory_client::RaphtoryGraphQLClient, - config::{app_config::AppConfigBuilder, otlp_config::TracingProtocol}, + config::{ + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol}, + }, server::GraphServer, }; use chrono::prelude::*; + use opentelemetry::trace::SpanId; use raphtory::{ db::api::storage::storage::Config, prelude::{AdditionOps, Graph, StableEncode, NO_PROPS}, @@ -508,7 +512,49 @@ mod server_tests { use tokio::time::{sleep, Duration}; use tracing::info; use url::Url; - use std::collections::HashMap; + use std::collections::{HashMap, HashSet}; + use raphtory::prelude::PropertyAdditionOps; + + fn print_span_tree(spans: &[opentelemetry_sdk::trace::SpanData]) { + let mut children: HashMap> = + HashMap::new(); + + for span in spans { + children + .entry(span.parent_span_id) + .or_default() + .push(span); + } + + for span in spans + .iter() + .filter(|span| span.parent_span_id == SpanId::INVALID) + .collect::>() + { + print_span_node(span, 0, &mut children); + } + } + + fn print_span_node( + span: &opentelemetry_sdk::trace::SpanData, + depth: usize, + children: &mut HashMap>, + ) { + println!( + "{}{} [{} -> {}]", + " ".repeat(depth), + span.name, + span.span_context.span_id(), + span.parent_span_id + ); + + if let Some(mut child_spans) = children.remove(&span.span_context.span_id()) { + child_spans.sort_by_key(|span| span.start_time); + for child in child_spans { + print_span_node(child, depth + 1, children); + } + } + } #[tokio::test] async fn test_server_start_stop() { @@ -554,22 +600,6 @@ mod server_tests { graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); graph.encode(tmp_dir.path().join("g")).unwrap(); - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); let add_node_query = r#" { updateGraph(path: "g") { @@ -580,37 +610,60 @@ mod server_tests { } "#; - let result = client - .query(add_node_query, HashMap::new()) + for tracing_level in [ + TracingLevel::COMPLETE, + TracingLevel::ESSENTIAL, + TracingLevel::MINIMAL, + ] { + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(tracing_level.clone()) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) .await .unwrap(); - let added = result - .get("updateGraph") - .and_then(|v| v.get("addNode")) - .and_then(|v| v.get("success")) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - assert!(added, "addNode mutation returned unsuccessful response"); + let handler = server.start_with_port(0).await.unwrap(); - sleep(Duration::from_secs(5)).await; - handler.stop().await; - assert!( - app_config + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + + let result = client + .query(add_node_query, HashMap::new()) + .await; + sleep(Duration::from_secs(5)).await; + assert!(result.is_ok(), "query failed for tracing level {tracing_level:?}"); + handler.stop().await; + + let finished_spans = app_config .tracing .exporters .span_exporter .get_finished_spans() - .unwrap() - .len() > 0 - ); - assert!( - app_config + .unwrap(); + let emitted_logs = app_config .tracing .exporters .log_exporter .get_emitted_logs() - .unwrap() - .len() > 0 - ); + .unwrap(); + + println!("\n=== tracing level: {tracing_level:?} ==="); + print_span_tree(&finished_spans); + + assert!( + finished_spans.len() > 0, + "expected spans for tracing level {tracing_level:?}" + ); + println!( + "emitted logs for tracing level {tracing_level:?}: {}", + emitted_logs.len() + ); + } } } From 8474e09bdb566c35d6b783a03ec6dae7ef2882f2 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 9 Jul 2026 17:38:02 -0500 Subject: [PATCH 04/22] working --- raphtory-graphql/src/server.rs | 448 ++++++++++++++++++++++++++------- 1 file changed, 360 insertions(+), 88 deletions(-) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 1d66fb9ac5..c213030b4f 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -1,17 +1,7 @@ use crate::{ - auth::{AuthenticatedGraphQL, MutationAuth}, - auth_policy::AuthorizationPolicy, - config::{app_config::AppConfig, auth_config::PublicKeyError}, - data::Data, - model::{ - plugins::{entry_point::EntryPoint, operation::Operation}, - App, - }, - observability::open_telemetry::OpenTelemetry, - paths::ExistingGraphFolder, - routes::{health, version, PublicFilesEndpoint}, - server::ServerError::SchemaError, - GQLError, + GQLError, auth::{AuthenticatedGraphQL, MutationAuth}, auth_policy::AuthorizationPolicy, config::{app_config::AppConfig, auth_config::PublicKeyError}, data::Data, model::{ + App, plugins::{entry_point::EntryPoint, operation::Operation}, + }, observability::open_telemetry::OpenTelemetry, paths::ExistingGraphFolder, routes::{PublicFilesEndpoint, health, version}, server::ServerError::SchemaError, }; use config::ConfigError; use once_cell::sync::Lazy; @@ -19,7 +9,7 @@ use opentelemetry::trace::TracerProvider; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_sdk::{ logs::SdkLoggerProvider, - trace::{SdkTracerProvider as TP, SdkTracerProvider, Tracer}, + trace::{SdkTracerProvider, Tracer}, }; use poem::{ get, @@ -495,16 +485,14 @@ async fn server_termination( #[cfg(test)] mod server_tests { use crate::{ - client::raphtory_client::RaphtoryGraphQLClient, - config::{ - app_config::AppConfigBuilder, - otlp_config::{TracingLevel, TracingProtocol}, - }, - server::GraphServer, + client::raphtory_client::RaphtoryGraphQLClient, config::{ + app_config::AppConfigBuilder, otlp_config::{ESSENTIAL_TRACE_SPANS, TracingLevel, TracingProtocol}, + }, server::GraphServer, }; use chrono::prelude::*; use opentelemetry::trace::SpanId; - use raphtory::{ + use opentelemetry_sdk::{logs::in_memory_exporter::LogDataWithResource, trace::SpanData}; +use raphtory::{ db::api::storage::storage::Config, prelude::{AdditionOps, Graph, StableEncode, NO_PROPS}, vectors::{storage::OpenAIEmbeddings, template::DocumentTemplate}, @@ -545,7 +533,6 @@ mod server_tests { } use url::Url; use std::collections::{HashMap, HashSet}; - use raphtory::prelude::PropertyAdditionOps; fn print_span_tree(spans: &[opentelemetry_sdk::trace::SpanData]) { let mut children: HashMap> = @@ -625,77 +612,362 @@ mod server_tests { handler.await.unwrap().stop().await } - #[tokio::test] - async fn test_open_telemetry() { + + fn expected_operations( + defined_query_operations: Vec, + tracing_level: TracingLevel, + ) -> HashSet { + let mut expected_operations_: HashSet = match tracing_level { + TracingLevel::COMPLETE => defined_query_operations.into_iter().collect(), + TracingLevel::ESSENTIAL => defined_query_operations + .into_iter() + .filter(|op| ESSENTIAL_TRACE_SPANS.contains(&op.as_str())) + .collect(), + TracingLevel::MINIMAL => HashSet::new(), + }; + expected_operations_.extend([ + "request".to_string(), + "parse".to_string(), + "validation".to_string(), + "execute".to_string(), + ]); + expected_operations_ + } + + fn all_operations(graph_path: String) -> (String, Vec) { + let operations: Vec = vec![ + "updateGraph", + "addNode", + "success", + "node", + "id", + "name", + "createNode", + "addNodes", + "addEdge", + "edge", + "src", + "dst", + "addEdges", + "addProperties", + "addMetadata", + "updateMetadata", + "setNodeType", + "addUpdates", + "delete", + "deleteEdge", + "nodeLookup", + "edgeLookup", + "graph", + "uniqueLayers", + "defaultLayer", + "countNodes", + "countEdges", + "layers", + "excludeLayers", + "layer", + "excludeLayer", + "subgraph", + "valid", + "subgraphNodeTypes", + "excludeNodes", + "window", + "at", + "latest", + "snapshotAt", + "snapshotLatest", + "before", + "after", + "shrinkWindow", + "shrinkStart", + "shrinkEnd", + "created", + "lastOpened", + "lastUpdated", + "earliestTime", + "timestamp", + "eventId", + "datetime", + "latestTime", + "earliestEdgeTime", + "latestEdgeTime", + "countTemporalEdges", + "hasNode", + "hasEdge", + "nodes", + "count", + "list", + "edges", + "properties", + "contains", + "keys", + "values", + "temporal", + "metadata", + "path", + "namespace", + "schema", + "typeName", + "propertyType", + "variants", + "srcType", + "dstType", + "sharedNeighbours", + "flush", + "key" + ] + .into_iter() + .map(String::from) + .collect(); + let query = format!( + "query {{ + updateGraph(path: \"{}\") {{ + addNode(time: 1, name: 1, properties: [{{ key: \"seed\", value: {{ str: \"yes\" }} }}], nodeType: \"seed\", layer: \"main\") {{ + success + node {{ id name }} + }} + createNode(time: 2, name: 3, properties: [{{ key: \"kind\", value: {{ str: \"new\" }} }}]) {{ + success + node {{ id name }} + }} + addNodes(nodes: [ + {{ + name: 4, + updates: [{{ time: 3, properties: [{{ key: \"batch\", value: {{ bool: true }} }}] }}], + metadata: [{{ key: \"group\", value: {{ str: \"ops\" }} }}] + }}, + {{ + name: 5, + updates: [{{ time: 4, properties: [{{ key: \"batch\", value: {{ bool: true }} }}] }}], + metadata: [{{ key: \"group\", value: {{ str: \"ops\" }} }}] + }} + ]) + addEdge(time: 5, src: 1, dst: 2, properties: [{{ key: \"weight\", value: {{ f64: 1.5 }} }}], layer: \"main\") {{ + success + edge {{ src {{ id }} dst {{ id }} }} + src {{ success }} + dst {{ success }} + }} + addEdges(edges: [ + {{ + src: 2, + dst: 3, + updates: [{{ time: 6, properties: [{{ key: \"weight\", value: {{ f64: 2.5 }} }}] }}], + metadata: [{{ key: \"kind\", value: {{ str: \"batch\" }} }}] + }}, + {{ + src: 4, + dst: 5, + updates: [{{ time: 6, properties: [{{ key: \"weight\", value: {{ f64: 3.5 }} }}] }}], + metadata: [{{ key: \"kind\", value: {{ str: \"batch\" }} }}] + }} + ]) + addProperties(t: 7, properties: [{{ key: \"epoch\", value: {{ u64: 1 }} }}]) + addMetadata(properties: [{{ key: \"owner\", value: {{ str: \"team-a\" }} }}]) + updateMetadata(properties: [{{ key: \"owner\", value: {{ str: \"team-b\" }} }}]) + node(name: 4) {{ + setNodeType(newType: \"worker\") + addUpdates(time: 8, properties: [{{ key: \"cpu\", value: {{ u64: 4 }} }}], layer: \"main\") + addMetadata(properties: [{{ key: \"zone\", value: {{ str: \"eu-west\" }} }}]) + updateMetadata(properties: [{{ key: \"zone\", value: {{ str: \"us-east\" }} }}]) + success + node {{ id name }} + }} + edge(src: 1, dst: 2) {{ + addUpdates(time: 9, properties: [{{ key: \"latency\", value: {{ u64: 23 }} }}], layer: \"main\") + addMetadata(properties: [{{ key: \"channel\", value: {{ str: \"grpc\" }} }}], layer: \"main\") + updateMetadata(properties: [{{ key: \"channel\", value: {{ str: \"http\" }} }}], layer: \"main\") + delete(time: 10, layer: \"main\") + success + edge {{ src {{ id }} dst {{ id }} }} + src {{ success }} + dst {{ success }} + }} + deleteEdge(time: 11, src: 4, dst: 5) {{ + success + edge {{ src {{ id }} dst {{ id }} }} + src {{ success }} + dst {{ success }} + }} + nodeLookup: node(name: 1) {{ success node {{ id name }} }} + edgeLookup: edge(src: 1, dst: 2) {{ + success + edge {{ src {{ id }} dst {{ id }} }} + src {{ success }} + dst {{ success }} + }} + graph {{ + uniqueLayers + defaultLayer {{ countNodes countEdges }} + layers(names: [\"main\"]) {{ countNodes countEdges }} + excludeLayers(names: [\"main\"]) {{ countNodes countEdges }} + layer(name: \"main\") {{ countNodes countEdges }} + excludeLayer(name: \"main\") {{ countNodes countEdges }} + subgraph(nodes: [1, 2, 3]) {{ countNodes countEdges }} + valid {{ countNodes countEdges }} + subgraphNodeTypes(nodeTypes: [\"worker\"]) {{ countNodes countEdges }} + excludeNodes(nodes: [5]) {{ countNodes countEdges }} + window(start: 1, end: 12) {{ countNodes countEdges }} + at(time: 9) {{ countNodes countEdges }} + latest {{ countNodes countEdges }} + snapshotAt(time: 9) {{ countNodes countEdges }} + snapshotLatest {{ countNodes countEdges }} + before(time: 12) {{ countNodes countEdges }} + after(time: 1) {{ countNodes countEdges }} + shrinkWindow(start: 1, end: 12) {{ countNodes countEdges }} + shrinkStart(start: 2) {{ countNodes countEdges }} + shrinkEnd(end: 11) {{ countNodes countEdges }} + created + lastOpened + lastUpdated + earliestTime {{ timestamp eventId datetime }} + latestTime {{ timestamp eventId datetime }} + earliestEdgeTime(includeNegative: true) {{ timestamp eventId datetime }} + latestEdgeTime(includeNegative: true) {{ timestamp eventId datetime }} + countNodes + countEdges + countTemporalEdges + hasNode(name: 1) + hasEdge(src: 1, dst: 2, layer: \"main\") + node(name: 1) {{ id name }} + nodes {{ count list {{ id name }} }} + edge(src: 1, dst: 2) {{ src {{ id name }} dst {{ id name }} }} + edges {{ count list {{ src {{ id name }} dst {{ id name }} }} }} + properties {{ + contains(key: \"epoch\") + keys + values {{ key }} + temporal {{ keys }} + }} + metadata {{ + contains(key: \"owner\") + keys + values {{ key }} + }} + name + path + namespace + schema {{ + nodes {{ + typeName + properties {{ key propertyType variants }} + metadata {{ key propertyType variants }} + }} + layers {{ + name + edges {{ + srcType + dstType + properties {{ key propertyType variants }} + metadata {{ key propertyType variants }} + }} + }} + }} + sharedNeighbours(selectedNodes: [1, 2]) {{ id name }} + }} + flush + }} + }}", + graph_path + ); + (query, operations) + } + + async fn get_spans_and_logs(tracing_level: TracingLevel, query: String) -> (Vec, Vec) { let tmp_dir = tempdir().unwrap(); + let graph_path = format!("g"); let graph = Graph::new(); graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); - graph.encode(tmp_dir.path().join("g")).unwrap(); + graph.encode(tmp_dir.path().join(&graph_path)).unwrap(); - let add_node_query = r#" - { - updateGraph(path: "g") { - addNode(time: 1, name: 1) { - success - } - } - } - "#; - - for tracing_level in [ - TracingLevel::COMPLETE, - TracingLevel::ESSENTIAL, - TracingLevel::MINIMAL, - ] { - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(tracing_level.clone()) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(tracing_level.clone()) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + + let result = client.query(&query, HashMap::new()).await; + assert!( + result.is_ok(), + "all-operations mutation failed for tracing level {tracing_level:?}: {:?}", + result + ); + + sleep(Duration::from_secs(5)).await; + handler.stop().await; + + let finished_spans = app_config + .tracing + .exporters + .span_exporter + .get_finished_spans() .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); - - let result = client - .query(add_node_query, HashMap::new()) - .await; - sleep(Duration::from_secs(5)).await; - assert!(result.is_ok(), "query failed for tracing level {tracing_level:?}"); - handler.stop().await; - - let finished_spans = app_config - .tracing - .exporters - .span_exporter - .get_finished_spans() - .unwrap(); - let emitted_logs = app_config - .tracing - .exporters - .log_exporter - .get_emitted_logs() - .unwrap(); + let emitted_logs = app_config + .tracing + .exporters + .log_exporter + .get_emitted_logs() + .unwrap(); + (finished_spans, emitted_logs) + } - println!("\n=== tracing level: {tracing_level:?} ==="); - print_span_tree(&finished_spans); - - assert!( - finished_spans.len() > 0, - "expected spans for tracing level {tracing_level:?}" - ); - println!( - "emitted logs for tracing level {tracing_level:?}: {}", - emitted_logs.len() - ); - } + fn assert_spans(tracing_level: TracingLevel, defined_query_operations: Vec, finished_spans: Vec) { + let actual_span_operations: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); + let expected_span_operations: HashSet = expected_operations(defined_query_operations, tracing_level.clone()); + + let mut missing_from_actual: Vec = expected_span_operations + .difference(&actual_span_operations) + .cloned() + .collect(); + missing_from_actual.sort(); + + let mut unexpected_in_actual: Vec = actual_span_operations + .difference(&expected_span_operations) + .cloned() + .collect(); + unexpected_in_actual.sort(); + + assert!( + missing_from_actual.is_empty() && unexpected_in_actual.is_empty(), + "span operations do not match for tracing level {tracing_level:?}. missing_from_actual={:?}; unexpected_in_actual={:?}", + missing_from_actual, + unexpected_in_actual + ); + } + + #[tokio::test] + async fn test_open_telemetry_spans_complete() { + let tracing_level = TracingLevel::COMPLETE; + let (query, defined_query_operations) = all_operations("g".to_string()); + let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; + assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); + } + + #[tokio::test] + async fn test_open_telemetry_spans_essential() { + let tracing_level = TracingLevel::ESSENTIAL; + let (query, defined_query_operations) = all_operations("g".to_string()); + let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; + assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); + } + + #[tokio::test] + async fn test_open_telemetry_spans_minimal() { + let tracing_level = TracingLevel::MINIMAL; + let (query, defined_query_operations) = all_operations("g".to_string()); + let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; + assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); } } From 3d1aac992c94c62468815bada18b74f187de6f29 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Thu, 9 Jul 2026 23:58:32 -0500 Subject: [PATCH 05/22] working on making the code developer friendly --- raphtory-graphql/src/config/otlp_config.rs | 37 +- raphtory-graphql/src/server.rs | 548 +++++++-------------- 2 files changed, 193 insertions(+), 392 deletions(-) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index c96572ab2e..d2695b5d27 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -7,13 +7,13 @@ use opentelemetry_appender_tracing::layer::{ OpenTelemetryTracingBridge, OpenTelemetryTracingBridgeBuilder, }; use opentelemetry_otlp::{LogExporter, Protocol, SpanExporter, WithExportConfig, WithHttpConfig}; +#[cfg(test)] +use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; use opentelemetry_sdk::{ logs::SdkLoggerProvider, trace::{Sampler, SdkTracerProvider}, Resource, }; -#[cfg(test)] -use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; use raphtory_api::core::storage::arc_str::OptionAsStr; use reqwest::{blocking::ClientBuilder, Certificate}; use serde::Deserialize; @@ -21,7 +21,6 @@ use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf, time::Du use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; - pub const DEFAULT_TRACING_ENABLED: bool = false; #[derive( @@ -84,7 +83,7 @@ pub enum TracingProtocol { HTTP, STDOUT, #[cfg(test)] - IN_MEMORY + IN_MEMORY, } impl TryFrom for TracingProtocol { @@ -125,6 +124,8 @@ pub struct TracingConfig { pub transport_headers: HashMap, pub transport_certificate: Option, #[cfg(test)] + #[serde(skip)] + // exposed in-memory exporters for testing pub exporters: InMemoryExporters, } @@ -139,7 +140,7 @@ impl Default for TracingConfig { transport_headers: Default::default(), transport_certificate: None, #[cfg(test)] - exporters: InMemoryExporters::default() + exporters: InMemoryExporters::default(), } } } @@ -273,7 +274,7 @@ impl TracingConfig { opentelemetry_stdout::SpanExporter::default(), opentelemetry_stdout::LogExporter::default(), )) - }, + } #[cfg(test)] TracingProtocol::IN_MEMORY => { eprintln!( @@ -315,26 +316,6 @@ impl PartialEq for InMemoryExporters { } } -#[cfg(test)] -impl serde::Serialize for InMemoryExporters { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_unit_struct("InMemoryExporters") - } -} -#[cfg(test)] -impl<'de> serde::Deserialize<'de> for InMemoryExporters { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let _ = >::deserialize(deserializer)?; - Ok(Self { - span_exporter: InMemorySpanExporter::default(), - log_exporter: InMemoryLogExporter::default(), - }) - } -} + + diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index c213030b4f..c452962fe1 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -1,7 +1,17 @@ use crate::{ - GQLError, auth::{AuthenticatedGraphQL, MutationAuth}, auth_policy::AuthorizationPolicy, config::{app_config::AppConfig, auth_config::PublicKeyError}, data::Data, model::{ - App, plugins::{entry_point::EntryPoint, operation::Operation}, - }, observability::open_telemetry::OpenTelemetry, paths::ExistingGraphFolder, routes::{PublicFilesEndpoint, health, version}, server::ServerError::SchemaError, + auth::{AuthenticatedGraphQL, MutationAuth}, + auth_policy::AuthorizationPolicy, + config::{app_config::AppConfig, auth_config::PublicKeyError}, + data::Data, + model::{ + plugins::{entry_point::EntryPoint, operation::Operation}, + App, + }, + observability::open_telemetry::OpenTelemetry, + paths::ExistingGraphFolder, + routes::{health, version, PublicFilesEndpoint}, + server::ServerError::SchemaError, + GQLError, }; use config::ConfigError; use once_cell::sync::Lazy; @@ -485,14 +495,17 @@ async fn server_termination( #[cfg(test)] mod server_tests { use crate::{ - client::raphtory_client::RaphtoryGraphQLClient, config::{ - app_config::AppConfigBuilder, otlp_config::{ESSENTIAL_TRACE_SPANS, TracingLevel, TracingProtocol}, - }, server::GraphServer, + client::raphtory_client::RaphtoryGraphQLClient, + config::{ + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol, ESSENTIAL_TRACE_SPANS}, + }, + server::GraphServer, }; use chrono::prelude::*; use opentelemetry::trace::SpanId; use opentelemetry_sdk::{logs::in_memory_exporter::LogDataWithResource, trace::SpanData}; -use raphtory::{ + use raphtory::{ db::api::storage::storage::Config, prelude::{AdditionOps, Graph, StableEncode, NO_PROPS}, vectors::{storage::OpenAIEmbeddings, template::DocumentTemplate}, @@ -531,49 +544,8 @@ use raphtory::{ running.stop().await } - use url::Url; use std::collections::{HashMap, HashSet}; - - fn print_span_tree(spans: &[opentelemetry_sdk::trace::SpanData]) { - let mut children: HashMap> = - HashMap::new(); - - for span in spans { - children - .entry(span.parent_span_id) - .or_default() - .push(span); - } - - for span in spans - .iter() - .filter(|span| span.parent_span_id == SpanId::INVALID) - .collect::>() - { - print_span_node(span, 0, &mut children); - } - } - - fn print_span_node( - span: &opentelemetry_sdk::trace::SpanData, - depth: usize, - children: &mut HashMap>, - ) { - println!( - "{}{} [{} -> {}]", - " ".repeat(depth), - span.name, - span.span_context.span_id(), - span.parent_span_id - ); - - if let Some(mut child_spans) = children.remove(&span.span_context.span_id()) { - child_spans.sort_by_key(|span| span.start_time); - for child in child_spans { - print_span_node(child, depth + 1, children); - } - } - } + use url::Url; #[tokio::test] async fn test_server_start_stop() { @@ -612,277 +584,54 @@ use raphtory::{ handler.await.unwrap().stop().await } + fn print_span_tree(spans: &[opentelemetry_sdk::trace::SpanData]) { + let mut children: HashMap> = + HashMap::new(); - fn expected_operations( - defined_query_operations: Vec, - tracing_level: TracingLevel, - ) -> HashSet { - let mut expected_operations_: HashSet = match tracing_level { - TracingLevel::COMPLETE => defined_query_operations.into_iter().collect(), - TracingLevel::ESSENTIAL => defined_query_operations - .into_iter() - .filter(|op| ESSENTIAL_TRACE_SPANS.contains(&op.as_str())) - .collect(), - TracingLevel::MINIMAL => HashSet::new(), - }; - expected_operations_.extend([ - "request".to_string(), - "parse".to_string(), - "validation".to_string(), - "execute".to_string(), - ]); - expected_operations_ + for span in spans { + children.entry(span.parent_span_id).or_default().push(span); + } + + for span in spans + .iter() + .filter(|span| span.parent_span_id == SpanId::INVALID) + .collect::>() + { + print_span_node(span, 0, &mut children); + } } - fn all_operations(graph_path: String) -> (String, Vec) { - let operations: Vec = vec![ - "updateGraph", - "addNode", - "success", - "node", - "id", - "name", - "createNode", - "addNodes", - "addEdge", - "edge", - "src", - "dst", - "addEdges", - "addProperties", - "addMetadata", - "updateMetadata", - "setNodeType", - "addUpdates", - "delete", - "deleteEdge", - "nodeLookup", - "edgeLookup", - "graph", - "uniqueLayers", - "defaultLayer", - "countNodes", - "countEdges", - "layers", - "excludeLayers", - "layer", - "excludeLayer", - "subgraph", - "valid", - "subgraphNodeTypes", - "excludeNodes", - "window", - "at", - "latest", - "snapshotAt", - "snapshotLatest", - "before", - "after", - "shrinkWindow", - "shrinkStart", - "shrinkEnd", - "created", - "lastOpened", - "lastUpdated", - "earliestTime", - "timestamp", - "eventId", - "datetime", - "latestTime", - "earliestEdgeTime", - "latestEdgeTime", - "countTemporalEdges", - "hasNode", - "hasEdge", - "nodes", - "count", - "list", - "edges", - "properties", - "contains", - "keys", - "values", - "temporal", - "metadata", - "path", - "namespace", - "schema", - "typeName", - "propertyType", - "variants", - "srcType", - "dstType", - "sharedNeighbours", - "flush", - "key" - ] - .into_iter() - .map(String::from) - .collect(); - let query = format!( - "query {{ - updateGraph(path: \"{}\") {{ - addNode(time: 1, name: 1, properties: [{{ key: \"seed\", value: {{ str: \"yes\" }} }}], nodeType: \"seed\", layer: \"main\") {{ - success - node {{ id name }} - }} - createNode(time: 2, name: 3, properties: [{{ key: \"kind\", value: {{ str: \"new\" }} }}]) {{ - success - node {{ id name }} - }} - addNodes(nodes: [ - {{ - name: 4, - updates: [{{ time: 3, properties: [{{ key: \"batch\", value: {{ bool: true }} }}] }}], - metadata: [{{ key: \"group\", value: {{ str: \"ops\" }} }}] - }}, - {{ - name: 5, - updates: [{{ time: 4, properties: [{{ key: \"batch\", value: {{ bool: true }} }}] }}], - metadata: [{{ key: \"group\", value: {{ str: \"ops\" }} }}] - }} - ]) - addEdge(time: 5, src: 1, dst: 2, properties: [{{ key: \"weight\", value: {{ f64: 1.5 }} }}], layer: \"main\") {{ - success - edge {{ src {{ id }} dst {{ id }} }} - src {{ success }} - dst {{ success }} - }} - addEdges(edges: [ - {{ - src: 2, - dst: 3, - updates: [{{ time: 6, properties: [{{ key: \"weight\", value: {{ f64: 2.5 }} }}] }}], - metadata: [{{ key: \"kind\", value: {{ str: \"batch\" }} }}] - }}, - {{ - src: 4, - dst: 5, - updates: [{{ time: 6, properties: [{{ key: \"weight\", value: {{ f64: 3.5 }} }}] }}], - metadata: [{{ key: \"kind\", value: {{ str: \"batch\" }} }}] - }} - ]) - addProperties(t: 7, properties: [{{ key: \"epoch\", value: {{ u64: 1 }} }}]) - addMetadata(properties: [{{ key: \"owner\", value: {{ str: \"team-a\" }} }}]) - updateMetadata(properties: [{{ key: \"owner\", value: {{ str: \"team-b\" }} }}]) - node(name: 4) {{ - setNodeType(newType: \"worker\") - addUpdates(time: 8, properties: [{{ key: \"cpu\", value: {{ u64: 4 }} }}], layer: \"main\") - addMetadata(properties: [{{ key: \"zone\", value: {{ str: \"eu-west\" }} }}]) - updateMetadata(properties: [{{ key: \"zone\", value: {{ str: \"us-east\" }} }}]) - success - node {{ id name }} - }} - edge(src: 1, dst: 2) {{ - addUpdates(time: 9, properties: [{{ key: \"latency\", value: {{ u64: 23 }} }}], layer: \"main\") - addMetadata(properties: [{{ key: \"channel\", value: {{ str: \"grpc\" }} }}], layer: \"main\") - updateMetadata(properties: [{{ key: \"channel\", value: {{ str: \"http\" }} }}], layer: \"main\") - delete(time: 10, layer: \"main\") - success - edge {{ src {{ id }} dst {{ id }} }} - src {{ success }} - dst {{ success }} - }} - deleteEdge(time: 11, src: 4, dst: 5) {{ - success - edge {{ src {{ id }} dst {{ id }} }} - src {{ success }} - dst {{ success }} - }} - nodeLookup: node(name: 1) {{ success node {{ id name }} }} - edgeLookup: edge(src: 1, dst: 2) {{ - success - edge {{ src {{ id }} dst {{ id }} }} - src {{ success }} - dst {{ success }} - }} - graph {{ - uniqueLayers - defaultLayer {{ countNodes countEdges }} - layers(names: [\"main\"]) {{ countNodes countEdges }} - excludeLayers(names: [\"main\"]) {{ countNodes countEdges }} - layer(name: \"main\") {{ countNodes countEdges }} - excludeLayer(name: \"main\") {{ countNodes countEdges }} - subgraph(nodes: [1, 2, 3]) {{ countNodes countEdges }} - valid {{ countNodes countEdges }} - subgraphNodeTypes(nodeTypes: [\"worker\"]) {{ countNodes countEdges }} - excludeNodes(nodes: [5]) {{ countNodes countEdges }} - window(start: 1, end: 12) {{ countNodes countEdges }} - at(time: 9) {{ countNodes countEdges }} - latest {{ countNodes countEdges }} - snapshotAt(time: 9) {{ countNodes countEdges }} - snapshotLatest {{ countNodes countEdges }} - before(time: 12) {{ countNodes countEdges }} - after(time: 1) {{ countNodes countEdges }} - shrinkWindow(start: 1, end: 12) {{ countNodes countEdges }} - shrinkStart(start: 2) {{ countNodes countEdges }} - shrinkEnd(end: 11) {{ countNodes countEdges }} - created - lastOpened - lastUpdated - earliestTime {{ timestamp eventId datetime }} - latestTime {{ timestamp eventId datetime }} - earliestEdgeTime(includeNegative: true) {{ timestamp eventId datetime }} - latestEdgeTime(includeNegative: true) {{ timestamp eventId datetime }} - countNodes - countEdges - countTemporalEdges - hasNode(name: 1) - hasEdge(src: 1, dst: 2, layer: \"main\") - node(name: 1) {{ id name }} - nodes {{ count list {{ id name }} }} - edge(src: 1, dst: 2) {{ src {{ id name }} dst {{ id name }} }} - edges {{ count list {{ src {{ id name }} dst {{ id name }} }} }} - properties {{ - contains(key: \"epoch\") - keys - values {{ key }} - temporal {{ keys }} - }} - metadata {{ - contains(key: \"owner\") - keys - values {{ key }} - }} - name - path - namespace - schema {{ - nodes {{ - typeName - properties {{ key propertyType variants }} - metadata {{ key propertyType variants }} - }} - layers {{ - name - edges {{ - srcType - dstType - properties {{ key propertyType variants }} - metadata {{ key propertyType variants }} - }} - }} - }} - sharedNeighbours(selectedNodes: [1, 2]) {{ id name }} - }} - flush - }} - }}", - graph_path + fn print_span_node( + span: &opentelemetry_sdk::trace::SpanData, + depth: usize, + children: &mut HashMap>, + ) { + println!( + "{}{} [{} -> {}]", + " ".repeat(depth), + span.name, + span.span_context.span_id(), + span.parent_span_id ); - (query, operations) + + if let Some(mut child_spans) = children.remove(&span.span_context.span_id()) { + child_spans.sort_by_key(|span| span.start_time); + for child in child_spans { + print_span_node(child, depth + 1, children); + } + } } - async fn get_spans_and_logs(tracing_level: TracingLevel, query: String) -> (Vec, Vec) { + #[tokio::test] + async fn test_open_telemetry_spans_complete() { let tmp_dir = tempdir().unwrap(); - let graph_path = format!("g"); let graph = Graph::new(); graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); - graph.encode(tmp_dir.path().join(&graph_path)).unwrap(); + graph.encode(tmp_dir.path().join("g")).unwrap(); let app_config = AppConfigBuilder::new() .with_tracing(true) - .with_tracing_level(tracing_level.clone()) + .with_tracing_level(TracingLevel::COMPLETE) .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) .build(); @@ -898,12 +647,24 @@ use raphtory::{ let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); let client = RaphtoryGraphQLClient::new(endpoint, None); + let query = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) + } + flush + } + }"; let result = client.query(&query, HashMap::new()).await; - assert!( - result.is_ok(), - "all-operations mutation failed for tracing level {tracing_level:?}: {:?}", - result - ); + assert!(result.is_ok(), "query failed: {:?}", result); sleep(Duration::from_secs(5)).await; handler.stop().await; @@ -919,55 +680,114 @@ use raphtory::{ .exporters .log_exporter .get_emitted_logs() - .unwrap(); - (finished_spans, emitted_logs) - } - - fn assert_spans(tracing_level: TracingLevel, defined_query_operations: Vec, finished_spans: Vec) { - let actual_span_operations: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); - let expected_span_operations: HashSet = expected_operations(defined_query_operations, tracing_level.clone()); - - let mut missing_from_actual: Vec = expected_span_operations - .difference(&actual_span_operations) - .cloned() - .collect(); - missing_from_actual.sort(); - - let mut unexpected_in_actual: Vec = actual_span_operations - .difference(&expected_span_operations) - .cloned() - .collect(); - unexpected_in_actual.sort(); - - assert!( - missing_from_actual.is_empty() && unexpected_in_actual.is_empty(), - "span operations do not match for tracing level {tracing_level:?}. missing_from_actual={:?}; unexpected_in_actual={:?}", - missing_from_actual, - unexpected_in_actual - ); - } - - #[tokio::test] - async fn test_open_telemetry_spans_complete() { - let tracing_level = TracingLevel::COMPLETE; - let (query, defined_query_operations) = all_operations("g".to_string()); - let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; - assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); - } - - #[tokio::test] - async fn test_open_telemetry_spans_essential() { - let tracing_level = TracingLevel::ESSENTIAL; - let (query, defined_query_operations) = all_operations("g".to_string()); - let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; - assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); - } - - #[tokio::test] - async fn test_open_telemetry_spans_minimal() { - let tracing_level = TracingLevel::MINIMAL; - let (query, defined_query_operations) = all_operations("g".to_string()); - let (finished_spans, _emitted_logs) = get_spans_and_logs(tracing_level.clone(), query).await; - assert_spans(tracing_level.clone(), defined_query_operations, finished_spans); - } + .unwrap(); + let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); + assert_eq!(all_spans, HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "countNodes".to_string(), + "execute".to_string(), + "flush".to_string(), + "graph".to_string(), + "hasNode".to_string(), + "id".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "success".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ])); + } + + // #[tokio::test] + // async fn test_open_telemetry_spans_essential() { + // let tmp_dir = tempdir().unwrap(); + // let graph = Graph::new(); + // graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); + // graph.encode(tmp_dir.path().join("g")).unwrap(); + + // let app_config = AppConfigBuilder::new() + // .with_tracing(true) + // .with_tracing_level(TracingLevel::ESSENTIAL) + // .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + // .build(); + + // let server = GraphServer::new( + // tmp_dir.path().to_path_buf(), + // Some(app_config.clone()), + // Config::default(), + // ) + // .await + // .unwrap(); + // let handler = server.start_with_port(0).await.unwrap(); + + // let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + // let client = RaphtoryGraphQLClient::new(endpoint, None); + + // let query = "query {{ + // updateGraph(path: \"g\") {{ + // addNode(time: 1, name: 1, properties: [{{ key: \"seed\", value: {{ str: \"yes\" }} }}], nodeType: \"seed\", layer: \"main\") {{ + // success + // node {{ id }} + // }} + // addEdge(time: 5, src: 1, dst: 2, properties: [{{ key: \"weight\", value: {{ f64: 1.5 }} }}], layer: \"main\") {{ + // success + // }} + // graph {{ + // countNodes + // hasNode(name: 1) + // }} + // flush + // }} + // }}"; + // let result = client.query(&query, HashMap::new()).await; + + // sleep(Duration::from_secs(5)).await; + // handler.stop().await; + + // let finished_spans = app_config + // .tracing + // .exporters + // .span_exporter + // .get_finished_spans() + // .unwrap(); + // let emitted_logs = app_config + // .tracing + // .exporters + // .log_exporter + // .get_emitted_logs() + // .unwrap(); + // let mut all_spans = finished_spans.iter().map(|span| span.name.clone()).collect::>(); + // all_spans.sort(); + // assert_eq!(all_spans, vec![ + // "request", + // "parse", + // "validation", + // "execute", + // "updateGraph", + // "addNode", + // "success", + // "node", + // "id", + // "addEdge", + // "graph", + // "countNodes", + // "hasNode", + // "flush", + // ]); + // } + + // #[tokio::test] + // async fn test_open_telemetry_spans_minimal() { + // let tracing_level = TracingLevel::MINIMAL; + // let (query, defined_query_operations) = all_operations("g".to_string()); + // let (finished_spans, _emitted_logs) = + // get_spans_and_logs(tracing_level.clone(), query).await; + // assert_spans( + // tracing_level.clone(), + // defined_query_operations, + // finished_spans, + // ); + // } } From 06781214e349f3ac582634e67b8e028fc9b493e6 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Fri, 10 Jul 2026 10:14:33 -0500 Subject: [PATCH 06/22] created 3 open telemetry tests where each test corresponds to one of the tracing levels - essential, minimal, complete in a developer friendly way with reduced DRY practices --- raphtory-graphql/src/server.rs | 245 ++++++++++++++++++++------------- 1 file changed, 148 insertions(+), 97 deletions(-) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index c452962fe1..58bdce694d 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -663,8 +663,7 @@ mod server_tests { flush } }"; - let result = client.query(&query, HashMap::new()).await; - assert!(result.is_ok(), "query failed: {:?}", result); + client.query(&query, HashMap::new()).await; sleep(Duration::from_secs(5)).await; handler.stop().await; @@ -675,119 +674,171 @@ mod server_tests { .span_exporter .get_finished_spans() .unwrap(); + let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); + assert_eq!(all_spans, HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "countNodes".to_string(), + "execute".to_string(), + "flush".to_string(), + "graph".to_string(), + "hasNode".to_string(), + "id".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "success".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ])); let emitted_logs = app_config .tracing .exporters .log_exporter .get_emitted_logs() .unwrap(); + // assert!(emitted_logs.len() == 1); + } + + + #[tokio::test] + async fn test_open_telemetry_spans_essential() { + let tmp_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); + graph.encode(tmp_dir.path().join("g")).unwrap(); + + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(TracingLevel::ESSENTIAL) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + + let query = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) + } + flush + } + }"; + client.query(&query, HashMap::new()).await; + + sleep(Duration::from_secs(5)).await; + handler.stop().await; + + let finished_spans = app_config + .tracing + .exporters + .span_exporter + .get_finished_spans() + .unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); assert_eq!(all_spans, HashSet::from([ "addEdge".to_string(), "addNode".to_string(), - "countNodes".to_string(), "execute".to_string(), - "flush".to_string(), "graph".to_string(), - "hasNode".to_string(), - "id".to_string(), "node".to_string(), "parse".to_string(), "request".to_string(), - "success".to_string(), "updateGraph".to_string(), "validation".to_string(), ])); + let emitted_logs = app_config + .tracing + .exporters + .log_exporter + .get_emitted_logs() + .unwrap(); + // assert!(emitted_logs.len() == 1); } - // #[tokio::test] - // async fn test_open_telemetry_spans_essential() { - // let tmp_dir = tempdir().unwrap(); - // let graph = Graph::new(); - // graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); - // graph.encode(tmp_dir.path().join("g")).unwrap(); - - // let app_config = AppConfigBuilder::new() - // .with_tracing(true) - // .with_tracing_level(TracingLevel::ESSENTIAL) - // .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - // .build(); - - // let server = GraphServer::new( - // tmp_dir.path().to_path_buf(), - // Some(app_config.clone()), - // Config::default(), - // ) - // .await - // .unwrap(); - // let handler = server.start_with_port(0).await.unwrap(); - - // let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - // let client = RaphtoryGraphQLClient::new(endpoint, None); - - // let query = "query {{ - // updateGraph(path: \"g\") {{ - // addNode(time: 1, name: 1, properties: [{{ key: \"seed\", value: {{ str: \"yes\" }} }}], nodeType: \"seed\", layer: \"main\") {{ - // success - // node {{ id }} - // }} - // addEdge(time: 5, src: 1, dst: 2, properties: [{{ key: \"weight\", value: {{ f64: 1.5 }} }}], layer: \"main\") {{ - // success - // }} - // graph {{ - // countNodes - // hasNode(name: 1) - // }} - // flush - // }} - // }}"; - // let result = client.query(&query, HashMap::new()).await; - - // sleep(Duration::from_secs(5)).await; - // handler.stop().await; - - // let finished_spans = app_config - // .tracing - // .exporters - // .span_exporter - // .get_finished_spans() - // .unwrap(); - // let emitted_logs = app_config - // .tracing - // .exporters - // .log_exporter - // .get_emitted_logs() - // .unwrap(); - // let mut all_spans = finished_spans.iter().map(|span| span.name.clone()).collect::>(); - // all_spans.sort(); - // assert_eq!(all_spans, vec![ - // "request", - // "parse", - // "validation", - // "execute", - // "updateGraph", - // "addNode", - // "success", - // "node", - // "id", - // "addEdge", - // "graph", - // "countNodes", - // "hasNode", - // "flush", - // ]); - // } - - // #[tokio::test] - // async fn test_open_telemetry_spans_minimal() { - // let tracing_level = TracingLevel::MINIMAL; - // let (query, defined_query_operations) = all_operations("g".to_string()); - // let (finished_spans, _emitted_logs) = - // get_spans_and_logs(tracing_level.clone(), query).await; - // assert_spans( - // tracing_level.clone(), - // defined_query_operations, - // finished_spans, - // ); - // } + #[tokio::test] + async fn test_open_telemetry_spans_minimal() { + let tmp_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); + graph.encode(tmp_dir.path().join("g")).unwrap(); + + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(TracingLevel::MINIMAL) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + + let query = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) + } + flush + } + }"; + client.query(&query, HashMap::new()).await; + + sleep(Duration::from_secs(5)).await; + handler.stop().await; + + let finished_spans = app_config + .tracing + .exporters + .span_exporter + .get_finished_spans() + .unwrap(); + let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); + assert_eq!(all_spans, HashSet::from([ + "execute".to_string(), + "parse".to_string(), + "request".to_string(), + "validation".to_string(), + ])); + let emitted_logs = app_config + .tracing + .exporters + .log_exporter + .get_emitted_logs() + .unwrap(); + // assert!(emitted_logs.len() == 1); + } } From 745de1a52e85bfbee449f841ae7fb9c24d0309ca Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Sun, 12 Jul 2026 13:04:36 -0500 Subject: [PATCH 07/22] finished open telemetry span tests except there's an issue when running all span tests at once with the logging --- raphtory-graphql/src/config/otlp_config.rs | 2 +- raphtory-graphql/src/server.rs | 246 +++++---------------- 2 files changed, 56 insertions(+), 192 deletions(-) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index d2695b5d27..55ff3ecd3c 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -125,7 +125,7 @@ pub struct TracingConfig { pub transport_certificate: Option, #[cfg(test)] #[serde(skip)] - // exposed in-memory exporters for testing + // Expose in-memory exporters to retrieve spans and logs in tests. pub exporters: InMemoryExporters, } diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 58bdce694d..144e653287 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -495,23 +495,20 @@ async fn server_termination( #[cfg(test)] mod server_tests { use crate::{ - client::raphtory_client::RaphtoryGraphQLClient, - config::{ - app_config::AppConfigBuilder, - otlp_config::{TracingLevel, TracingProtocol, ESSENTIAL_TRACE_SPANS}, - }, - server::GraphServer, + client::raphtory_client::RaphtoryGraphQLClient, config::{ + app_config::AppConfigBuilder, otlp_config::{ESSENTIAL_TRACE_SPANS, TracingLevel, TracingProtocol}, + }, server::{GraphServer, RunningGraphServer}, }; use chrono::prelude::*; use opentelemetry::trace::SpanId; - use opentelemetry_sdk::{logs::in_memory_exporter::LogDataWithResource, trace::SpanData}; + use opentelemetry_sdk::{logs::{InMemoryLogExporter, in_memory_exporter::LogDataWithResource}, trace::{InMemorySpanExporter, SpanData}}; use raphtory::{ db::api::storage::storage::Config, prelude::{AdditionOps, Graph, StableEncode, NO_PROPS}, vectors::{storage::OpenAIEmbeddings, template::DocumentTemplate}, }; use raphtory_api::core::utils::logging::global_info_logger; - use tempfile::tempdir; + use tempfile::{tempdir, TempDir}; use tokio::time::{sleep, Duration}; use tracing::info; @@ -584,54 +581,39 @@ mod server_tests { handler.await.unwrap().stop().await } - fn print_span_tree(spans: &[opentelemetry_sdk::trace::SpanData]) { - let mut children: HashMap> = - HashMap::new(); - - for span in spans { - children.entry(span.parent_span_id).or_default().push(span); - } - - for span in spans - .iter() - .filter(|span| span.parent_span_id == SpanId::INVALID) - .collect::>() - { - print_span_node(span, 0, &mut children); - } - } - - fn print_span_node( - span: &opentelemetry_sdk::trace::SpanData, - depth: usize, - children: &mut HashMap>, - ) { - println!( - "{}{} [{} -> {}]", - " ".repeat(depth), - span.name, - span.span_context.span_id(), - span.parent_span_id - ); - - if let Some(mut child_spans) = children.remove(&span.span_context.span_id()) { - child_spans.sort_by_key(|span| span.start_time); - for child in child_spans { - print_span_node(child, depth + 1, children); + const OPEN_TELEMETRY_QUERY: &str = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) } + flush } - } - - #[tokio::test] - async fn test_open_telemetry_spans_complete() { + }"; + + async fn setup_for_span_tests( + tracing_level: TracingLevel, + ) -> ( + RaphtoryGraphQLClient, + RunningGraphServer, + InMemorySpanExporter, + InMemoryLogExporter, + TempDir, + ) { let tmp_dir = tempdir().unwrap(); let graph = Graph::new(); - graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); graph.encode(tmp_dir.path().join("g")).unwrap(); let app_config = AppConfigBuilder::new() .with_tracing(true) - .with_tracing_level(TracingLevel::COMPLETE) + .with_tracing_level(tracing_level) .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) .build(); @@ -646,34 +628,19 @@ mod server_tests { let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); let client = RaphtoryGraphQLClient::new(endpoint, None); + let span_exporter = app_config.tracing.exporters.span_exporter.clone(); + let log_exporter = app_config.tracing.exporters.log_exporter.clone(); + (client, handler, span_exporter, log_exporter, tmp_dir) + } - let query = "query { - updateGraph(path: \"g\") { - addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { - success - node { id } - } - addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { - success - } - graph { - countNodes - hasNode(name: 1) - } - flush - } - }"; - client.query(&query, HashMap::new()).await; - + #[tokio::test] + async fn test_open_telemetry_spans_complete() { + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::COMPLETE).await; + let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); sleep(Duration::from_secs(5)).await; handler.stop().await; - - let finished_spans = app_config - .tracing - .exporters - .span_exporter - .get_finished_spans() - .unwrap(); + let finished_spans = span_exporter.get_finished_spans().unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); assert_eq!(all_spans, HashSet::from([ "addEdge".to_string(), @@ -691,68 +658,19 @@ mod server_tests { "updateGraph".to_string(), "validation".to_string(), ])); - let emitted_logs = app_config - .tracing - .exporters - .log_exporter - .get_emitted_logs() - .unwrap(); - // assert!(emitted_logs.len() == 1); + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); } #[tokio::test] async fn test_open_telemetry_spans_essential() { - let tmp_dir = tempdir().unwrap(); - let graph = Graph::new(); - graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); - graph.encode(tmp_dir.path().join("g")).unwrap(); - - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(TracingLevel::ESSENTIAL) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); - - let query = "query { - updateGraph(path: \"g\") { - addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { - success - node { id } - } - addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { - success - } - graph { - countNodes - hasNode(name: 1) - } - flush - } - }"; - client.query(&query, HashMap::new()).await; - + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::ESSENTIAL).await; + let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); sleep(Duration::from_secs(5)).await; handler.stop().await; - - let finished_spans = app_config - .tracing - .exporters - .span_exporter - .get_finished_spans() - .unwrap(); + let finished_spans = span_exporter.get_finished_spans().unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); assert_eq!(all_spans, HashSet::from([ "addEdge".to_string(), @@ -765,80 +683,26 @@ mod server_tests { "updateGraph".to_string(), "validation".to_string(), ])); - let emitted_logs = app_config - .tracing - .exporters - .log_exporter - .get_emitted_logs() - .unwrap(); - // assert!(emitted_logs.len() == 1); + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); } #[tokio::test] async fn test_open_telemetry_spans_minimal() { - let tmp_dir = tempdir().unwrap(); - let graph = Graph::new(); - graph.add_node(0, 0, NO_PROPS, None, None).unwrap(); - graph.encode(tmp_dir.path().join("g")).unwrap(); - - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(TracingLevel::MINIMAL) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); - - let query = "query { - updateGraph(path: \"g\") { - addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { - success - node { id } - } - addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { - success - } - graph { - countNodes - hasNode(name: 1) - } - flush - } - }"; - client.query(&query, HashMap::new()).await; - + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::MINIMAL).await; + let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); sleep(Duration::from_secs(5)).await; handler.stop().await; - - let finished_spans = app_config - .tracing - .exporters - .span_exporter - .get_finished_spans() - .unwrap(); - let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); assert_eq!(all_spans, HashSet::from([ "execute".to_string(), "parse".to_string(), "request".to_string(), "validation".to_string(), ])); - let emitted_logs = app_config - .tracing - .exporters - .log_exporter - .get_emitted_logs() - .unwrap(); - // assert!(emitted_logs.len() == 1); + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); } } From 9a70aa78651cd256ea6eca915486e1c8feb71b1f Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Mon, 13 Jul 2026 13:38:19 -0500 Subject: [PATCH 08/22] switch to single exporters for testing --- raphtory-graphql/src/config/otlp_config.rs | 30 +++++++++++++++++++++- raphtory-graphql/src/server.rs | 9 +++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index 55ff3ecd3c..14d67ba308 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -173,6 +173,34 @@ impl TracingConfig { (tracer, logger) } + #[cfg(test)] + fn with_simple_exporter< + E: opentelemetry_sdk::trace::SpanExporter + 'static, + L: opentelemetry_sdk::logs::LogExporter + 'static, + >( + &self, + span_exporter: E, + log_exporter: L, + ) -> (SdkTracerProvider, SdkLoggerProvider) { + let resource = Resource::builder() + .with_attributes(vec![KeyValue::new( + "service.name", + self.service_name.clone(), + )]) + .build(); + let tracer = SdkTracerProvider::builder() + .with_simple_exporter(span_exporter) + .with_sampler(Sampler::AlwaysOn) + .with_resource(resource.clone()) + .build(); + + let logger = SdkLoggerProvider::builder() + .with_simple_exporter(log_exporter) + .with_resource(resource) + .build(); + (tracer, logger) + } + pub async fn tracer_provider( &self, ) -> Result, ServerError> { @@ -281,7 +309,7 @@ impl TracingConfig { "Sending traces to in-memory exporter with tracing level `{}`", self.level ); - Ok(self.with_exporter( + Ok(self.with_simple_exporter( self.exporters.span_exporter.clone(), self.exporters.log_exporter.clone(), )) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 144e653287..7f428be1f5 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -638,7 +638,6 @@ mod server_tests { let (client, handler, span_exporter, log_exporter, _tmp_dir) = setup_for_span_tests(TracingLevel::COMPLETE).await; let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - sleep(Duration::from_secs(5)).await; handler.stop().await; let finished_spans = span_exporter.get_finished_spans().unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); @@ -659,7 +658,7 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); + // assert!(!emitted_logs.is_empty()); } @@ -668,7 +667,6 @@ mod server_tests { let (client, handler, span_exporter, log_exporter, _tmp_dir) = setup_for_span_tests(TracingLevel::ESSENTIAL).await; let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - sleep(Duration::from_secs(5)).await; handler.stop().await; let finished_spans = span_exporter.get_finished_spans().unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); @@ -684,7 +682,7 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); + // assert!(!emitted_logs.is_empty()); } #[tokio::test] @@ -692,7 +690,6 @@ mod server_tests { let (client, handler, span_exporter, log_exporter, _tmp_dir) = setup_for_span_tests(TracingLevel::MINIMAL).await; let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - sleep(Duration::from_secs(5)).await; handler.stop().await; let finished_spans = span_exporter.get_finished_spans().unwrap(); let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); @@ -703,6 +700,6 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); + // assert!(!emitted_logs.is_empty()); } } From 1cabb7f5a7e12dff58c649212b7e481af836b3d7 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Mon, 13 Jul 2026 14:55:01 -0500 Subject: [PATCH 09/22] fixing up tests --- raphtory-graphql/src/server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 7f428be1f5..d6c858f779 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -658,7 +658,7 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - // assert!(!emitted_logs.is_empty()); + assert!(!emitted_logs.is_empty()); } @@ -682,7 +682,7 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - // assert!(!emitted_logs.is_empty()); + assert!(!emitted_logs.is_empty()); } #[tokio::test] @@ -700,6 +700,6 @@ mod server_tests { "validation".to_string(), ])); let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - // assert!(!emitted_logs.is_empty()); + assert!(!emitted_logs.is_empty()); } } From bcf8e0c3648123aedc551d995d333cb937229e81 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Mon, 13 Jul 2026 23:52:24 -0500 Subject: [PATCH 10/22] fixed logging and span issues with tests --- raphtory-graphql/Cargo.toml | 4 +- raphtory-graphql/src/config/app_config.rs | 2 +- raphtory-graphql/src/config/otlp_config.rs | 16 +- raphtory-graphql/src/server.rs | 155 ++----------------- raphtory-graphql/tests/open_telemetry.rs | 166 +++++++++++++++++++++ 5 files changed, 192 insertions(+), 151 deletions(-) create mode 100644 raphtory-graphql/tests/open_telemetry.rs diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index d0f00fbff3..4c01367ad0 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -35,7 +35,7 @@ dynamic-graphql = { workspace = true } async-graphql-poem = { workspace = true } futures-util = { workspace = true } opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["testing"] } opentelemetry-otlp = { workspace = true, features = ["http-json", "http-proto", "reqwest-rustls", "tls-aws-lc", "reqwest-blocking-client", "internal-logs"] } opentelemetry-stdout = { workspace = true } opentelemetry-appender-tracing = { workspace = true } @@ -74,6 +74,8 @@ parquet = { workspace = true } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } [features] +default = [] python = ["dep:pyo3", "dep:pythonize", "raphtory/python"] search = ["raphtory/search"] panic-on-drop = ["raphtory/panic-on-drop"] +integration-test = [] diff --git a/raphtory-graphql/src/config/app_config.rs b/raphtory-graphql/src/config/app_config.rs index b590873435..198be43914 100644 --- a/raphtory-graphql/src/config/app_config.rs +++ b/raphtory-graphql/src/config/app_config.rs @@ -167,7 +167,7 @@ impl AppConfigBuilder { .map_err(|e| invalid_value([path, sub_path], e))?, ); } - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] TracingConfigFieldName::Exporters => { // Exporter handles are runtime-only and should not be loaded from config. } diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index 14d67ba308..ed14847925 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -7,7 +7,7 @@ use opentelemetry_appender_tracing::layer::{ OpenTelemetryTracingBridge, OpenTelemetryTracingBridgeBuilder, }; use opentelemetry_otlp::{LogExporter, Protocol, SpanExporter, WithExportConfig, WithHttpConfig}; -#[cfg(test)] +#[cfg(any(feature = "integration-test", debug_assertions))] use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; use opentelemetry_sdk::{ logs::SdkLoggerProvider, @@ -82,7 +82,7 @@ pub enum TracingProtocol { TONIC, HTTP, STDOUT, - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] IN_MEMORY, } @@ -123,7 +123,7 @@ pub struct TracingConfig { /// Headers to use when transport_protocol is set to HTTP pub transport_headers: HashMap, pub transport_certificate: Option, - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] #[serde(skip)] // Expose in-memory exporters to retrieve spans and logs in tests. pub exporters: InMemoryExporters, @@ -139,7 +139,7 @@ impl Default for TracingConfig { transport_protocol: DEFAULT_OTLP_TRANSPORT_PROTOCOL, transport_headers: Default::default(), transport_certificate: None, - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] exporters: InMemoryExporters::default(), } } @@ -173,7 +173,7 @@ impl TracingConfig { (tracer, logger) } - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] fn with_simple_exporter< E: opentelemetry_sdk::trace::SpanExporter + 'static, L: opentelemetry_sdk::logs::LogExporter + 'static, @@ -303,7 +303,7 @@ impl TracingConfig { opentelemetry_stdout::LogExporter::default(), )) } - #[cfg(test)] + #[cfg(any(feature = "integration-test", debug_assertions))] TracingProtocol::IN_MEMORY => { eprintln!( "Sending traces to in-memory exporter with tracing level `{}`", @@ -329,14 +329,14 @@ impl TracingConfig { } } -#[cfg(test)] +#[cfg(any(feature = "integration-test", debug_assertions))] #[derive(Clone, Debug, Default)] pub struct InMemoryExporters { pub span_exporter: InMemorySpanExporter, pub log_exporter: InMemoryLogExporter, } -#[cfg(test)] +#[cfg(any(feature = "integration-test", debug_assertions))] impl PartialEq for InMemoryExporters { fn eq(&self, _other: &Self) -> bool { // Exporters are runtime handles; identity/equality semantics are not meaningful. diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index d6c858f779..f4f82a008a 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -57,7 +57,7 @@ use tokio::{ }; use tracing::{debug, error, info, warn}; use tracing_subscriber::{ - fmt, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, Registry, + fmt, fmt::format::FmtSpan, layer::SubscriberExt, Registry, }; use url::ParseError; @@ -273,20 +273,16 @@ impl GraphServer { let registry = Registry::default().with(filter).with( fmt::layer().pretty().with_span_events(FmtSpan::NONE), //(FULL, NEW, ENTER, EXIT, CLOSE) ); - match tp.clone() { - Some((span, log)) => { + let dispatch = match tp.clone() { + Some((span, log)) => tracing::Dispatch::new( registry .with( tracing_opentelemetry::layer() .with_tracer(span.tracer(tracer_name.clone())), ) - .with(OpenTelemetryTracingBridge::new(&log)) - .try_init() - .unwrap_or_else(|err| error!("Failed to initialise tracer provider: {err}")); - } - None => { - registry.try_init().ok(); - } + .with(OpenTelemetryTracingBridge::new(&log)), + ), + None => tracing::Dispatch::new(registry), }; let work_dir = self.work_dir(); @@ -311,7 +307,11 @@ impl GraphServer { server_termination(signal_receiver, tp), None, ); - let server_result = AbortOnDrop(tokio::spawn(server_task)); + let server_result = AbortOnDrop(tokio::spawn(async move { + let _guard = tracing::dispatcher::set_default(&dispatch); + info!("Server tracing dispatch initialised"); + server_task.await + })); info!("UI listening on 0.0.0.0:{actual_port}, live at: http://localhost:{actual_port}"); debug!( @@ -495,20 +495,17 @@ async fn server_termination( #[cfg(test)] mod server_tests { use crate::{ - client::raphtory_client::RaphtoryGraphQLClient, config::{ - app_config::AppConfigBuilder, otlp_config::{ESSENTIAL_TRACE_SPANS, TracingLevel, TracingProtocol}, - }, server::{GraphServer, RunningGraphServer}, + config::app_config::AppConfigBuilder, + server::GraphServer, }; use chrono::prelude::*; - use opentelemetry::trace::SpanId; - use opentelemetry_sdk::{logs::{InMemoryLogExporter, in_memory_exporter::LogDataWithResource}, trace::{InMemorySpanExporter, SpanData}}; use raphtory::{ db::api::storage::storage::Config, prelude::{AdditionOps, Graph, StableEncode, NO_PROPS}, vectors::{storage::OpenAIEmbeddings, template::DocumentTemplate}, }; use raphtory_api::core::utils::logging::global_info_logger; - use tempfile::{tempdir, TempDir}; + use tempfile::tempdir; use tokio::time::{sleep, Duration}; use tracing::info; @@ -541,9 +538,6 @@ mod server_tests { running.stop().await } - use std::collections::{HashMap, HashSet}; - use url::Url; - #[tokio::test] async fn test_server_start_stop() { global_info_logger(); @@ -581,125 +575,4 @@ mod server_tests { handler.await.unwrap().stop().await } - const OPEN_TELEMETRY_QUERY: &str = "query { - updateGraph(path: \"g\") { - addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { - success - node { id } - } - addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { - success - } - graph { - countNodes - hasNode(name: 1) - } - flush - } - }"; - - async fn setup_for_span_tests( - tracing_level: TracingLevel, - ) -> ( - RaphtoryGraphQLClient, - RunningGraphServer, - InMemorySpanExporter, - InMemoryLogExporter, - TempDir, - ) { - let tmp_dir = tempdir().unwrap(); - let graph = Graph::new(); - graph.encode(tmp_dir.path().join("g")).unwrap(); - - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(tracing_level) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); - let span_exporter = app_config.tracing.exporters.span_exporter.clone(); - let log_exporter = app_config.tracing.exporters.log_exporter.clone(); - (client, handler, span_exporter, log_exporter, tmp_dir) - } - - #[tokio::test] - async fn test_open_telemetry_spans_complete() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::COMPLETE).await; - let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); - assert_eq!(all_spans, HashSet::from([ - "addEdge".to_string(), - "addNode".to_string(), - "countNodes".to_string(), - "execute".to_string(), - "flush".to_string(), - "graph".to_string(), - "hasNode".to_string(), - "id".to_string(), - "node".to_string(), - "parse".to_string(), - "request".to_string(), - "success".to_string(), - "updateGraph".to_string(), - "validation".to_string(), - ])); - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); - } - - - #[tokio::test] - async fn test_open_telemetry_spans_essential() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::ESSENTIAL).await; - let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); - assert_eq!(all_spans, HashSet::from([ - "addEdge".to_string(), - "addNode".to_string(), - "execute".to_string(), - "graph".to_string(), - "node".to_string(), - "parse".to_string(), - "request".to_string(), - "updateGraph".to_string(), - "validation".to_string(), - ])); - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); - } - - #[tokio::test] - async fn test_open_telemetry_spans_minimal() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::MINIMAL).await; - let _ = client.query(&OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans.iter().map(|span| span.name.to_string()).collect(); - assert_eq!(all_spans, HashSet::from([ - "execute".to_string(), - "parse".to_string(), - "request".to_string(), - "validation".to_string(), - ])); - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); - } } diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs new file mode 100644 index 0000000000..8a1b6d8c75 --- /dev/null +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -0,0 +1,166 @@ +#![cfg(any(feature = "integration-test", debug_assertions))] + +use raphtory::db::api::storage::storage::Config; +use raphtory::prelude::{Graph, StableEncode}; +use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; +use raphtory_graphql::config::{ + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol}, +}; +use raphtory_graphql::server::{GraphServer, RunningGraphServer}; +use std::collections::{HashMap, HashSet}; +use tempfile::{tempdir, TempDir}; +use url::Url; + +use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; + +const OPEN_TELEMETRY_QUERY: &str = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) + } + flush + } +}"; + +async fn setup_for_span_tests( + tracing_level: TracingLevel, +) -> ( + RaphtoryGraphQLClient, + RunningGraphServer, + InMemorySpanExporter, + InMemoryLogExporter, + TempDir, +) { + let tmp_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.encode(tmp_dir.path().join("g")).unwrap(); + + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(tracing_level) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + let span_exporter = app_config.tracing.exporters.span_exporter.clone(); + let log_exporter = app_config.tracing.exporters.log_exporter.clone(); + (client, handler, span_exporter, log_exporter, tmp_dir) +} + +async fn test_open_telemetry_spans_complete() { + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::COMPLETE).await; + let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "countNodes".to_string(), + "execute".to_string(), + "flush".to_string(), + "graph".to_string(), + "hasNode".to_string(), + "id".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "success".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); +} + +async fn test_open_telemetry_spans_essential() { + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::ESSENTIAL).await; + let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "execute".to_string(), + "graph".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); +} + +async fn test_open_telemetry_spans_minimal() { + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::MINIMAL).await; + let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "execute".to_string(), + "parse".to_string(), + "request".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); +} + +#[tokio::test] +async fn test_open_telemetry_spans() { + test_open_telemetry_spans_complete().await; + test_open_telemetry_spans_essential().await; + test_open_telemetry_spans_minimal().await; +} + + From 78f96f74bda4108f7c3e0a20b6a20028dc5e9f83 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 12:37:09 -0500 Subject: [PATCH 11/22] got rid of debug assertion --- Cargo.lock | 1 + raphtory-graphql/Cargo.toml | 4 ++-- raphtory-graphql/src/config/app_config.rs | 2 +- raphtory-graphql/src/config/otlp_config.rs | 16 ++++++++-------- raphtory-graphql/tests/open_telemetry.rs | 2 -- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e22f507a1e..c7575d12c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6544,6 +6544,7 @@ dependencies = [ "quick_cache", "raphtory", "raphtory-api", + "raphtory-graphql", "raphtory-storage", "rayon", "reqwest 0.13.3", diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 4c01367ad0..417461fbb8 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -71,11 +71,11 @@ tempfile = { workspace = true } pretty_assertions = { workspace = true } arrow = { workspace = true } parquet = { workspace = true } +raphtory-graphql = { workspace = true, features = ["integration-test"] } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } [features] -default = [] +integration-test = [] python = ["dep:pyo3", "dep:pythonize", "raphtory/python"] search = ["raphtory/search"] panic-on-drop = ["raphtory/panic-on-drop"] -integration-test = [] diff --git a/raphtory-graphql/src/config/app_config.rs b/raphtory-graphql/src/config/app_config.rs index 198be43914..173f94f547 100644 --- a/raphtory-graphql/src/config/app_config.rs +++ b/raphtory-graphql/src/config/app_config.rs @@ -167,7 +167,7 @@ impl AppConfigBuilder { .map_err(|e| invalid_value([path, sub_path], e))?, ); } - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] TracingConfigFieldName::Exporters => { // Exporter handles are runtime-only and should not be loaded from config. } diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index ed14847925..aae68a0b9a 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -7,7 +7,7 @@ use opentelemetry_appender_tracing::layer::{ OpenTelemetryTracingBridge, OpenTelemetryTracingBridgeBuilder, }; use opentelemetry_otlp::{LogExporter, Protocol, SpanExporter, WithExportConfig, WithHttpConfig}; -#[cfg(any(feature = "integration-test", debug_assertions))] +#[cfg(feature = "integration-test")] use opentelemetry_sdk::{logs::InMemoryLogExporter, trace::InMemorySpanExporter}; use opentelemetry_sdk::{ logs::SdkLoggerProvider, @@ -82,7 +82,7 @@ pub enum TracingProtocol { TONIC, HTTP, STDOUT, - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] IN_MEMORY, } @@ -123,7 +123,7 @@ pub struct TracingConfig { /// Headers to use when transport_protocol is set to HTTP pub transport_headers: HashMap, pub transport_certificate: Option, - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] #[serde(skip)] // Expose in-memory exporters to retrieve spans and logs in tests. pub exporters: InMemoryExporters, @@ -139,7 +139,7 @@ impl Default for TracingConfig { transport_protocol: DEFAULT_OTLP_TRANSPORT_PROTOCOL, transport_headers: Default::default(), transport_certificate: None, - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] exporters: InMemoryExporters::default(), } } @@ -173,7 +173,7 @@ impl TracingConfig { (tracer, logger) } - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] fn with_simple_exporter< E: opentelemetry_sdk::trace::SpanExporter + 'static, L: opentelemetry_sdk::logs::LogExporter + 'static, @@ -303,7 +303,7 @@ impl TracingConfig { opentelemetry_stdout::LogExporter::default(), )) } - #[cfg(any(feature = "integration-test", debug_assertions))] + #[cfg(feature = "integration-test")] TracingProtocol::IN_MEMORY => { eprintln!( "Sending traces to in-memory exporter with tracing level `{}`", @@ -329,14 +329,14 @@ impl TracingConfig { } } -#[cfg(any(feature = "integration-test", debug_assertions))] +#[cfg(feature = "integration-test")] #[derive(Clone, Debug, Default)] pub struct InMemoryExporters { pub span_exporter: InMemorySpanExporter, pub log_exporter: InMemoryLogExporter, } -#[cfg(any(feature = "integration-test", debug_assertions))] +#[cfg(feature = "integration-test")] impl PartialEq for InMemoryExporters { fn eq(&self, _other: &Self) -> bool { // Exporters are runtime handles; identity/equality semantics are not meaningful. diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index 8a1b6d8c75..ea817f18fb 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -1,5 +1,3 @@ -#![cfg(any(feature = "integration-test", debug_assertions))] - use raphtory::db::api::storage::storage::Config; use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; From e1a58e85efaa58299329257882c28d0cfb5ad329 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 12:40:35 -0500 Subject: [PATCH 12/22] enable dev dependency raphtory-graphql default-feature to false --- raphtory-graphql/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 417461fbb8..18b12920ce 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -71,7 +71,7 @@ tempfile = { workspace = true } pretty_assertions = { workspace = true } arrow = { workspace = true } parquet = { workspace = true } -raphtory-graphql = { workspace = true, features = ["integration-test"] } +raphtory-graphql = { workspace = true, default-features = false, features = ["integration-test"] } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } [features] From ce64a26ca8ed994d83c560895e01c5e9958fa00a Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 12:45:43 -0500 Subject: [PATCH 13/22] enable dev dependency raphtory-graphql default-feature to false --- raphtory-graphql/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 18b12920ce..1b7a4b9e9a 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -35,7 +35,7 @@ dynamic-graphql = { workspace = true } async-graphql-poem = { workspace = true } futures-util = { workspace = true } opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true, features = ["testing"] } +opentelemetry_sdk = { workspace = true } opentelemetry-otlp = { workspace = true, features = ["http-json", "http-proto", "reqwest-rustls", "tls-aws-lc", "reqwest-blocking-client", "internal-logs"] } opentelemetry-stdout = { workspace = true } opentelemetry-appender-tracing = { workspace = true } @@ -71,6 +71,7 @@ tempfile = { workspace = true } pretty_assertions = { workspace = true } arrow = { workspace = true } parquet = { workspace = true } +# Self-reference lets integration tests compile this crate with the `integration-test` feature enabled. raphtory-graphql = { workspace = true, default-features = false, features = ["integration-test"] } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } From cd032b34ead520cbdc8af1d7500be193a5002234 Mon Sep 17 00:00:00 2001 From: Daniel Lacina <127566471+DanielLacina@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:34:12 -0500 Subject: [PATCH 14/22] Update raphtory-graphql/Cargo.toml Co-authored-by: Fadhil Abubaker --- raphtory-graphql/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 02c80c53ae..97f3d0ae7f 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -70,7 +70,7 @@ tempfile = { workspace = true } pretty_assertions = { workspace = true } arrow = { workspace = true } parquet = { workspace = true } -# Self-reference lets integration tests compile this crate with the `integration-test` feature enabled. +# Self-reference lets cargo test compile this crate with the `integration-test` feature enabled. raphtory-graphql = { workspace = true, default-features = false, features = ["integration-test"] } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } From 818c41465bdea79e2cc8c74e21e6ff22e4010966 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 13:42:39 -0500 Subject: [PATCH 15/22] added comment --- raphtory-graphql/tests/open_telemetry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index ea817f18fb..0dcd1cd9e0 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -1,3 +1,4 @@ +// tests must be separated into their own binary to avoid conflicts with initializing the tracing subscriber multiple times in the same process use raphtory::db::api::storage::storage::Config; use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; From 225f045210cf0825e7f6a7a2645f0d5bb642ef7a Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 15:29:54 -0500 Subject: [PATCH 16/22] made exporters reusable across multiple tracing tests --- raphtory-graphql/src/config/app_config.rs | 4 -- raphtory-graphql/src/config/otlp_config.rs | 47 +++++++++++----------- raphtory-graphql/src/server.rs | 25 ++++++------ raphtory-graphql/tests/open_telemetry.rs | 10 +++-- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/raphtory-graphql/src/config/app_config.rs b/raphtory-graphql/src/config/app_config.rs index 173f94f547..628872e414 100644 --- a/raphtory-graphql/src/config/app_config.rs +++ b/raphtory-graphql/src/config/app_config.rs @@ -167,10 +167,6 @@ impl AppConfigBuilder { .map_err(|e| invalid_value([path, sub_path], e))?, ); } - #[cfg(feature = "integration-test")] - TracingConfigFieldName::Exporters => { - // Exporter handles are runtime-only and should not be loaded from config. - } } } } diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index aae68a0b9a..d638bdda37 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -17,7 +17,15 @@ use opentelemetry_sdk::{ use raphtory_api::core::storage::arc_str::OptionAsStr; use reqwest::{blocking::ClientBuilder, Certificate}; use serde::Deserialize; -use std::{collections::HashMap, env, fs::File, io::Read, path::PathBuf, time::Duration}; +use std::{ + collections::HashMap, + env, + fs::File, + io::Read, + path::PathBuf, + sync::LazyLock, + time::Duration, +}; use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; @@ -113,6 +121,19 @@ pub const DEFAULT_OTLP_TRANSPORT_PROTOCOL: TracingProtocol = TracingProtocol::TO pub const DEFAULT_OTLP_AGENT_PORT_TONIC: u16 = 4317; pub const DEFAULT_OTLP_TRACING_SERVICE_NAME: &'static str = "Raphtory"; +#[cfg(feature = "integration-test")] +#[derive(Clone)] +pub struct GlobalExporters { + pub span: InMemorySpanExporter, + pub log: InMemoryLogExporter, +} + +#[cfg(feature = "integration-test")] +pub static GLOBAL_EXPORTERS: LazyLock = LazyLock::new(|| GlobalExporters { + span: InMemorySpanExporter::default(), + log: InMemoryLogExporter::default(), +}); + #[derive(Clone, Deserialize, Debug, PartialEq, serde::Serialize, FieldName)] pub struct TracingConfig { pub enabled: bool, @@ -123,10 +144,6 @@ pub struct TracingConfig { /// Headers to use when transport_protocol is set to HTTP pub transport_headers: HashMap, pub transport_certificate: Option, - #[cfg(feature = "integration-test")] - #[serde(skip)] - // Expose in-memory exporters to retrieve spans and logs in tests. - pub exporters: InMemoryExporters, } impl Default for TracingConfig { @@ -139,8 +156,6 @@ impl Default for TracingConfig { transport_protocol: DEFAULT_OTLP_TRANSPORT_PROTOCOL, transport_headers: Default::default(), transport_certificate: None, - #[cfg(feature = "integration-test")] - exporters: InMemoryExporters::default(), } } } @@ -310,8 +325,8 @@ impl TracingConfig { self.level ); Ok(self.with_simple_exporter( - self.exporters.span_exporter.clone(), - self.exporters.log_exporter.clone(), + GLOBAL_EXPORTERS.span.clone(), + GLOBAL_EXPORTERS.log.clone(), )) } }; @@ -329,20 +344,6 @@ impl TracingConfig { } } -#[cfg(feature = "integration-test")] -#[derive(Clone, Debug, Default)] -pub struct InMemoryExporters { - pub span_exporter: InMemorySpanExporter, - pub log_exporter: InMemoryLogExporter, -} - -#[cfg(feature = "integration-test")] -impl PartialEq for InMemoryExporters { - fn eq(&self, _other: &Self) -> bool { - // Exporters are runtime handles; identity/equality semantics are not meaningful. - true - } -} diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index bb1a0f03c5..1b291b6bef 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -56,7 +56,7 @@ use tokio::{ }; use tracing::{debug, error, info, warn}; use tracing_subscriber::{ - fmt, fmt::format::FmtSpan, layer::SubscriberExt, Registry, + Registry, fmt::{self, format::FmtSpan}, layer::SubscriberExt, util::SubscriberInitExt, }; use url::ParseError; @@ -274,16 +274,20 @@ impl GraphServer { let registry = Registry::default().with(filter).with( fmt::layer().pretty().with_span_events(FmtSpan::NONE), //(FULL, NEW, ENTER, EXIT, CLOSE) ); - let dispatch = match tp.clone() { - Some((span, log)) => tracing::Dispatch::new( + match tp.clone() { + Some((span, log)) => { registry .with( tracing_opentelemetry::layer() .with_tracer(span.tracer(tracer_name.clone())), ) - .with(OpenTelemetryTracingBridge::new(&log)), - ), - None => tracing::Dispatch::new(registry), + .with(OpenTelemetryTracingBridge::new(&log)) + .try_init() + .unwrap_or_else(|err| error!("Failed to initialise tracer provider: {err}")); + } + None => { + registry.try_init().ok(); + } }; let work_dir = self.work_dir(); @@ -308,11 +312,7 @@ impl GraphServer { server_termination(signal_receiver, tp), None, ); - let server_result = AbortOnDrop(tokio::spawn(async move { - let _guard = tracing::dispatcher::set_default(&dispatch); - info!("Server tracing dispatch initialised"); - server_task.await - })); + let server_result = AbortOnDrop(tokio::spawn(server_task)); info!("UI listening on 0.0.0.0:{actual_port}, live at: http://localhost:{actual_port}"); debug!( @@ -328,7 +328,7 @@ impl GraphServer { server_result, port: actual_port, }) - } + } async fn generate_endpoint( &self, @@ -477,6 +477,7 @@ async fn server_termination( match tp { None => {} Some((tp, lp)) => { + #[cfg(not(feature="integration-test"))] task::spawn_blocking(move || { let res = tp.shutdown(); if let Err(e) = res { diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index 0dcd1cd9e0..bb0431e9eb 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -4,7 +4,7 @@ use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; use raphtory_graphql::config::{ app_config::AppConfigBuilder, - otlp_config::{TracingLevel, TracingProtocol}, + otlp_config::{TracingLevel, TracingProtocol, GLOBAL_EXPORTERS}, }; use raphtory_graphql::server::{GraphServer, RunningGraphServer}; use std::collections::{HashMap, HashSet}; @@ -39,6 +39,10 @@ async fn setup_for_span_tests( InMemoryLogExporter, TempDir, ) { + let span_exporter = GLOBAL_EXPORTERS.span.clone(); + let log_exporter = GLOBAL_EXPORTERS.log.clone(); + span_exporter.reset(); + log_exporter.reset(); let tmp_dir = tempdir().unwrap(); let graph = Graph::new(); graph.encode(tmp_dir.path().join("g")).unwrap(); @@ -60,8 +64,6 @@ async fn setup_for_span_tests( let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); let client = RaphtoryGraphQLClient::new(endpoint, None); - let span_exporter = app_config.tracing.exporters.span_exporter.clone(); - let log_exporter = app_config.tracing.exporters.log_exporter.clone(); (client, handler, span_exporter, log_exporter, tmp_dir) } @@ -98,6 +100,7 @@ async fn test_open_telemetry_spans_complete() { let emitted_logs = log_exporter.get_emitted_logs().unwrap(); assert!(!emitted_logs.is_empty()); + handler.wait().await.unwrap(); } async fn test_open_telemetry_spans_essential() { @@ -128,6 +131,7 @@ async fn test_open_telemetry_spans_essential() { let emitted_logs = log_exporter.get_emitted_logs().unwrap(); assert!(!emitted_logs.is_empty()); + handler.wait().await.unwrap(); } async fn test_open_telemetry_spans_minimal() { From a5ae2ac310fd92ad3352460ecfd6feb846ad809b Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Tue, 14 Jul 2026 18:35:20 -0500 Subject: [PATCH 17/22] added open telemetry end to end test --- Cargo.lock | 128 ++++++++++++++++--- raphtory-graphql/Cargo.toml | 1 + raphtory-graphql/tests/open_telemetry_ete.rs | 100 +++++++++++++++ 3 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 raphtory-graphql/tests/open_telemetry_ete.rs diff --git a/Cargo.lock b/Cargo.lock index 116c084129..453505974c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5034,6 +5034,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mock-collector" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5f0b06ce724a9050cf5cd5038580123ed1fcfc3d8e3803feccf68bbd66039f" +dependencies = [ + "axum", + "opentelemetry-otlp 0.31.1", + "opentelemetry-proto 0.31.0", + "prost", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tonic", + "tower-http", +] + [[package]] name = "moka" version = "0.12.15" @@ -5395,6 +5414,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "opentelemetry" version = "0.32.0" @@ -5415,12 +5448,24 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" dependencies = [ - "opentelemetry", + "opentelemetry 0.32.0", "tracing", "tracing-core", "tracing-subscriber", ] +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry 0.31.0", +] + [[package]] name = "opentelemetry-http" version = "0.32.0" @@ -5430,10 +5475,25 @@ dependencies = [ "async-trait", "bytes", "http", - "opentelemetry", + "opentelemetry 0.32.0", "reqwest 0.13.4", ] +[[package]] +name = "opentelemetry-otlp" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" +dependencies = [ + "http", + "opentelemetry 0.31.0", + "opentelemetry-http 0.31.0", + "opentelemetry-proto 0.31.0", + "opentelemetry_sdk 0.31.0", + "prost", + "thiserror 2.0.18", +] + [[package]] name = "opentelemetry-otlp" version = "0.32.0" @@ -5441,10 +5501,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", + "opentelemetry 0.32.0", + "opentelemetry-http 0.32.0", + "opentelemetry-proto 0.32.0", + "opentelemetry_sdk 0.32.1", "prost", "reqwest 0.13.4", "serde_json", @@ -5454,6 +5514,23 @@ dependencies = [ "tonic-types", ] +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "base64", + "const-hex", + "opentelemetry 0.31.0", + "opentelemetry_sdk 0.31.0", + "prost", + "serde", + "serde_json", + "tonic", + "tonic-prost", +] + [[package]] name = "opentelemetry-proto" version = "0.32.0" @@ -5462,8 +5539,8 @@ checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "base64", "const-hex", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.1", "prost", "serde", "tonic", @@ -5477,8 +5554,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1b1c6a247d79091f0062a5f4bd058589525cf987a8d4c169440d9c1be72f0ad" dependencies = [ "chrono", - "opentelemetry", - "opentelemetry_sdk", + "opentelemetry 0.32.0", + "opentelemetry_sdk 0.32.1", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry 0.31.0", + "percent-encoding", + "rand 0.9.4", + "thiserror 2.0.18", ] [[package]] @@ -5490,7 +5582,7 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry", + "opentelemetry 0.32.0", "percent-encoding", "portable-atomic", "rand 0.9.4", @@ -6733,12 +6825,13 @@ dependencies = [ "itertools 0.14.0", "jsonwebtoken", "minijinja", + "mock-collector", "once_cell", - "opentelemetry", + "opentelemetry 0.32.0", "opentelemetry-appender-tracing", - "opentelemetry-otlp", + "opentelemetry-otlp 0.32.0", "opentelemetry-stdout", - "opentelemetry_sdk", + "opentelemetry_sdk 0.32.1", "parking_lot", "parquet", "poem", @@ -8562,8 +8655,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", + "axum", "base64", "bytes", + "flate2", + "h2", "http", "http-body", "http-body-util", @@ -8572,6 +8668,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", + "socket2", "sync_wrapper", "tokio", "tokio-rustls", @@ -8580,6 +8677,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", + "zstd", ] [[package]] @@ -8709,7 +8807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", - "opentelemetry", + "opentelemetry 0.32.0", "smallvec", "tracing", "tracing-core", diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index 97f3d0ae7f..6c4b9ab4e8 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -73,6 +73,7 @@ parquet = { workspace = true } # Self-reference lets cargo test compile this crate with the `integration-test` feature enabled. raphtory-graphql = { workspace = true, default-features = false, features = ["integration-test"] } opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio", "testing"] } +mock-collector = "0.2" [features] default = [] diff --git a/raphtory-graphql/tests/open_telemetry_ete.rs b/raphtory-graphql/tests/open_telemetry_ete.rs new file mode 100644 index 0000000000..ad09ea9cac --- /dev/null +++ b/raphtory-graphql/tests/open_telemetry_ete.rs @@ -0,0 +1,100 @@ +use mock_collector::{MockServer, Protocol}; +use raphtory::db::api::storage::storage::Config; +use raphtory::prelude::{Graph, StableEncode}; +use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; +use raphtory_graphql::config::{ + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol}, +}; +use raphtory_graphql::server::{GraphServer}; +use std::collections::{HashMap, HashSet}; +use std::time::Duration; +use tempfile::{tempdir}; +use url::Url; + +#[tokio::test] +async fn test_open_telemetry_http_tracing_server() { + let tracing_server = MockServer::builder() + .protocol(Protocol::HttpBinary) + .start() + .await + .unwrap(); + + let work_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.encode(work_dir.path().join("g")).unwrap(); + + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(TracingLevel::ESSENTIAL) + .with_otlp_agent_host(Some(format!("http://{}", tracing_server.addr()))) + .with_otlp_transport_protocol(TracingProtocol::HTTP) + .build(); + + let server = GraphServer::new(work_dir.path().to_path_buf(), Some(app_config), Config::default()) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + let open_telemetry_query = "query { + updateGraph(path: \"g\") { + addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { + success + node { id } + } + addEdge(time: 5, src: 1, dst: 2, properties: [{ key: \"weight\", value: { f64: 1.5 } }], layer: \"main\") { + success + } + graph { + countNodes + hasNode(name: 1) + } + flush + } +}".to_string(); + let _ = client + .query(&open_telemetry_query, HashMap::new()) + .await + .unwrap(); + + handler.stop().await; + + tracing_server + .wait_for_spans(1, Duration::from_secs(5)) + .await + .unwrap(); + tracing_server + .wait_for_logs(1, Duration::from_secs(5)) + .await + .unwrap(); + + tracing_server + .with_collector(|collector| { + let all_spans: HashSet = collector + .spans() + .iter() + .map(|span| span.span().name.clone()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "execute".to_string(), + "graph".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + assert!(collector.log_count() > 0); + }) + .await; + + tracing_server.shutdown().await.unwrap(); +} + From 7cfc3547f8d0ca9c23525f780a41ccf795080864 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 15 Jul 2026 11:35:17 -0500 Subject: [PATCH 18/22] add comments and semantics to make the code more interpretable and readable --- raphtory-graphql/src/config/otlp_config.rs | 11 ++++++++++- raphtory-graphql/src/server.rs | 4 ++++ raphtory-graphql/tests/open_telemetry.rs | 3 +++ .../{open_telemetry_ete.rs => open_telemetry_e2e.rs} | 0 4 files changed, 17 insertions(+), 1 deletion(-) rename raphtory-graphql/tests/{open_telemetry_ete.rs => open_telemetry_e2e.rs} (100%) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index d638bdda37..ff40821ff2 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -122,6 +122,7 @@ pub const DEFAULT_OTLP_AGENT_PORT_TONIC: u16 = 4317; pub const DEFAULT_OTLP_TRACING_SERVICE_NAME: &'static str = "Raphtory"; #[cfg(feature = "integration-test")] +// in-memory exporters to retrieve spans and logs in tests. #[derive(Clone)] pub struct GlobalExporters { pub span: InMemorySpanExporter, @@ -129,6 +130,14 @@ pub struct GlobalExporters { } #[cfg(feature = "integration-test")] +/* GraphServer registers span and log exporters + across the entire process, which can conflict + when starting up servers with their own exporters. + Making in-memory exporters global allows them to be + initialized once and reused across multiple tests + allowing the tests to retrieve spans and logs + without conflicts. +*/ pub static GLOBAL_EXPORTERS: LazyLock = LazyLock::new(|| GlobalExporters { span: InMemorySpanExporter::default(), log: InMemoryLogExporter::default(), @@ -228,7 +237,7 @@ impl TracingConfig { return Err(err); } } - + let providers = match self.transport_protocol { TracingProtocol::TONIC => { let mut span_builder = SpanExporter::builder() diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 1b291b6bef..d416db6764 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -477,6 +477,10 @@ async fn server_termination( match tp { None => {} Some((tp, lp)) => { + /* avoid shutting down tracing exporters on server shutdown during integration tests + since the tracing exporters will be used for next test because tracing exporters + are global + */ #[cfg(not(feature="integration-test"))] task::spawn_blocking(move || { let res = tp.shutdown(); diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index bb0431e9eb..21a76e574e 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -41,6 +41,7 @@ async fn setup_for_span_tests( ) { let span_exporter = GLOBAL_EXPORTERS.span.clone(); let log_exporter = GLOBAL_EXPORTERS.log.clone(); + // reset logs and spans for next test span_exporter.reset(); log_exporter.reset(); let tmp_dir = tempdir().unwrap(); @@ -161,6 +162,8 @@ async fn test_open_telemetry_spans_minimal() { #[tokio::test] async fn test_open_telemetry_spans() { + // All tests use the same exporters so all tests need to be ran sequentlially to avoid conflicts + // with spans and logs from all tests being mixed together on each test's span/log assertions. test_open_telemetry_spans_complete().await; test_open_telemetry_spans_essential().await; test_open_telemetry_spans_minimal().await; diff --git a/raphtory-graphql/tests/open_telemetry_ete.rs b/raphtory-graphql/tests/open_telemetry_e2e.rs similarity index 100% rename from raphtory-graphql/tests/open_telemetry_ete.rs rename to raphtory-graphql/tests/open_telemetry_e2e.rs From 85ed554a6e1bfddf5a3137003b28cafe750ecb79 Mon Sep 17 00:00:00 2001 From: Daniel Lacina Date: Wed, 15 Jul 2026 11:36:42 -0500 Subject: [PATCH 19/22] format code --- raphtory-graphql/src/config/otlp_config.rs | 25 +- raphtory-graphql/src/server.rs | 17 +- raphtory-graphql/tests/open_telemetry.rs | 255 ++++++++++--------- raphtory-graphql/tests/open_telemetry_e2e.rs | 129 +++++----- 4 files changed, 212 insertions(+), 214 deletions(-) diff --git a/raphtory-graphql/src/config/otlp_config.rs b/raphtory-graphql/src/config/otlp_config.rs index ff40821ff2..0b4b49e7c5 100644 --- a/raphtory-graphql/src/config/otlp_config.rs +++ b/raphtory-graphql/src/config/otlp_config.rs @@ -18,13 +18,7 @@ use raphtory_api::core::storage::arc_str::OptionAsStr; use reqwest::{blocking::ClientBuilder, Certificate}; use serde::Deserialize; use std::{ - collections::HashMap, - env, - fs::File, - io::Read, - path::PathBuf, - sync::LazyLock, - time::Duration, + collections::HashMap, env, fs::File, io::Read, path::PathBuf, sync::LazyLock, time::Duration, }; use strum::IntoEnumIterator; use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; @@ -130,14 +124,14 @@ pub struct GlobalExporters { } #[cfg(feature = "integration-test")] -/* GraphServer registers span and log exporters - across the entire process, which can conflict - when starting up servers with their own exporters. - Making in-memory exporters global allows them to be +/* GraphServer registers span and log exporters + across the entire process, which can conflict + when starting up servers with their own exporters. + Making in-memory exporters global allows them to be initialized once and reused across multiple tests allowing the tests to retrieve spans and logs without conflicts. -*/ +*/ pub static GLOBAL_EXPORTERS: LazyLock = LazyLock::new(|| GlobalExporters { span: InMemorySpanExporter::default(), log: InMemoryLogExporter::default(), @@ -237,7 +231,7 @@ impl TracingConfig { return Err(err); } } - + let providers = match self.transport_protocol { TracingProtocol::TONIC => { let mut span_builder = SpanExporter::builder() @@ -352,8 +346,3 @@ impl TracingConfig { } } } - - - - - diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index d416db6764..d97621dae0 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -56,7 +56,10 @@ use tokio::{ }; use tracing::{debug, error, info, warn}; use tracing_subscriber::{ - Registry, fmt::{self, format::FmtSpan}, layer::SubscriberExt, util::SubscriberInitExt, + fmt::{self, format::FmtSpan}, + layer::SubscriberExt, + util::SubscriberInitExt, + Registry, }; use url::ParseError; @@ -328,7 +331,7 @@ impl GraphServer { server_result, port: actual_port, }) - } + } async fn generate_endpoint( &self, @@ -478,10 +481,10 @@ async fn server_termination( None => {} Some((tp, lp)) => { /* avoid shutting down tracing exporters on server shutdown during integration tests - since the tracing exporters will be used for next test because tracing exporters + since the tracing exporters will be used for next test because tracing exporters are global */ - #[cfg(not(feature="integration-test"))] + #[cfg(not(feature = "integration-test"))] task::spawn_blocking(move || { let res = tp.shutdown(); if let Err(e) = res { @@ -500,10 +503,7 @@ async fn server_termination( #[cfg(test)] mod server_tests { - use crate::{ - config::app_config::AppConfigBuilder, - server::GraphServer, - }; + use crate::{config::app_config::AppConfigBuilder, server::GraphServer}; use chrono::prelude::*; #[cfg(feature = "vectors")] use raphtory::vectors::{storage::OpenAIEmbeddings, template::DocumentTemplate}; @@ -582,5 +582,4 @@ mod server_tests { sleep(Duration::from_secs(5)).await; handler.await.unwrap().stop().await } - } diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index 21a76e574e..997b16d13e 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -3,8 +3,8 @@ use raphtory::db::api::storage::storage::Config; use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; use raphtory_graphql::config::{ - app_config::AppConfigBuilder, - otlp_config::{TracingLevel, TracingProtocol, GLOBAL_EXPORTERS}, + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol, GLOBAL_EXPORTERS}, }; use raphtory_graphql::server::{GraphServer, RunningGraphServer}; use std::collections::{HashMap, HashSet}; @@ -31,142 +31,149 @@ const OPEN_TELEMETRY_QUERY: &str = "query { }"; async fn setup_for_span_tests( - tracing_level: TracingLevel, + tracing_level: TracingLevel, ) -> ( - RaphtoryGraphQLClient, - RunningGraphServer, - InMemorySpanExporter, - InMemoryLogExporter, - TempDir, + RaphtoryGraphQLClient, + RunningGraphServer, + InMemorySpanExporter, + InMemoryLogExporter, + TempDir, ) { let span_exporter = GLOBAL_EXPORTERS.span.clone(); - let log_exporter = GLOBAL_EXPORTERS.log.clone(); - // reset logs and spans for next test - span_exporter.reset(); - log_exporter.reset(); - let tmp_dir = tempdir().unwrap(); - let graph = Graph::new(); - graph.encode(tmp_dir.path().join("g")).unwrap(); - - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(tracing_level) - .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) - .build(); - - let server = GraphServer::new( - tmp_dir.path().to_path_buf(), - Some(app_config.clone()), - Config::default(), - ) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); - - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); - (client, handler, span_exporter, log_exporter, tmp_dir) + let log_exporter = GLOBAL_EXPORTERS.log.clone(); + // reset logs and spans for next test + span_exporter.reset(); + log_exporter.reset(); + let tmp_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.encode(tmp_dir.path().join("g")).unwrap(); + + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(tracing_level) + .with_otlp_transport_protocol(TracingProtocol::IN_MEMORY) + .build(); + + let server = GraphServer::new( + tmp_dir.path().to_path_buf(), + Some(app_config.clone()), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); + + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); + (client, handler, span_exporter, log_exporter, tmp_dir) } async fn test_open_telemetry_spans_complete() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::COMPLETE).await; - let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans - .iter() - .map(|span| span.name.to_string()) - .collect(); - assert_eq!( - all_spans, - HashSet::from([ - "addEdge".to_string(), - "addNode".to_string(), - "countNodes".to_string(), - "execute".to_string(), - "flush".to_string(), - "graph".to_string(), - "hasNode".to_string(), - "id".to_string(), - "node".to_string(), - "parse".to_string(), - "request".to_string(), - "success".to_string(), - "updateGraph".to_string(), - "validation".to_string(), - ]) - ); - - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); - handler.wait().await.unwrap(); + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::COMPLETE).await; + let _ = client + .query(OPEN_TELEMETRY_QUERY, HashMap::new()) + .await + .unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "countNodes".to_string(), + "execute".to_string(), + "flush".to_string(), + "graph".to_string(), + "hasNode".to_string(), + "id".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "success".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); + handler.wait().await.unwrap(); } async fn test_open_telemetry_spans_essential() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::ESSENTIAL).await; - let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans - .iter() - .map(|span| span.name.to_string()) - .collect(); - assert_eq!( - all_spans, - HashSet::from([ - "addEdge".to_string(), - "addNode".to_string(), - "execute".to_string(), - "graph".to_string(), - "node".to_string(), - "parse".to_string(), - "request".to_string(), - "updateGraph".to_string(), - "validation".to_string(), - ]) - ); - - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); - handler.wait().await.unwrap(); + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::ESSENTIAL).await; + let _ = client + .query(OPEN_TELEMETRY_QUERY, HashMap::new()) + .await + .unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "execute".to_string(), + "graph".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); + handler.wait().await.unwrap(); } async fn test_open_telemetry_spans_minimal() { - let (client, handler, span_exporter, log_exporter, _tmp_dir) = - setup_for_span_tests(TracingLevel::MINIMAL).await; - let _ = client.query(OPEN_TELEMETRY_QUERY, HashMap::new()).await.unwrap(); - handler.stop().await; - - let finished_spans = span_exporter.get_finished_spans().unwrap(); - let all_spans: HashSet = finished_spans - .iter() - .map(|span| span.name.to_string()) - .collect(); - assert_eq!( - all_spans, - HashSet::from([ - "execute".to_string(), - "parse".to_string(), - "request".to_string(), - "validation".to_string(), - ]) - ); - - let emitted_logs = log_exporter.get_emitted_logs().unwrap(); - assert!(!emitted_logs.is_empty()); + let (client, handler, span_exporter, log_exporter, _tmp_dir) = + setup_for_span_tests(TracingLevel::MINIMAL).await; + let _ = client + .query(OPEN_TELEMETRY_QUERY, HashMap::new()) + .await + .unwrap(); + handler.stop().await; + + let finished_spans = span_exporter.get_finished_spans().unwrap(); + let all_spans: HashSet = finished_spans + .iter() + .map(|span| span.name.to_string()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "execute".to_string(), + "parse".to_string(), + "request".to_string(), + "validation".to_string(), + ]) + ); + + let emitted_logs = log_exporter.get_emitted_logs().unwrap(); + assert!(!emitted_logs.is_empty()); } #[tokio::test] async fn test_open_telemetry_spans() { - // All tests use the same exporters so all tests need to be ran sequentlially to avoid conflicts - // with spans and logs from all tests being mixed together on each test's span/log assertions. - test_open_telemetry_spans_complete().await; - test_open_telemetry_spans_essential().await; - test_open_telemetry_spans_minimal().await; + // All tests use the same exporters so all tests need to be ran sequentlially to avoid conflicts + // with spans and logs from all tests being mixed together on each test's span/log assertions. + test_open_telemetry_spans_complete().await; + test_open_telemetry_spans_essential().await; + test_open_telemetry_spans_minimal().await; } - - diff --git a/raphtory-graphql/tests/open_telemetry_e2e.rs b/raphtory-graphql/tests/open_telemetry_e2e.rs index ad09ea9cac..d290f1bc11 100644 --- a/raphtory-graphql/tests/open_telemetry_e2e.rs +++ b/raphtory-graphql/tests/open_telemetry_e2e.rs @@ -3,41 +3,45 @@ use raphtory::db::api::storage::storage::Config; use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; use raphtory_graphql::config::{ - app_config::AppConfigBuilder, - otlp_config::{TracingLevel, TracingProtocol}, + app_config::AppConfigBuilder, + otlp_config::{TracingLevel, TracingProtocol}, }; -use raphtory_graphql::server::{GraphServer}; +use raphtory_graphql::server::GraphServer; use std::collections::{HashMap, HashSet}; use std::time::Duration; -use tempfile::{tempdir}; +use tempfile::tempdir; use url::Url; #[tokio::test] async fn test_open_telemetry_http_tracing_server() { let tracing_server = MockServer::builder() - .protocol(Protocol::HttpBinary) - .start() - .await - .unwrap(); + .protocol(Protocol::HttpBinary) + .start() + .await + .unwrap(); - let work_dir = tempdir().unwrap(); - let graph = Graph::new(); - graph.encode(work_dir.path().join("g")).unwrap(); + let work_dir = tempdir().unwrap(); + let graph = Graph::new(); + graph.encode(work_dir.path().join("g")).unwrap(); - let app_config = AppConfigBuilder::new() - .with_tracing(true) - .with_tracing_level(TracingLevel::ESSENTIAL) - .with_otlp_agent_host(Some(format!("http://{}", tracing_server.addr()))) - .with_otlp_transport_protocol(TracingProtocol::HTTP) - .build(); + let app_config = AppConfigBuilder::new() + .with_tracing(true) + .with_tracing_level(TracingLevel::ESSENTIAL) + .with_otlp_agent_host(Some(format!("http://{}", tracing_server.addr()))) + .with_otlp_transport_protocol(TracingProtocol::HTTP) + .build(); - let server = GraphServer::new(work_dir.path().to_path_buf(), Some(app_config), Config::default()) - .await - .unwrap(); - let handler = server.start_with_port(0).await.unwrap(); + let server = GraphServer::new( + work_dir.path().to_path_buf(), + Some(app_config), + Config::default(), + ) + .await + .unwrap(); + let handler = server.start_with_port(0).await.unwrap(); - let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); - let client = RaphtoryGraphQLClient::new(endpoint, None); + let endpoint = Url::parse(&format!("http://localhost:{}/", handler.port())).unwrap(); + let client = RaphtoryGraphQLClient::new(endpoint, None); let open_telemetry_query = "query { updateGraph(path: \"g\") { addNode(time: 1, name: 1, properties: [{ key: \"seed\", value: { str: \"yes\" } }], nodeType: \"seed\", layer: \"main\") { @@ -53,48 +57,47 @@ async fn test_open_telemetry_http_tracing_server() { } flush } -}".to_string(); - let _ = client - .query(&open_telemetry_query, HashMap::new()) - .await - .unwrap(); +}".to_string(); + let _ = client + .query(&open_telemetry_query, HashMap::new()) + .await + .unwrap(); - handler.stop().await; + handler.stop().await; - tracing_server - .wait_for_spans(1, Duration::from_secs(5)) - .await - .unwrap(); - tracing_server - .wait_for_logs(1, Duration::from_secs(5)) - .await - .unwrap(); + tracing_server + .wait_for_spans(1, Duration::from_secs(5)) + .await + .unwrap(); + tracing_server + .wait_for_logs(1, Duration::from_secs(5)) + .await + .unwrap(); - tracing_server - .with_collector(|collector| { - let all_spans: HashSet = collector - .spans() - .iter() - .map(|span| span.span().name.clone()) - .collect(); - assert_eq!( - all_spans, - HashSet::from([ - "addEdge".to_string(), - "addNode".to_string(), - "execute".to_string(), - "graph".to_string(), - "node".to_string(), - "parse".to_string(), - "request".to_string(), - "updateGraph".to_string(), - "validation".to_string(), - ]) - ); - assert!(collector.log_count() > 0); - }) - .await; + tracing_server + .with_collector(|collector| { + let all_spans: HashSet = collector + .spans() + .iter() + .map(|span| span.span().name.clone()) + .collect(); + assert_eq!( + all_spans, + HashSet::from([ + "addEdge".to_string(), + "addNode".to_string(), + "execute".to_string(), + "graph".to_string(), + "node".to_string(), + "parse".to_string(), + "request".to_string(), + "updateGraph".to_string(), + "validation".to_string(), + ]) + ); + assert!(collector.log_count() > 0); + }) + .await; - tracing_server.shutdown().await.unwrap(); + tracing_server.shutdown().await.unwrap(); } - From 23f8e50d98c47ef11bfb1abc1a024507d75577f1 Mon Sep 17 00:00:00 2001 From: Daniel Lacina <127566471+DanielLacina@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:29:18 -0500 Subject: [PATCH 20/22] Update raphtory-graphql/tests/open_telemetry.rs Co-authored-by: Fadhil Abubaker --- raphtory-graphql/tests/open_telemetry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index 997b16d13e..09ef52f093 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -1,4 +1,4 @@ -// tests must be separated into their own binary to avoid conflicts with initializing the tracing subscriber multiple times in the same process +// OpenTelemetry Tests must be separated into their own binary to prevent polluting other tests since the span and log exporters are set globally. use raphtory::db::api::storage::storage::Config; use raphtory::prelude::{Graph, StableEncode}; use raphtory_graphql::client::raphtory_client::RaphtoryGraphQLClient; From e21accbffe8a5d45fb0e1f05c79e9228eeb7f330 Mon Sep 17 00:00:00 2001 From: Daniel Lacina <127566471+DanielLacina@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:29:36 -0500 Subject: [PATCH 21/22] Update raphtory-graphql/tests/open_telemetry.rs Co-authored-by: Fadhil Abubaker --- raphtory-graphql/tests/open_telemetry.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/raphtory-graphql/tests/open_telemetry.rs b/raphtory-graphql/tests/open_telemetry.rs index 09ef52f093..8395a8d111 100644 --- a/raphtory-graphql/tests/open_telemetry.rs +++ b/raphtory-graphql/tests/open_telemetry.rs @@ -171,8 +171,7 @@ async fn test_open_telemetry_spans_minimal() { #[tokio::test] async fn test_open_telemetry_spans() { - // All tests use the same exporters so all tests need to be ran sequentlially to avoid conflicts - // with spans and logs from all tests being mixed together on each test's span/log assertions. + // The following tests share the same global in-memory exporters and hence need to be run sequentially to prevent spans and logs from getting mangled. test_open_telemetry_spans_complete().await; test_open_telemetry_spans_essential().await; test_open_telemetry_spans_minimal().await; From 139512905c0e5e3b87306757edc52db5da86e120 Mon Sep 17 00:00:00 2001 From: Daniel Lacina <127566471+DanielLacina@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:30:20 -0500 Subject: [PATCH 22/22] Update raphtory-graphql/src/server.rs Co-authored-by: Fadhil Abubaker --- raphtory-graphql/src/server.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index d97621dae0..19e7893169 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -480,9 +480,8 @@ async fn server_termination( match tp { None => {} Some((tp, lp)) => { - /* avoid shutting down tracing exporters on server shutdown during integration tests - since the tracing exporters will be used for next test because tracing exporters - are global + /* Avoid shutting down global tracing exporters on server shutdown during integration tests + since they are reused across multiple tests. */ #[cfg(not(feature = "integration-test"))] task::spawn_blocking(move || {