1use std::collections::BTreeMap;
16
17use ahash::{HashMap, HashMapExt};
18use api::helper::{ColumnDataTypeWrapper, timestamp_unit};
19use api::v1::column_data_type_extension::TypeExt;
20use api::v1::column_def::{options_from_column_schema, options_from_skipping};
21use api::v1::value::ValueData;
22use api::v1::{
23 ColumnDataType, ColumnDataTypeExtension, ColumnOptions, ColumnSchema, JsonTypeExtension, Row,
24 RowInsertRequest, Rows, SemanticType, Value as GreptimeValue,
25};
26use bytes::Bytes;
27use common_catalog::consts::SERVICE_NAME_COLUMN;
28use common_telemetry::warn;
29use common_time::Timestamp;
30use common_time::timestamp::TimeUnit;
31use datatypes::schema::SkippingIndexOptions;
32use jsonb::{Number as JsonbNumber, Value as JsonbValue};
33use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
34use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue, any_value};
35use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs};
36use pipeline::{
37 ContextReq, GreptimePipelineParams, PipelineContext, PipelineWay, SchemaInfo, SelectInfo,
38};
39use session::context::QueryContextRef;
40use snafu::ensure;
41use table::requests::TABLE_DATA_MODEL_LOG_V1;
42use vrl::prelude::NotNan;
43use vrl::value::{KeyString, Value as VrlValue};
44
45use crate::error::{
46 Error, IncompatibleSchemaSnafu, InvalidParameterSnafu, NotSupportedSnafu, Result,
47 UnsupportedJsonDataTypeForTagSnafu,
48};
49use crate::http::event::PipelineIngestRequest;
50use crate::otlp::coerce::{coerce_value_data, is_supported_signed_to_unsigned_coercion};
51use crate::otlp::trace::attributes::OtlpAnyValue;
52use crate::otlp::utils::{bytes_to_hex_string, key_value_to_jsonb, resource_service_name};
53use crate::pipeline::run_pipeline;
54use crate::query_handler::PipelineHandlerRef;
55
56pub const LOG_TABLE_NAME: &str = "opentelemetry_logs";
57const EVENT_NAME_COLUMN: &str = "event_name";
58const OBSERVED_TIMESTAMP_COLUMN: &str = "observed_timestamp";
59
60pub async fn to_grpc_insert_requests(
68 request: ExportLogsServiceRequest,
69 pipeline: PipelineWay,
70 pipeline_params: GreptimePipelineParams,
71 table_name: String,
72 query_ctx: &QueryContextRef,
73 pipeline_handler: PipelineHandlerRef,
74) -> Result<ContextReq> {
75 match pipeline {
76 PipelineWay::OtlpLogDirect(select_info) => {
77 let table = pipeline_handler
78 .get_table(&table_name, query_ctx)
79 .await
80 .map_err(Error::from)?;
81 let v1_schema = table.as_deref().is_none_or(|table| {
84 table.table_info().meta.options.data_model() == Some(TABLE_DATA_MODEL_LOG_V1)
85 });
86 let existing_schema = table
87 .as_deref()
88 .map(ExistingLogSchema::try_from_table)
89 .transpose()?;
90 let rows = parse_export_logs_service_request_to_rows(
91 request,
92 select_info,
93 v1_schema,
94 existing_schema.as_ref(),
95 &table_name,
96 )?;
97 let insert_request = RowInsertRequest {
98 rows: Some(rows),
99 table_name,
100 };
101
102 Ok(ContextReq::default_opt_with_reqs(vec![insert_request]))
103 }
104 PipelineWay::Pipeline(pipeline_def) => {
105 let array = parse_export_logs_service_request(request);
106
107 let pipeline_ctx =
108 PipelineContext::new(&pipeline_def, &pipeline_params, query_ctx.channel());
109 run_pipeline(
110 &pipeline_handler,
111 &pipeline_ctx,
112 PipelineIngestRequest {
113 table: table_name,
114 values: array,
115 },
116 query_ctx,
117 true,
118 )
119 .await
120 }
121 _ => NotSupportedSnafu {
122 feat: "Unsupported pipeline for logs",
123 }
124 .fail(),
125 }
126}
127
128fn scope_to_pipeline_value(scope: Option<InstrumentationScope>) -> (VrlValue, VrlValue, VrlValue) {
129 scope
130 .map(|x| {
131 (
132 VrlValue::Object(key_value_to_map(x.attributes)),
133 VrlValue::Bytes(x.version.into()),
134 VrlValue::Bytes(x.name.into()),
135 )
136 })
137 .unwrap_or((VrlValue::Null, VrlValue::Null, VrlValue::Null))
138}
139
140fn scope_to_jsonb(
141 scope: Option<InstrumentationScope>,
142) -> (JsonbValue<'static>, Option<String>, Option<String>) {
143 scope
144 .map(|x| {
145 (
146 key_value_to_jsonb(x.attributes),
147 Some(x.version),
148 Some(x.name),
149 )
150 })
151 .unwrap_or((JsonbValue::Null, None, None))
152}
153
154fn log_to_pipeline_value(
155 log: LogRecord,
156 resource_schema_url: VrlValue,
157 resource_attr: VrlValue,
158 scope_schema_url: VrlValue,
159 scope_name: VrlValue,
160 scope_version: VrlValue,
161 scope_attrs: VrlValue,
162) -> VrlValue {
163 let log_attrs = VrlValue::Object(key_value_to_map(log.attributes));
164 let mut map = BTreeMap::new();
165 map.insert(
166 "Timestamp".into(),
167 VrlValue::Integer(log.time_unix_nano as i64),
168 );
169 map.insert(
170 "ObservedTimestamp".into(),
171 VrlValue::Integer(log.observed_time_unix_nano as i64),
172 );
173
174 map.insert(
176 "TraceId".into(),
177 VrlValue::Bytes(bytes_to_hex_string(&log.trace_id).into()),
178 );
179 map.insert(
180 "SpanId".into(),
181 VrlValue::Bytes(bytes_to_hex_string(&log.span_id).into()),
182 );
183 map.insert("TraceFlags".into(), VrlValue::Integer(log.flags as i64));
184 map.insert(
185 "SeverityText".into(),
186 VrlValue::Bytes(log.severity_text.into()),
187 );
188 map.insert(
189 "SeverityNumber".into(),
190 VrlValue::Integer(log.severity_number as i64),
191 );
192 map.insert(
194 "Body".into(),
195 log.body
196 .as_ref()
197 .map(|x| VrlValue::Bytes(log_body_to_string(x).into()))
198 .unwrap_or(VrlValue::Null),
199 );
200 map.insert("ResourceSchemaUrl".into(), resource_schema_url);
201
202 map.insert("ResourceAttributes".into(), resource_attr);
203 map.insert("ScopeSchemaUrl".into(), scope_schema_url);
204 map.insert("ScopeName".into(), scope_name);
205 map.insert("ScopeVersion".into(), scope_version);
206 map.insert("ScopeAttributes".into(), scope_attrs);
207 map.insert("LogAttributes".into(), log_attrs);
208 VrlValue::Object(map)
209}
210
211fn build_otlp_logs_identity_schema(v1_schema: bool) -> Result<Vec<ColumnSchema>> {
216 let trace_id_options = if v1_schema {
217 options_from_skipping(&SkippingIndexOptions::default())?
218 } else {
219 None
220 };
221 let mut columns = vec![
222 (
223 "timestamp",
224 ColumnDataType::TimestampNanosecond,
225 SemanticType::Timestamp,
226 None,
227 None,
228 ),
229 (
230 "trace_id",
231 ColumnDataType::String,
232 SemanticType::Field,
233 None,
234 trace_id_options,
235 ),
236 (
237 "span_id",
238 ColumnDataType::String,
239 SemanticType::Field,
240 None,
241 None,
242 ),
243 (
244 "severity_text",
245 ColumnDataType::String,
246 SemanticType::Field,
247 None,
248 None,
249 ),
250 (
251 "severity_number",
252 ColumnDataType::Int32,
253 SemanticType::Field,
254 None,
255 None,
256 ),
257 (
258 "body",
259 ColumnDataType::String,
260 SemanticType::Field,
261 None,
262 Some(ColumnOptions {
263 options: std::collections::HashMap::from([(
264 "fulltext".to_string(),
265 r#"{"enable":true}"#.to_string(),
266 )]),
267 }),
268 ),
269 (
270 "log_attributes",
271 ColumnDataType::Binary,
272 SemanticType::Field,
273 Some(ColumnDataTypeExtension {
274 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
275 }),
276 None,
277 ),
278 (
279 "trace_flags",
280 ColumnDataType::Int32,
281 SemanticType::Field,
282 None,
283 None,
284 ),
285 ];
286 if v1_schema {
287 columns.push((
288 SERVICE_NAME_COLUMN,
289 ColumnDataType::String,
290 SemanticType::Tag,
291 None,
292 None,
293 ));
294 }
295 columns.extend([
296 (
297 "scope_name",
298 ColumnDataType::String,
299 SemanticType::Tag,
300 None,
301 None,
302 ),
303 (
304 "scope_version",
305 ColumnDataType::String,
306 SemanticType::Field,
307 None,
308 None,
309 ),
310 (
311 "scope_attributes",
312 ColumnDataType::Binary,
313 SemanticType::Field,
314 Some(ColumnDataTypeExtension {
315 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
316 }),
317 None,
318 ),
319 (
320 "scope_schema_url",
321 ColumnDataType::String,
322 SemanticType::Field,
323 None,
324 None,
325 ),
326 (
327 "resource_attributes",
328 ColumnDataType::Binary,
329 SemanticType::Field,
330 Some(ColumnDataTypeExtension {
331 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
332 }),
333 None,
334 ),
335 (
336 "resource_schema_url",
337 ColumnDataType::String,
338 SemanticType::Field,
339 None,
340 None,
341 ),
342 ]);
343 if v1_schema {
344 columns.extend([
345 (
346 EVENT_NAME_COLUMN,
347 ColumnDataType::String,
348 SemanticType::Field,
349 None,
350 None,
351 ),
352 (
353 OBSERVED_TIMESTAMP_COLUMN,
354 ColumnDataType::TimestampNanosecond,
355 SemanticType::Field,
356 None,
357 None,
358 ),
359 ]);
360 }
361
362 Ok(columns
363 .into_iter()
364 .map(
365 |(field_name, column_type, semantic_type, datatype_extension, options)| ColumnSchema {
366 column_name: field_name.to_string(),
367 datatype: column_type as i32,
368 semantic_type: semantic_type as i32,
369 datatype_extension,
370 options,
371 },
372 )
373 .collect())
374}
375
376#[derive(Clone)]
377struct ExistingLogColumn {
378 schema: ColumnSchema,
379 datatype: ColumnDataType,
380}
381
382impl ExistingLogColumn {
383 fn schema_for_request_type(&self, request_type: ColumnDataType) -> ColumnSchema {
384 let mut schema = self.schema.clone();
385 if request_type == ColumnDataType::Binary && self.is_json_binary() {
386 schema.datatype = ColumnDataType::Binary as i32;
387 }
388 schema
389 }
390
391 fn is_json_binary(&self) -> bool {
392 self.datatype == ColumnDataType::Json
393 && matches!(
394 self.schema
395 .datatype_extension
396 .as_ref()
397 .and_then(|datatype_extension| datatype_extension.type_ext.as_ref()),
398 Some(TypeExt::JsonType(json_type))
399 if *json_type == JsonTypeExtension::JsonBinary as i32
400 )
401 }
402}
403
404#[derive(Default)]
405struct ExistingLogSchema {
406 columns: HashMap<String, ExistingLogColumn>,
407}
408
409impl ExistingLogSchema {
410 fn try_from_table(table: &table::Table) -> Result<Self> {
411 let table_info = table.table_info();
412 Self::try_from_schema_parts(
413 table.schema_ref().column_schemas(),
414 &table_info.meta.primary_key_indices,
415 )
416 }
417
418 fn try_from_schema_parts(
419 column_schemas: &[datatypes::schema::ColumnSchema],
420 primary_key_indices: &[usize],
421 ) -> Result<Self> {
422 let mut columns = HashMap::with_capacity(column_schemas.len());
423
424 for (index, column_schema) in column_schemas.iter().enumerate() {
425 let (datatype, datatype_extension) =
426 ColumnDataTypeWrapper::try_from(column_schema.data_type.clone())
427 .map(|wrapper| wrapper.into_parts())
428 .map_err(Error::from)?;
429 let semantic_type = if column_schema.is_time_index() {
430 SemanticType::Timestamp
431 } else if primary_key_indices.contains(&index) {
432 SemanticType::Tag
433 } else {
434 SemanticType::Field
435 };
436 let schema = ColumnSchema {
437 column_name: column_schema.name.clone(),
438 datatype: datatype as i32,
439 semantic_type: semantic_type as i32,
440 datatype_extension,
441 options: options_from_column_schema(column_schema),
442 };
443 columns.insert(
444 schema.column_name.clone(),
445 ExistingLogColumn { schema, datatype },
446 );
447 }
448
449 Ok(Self { columns })
450 }
451
452 fn get(&self, column_name: &str) -> Option<&ExistingLogColumn> {
453 self.columns.get(column_name)
454 }
455}
456
457fn build_otlp_build_in_row(
458 log: LogRecord,
459 log_attr: &JsonbValue,
460 service_name: Option<String>,
461 parse_ctx: &ParseContext,
462) -> Row {
463 let ts = if log.time_unix_nano != 0 {
464 log.time_unix_nano
465 } else {
466 log.observed_time_unix_nano
467 };
468
469 let mut row = vec![
470 GreptimeValue {
471 value_data: Some(ValueData::TimestampNanosecondValue(ts as i64)),
472 },
473 GreptimeValue {
474 value_data: Some(ValueData::StringValue(bytes_to_hex_string(&log.trace_id))),
475 },
476 GreptimeValue {
477 value_data: Some(ValueData::StringValue(bytes_to_hex_string(&log.span_id))),
478 },
479 GreptimeValue {
480 value_data: Some(ValueData::StringValue(log.severity_text)),
481 },
482 GreptimeValue {
483 value_data: Some(ValueData::I32Value(log.severity_number)),
484 },
485 GreptimeValue {
486 value_data: log
487 .body
488 .as_ref()
489 .map(|x| ValueData::StringValue(log_body_to_string(x))),
490 },
491 GreptimeValue {
492 value_data: Some(ValueData::BinaryValue(log_attr.to_vec())),
493 },
494 GreptimeValue {
495 value_data: Some(ValueData::I32Value(log.flags as i32)),
496 },
497 ];
498 if parse_ctx.v1_schema {
499 row.push(GreptimeValue {
500 value_data: service_name.map(ValueData::StringValue),
501 });
502 }
503 row.extend([
504 GreptimeValue {
505 value_data: parse_ctx.scope_name.clone().map(ValueData::StringValue),
506 },
507 GreptimeValue {
508 value_data: parse_ctx.scope_version.clone().map(ValueData::StringValue),
509 },
510 GreptimeValue {
511 value_data: Some(ValueData::BinaryValue(parse_ctx.scope_attrs.to_vec())),
512 },
513 GreptimeValue {
514 value_data: Some(ValueData::StringValue(parse_ctx.scope_url.clone())),
515 },
516 GreptimeValue {
517 value_data: Some(ValueData::BinaryValue(parse_ctx.resource_attr.to_vec())),
518 },
519 GreptimeValue {
520 value_data: Some(ValueData::StringValue(parse_ctx.resource_url.clone())),
521 },
522 ]);
523 if parse_ctx.v1_schema {
524 row.extend([
525 GreptimeValue {
526 value_data: (!log.event_name.is_empty())
527 .then_some(ValueData::StringValue(log.event_name)),
528 },
529 GreptimeValue {
530 value_data: (log.observed_time_unix_nano != 0).then_some(
531 ValueData::TimestampNanosecondValue(log.observed_time_unix_nano as i64),
532 ),
533 },
534 ]);
535 }
536 Row { values: row }
537}
538
539fn extracted_service_name(attrs: &JsonbValue) -> Result<Option<String>> {
542 attrs
543 .get_by_name_ignore_case(SERVICE_NAME_COLUMN)
544 .cloned()
545 .map(|value| scalar_attr_to_string(SERVICE_NAME_COLUMN, value))
546 .transpose()
547 .map(Option::flatten)
548}
549
550fn scalar_attr_to_string(name: &str, value: JsonbValue) -> Result<Option<String>> {
551 Ok(jsonb_value_to_log_value_data(name, value, true)?
552 .and_then(|(value_data, _)| stringify_scalar_value(value_data))
553 .and_then(|value_data| match value_data {
554 ValueData::StringValue(value) => Some(value),
555 _ => None,
556 }))
557}
558
559fn extract_field_from_attr_and_combine_schema(
560 select_info: &SelectInfo,
561 select_schema: &mut SchemaInfo,
562 attrs: &jsonb::Value,
563 existing_schema: Option<&ExistingLogSchema>,
564 table_name: &str,
565) -> Result<Vec<GreptimeValue>> {
566 let mut extracted_values = vec![GreptimeValue::default(); select_schema.schema.len()];
569
570 for key in select_info.keys.iter() {
571 let Some(value) = attrs.get_by_name_ignore_case(key).cloned() else {
572 continue;
573 };
574 let Some((schema, value)) =
575 decide_column_schema_and_convert_value(key, value, existing_schema, table_name)?
576 else {
577 continue;
578 };
579
580 if let Some(index) = select_schema.index.get(key) {
581 let column_schema = &select_schema.schema[*index];
582 let column_schema: ColumnSchema = column_schema.clone().try_into()?;
583 ensure!(
585 column_schema.datatype == schema.datatype,
586 IncompatibleSchemaSnafu {
587 column_name: key,
588 datatype: column_schema.datatype().as_str_name(),
589 expected: column_schema.datatype,
590 actual: schema.datatype,
591 }
592 );
593 extracted_values[*index] = value;
594 } else {
595 select_schema.schema.push(schema.into());
596 select_schema
597 .index
598 .insert(key.clone(), select_schema.schema.len() - 1);
599 extracted_values.push(value);
600 }
601 }
602
603 Ok(extracted_values)
604}
605
606fn decide_column_schema_and_convert_value(
607 column_name: &str,
608 value: JsonbValue,
609 existing_schema: Option<&ExistingLogSchema>,
610 table_name: &str,
611) -> Result<Option<(ColumnSchema, GreptimeValue)>> {
612 if let Some(existing_column) = existing_schema.and_then(|schema| schema.get(column_name)) {
613 return decide_existing_column_schema_and_convert_value(
614 column_name,
615 value,
616 existing_column,
617 table_name,
618 );
619 }
620
621 let column_info = match value {
622 JsonbValue::String(s) => Ok(Some((
623 GreptimeValue {
624 value_data: Some(ValueData::StringValue(s.into())),
625 },
626 ColumnDataType::String,
627 SemanticType::Tag,
628 None,
629 ))),
630 JsonbValue::Number(n) => match n {
631 JsonbNumber::Int64(i) => Ok(Some((
632 GreptimeValue {
633 value_data: Some(ValueData::I64Value(i)),
634 },
635 ColumnDataType::Int64,
636 SemanticType::Tag,
637 None,
638 ))),
639 JsonbNumber::Float64(_) => UnsupportedJsonDataTypeForTagSnafu {
640 ty: "FLOAT".to_string(),
641 key: column_name,
642 }
643 .fail(),
644 JsonbNumber::Decimal64(_) | JsonbNumber::Decimal128(_) | JsonbNumber::Decimal256(_) => {
645 UnsupportedJsonDataTypeForTagSnafu {
646 ty: "DECIMAL".to_string(),
647 key: column_name,
648 }
649 .fail()
650 }
651 JsonbNumber::UInt64(u) => {
652 let value = jsonb_uint64_to_log_value(u, column_name)?;
653 Ok(Some((
654 GreptimeValue {
655 value_data: Some(ValueData::I64Value(value)),
656 },
657 ColumnDataType::Int64,
658 SemanticType::Tag,
659 None,
660 )))
661 }
662 },
663 JsonbValue::Bool(b) => Ok(Some((
664 GreptimeValue {
665 value_data: Some(ValueData::BoolValue(b)),
666 },
667 ColumnDataType::Boolean,
668 SemanticType::Tag,
669 None,
670 ))),
671 JsonbValue::Array(_) | JsonbValue::Object(_) => UnsupportedJsonDataTypeForTagSnafu {
672 ty: "Json".to_string(),
673 key: column_name,
674 }
675 .fail(),
676 JsonbValue::Binary(_) => UnsupportedJsonDataTypeForTagSnafu {
677 ty: "Binary".to_string(),
678 key: column_name,
679 }
680 .fail(),
681 JsonbValue::Date(_) => UnsupportedJsonDataTypeForTagSnafu {
682 ty: "Date".to_string(),
683 key: column_name,
684 }
685 .fail(),
686 JsonbValue::Timestamp(_) => UnsupportedJsonDataTypeForTagSnafu {
687 ty: "Timestamp".to_string(),
688 key: column_name,
689 }
690 .fail(),
691 JsonbValue::TimestampTz(_) => UnsupportedJsonDataTypeForTagSnafu {
692 ty: "TimestampTz".to_string(),
693 key: column_name,
694 }
695 .fail(),
696 JsonbValue::Interval(_) => UnsupportedJsonDataTypeForTagSnafu {
697 ty: "Interval".to_string(),
698 key: column_name,
699 }
700 .fail(),
701 JsonbValue::Null => Ok(None),
702 };
703 column_info.map(|c| {
704 c.map(|(value, column_type, semantic_type, datatype_extension)| {
705 (
706 ColumnSchema {
707 column_name: column_name.to_string(),
708 datatype: column_type as i32,
709 semantic_type: semantic_type as i32,
710 datatype_extension,
711 options: None,
712 },
713 value,
714 )
715 })
716 })
717}
718
719fn decide_existing_column_schema_and_convert_value(
720 column_name: &str,
721 value: JsonbValue,
722 existing_column: &ExistingLogColumn,
723 table_name: &str,
724) -> Result<Option<(ColumnSchema, GreptimeValue)>> {
725 let Some((value_data, request_type)) = jsonb_value_to_log_value_data(column_name, value, true)?
726 else {
727 return Ok(None);
728 };
729 let value_data = coerce_log_value_data(
730 Some(value_data),
731 existing_column.datatype,
732 existing_column.schema.semantic_type(),
733 request_type,
734 existing_column.is_json_binary(),
735 column_name,
736 table_name,
737 )?;
738
739 Ok(Some((
740 existing_column.schema.clone(),
741 GreptimeValue { value_data },
742 )))
743}
744
745fn jsonb_uint64_to_log_value(u: u64, column_name: &str) -> Result<i64> {
752 i64::try_from(u).map_err(|_| InvalidParameterSnafu {
753 reason: format!(
754 "uint64 value {u} in column '{column_name}' exceeds the i64 range supported by built-in log columns"
755 ),
756 }
757 .build())
758}
759
760fn jsonb_value_to_log_value_data(
761 column_name: &str,
762 value: JsonbValue,
763 allow_float: bool,
764) -> Result<Option<(ValueData, ColumnDataType)>> {
765 match value {
766 JsonbValue::String(s) => Ok(Some((
767 ValueData::StringValue(s.into()),
768 ColumnDataType::String,
769 ))),
770 JsonbValue::Number(n) => match n {
771 JsonbNumber::Int64(i) => Ok(Some((ValueData::I64Value(i), ColumnDataType::Int64))),
772 JsonbNumber::Float64(f) if allow_float => {
773 Ok(Some((ValueData::F64Value(f), ColumnDataType::Float64)))
774 }
775 JsonbNumber::Float64(_) => UnsupportedJsonDataTypeForTagSnafu {
776 ty: "FLOAT".to_string(),
777 key: column_name,
778 }
779 .fail(),
780 JsonbNumber::Decimal64(_) | JsonbNumber::Decimal128(_) | JsonbNumber::Decimal256(_) => {
781 UnsupportedJsonDataTypeForTagSnafu {
782 ty: "DECIMAL".to_string(),
783 key: column_name,
784 }
785 .fail()
786 }
787 JsonbNumber::UInt64(u) => Ok(Some((
788 ValueData::I64Value(jsonb_uint64_to_log_value(u, column_name)?),
789 ColumnDataType::Int64,
790 ))),
791 },
792 JsonbValue::Bool(b) => Ok(Some((ValueData::BoolValue(b), ColumnDataType::Boolean))),
793 JsonbValue::Array(_) | JsonbValue::Object(_) => UnsupportedJsonDataTypeForTagSnafu {
794 ty: "Json".to_string(),
795 key: column_name,
796 }
797 .fail(),
798 JsonbValue::Binary(_) => UnsupportedJsonDataTypeForTagSnafu {
799 ty: "Binary".to_string(),
800 key: column_name,
801 }
802 .fail(),
803 JsonbValue::Date(_) => UnsupportedJsonDataTypeForTagSnafu {
804 ty: "Date".to_string(),
805 key: column_name,
806 }
807 .fail(),
808 JsonbValue::Timestamp(_) => UnsupportedJsonDataTypeForTagSnafu {
809 ty: "Timestamp".to_string(),
810 key: column_name,
811 }
812 .fail(),
813 JsonbValue::TimestampTz(_) => UnsupportedJsonDataTypeForTagSnafu {
814 ty: "TimestampTz".to_string(),
815 key: column_name,
816 }
817 .fail(),
818 JsonbValue::Interval(_) => UnsupportedJsonDataTypeForTagSnafu {
819 ty: "Interval".to_string(),
820 key: column_name,
821 }
822 .fail(),
823 JsonbValue::Null => Ok(None),
824 }
825}
826
827fn align_rows_with_existing_schema(
828 schemas: &mut [ColumnSchema],
829 rows: &mut [Row],
830 existing_schema: Option<&ExistingLogSchema>,
831 table_name: &str,
832) -> Result<()> {
833 let Some(existing_schema) = existing_schema else {
834 return Ok(());
835 };
836
837 for (column_idx, schema) in schemas.iter_mut().enumerate() {
838 let request_type = schema.datatype();
839 let Some(existing_column) = existing_schema.get(&schema.column_name) else {
840 if schema.semantic_type() == SemanticType::Tag {
843 schema.semantic_type = SemanticType::Field as i32;
844 }
845 continue;
846 };
847
848 let target_type = existing_column.datatype;
849 let semantic_type = existing_column.schema.semantic_type();
850 let target_is_json_binary = existing_column.is_json_binary();
851 for row in rows.iter_mut() {
852 let Some(value) = row.values.get_mut(column_idx) else {
853 continue;
854 };
855 value.value_data = coerce_log_value_data(
856 value.value_data.take(),
857 target_type,
858 semantic_type,
859 request_type,
860 target_is_json_binary,
861 &schema.column_name,
862 table_name,
863 )?;
864 }
865 *schema = existing_column.schema_for_request_type(request_type);
866 }
867
868 Ok(())
869}
870
871fn coerce_log_value_data(
872 value_data: Option<ValueData>,
873 target_type: ColumnDataType,
874 _semantic_type: SemanticType,
875 request_type: ColumnDataType,
876 target_is_json_binary: bool,
877 column_name: &str,
878 table_name: &str,
879) -> Result<Option<ValueData>> {
880 let Some(value_data) = value_data else {
881 return Ok(None);
882 };
883
884 if request_type == target_type {
885 return Ok(Some(value_data));
886 }
887
888 if request_type == ColumnDataType::Binary && target_is_json_binary {
889 return Ok(Some(value_data));
890 }
891
892 if is_timestamp_type(request_type)
893 && let Some(target_unit) = timestamp_unit(target_type)
894 {
895 return align_timestamp_value(value_data, target_unit, column_name, table_name).map(Some);
896 }
897
898 if target_type == ColumnDataType::String {
907 if let Ok(value_data) =
908 coerce_value_data(&Some(value_data.clone()), target_type, request_type)
909 {
910 return Ok(value_data);
911 }
912 if let Some(value_data) = stringify_scalar_value(value_data) {
913 return Ok(Some(value_data));
914 }
915 } else if is_supported_signed_to_unsigned_coercion(request_type, target_type)
916 && let Ok(Some(value_data)) =
917 coerce_value_data(&Some(value_data), target_type, request_type)
918 {
919 return Ok(Some(value_data));
920 }
921
922 InvalidParameterSnafu {
923 reason: format!(
924 "failed to align log column '{}' in table '{}' from {:?} to {:?}",
925 column_name, table_name, request_type, target_type
926 ),
927 }
928 .fail()
929}
930
931fn stringify_scalar_value(value_data: ValueData) -> Option<ValueData> {
932 let value = match value_data {
933 ValueData::StringValue(value) => value,
934 ValueData::BoolValue(value) => value.to_string(),
935 ValueData::I8Value(value) => value.to_string(),
936 ValueData::I16Value(value) => value.to_string(),
937 ValueData::I32Value(value) => value.to_string(),
938 ValueData::I64Value(value) => value.to_string(),
939 ValueData::U8Value(value) => value.to_string(),
940 ValueData::U16Value(value) => value.to_string(),
941 ValueData::U32Value(value) => value.to_string(),
942 ValueData::U64Value(value) => value.to_string(),
943 ValueData::F32Value(value) => value.to_string(),
944 ValueData::F64Value(value) => value.to_string(),
945 _ => return None,
946 };
947 Some(ValueData::StringValue(value))
948}
949
950fn align_timestamp_value(
951 value_data: ValueData,
952 target_unit: TimeUnit,
953 column_name: &str,
954 table_name: &str,
955) -> Result<ValueData> {
956 let timestamp = match value_data {
957 ValueData::TimestampSecondValue(value) => Timestamp::new_second(value),
958 ValueData::TimestampMillisecondValue(value) => Timestamp::new_millisecond(value),
959 ValueData::TimestampMicrosecondValue(value) => Timestamp::new_microsecond(value),
960 ValueData::TimestampNanosecondValue(value) => Timestamp::new_nanosecond(value),
961 value_data => {
962 return InvalidParameterSnafu {
963 reason: format!(
964 "failed to align log column '{}' in table '{}' from non-timestamp value {:?}",
965 column_name, table_name, value_data
966 ),
967 }
968 .fail();
969 }
970 };
971 let timestamp = timestamp.convert_to(target_unit).ok_or_else(|| {
972 InvalidParameterSnafu {
973 reason: format!(
974 "failed to align log column '{}' in table '{}' to timestamp unit {}",
975 column_name, table_name, target_unit
976 ),
977 }
978 .build()
979 })?;
980
981 Ok(match target_unit {
982 TimeUnit::Second => ValueData::TimestampSecondValue(timestamp.value()),
983 TimeUnit::Millisecond => ValueData::TimestampMillisecondValue(timestamp.value()),
984 TimeUnit::Microsecond => ValueData::TimestampMicrosecondValue(timestamp.value()),
985 TimeUnit::Nanosecond => ValueData::TimestampNanosecondValue(timestamp.value()),
986 })
987}
988
989fn is_timestamp_type(datatype: ColumnDataType) -> bool {
990 timestamp_unit(datatype).is_some()
991}
992
993fn parse_export_logs_service_request_to_rows(
994 request: ExportLogsServiceRequest,
995 mut select_info: Box<SelectInfo>,
996 v1_schema: bool,
997 existing_schema: Option<&ExistingLogSchema>,
998 table_name: &str,
999) -> Result<Rows> {
1000 let mut schemas = build_otlp_logs_identity_schema(v1_schema)?;
1001
1002 let extract_service_name = v1_schema
1007 && select_info
1008 .keys
1009 .iter()
1010 .any(|key| key == SERVICE_NAME_COLUMN);
1011 if v1_schema {
1012 select_info.keys.retain(|key| {
1013 !matches!(
1014 key.as_str(),
1015 SERVICE_NAME_COLUMN | EVENT_NAME_COLUMN | OBSERVED_TIMESTAMP_COLUMN
1016 )
1017 });
1018 }
1019 let mut parse_ctx = ParseContext::new(
1020 select_info,
1021 v1_schema,
1022 extract_service_name,
1023 existing_schema,
1024 table_name,
1025 );
1026 let mut rows = parse_resource(&mut parse_ctx, request.resource_logs)?;
1027
1028 schemas.extend(parse_ctx.select_schema.column_schemas()?);
1029 align_rows_with_existing_schema(&mut schemas, &mut rows, existing_schema, table_name)?;
1030
1031 rows.iter_mut().for_each(|row| {
1032 row.values.resize(schemas.len(), GreptimeValue::default());
1033 });
1034
1035 Ok(Rows {
1036 schema: schemas,
1037 rows,
1038 })
1039}
1040
1041fn parse_resource(
1042 parse_ctx: &mut ParseContext,
1043 resource_logs_vec: Vec<ResourceLogs>,
1044) -> Result<Vec<Row>> {
1045 let total_len = resource_logs_vec
1046 .iter()
1047 .flat_map(|r| r.scope_logs.iter())
1048 .map(|s| s.log_records.len())
1049 .sum();
1050
1051 let mut results = Vec::with_capacity(total_len);
1052
1053 for r in resource_logs_vec {
1054 let mut resource_service = None;
1055 parse_ctx.resource_attr = match r.resource {
1056 Some(resource) => {
1057 if parse_ctx.v1_schema {
1058 resource_service = resource_service_name(&resource.attributes);
1059 }
1060 key_value_to_jsonb(resource.attributes)
1061 }
1062 None => JsonbValue::Null,
1063 };
1064
1065 parse_ctx.resource_url = r.schema_url;
1066 if parse_ctx.v1_schema {
1067 let extracted = if parse_ctx.extract_service_name {
1068 extracted_service_name(&parse_ctx.resource_attr)?
1069 } else {
1070 None
1071 };
1072 parse_ctx.resource_service_name = extracted.or(resource_service);
1073 }
1074
1075 parse_ctx.resource_uplift_values = extract_field_from_attr_and_combine_schema(
1076 &parse_ctx.select_info,
1077 &mut parse_ctx.select_schema,
1078 &parse_ctx.resource_attr,
1079 parse_ctx.existing_schema,
1080 parse_ctx.table_name,
1081 )?;
1082
1083 let rows = parse_scope(r.scope_logs, parse_ctx)?;
1084 results.extend(rows);
1085 }
1086 Ok(results)
1087}
1088
1089struct ParseContext<'a> {
1090 select_info: Box<SelectInfo>,
1092 v1_schema: bool,
1093 extract_service_name: bool,
1094 existing_schema: Option<&'a ExistingLogSchema>,
1095 table_name: &'a str,
1096 select_schema: SchemaInfo,
1099
1100 resource_uplift_values: Vec<GreptimeValue>,
1102 scope_uplift_values: Vec<GreptimeValue>,
1103
1104 resource_url: String,
1106 resource_attr: JsonbValue<'a>,
1107 scope_name: Option<String>,
1108 scope_version: Option<String>,
1109 scope_url: String,
1110 scope_attrs: JsonbValue<'a>,
1111 resource_service_name: Option<String>,
1114 scope_service_name: Option<String>,
1115}
1116
1117impl<'a> ParseContext<'a> {
1118 pub fn new(
1119 select_info: Box<SelectInfo>,
1120 v1_schema: bool,
1121 extract_service_name: bool,
1122 existing_schema: Option<&'a ExistingLogSchema>,
1123 table_name: &'a str,
1124 ) -> ParseContext<'a> {
1125 let len = select_info.keys.len();
1126 ParseContext {
1127 select_info,
1128 v1_schema,
1129 extract_service_name,
1130 existing_schema,
1131 table_name,
1132 select_schema: SchemaInfo::with_capacity(len),
1133 resource_uplift_values: vec![],
1134 scope_uplift_values: vec![],
1135 resource_url: String::new(),
1136 resource_attr: JsonbValue::Null,
1137 scope_name: None,
1138 scope_version: None,
1139 scope_url: String::new(),
1140 scope_attrs: JsonbValue::Null,
1141 resource_service_name: None,
1142 scope_service_name: None,
1143 }
1144 }
1145}
1146
1147fn parse_scope(scopes_log_vec: Vec<ScopeLogs>, parse_ctx: &mut ParseContext) -> Result<Vec<Row>> {
1148 let len = scopes_log_vec.iter().map(|l| l.log_records.len()).sum();
1149 let mut results = Vec::with_capacity(len);
1150
1151 for scope_logs in scopes_log_vec {
1152 let (scope_attrs, scope_version, scope_name) = scope_to_jsonb(scope_logs.scope);
1153 parse_ctx.scope_name = scope_name;
1154 parse_ctx.scope_version = scope_version;
1155 parse_ctx.scope_url = scope_logs.schema_url;
1156 parse_ctx.scope_attrs = scope_attrs;
1157 parse_ctx.scope_service_name = if parse_ctx.extract_service_name {
1158 extracted_service_name(&parse_ctx.scope_attrs)?
1159 } else {
1160 None
1161 };
1162
1163 parse_ctx.scope_uplift_values = extract_field_from_attr_and_combine_schema(
1164 &parse_ctx.select_info,
1165 &mut parse_ctx.select_schema,
1166 &parse_ctx.scope_attrs,
1167 parse_ctx.existing_schema,
1168 parse_ctx.table_name,
1169 )?;
1170
1171 let rows = parse_log(scope_logs.log_records, parse_ctx)?;
1172 results.extend(rows);
1173 }
1174 Ok(results)
1175}
1176
1177fn parse_log(log_records: Vec<LogRecord>, parse_ctx: &mut ParseContext) -> Result<Vec<Row>> {
1178 let mut result = Vec::with_capacity(log_records.len());
1179
1180 for mut log in log_records {
1181 let log_attr = key_value_to_jsonb(std::mem::take(&mut log.attributes));
1182 let service_name = if parse_ctx.v1_schema {
1183 let extracted = if parse_ctx.extract_service_name {
1184 extracted_service_name(&log_attr)?
1185 } else {
1186 None
1187 };
1188 extracted
1189 .or_else(|| parse_ctx.scope_service_name.clone())
1190 .or_else(|| parse_ctx.resource_service_name.clone())
1191 } else {
1192 None
1193 };
1194 let mut row = build_otlp_build_in_row(log, &log_attr, service_name, parse_ctx);
1195
1196 let log_values = extract_field_from_attr_and_combine_schema(
1197 &parse_ctx.select_info,
1198 &mut parse_ctx.select_schema,
1199 &log_attr,
1200 parse_ctx.existing_schema,
1201 parse_ctx.table_name,
1202 )?;
1203
1204 let extracted_values = merge_values(
1205 log_values,
1206 &parse_ctx.scope_uplift_values,
1207 &parse_ctx.resource_uplift_values,
1208 );
1209
1210 row.values.extend(extracted_values);
1211
1212 result.push(row);
1213 }
1214 Ok(result)
1215}
1216
1217fn merge_values(
1218 log: Vec<GreptimeValue>,
1219 scope: &[GreptimeValue],
1220 resource: &[GreptimeValue],
1221) -> Vec<GreptimeValue> {
1222 log.into_iter()
1223 .enumerate()
1224 .map(|(i, value)| GreptimeValue {
1225 value_data: value
1226 .value_data
1227 .or_else(|| scope.get(i).and_then(|x| x.value_data.clone()))
1228 .or_else(|| resource.get(i).and_then(|x| x.value_data.clone())),
1229 })
1230 .collect()
1231}
1232
1233fn parse_export_logs_service_request(request: ExportLogsServiceRequest) -> Vec<VrlValue> {
1236 let mut result = Vec::new();
1237 for r in request.resource_logs {
1238 let resource_attr = r
1239 .resource
1240 .map(|x| VrlValue::Object(key_value_to_map(x.attributes)))
1241 .unwrap_or(VrlValue::Null);
1242 let resource_schema_url = VrlValue::Bytes(r.schema_url.into());
1243 for scope_logs in r.scope_logs {
1244 let (scope_attrs, scope_version, scope_name) =
1245 scope_to_pipeline_value(scope_logs.scope);
1246 let scope_schema_url = VrlValue::Bytes(scope_logs.schema_url.into());
1247 for log in scope_logs.log_records {
1248 let value = log_to_pipeline_value(
1249 log,
1250 resource_schema_url.clone(),
1251 resource_attr.clone(),
1252 scope_schema_url.clone(),
1253 scope_name.clone(),
1254 scope_version.clone(),
1255 scope_attrs.clone(),
1256 );
1257 result.push(value);
1258 }
1259 }
1260 }
1261 result
1262}
1263
1264fn any_value_to_vrl_value(value: any_value::Value) -> VrlValue {
1266 match value {
1267 any_value::Value::StringValue(s) => VrlValue::Bytes(s.into()),
1268 any_value::Value::IntValue(i) => VrlValue::Integer(i),
1269 any_value::Value::DoubleValue(d) => VrlValue::Float(NotNan::new(d).unwrap()),
1270 any_value::Value::BoolValue(b) => VrlValue::Boolean(b),
1271 any_value::Value::ArrayValue(array_value) => {
1272 let values = array_value
1273 .values
1274 .into_iter()
1275 .filter_map(|v| v.value.map(any_value_to_vrl_value))
1276 .collect();
1277 VrlValue::Array(values)
1278 }
1279 any_value::Value::KvlistValue(key_value_list) => {
1280 VrlValue::Object(key_value_to_map(key_value_list.values))
1281 }
1282 any_value::Value::BytesValue(items) => VrlValue::Bytes(Bytes::from(items)),
1283 any_value::Value::StringValueStrindex(_) => {
1288 warn!(
1289 "encountered a profiling-only `StringValueStrindex` value in a non-profiling signal; ignoring"
1290 );
1291 VrlValue::Null
1292 }
1293 }
1294}
1295
1296fn key_value_to_map(key_values: Vec<KeyValue>) -> BTreeMap<KeyString, VrlValue> {
1298 let mut map = BTreeMap::new();
1299 for kv in key_values {
1300 let value = match kv.value {
1301 Some(value) => match value.value {
1302 Some(value) => any_value_to_vrl_value(value),
1303 None => VrlValue::Null,
1304 },
1305 None => VrlValue::Null,
1306 };
1307 map.insert(kv.key.into(), value);
1308 }
1309 map
1310}
1311
1312fn log_body_to_string(body: &AnyValue) -> String {
1313 let otlp_value = OtlpAnyValue::from(body);
1314 otlp_value.to_string()
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319 use datatypes::prelude::ConcreteDataType;
1320 use datatypes::schema::ColumnSchema as DatatypesColumnSchema;
1321 use opentelemetry_proto::tonic::common::v1::any_value::Value as OtlpValue;
1322
1323 use super::*;
1324 use crate::otlp::trace::KEY_SERVICE_NAME;
1325
1326 fn time_column(datatype: ConcreteDataType) -> DatatypesColumnSchema {
1327 DatatypesColumnSchema::new("timestamp", datatype, false).with_time_index(true)
1328 }
1329
1330 fn column(name: &str, datatype: ConcreteDataType) -> DatatypesColumnSchema {
1331 DatatypesColumnSchema::new(name, datatype, true)
1332 }
1333
1334 fn existing_schema(
1335 columns: Vec<DatatypesColumnSchema>,
1336 primary_key_indices: &[usize],
1337 ) -> ExistingLogSchema {
1338 ExistingLogSchema::try_from_schema_parts(&columns, primary_key_indices).unwrap()
1339 }
1340
1341 fn existing_uint32_trace_flags_schema() -> ExistingLogSchema {
1342 existing_schema(
1343 vec![
1344 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1345 column("trace_flags", ConcreteDataType::uint32_datatype()),
1346 ],
1347 &[],
1348 )
1349 }
1350
1351 fn kv(key: &str, value: OtlpValue) -> KeyValue {
1352 KeyValue {
1353 key: key.to_string(),
1354 value: Some(AnyValue { value: Some(value) }),
1355 ..Default::default()
1356 }
1357 }
1358
1359 fn request_with_log_attrs(attrs: Vec<KeyValue>) -> ExportLogsServiceRequest {
1360 request_with_log_attrs_and_flags(attrs, 0)
1361 }
1362
1363 fn request_with_log_attrs_and_flags(
1364 attrs: Vec<KeyValue>,
1365 flags: u32,
1366 ) -> ExportLogsServiceRequest {
1367 ExportLogsServiceRequest {
1368 resource_logs: vec![ResourceLogs {
1369 scope_logs: vec![ScopeLogs {
1370 log_records: vec![LogRecord {
1371 time_unix_nano: 1_234_000_000,
1372 trace_id: vec![1; 16],
1373 flags,
1374 attributes: attrs,
1375 ..Default::default()
1376 }],
1377 ..Default::default()
1378 }],
1379 ..Default::default()
1380 }],
1381 }
1382 }
1383
1384 fn parse_with_select(
1385 request: ExportLogsServiceRequest,
1386 select: &str,
1387 existing_schema: Option<&ExistingLogSchema>,
1388 ) -> Result<Rows> {
1389 parse_export_logs_service_request_to_rows(
1392 request,
1393 Box::new(SelectInfo::from(select.to_string())),
1394 existing_schema.is_none(),
1395 existing_schema,
1396 "test_logs",
1397 )
1398 }
1399
1400 fn column_index(rows: &Rows, name: &str) -> usize {
1401 rows.schema
1402 .iter()
1403 .position(|schema| schema.column_name == name)
1404 .unwrap()
1405 }
1406
1407 #[test]
1408 fn test_no_existing_table_preserves_direct_schema() {
1409 let rows = parse_with_select(request_with_log_attrs(vec![]), "", None).unwrap();
1410
1411 assert_eq!(rows.schema[0].column_name, "timestamp");
1412 assert_eq!(
1413 rows.schema[0].datatype,
1414 ColumnDataType::TimestampNanosecond as i32
1415 );
1416 assert_eq!(rows.schema[0].semantic_type, SemanticType::Timestamp as i32);
1417 let scope_name_idx = column_index(&rows, "scope_name");
1418 assert_eq!(
1419 rows.schema[scope_name_idx].semantic_type,
1420 SemanticType::Tag as i32
1421 );
1422 }
1423
1424 fn request_with_resource_and_logs(
1425 resource_attrs: Vec<KeyValue>,
1426 log_records: Vec<LogRecord>,
1427 ) -> ExportLogsServiceRequest {
1428 ExportLogsServiceRequest {
1429 resource_logs: vec![ResourceLogs {
1430 resource: Some(opentelemetry_proto::tonic::resource::v1::Resource {
1431 attributes: resource_attrs,
1432 ..Default::default()
1433 }),
1434 scope_logs: vec![ScopeLogs {
1435 scope: Some(InstrumentationScope {
1436 name: "com.example.Checkout".to_string(),
1437 ..Default::default()
1438 }),
1439 log_records,
1440 ..Default::default()
1441 }],
1442 ..Default::default()
1443 }],
1444 }
1445 }
1446
1447 fn value_at(rows: &Rows, row: usize, column: &str) -> Option<ValueData> {
1448 rows.rows[row].values[column_index(rows, column)]
1449 .value_data
1450 .clone()
1451 }
1452
1453 #[test]
1454 fn test_new_table_uses_v1_schema() {
1455 let request = request_with_resource_and_logs(
1456 vec![kv(
1457 KEY_SERVICE_NAME,
1458 OtlpValue::StringValue("checkout".to_string()),
1459 )],
1460 vec![
1461 LogRecord {
1462 time_unix_nano: 1_000,
1463 observed_time_unix_nano: 2_000,
1464 event_name: "gen_ai.choice".to_string(),
1465 ..Default::default()
1466 },
1467 LogRecord {
1468 time_unix_nano: 3_000,
1469 ..Default::default()
1470 },
1471 ],
1472 );
1473 let rows = parse_with_select(request, "", None).unwrap();
1474
1475 let tags: Vec<_> = rows
1477 .schema
1478 .iter()
1479 .filter(|schema| schema.semantic_type == SemanticType::Tag as i32)
1480 .map(|schema| schema.column_name.as_str())
1481 .collect();
1482 assert_eq!(tags, [SERVICE_NAME_COLUMN, "scope_name"]);
1483 let trace_id = &rows.schema[column_index(&rows, "trace_id")];
1484 assert!(
1485 trace_id
1486 .options
1487 .as_ref()
1488 .is_some_and(|options| options.options.contains_key("skipping_index"))
1489 );
1490
1491 assert_eq!(
1492 value_at(&rows, 0, SERVICE_NAME_COLUMN),
1493 Some(ValueData::StringValue("checkout".to_string()))
1494 );
1495 assert_eq!(
1496 value_at(&rows, 0, EVENT_NAME_COLUMN),
1497 Some(ValueData::StringValue("gen_ai.choice".to_string()))
1498 );
1499 assert_eq!(
1500 value_at(&rows, 0, OBSERVED_TIMESTAMP_COLUMN),
1501 Some(ValueData::TimestampNanosecondValue(2_000))
1502 );
1503 assert_eq!(value_at(&rows, 1, EVENT_NAME_COLUMN), None);
1505 assert_eq!(value_at(&rows, 1, OBSERVED_TIMESTAMP_COLUMN), None);
1506 }
1507
1508 #[test]
1509 fn test_extracted_service_name_overrides_resource_service_name() {
1510 let mut request = request_with_resource_and_logs(
1511 vec![kv(
1512 KEY_SERVICE_NAME,
1513 OtlpValue::StringValue("from-resource".to_string()),
1514 )],
1515 vec![
1516 LogRecord {
1517 attributes: vec![
1518 kv(
1519 SERVICE_NAME_COLUMN,
1520 OtlpValue::StringValue("from-log".to_string()),
1521 ),
1522 kv(
1523 EVENT_NAME_COLUMN,
1524 OtlpValue::StringValue("from-attr".to_string()),
1525 ),
1526 ],
1527 event_name: "from-record".to_string(),
1528 ..Default::default()
1529 },
1530 LogRecord::default(),
1531 ],
1532 );
1533 let mut scope_logs = request.resource_logs[0].scope_logs[0].clone();
1534 scope_logs.scope.as_mut().unwrap().attributes = vec![kv(
1535 SERVICE_NAME_COLUMN,
1536 OtlpValue::StringValue("from-scope".to_string()),
1537 )];
1538 scope_logs.log_records = vec![LogRecord::default()];
1539 request.resource_logs[0].scope_logs.push(scope_logs);
1540 let rows = parse_with_select(request, "service_name,event_name,host", None).unwrap();
1541
1542 for column in [SERVICE_NAME_COLUMN, EVENT_NAME_COLUMN] {
1543 assert_eq!(
1544 rows.schema
1545 .iter()
1546 .filter(|schema| schema.column_name == column)
1547 .count(),
1548 1
1549 );
1550 }
1551 assert_eq!(
1552 value_at(&rows, 0, SERVICE_NAME_COLUMN),
1553 Some(ValueData::StringValue("from-log".to_string()))
1554 );
1555 assert_eq!(
1556 value_at(&rows, 0, EVENT_NAME_COLUMN),
1557 Some(ValueData::StringValue("from-record".to_string()))
1558 );
1559 assert_eq!(
1560 value_at(&rows, 1, SERVICE_NAME_COLUMN),
1561 Some(ValueData::StringValue("from-resource".to_string()))
1562 );
1563 assert_eq!(
1564 value_at(&rows, 2, SERVICE_NAME_COLUMN),
1565 Some(ValueData::StringValue("from-scope".to_string()))
1566 );
1567 }
1568
1569 #[test]
1570 fn test_non_scalar_resource_service_name_is_not_an_error() {
1571 let request = request_with_resource_and_logs(
1572 vec![kv(
1573 KEY_SERVICE_NAME,
1574 OtlpValue::ArrayValue(Default::default()),
1575 )],
1576 vec![LogRecord::default()],
1577 );
1578 let rows = parse_with_select(request, "", None).unwrap();
1579
1580 assert_eq!(value_at(&rows, 0, SERVICE_NAME_COLUMN), None);
1581 }
1582
1583 #[test]
1584 fn test_fresh_table_trace_flags_is_int32() {
1585 let rows = parse_with_select(request_with_log_attrs(vec![]), "", None).unwrap();
1589 let idx = column_index(&rows, "trace_flags");
1590
1591 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Int32 as i32);
1592 assert_eq!(
1593 rows.rows[0].values[idx].value_data,
1594 Some(ValueData::I32Value(0))
1595 );
1596 }
1597
1598 #[test]
1599 fn test_existing_uint32_trace_flags_keeps_type_and_coerces_int32_request() {
1600 let existing = existing_uint32_trace_flags_schema();
1603
1604 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1605 let idx = column_index(&rows, "trace_flags");
1606
1607 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1608 assert_eq!(
1609 rows.rows[0].values[idx].value_data,
1610 Some(ValueData::U32Value(0))
1611 );
1612 }
1613
1614 #[test]
1615 fn test_existing_uint32_trace_flags_preserves_nonzero_flags_value() {
1616 let existing = existing_uint32_trace_flags_schema();
1621
1622 let rows = parse_with_select(
1623 request_with_log_attrs_and_flags(vec![], 256),
1624 "",
1625 Some(&existing),
1626 )
1627 .unwrap();
1628 let idx = column_index(&rows, "trace_flags");
1629
1630 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1631 assert_eq!(
1632 rows.rows[0].values[idx].value_data,
1633 Some(ValueData::U32Value(256))
1634 );
1635 }
1636
1637 #[test]
1638 fn test_existing_uint32_trace_flags_preserves_high_bit_flags_value() {
1639 let existing = existing_uint32_trace_flags_schema();
1644
1645 let rows = parse_with_select(
1646 request_with_log_attrs_and_flags(vec![], 0x8000_0000),
1647 "",
1648 Some(&existing),
1649 )
1650 .unwrap();
1651 let idx = column_index(&rows, "trace_flags");
1652
1653 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1654 assert_eq!(
1655 rows.rows[0].values[idx].value_data,
1656 Some(ValueData::U32Value(0x8000_0000))
1657 );
1658 }
1659
1660 #[test]
1661 fn test_existing_primary_key_updates_builtin_column_semantic_type() {
1662 let existing = existing_schema(
1663 vec![
1664 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1665 column("trace_id", ConcreteDataType::string_datatype()),
1666 ],
1667 &[1],
1668 );
1669
1670 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1671 let trace_id_idx = column_index(&rows, "trace_id");
1672
1673 assert_eq!(
1674 rows.schema[trace_id_idx].semantic_type,
1675 SemanticType::Tag as i32
1676 );
1677 }
1678
1679 #[test]
1680 fn test_existing_string_primary_key_stringifies_selected_scalar_values() {
1681 let existing = existing_schema(
1682 vec![
1683 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1684 column("host", ConcreteDataType::string_datatype()),
1685 ],
1686 &[1],
1687 );
1688 let rows = parse_with_select(
1689 request_with_log_attrs(vec![kv("host", OtlpValue::IntValue(42))]),
1690 "host",
1691 Some(&existing),
1692 )
1693 .unwrap();
1694 let host_idx = column_index(&rows, "host");
1695
1696 assert_eq!(
1697 rows.schema[host_idx].datatype,
1698 ColumnDataType::String as i32
1699 );
1700 assert_eq!(
1701 rows.schema[host_idx].semantic_type,
1702 SemanticType::Tag as i32
1703 );
1704 assert_eq!(
1705 rows.rows[0].values[host_idx].value_data,
1706 Some(ValueData::StringValue("42".to_string()))
1707 );
1708 }
1709
1710 #[test]
1711 fn test_existing_string_field_stringifies_selected_scalar_values() {
1712 let existing = existing_schema(
1713 vec![
1714 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1715 column("host", ConcreteDataType::string_datatype()),
1716 ],
1717 &[],
1718 );
1719 let rows = parse_with_select(
1720 request_with_log_attrs(vec![kv("host", OtlpValue::IntValue(42))]),
1721 "host",
1722 Some(&existing),
1723 )
1724 .unwrap();
1725 let host_idx = column_index(&rows, "host");
1726
1727 assert_eq!(
1728 rows.schema[host_idx].datatype,
1729 ColumnDataType::String as i32
1730 );
1731 assert_eq!(
1732 rows.schema[host_idx].semantic_type,
1733 SemanticType::Field as i32
1734 );
1735 assert_eq!(
1736 rows.rows[0].values[host_idx].value_data,
1737 Some(ValueData::StringValue("42".to_string()))
1738 );
1739 }
1740
1741 #[test]
1742 fn test_existing_non_string_primary_key_rejects_incompatible_selected_value() {
1743 let existing = existing_schema(
1744 vec![
1745 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1746 column("host", ConcreteDataType::int64_datatype()),
1747 ],
1748 &[1],
1749 );
1750 let err = parse_with_select(
1751 request_with_log_attrs(vec![kv(
1752 "host",
1753 OtlpValue::StringValue("node-a".to_string()),
1754 )]),
1755 "host",
1756 Some(&existing),
1757 )
1758 .unwrap_err();
1759
1760 assert!(
1761 err.to_string()
1762 .contains("failed to align log column 'host'")
1763 );
1764 }
1765
1766 #[test]
1767 fn test_existing_timestamp_unit_is_respected() {
1768 let existing = existing_schema(
1769 vec![time_column(
1770 ConcreteDataType::timestamp_millisecond_datatype(),
1771 )],
1772 &[],
1773 );
1774 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1775
1776 assert_eq!(
1777 rows.schema[0].datatype,
1778 ColumnDataType::TimestampMillisecond as i32
1779 );
1780 assert_eq!(
1781 rows.rows[0].values[0].value_data,
1782 Some(ValueData::TimestampMillisecondValue(1234))
1783 );
1784 }
1785
1786 #[test]
1787 fn test_missing_existing_primary_key_is_not_generated() {
1788 let existing = existing_schema(
1789 vec![
1790 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1791 column("host", ConcreteDataType::string_datatype()),
1792 ],
1793 &[1],
1794 );
1795 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1796
1797 assert!(
1798 !rows
1799 .schema
1800 .iter()
1801 .any(|schema| schema.column_name == "host")
1802 );
1803 }
1804
1805 #[test]
1806 fn test_existing_table_keeps_new_generated_columns_as_fields() {
1807 let existing = existing_schema(
1808 vec![
1809 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1810 column("trace_id", ConcreteDataType::string_datatype()),
1811 ],
1812 &[1],
1813 );
1814 let rows = parse_with_select(
1815 request_with_log_attrs(vec![kv(
1816 "host",
1817 OtlpValue::StringValue("node-a".to_string()),
1818 )]),
1819 "host",
1820 Some(&existing),
1821 )
1822 .unwrap();
1823 let host_idx = column_index(&rows, "host");
1824 let scope_name_idx = column_index(&rows, "scope_name");
1825
1826 assert_eq!(
1827 rows.schema[host_idx].semantic_type,
1828 SemanticType::Field as i32
1829 );
1830 assert_eq!(
1831 rows.schema[scope_name_idx].semantic_type,
1832 SemanticType::Field as i32
1833 );
1834 for column in [
1836 SERVICE_NAME_COLUMN,
1837 EVENT_NAME_COLUMN,
1838 OBSERVED_TIMESTAMP_COLUMN,
1839 ] {
1840 assert!(
1841 rows.schema
1842 .iter()
1843 .all(|schema| schema.column_name != column)
1844 );
1845 }
1846 }
1847
1848 #[test]
1849 fn test_existing_uint64_column_keeps_type_and_coerces_int64_request() {
1850 let existing = existing_schema(
1854 vec![
1855 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1856 column("counter", ConcreteDataType::uint64_datatype()),
1857 ],
1858 &[],
1859 );
1860
1861 let rows = parse_with_select(
1862 request_with_log_attrs(vec![kv("counter", OtlpValue::IntValue(42))]),
1863 "counter",
1864 Some(&existing),
1865 )
1866 .unwrap();
1867 let idx = column_index(&rows, "counter");
1868
1869 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint64 as i32);
1870 assert_eq!(
1871 rows.rows[0].values[idx].value_data,
1872 Some(ValueData::U64Value(42))
1873 );
1874 }
1875
1876 #[test]
1877 fn test_jsonb_extended_log_values_rejected() {
1878 for (value, expected_type) in [
1879 (
1880 JsonbValue::Number(JsonbNumber::Decimal64(jsonb::Decimal64 {
1881 value: 123,
1882 scale: 2,
1883 })),
1884 "DECIMAL",
1885 ),
1886 (JsonbValue::Binary(&[1, 2, 3]), "Binary"),
1887 (JsonbValue::Date(jsonb::Date { value: 0 }), "Date"),
1888 (
1889 JsonbValue::Timestamp(jsonb::Timestamp { value: 0 }),
1890 "Timestamp",
1891 ),
1892 (
1893 JsonbValue::TimestampTz(jsonb::TimestampTz {
1894 value: 0,
1895 offset: 0,
1896 }),
1897 "TimestampTz",
1898 ),
1899 (
1900 JsonbValue::Interval(jsonb::Interval {
1901 months: 0,
1902 days: 0,
1903 micros: 0,
1904 }),
1905 "Interval",
1906 ),
1907 ] {
1908 let schema_error =
1909 decide_column_schema_and_convert_value("attr", value.clone(), None, "logs")
1910 .expect_err("extended JSONB type must be rejected");
1911 let value_error = jsonb_value_to_log_value_data("attr", value, true)
1912 .expect_err("extended JSONB type must be rejected");
1913 for error in [schema_error, value_error] {
1914 assert!(matches!(error,
1915 Error::UnsupportedJsonDataTypeForTag { key, ty, .. }
1916 if key == "attr" && ty == expected_type
1917 ));
1918 }
1919 }
1920 }
1921
1922 #[test]
1923 fn test_jsonb_uint64_exceeding_i64_range_rejected() {
1924 let err = jsonb_value_to_log_value_data(
1929 "counter",
1930 JsonbValue::Number(JsonbNumber::UInt64(u64::MAX)),
1931 false,
1932 )
1933 .unwrap_err();
1934
1935 assert!(
1936 err.to_string()
1937 .contains("exceeds the i64 range supported by built-in log columns")
1938 );
1939 }
1940
1941 #[test]
1942 fn test_existing_int64_column_rejects_numeric_string_value() {
1943 let existing = existing_schema(
1949 vec![
1950 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1951 column("counter", ConcreteDataType::int64_datatype()),
1952 ],
1953 &[],
1954 );
1955
1956 let err = parse_with_select(
1957 request_with_log_attrs(vec![kv(
1958 "counter",
1959 OtlpValue::StringValue("42".to_string()),
1960 )]),
1961 "counter",
1962 Some(&existing),
1963 )
1964 .unwrap_err();
1965
1966 assert!(
1967 err.to_string()
1968 .contains("failed to align log column 'counter'")
1969 );
1970 }
1971}