1use std::borrow::Cow;
14use std::collections::HashMap;
15use tracing::debug;
16
17use opentelemetry_proto::tonic::common::v1::{
18 any_value::Value as ProtoAnyValue, InstrumentationScope, KeyValue,
19};
20use opentelemetry_proto::tonic::resource::v1::Resource;
21use opentelemetry_proto::tonic::trace::v1::Span;
22use otap_df_pdata_views::views::common::{
23 AnyValueView, AttributeView, InstrumentationScopeView, ValueType,
24};
25use otap_df_pdata_views::views::logs::LogRecordView;
26use otap_df_pdata_views::views::resource::ResourceView;
27
28#[derive(Clone, Debug)]
30pub enum LogsEventNameRoutingKey {
31 EventName,
32 ResourceAttribute(String),
33 ScopeAttribute(String),
34 LogRecordAttribute(String),
35}
36
37impl LogsEventNameRoutingKey {
38 fn attribute_key(&self) -> Option<&str> {
40 match self {
41 LogsEventNameRoutingKey::EventName => None,
42 LogsEventNameRoutingKey::ResourceAttribute(key)
43 | LogsEventNameRoutingKey::ScopeAttribute(key)
44 | LogsEventNameRoutingKey::LogRecordAttribute(key) => Some(key.as_str()),
45 }
46 }
47}
48
49#[derive(Clone, Debug)]
52pub struct LogsEventNameMapping {
53 pub routing_key: LogsEventNameRoutingKey,
54 pub events: HashMap<String, Option<String>>,
55}
56
57impl LogsEventNameMapping {
58 pub(crate) fn validate(&self) -> Result<(), String> {
61 validate_event_mapping(
62 "LogsConfig.event_name_mapping",
63 self.routing_key.attribute_key(),
64 &self.events,
65 )
66 }
67}
68
69#[derive(Clone, Debug)]
71pub enum SpanEventNameRoutingKey {
72 ResourceAttribute(String),
73 ScopeAttribute(String),
74 SpanAttribute(String),
75}
76
77impl SpanEventNameRoutingKey {
78 fn attribute_key(&self) -> Option<&str> {
80 match self {
81 SpanEventNameRoutingKey::ResourceAttribute(key)
82 | SpanEventNameRoutingKey::ScopeAttribute(key)
83 | SpanEventNameRoutingKey::SpanAttribute(key) => Some(key.as_str()),
84 }
85 }
86}
87
88#[derive(Clone, Debug)]
91pub struct SpanEventNameMapping {
92 pub routing_key: SpanEventNameRoutingKey,
93 pub events: HashMap<String, Option<String>>,
94}
95
96impl SpanEventNameMapping {
97 pub(crate) fn validate(&self) -> Result<(), String> {
100 validate_event_mapping(
101 "TracesConfig.event_name_mapping",
102 self.routing_key.attribute_key(),
103 &self.events,
104 )
105 }
106}
107
108fn validate_event_mapping(
111 ctx: &str,
112 routing_key_name: Option<&str>,
113 events: &HashMap<String, Option<String>>,
114) -> Result<(), String> {
115 if events.is_empty() {
116 return Err(format!(
117 "{ctx}.events must be non-empty when routing is configured"
118 ));
119 }
120 if let Some(name) = routing_key_name {
121 if name.trim().is_empty() {
122 return Err(format!(
123 "{ctx}.routing_key attribute name must not be blank"
124 ));
125 }
126 }
127 if events.keys().any(|key| key.trim().is_empty()) {
128 return Err(format!("{ctx}.events source keys must not be blank"));
129 }
130 Ok(())
131}
132
133fn non_blank_utf8(bytes: &[u8]) -> Option<&str> {
134 let s = std::str::from_utf8(bytes).ok()?;
135 (!s.trim().is_empty()).then_some(s)
136}
137
138pub(crate) fn normalized_event_name(record: &impl LogRecordView) -> Option<&str> {
139 record.event_name().and_then(non_blank_utf8)
140}
141
142pub(crate) const SCOPE_NAME_ROUTING_KEY: &str = "scope.name";
144pub(crate) const SCOPE_VERSION_ROUTING_KEY: &str = "scope.version";
146
147pub(crate) fn resolve_mapped_destination<'a>(
158 events: &'a HashMap<String, Option<String>>,
159 source_value: &str,
160) -> Option<Cow<'a, str>> {
161 let mapped = events.get(source_value)?;
162 match mapped.as_deref() {
163 Some(destination) if !destination.trim().is_empty() => Some(Cow::Borrowed(destination)),
164 _ => Some(Cow::Owned(source_value.to_string())),
165 }
166}
167
168pub(crate) enum RoutingPrimitive<'a> {
171 Str(&'a str),
172 Int(i64),
173 Double(f64),
174 Bool(bool),
175}
176
177pub(crate) fn stringify_routing_primitive(value: RoutingPrimitive<'_>) -> Option<Cow<'_, str>> {
189 match value {
190 RoutingPrimitive::Str(s) => (!s.trim().is_empty()).then_some(Cow::Borrowed(s)),
191 RoutingPrimitive::Int(v) => Some(Cow::Owned(v.to_string())),
192 RoutingPrimitive::Double(v) => Some(Cow::Owned(v.to_string())),
193 RoutingPrimitive::Bool(v) => Some(Cow::Owned(v.to_string())),
194 }
195}
196
197fn routing_value_from_attributes<'a, A, I>(attributes: I, key: &str) -> Option<String>
202where
203 A: AttributeView + 'a,
204 I: IntoIterator<Item = A>,
205{
206 for attr in attributes {
207 let Ok(attr_key) = std::str::from_utf8(attr.key()) else {
208 continue;
209 };
210 if attr_key != key {
211 continue;
212 }
213 let value = attr.value()?;
214
215 return match value.value_type() {
220 ValueType::String => value
221 .as_string()
222 .and_then(|b| std::str::from_utf8(b).ok())
223 .and_then(|s| stringify_routing_primitive(RoutingPrimitive::Str(s)))
224 .map(Cow::into_owned),
225 ValueType::Int64 => value
226 .as_int64()
227 .and_then(|v| stringify_routing_primitive(RoutingPrimitive::Int(v)))
228 .map(Cow::into_owned),
229 ValueType::Double => value
230 .as_double()
231 .and_then(|v| stringify_routing_primitive(RoutingPrimitive::Double(v)))
232 .map(Cow::into_owned),
233 ValueType::Bool => value
234 .as_bool()
235 .and_then(|v| stringify_routing_primitive(RoutingPrimitive::Bool(v)))
236 .map(Cow::into_owned),
237 _ => None,
238 };
239 }
240 None
241}
242
243fn routing_value_from_scope<SV>(scope: &SV, key: &str) -> Option<String>
244where
245 SV: InstrumentationScopeView,
246{
247 match key {
248 SCOPE_NAME_ROUTING_KEY => scope
249 .name()
250 .and_then(|b| std::str::from_utf8(b).ok())
251 .and_then(|s| stringify_routing_primitive(RoutingPrimitive::Str(s)))
252 .map(Cow::into_owned),
253 SCOPE_VERSION_ROUTING_KEY => scope
254 .version()
255 .and_then(|b| std::str::from_utf8(b).ok())
256 .and_then(|s| stringify_routing_primitive(RoutingPrimitive::Str(s)))
257 .map(Cow::into_owned),
258 _ => routing_value_from_attributes(scope.attributes(), key),
259 }
260}
261
262pub(crate) enum LogScopeRouting<'a> {
264 None,
266 Fixed(String),
269 PerRecord(&'a LogsEventNameMapping),
271}
272
273fn resolve_log_event_name_from_value<'a>(
276 mapping: &'a LogsEventNameMapping,
277 source_value: Option<&str>,
278 table_name: &'a str,
279) -> Cow<'a, str> {
280 if let Some(source_value) = source_value {
281 if let Some(mapped_event_name) = resolve_mapped_destination(&mapping.events, source_value) {
282 debug!(
283 name: "otlp_encoder.log_event_routing",
284 target: "geneva-uploader",
285 source_value = %source_value,
286 routed_event_name = %mapped_event_name,
287 "Resolved log event routing from mapping"
288 );
289 return mapped_event_name;
290 }
291
292 debug!(
293 name: "otlp_encoder.log_event_routing",
294 target: "geneva-uploader",
295 source_value = %source_value,
296 fallback_event_name = %table_name,
297 "No mapping entry for routing source; using fallback event name"
298 );
299 return Cow::Borrowed(table_name);
300 }
301
302 debug!(
303 name: "otlp_encoder.log_event_routing",
304 target: "geneva-uploader",
305 fallback_event_name = %table_name,
306 "Routing source value not found on log record; using fallback event name"
307 );
308 Cow::Borrowed(table_name)
309}
310
311pub(crate) fn resolve_log_scope_routing<'a, RV, SV>(
317 resource: Option<&RV>,
318 scope: Option<&SV>,
319 table_name: &str,
320 event_name_mapping: Option<&'a LogsEventNameMapping>,
321) -> LogScopeRouting<'a>
322where
323 RV: ResourceView,
324 SV: InstrumentationScopeView,
325{
326 let Some(mapping) = event_name_mapping else {
327 return LogScopeRouting::None;
328 };
329
330 match &mapping.routing_key {
331 LogsEventNameRoutingKey::ResourceAttribute(key) => {
332 let value = resource
333 .and_then(|res| routing_value_from_attributes(res.attributes(), key.as_str()));
334 LogScopeRouting::Fixed(
335 resolve_log_event_name_from_value(mapping, value.as_deref(), table_name)
336 .into_owned(),
337 )
338 }
339 LogsEventNameRoutingKey::ScopeAttribute(key) => {
340 let value = scope.and_then(|scope| routing_value_from_scope(scope, key.as_str()));
341 LogScopeRouting::Fixed(
342 resolve_log_event_name_from_value(mapping, value.as_deref(), table_name)
343 .into_owned(),
344 )
345 }
346 LogsEventNameRoutingKey::EventName | LogsEventNameRoutingKey::LogRecordAttribute(_) => {
347 LogScopeRouting::PerRecord(mapping)
348 }
349 }
350}
351
352pub(crate) fn resolve_log_record_routing_event_name<'a, R>(
355 record: &R,
356 mapping: &'a LogsEventNameMapping,
357 table_name: &'a str,
358) -> Cow<'a, str>
359where
360 R: LogRecordView,
361{
362 let routing_value = match &mapping.routing_key {
363 LogsEventNameRoutingKey::EventName => normalized_event_name(record).map(str::to_owned),
364 LogsEventNameRoutingKey::LogRecordAttribute(key) => {
365 routing_value_from_attributes(record.attributes(), key.as_str())
366 }
367 LogsEventNameRoutingKey::ResourceAttribute(_)
369 | LogsEventNameRoutingKey::ScopeAttribute(_) => None,
370 };
371
372 resolve_log_event_name_from_value(mapping, routing_value.as_deref(), table_name)
373}
374
375pub(crate) fn span_routing_value_from_attributes<'a>(
380 attributes: &'a [KeyValue],
381 key: &str,
382) -> Option<Cow<'a, str>> {
383 for attr in attributes {
384 if attr.key != key {
385 continue;
386 }
387 let value = attr.value.as_ref()?.value.as_ref()?;
388 return match value {
389 ProtoAnyValue::StringValue(value) => {
390 stringify_routing_primitive(RoutingPrimitive::Str(value))
391 }
392 ProtoAnyValue::IntValue(value) => {
393 stringify_routing_primitive(RoutingPrimitive::Int(*value))
394 }
395 ProtoAnyValue::DoubleValue(value) => {
396 stringify_routing_primitive(RoutingPrimitive::Double(*value))
397 }
398 ProtoAnyValue::BoolValue(value) => {
399 stringify_routing_primitive(RoutingPrimitive::Bool(*value))
400 }
401 _ => None,
402 };
403 }
404 None
405}
406
407pub(crate) fn span_routing_value_from_scope<'a>(
408 scope: Option<&'a InstrumentationScope>,
409 key: &str,
410) -> Option<Cow<'a, str>> {
411 let scope = scope?;
412 match key {
413 SCOPE_NAME_ROUTING_KEY => stringify_routing_primitive(RoutingPrimitive::Str(&scope.name)),
414 SCOPE_VERSION_ROUTING_KEY => {
415 stringify_routing_primitive(RoutingPrimitive::Str(&scope.version))
416 }
417 _ => span_routing_value_from_attributes(&scope.attributes, key),
418 }
419}
420
421pub(crate) fn span_routing_value_from_resource<'a>(
422 resource: Option<&'a Resource>,
423 key: &str,
424) -> Option<Cow<'a, str>> {
425 resource.and_then(|resource| span_routing_value_from_attributes(&resource.attributes, key))
426}
427
428pub(crate) enum SpanScopeRouting<'a> {
430 Fixed(String),
433 PerSpan {
436 mapping: &'a SpanEventNameMapping,
437 key: &'a str,
438 },
439}
440
441pub(crate) fn resolve_span_scope_routing<'a>(
447 resource: Option<&Resource>,
448 scope: Option<&InstrumentationScope>,
449 table_name: &str,
450 event_name_mapping: Option<&'a SpanEventNameMapping>,
451) -> SpanScopeRouting<'a> {
452 let Some(mapping) = event_name_mapping else {
453 return SpanScopeRouting::Fixed(table_name.to_string());
454 };
455
456 match &mapping.routing_key {
457 SpanEventNameRoutingKey::ResourceAttribute(key) => SpanScopeRouting::Fixed(
458 span_routing_value_from_resource(resource, key)
459 .and_then(|value| resolve_mapped_destination(&mapping.events, &value))
460 .map(Cow::into_owned)
461 .unwrap_or_else(|| table_name.to_string()),
462 ),
463 SpanEventNameRoutingKey::ScopeAttribute(key) => SpanScopeRouting::Fixed(
464 span_routing_value_from_scope(scope, key)
465 .and_then(|value| resolve_mapped_destination(&mapping.events, &value))
466 .map(Cow::into_owned)
467 .unwrap_or_else(|| table_name.to_string()),
468 ),
469 SpanEventNameRoutingKey::SpanAttribute(key) => SpanScopeRouting::PerSpan { mapping, key },
470 }
471}
472
473pub(crate) fn resolve_span_event_name_in_scope<'a>(
479 scope_routing: &'a SpanScopeRouting<'a>,
480 span: &Span,
481 table_name: &'a str,
482) -> Cow<'a, str> {
483 match scope_routing {
484 SpanScopeRouting::Fixed(name) => Cow::Borrowed(name.as_str()),
485 SpanScopeRouting::PerSpan { mapping, key } => {
486 match span_routing_value_from_attributes(&span.attributes, key)
487 .and_then(|value| resolve_mapped_destination(&mapping.events, &value))
488 {
489 Some(name) => name,
490 None => Cow::Borrowed(table_name),
491 }
492 }
493 }
494}
495
496#[cfg(test)]
499pub(crate) fn resolve_span_event_name(
500 resource: Option<&Resource>,
501 scope: Option<&InstrumentationScope>,
502 span: &Span,
503 table_name: &str,
504 event_name_mapping: Option<&SpanEventNameMapping>,
505) -> String {
506 let scope_routing = resolve_span_scope_routing(resource, scope, table_name, event_name_mapping);
507 resolve_span_event_name_in_scope(&scope_routing, span, table_name).into_owned()
508}