Skip to main content

table/requests/
semantic.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Table semantic layer vocabulary.
16//!
17//! A thin layer of semantic metadata attached to a table via `table_options`, so
18//! machine consumers (LLM agents, alert/dashboard builders, MCP servers, ETL) can
19//! align a table with the observability concept it stands for without guessing
20//! from column names. See `docs/rfcs/2026-05-28-table-semantic-layer.md`.
21//!
22//! The vocabulary is intentionally small: a key earns its place only when it
23//! records something a consumer cannot cheaply and reliably recover from the
24//! schema/data itself. Keys whose value is already in the metric name by
25//! convention, is a constant, or duplicates an existing column are deliberately
26//! omitted rather than stamped for completeness.
27//!
28//! All public keys share the [`SEMANTIC_PREFIX`] namespace and are string-valued.
29//! [`is_semantic_option_key`] gates them through
30//! [`crate::requests::validate_table_option`], so they are accepted both on the
31//! ingestion auto-create path and on explicit `CREATE TABLE ... WITH (...)` DDL.
32
33/// Reserved prefix for every public semantic table-option key.
34pub const SEMANTIC_PREFIX: &str = "greptime.semantic.";
35
36/// Internal `QueryContext` extension key carrying the per-table semantic index
37/// (a `{table_name -> {semantic_key: value}}` JSON blob) from the ingestion
38/// encode path to the auto-create site. Deliberately OUTSIDE [`SEMANTIC_PREFIX`]
39/// so it is not a valid table option and never leaks into a table's options.
40pub const SEMANTIC_PER_TABLE_INDEX_KEY: &str = "greptime.internal.semantic.per_table_index";
41
42// ---- Common keys (all signals) ----
43
44/// Signal kind: one of [`SIGNAL_TYPE_TRACE`] / [`SIGNAL_TYPE_LOG`] /
45/// [`SIGNAL_TYPE_METRIC`] / [`SIGNAL_TYPE_EVENT`].
46pub const SEMANTIC_SIGNAL_TYPE: &str = "greptime.semantic.signal_type";
47/// Ingestion ecosystem, e.g. [`SOURCE_OPENTELEMETRY`] / [`SOURCE_PROMETHEUS`].
48pub const SEMANTIC_SOURCE: &str = "greptime.semantic.source";
49/// Source protocol version, e.g. Prometheus remote write `1.0` / `2.0`.
50pub const SEMANTIC_SOURCE_VERSION: &str = "greptime.semantic.source_version";
51/// Internal ingestion pipeline / data model, e.g. `greptime_trace_v1`. The
52/// signal-agnostic successor to the engine-specific `table_data_model` option.
53pub const SEMANTIC_PIPELINE: &str = "greptime.semantic.pipeline";
54
55// ---- Trace keys ----
56
57/// Semantic-conventions version the rows conform to (e.g. the OTel schema URL),
58/// or [`SEMANTIC_VALUE_UNKNOWN`] / [`SEMANTIC_VALUE_MIXED`] when not single-valued.
59pub const SEMANTIC_TRACE_CONVENTIONS: &str = "greptime.semantic.trace.conventions";
60
61// ---- Metric keys ----
62
63/// Instrument kind: `counter` / `gauge` / `histogram` / `summary` /
64/// `updown_counter` / `gauge_histogram` / `info` / `stateset`.
65pub const SEMANTIC_METRIC_TYPE: &str = "greptime.semantic.metric.type";
66/// UCUM unit, e.g. `s`, `By`, `{request}`. Discarded by the row encoders, so it
67/// is unrecoverable once ingested.
68pub const SEMANTIC_METRIC_UNIT: &str = "greptime.semantic.metric.unit";
69/// `cumulative` / `delta` (OTel only). Invisible in the metric name, so it is
70/// unrecoverable from the table alone.
71pub const SEMANTIC_METRIC_TEMPORALITY: &str = "greptime.semantic.metric.temporality";
72/// [`METADATA_QUALITY_DECLARED`] when the protocol stated the type, or
73/// [`METADATA_QUALITY_INFERRED`] when guessed from a name suffix.
74pub const SEMANTIC_METRIC_METADATA_QUALITY: &str = "greptime.semantic.metric.metadata_quality";
75/// Pre-translation OTel name when the table name was Prometheus-ised; the key a
76/// consumer uses to look the metric up in the OTel semantic conventions.
77pub const SEMANTIC_METRIC_ORIGINAL_NAME: &str = "greptime.semantic.metric.original_name";
78
79// ---- Value constants ----
80
81pub const SIGNAL_TYPE_TRACE: &str = "trace";
82pub const SIGNAL_TYPE_LOG: &str = "log";
83pub const SIGNAL_TYPE_METRIC: &str = "metric";
84pub const SIGNAL_TYPE_EVENT: &str = "event";
85
86pub const SOURCE_OPENTELEMETRY: &str = "opentelemetry";
87pub const SOURCE_PROMETHEUS: &str = "prometheus";
88pub const SOURCE_INFLUXDB: &str = "influxdb";
89pub const SOURCE_OPENTSDB: &str = "opentsdb";
90pub const SOURCE_LOKI: &str = "loki";
91pub const SOURCE_ELASTICSEARCH: &str = "elasticsearch";
92
93pub const METADATA_QUALITY_DECLARED: &str = "declared";
94pub const METADATA_QUALITY_INFERRED: &str = "inferred";
95
96/// Sentinel for a key that cannot be determined at stamp time.
97pub const SEMANTIC_VALUE_UNKNOWN: &str = "unknown";
98/// Sentinel for a single-valued key that saw conflicting sources.
99pub const SEMANTIC_VALUE_MIXED: &str = "mixed";
100
101/// Every recognised public semantic table-option key. The set is a closed
102/// whitelist: keys under [`SEMANTIC_PREFIX`] that are not listed here are rejected,
103/// so an unknown key like `greptime.semantic.unknown_key` does not silently land
104/// in a table's options. Adding a key to the vocabulary means adding it here.
105pub const SEMANTIC_OPTION_KEYS: &[&str] = &[
106    SEMANTIC_SIGNAL_TYPE,
107    SEMANTIC_SOURCE,
108    SEMANTIC_SOURCE_VERSION,
109    SEMANTIC_PIPELINE,
110    SEMANTIC_TRACE_CONVENTIONS,
111    SEMANTIC_METRIC_TYPE,
112    SEMANTIC_METRIC_UNIT,
113    SEMANTIC_METRIC_TEMPORALITY,
114    SEMANTIC_METRIC_METADATA_QUALITY,
115    SEMANTIC_METRIC_ORIGINAL_NAME,
116];
117
118/// Returns true if `key` is a recognised semantic table-option key (whitelist).
119///
120/// Note this is membership, not a prefix test: unknown keys under
121/// [`SEMANTIC_PREFIX`] are rejected, and the internal
122/// [`SEMANTIC_PER_TABLE_INDEX_KEY`] (outside the prefix) never matches.
123pub fn is_semantic_option_key(key: &str) -> bool {
124    SEMANTIC_OPTION_KEYS.contains(&key)
125}
126
127/// Validates a `greptime.semantic.*` option's `value` against its allowed domain.
128///
129/// Open-value keys (unit, original_name, pipeline, conventions) accept any
130/// non-empty string. Closed-domain keys accept a fixed set, plus the `unknown`
131/// sentinel, plus `mixed` for the keys where one long-lived table can
132/// legitimately see multiple values. Keys not in [`SEMANTIC_OPTION_KEYS`] are
133/// rejected.
134pub fn validate_semantic_option(key: &str, value: &str) -> bool {
135    match key {
136        SEMANTIC_PIPELINE
137        | SEMANTIC_SOURCE_VERSION
138        | SEMANTIC_METRIC_UNIT
139        | SEMANTIC_METRIC_ORIGINAL_NAME
140        | SEMANTIC_TRACE_CONVENTIONS => !value.is_empty(),
141
142        SEMANTIC_SIGNAL_TYPE => matches!(value, "trace" | "log" | "metric" | "event" | "unknown"),
143        SEMANTIC_SOURCE => matches!(
144            value,
145            "opentelemetry"
146                | "prometheus"
147                | "influxdb"
148                | "opentsdb"
149                | "elasticsearch"
150                | "loki"
151                | "custom"
152                | "mixed"
153                | "unknown"
154        ),
155        SEMANTIC_METRIC_TYPE => matches!(
156            value,
157            "counter"
158                | "gauge"
159                | "histogram"
160                | "summary"
161                | "updown_counter"
162                | "gauge_histogram"
163                | "info"
164                | "stateset"
165                | "mixed"
166                | "unknown"
167        ),
168        SEMANTIC_METRIC_TEMPORALITY => {
169            matches!(value, "cumulative" | "delta" | "mixed" | "unknown")
170        }
171        SEMANTIC_METRIC_METADATA_QUALITY => matches!(value, "declared" | "inferred" | "unknown"),
172
173        _ => false,
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_is_semantic_option_key() {
183        assert!(is_semantic_option_key(SEMANTIC_SIGNAL_TYPE));
184        assert!(is_semantic_option_key(SEMANTIC_METRIC_TYPE));
185        assert!(is_semantic_option_key(SEMANTIC_PIPELINE));
186
187        // Unknown keys under the prefix are not whitelisted.
188        assert!(!is_semantic_option_key("greptime.semantic.future.key"));
189        assert!(!is_semantic_option_key("greptime.semantic.unknown_key"));
190        // Keys cut from the vocabulary are no longer accepted.
191        assert!(!is_semantic_option_key(
192            "greptime.semantic.metric.monotonic"
193        ));
194        assert!(!is_semantic_option_key(
195            "greptime.semantic.resource.attributes_dropped"
196        ));
197        // Near-misses must not match.
198        assert!(!is_semantic_option_key("greptime.semanticx"));
199        assert!(!is_semantic_option_key("semantic.signal_type"));
200        assert!(!is_semantic_option_key("table_data_model"));
201        // The internal transport key must never be treated as a table option.
202        assert!(!is_semantic_option_key(SEMANTIC_PER_TABLE_INDEX_KEY));
203    }
204
205    #[test]
206    fn test_validate_semantic_option() {
207        // Enum keys reject out-of-domain values.
208        assert!(validate_semantic_option(SEMANTIC_SIGNAL_TYPE, "metric"));
209        assert!(!validate_semantic_option(SEMANTIC_SIGNAL_TYPE, "spans"));
210        assert!(validate_semantic_option(SEMANTIC_METRIC_TYPE, "counter"));
211        assert!(validate_semantic_option(SEMANTIC_METRIC_TYPE, "mixed"));
212        assert!(!validate_semantic_option(SEMANTIC_METRIC_TYPE, "bogus"));
213
214        // Sentinels and open values.
215        assert!(validate_semantic_option(
216            SEMANTIC_METRIC_TEMPORALITY,
217            "unknown"
218        ));
219        assert!(validate_semantic_option(SEMANTIC_METRIC_UNIT, "By"));
220        assert!(!validate_semantic_option(SEMANTIC_METRIC_UNIT, ""));
221        assert!(validate_semantic_option(
222            SEMANTIC_PIPELINE,
223            "greptime_trace_v1"
224        ));
225
226        // A cut key validates to false regardless of value.
227        assert!(!validate_semantic_option(
228            "greptime.semantic.metric.monotonic",
229            "true"
230        ));
231        // Unknown key is rejected regardless of value.
232        assert!(!validate_semantic_option(
233            "greptime.semantic.future.key",
234            "x"
235        ));
236
237        // Drift guard: every value stamped by the ingestion path must validate.
238        assert!(validate_semantic_option(
239            SEMANTIC_SIGNAL_TYPE,
240            SIGNAL_TYPE_TRACE
241        ));
242        assert!(validate_semantic_option(
243            SEMANTIC_SIGNAL_TYPE,
244            SIGNAL_TYPE_METRIC
245        ));
246        assert!(validate_semantic_option(
247            SEMANTIC_SIGNAL_TYPE,
248            SIGNAL_TYPE_LOG
249        ));
250        assert!(validate_semantic_option(
251            SEMANTIC_SOURCE,
252            SOURCE_OPENTELEMETRY
253        ));
254        assert!(validate_semantic_option(SEMANTIC_SOURCE, SOURCE_PROMETHEUS));
255        assert!(validate_semantic_option(SEMANTIC_SOURCE_VERSION, "2.0"));
256        assert!(validate_semantic_option(
257            SEMANTIC_METRIC_METADATA_QUALITY,
258            METADATA_QUALITY_INFERRED
259        ));
260        assert!(validate_semantic_option(
261            SEMANTIC_METRIC_METADATA_QUALITY,
262            METADATA_QUALITY_DECLARED
263        ));
264        assert!(validate_semantic_option(
265            SEMANTIC_TRACE_CONVENTIONS,
266            SEMANTIC_VALUE_UNKNOWN
267        ));
268        // An empty value never validates, for any whitelisted key.
269        for key in SEMANTIC_OPTION_KEYS {
270            assert!(
271                !validate_semantic_option(key, ""),
272                "empty value should never validate for {key}"
273            );
274        }
275    }
276}