196
model_id: String,
197
request_id: String,
198
duration_ms: u64,
199
+ ttft_ms: Option<u64>,
200
status: String,
201
) {
202
let client = self.clone();
206
model_id,
207
request_id,
208
duration_ms,
209
+ ttft_ms,
210
status,
211
onde_app_id: client.onde_app_id.clone(),
212
};
320
return;
321
};
322
let event_key = scoped_document_key(onde_app_id, &[&event.request_id]);
323
+ let document = inference_document(event);
324
322
- self.write_doc(
323
- "pulse_inference_events",
324
- Some(&event_key),
325
- &serde_json::json!({
326
- "edge_id": event.edge_id,
327
- "model_id": event.model_id,
328
- "request_id": event.request_id,
329
- "duration_ms": event.duration_ms,
330
- "status": event.status,
331
- "onde_app_id": event.onde_app_id,
332
- }),
333
- )
334
- .await;
325
+ self.write_doc("pulse_inference_events", Some(&event_key), &document)
326
+ .await;
327
}
328
329
/// Upsert (or append, when `key` is `None`) one document, logging failures.
350
.join(":")
351
}
352
353
+/// Build the inference document. `ttft_ms` is omitted entirely when the path
354
+/// could not measure it, so a reader can tell "not measured" from a real zero
355
+/// — the relational route stores a hardcoded `0` and cannot make that
356
+/// distinction.
357
+fn inference_document(event: &InferenceEvent) -> serde_json::Value {
358
+ let mut document = serde_json::json!({
359
+ "edge_id": event.edge_id,
360
+ "model_id": event.model_id,
361
+ "request_id": event.request_id,
362
+ "duration_ms": event.duration_ms,
363
+ "status": event.status,
364
+ "onde_app_id": event.onde_app_id,
365
+ });
366
+
367
+ if let (Some(map), Some(ttft_ms)) = (document.as_object_mut(), event.ttft_ms) {
368
+ map.insert("ttft_ms".to_string(), serde_json::json!(ttft_ms));
369
+ }
370
+
371
+ document
372
+}
373
+
374
+/// `status` is always `"online"`: this document is only written when the edge
375
+/// just loaded a model, and nothing reports a shutdown. `last_seen_ms` is what
376
+/// makes that honest — a reader counts an edge as active by recency, the way
377
+/// the relational dashboard windows on `last_seen_at`, rather than trusting a
378
+/// status that is never revoked.
379
fn edge_document(event: &ModelLoadedEvent, onde_app_id: &str) -> serde_json::Value {
380
serde_json::json!({
381
"edge_id": event.edge_id,
382
"onde_app_id": onde_app_id,
383
"status": "online",
384
+ "last_seen_ms": epoch_millis(),
385
})
386
}
387
388
+/// Milliseconds since the Unix epoch. The crate has no date dependency and
389
+/// this is only ever read as a recency window, so an integer beats pulling in
390
+/// a formatting library for an RFC 3339 string.
391
+fn epoch_millis() -> u64 {
392
+ std::time::SystemTime::now()
393
+ .duration_since(std::time::UNIX_EPOCH)
394
+ .map(|elapsed| elapsed.as_millis() as u64)
395
+ .unwrap_or(0)
396
+}
397
+
398
#[cfg(test)]
399
mod tests {
400
use super::*;
445
assert_eq!(document["edge_id"], "edge-1");
446
assert_eq!(document["onde_app_id"], "app-a");
447
assert_eq!(document["status"], "online");
448
+ // Liveness has to be derivable from the document itself: nothing ever
449
+ // writes an offline status, so a reader windows on this instead.
450
+ assert!(document["last_seen_ms"].as_u64().unwrap_or(0) > 0);
451
+ }
452
+
453
+ #[test]
454
+ fn inference_document_omits_ttft_when_it_was_not_measured() {
455
+ let mut event = InferenceEvent {
456
+ edge_id: "edge-1".to_string(),
457
+ model_id: "model-1".to_string(),
458
+ request_id: "req-1".to_string(),
459
+ duration_ms: 400,
460
+ ttft_ms: None,
461
+ status: "success".to_string(),
462
+ onde_app_id: Some("app-a".to_string()),
463
+ };
464
+ assert!(inference_document(&event).get("ttft_ms").is_none());
465
+
466
+ event.ttft_ms = Some(90);
467
+ assert_eq!(inference_document(&event)["ttft_ms"], 90);
468
}
469
}