-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(datadog_agent source): accept LLMObs telemetry at /api/v2/llmobs #25636
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
1b01587
2d0bd16
ca39991
a4f25b6
072fb3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| The `datadog_agent` source now accepts LLM Observability (LLMObs) telemetry at `/api/v2/llmobs`. When `multiple_outputs` is enabled, LLMObs span events are available as log events on the `llmobs` output port. | ||
|
|
||
| authors: ronitanilkumar |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| use bytes::Bytes; | ||
| use http::StatusCode; | ||
| use serde::Deserialize; | ||
| use serde_json::Value; | ||
| use std::sync::Arc; | ||
| use warp::{Filter, filters::BoxedFilter, path, path::FullPath, reply::Response}; | ||
|
|
||
| use super::{ApiKeyQueryParams, DatadogAgentSource, RequestHandler}; | ||
| use crate::{ | ||
| common::http::ErrorMessage, | ||
| event::{Event, LogEvent}, | ||
| internal_events::DatadogAgentJsonParseError, | ||
| }; | ||
|
|
||
| pub(super) fn build_warp_filter( | ||
| handler: RequestHandler, | ||
| source: DatadogAgentSource, | ||
| ) -> BoxedFilter<(Response,)> { | ||
| warp::post() | ||
| .and(path!("api" / "v2" / "llmobs" / ..)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an LLMObs SDK is configured to send through its Datadog Agent, it posts spans to Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a4f25b6. A second warp filter for |
||
| .and(warp::path::full()) | ||
| .and(warp::header::optional::<String>("content-encoding")) | ||
| .and(warp::header::optional::<String>("dd-api-key")) | ||
| .and(warp::query::<ApiKeyQueryParams>()) | ||
| .and(warp::body::bytes()) | ||
| .and_then( | ||
| move |path: FullPath, | ||
| encoding_header: Option<String>, | ||
| api_token: Option<String>, | ||
| query_params: ApiKeyQueryParams, | ||
| body: Bytes| { | ||
| let events = source | ||
| .decode(&encoding_header, body, path.as_str()) | ||
| .and_then(|body| { | ||
| decode_llmobs_body( | ||
| body, | ||
| source.api_key_extractor.extract( | ||
| path.as_str(), | ||
| api_token, | ||
| query_params.dd_api_key, | ||
| ), | ||
| ) | ||
| }); | ||
| handler.clone().handle_request(events, super::LLMOBS) | ||
| }, | ||
| ) | ||
| .boxed() | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct LLMObsEnvelopeItem { | ||
| #[serde(rename = "event_type")] | ||
| _event_type: Option<String>, | ||
| spans: Vec<LLMObsSpan>, | ||
| #[serde(rename = "_dd.tracer_version")] | ||
| dd_tracer_version: Option<String>, | ||
| #[serde(rename = "_dd.scope")] | ||
| _dd_scope: Option<String>, | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct LLMObsSpan { | ||
|
pront marked this conversation as resolved.
pront marked this conversation as resolved.
|
||
| span_id: String, | ||
| trace_id: String, | ||
| parent_id: Option<String>, | ||
| name: Option<String>, | ||
| session_id: Option<String>, | ||
| service: Option<String>, | ||
| start_ns: Option<i64>, | ||
| duration: Option<i64>, | ||
| status: Option<String>, | ||
| status_message: Option<String>, | ||
| meta: Option<Value>, | ||
| metrics: Option<Value>, | ||
| #[serde(default)] | ||
| tags: Vec<String>, | ||
| #[serde(rename = "_dd")] | ||
| dd: Option<Value>, | ||
| } | ||
|
|
||
| pub(crate) fn decode_llmobs_body( | ||
| body: Bytes, | ||
| api_key: Option<Arc<str>>, | ||
| ) -> Result<Vec<Event>, ErrorMessage> { | ||
| let envelope: Vec<LLMObsEnvelopeItem> = serde_json::from_slice(&body).map_err(|error| { | ||
|
pront marked this conversation as resolved.
pront marked this conversation as resolved.
|
||
| emit!(DatadogAgentJsonParseError { error: &error }); | ||
| ErrorMessage::new( | ||
| StatusCode::BAD_REQUEST, | ||
| format!("Error parsing JSON: {error:?}"), | ||
| ) | ||
| })?; | ||
|
|
||
| let events = envelope | ||
| .into_iter() | ||
| .flat_map(|item| { | ||
| let tracer_version = item.dd_tracer_version.clone(); | ||
| item.spans.into_iter().map(move |span| { | ||
| let mut log = LogEvent::default(); | ||
|
pront marked this conversation as resolved.
pront marked this conversation as resolved.
|
||
| log.insert("span_id", span.span_id); | ||
| log.insert("trace_id", span.trace_id); | ||
| if let Some(v) = span.parent_id { | ||
| log.insert("parent_id", v); | ||
| } | ||
| if let Some(v) = span.name { | ||
| log.insert("name", v); | ||
| } | ||
| if let Some(v) = span.session_id { | ||
| log.insert("session_id", v); | ||
| } | ||
| if let Some(v) = span.service { | ||
| log.insert("service", v); | ||
| } | ||
| if let Some(v) = span.start_ns { | ||
| log.insert("start_ns", v); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
LLMObs spans carry their actual event time in nanoseconds via Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a4f25b6. When
pront marked this conversation as resolved.
Outdated
|
||
| if let Some(v) = span.duration { | ||
| log.insert("duration", v); | ||
| } | ||
| if let Some(v) = span.status { | ||
| log.insert("status", v); | ||
| } | ||
| if let Some(v) = span.status_message { | ||
| log.insert("status_message", v); | ||
| } | ||
| if let Some(v) = span.meta { | ||
| log.insert("meta", v); | ||
| } | ||
| if let Some(v) = span.metrics { | ||
| log.insert("metrics", v); | ||
| } | ||
| if !span.tags.is_empty() { | ||
| log.insert("tags", span.tags); | ||
| } | ||
| if let Some(ml_app) = span | ||
| .dd | ||
| .as_ref() | ||
| .and_then(|dd| dd.get("ml_app")) | ||
| .and_then(|v| v.as_str()) | ||
| { | ||
| log.insert("ml_app", ml_app.to_owned()); | ||
|
pront marked this conversation as resolved.
Outdated
pront marked this conversation as resolved.
Outdated
|
||
| } | ||
| if let Some(v) = tracer_version.clone() { | ||
| log.insert("_dd.tracer_version", v); | ||
| } | ||
| Event::Log(log) | ||
| }) | ||
| }) | ||
| .map(|mut event| { | ||
| if let Some(k) = &api_key { | ||
| event.metadata_mut().set_datadog_api_key(Arc::clone(k)); | ||
| } | ||
| event | ||
| }) | ||
| .collect(); | ||
|
|
||
| Ok(events) | ||
|
Comment on lines
+235
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After decoding succeeds, this function returns the events without emitting through Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a4f25b6.
pront marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ mod tests; | |
| pub mod logs; | ||
| pub mod metrics; | ||
| pub mod traces; | ||
| pub mod llmobs; | ||
|
|
||
| #[allow(warnings, clippy::pedantic, clippy::nursery)] | ||
| pub(crate) mod ddmetric_proto { | ||
|
|
@@ -69,6 +70,7 @@ use crate::{ | |
| pub const LOGS: &str = "logs"; | ||
| pub const METRICS: &str = "metrics"; | ||
| pub const TRACES: &str = "traces"; | ||
| pub const LLMOBS: &str = "llmobs"; | ||
|
|
||
| /// Configuration for the `datadog_agent` source. | ||
| #[configurable_component(source( | ||
|
|
@@ -106,6 +108,11 @@ pub struct DatadogAgentConfig { | |
| #[serde(default = "crate::serde::default_false")] | ||
| disable_traces: bool, | ||
|
|
||
| /// If this is set to `true`, LLM Observability events are not accepted by the component. | ||
| #[configurable(metadata(docs::advanced))] | ||
| #[serde(default = "crate::serde::default_false")] | ||
| disable_llmobs: bool, | ||
|
|
||
| /// If this is set to `true`, logs, metrics (beta), and traces (alpha) are sent to different outputs. | ||
| /// | ||
| /// | ||
|
|
@@ -179,6 +186,7 @@ impl GenerateConfig for DatadogAgentConfig { | |
| disable_logs: false, | ||
| disable_metrics: false, | ||
| disable_traces: false, | ||
| disable_llmobs: false, | ||
| multiple_outputs: false, | ||
| parse_ddtags: false, | ||
| split_metric_namespace: true, | ||
|
|
@@ -322,6 +330,7 @@ impl SourceConfig for DatadogAgentConfig { | |
| .with_standard_vector_source_metadata(); | ||
|
|
||
| let mut output = Vec::with_capacity(1); | ||
| let llmobs_definition = definition.clone(); | ||
|
|
||
| if self.multiple_outputs { | ||
| if !self.disable_logs { | ||
|
|
@@ -333,6 +342,9 @@ impl SourceConfig for DatadogAgentConfig { | |
| if !self.disable_traces { | ||
| output.push(SourceOutput::new_traces().with_port(TRACES)) | ||
| } | ||
| if !self.disable_llmobs { | ||
| output.push(SourceOutput::new_maybe_logs(DataType::Log, llmobs_definition).with_port(LLMOBS)) | ||
|
pront marked this conversation as resolved.
Outdated
pront marked this conversation as resolved.
Outdated
|
||
| } | ||
| } else { | ||
| output.push(SourceOutput::new_maybe_logs( | ||
| DataType::all_bits(), | ||
|
Comment on lines
387
to
388
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
|
|
@@ -459,12 +471,19 @@ impl DatadogAgentSource { | |
| } | ||
|
|
||
| if !config.disable_metrics { | ||
| let metrics_filter = metrics::build_warp_filter(handler, self.clone()); | ||
| let metrics_filter = metrics::build_warp_filter(handler.clone(), self.clone()); | ||
| filters = filters | ||
| .map(|f| f.or(metrics_filter.clone()).unify().boxed()) | ||
| .or(Some(metrics_filter)); | ||
| } | ||
|
|
||
| if !config.disable_llmobs { | ||
| let llmobs_filter = llmobs::build_warp_filter(handler.clone(), self.clone()); | ||
| filters = filters | ||
| .map(|f| f.or(llmobs_filter.clone()).unify().boxed()) | ||
| .or(Some(llmobs_filter)); | ||
| } | ||
|
|
||
| filters.ok_or_else(|| "At least one of the supported data type shall be enabled".into()) | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.