Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion relay-event-normalization/src/eap/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use relay_protocol::Annotated;

use crate::ModelCosts;
use crate::span::ai;
use crate::statsd::Counters;

/// Normalizes AI attributes.
///
Expand Down Expand Up @@ -108,8 +109,59 @@ fn normalize_tokens_per_second(attributes: &mut Attributes, duration: Option<Dur
}
}

/// Maps a span origin to a well-known AI integration name for metrics.
///
/// Origins follow the pattern `auto.<integration>.<source>` or `auto.<category>.<protocol>.<source>`.
/// This function extracts recognized AI integrations for cleaner metric tagging.
fn map_origin_to_integration(origin: Option<&str>) -> &'static str {
match origin {
Some(o) if o.starts_with("auto.ai.openai") => "openai",
Some(o) if o.starts_with("auto.ai.openai_agents") => "openai_agents",
Some(o) if o.starts_with("auto.ai.anthropic") => "anthropic",
Some(o) if o.starts_with("auto.ai.cohere") => "cohere",
Some(o) if o.starts_with("auto.vercelai.") => "vercelai",
Some(o) if o.starts_with("auto.ai.langchain") => "langchain",
Some(o) if o.starts_with("auto.ai.langgraph") => "langgraph",
Some(o) if o.starts_with("auto.ai.google_genai") => "google_genai",
Some(o) if o.starts_with("auto.ai.pydantic_ai") => "pydantic_ai",
Some(o) if o.starts_with("auto.ai.huggingface_hub") => "huggingface_hub",
Some(o) if o.starts_with("auto.ai.litellm") => "litellm",
Some(o) if o.starts_with("auto.ai.mcp") => "mcp",
Some(o) if o.starts_with("auto.ai.mcp_server") => "mcp_server",
Some(o) if o.starts_with("auto.ai.claude_agent_sdk") => "claude_agent_sdk",
Some(o) if o.starts_with("auto.ai.") => "other",
Some(_) => "other",
None => "unknown",
}
}

fn platform_tag(platform: Option<&str>) -> &'static str {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit awkward because we already have such a function in relay-server, but no good way of sharing it :(

Fine to keep here, would consider just starting utils.rs module in the crate to hide it away, but also not a big deal, just would move it at the end of the file, generally we like to keep public interfaces at the top.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this to statsd.rs module because it is a concern of metrics reporting (same for map_origin_to_integration).

match platform {
Some("cocoa") => "cocoa",
Some("csharp") => "csharp",
Some("edge") => "edge",
Some("go") => "go",
Some("java") => "java",
Some("javascript") => "javascript",
Some("julia") => "julia",
Some("native") => "native",
Some("node") => "node",
Some("objc") => "objc",
Some("perl") => "perl",
Some("php") => "php",
Some("python") => "python",
Some("ruby") => "ruby",
Some("swift") => "swift",
Some(_) => "other",
None => "unknown",
}
}

/// Calculates model costs and serializes them into attributes.
fn normalize_ai_costs(attributes: &mut Attributes, model_costs: Option<&ModelCosts>) {
let origin = extract_string_value(attributes, ORIGIN);
let platform = extract_string_value(attributes, PLATFORM);

let model_cost = attributes
.get_value(GEN_AI_REQUEST_MODEL)
.or_else(|| attributes.get_value(GEN_AI_RESPONSE_MODEL))
Expand All @@ -133,16 +185,43 @@ fn normalize_ai_costs(attributes: &mut Attributes, model_costs: Option<&ModelCos
output_reasoning_tokens: get_tokens(GEN_AI_USAGE_OUTPUT_REASONING_TOKENS),
};

let integration = map_origin_to_integration(origin);
let platform = platform_tag(platform);

let Some(costs) = ai::calculate_costs(model_cost, tokens) else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in Slack, the change here would be only for the span streaming pipeline, not the transaction one (the only one in active use atm)

Maybe you can move the metric emission into ai::calculate_costs, then it covers both pipelines at once.

relay_statsd::metric!(
counter(Counters::GenAiCostCalculationResult) += 1,
result = "calculation_none",
integration = integration,
platform = platform,
);

return;
};

// Overwrite all values, the attributes should reflect the values we used to calculate the total.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume removing that comment was a mistake?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, adding it back in

let metric_label = if costs.input > 0.0 && costs.output > 0.0 {
"calculation_positive"
} else if costs.input < 0.0 || costs.output < 0.0 {
"calculation_negative"
} else {
"calculation_zero"
};
relay_statsd::metric!(
counter(Counters::GenAiCostCalculationResult) += 1,
result = metric_label,
integration = integration,
platform = platform,
);

attributes.insert(GEN_AI_COST_INPUT_TOKENS, costs.input);
attributes.insert(GEN_AI_COST_OUTPUT_TOKENS, costs.output);
attributes.insert(GEN_AI_COST_TOTAL_TOKENS, costs.total());
}

fn extract_string_value<'a>(attributes: &'a Attributes, key: &str) -> Option<&'a str> {
attributes.get_value(key).and_then(|v| v.as_str())
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;
Expand Down
14 changes: 13 additions & 1 deletion relay-event-normalization/src/statsd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
use relay_statsd::TimerMetric;
use relay_statsd::{CounterMetric, TimerMetric};

pub enum Counters {
GenAiCostCalculationResult,
}

impl CounterMetric for Counters {
fn name(&self) -> &'static str {
match *self {
Self::GenAiCostCalculationResult => "genai.cost_calculation.result",
}
}
}

pub enum Timers {
/// Measures how log normalization of SQL queries in span description take.
Expand Down
Loading