Skip to content

Commit 227f9cf

Browse files
authored
trace summaries triggers (#952)
* tmp * refactor * tmp * tmp * trigger span id * tmp
1 parent f2e2cb6 commit 227f9cf

File tree

8 files changed

+263
-61
lines changed

8 files changed

+263
-61
lines changed

app-server/src/cache/keys.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ pub const PROJECT_API_KEY_CACHE_KEY: &str = "project_api_key";
66
pub const PROJECT_CACHE_KEY: &str = "project";
77
pub const WORKSPACE_LIMITS_CACHE_KEY: &str = "workspace_limits";
88
pub const PROJECT_EVALUATORS_BY_PATH_CACHE_KEY: &str = "project_evaluators_by_path";
9+
pub const SUMMARY_TRIGGER_SPANS_CACHE_KEY: &str = "summary_trigger_spans";
910

1011
pub const WORKSPACE_BYTES_USAGE_CACHE_KEY: &str = "workspace_bytes_usage";

app-server/src/db/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod projects;
1313
pub mod provider_api_keys;
1414
pub mod spans;
1515
pub mod stats;
16+
pub mod summary_trigger_spans;
1617
pub mod tags;
1718
pub mod trace;
1819
pub mod user_cookies;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use serde::{Deserialize, Serialize};
2+
use serde_json::Value;
3+
use sqlx::PgPool;
4+
use uuid::Uuid;
5+
6+
/// Event definition with semantic analysis configuration
7+
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
8+
pub struct EventDefinition {
9+
pub id: Uuid,
10+
pub name: String,
11+
pub prompt: Option<String>,
12+
pub structured_output: Option<Value>,
13+
}
14+
15+
/// Summary trigger span with joined event definition
16+
#[derive(Debug, Clone, Serialize, Deserialize)]
17+
pub struct SummaryTriggerSpanWithEvent {
18+
pub span_name: String,
19+
pub event_definition: Option<EventDefinition>,
20+
}
21+
22+
#[derive(Debug, Clone, sqlx::FromRow)]
23+
struct DBTriggerSpanWithEvent {
24+
span_name: String,
25+
event_definition_id: Option<Uuid>,
26+
event_definition_name: Option<String>,
27+
event_definition_prompt: Option<String>,
28+
event_definition_structured_output: Option<Value>,
29+
}
30+
31+
/// Get summary trigger spans for a project with their associated semantic event definitions
32+
/// Returns all trigger spans for the project
33+
/// Only joins semantic event definitions (is_semantic = true) via the LEFT JOIN condition
34+
/// Triggers without event definitions will have event_definition = None
35+
pub async fn get_summary_trigger_spans_with_events(
36+
pool: &PgPool,
37+
project_id: Uuid,
38+
) -> Result<Vec<SummaryTriggerSpanWithEvent>, sqlx::Error> {
39+
let results = sqlx::query_as::<_, DBTriggerSpanWithEvent>(
40+
r#"
41+
SELECT
42+
sts.span_name as span_name,
43+
ed.id as event_definition_id,
44+
ed.name as event_definition_name,
45+
ed.prompt as event_definition_prompt,
46+
ed.structured_output as event_definition_structured_output
47+
FROM
48+
summary_trigger_spans sts
49+
LEFT JOIN
50+
event_definitions ed
51+
ON sts.event_name = ed.name
52+
AND sts.project_id = ed.project_id
53+
AND ed.is_semantic = true
54+
WHERE
55+
sts.project_id = $1
56+
"#,
57+
)
58+
.bind(project_id)
59+
.fetch_all(pool)
60+
.await?;
61+
62+
Ok(results
63+
.into_iter()
64+
.map(|db_trigger_span_with_event| {
65+
let event_definition = if let Some(id) = db_trigger_span_with_event.event_definition_id
66+
{
67+
Some(EventDefinition {
68+
id,
69+
name: db_trigger_span_with_event
70+
.event_definition_name
71+
.unwrap_or_default(),
72+
prompt: db_trigger_span_with_event.event_definition_prompt,
73+
structured_output: db_trigger_span_with_event
74+
.event_definition_structured_output,
75+
})
76+
} else {
77+
None
78+
};
79+
80+
SummaryTriggerSpanWithEvent {
81+
span_name: db_trigger_span_with_event.span_name,
82+
event_definition,
83+
}
84+
})
85+
.collect())
86+
}

app-server/src/traces/consumer.rs

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use uuid::Uuid;
1212
use super::{
1313
OBSERVATIONS_EXCHANGE, OBSERVATIONS_QUEUE, OBSERVATIONS_ROUTING_KEY,
1414
summary::push_to_trace_summary_queue,
15+
trigger::{check_span_trigger, get_summary_trigger_spans_cached},
1516
};
1617
use crate::{
1718
api::v1::traces::RabbitMqSpanMessage,
@@ -236,6 +237,13 @@ async fn process_batch(
236237
let mut span_usage_vec = Vec::new();
237238
let mut all_events = Vec::new();
238239

240+
// we get project id from the first span in the batch
241+
// because all spans in the batch have the same project id
242+
// batching is happening on the Otel SpanProcessor level
243+
// project_id can never be None, because batch is never empty
244+
// but we do unwrap_or_default to avoid Option<Uuid> in the rest of the code
245+
let project_id = spans.first().map(|s| s.project_id).unwrap_or_default();
246+
239247
for span in &mut spans {
240248
let span_usage =
241249
get_llm_usage_for_span(&mut span.attributes, db.clone(), cache.clone(), &span.name)
@@ -330,21 +338,9 @@ async fn process_batch(
330338
}
331339
}
332340

333-
// Check for completed traces (top-level spans) and push to trace summary queue
334-
for span in &spans {
335-
if span.parent_span_id.is_none() {
336-
if let Err(e) =
337-
push_to_trace_summary_queue(span.trace_id, span.project_id, queue.clone()).await
338-
{
339-
log::error!(
340-
"Failed to push trace completion to summary queue: trace_id={}, project_id={}, error={:?}",
341-
span.trace_id,
342-
span.project_id,
343-
e
344-
);
345-
}
346-
}
347-
}
341+
// Check for spans matching trigger conditions and push to trace summary queue
342+
check_and_push_trace_summaries(project_id, &spans, db.clone(), cache.clone(), queue.clone())
343+
.await;
348344

349345
// Send realtime messages directly to SSE connections after successful ClickHouse writes
350346
send_realtime_messages_to_sse(&spans, &sse_connections).await;
@@ -379,26 +375,21 @@ async fn process_batch(
379375
.sum::<usize>()
380376
+ total_events_ingested_bytes;
381377

382-
// we get project id from the first span in the batch
383-
// because all spans in the batch have the same project id
384-
// batching is happening on the Otel SpanProcessor level
385-
if let Some(project_id) = stripped_spans.first().map(|s| s.project_id) {
386-
if is_feature_enabled(Feature::UsageLimit) {
387-
if let Err(e) = update_workspace_limit_exceeded_by_project_id(
388-
db.clone(),
389-
clickhouse.clone(),
390-
cache.clone(),
378+
if is_feature_enabled(Feature::UsageLimit) {
379+
if let Err(e) = update_workspace_limit_exceeded_by_project_id(
380+
db.clone(),
381+
clickhouse.clone(),
382+
cache.clone(),
383+
project_id,
384+
total_ingested_bytes,
385+
)
386+
.await
387+
{
388+
log::error!(
389+
"Failed to update workspace limit exceeded for project [{}]: {:?}",
391390
project_id,
392-
total_ingested_bytes,
393-
)
394-
.await
395-
{
396-
log::error!(
397-
"Failed to update workspace limit exceeded for project [{}]: {:?}",
398-
project_id,
399-
e
400-
);
401-
}
391+
e
392+
);
402393
}
403394
}
404395

@@ -513,3 +504,51 @@ fn span_to_realtime_span(span: &Span) -> Value {
513504
// Note: input and output fields are intentionally excluded for performance
514505
})
515506
}
507+
508+
/// Check spans against trigger conditions and push matching traces to summary queue
509+
/// This function groups spans by project to minimize database/cache queries
510+
async fn check_and_push_trace_summaries(
511+
project_id: Uuid,
512+
spans: &[Span],
513+
db: Arc<DB>,
514+
cache: Arc<Cache>,
515+
queue: Arc<MessageQueue>,
516+
) {
517+
match get_summary_trigger_spans_cached(db.clone(), cache.clone(), project_id).await {
518+
Ok(trigger_spans) => {
519+
// Check each span against its project's trigger spans
520+
for span in spans {
521+
// Check if this span name matches any trigger
522+
let matching_triggers = check_span_trigger(&span.name, &trigger_spans);
523+
524+
// Send one message per matching trigger
525+
for trigger in matching_triggers {
526+
if let Err(e) = push_to_trace_summary_queue(
527+
span.trace_id,
528+
span.project_id,
529+
span.span_id,
530+
trigger.event_definition,
531+
queue.clone(),
532+
)
533+
.await
534+
{
535+
log::error!(
536+
"Failed to push trace completion to summary queue: trace_id={}, project_id={}, span_name={}, error={:?}",
537+
span.trace_id,
538+
span.project_id,
539+
span.name,
540+
e
541+
);
542+
}
543+
}
544+
}
545+
}
546+
Err(e) => {
547+
log::error!(
548+
"Failed to get summary trigger spans for project {}: {:?}",
549+
project_id,
550+
e
551+
);
552+
}
553+
}
554+
}

app-server/src/traces/eligibility.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ use crate::{
1515
#[derive(Debug, Clone)]
1616
pub struct TraceEligibilityResult {
1717
pub is_eligible: bool,
18-
pub reason: Option<String>,
1918
#[allow(dead_code)]
2019
pub tier_name: Option<String>,
2120
#[allow(dead_code)]
@@ -41,7 +40,6 @@ pub async fn check_trace_eligibility(
4140
Err(_) => {
4241
return Ok(TraceEligibilityResult {
4342
is_eligible: false,
44-
reason: Some("project not found".to_string()),
4543
tier_name: None,
4644
has_trace_analysis: false,
4745
});
@@ -58,7 +56,6 @@ pub async fn check_trace_eligibility(
5856
if !is_paid_tier {
5957
return Ok(TraceEligibilityResult {
6058
is_eligible: false,
61-
reason: Some("workspace is on free tier".to_string()),
6259
tier_name,
6360
has_trace_analysis: false,
6461
});
@@ -70,15 +67,13 @@ pub async fn check_trace_eligibility(
7067
if !is_trace_analysis_enabled {
7168
return Ok(TraceEligibilityResult {
7269
is_eligible: false,
73-
reason: Some("trace analysis not enabled in project settings".to_string()),
7470
tier_name,
7571
has_trace_analysis: false,
7672
});
7773
}
7874

7975
Ok(TraceEligibilityResult {
8076
is_eligible: true,
81-
reason: None,
8277
tier_name,
8378
has_trace_analysis: true,
8479
})

app-server/src/traces/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod provider;
88
pub mod span_attributes;
99
pub mod spans;
1010
pub mod summary;
11+
pub mod trigger;
1112
pub mod utils;
1213

1314
pub const OBSERVATIONS_QUEUE: &str = "observations_queue";

0 commit comments

Comments
 (0)