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