use std::borrow::Cow;
use std::collections::HashMap;
use crate::canonical;
use crate::engine::{LogSignal, Matchable, MetricSignal, TraceSignal, Transformable};
use crate::field::{LogFieldSelector, MetricFieldSelector, TraceFieldSelector};
use crate::proto::tero::policy::v1::{
AggregationTemporality, LogField, MetricField, MetricType, SpanKind, SpanStatusCode, TraceField,
};
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Bytes(Vec<u8>),
}
impl Value {
fn as_str(&self) -> Option<&str> {
match self {
Value::String(s) => Some(s.as_str()),
_ => None,
}
}
}
pub type Attrs = HashMap<String, Value>;
fn get_attr<'a>(attrs: &'a Attrs, path: &[String]) -> Option<Cow<'a, str>> {
let key = path.first()?;
attrs.get(key)?.as_str().map(Cow::Borrowed)
}
fn attr_exists(attrs: &Attrs, path: &[String]) -> bool {
path.first().map(|k| attrs.contains_key(k)).unwrap_or(false)
}
fn set_attr(attrs: &mut Attrs, path: &[String], value: &str) {
if let Some(key) = path.first() {
attrs.insert(key.clone(), Value::String(value.to_string()));
}
}
fn delete_attr(attrs: &mut Attrs, path: &[String]) -> bool {
path.first().and_then(|k| attrs.remove(k)).is_some()
}
#[derive(Debug, Default, Clone)]
pub struct LogRecord {
pub body: Option<String>,
pub severity_text: Option<String>,
pub trace_id: Option<String>,
pub span_id: Option<String>,
pub event_name: Option<String>,
pub resource_schema_url: Option<String>,
pub scope_schema_url: Option<String>,
pub log_attrs: Attrs,
pub resource_attrs: Attrs,
pub scope_attrs: Attrs,
}
impl LogRecord {
pub fn new() -> Self {
Self::default()
}
pub fn set_log_attr(&mut self, key: impl Into<String>, value: Value) {
self.log_attrs.insert(key.into(), value);
}
pub fn set_resource_attr(&mut self, key: impl Into<String>, value: Value) {
self.resource_attrs.insert(key.into(), value);
}
pub fn set_scope_attr(&mut self, key: impl Into<String>, value: Value) {
self.scope_attrs.insert(key.into(), value);
}
}
impl Matchable for LogRecord {
type Signal = LogSignal;
fn get_field(&self, field: &LogFieldSelector) -> Option<Cow<'_, str>> {
match field {
LogFieldSelector::Simple(f) => match f {
LogField::Body => self.body.as_deref().map(Cow::Borrowed),
LogField::SeverityText => self.severity_text.as_deref().map(Cow::Borrowed),
LogField::TraceId => self.trace_id.as_deref().map(Cow::Borrowed),
LogField::SpanId => self.span_id.as_deref().map(Cow::Borrowed),
LogField::EventName => self.event_name.as_deref().map(Cow::Borrowed),
LogField::ResourceSchemaUrl => {
self.resource_schema_url.as_deref().map(Cow::Borrowed)
}
LogField::ScopeSchemaUrl => self.scope_schema_url.as_deref().map(Cow::Borrowed),
_ => None,
},
LogFieldSelector::LogAttribute(path) => get_attr(&self.log_attrs, path),
LogFieldSelector::ResourceAttribute(path) => get_attr(&self.resource_attrs, path),
LogFieldSelector::ScopeAttribute(path) => get_attr(&self.scope_attrs, path),
}
}
fn field_exists(&self, field: &LogFieldSelector) -> bool {
match field {
LogFieldSelector::Simple(_) => self.get_field(field).is_some(),
LogFieldSelector::LogAttribute(path) => attr_exists(&self.log_attrs, path),
LogFieldSelector::ResourceAttribute(path) => attr_exists(&self.resource_attrs, path),
LogFieldSelector::ScopeAttribute(path) => attr_exists(&self.scope_attrs, path),
}
}
}
impl Transformable for LogRecord {
fn set_field(&mut self, field: &LogFieldSelector, value: &str) {
match field {
LogFieldSelector::Simple(f) => match f {
LogField::Body => self.body = Some(value.to_string()),
LogField::SeverityText => self.severity_text = Some(value.to_string()),
LogField::TraceId => self.trace_id = Some(value.to_string()),
LogField::SpanId => self.span_id = Some(value.to_string()),
LogField::EventName => self.event_name = Some(value.to_string()),
LogField::ResourceSchemaUrl => self.resource_schema_url = Some(value.to_string()),
LogField::ScopeSchemaUrl => self.scope_schema_url = Some(value.to_string()),
_ => {}
},
LogFieldSelector::LogAttribute(path) => set_attr(&mut self.log_attrs, path, value),
LogFieldSelector::ResourceAttribute(path) => {
set_attr(&mut self.resource_attrs, path, value)
}
LogFieldSelector::ScopeAttribute(path) => set_attr(&mut self.scope_attrs, path, value),
}
}
fn delete_field(&mut self, field: &LogFieldSelector) -> bool {
match field {
LogFieldSelector::Simple(f) => match f {
LogField::Body => self.body.take().is_some(),
LogField::SeverityText => self.severity_text.take().is_some(),
LogField::TraceId => self.trace_id.take().is_some(),
LogField::SpanId => self.span_id.take().is_some(),
LogField::EventName => self.event_name.take().is_some(),
LogField::ResourceSchemaUrl => self.resource_schema_url.take().is_some(),
LogField::ScopeSchemaUrl => self.scope_schema_url.take().is_some(),
_ => false,
},
LogFieldSelector::LogAttribute(path) => delete_attr(&mut self.log_attrs, path),
LogFieldSelector::ResourceAttribute(path) => {
delete_attr(&mut self.resource_attrs, path)
}
LogFieldSelector::ScopeAttribute(path) => delete_attr(&mut self.scope_attrs, path),
}
}
fn move_field(&mut self, from: &LogFieldSelector, to: &LogFieldSelector) {
let value = match from {
LogFieldSelector::Simple(f) => match f {
LogField::Body => self.body.take().map(Value::String),
LogField::SeverityText => self.severity_text.take().map(Value::String),
LogField::TraceId => self.trace_id.take().map(Value::String),
LogField::SpanId => self.span_id.take().map(Value::String),
LogField::EventName => self.event_name.take().map(Value::String),
LogField::ResourceSchemaUrl => self.resource_schema_url.take().map(Value::String),
LogField::ScopeSchemaUrl => self.scope_schema_url.take().map(Value::String),
_ => None,
},
LogFieldSelector::LogAttribute(path) => {
path.first().and_then(|k| self.log_attrs.remove(k))
}
LogFieldSelector::ResourceAttribute(path) => {
path.first().and_then(|k| self.resource_attrs.remove(k))
}
LogFieldSelector::ScopeAttribute(path) => {
path.first().and_then(|k| self.scope_attrs.remove(k))
}
};
let Some(v) = value else { return };
match to {
LogFieldSelector::LogAttribute(path) => {
if let Some(k) = path.first() {
self.log_attrs.insert(k.clone(), v);
}
}
LogFieldSelector::ResourceAttribute(path) => {
if let Some(k) = path.first() {
self.resource_attrs.insert(k.clone(), v);
}
}
LogFieldSelector::ScopeAttribute(path) => {
if let Some(k) = path.first() {
self.scope_attrs.insert(k.clone(), v);
}
}
_ => {}
}
}
}
#[derive(Debug, Default, Clone)]
pub struct MetricRecord {
pub name: Option<String>,
pub description: Option<String>,
pub unit: Option<String>,
pub resource_schema_url: Option<String>,
pub scope_schema_url: Option<String>,
pub scope_name: Option<String>,
pub scope_version: Option<String>,
pub metric_type: Option<MetricType>,
pub temporality: Option<AggregationTemporality>,
pub datapoint_attrs: Attrs,
pub resource_attrs: Attrs,
pub scope_attrs: Attrs,
}
impl MetricRecord {
pub fn new() -> Self {
Self::default()
}
}
impl Matchable for MetricRecord {
type Signal = MetricSignal;
fn get_field(&self, field: &MetricFieldSelector) -> Option<Cow<'_, str>> {
match field {
MetricFieldSelector::Simple(f) => match f {
MetricField::Name => self.name.as_deref().map(Cow::Borrowed),
MetricField::Description => self.description.as_deref().map(Cow::Borrowed),
MetricField::Unit => self.unit.as_deref().map(Cow::Borrowed),
MetricField::ResourceSchemaUrl => {
self.resource_schema_url.as_deref().map(Cow::Borrowed)
}
MetricField::ScopeSchemaUrl => self.scope_schema_url.as_deref().map(Cow::Borrowed),
MetricField::ScopeName => self.scope_name.as_deref().map(Cow::Borrowed),
MetricField::ScopeVersion => self.scope_version.as_deref().map(Cow::Borrowed),
_ => None,
},
MetricFieldSelector::Type => self
.metric_type
.map(|mt| Cow::Borrowed(canonical::metric_type_str(mt))),
MetricFieldSelector::Temporality => self
.temporality
.map(|at| Cow::Borrowed(canonical::aggregation_temporality_str(at))),
MetricFieldSelector::DatapointAttribute(path) => get_attr(&self.datapoint_attrs, path),
MetricFieldSelector::ResourceAttribute(path) => get_attr(&self.resource_attrs, path),
MetricFieldSelector::ScopeAttribute(path) => get_attr(&self.scope_attrs, path),
}
}
fn field_exists(&self, field: &MetricFieldSelector) -> bool {
match field {
MetricFieldSelector::Type => self.metric_type.is_some(),
MetricFieldSelector::Temporality => self.temporality.is_some(),
MetricFieldSelector::DatapointAttribute(path) => {
attr_exists(&self.datapoint_attrs, path)
}
MetricFieldSelector::ResourceAttribute(path) => attr_exists(&self.resource_attrs, path),
MetricFieldSelector::ScopeAttribute(path) => attr_exists(&self.scope_attrs, path),
_ => self.get_field(field).is_some(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct SpanRecord {
pub name: Option<String>,
pub trace_id: Option<String>,
pub trace_id_bytes: Option<Vec<u8>>,
pub span_id: Option<String>,
pub span_id_bytes: Option<Vec<u8>>,
pub parent_span_id: Option<String>,
pub parent_span_id_bytes: Option<Vec<u8>>,
pub trace_state: Option<String>,
pub resource_schema_url: Option<String>,
pub scope_schema_url: Option<String>,
pub scope_name: Option<String>,
pub scope_version: Option<String>,
pub span_kind: Option<SpanKind>,
pub span_status: Option<SpanStatusCode>,
pub span_attrs: Attrs,
pub resource_attrs: Attrs,
pub scope_attrs: Attrs,
pub sampling_threshold: Option<String>,
}
impl SpanRecord {
pub fn new() -> Self {
Self::default()
}
}
impl Matchable for SpanRecord {
type Signal = TraceSignal;
fn get_field(&self, field: &TraceFieldSelector) -> Option<Cow<'_, str>> {
match field {
TraceFieldSelector::Simple(f) => match f {
TraceField::Name => self.name.as_deref().map(Cow::Borrowed),
TraceField::TraceId => self.trace_id.as_deref().map(Cow::Borrowed),
TraceField::SpanId => self.span_id.as_deref().map(Cow::Borrowed),
TraceField::ParentSpanId => self.parent_span_id.as_deref().map(Cow::Borrowed),
TraceField::TraceState => self.trace_state.as_deref().map(Cow::Borrowed),
TraceField::ResourceSchemaUrl => {
self.resource_schema_url.as_deref().map(Cow::Borrowed)
}
TraceField::ScopeSchemaUrl => self.scope_schema_url.as_deref().map(Cow::Borrowed),
TraceField::ScopeName => self.scope_name.as_deref().map(Cow::Borrowed),
TraceField::ScopeVersion => self.scope_version.as_deref().map(Cow::Borrowed),
_ => None,
},
TraceFieldSelector::SpanKind => self
.span_kind
.map(|k| Cow::Borrowed(canonical::span_kind_str(k))),
TraceFieldSelector::SpanStatus => {
let sc = self.span_status.unwrap_or(SpanStatusCode::Unspecified);
Some(Cow::Borrowed(canonical::span_status_code_str(sc)))
}
TraceFieldSelector::SpanAttribute(path) => get_attr(&self.span_attrs, path),
TraceFieldSelector::ResourceAttribute(path) => get_attr(&self.resource_attrs, path),
TraceFieldSelector::ScopeAttribute(path) => get_attr(&self.scope_attrs, path),
TraceFieldSelector::EventName | TraceFieldSelector::EventAttribute(_) => None,
TraceFieldSelector::LinkTraceId => None,
TraceFieldSelector::SamplingThreshold => {
self.sampling_threshold.as_deref().map(Cow::Borrowed)
}
}
}
fn field_exists(&self, field: &TraceFieldSelector) -> bool {
match field {
TraceFieldSelector::SpanKind => self.span_kind.is_some(),
TraceFieldSelector::SpanStatus => true,
TraceFieldSelector::SpanAttribute(path) => attr_exists(&self.span_attrs, path),
TraceFieldSelector::ResourceAttribute(path) => attr_exists(&self.resource_attrs, path),
TraceFieldSelector::ScopeAttribute(path) => attr_exists(&self.scope_attrs, path),
_ => self.get_field(field).is_some(),
}
}
fn get_typed_value(&self, field: &TraceFieldSelector) -> Option<crate::engine::TypedValue<'_>> {
use crate::engine::TypedValue;
match field {
TraceFieldSelector::Simple(TraceField::TraceId) => self
.trace_id_bytes
.as_deref()
.map(TypedValue::Bytes)
.or_else(|| {
self.trace_id
.as_deref()
.map(|s| TypedValue::String(Cow::Borrowed(s)))
}),
TraceFieldSelector::Simple(TraceField::SpanId) => self
.span_id_bytes
.as_deref()
.map(TypedValue::Bytes)
.or_else(|| {
self.span_id
.as_deref()
.map(|s| TypedValue::String(Cow::Borrowed(s)))
}),
TraceFieldSelector::Simple(TraceField::ParentSpanId) => self
.parent_span_id_bytes
.as_deref()
.map(TypedValue::Bytes)
.or_else(|| {
self.parent_span_id
.as_deref()
.map(|s| TypedValue::String(Cow::Borrowed(s)))
}),
_ => self.get_field(field).map(TypedValue::String),
}
}
}
impl Transformable for SpanRecord {
fn set_field(&mut self, field: &TraceFieldSelector, value: &str) {
match field {
TraceFieldSelector::Simple(f) => match f {
TraceField::Name => self.name = Some(value.to_string()),
TraceField::TraceId => self.trace_id = Some(value.to_string()),
TraceField::SpanId => self.span_id = Some(value.to_string()),
TraceField::ParentSpanId => self.parent_span_id = Some(value.to_string()),
TraceField::TraceState => self.trace_state = Some(value.to_string()),
TraceField::ResourceSchemaUrl => self.resource_schema_url = Some(value.to_string()),
TraceField::ScopeSchemaUrl => self.scope_schema_url = Some(value.to_string()),
TraceField::ScopeName => self.scope_name = Some(value.to_string()),
TraceField::ScopeVersion => self.scope_version = Some(value.to_string()),
_ => {}
},
TraceFieldSelector::SpanAttribute(path) => set_attr(&mut self.span_attrs, path, value),
TraceFieldSelector::ResourceAttribute(path) => {
set_attr(&mut self.resource_attrs, path, value)
}
TraceFieldSelector::ScopeAttribute(path) => {
set_attr(&mut self.scope_attrs, path, value)
}
TraceFieldSelector::SamplingThreshold => {
self.sampling_threshold = Some(value.to_string());
}
_ => {}
}
}
fn delete_field(&mut self, field: &TraceFieldSelector) -> bool {
match field {
TraceFieldSelector::Simple(f) => match f {
TraceField::Name => self.name.take().is_some(),
TraceField::TraceId => self.trace_id.take().is_some(),
TraceField::SpanId => self.span_id.take().is_some(),
TraceField::ParentSpanId => self.parent_span_id.take().is_some(),
TraceField::TraceState => self.trace_state.take().is_some(),
TraceField::ResourceSchemaUrl => self.resource_schema_url.take().is_some(),
TraceField::ScopeSchemaUrl => self.scope_schema_url.take().is_some(),
TraceField::ScopeName => self.scope_name.take().is_some(),
TraceField::ScopeVersion => self.scope_version.take().is_some(),
_ => false,
},
TraceFieldSelector::SpanAttribute(path) => delete_attr(&mut self.span_attrs, path),
TraceFieldSelector::ResourceAttribute(path) => {
delete_attr(&mut self.resource_attrs, path)
}
TraceFieldSelector::ScopeAttribute(path) => delete_attr(&mut self.scope_attrs, path),
_ => false,
}
}
fn move_field(&mut self, from: &TraceFieldSelector, to: &TraceFieldSelector) {
let value = match from {
TraceFieldSelector::SpanAttribute(path) => {
path.first().and_then(|k| self.span_attrs.remove(k))
}
TraceFieldSelector::ResourceAttribute(path) => {
path.first().and_then(|k| self.resource_attrs.remove(k))
}
TraceFieldSelector::ScopeAttribute(path) => {
path.first().and_then(|k| self.scope_attrs.remove(k))
}
_ => None,
};
let Some(v) = value else { return };
match to {
TraceFieldSelector::SpanAttribute(path) => {
if let Some(k) = path.first() {
self.span_attrs.insert(k.clone(), v);
}
}
TraceFieldSelector::ResourceAttribute(path) => {
if let Some(k) = path.first() {
self.resource_attrs.insert(k.clone(), v);
}
}
TraceFieldSelector::ScopeAttribute(path) => {
if let Some(k) = path.first() {
self.scope_attrs.insert(k.clone(), v);
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::field::LogFieldSelector;
use crate::proto::tero::policy::v1::LogField;
#[test]
fn log_record_string_value_is_matchable() {
let mut rec = LogRecord::new();
rec.body = Some("hello".to_string());
rec.set_log_attr("k", Value::String("v".to_string()));
assert_eq!(
rec.get_field(&LogFieldSelector::Simple(LogField::Body)),
Some(Cow::Borrowed("hello"))
);
assert_eq!(
rec.get_field(&LogFieldSelector::LogAttribute(vec!["k".to_string()])),
Some(Cow::Borrowed("v"))
);
}
#[test]
fn log_record_non_string_attr_is_present_but_not_matchable() {
let mut rec = LogRecord::new();
rec.set_log_attr("count", Value::Int(42));
assert_eq!(
rec.get_field(&LogFieldSelector::LogAttribute(vec!["count".to_string()])),
None
);
assert!(rec.field_exists(&LogFieldSelector::LogAttribute(vec!["count".to_string()])));
}
#[test]
fn metric_record_type_and_temporality_use_canonical_strings() {
use crate::field::MetricFieldSelector;
let mut rec = MetricRecord::new();
rec.metric_type = Some(MetricType::Sum);
rec.temporality = Some(AggregationTemporality::Delta);
assert_eq!(
rec.get_field(&MetricFieldSelector::Type),
Some(Cow::Borrowed("METRIC_TYPE_SUM"))
);
assert_eq!(
rec.get_field(&MetricFieldSelector::Temporality),
Some(Cow::Borrowed("AGGREGATION_TEMPORALITY_DELTA"))
);
}
#[test]
fn span_record_sampling_threshold_set_by_set_field() {
use crate::field::TraceFieldSelector;
let mut span = SpanRecord::new();
span.set_field(&TraceFieldSelector::SamplingThreshold, "8");
assert_eq!(span.sampling_threshold, Some("8".to_string()));
}
#[test]
fn span_record_span_status_is_always_present() {
use crate::field::TraceFieldSelector;
let span = SpanRecord::new(); assert!(span.field_exists(&TraceFieldSelector::SpanStatus));
assert_eq!(
span.get_field(&TraceFieldSelector::SpanStatus),
Some(Cow::Borrowed("SPAN_STATUS_CODE_UNSPECIFIED"))
);
let mut span2 = SpanRecord::new();
span2.span_status = Some(SpanStatusCode::Error);
assert_eq!(
span2.get_field(&TraceFieldSelector::SpanStatus),
Some(Cow::Borrowed("SPAN_STATUS_CODE_ERROR"))
);
}
}