From 7ef4ff328d8eadc39acfd047399f552e3adb1f36 Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Tue, 17 Mar 2026 15:33:34 -0400 Subject: [PATCH 1/3] add update span --- src/dataset.rs | 1 + src/experiments/experiment.rs | 11 ++- src/log_queue/queue.rs | 164 +++++++++++++++++++++++++++++----- src/logger.rs | 95 +++++++++++++++++++- src/span.rs | 34 ++++++- src/types.rs | 3 + tests/span_lifecycle.rs | 142 ++++++++++++++++++++++++++++- 7 files changed, 421 insertions(+), 29 deletions(-) diff --git a/src/dataset.rs b/src/dataset.rs index 2a9ff05..57c37c4 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -712,6 +712,7 @@ impl, + span_components: &crate::span_components::SpanComponents, + ) -> std::result::Result { + if let Some(object_id) = span_components.object_id.as_ref() { + return Ok(match span_components.object_type { + SpanObjectType::Experiment => LogDestination::experiment(object_id.clone()), + SpanObjectType::ProjectLogs => LogDestination::project_logs(object_id.clone()), + SpanObjectType::PlaygroundLogs => { + LogDestination::playground_logs(object_id.clone()) + } + }); + } + + match span_components.object_type { + SpanObjectType::ProjectLogs => { + let args = span_components + .compute_object_metadata_args + .as_ref() + .ok_or_else(|| anyhow::anyhow!("missing compute_object_metadata_args"))?; + if let Some(project_id) = args.get("project_id").and_then(Value::as_str) { + return Ok(LogDestination::project_logs(project_id.to_string())); + } + let project_name = args + .get("project_name") + .and_then(Value::as_str) + .ok_or_else(|| anyhow::anyhow!("missing project_name"))?; + let project_id = self + .ensure_project_id(token, org_id, org_name, project_name) + .await?; + Ok(LogDestination::project_logs(project_id)) + } + SpanObjectType::Experiment => anyhow::bail!("experiment span is missing object_id"), + SpanObjectType::PlaygroundLogs => { + anyhow::bail!("playground span is missing object_id") + } + } + } + /// Ensure a project ID is available for the given project name, registering /// it via the API if it is not yet cached. /// @@ -606,42 +692,73 @@ impl LogQueueCore { parent_info, } = cmd; + let span_components = payload.span_components.clone(); + // Build a best-effort destination from parent_info (project registration // hasn't happened yet, so project-name-based destinations are unknown). let span_id = payload.span_id.clone(); - let destination = match parent_info.as_ref() { - Some(ParentSpanInfo::Experiment { object_id }) => { - LogDestination::experiment(object_id.clone()) - } - Some(ParentSpanInfo::ProjectLogs { object_id }) => { - LogDestination::project_logs(object_id.clone()) - } - Some(ParentSpanInfo::PlaygroundLogs { object_id }) => { - LogDestination::playground_logs(object_id.clone()) - } - Some(ParentSpanInfo::Dataset { object_id }) => { - LogDestination::dataset(object_id.clone()) + let destination = if let Some(span_components) = span_components.as_ref() { + if let Some(object_id) = span_components.object_id.as_ref() { + match span_components.object_type { + SpanObjectType::Experiment => LogDestination::experiment(object_id.clone()), + SpanObjectType::ProjectLogs => { + LogDestination::project_logs(object_id.clone()) + } + SpanObjectType::PlaygroundLogs => { + LogDestination::playground_logs(object_id.clone()) + } + } + } else if let Some(project_id) = span_components + .compute_object_metadata_args + .as_ref() + .and_then(|args| args.get("project_id")) + .and_then(Value::as_str) + { + LogDestination::project_logs(project_id.to_string()) + } else { + LogDestination::project_logs("unknown".to_string()) } - Some(ParentSpanInfo::FullSpan { - object_type, - object_id, - .. - }) => match object_type { - SpanObjectType::Experiment => LogDestination::experiment(object_id.clone()), - SpanObjectType::ProjectLogs => LogDestination::project_logs(object_id.clone()), - SpanObjectType::PlaygroundLogs => { + } else { + match parent_info.as_ref() { + Some(ParentSpanInfo::Experiment { object_id }) => { + LogDestination::experiment(object_id.clone()) + } + Some(ParentSpanInfo::ProjectLogs { object_id }) => { + LogDestination::project_logs(object_id.clone()) + } + Some(ParentSpanInfo::PlaygroundLogs { object_id }) => { LogDestination::playground_logs(object_id.clone()) } - }, - _ => LogDestination::project_logs("unknown".to_string()), + Some(ParentSpanInfo::Dataset { object_id }) => { + LogDestination::dataset(object_id.clone()) + } + Some(ParentSpanInfo::FullSpan { + object_type, + object_id, + .. + }) => match object_type { + SpanObjectType::Experiment => LogDestination::experiment(object_id.clone()), + SpanObjectType::ProjectLogs => { + LogDestination::project_logs(object_id.clone()) + } + SpanObjectType::PlaygroundLogs => { + LogDestination::playground_logs(object_id.clone()) + } + }, + _ => LogDestination::project_logs("unknown".to_string()), + } }; + let root_span_id = span_components + .as_ref() + .and_then(|components| components.root_span_id.clone()) + .unwrap_or_else(|| span_id.clone()); let row = Logs3Row { id: payload.row_id, span_id: span_id.clone(), is_merge: if payload.is_merge { Some(true) } else { None }, merge_paths: None, - root_span_id: span_id, + root_span_id, span_parents: None, destination, org_id: payload.org_id, @@ -826,6 +943,7 @@ mod tests { row_id: id.to_string(), span_id: id.to_string(), is_merge: false, + span_components: None, org_id: "org".to_string(), org_name: Some("test-org".to_string()), project_name: None, diff --git a/src/logger.rs b/src/logger.rs index 0491256..3802cf2 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -20,7 +20,8 @@ use crate::experiments::api::{ use crate::experiments::{BaseExperimentInfo, ExperimentBuilder}; use crate::log_queue::{LogQueue, LogQueueConfig}; use crate::span::SpanSubmitter; -use crate::types::{ParentSpanInfo, SpanPayload}; +use crate::span_components::SpanComponents; +use crate::types::{ParentSpanInfo, SpanAttributes, SpanObjectType, SpanPayload}; // 30s covers both quick API calls (login, project registration) and slower batch log uploads. // The TypeScript SDK applies no explicit timeout. @@ -527,6 +528,98 @@ impl BraintrustClient { crate::span::SpanBuilder::new(submitter, token, org_id) } + /// Update an existing span using the output of `SpanHandle::export()`. + /// + /// Validation happens before queuing. The actual upload remains asynchronous. + pub async fn update_span(&self, exported: &str, event: crate::span::SpanLog) -> Result<()> { + let components = SpanComponents::parse(exported)?; + let row_id = components.row_id.clone().ok_or_else(|| { + BraintrustError::InvalidConfig("Exported span must have a row_id".into()) + })?; + + if components.root_span_id.is_some() != components.span_id.is_some() { + return Err(BraintrustError::InvalidConfig( + "both root_span_id and span_id must be set, or neither".into(), + )); + } + + if components.object_id.is_none() { + match components.object_type { + SpanObjectType::ProjectLogs => { + let args = components.compute_object_metadata_args.as_ref().ok_or_else(|| { + BraintrustError::InvalidConfig( + "Exported project-log span must include object_id or compute_object_metadata_args".into(), + ) + })?; + let has_project_id = args.get("project_id").and_then(Value::as_str).is_some(); + let has_project_name = + args.get("project_name").and_then(Value::as_str).is_some(); + if !has_project_id && !has_project_name { + return Err(BraintrustError::InvalidConfig( + "project-log compute_object_metadata_args must include project_id or project_name".into(), + )); + } + } + SpanObjectType::Experiment => { + return Err(BraintrustError::InvalidConfig( + "Exported experiment span must include object_id".into(), + )); + } + SpanObjectType::PlaygroundLogs => { + return Err(BraintrustError::InvalidConfig( + "Exported playground span must include object_id".into(), + )); + } + } + } + + let login_state = if self.inner.login_state.is_logged_in() { + self.inner.login_state.clone() + } else if self.inner.login_skipped { + return Err(BraintrustError::InvalidConfig( + "update_span() requires credentials; call span_builder_with_credentials() or experiment_builder_with_credentials() first when skip_login is enabled".into(), + )); + } else { + self.wait_for_login_state().await? + }; + + let api_key = login_state + .api_key() + .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; + let org_id = login_state + .org_id() + .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; + let span_id = components.span_id.clone().unwrap_or_else(|| row_id.clone()); + + let payload = SpanPayload { + row_id, + span_id, + is_merge: true, + span_components: Some(components), + org_id, + org_name: login_state.org_name(), + project_name: None, + input: event.input, + output: event.output, + expected: event.expected, + error: event.error, + scores: event.scores, + metadata: event.metadata, + metrics: event.metrics, + tags: event.tags, + context: event.context, + span_attributes: event.name.map(|name| SpanAttributes { + name: Some(name), + span_type: None, + purpose: None, + extra: HashMap::new(), + }), + }; + + self.submit_payload(api_key, payload, None); + Ok(()) + } + /// Perform login synchronously. async fn perform_login(&self, api_key: &str, org_name: Option<&str>) -> Result<()> { let login_url = self diff --git a/src/span.rs b/src/span.rs index 6239f11..440c6b4 100644 --- a/src/span.rs +++ b/src/span.rs @@ -509,10 +509,23 @@ impl SpanHandle { _ => Some(inner.span_id.clone()), }; + let compute_object_metadata_args = if object_id.is_none() { + inner.project_name.as_ref().map(|project_name| { + let mut args = Map::new(); + args.insert( + "project_name".to_string(), + Value::String(project_name.clone()), + ); + args + }) + } else { + None + }; + Ok(SpanComponents { object_type, object_id, - compute_object_metadata_args: None, + compute_object_metadata_args, row_id: Some(inner.row_id.clone()), span_id: Some(inner.span_id.clone()), root_span_id, @@ -571,6 +584,7 @@ impl From for SpanPayload { row_id: data.row_id, span_id: data.span_id, is_merge: data.has_flushed, // First flush = false (replace), subsequent = true (merge) + span_components: None, org_id: data.org_id, org_name: data.org_name, project_name: data.project_name, @@ -919,4 +933,22 @@ mod tests { Some("test_value") ); } + + #[tokio::test] + async fn span_export_includes_compute_object_metadata_args_for_project_logs() { + let (builder, _collector) = mock_span_builder(); + let span = builder.project_name("demo-project").build(); + + let exported = span.export().await.unwrap(); + + assert_eq!(exported.object_type, SpanObjectType::ProjectLogs); + assert!(exported.object_id.is_none()); + let args = exported + .compute_object_metadata_args + .expect("compute args should be exported"); + assert_eq!( + args.get("project_name").and_then(|value| value.as_str()), + Some("demo-project") + ); + } } diff --git a/src/types.rs b/src/types.rs index a17bf90..d9cb829 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,8 @@ use serde_json::{Map, Value}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::fmt; +use crate::span_components::SpanComponents; + pub const LOGS_API_VERSION: u8 = 2; /// The type of span object, serialized as its integer representation for wire compatibility. @@ -320,6 +322,7 @@ pub(crate) struct SpanPayload { pub row_id: String, pub span_id: String, pub is_merge: bool, + pub span_components: Option, pub org_id: String, pub org_name: Option, pub project_name: Option, diff --git a/tests/span_lifecycle.rs b/tests/span_lifecycle.rs index 0c476b5..6e65704 100644 --- a/tests/span_lifecycle.rs +++ b/tests/span_lifecycle.rs @@ -1,7 +1,8 @@ use braintrust_sdk_rust::{ - extract_anthropic_usage, extract_openai_usage, BraintrustClient, SpanLog, + extract_anthropic_usage, extract_openai_usage, BraintrustClient, SpanComponents, SpanLog, + SpanObjectType, }; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -68,6 +69,143 @@ async fn span_lifecycle_flushes_to_logs_endpoint() { assert_eq!(row["project_id"], "proj-id"); } +#[tokio::test] +async fn client_update_span_uses_exported_ids_for_project_logs() { + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/logs3")) + .respond_with(ResponseTemplate::new(200).set_body_string("{}")) + .mount(&server) + .await; + + let client = BraintrustClient::builder() + .skip_login(true) + .api_url(server.uri()) + .app_url(server.uri()) + .build() + .await + .expect("client"); + let _ = client.span_builder_with_credentials("token", "org-id"); + + let exported = SpanComponents { + object_type: SpanObjectType::ProjectLogs, + object_id: Some("proj-id".to_string()), + compute_object_metadata_args: None, + row_id: Some("row-id".to_string()), + span_id: Some("span-id".to_string()), + root_span_id: Some("root-id".to_string()), + propagated_event: None, + } + .to_str(); + + client + .update_span( + &exported, + SpanLog::builder() + .output(json!({"status": "updated"})) + .build() + .expect("build"), + ) + .await + .expect("update"); + client.flush().await.expect("flush"); + + let logs_requests: Vec<_> = server + .received_requests() + .await + .unwrap() + .into_iter() + .filter(|request| request.url.path() == "/logs3") + .collect(); + assert_eq!(logs_requests.len(), 1); + + let body: Value = serde_json::from_slice(&logs_requests[0].body).expect("json body"); + let row = body["rows"] + .as_array() + .and_then(|rows| rows.first()) + .expect("row"); + assert_eq!(row["id"], "row-id"); + assert_eq!(row["project_id"], "proj-id"); + assert_eq!(row["span_id"], "span-id"); + assert_eq!(row["root_span_id"], "root-id"); + assert_eq!(row["_is_merge"], true); + assert!(row.get("span_parents").is_none()); +} + +#[tokio::test] +async fn client_update_span_resolves_project_name_from_exported_compute_metadata_args() { + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/api/project/register")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "project": { "id": "proj-id" } + }))) + .expect(1) + .mount(&server) + .await; + + Mock::given(method("POST")) + .and(path("/logs3")) + .respond_with(ResponseTemplate::new(200).set_body_string("{}")) + .mount(&server) + .await; + + let client = BraintrustClient::builder() + .skip_login(true) + .api_url(server.uri()) + .app_url(server.uri()) + .build() + .await + .expect("client"); + let _ = client.span_builder_with_credentials("token", "org-id"); + + let mut compute_object_metadata_args = Map::new(); + compute_object_metadata_args.insert("project_name".to_string(), json!("demo-project")); + + let exported = SpanComponents { + object_type: SpanObjectType::ProjectLogs, + object_id: None, + compute_object_metadata_args: Some(compute_object_metadata_args), + row_id: Some("row-id".to_string()), + span_id: Some("span-id".to_string()), + root_span_id: Some("root-id".to_string()), + propagated_event: None, + } + .to_str(); + + client + .update_span( + &exported, + SpanLog::builder() + .output(json!({"status": "updated"})) + .build() + .expect("build"), + ) + .await + .expect("update"); + client.flush().await.expect("flush"); + + let logs_requests: Vec<_> = server + .received_requests() + .await + .unwrap() + .into_iter() + .filter(|request| request.url.path() == "/logs3") + .collect(); + assert_eq!(logs_requests.len(), 1); + + let body: Value = serde_json::from_slice(&logs_requests[0].body).expect("json body"); + let row = body["rows"] + .as_array() + .and_then(|rows| rows.first()) + .expect("row"); + assert_eq!(row["project_id"], "proj-id"); + assert_eq!(row["span_id"], "span-id"); + assert_eq!(row["root_span_id"], "root-id"); +} + #[test] fn usage_extractors_return_expected_metrics() { let openai_usage = extract_openai_usage(&json!({ From 96626a6e5687e39dc108dcc83ffc23e047a8d746 Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Tue, 17 Mar 2026 15:49:56 -0400 Subject: [PATCH 2/3] add update span with credentials --- src/logger.rs | 71 ++++++++++++++++++++++++++++++----------- tests/span_lifecycle.rs | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/src/logger.rs b/src/logger.rs index 3802cf2..52dfb29 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -532,6 +532,57 @@ impl BraintrustClient { /// /// Validation happens before queuing. The actual upload remains asynchronous. pub async fn update_span(&self, exported: &str, event: crate::span::SpanLog) -> Result<()> { + let login_state = if self.inner.login_state.is_logged_in() { + self.inner.login_state.clone() + } else if self.inner.login_skipped { + return Err(BraintrustError::InvalidConfig( + "update_span() requires credentials; call span_builder_with_credentials() or experiment_builder_with_credentials() first when skip_login is enabled".into(), + )); + } else { + self.wait_for_login_state().await? + }; + + let api_key = login_state + .api_key() + .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; + let org_id = login_state + .org_id() + .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; + self.update_span_internal(api_key, org_id, login_state.org_name(), exported, event) + } + + /// Update an existing span using explicit credentials instead of shared login state. + /// + /// This is the safe entrypoint for multi-tenant `skip_login` clients. + pub async fn update_span_with_credentials( + &self, + token: impl Into, + org_id: impl Into, + exported: &str, + event: crate::span::SpanLog, + ) -> Result<()> { + let token = token.into(); + let org_id = org_id.into(); + + let _ = self.inner.login_state.set( + token.clone(), + org_id.clone(), + String::new(), + self.inner.api_url.to_string(), + self.inner.app_url.to_string(), + ); + + self.update_span_internal(token, org_id, None, exported, event) + } + + fn update_span_internal( + &self, + token: String, + org_id: String, + org_name: Option, + exported: &str, + event: crate::span::SpanLog, + ) -> Result<()> { let components = SpanComponents::parse(exported)?; let row_id = components.row_id.clone().ok_or_else(|| { BraintrustError::InvalidConfig("Exported span must have a row_id".into()) @@ -573,22 +624,6 @@ impl BraintrustClient { } } - let login_state = if self.inner.login_state.is_logged_in() { - self.inner.login_state.clone() - } else if self.inner.login_skipped { - return Err(BraintrustError::InvalidConfig( - "update_span() requires credentials; call span_builder_with_credentials() or experiment_builder_with_credentials() first when skip_login is enabled".into(), - )); - } else { - self.wait_for_login_state().await? - }; - - let api_key = login_state - .api_key() - .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; - let org_id = login_state - .org_id() - .ok_or_else(|| BraintrustError::InvalidConfig("Not logged in".into()))?; let span_id = components.span_id.clone().unwrap_or_else(|| row_id.clone()); let payload = SpanPayload { @@ -597,7 +632,7 @@ impl BraintrustClient { is_merge: true, span_components: Some(components), org_id, - org_name: login_state.org_name(), + org_name, project_name: None, input: event.input, output: event.output, @@ -616,7 +651,7 @@ impl BraintrustClient { }), }; - self.submit_payload(api_key, payload, None); + self.submit_payload(token, payload, None); Ok(()) } diff --git a/tests/span_lifecycle.rs b/tests/span_lifecycle.rs index 6e65704..2840cfc 100644 --- a/tests/span_lifecycle.rs +++ b/tests/span_lifecycle.rs @@ -133,6 +133,69 @@ async fn client_update_span_uses_exported_ids_for_project_logs() { assert!(row.get("span_parents").is_none()); } +#[tokio::test] +async fn client_update_span_with_credentials_works_without_priming_login_state() { + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/logs3")) + .respond_with(ResponseTemplate::new(200).set_body_string("{}")) + .mount(&server) + .await; + + let client = BraintrustClient::builder() + .skip_login(true) + .api_url(server.uri()) + .app_url(server.uri()) + .build() + .await + .expect("client"); + + let exported = SpanComponents { + object_type: SpanObjectType::ProjectLogs, + object_id: Some("proj-id".to_string()), + compute_object_metadata_args: None, + row_id: Some("row-id".to_string()), + span_id: Some("span-id".to_string()), + root_span_id: Some("root-id".to_string()), + propagated_event: None, + } + .to_str(); + + client + .update_span_with_credentials( + "token", + "org-id", + &exported, + SpanLog::builder() + .output(json!({"status": "updated"})) + .build() + .expect("build"), + ) + .await + .expect("update"); + client.flush().await.expect("flush"); + + let logs_requests: Vec<_> = server + .received_requests() + .await + .unwrap() + .into_iter() + .filter(|request| request.url.path() == "/logs3") + .collect(); + assert_eq!(logs_requests.len(), 1); + + let body: Value = serde_json::from_slice(&logs_requests[0].body).expect("json body"); + let row = body["rows"] + .as_array() + .and_then(|rows| rows.first()) + .expect("row"); + assert_eq!(row["id"], "row-id"); + assert_eq!(row["project_id"], "proj-id"); + assert_eq!(row["span_id"], "span-id"); + assert_eq!(row["root_span_id"], "root-id"); +} + #[tokio::test] async fn client_update_span_resolves_project_name_from_exported_compute_metadata_args() { let server = MockServer::start().await; From 0ff9e028249fca1ac7c561daa1633a266efdb2e4 Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Wed, 18 Mar 2026 14:27:55 -0400 Subject: [PATCH 3/3] address pr comments --- src/log_queue/queue.rs | 55 ++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/log_queue/queue.rs b/src/log_queue/queue.rs index 9db7a2b..56d0d49 100644 --- a/src/log_queue/queue.rs +++ b/src/log_queue/queue.rs @@ -694,38 +694,41 @@ impl LogQueueCore { let span_components = payload.span_components.clone(); - // Build a best-effort destination from parent_info (project registration - // hasn't happened yet, so project-name-based destinations are unknown). let span_id = payload.span_id.clone(); - let destination = if let Some(span_components) = span_components.as_ref() { - if let Some(object_id) = span_components.object_id.as_ref() { - match span_components.object_type { - SpanObjectType::Experiment => LogDestination::experiment(object_id.clone()), - SpanObjectType::ProjectLogs => { - LogDestination::project_logs(object_id.clone()) - } - SpanObjectType::PlaygroundLogs => { - LogDestination::playground_logs(object_id.clone()) - } + let destination = match span_components.as_ref() { + Some(span_components) => match ( + span_components.object_type, + span_components.object_id.as_ref(), + ) { + (SpanObjectType::Experiment, Some(object_id)) => { + LogDestination::experiment(object_id.clone()) } - } else if let Some(project_id) = span_components - .compute_object_metadata_args - .as_ref() - .and_then(|args| args.get("project_id")) - .and_then(Value::as_str) - { - LogDestination::project_logs(project_id.to_string()) - } else { - LogDestination::project_logs("unknown".to_string()) - } - } else { - match parent_info.as_ref() { + (SpanObjectType::ProjectLogs, Some(object_id)) => { + LogDestination::project_logs(object_id.clone()) + } + (SpanObjectType::PlaygroundLogs, Some(object_id)) => { + LogDestination::playground_logs(object_id.clone()) + } + (SpanObjectType::ProjectLogs, None) => match span_components + .compute_object_metadata_args + .as_ref() + .and_then(|args| args.get("project_id")) + .and_then(Value::as_str) + { + Some(project_id) => LogDestination::project_logs(project_id.to_string()), + None => return, + }, + (SpanObjectType::Experiment, None) => return, + (SpanObjectType::PlaygroundLogs, None) => return, + }, + None => match parent_info.as_ref() { Some(ParentSpanInfo::Experiment { object_id }) => { LogDestination::experiment(object_id.clone()) } Some(ParentSpanInfo::ProjectLogs { object_id }) => { LogDestination::project_logs(object_id.clone()) } + Some(ParentSpanInfo::ProjectName { .. }) => return, Some(ParentSpanInfo::PlaygroundLogs { object_id }) => { LogDestination::playground_logs(object_id.clone()) } @@ -745,8 +748,8 @@ impl LogQueueCore { LogDestination::playground_logs(object_id.clone()) } }, - _ => LogDestination::project_logs("unknown".to_string()), - } + None => return, + }, }; let root_span_id = span_components