use harness_core::{CompactionStage, ModelInfo, Usage};
use std::time::Duration;
pub const OPERATION_DURATION: &str = "gen_ai.client.operation.duration";
pub const TOKEN_USAGE: &str = "gen_ai.client.token.usage";
pub const TOOL_DURATION: &str = "harness.tool.duration";
pub const RUN_DURATION: &str = "harness.run.duration";
pub const TOKENS_SAVED: &str = "harness.compaction.tokens_saved";
pub fn describe() {
use metrics::{Unit, describe_counter, describe_histogram};
describe_histogram!(
OPERATION_DURATION,
Unit::Seconds,
"Duration of one model call, cancelled and failed calls included."
);
describe_histogram!(
TOKEN_USAGE,
Unit::Count,
"Tokens per model call, by gen_ai.token.type."
);
describe_histogram!(
TOOL_DURATION,
Unit::Seconds,
"Duration of one tool call, deadline and cancellation included."
);
describe_histogram!(
RUN_DURATION,
Unit::Seconds,
"Duration of one agent run, by how it ended."
);
describe_counter!(
TOKENS_SAVED,
Unit::Count,
"Context tokens removed by compaction, by stage."
);
}
pub(crate) enum ModelCallEnd<'a> {
Done(&'a Usage),
Failed,
Cancelled,
}
pub(crate) fn model_call(info: &ModelInfo, elapsed: Duration, end: ModelCallEnd<'_>) {
let provider = info.provider.clone();
let model = info.model.clone();
let duration = elapsed.as_secs_f64();
match end {
ModelCallEnd::Done(usage) => {
metrics::histogram!(
OPERATION_DURATION,
"gen_ai.operation.name" => "chat",
"gen_ai.provider.name" => provider.clone(),
"gen_ai.request.model" => model.clone(),
)
.record(duration);
for (kind, n) in [
("input", usage.input_tokens),
("output", usage.output_tokens),
] {
metrics::histogram!(
TOKEN_USAGE,
"gen_ai.token.type" => kind,
"gen_ai.provider.name" => provider.clone(),
"gen_ai.request.model" => model.clone(),
)
.record(n as f64);
}
}
ModelCallEnd::Failed | ModelCallEnd::Cancelled => {
let error = if matches!(end, ModelCallEnd::Failed) {
"model_error"
} else {
"cancelled"
};
metrics::histogram!(
OPERATION_DURATION,
"gen_ai.operation.name" => "chat",
"gen_ai.provider.name" => provider,
"gen_ai.request.model" => model,
"error.type" => error,
)
.record(duration);
}
}
}
pub(crate) fn tool_call(tool: &str, ok: bool, elapsed: Duration) {
metrics::histogram!(
TOOL_DURATION,
"gen_ai.tool.name" => tool.to_string(),
"ok" => if ok { "true" } else { "false" },
)
.record(elapsed.as_secs_f64());
}
pub(crate) fn run(outcome: &'static str, elapsed: Duration) {
metrics::histogram!(RUN_DURATION, "outcome" => outcome).record(elapsed.as_secs_f64());
}
pub(crate) fn compaction(stage: CompactionStage, before: u32, after: u32) {
metrics::counter!(TOKENS_SAVED, "stage" => format!("{stage:?}"))
.increment(before.saturating_sub(after) as u64);
}