Skip to content

Commit 595af02

Browse files
update: context api support for start and end time (#1695)
context api request to have support to send either of - 1. context window (1m, 5m, 10m etc) 2. context start and end time add validations - 1. p_timestamp of the reference log should be within the context window 2. window and start/end time - both should not be present in the request 3. start <= end time
1 parent ad4e004 commit 595af02

1 file changed

Lines changed: 161 additions & 12 deletions

File tree

src/handlers/http/query_context.rs

Lines changed: 161 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const LOG_CONTEXT_ANCHORED_DUPLICATE: &str = "first";
4545
#[serde(rename_all = "camelCase")]
4646
pub struct LogContextRequest {
4747
pub dataset: String,
48-
pub context_window: String,
48+
pub context_window: Option<String>,
49+
pub context_start_time: Option<String>,
50+
pub context_end_time: Option<String>,
4951
pub p_timestamp: String,
5052
pub log: Option<String>,
5153
pub body: Option<String>,
@@ -145,8 +147,13 @@ pub async fn query_context(
145147
let page_size = normalize_log_context_page_size(context_request.page_size)?;
146148
span.record("page_size", page_size);
147149
let anchor_timestamp = parse_log_context_timestamp(&context_request.p_timestamp)?;
148-
let (context_start_time, context_end_time) =
149-
log_context_bounds(anchor_timestamp, &context_request.context_window)?;
150+
let (context_start_time, context_end_time) = resolve_log_context_bounds(
151+
anchor_timestamp,
152+
context_request.context_window.as_deref(),
153+
context_request.context_start_time.as_deref(),
154+
context_request.context_end_time.as_deref(),
155+
)?;
156+
validate_log_context_anchor_in_bounds(anchor_timestamp, context_start_time, context_end_time)?;
150157
debug!(
151158
page_size,
152159
anchor_timestamp = %anchor_timestamp,
@@ -290,17 +297,23 @@ fn normalize_log_context_page_size(page_size: Option<u64>) -> Result<u64, QueryE
290297
}
291298

292299
fn parse_log_context_timestamp(raw: &str) -> Result<DateTime<Utc>, QueryError> {
300+
parse_log_context_time_field(raw, "pTimestamp")
301+
}
302+
303+
fn parse_log_context_time_field(raw: &str, field_name: &str) -> Result<DateTime<Utc>, QueryError> {
293304
let raw = raw.trim();
294305
let timestamp = DateTime::parse_from_rfc3339(raw)
295306
.map(|timestamp| timestamp.with_timezone(&Utc))
296307
.or_else(|rfc3339_err| {
297308
parse_log_context_naive_utc_timestamp(raw)
298309
.map(|timestamp| DateTime::from_naive_utc_and_offset(timestamp, Utc))
299-
.map_err(|_| QueryError::CustomError(format!("Invalid pTimestamp: {rfc3339_err}")))
310+
.map_err(|_| {
311+
QueryError::CustomError(format!("Invalid {field_name}: {rfc3339_err}"))
312+
})
300313
})?;
301314

302315
DateTime::from_timestamp_millis(timestamp.timestamp_millis()).ok_or_else(|| {
303-
QueryError::CustomError("pTimestamp is outside the supported range".to_string())
316+
QueryError::CustomError(format!("{field_name} is outside the supported range"))
304317
})
305318
}
306319

@@ -309,7 +322,33 @@ fn parse_log_context_naive_utc_timestamp(raw: &str) -> Result<NaiveDateTime, chr
309322
.or_else(|_| NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f"))
310323
}
311324

312-
fn log_context_bounds(
325+
fn resolve_log_context_bounds(
326+
anchor_timestamp: DateTime<Utc>,
327+
context_window: Option<&str>,
328+
context_start_time: Option<&str>,
329+
context_end_time: Option<&str>,
330+
) -> Result<(DateTime<Utc>, DateTime<Utc>), QueryError> {
331+
match (context_window, context_start_time, context_end_time) {
332+
(Some(_), Some(_), _) | (Some(_), _, Some(_)) => Err(QueryError::CustomError(
333+
"Request must include either contextWindow or contextStartTime/contextEndTime, not both"
334+
.to_string(),
335+
)),
336+
(Some(context_window), None, None) => {
337+
log_context_window_bounds(anchor_timestamp, context_window)
338+
}
339+
(None, Some(context_start_time), Some(context_end_time)) => {
340+
log_context_explicit_bounds(context_start_time, context_end_time)
341+
}
342+
(None, Some(_), None) | (None, None, Some(_)) => Err(QueryError::CustomError(
343+
"contextStartTime and contextEndTime must be provided together".to_string(),
344+
)),
345+
(None, None, None) => Err(QueryError::CustomError(
346+
"Request must include either contextWindow or contextStartTime/contextEndTime".to_string(),
347+
)),
348+
}
349+
}
350+
351+
fn log_context_window_bounds(
313352
anchor_timestamp: DateTime<Utc>,
314353
context_window: &str,
315354
) -> Result<(DateTime<Utc>, DateTime<Utc>), QueryError> {
@@ -333,6 +372,37 @@ fn log_context_bounds(
333372
Ok((start, end))
334373
}
335374

375+
fn log_context_explicit_bounds(
376+
context_start_time: &str,
377+
context_end_time: &str,
378+
) -> Result<(DateTime<Utc>, DateTime<Utc>), QueryError> {
379+
let start = parse_log_context_time_field(context_start_time, "contextStartTime")?;
380+
let end = parse_log_context_time_field(context_end_time, "contextEndTime")?;
381+
382+
if start >= end {
383+
return Err(QueryError::CustomError(
384+
"contextStartTime must be before contextEndTime".to_string(),
385+
));
386+
}
387+
388+
Ok((start, end))
389+
}
390+
391+
fn validate_log_context_anchor_in_bounds(
392+
anchor_timestamp: DateTime<Utc>,
393+
context_start_time: DateTime<Utc>,
394+
context_end_time: DateTime<Utc>,
395+
) -> Result<(), QueryError> {
396+
if anchor_timestamp >= context_start_time && anchor_timestamp < context_end_time {
397+
return Ok(());
398+
}
399+
400+
Err(QueryError::CustomError(
401+
"pTimestamp must be greater than or equal to contextStartTime and less than contextEndTime"
402+
.to_string(),
403+
))
404+
}
405+
336406
fn normalize_log_context_match_fields(
337407
log: &Option<String>,
338408
body: &Option<String>,
@@ -942,7 +1012,7 @@ fn log_context_cursor_from_record(
9421012
}
9431013

9441014
fn format_log_context_api_time(timestamp: DateTime<Utc>) -> String {
945-
timestamp.to_rfc3339_opts(SecondsFormat::Secs, true)
1015+
timestamp.to_rfc3339_opts(SecondsFormat::AutoSi, true)
9461016
}
9471017

9481018
fn timestamp_sql_literal(timestamp: DateTime<Utc>) -> String {
@@ -1023,16 +1093,93 @@ mod tests {
10231093
}
10241094

10251095
#[test]
1026-
fn log_context_bounds_apply_window_and_truncate_to_minute() {
1027-
let (start, end) = log_context_bounds(anchor_timestamp(), "1m").unwrap();
1096+
fn log_context_explicit_bounds_accept_start_and_end_times() {
1097+
let (start, end) =
1098+
log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap();
10281099
assert_eq!(format_log_context_api_time(start), "2026-06-17T10:14:00Z");
10291100
assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z");
10301101

1031-
let (start, end) = log_context_bounds(anchor_timestamp(), "5s").unwrap();
1102+
let (start, end) =
1103+
log_context_explicit_bounds("2026-06-17T10:15:42.100Z", "2026-06-17T10:15:42.900Z")
1104+
.unwrap();
1105+
assert_eq!(
1106+
format_log_context_api_time(start),
1107+
"2026-06-17T10:15:42.100Z"
1108+
);
1109+
assert_eq!(format_log_context_api_time(end), "2026-06-17T10:15:42.900Z");
1110+
1111+
assert!(
1112+
log_context_explicit_bounds("2026-06-17T10:16:00Z", "2026-06-17T10:16:00Z").is_err()
1113+
);
1114+
assert!(
1115+
log_context_explicit_bounds("2026-06-17T10:17:00Z", "2026-06-17T10:16:00Z").is_err()
1116+
);
1117+
}
1118+
1119+
#[test]
1120+
fn log_context_window_bounds_apply_window_and_truncate_to_minute() {
1121+
let (start, end) = log_context_window_bounds(anchor_timestamp(), "1m").unwrap();
1122+
assert_eq!(format_log_context_api_time(start), "2026-06-17T10:14:00Z");
1123+
assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z");
1124+
1125+
let (start, end) = log_context_window_bounds(anchor_timestamp(), "5s").unwrap();
10321126
assert_eq!(format_log_context_api_time(start), "2026-06-17T10:15:00Z");
10331127
assert_eq!(format_log_context_api_time(end), "2026-06-17T10:16:00Z");
10341128
}
10351129

1130+
#[test]
1131+
fn log_context_bounds_resolver_accepts_one_mode_only() {
1132+
let anchor = anchor_timestamp();
1133+
assert!(resolve_log_context_bounds(anchor, Some("1m"), None, None).is_ok());
1134+
assert!(
1135+
resolve_log_context_bounds(
1136+
anchor,
1137+
None,
1138+
Some("2026-06-17T10:14:00Z"),
1139+
Some("2026-06-17T10:16:00Z"),
1140+
)
1141+
.is_ok()
1142+
);
1143+
assert!(
1144+
resolve_log_context_bounds(anchor, Some("1m"), Some("2026-06-17T10:14:00Z"), None,)
1145+
.is_err()
1146+
);
1147+
assert!(
1148+
resolve_log_context_bounds(anchor, None, Some("2026-06-17T10:14:00Z"), None).is_err()
1149+
);
1150+
assert!(resolve_log_context_bounds(anchor, None, None, None).is_err());
1151+
}
1152+
1153+
#[test]
1154+
fn log_context_anchor_must_be_inside_context_bounds() {
1155+
let (start, end) =
1156+
log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap();
1157+
1158+
validate_log_context_anchor_in_bounds(
1159+
parse_log_context_timestamp("2026-06-17T10:14:00Z").unwrap(),
1160+
start,
1161+
end,
1162+
)
1163+
.unwrap();
1164+
validate_log_context_anchor_in_bounds(anchor_timestamp(), start, end).unwrap();
1165+
assert!(
1166+
validate_log_context_anchor_in_bounds(
1167+
parse_log_context_timestamp("2026-06-17T10:13:59.999Z").unwrap(),
1168+
start,
1169+
end,
1170+
)
1171+
.is_err()
1172+
);
1173+
assert!(
1174+
validate_log_context_anchor_in_bounds(
1175+
parse_log_context_timestamp("2026-06-17T10:16:00Z").unwrap(),
1176+
start,
1177+
end,
1178+
)
1179+
.is_err()
1180+
);
1181+
}
1182+
10361183
#[test]
10371184
fn log_context_match_fields_accept_exactly_one_anchor_field() {
10381185
let schema = schema_with(&[DEFAULT_TIMESTAMP_KEY, "body", "log", "message"]);
@@ -1130,7 +1277,8 @@ mod tests {
11301277
#[test]
11311278
fn log_context_anchor_count_sql_only_counts_anchor_duplicates() {
11321279
let anchor = anchor_timestamp();
1133-
let (start, end) = log_context_bounds(anchor, "1m").unwrap();
1280+
let (start, end) =
1281+
log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap();
11341282
let match_fields = vec![LogContextMatchField {
11351283
name: "message".to_string(),
11361284
value: "alpha".to_string(),
@@ -1150,7 +1298,8 @@ mod tests {
11501298
#[test]
11511299
fn log_context_cursor_sql_builds_previous_and_next_pages() {
11521300
let anchor = anchor_timestamp();
1153-
let (start, end) = log_context_bounds(anchor, "1m").unwrap();
1301+
let (start, end) =
1302+
log_context_explicit_bounds("2026-06-17T10:14:00Z", "2026-06-17T10:16:00Z").unwrap();
11541303
let match_fields = vec![LogContextMatchField {
11551304
name: "message".to_string(),
11561305
value: "alpha".to_string(),

0 commit comments

Comments
 (0)