blob: ab0c57ffbb0baa44166ba24860a840e1ca414078 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::{EventLog, Page, Query, StoreError, invalid};
use maka_agent_facts::accounting::ActivityKind;
use maka_agent_facts::accounting::ActivityStatus;
use maka_agent_facts::accounting::Selection;
use serde::Serialize;
use sqlx::{Connection, QueryBuilder, sqlite::SqliteRow};
pub(super) enum View {
Models,
Tools,
Activity(Selection),
}
impl View {
fn table(&self) -> &'static str {
match self {
Self::Models => "model_usage",
Self::Tools => "tool_usage",
Self::Activity(_) => "usage_activity",
}
}
fn columns(&self) -> &'static str {
match self {
Self::Models => {
"event_id, origin, session_id, binding, model_id, started_at, completed_at, outcome, usage, quote_json, usd"
}
Self::Tools => "event_id, invocation, call, name, binding, completed_at, result",
Self::Activity(_) => {
"kind, event_id, origin, session_id, binding, model_id, started_at, completed_at, outcome, usage, quote_json, usd, invocation, call, name, result"
}
}
}
fn restrict(&self, sql: &mut QueryBuilder<sqlx::Sqlite>, query: &Query, through: u64) {
query.restrict(sql, through);
if let Self::Activity(selection) = self {
if let Some(kind) = selection.kind {
sql.push(" AND kind = ").push_bind(match kind {
ActivityKind::Model => "model",
ActivityKind::Tool => "tool",
});
}
if let Some(status) = selection.status {
sql.push(" AND status = ").push_bind(match status {
ActivityStatus::Success => "success",
ActivityStatus::Error => "error",
ActivityStatus::Aborted => "aborted",
ActivityStatus::Unknown => "unknown",
ActivityStatus::Rejected => "rejected",
});
}
if !selection.search.is_empty() {
sql.push(" AND instr(lower(search), lower(")
.push_bind(selection.search.clone())
.push(")) > 0");
}
}
}
}
pub(super) async fn page<T: Serialize + Send + 'static>(
log: &EventLog,
query: Query,
offset: u64,
limit: u32,
view: View,
read: fn(&SqliteRow) -> Result<T, StoreError>,
) -> Result<Page<T>, StoreError> {
log.validate_root()?;
query.validate()?;
if offset > i64::MAX as u64 || !(1..=100).contains(&limit) {
return Err(invalid("invalid Usage page"));
}
log.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let through = query.fence(&mut tx).await?;
// Only closed internal view/column identifiers are interpolated.
let mut count = QueryBuilder::new("SELECT COUNT(*) FROM ");
count.push(view.table());
view.restrict(&mut count, &query, through);
let total: i64 = count.build_query_scalar().fetch_one(&mut *tx).await?;
let mut select = QueryBuilder::new("SELECT ");
select
.push(view.columns())
.push(" FROM ")
.push(view.table());
view.restrict(&mut select, &query, through);
select
.push(" ORDER BY completed_at DESC, sequence DESC LIMIT ")
.push_bind(limit)
.push(" OFFSET ")
.push_bind(offset as i64);
let rows = select.build().fetch_all(&mut *tx).await?;
let mut attempts = Vec::with_capacity(rows.len());
// Leave envelope/cursor headroom. Never truncate one accounting fact.
let mut bytes = 0;
for row in &rows {
let attempt = read(row)?;
let size = serde_json::to_vec(&attempt)?.len() + 1;
if bytes + size > 32 * 1024 {
if attempts.is_empty() {
return Err(invalid("one Usage record exceeds page capacity"));
}
break;
}
bytes += size;
attempts.push(attempt);
}
let total = crate::sequence_number(total)?;
let end = offset + attempts.len() as u64;
tx.commit().await?;
Ok(Page {
through,
attempts,
total,
next_offset: (end < total).then_some(end),
})
})
})
.await
}