Skip to content

Commit 32979cb

Browse files
update: tool catalog (#1831)
* update: tool catalog add get_trace_activity to tool catalog update executor to have tool implementations and reuse in enterprise * coderabbit comments fix * coderabbit comments fix
1 parent af11d36 commit 32979cb

5 files changed

Lines changed: 1232 additions & 240 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ tracing-actix-web = "0.7"
138138

139139
# Time and Date
140140
chrono = "0.4"
141+
chrono-tz = "0.10"
141142
chrono-humanize = "0.2"
142143
humantime = "2.1.0"
143144
humantime-serde = "1.1"

‎src/handlers/http/query_context.rs‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,26 @@ pub async fn query_context(
138138
let permissions = Users.get_permissions(&creds);
139139

140140
create_streams_for_distributed(vec![context_request.dataset.clone()], &tenant_id).await?;
141-
let authorized_datasets = vec![context_request.dataset.clone()];
142141
user_auth_for_datasets(
143142
&permissions,
144143
std::slice::from_ref(&context_request.dataset),
145144
&tenant_id,
146145
)
147146
.await?;
148147

148+
query_context_for_authorized_dataset(context_request, &tenant_id)
149+
.await
150+
.map(web::Json)
151+
}
152+
153+
/// Fetches log context after the caller has authorized the dataset and made it
154+
/// available on the local query node.
155+
pub(crate) async fn query_context_for_authorized_dataset(
156+
context_request: LogContextRequest,
157+
tenant_id: &Option<String>,
158+
) -> Result<LogContextResponse, QueryError> {
159+
let span = Span::current();
160+
let authorized_datasets = vec![context_request.dataset.clone()];
149161
let page_size = normalize_log_context_page_size(context_request.page_size)?;
150162
span.record("page_size", page_size);
151163
let anchor_timestamp = parse_log_context_timestamp(&context_request.p_timestamp)?;
@@ -164,7 +176,7 @@ pub async fn query_context(
164176
"query context request normalized"
165177
);
166178

167-
let dataset = PARSEABLE.get_stream(&context_request.dataset, &tenant_id)?;
179+
let dataset = PARSEABLE.get_stream(&context_request.dataset, tenant_id)?;
168180
let schema = dataset.get_schema();
169181
validate_log_context_schema(schema.as_ref(), DEFAULT_TIMESTAMP_KEY)?;
170182
let match_fields = normalize_log_context_match_fields(
@@ -191,7 +203,7 @@ pub async fn query_context(
191203
&authorized_datasets,
192204
&context_start_time_str,
193205
&context_end_time_str,
194-
&tenant_id,
206+
tenant_id,
195207
)
196208
.await?;
197209
debug!(
@@ -242,8 +254,8 @@ pub async fn query_context(
242254
);
243255

244256
let (newer_records, older_records) = tokio::try_join!(
245-
execute_log_context_rows(&newer_payload, &authorized_datasets, &tenant_id),
246-
execute_log_context_rows(&older_payload, &authorized_datasets, &tenant_id),
257+
execute_log_context_rows(&newer_payload, &authorized_datasets, tenant_id),
258+
execute_log_context_rows(&older_payload, &authorized_datasets, tenant_id),
247259
)?;
248260
let (records, anchor_index) =
249261
build_log_context_records_window(newer_records, older_records, page_size)?;
@@ -274,7 +286,7 @@ pub async fn query_context(
274286
tenant_id.as_deref().unwrap_or(DEFAULT_TENANT),
275287
);
276288

277-
Ok(web::Json(LogContextResponse {
289+
Ok(LogContextResponse {
278290
scope,
279291
context_start_time: context_start_time_str,
280292
context_end_time: context_end_time_str,
@@ -284,7 +296,7 @@ pub async fn query_context(
284296
anchored_duplicate: LOG_CONTEXT_ANCHORED_DUPLICATE,
285297
records,
286298
queries,
287-
}))
299+
})
288300
}
289301

290302
fn normalize_log_context_page_size(page_size: Option<u64>) -> Result<u64, QueryError> {

‎src/tool_catalog.rs‎

Lines changed: 124 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ impl ToolSpec {
105105
| "discover_datasets"
106106
| "get_traces"
107107
| "get_trace"
108+
| "get_trace_activity"
108109
| "list_log_errors"
109110
| "list_trace_errors" => ToolEffect::ExpensiveRead,
110111
"create_alert"
@@ -135,7 +136,10 @@ pub fn validate_tool_arguments(schema: &Value, arguments: &Value) -> Result<(),
135136

136137
fn validate_schema_value(schema: &Value, value: &Value, path: &str) -> Result<(), String> {
137138
if let Some(result) = validate_any_of(schema, value, path) {
138-
return result;
139+
result?;
140+
}
141+
if let Some(result) = validate_one_of(schema, value, path) {
142+
result?;
139143
}
140144

141145
validate_enum(schema, value, path)?;
@@ -146,6 +150,21 @@ fn validate_schema_value(schema: &Value, value: &Value, path: &str) -> Result<()
146150
validate_number(schema, value, path)
147151
}
148152

153+
fn validate_one_of(schema: &Value, value: &Value, path: &str) -> Option<Result<(), String>> {
154+
schema
155+
.get("oneOf")
156+
.and_then(Value::as_array)
157+
.map(|variants| {
158+
let matches = variants
159+
.iter()
160+
.filter(|variant| validate_schema_value(variant, value, path).is_ok())
161+
.count();
162+
(matches == 1)
163+
.then_some(())
164+
.ok_or_else(|| format!("{path} must match exactly one allowed schema"))
165+
})
166+
}
167+
149168
fn validate_any_of(schema: &Value, value: &Value, path: &str) -> Option<Result<(), String>> {
150169
schema
151170
.get("anyOf")
@@ -336,7 +355,9 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
336355
json!({
337356
"query": string_property("User request or dataset search text."),
338357
"selectedDataset": { "type": "string" },
339-
"contextDataset": { "type": "string" }
358+
"contextDataset": { "type": "string" },
359+
"telemetryType": { "type": "string", "enum": ["logs", "metrics", "traces"] },
360+
"limit": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 }
340361
}),
341362
&["query"],
342363
),
@@ -380,6 +401,8 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
380401
"dataset": string_property("Dataset name."),
381402
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 },
382403
"minutes": { "type": "integer", "minimum": 1, "maximum": 1440, "default": 60 },
404+
"startTime": string_property("Optional explicit RFC3339 range start; provide with endTime."),
405+
"endTime": string_property("Optional explicit RFC3339 range end; provide with startTime."),
383406
}),
384407
&["dataset"],
385408
),
@@ -388,21 +411,29 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
388411
"get_log_context",
389412
"Get surrounding log context",
390413
"Fetch records around an exact anchor log row.",
391-
object_schema(
392-
json!({
393-
"dataset": string_property("Dataset containing the anchor row."),
394-
"pTimestamp": string_property("Exact p_timestamp from the anchor row."),
395-
"log": { "type": "string" },
396-
"body": { "type": "string" },
397-
"message": { "type": "string" },
398-
"contextWindow": { "type": "string" },
399-
"contextStartTime": { "type": "string" },
400-
"contextEndTime": { "type": "string" },
401-
"conditions": { "type": "object", "additionalProperties": true },
402-
"pageSize": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 },
403-
}),
404-
&["dataset", "pTimestamp"],
405-
),
414+
{
415+
let mut schema = object_schema(
416+
json!({
417+
"dataset": string_property("Dataset containing the anchor row."),
418+
"pTimestamp": string_property("Exact p_timestamp from the anchor row."),
419+
"log": string_property("Exact log value from the anchor row. Provide exactly one of log, body, or message."),
420+
"body": string_property("Exact body value from the anchor row. Provide exactly one of log, body, or message."),
421+
"message": string_property("Exact message value from the anchor row. Provide exactly one of log, body, or message."),
422+
"contextWindow": { "type": "string" },
423+
"contextStartTime": { "type": "string" },
424+
"contextEndTime": { "type": "string" },
425+
"conditions": { "type": "object", "additionalProperties": true },
426+
"pageSize": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 },
427+
}),
428+
&["dataset", "pTimestamp"],
429+
);
430+
schema["oneOf"] = json!([
431+
{ "required": ["log"] },
432+
{ "required": ["body"] },
433+
{ "required": ["message"] }
434+
]);
435+
schema
436+
},
406437
),
407438
ToolSpec::new(
408439
"list_log_errors",
@@ -504,6 +535,22 @@ pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
504535
&["traceId", "dataset", "startTime", "endTime"],
505536
),
506537
),
538+
ToolSpec::new(
539+
"get_trace_activity",
540+
"Get trace activity",
541+
"Return a bounded trace-activity time series using Parseable's canonical semantics: span_count is COUNT(DISTINCT span_span_id) and trace_count is COUNT(DISTINCT span_trace_id). Use this for span or trace volume, trends, charts, and service breakdowns instead of deriving counts from raw trace rows. Returns the exact executed SQL with its rows so clients can reuse it unchanged.",
542+
object_schema(
543+
json!({
544+
"dataset": string_property("One traces dataset."),
545+
"startTime": string_property("RFC3339 range start."),
546+
"endTime": string_property("RFC3339 range end."),
547+
"groupBy": { "type": "array", "maxItems": 5, "items": { "type": "string" }, "description": "Optional exact trace fields, such as service.name, used to split the activity series." },
548+
"filters": { "type": "object", "description": "Optional exact trace-field filters.", "additionalProperties": true },
549+
"bucketCount": { "type": "integer", "minimum": 1, "maximum": 100, "default": 60, "description": "Approximate number of time buckets." }
550+
}),
551+
&["dataset", "startTime", "endTime"],
552+
),
553+
),
507554
ToolSpec::new(
508555
"ping",
509556
"Ping Parseable server",
@@ -854,7 +901,7 @@ mod tests {
854901
let tools = oss_tool_specs();
855902
let names = tools.iter().map(|tool| tool.name).collect::<HashSet<_>>();
856903

857-
assert_eq!(tools.len(), 49);
904+
assert_eq!(tools.len(), 50);
858905
assert_eq!(tools.len(), names.len());
859906
}
860907

@@ -893,7 +940,7 @@ mod tests {
893940
let read_tools = oss_read_tool_specs();
894941
let action_tools = oss_action_tool_specs();
895942

896-
assert_eq!(read_tools.len(), 36);
943+
assert_eq!(read_tools.len(), 37);
897944
assert_eq!(action_tools.len(), 13);
898945
assert!(read_tools.iter().all(|tool| matches!(
899946
tool.effect(),
@@ -977,6 +1024,18 @@ mod tests {
9771024
);
9781025
}
9791026

1027+
#[actix_web::test]
1028+
async fn oss_catalog_exposes_trace_activity() {
1029+
let tool = oss_read_tool_specs()
1030+
.into_iter()
1031+
.find(|tool| tool.name == "get_trace_activity")
1032+
.expect("get_trace_activity must be available in OSS");
1033+
1034+
assert_eq!(tool.effect(), ToolEffect::ExpensiveRead);
1035+
assert_eq!(tool.input_schema["properties"]["dataset"]["type"], "string");
1036+
assert_eq!(tool.input_schema["properties"]["groupBy"]["maxItems"], 5);
1037+
}
1038+
9801039
#[actix_web::test]
9811040
async fn tool_call_defaults_to_empty_arguments() {
9821041
let call: ToolCallRequest = serde_json::from_value(serde_json::json!({
@@ -1012,6 +1071,52 @@ mod tests {
10121071
.validate_arguments(&serde_json::json!({ "dataset": "logs" }))
10131072
.is_ok()
10141073
);
1074+
1075+
let resolve_tool = oss_tool_specs()
1076+
.into_iter()
1077+
.find(|tool| tool.name == "resolve_dataset")
1078+
.unwrap();
1079+
assert!(
1080+
resolve_tool
1081+
.validate_arguments(&serde_json::json!({
1082+
"query": "metrics",
1083+
"telemetryType": "metrics",
1084+
"limit": 10
1085+
}))
1086+
.is_ok()
1087+
);
1088+
1089+
let context_tool = oss_tool_specs()
1090+
.into_iter()
1091+
.find(|tool| tool.name == "get_log_context")
1092+
.unwrap();
1093+
assert!(
1094+
context_tool
1095+
.validate_arguments(&serde_json::json!({
1096+
"dataset": "logs",
1097+
"pTimestamp": "2026-10-22T00:00:00Z"
1098+
}))
1099+
.is_err()
1100+
);
1101+
assert!(
1102+
context_tool
1103+
.validate_arguments(&serde_json::json!({
1104+
"dataset": "logs",
1105+
"pTimestamp": "2026-10-22T00:00:00Z",
1106+
"message": "failed"
1107+
}))
1108+
.is_ok()
1109+
);
1110+
assert!(
1111+
context_tool
1112+
.validate_arguments(&serde_json::json!({
1113+
"dataset": "logs",
1114+
"pTimestamp": "2026-10-22T00:00:00Z",
1115+
"log": "failed",
1116+
"message": "failed"
1117+
}))
1118+
.is_err()
1119+
);
10151120
}
10161121

10171122
#[actix_web::test]

0 commit comments

Comments
 (0)