From 6d98d944a9077978d1f620bf38720ca5b5a2dceb Mon Sep 17 00:00:00 2001 From: bravo1goingdark Date: Wed, 6 May 2026 01:42:52 +0530 Subject: [PATCH] feat(events): expose user_agent and client_ip in /v1/events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both fields are already accepted on ingest and stored in the label table (LABEL_CLIENT_IP=3, LABEL_USER_AGENT=4) — they survive the event roundtrip into LlmEvent. This change just plumbs them through the read path so /v1/events consumers can derive SDK / geographic breakdowns without re-querying the raw event row. - EventSummary (keplor-store/types.rs) gains client_ip + user_agent. - llm_to_summary copies them from the underlying LlmEvent. - EventResponse (keplor-server/routes.rs) gains the same two fields, serialized with skip_serializing_if = Option::is_none so events without recorded IP/UA stay compact on the wire. - export_events and the s3 archive-merge path mirror the change. No schema change. No new column read on the hot path. --- crates/keplor-server/src/routes.rs | 16 ++++++++++++++++ crates/keplor-store/src/kdb_store.rs | 2 ++ crates/keplor-store/src/types.rs | 8 ++++++++ 3 files changed, 26 insertions(+) diff --git a/crates/keplor-server/src/routes.rs b/crates/keplor-server/src/routes.rs index d72a2da..70dabc7 100644 --- a/crates/keplor-server/src/routes.rs +++ b/crates/keplor-server/src/routes.rs @@ -224,6 +224,16 @@ pub struct EventResponse { pub streaming: bool, pub error: Option, pub metadata: Option, + /// Client source IP as a string. Surfaced for SDK / geographic + /// breakdowns; never None in practice — events without a recorded + /// IP show as `null`. Consumers should bucket to /24 for IPv4 + /// before display rather than render raw addresses. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_ip: Option, + /// Caller-supplied User-Agent string. Used by clients to derive + /// coarse SDK identification (e.g. `openai-python/1.42`). + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, } /// Token usage in query response. @@ -317,6 +327,8 @@ pub async fn query_events( endpoint: e.endpoint, streaming: e.streaming, error: e.error_type, + client_ip: e.client_ip, + user_agent: e.user_agent, metadata, } }) @@ -506,6 +518,8 @@ fn llm_event_to_response(ev: keplor_core::LlmEvent) -> EventResponse { .to_owned() }), metadata: ev.metadata, + client_ip: ev.client_ip.map(|ip| ip.to_string()), + user_agent: ev.user_agent.map(|ua| ua.to_string()), } } @@ -1024,6 +1038,8 @@ pub async fn export_events( .metadata_json .as_deref() .and_then(|s| serde_json::from_str(s).ok()), + client_ip: event.client_ip, + user_agent: event.user_agent, }; if let Ok(json) = serde_json::to_string(&resp) { lines.push(json); diff --git a/crates/keplor-store/src/kdb_store.rs b/crates/keplor-store/src/kdb_store.rs index 54e17a5..a4f82b4 100644 --- a/crates/keplor-store/src/kdb_store.rs +++ b/crates/keplor-store/src/kdb_store.rs @@ -817,6 +817,8 @@ fn llm_to_summary(ev: LlmEvent) -> EventSummary { source: ev.source.map(|s| s.to_string()), error_type: ev.error.as_ref().map(|e| provider_error_type_key(e).to_owned()), metadata_json: ev.metadata.as_ref().map(|v| v.to_string()), + client_ip: ev.client_ip.map(|ip| ip.to_string()), + user_agent: ev.user_agent.map(|ua| ua.to_string()), } } diff --git a/crates/keplor-store/src/types.rs b/crates/keplor-store/src/types.rs index 3416ba9..5711fdf 100644 --- a/crates/keplor-store/src/types.rs +++ b/crates/keplor-store/src/types.rs @@ -79,6 +79,14 @@ pub struct EventSummary { pub error_type: Option, /// Arbitrary metadata as JSON text. pub metadata_json: Option, + /// Client source IP as a string ("203.0.113.1" or "2001:db8::1"). + /// Already accepted on ingest and stored in the label table — + /// surfaced here so consumers can derive geographic / network + /// breakdowns without re-querying the raw event row. + pub client_ip: Option, + /// Caller-supplied User-Agent header. Used to derive coarse SDK + /// identification (e.g. "openai-python", "anthropic-sdk-typescript"). + pub user_agent: Option, } /// Cost + event count from a quota query.