1use common_catalog::consts::{
34 RESOURCE_ATTRIBUTES_COLUMN, SCOPE_ATTRIBUTES_COLUMN, SPAN_ATTRIBUTES_COLUMN,
35};
36use datatypes::prelude::ConcreteDataType;
37use datatypes::schema::{ColumnSchema, Schema};
38
39use crate::requests::TABLE_DATA_MODEL_TRACE_V2;
40
41pub const SEMANTIC_PREFIX: &str = "greptime.semantic.";
43
44pub const SEMANTIC_PER_TABLE_INDEX_KEY: &str = "greptime.internal.semantic.per_table_index";
49
50pub const SEMANTIC_SIGNAL_TYPE: &str = "greptime.semantic.signal_type";
55pub const SEMANTIC_SOURCE: &str = "greptime.semantic.source";
57pub const SEMANTIC_SOURCE_VERSION: &str = "greptime.semantic.source_version";
59pub const SEMANTIC_PIPELINE: &str = "greptime.semantic.pipeline";
62
63pub const SEMANTIC_TRACE_CONVENTIONS: &str = "greptime.semantic.trace.conventions";
68
69pub const SEMANTIC_METRIC_TYPE: &str = "greptime.semantic.metric.type";
74pub const SEMANTIC_METRIC_UNIT: &str = "greptime.semantic.metric.unit";
77pub const SEMANTIC_METRIC_TEMPORALITY: &str = "greptime.semantic.metric.temporality";
80pub const SEMANTIC_METRIC_METADATA_QUALITY: &str = "greptime.semantic.metric.metadata_quality";
83pub const SEMANTIC_METRIC_ORIGINAL_NAME: &str = "greptime.semantic.metric.original_name";
86
87pub const SEMANTIC_ENTITY_PREFIX: &str = "greptime.semantic.entity.";
96
97pub const SEMANTIC_ENTITY_SERVICE_ID: &str = "greptime.semantic.entity.service.id";
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum EntityRole {
107 Id,
112 Descriptive,
113 Scope,
114}
115
116impl EntityRole {
117 fn parse(role: &str) -> Option<Self> {
118 match role {
119 "id" => Some(EntityRole::Id),
120 "descriptive" => Some(EntityRole::Descriptive),
121 "scope" => Some(EntityRole::Scope),
122 _ => None,
123 }
124 }
125}
126
127pub const SIGNAL_TYPE_TRACE: &str = "trace";
130pub const SIGNAL_TYPE_LOG: &str = "log";
131pub const SIGNAL_TYPE_METRIC: &str = "metric";
132pub const SIGNAL_TYPE_EVENT: &str = "event";
133
134pub const SOURCE_OPENTELEMETRY: &str = "opentelemetry";
135pub const SOURCE_PROMETHEUS: &str = "prometheus";
136pub const SOURCE_INFLUXDB: &str = "influxdb";
137pub const SOURCE_OPENTSDB: &str = "opentsdb";
138pub const SOURCE_LOKI: &str = "loki";
139pub const SOURCE_ELASTICSEARCH: &str = "elasticsearch";
140
141pub const METADATA_QUALITY_DECLARED: &str = "declared";
142pub const METADATA_QUALITY_INFERRED: &str = "inferred";
143
144pub const METRIC_TEMPORALITY_CUMULATIVE: &str = "cumulative";
145pub const METRIC_TEMPORALITY_DELTA: &str = "delta";
146
147pub const SEMANTIC_VALUE_UNKNOWN: &str = "unknown";
149pub const SEMANTIC_VALUE_MIXED: &str = "mixed";
151
152pub const SEMANTIC_OPTION_KEYS: &[&str] = &[
157 SEMANTIC_SIGNAL_TYPE,
158 SEMANTIC_SOURCE,
159 SEMANTIC_SOURCE_VERSION,
160 SEMANTIC_PIPELINE,
161 SEMANTIC_TRACE_CONVENTIONS,
162 SEMANTIC_METRIC_TYPE,
163 SEMANTIC_METRIC_UNIT,
164 SEMANTIC_METRIC_TEMPORALITY,
165 SEMANTIC_METRIC_METADATA_QUALITY,
166 SEMANTIC_METRIC_ORIGINAL_NAME,
167];
168
169fn is_valid_entity_type(ty: &str) -> bool {
174 !ty.is_empty()
175 && ty.split('.').all(|seg| {
176 !seg.is_empty()
177 && seg
178 .bytes()
179 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
180 })
181}
182
183pub fn parse_entity_option_key(key: &str) -> Option<(&str, EntityRole)> {
189 let rest = key.strip_prefix(SEMANTIC_ENTITY_PREFIX)?;
190 let (ty, role) = rest.rsplit_once('.')?;
191 if !is_valid_entity_type(ty) {
192 return None;
193 }
194 Some((ty, EntityRole::parse(role)?))
195}
196
197pub fn is_entity_option_key(key: &str) -> bool {
199 parse_entity_option_key(key).is_some()
200}
201
202pub fn trace_v2_attribute<'a>(
206 schema: &Schema,
207 data_model: Option<&str>,
208 column: &'a str,
209) -> Option<(&'a str, &'a str)> {
210 if data_model != Some(TABLE_DATA_MODEL_TRACE_V2)
211 || schema.column_schema_by_name(column).is_some()
212 {
213 return None;
214 }
215 let (root, key) = column.split_once('.')?;
216 (!key.is_empty()
217 && schema
218 .column_schema_by_name(root)
219 .is_some_and(is_v2_attribute_column))
220 .then_some((root, key))
221}
222
223fn is_v2_attribute_column(schema: &ColumnSchema) -> bool {
224 matches!(
225 schema.name.as_str(),
226 RESOURCE_ATTRIBUTES_COLUMN | SCOPE_ATTRIBUTES_COLUMN | SPAN_ATTRIBUTES_COLUMN
227 ) && schema.data_type.is_json2()
228}
229
230pub fn has_stable_string_form(data_type: &ConcreteDataType) -> bool {
236 !matches!(
237 data_type,
238 ConcreteDataType::Binary(_)
239 | ConcreteDataType::Json(_)
240 | ConcreteDataType::Vector(_)
241 | ConcreteDataType::List(_)
242 | ConcreteDataType::Struct(_)
243 | ConcreteDataType::Dictionary(_)
244 | ConcreteDataType::Null(_)
245 )
246}
247
248pub fn parse_entity_columns(value: &str) -> Vec<String> {
252 value
253 .split(',')
254 .map(str::trim)
255 .filter(|column| !column.is_empty())
256 .map(String::from)
257 .collect()
258}
259
260pub fn is_semantic_option_key(key: &str) -> bool {
267 SEMANTIC_OPTION_KEYS.contains(&key) || is_entity_option_key(key)
268}
269
270pub fn validate_semantic_option(key: &str, value: &str) -> bool {
281 if is_entity_option_key(key) {
282 return !value.is_empty() && value.split(',').all(|column| !column.trim().is_empty());
283 }
284 match key {
285 SEMANTIC_PIPELINE
286 | SEMANTIC_SOURCE_VERSION
287 | SEMANTIC_METRIC_UNIT
288 | SEMANTIC_METRIC_ORIGINAL_NAME
289 | SEMANTIC_TRACE_CONVENTIONS => !value.is_empty(),
290
291 SEMANTIC_SIGNAL_TYPE => matches!(value, "trace" | "log" | "metric" | "event" | "unknown"),
292 SEMANTIC_SOURCE => matches!(
293 value,
294 "opentelemetry"
295 | "prometheus"
296 | "influxdb"
297 | "opentsdb"
298 | "elasticsearch"
299 | "loki"
300 | "custom"
301 | "mixed"
302 | "unknown"
303 ),
304 SEMANTIC_METRIC_TYPE => matches!(
305 value,
306 "counter"
307 | "gauge"
308 | "histogram"
309 | "summary"
310 | "updown_counter"
311 | "gauge_histogram"
312 | "info"
313 | "stateset"
314 | "mixed"
315 | "unknown"
316 ),
317 SEMANTIC_METRIC_TEMPORALITY => {
318 matches!(
319 value,
320 METRIC_TEMPORALITY_CUMULATIVE | METRIC_TEMPORALITY_DELTA | "mixed" | "unknown"
321 )
322 }
323 SEMANTIC_METRIC_METADATA_QUALITY => matches!(value, "declared" | "inferred" | "unknown"),
324
325 _ => false,
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use datatypes::types::JsonType;
332
333 use super::*;
334
335 #[test]
336 fn test_trace_v2_attribute_references() {
337 let schema = Schema::new(vec![
338 ColumnSchema::new(
339 RESOURCE_ATTRIBUTES_COLUMN,
340 ConcreteDataType::Json(JsonType::null()),
341 true,
342 ),
343 ColumnSchema::new(
344 SCOPE_ATTRIBUTES_COLUMN,
345 ConcreteDataType::string_datatype(),
346 true,
347 ),
348 ColumnSchema::new(
349 "resource_attributes.physical",
350 ConcreteDataType::string_datatype(),
351 true,
352 ),
353 ]);
354 let model = Some(TABLE_DATA_MODEL_TRACE_V2);
355 for key in ["host.name", "host..name", "host[0]", "host.*", r#"a"b\c.d"#] {
356 let column = format!("resource_attributes.{key}");
357 assert_eq!(
358 trace_v2_attribute(&schema, model, &column),
359 Some((RESOURCE_ATTRIBUTES_COLUMN, key))
360 );
361 }
362 for column in [
363 "resource_attributes",
364 "resource_attributes.",
365 "scope_attributes.name",
366 "span_attributes.name",
367 "resource_attributes.physical",
368 "missing.key",
369 ] {
370 assert_eq!(trace_v2_attribute(&schema, model, column), None, "{column}");
371 }
372 for model in [None, Some(crate::requests::TABLE_DATA_MODEL_TRACE_V1)] {
373 assert_eq!(
374 trace_v2_attribute(&schema, model, "resource_attributes.host.name"),
375 None
376 );
377 }
378 }
379
380 #[test]
381 fn test_is_semantic_option_key() {
382 assert!(is_semantic_option_key(SEMANTIC_SIGNAL_TYPE));
383 assert!(is_semantic_option_key(SEMANTIC_METRIC_TYPE));
384 assert!(is_semantic_option_key(SEMANTIC_PIPELINE));
385
386 assert!(!is_semantic_option_key("greptime.semantic.future.key"));
388 assert!(!is_semantic_option_key("greptime.semantic.unknown_key"));
389 assert!(!is_semantic_option_key(
391 "greptime.semantic.metric.monotonic"
392 ));
393 assert!(!is_semantic_option_key(
394 "greptime.semantic.resource.attributes_dropped"
395 ));
396 assert!(!is_semantic_option_key("greptime.semanticx"));
398 assert!(!is_semantic_option_key("semantic.signal_type"));
399 assert!(!is_semantic_option_key("table_data_model"));
400 assert!(!is_semantic_option_key(SEMANTIC_PER_TABLE_INDEX_KEY));
402 }
403
404 #[test]
405 fn test_validate_semantic_option() {
406 assert!(validate_semantic_option(SEMANTIC_SIGNAL_TYPE, "metric"));
408 assert!(!validate_semantic_option(SEMANTIC_SIGNAL_TYPE, "spans"));
409 assert!(validate_semantic_option(SEMANTIC_METRIC_TYPE, "counter"));
410 assert!(validate_semantic_option(SEMANTIC_METRIC_TYPE, "mixed"));
411 assert!(!validate_semantic_option(SEMANTIC_METRIC_TYPE, "bogus"));
412
413 assert!(validate_semantic_option(
415 SEMANTIC_METRIC_TEMPORALITY,
416 "unknown"
417 ));
418 assert!(validate_semantic_option(SEMANTIC_METRIC_UNIT, "By"));
419 assert!(!validate_semantic_option(SEMANTIC_METRIC_UNIT, ""));
420 assert!(validate_semantic_option(
421 SEMANTIC_PIPELINE,
422 "greptime_trace_v1"
423 ));
424
425 assert!(!validate_semantic_option(
427 "greptime.semantic.metric.monotonic",
428 "true"
429 ));
430 assert!(!validate_semantic_option(
432 "greptime.semantic.future.key",
433 "x"
434 ));
435
436 assert!(validate_semantic_option(
438 SEMANTIC_SIGNAL_TYPE,
439 SIGNAL_TYPE_TRACE
440 ));
441 assert!(validate_semantic_option(
442 SEMANTIC_SIGNAL_TYPE,
443 SIGNAL_TYPE_METRIC
444 ));
445 assert!(validate_semantic_option(
446 SEMANTIC_SIGNAL_TYPE,
447 SIGNAL_TYPE_LOG
448 ));
449 assert!(validate_semantic_option(
450 SEMANTIC_SOURCE,
451 SOURCE_OPENTELEMETRY
452 ));
453 assert!(validate_semantic_option(SEMANTIC_SOURCE, SOURCE_PROMETHEUS));
454 assert!(validate_semantic_option(SEMANTIC_SOURCE_VERSION, "2.0"));
455 assert!(validate_semantic_option(
456 SEMANTIC_METRIC_METADATA_QUALITY,
457 METADATA_QUALITY_INFERRED
458 ));
459 assert!(validate_semantic_option(
460 SEMANTIC_METRIC_METADATA_QUALITY,
461 METADATA_QUALITY_DECLARED
462 ));
463 assert!(validate_semantic_option(
464 SEMANTIC_TRACE_CONVENTIONS,
465 SEMANTIC_VALUE_UNKNOWN
466 ));
467 for key in SEMANTIC_OPTION_KEYS {
469 assert!(
470 !validate_semantic_option(key, ""),
471 "empty value should never validate for {key}"
472 );
473 }
474 }
475
476 #[test]
477 fn test_entity_option_key() {
478 for key in [
481 "greptime.semantic.entity.service.id",
482 "greptime.semantic.entity.k8s.pod.id",
483 "greptime.semantic.entity.service.instance.descriptive",
484 "greptime.semantic.entity.host.scope",
485 "greptime.semantic.entity.agent.id",
486 ] {
487 assert!(is_entity_option_key(key), "should accept {key}");
488 assert!(is_semantic_option_key(key), "should recognise {key}");
489 }
490
491 for key in [
494 "greptime.semantic.entity..id",
495 "greptime.semantic.entity.service.bogusrole",
496 "greptime.semantic.entity.service",
497 "greptime.semantic.entity.",
498 "greptime.semantic.entity.Service.id",
499 ] {
500 assert!(!is_entity_option_key(key), "should reject {key}");
501 assert!(!is_semantic_option_key(key), "should not recognise {key}");
502 }
503
504 assert!(!is_entity_option_key(SEMANTIC_SIGNAL_TYPE));
506
507 assert!(is_entity_option_key(SEMANTIC_ENTITY_SERVICE_ID));
509 assert!(is_semantic_option_key(SEMANTIC_ENTITY_SERVICE_ID));
510 }
511
512 #[test]
513 fn test_validate_entity_option() {
514 assert!(validate_semantic_option(
516 "greptime.semantic.entity.service.id",
517 "service_name"
518 ));
519 assert!(validate_semantic_option(
520 "greptime.semantic.entity.process.id",
521 "pid,start_time"
522 ));
523 assert!(!validate_semantic_option(
525 "greptime.semantic.entity.service.id",
526 ""
527 ));
528 assert!(!validate_semantic_option(
529 "greptime.semantic.entity.process.id",
530 "pid,"
531 ));
532 assert!(!validate_semantic_option(
534 "greptime.semantic.entity.service.bogusrole",
535 "service_name"
536 ));
537 }
538}