mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-09-05 13:08:58 +00:00
feat: support raw OTLP delta metrics (#8970)
* feat: support raw OTLP delta metrics Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix: fmt Signed-off-by: shuiyisong <xixing.sys@gmail.com> * test(promql): update sqlness results for normalized label matching Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix: derive temporality label from default column prefix Signed-off-by: shuiyisong <xixing.sys@gmail.com> * test(promql): add analyze coverage for delta temporality Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix(promql): scope label alignment to temporality marker Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix: handle count-only histograms and vector broadcasts Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix: exclude temporality marker from entity descriptions Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix: use a fixed label for OTLP aggregation temporality Signed-off-by: shuiyisong <xixing.sys@gmail.com> * fix(promql): preserve mixed-range semantics for raw delta Signed-off-by: shuiyisong <xixing.sys@gmail.com> --------- Signed-off-by: shuiyisong <xixing.sys@gmail.com>
This commit is contained in:
@@ -78,7 +78,7 @@ concern, not query-time semantics).
|
||||
| --- | --- |
|
||||
| `greptime.semantic.metric.type` | `counter` / `gauge` / `histogram` / `summary` / `updown_counter` / `gauge_histogram` / `info` / `stateset` |
|
||||
| `greptime.semantic.metric.unit` | UCUM, e.g. `s`, `By`, `{request}` (discarded by the row encoders, so unrecoverable once ingested) |
|
||||
| `greptime.semantic.metric.temporality` | `cumulative` / `delta` (OTel only; invisible in the name) |
|
||||
| `greptime.semantic.metric.temporality` | `cumulative` / `delta` / `mixed` (OTel only; catalog-level description) |
|
||||
| `greptime.semantic.metric.metadata_quality` | `declared` (OTLP / exposition) or `inferred` (Prom RW v1, name-suffix guess) |
|
||||
| `greptime.semantic.metric.original_name` | Pre-translation OTel name when the table name was Prometheus-ised; the key a consumer uses to look the metric up in the OTel semantic conventions |
|
||||
|
||||
@@ -93,6 +93,58 @@ Two design decisions worth pinning down up front, because they constrain everyth
|
||||
- **Conflict.** Some table-level keys (`trace.conventions` lifted from `schema_url`, `metric.temporality`, ...) cannot represent the truth when a long-lived table sees rows from multiple sources. v1 records `mixed` or `unknown` rather than a fictitious single value. Downstream consumers must treat any single-valued semantic key as best-effort, not strong evidence.
|
||||
- **Update.** Semantic options are stamped at table creation. v1 does not specify an update path; promoting `metadata_quality` from `inferred` to `declared`, refreshing `resource.attributes_preserved`, or revising `trace.conventions` on later writes is deferred. If real usage shows update is needed, it lands as a separate RFC.
|
||||
|
||||
OTLP delta sums and explicit histograms additionally store the query-visible
|
||||
String tag `otlp_aggregation_temporality="delta"` on each generated row. Its
|
||||
name is fixed and does not follow `default_column_prefix`. The tag is part of
|
||||
series identity and is authoritative for per-series float `rate()` and
|
||||
`increase()` behavior; the table option is never used as a row-level
|
||||
discriminator. Native histograms retain their native algorithms. Prometheus
|
||||
metadata reports `unknown` for counter, histogram, and up/down-counter tables
|
||||
whose catalog temporality is `delta` or `mixed`. A same-request conflict can
|
||||
create `mixed`; a later write does not update an existing table option, so the
|
||||
catalog value can also be stale while the row tag remains authoritative.
|
||||
|
||||
The concrete mixed-temporality workload is a rolling production change from
|
||||
cumulative to delta for the same metric name. Old and new exporters can overlap
|
||||
during rollout, retries and late points can extend that overlap, and retained
|
||||
cumulative history must remain queryable after the fleet converges. Rejecting a
|
||||
different temporality at table level therefore prevents an in-place transition.
|
||||
Routing delta rows to another table either exposes a different metric/table to
|
||||
users or requires a logical union that still needs a per-series temporality
|
||||
discriminator. v1 keeps one table and stores that discriminator on the series.
|
||||
|
||||
The marker remains in PromQL results and follows ordinary label matching and
|
||||
grouping. The supported states of this reserved label are absent/NULL
|
||||
(cumulative) and `delta`. Exact Metric Engine arithmetic and comparison matching
|
||||
reuses the existing `__tsid` key, so mixed-temporality support adds no projected
|
||||
matching columns on that path. When `__tsid` is unavailable and the marker
|
||||
participates in matching, the planner aligns only
|
||||
`otlp_aggregation_temporality`, projecting a nullable String on an input whose
|
||||
schema lacks it; unrelated nullable labels retain their existing matching
|
||||
behavior. Ignoring the marker adds no marker-related matching work.
|
||||
|
||||
Use `ignoring(otlp_aggregation_temporality)` when temporality should not
|
||||
participate in matching. Apply `rate()` or `increase()` before an aggregation
|
||||
or subquery that drops the marker. `irate()` and `resets()` are not
|
||||
temporality-aware in v1; `delta()`, `idelta()`, and `changes()` retain their
|
||||
existing raw-sample semantics.
|
||||
|
||||
`otlp_aggregation_temporality` is a reserved stored key for OTLP metric
|
||||
attributes. Existing float tables that already contain that exact String tag
|
||||
and the value `delta` opt into this behavior after upgrade. Producers should
|
||||
rename such a user-defined label if that was not their intent. A non-OTLP writer
|
||||
may opt in deliberately, while a namespaced OTLP scope attribute cannot collide
|
||||
with the stored key.
|
||||
|
||||
OTLP `NoRecordedValue` sums store the canonical Prometheus stale marker.
|
||||
Classic-histogram tombstones mark the point's supplied bounds, implicit `+Inf`,
|
||||
`_count`, and `_sum` when supplied. If the optional `sum` is absent, no `_sum`
|
||||
stale marker is emitted, so a previously stored `_sum` sample can remain
|
||||
visible to instant selectors until lookback expiry while `_count` and the
|
||||
supplied bucket series are stale. Bounds absent from or changed on the
|
||||
tombstone can likewise remain visible; complete marking would require retained
|
||||
per-stream component and bound-layout history and is outside v1.
|
||||
|
||||
## `information_schema.table_semantics`
|
||||
|
||||
A consumer's first SQL on connect:
|
||||
|
||||
@@ -93,6 +93,10 @@ const GREPTIME_TIMESTAMP: &str = "greptime_timestamp";
|
||||
const GREPTIME_VALUE: &str = "greptime_value";
|
||||
/// Default counter column name for OTLP metrics (legacy mode).
|
||||
pub const GREPTIME_COUNT: &str = "greptime_count";
|
||||
/// Stored series label that opts ordinary float samples into raw-delta math.
|
||||
pub const OTLP_AGGREGATION_TEMPORALITY_LABEL: &str = "otlp_aggregation_temporality";
|
||||
/// Authoritative raw-delta value for [`OTLP_AGGREGATION_TEMPORALITY_LABEL`].
|
||||
pub const GREPTIME_TEMPORALITY_DELTA: &str = "delta";
|
||||
/// Default physical table name
|
||||
pub const GREPTIME_PHYSICAL_TABLE: &str = "greptime_physical_table";
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ use common_catalog::consts::{
|
||||
use common_error::ext::{BoxedError, ErrorExt};
|
||||
use common_error::status_code::StatusCode;
|
||||
use common_query::OutputData;
|
||||
use common_query::prelude::OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
use common_recordbatch::SendableRecordBatchStream;
|
||||
use common_telemetry::{debug, warn};
|
||||
use common_time::timestamp::TimeUnit;
|
||||
@@ -342,6 +343,7 @@ impl EntityGraphProviderImpl {
|
||||
.meta
|
||||
.row_key_column_names()
|
||||
.filter(|c| !implicit.id.contains(c))
|
||||
.filter(|c| c.as_str() != OTLP_AGGREGATION_TEMPORALITY_LABEL)
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
@@ -1249,9 +1251,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn target_info_descriptive_rest_covers_remaining_tags() {
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
let info = prom_table_info(
|
||||
"target_info",
|
||||
&["job", "instance", "k8s_cluster_name", "service_version"],
|
||||
&[
|
||||
"job",
|
||||
"instance",
|
||||
"k8s_cluster_name",
|
||||
"service_version",
|
||||
marker,
|
||||
],
|
||||
PROM_STAMPS,
|
||||
);
|
||||
let declarations = sorted_declarations(&info);
|
||||
|
||||
@@ -130,7 +130,7 @@ impl OpenTelemetryProtocolHandler for Instance {
|
||||
} = otlp::metrics::to_grpc_insert_requests(request, &mut metric_ctx)?;
|
||||
if outcome.rejected_data_points > 0 {
|
||||
warn!(
|
||||
"Rejected {} OTLP exponential histogram data points: {}",
|
||||
"Rejected {} OTLP metrics data points: {}",
|
||||
outcome.rejected_data_points,
|
||||
outcome.error_message.as_deref().unwrap_or_default()
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ use datatypes::arrow::datatypes::{DataType, Field};
|
||||
use futures::StreamExt;
|
||||
use promql::extension_plan::RangeManipulate;
|
||||
use promql::functions::{
|
||||
Changes, Delta, IDelta, Increase, PredictLinear, QuantileOverTime, Rate, Resets,
|
||||
Changes, Delta, IDelta, Increase, PredictLinear, QuantileOverTime, Rate, Resets, SumOverTime,
|
||||
};
|
||||
use promql::range_array::RangeArray;
|
||||
|
||||
@@ -135,6 +135,62 @@ fn make_extrapolated_rate_input(
|
||||
]
|
||||
}
|
||||
|
||||
fn make_delta_rate_comparison_input(
|
||||
series_count: usize,
|
||||
hours: usize,
|
||||
sample_step_seconds: usize,
|
||||
query_step_seconds: usize,
|
||||
window_seconds: usize,
|
||||
) -> (Vec<ColumnarValue>, Vec<ColumnarValue>) {
|
||||
let points_per_series = hours * 60 * 60 / sample_step_seconds;
|
||||
let window_points = window_seconds / sample_step_seconds;
|
||||
let query_stride = query_step_seconds / sample_step_seconds;
|
||||
let mut timestamps = Vec::with_capacity(series_count * points_per_series);
|
||||
let mut deltas = Vec::with_capacity(timestamps.capacity());
|
||||
let mut cumulative = Vec::with_capacity(timestamps.capacity());
|
||||
let mut ranges = Vec::new();
|
||||
let mut eval_timestamps = Vec::new();
|
||||
|
||||
for _ in 0..series_count {
|
||||
let offset = timestamps.len();
|
||||
let mut total = 0.0;
|
||||
for point in 0..points_per_series {
|
||||
let delta = 1.0 + (point % 7) as f64 * 0.25;
|
||||
total += delta;
|
||||
timestamps.push((point as i64 + 1) * sample_step_seconds as i64 * 1_000);
|
||||
deltas.push(delta);
|
||||
cumulative.push(total);
|
||||
}
|
||||
for end in (window_points - 1..points_per_series).step_by(query_stride) {
|
||||
ranges.push((
|
||||
(offset + end + 1 - window_points) as u32,
|
||||
window_points as u32,
|
||||
));
|
||||
eval_timestamps.push(timestamps[offset + end] + 500);
|
||||
}
|
||||
}
|
||||
|
||||
let timestamps = Arc::new(TimestampMillisecondArray::from(timestamps));
|
||||
let delta_timestamp_ranges =
|
||||
RangeArray::from_ranges(timestamps.clone(), ranges.clone()).unwrap();
|
||||
let cumulative_timestamp_ranges = RangeArray::from_ranges(timestamps, ranges.clone()).unwrap();
|
||||
let delta_ranges =
|
||||
RangeArray::from_ranges(Arc::new(Float64Array::from(deltas)), ranges.clone()).unwrap();
|
||||
let cumulative_ranges =
|
||||
RangeArray::from_ranges(Arc::new(Float64Array::from(cumulative)), ranges).unwrap();
|
||||
let delta = vec![
|
||||
ColumnarValue::Array(Arc::new(delta_timestamp_ranges.into_dict())),
|
||||
ColumnarValue::Array(Arc::new(delta_ranges.into_dict())),
|
||||
];
|
||||
let cumulative = vec![
|
||||
ColumnarValue::Array(Arc::new(cumulative_timestamp_ranges.into_dict())),
|
||||
ColumnarValue::Array(Arc::new(cumulative_ranges.into_dict())),
|
||||
ColumnarValue::Array(Arc::new(TimestampMillisecondArray::from(eval_timestamps))),
|
||||
ColumnarValue::Scalar(ScalarValue::Int64(Some(window_seconds as i64 * 1_000))),
|
||||
];
|
||||
(delta, cumulative)
|
||||
}
|
||||
|
||||
fn make_idelta_input(num_points: usize, window_size: u32) -> Vec<ColumnarValue> {
|
||||
let (ts_range, val_range, _) =
|
||||
build_sliding_ranges(num_points, window_size, build_default_values(num_points), 0);
|
||||
@@ -453,6 +509,49 @@ fn bench_range_functions(c: &mut Criterion) {
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_delta_rate_comparison(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("delta_rate_comparison");
|
||||
let series_count = 64;
|
||||
let hours = 4;
|
||||
let sample_step_seconds = 15;
|
||||
let window_seconds = 2 * 60 * 60;
|
||||
let delta_udf = SumOverTime::scalar_udf();
|
||||
let cumulative_udf = Rate::scalar_udf();
|
||||
|
||||
// Release acceptance threshold: on both step sweeps, the sum reducer that
|
||||
// dominates delta-rate cost should stay below 100 ms and within 100x of
|
||||
// cumulative rate on this 64-series, four-hour data set. Optimize the
|
||||
// reducer before release if either bound is exceeded on a typical CI host.
|
||||
for query_step_seconds in [60, 300] {
|
||||
let (delta, cumulative) = make_delta_rate_comparison_input(
|
||||
series_count,
|
||||
hours,
|
||||
sample_step_seconds,
|
||||
query_step_seconds,
|
||||
window_seconds,
|
||||
);
|
||||
let delta = PreparedUdfCall::new(delta);
|
||||
let cumulative = PreparedUdfCall::new(cumulative);
|
||||
let parameters = format!(
|
||||
"series{series_count}_hours{hours}_window{}h_step{}s",
|
||||
window_seconds / 60 / 60,
|
||||
query_step_seconds
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("delta_sum_over_time", ¶meters),
|
||||
&(),
|
||||
|b, _| b.iter(|| invoke_prepared(&delta_udf, &delta)),
|
||||
);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("cumulative_rate", ¶meters),
|
||||
&(),
|
||||
|b, _| b.iter(|| invoke_prepared(&cumulative_udf, &cumulative)),
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_edge_count_functions(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("edge_count_fn");
|
||||
let num_points = 4_096;
|
||||
@@ -781,6 +880,7 @@ fn bench_range_manipulate_wall_time(c: &mut Criterion) {
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_range_functions,
|
||||
bench_delta_rate_comparison,
|
||||
bench_edge_count_functions,
|
||||
bench_range_manipulate_wall_time
|
||||
);
|
||||
|
||||
@@ -1548,6 +1548,9 @@ impl ScalarUDFImpl for MixedRangeUdf {
|
||||
enum MixedRangeFunction {
|
||||
Rate,
|
||||
Increase,
|
||||
// Raw-delta modes sum floats while preserving mixed-range drop/warning semantics.
|
||||
RawDeltaRate,
|
||||
RawDeltaIncrease,
|
||||
Delta,
|
||||
IDelta,
|
||||
IRate,
|
||||
@@ -1587,6 +1590,8 @@ impl MixedRangeFunction {
|
||||
match name {
|
||||
"rate" => Ok(Self::Rate),
|
||||
"increase" => Ok(Self::Increase),
|
||||
"raw_delta_rate" => Ok(Self::RawDeltaRate),
|
||||
"raw_delta_increase" => Ok(Self::RawDeltaIncrease),
|
||||
"delta" => Ok(Self::Delta),
|
||||
"idelta" => Ok(Self::IDelta),
|
||||
"irate" => Ok(Self::IRate),
|
||||
@@ -1617,6 +1622,8 @@ impl MixedRangeFunction {
|
||||
match self {
|
||||
Self::Rate => "rate",
|
||||
Self::Increase => "increase",
|
||||
Self::RawDeltaRate => "rate",
|
||||
Self::RawDeltaIncrease => "increase",
|
||||
Self::Delta => "delta",
|
||||
Self::IDelta => "idelta",
|
||||
Self::IRate => "irate",
|
||||
@@ -1642,9 +1649,13 @@ impl MixedRangeFunction {
|
||||
|
||||
fn policy(self) -> MixedRangePolicy {
|
||||
match self {
|
||||
Self::Rate | Self::Increase | Self::Delta | Self::AvgOverTime | Self::SumOverTime => {
|
||||
MixedRangePolicy::DropMixed
|
||||
}
|
||||
Self::Rate
|
||||
| Self::Increase
|
||||
| Self::RawDeltaRate
|
||||
| Self::RawDeltaIncrease
|
||||
| Self::Delta
|
||||
| Self::AvgOverTime
|
||||
| Self::SumOverTime => MixedRangePolicy::DropMixed,
|
||||
Self::IDelta | Self::IRate => MixedRangePolicy::LastTwo,
|
||||
Self::LastOverTime => MixedRangePolicy::Last,
|
||||
Self::Changes
|
||||
@@ -1668,6 +1679,7 @@ impl MixedRangeFunction {
|
||||
match self {
|
||||
Self::Rate => Some(Rate::scalar_udf()),
|
||||
Self::Increase => Some(Increase::scalar_udf()),
|
||||
Self::RawDeltaRate | Self::RawDeltaIncrease => Some(SumOverTime::scalar_udf()),
|
||||
Self::Delta => Some(crate::functions::Delta::scalar_udf()),
|
||||
Self::IDelta => Some(IDelta::<false>::scalar_udf()),
|
||||
Self::IRate => Some(IDelta::<true>::scalar_udf()),
|
||||
@@ -1708,6 +1720,7 @@ impl MixedRangeFunction {
|
||||
collector,
|
||||
)),
|
||||
Self::LastOverTime => Some(NativeHistogramLastOverTime::scalar_udf()),
|
||||
Self::RawDeltaRate | Self::RawDeltaIncrease => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
+471
-103
@@ -23,7 +23,10 @@ use common_error::ext::ErrorExt;
|
||||
use common_error::status_code::StatusCode;
|
||||
use common_function::function::FunctionContext;
|
||||
use common_query::native_histogram::native_histogram_value_type;
|
||||
use common_query::prelude::{greptime_native_histogram, greptime_value};
|
||||
use common_query::prelude::{
|
||||
GREPTIME_TEMPORALITY_DELTA, OTLP_AGGREGATION_TEMPORALITY_LABEL, greptime_native_histogram,
|
||||
greptime_value,
|
||||
};
|
||||
use common_query::promql_annotations::PromqlAnnotationCollector;
|
||||
use datafusion::common::DFSchemaRef;
|
||||
use datafusion::datasource::DefaultTableSource;
|
||||
@@ -1062,8 +1065,14 @@ impl PromPlanner {
|
||||
first_leaf: &PlannedIslandLeaf,
|
||||
right_leaf: &PlannedIslandLeaf,
|
||||
) -> Result<LogicalPlan> {
|
||||
let only_join_time_index =
|
||||
first_leaf.ctx.tag_columns.is_empty() || right_leaf.ctx.tag_columns.is_empty();
|
||||
let only_join_time_index = (first_leaf.ctx.tag_columns.is_empty()
|
||||
|| right_leaf.ctx.tag_columns.is_empty())
|
||||
&& !first_leaf
|
||||
.ctx
|
||||
.tag_columns
|
||||
.iter()
|
||||
.chain(&right_leaf.ctx.tag_columns)
|
||||
.any(|tag| tag == OTLP_AGGREGATION_TEMPORALITY_LABEL);
|
||||
let (mut left_keys, mut right_keys, force_empty_join) = self.binary_join_key_columns(
|
||||
left.schema(),
|
||||
right_leaf.plan.schema(),
|
||||
@@ -1502,6 +1511,8 @@ impl PromPlanner {
|
||||
.table_ref()
|
||||
.unwrap_or_else(|_| TableReference::bare(""));
|
||||
let right_context = self.ctx.clone();
|
||||
let left_is_empty_metric = Self::is_empty_metric(&left_input);
|
||||
let right_is_empty_metric = Self::is_empty_metric(&right_input);
|
||||
|
||||
// TODO(ruihang): avoid join if left and right are the same table
|
||||
|
||||
@@ -1541,6 +1552,8 @@ impl PromPlanner {
|
||||
} else {
|
||||
self.ctx.table_name = Some("rhs".to_string());
|
||||
}
|
||||
} else if right_is_empty_metric && !left_is_empty_metric {
|
||||
self.ctx = left_context.clone();
|
||||
}
|
||||
// Computed scalars reach this join path instead of the literal projection paths.
|
||||
// Broadcast them for arithmetic in the same way as literal scalars.
|
||||
@@ -1577,6 +1590,8 @@ impl PromPlanner {
|
||||
.map(|(output, _)| output.clone())
|
||||
.collect();
|
||||
let mut field_groups = field_groups.into_iter();
|
||||
// `vector()` uses EmptyMetric and keeps GreptimeDB's timestamp broadcast.
|
||||
let has_empty_metric_operand = left_is_empty_metric || right_is_empty_metric;
|
||||
|
||||
let join_plan = self.join_on_non_field_columns(
|
||||
left_input,
|
||||
@@ -1585,9 +1600,16 @@ impl PromPlanner {
|
||||
right_table_ref.clone(),
|
||||
left_time_index_column,
|
||||
right_time_index_column,
|
||||
// if left plan or right plan tag is empty, means case like `scalar(...) + host` or `host + scalar(...)`
|
||||
// under this case we only join on time index
|
||||
left_context.tag_columns.is_empty() || right_context.tag_columns.is_empty(),
|
||||
lhs.value_type() == ValueType::Scalar
|
||||
|| rhs.value_type() == ValueType::Scalar
|
||||
|| has_empty_metric_operand
|
||||
|| ((left_context.tag_columns.is_empty()
|
||||
|| right_context.tag_columns.is_empty())
|
||||
&& !left_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.chain(&right_context.tag_columns)
|
||||
.any(|tag| tag == OTLP_AGGREGATION_TEMPORALITY_LABEL)),
|
||||
modifier,
|
||||
&left_context,
|
||||
&right_context,
|
||||
@@ -2572,9 +2594,31 @@ impl PromPlanner {
|
||||
continue;
|
||||
}
|
||||
|
||||
let accepts_empty = matcher.is_match("");
|
||||
let column_name = Self::find_case_sensitive_column(table_schema, matcher.name.as_str());
|
||||
let col = if let Some(column_name) = column_name {
|
||||
DfExpr::Column(Column::from_name(column_name))
|
||||
let column = DfExpr::Column(Column::from_name(&column_name));
|
||||
let field = table_schema
|
||||
.index_of_column_by_name(None, &column_name)
|
||||
.map(|index| table_schema.field(index));
|
||||
if accepts_empty
|
||||
&& column_name == OTLP_AGGREGATION_TEMPORALITY_LABEL
|
||||
&& let Some(data_type) = field
|
||||
.filter(|field| {
|
||||
field.is_nullable()
|
||||
&& Self::string_value_data_type(field.data_type()).is_some()
|
||||
})
|
||||
.map(|field| field.data_type())
|
||||
{
|
||||
let empty = Self::string_scalar_value(data_type, Some(String::new()))
|
||||
.expect("nullable label has a string type");
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: coalesce(),
|
||||
args: vec![column, DfExpr::Literal(empty, None)],
|
||||
})
|
||||
} else {
|
||||
column
|
||||
}
|
||||
} else {
|
||||
DfExpr::Literal(ScalarValue::Utf8(Some(String::new())), None)
|
||||
.alias(matcher.name.clone())
|
||||
@@ -3248,6 +3292,7 @@ impl PromPlanner {
|
||||
mut other_input_exprs: VecDeque<DfExpr>,
|
||||
float_field: &str,
|
||||
histogram_field: &str,
|
||||
input_schema: &DFSchemaRef,
|
||||
) -> Result<Option<Vec<DfExpr>>> {
|
||||
let returns_histogram = matches!(
|
||||
func.name,
|
||||
@@ -3289,25 +3334,51 @@ impl PromPlanner {
|
||||
});
|
||||
}
|
||||
|
||||
let mut args = Vec::with_capacity(other_input_exprs.len() + 6);
|
||||
args.push(lit(func.name));
|
||||
args.push(DfExpr::Column(Column::from_name(
|
||||
let timestamp_range = DfExpr::Column(Column::from_name(
|
||||
RangeManipulate::build_timestamp_range_name(
|
||||
self.ctx.time_index_column.as_ref().unwrap(),
|
||||
),
|
||||
)));
|
||||
args.push(DfExpr::Column(Column::from_name(float_field)));
|
||||
args.push(DfExpr::Column(Column::from_name(histogram_field)));
|
||||
));
|
||||
let float_range = DfExpr::Column(Column::from_name(float_field));
|
||||
let histogram_range = DfExpr::Column(Column::from_name(histogram_field));
|
||||
let mut args = Vec::with_capacity(other_input_exprs.len() + 6);
|
||||
args.push(lit(func.name));
|
||||
args.push(timestamp_range.clone());
|
||||
args.push(float_range.clone());
|
||||
args.push(histogram_range.clone());
|
||||
args.extend(other_input_exprs);
|
||||
if matches!(func.name, "rate" | "increase" | "delta") {
|
||||
args.push(self.create_time_index_column_expr()?);
|
||||
args.push(lit(self.ctx.range.context(ExpectRangeSelectorSnafu)?));
|
||||
}
|
||||
|
||||
let float_expr = DfExpr::ScalarFunction(ScalarFunction {
|
||||
let mut float_expr = DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: Arc::new(MixedRange::float_udf(self.promql_annotations.clone())),
|
||||
args: args.clone(),
|
||||
});
|
||||
if matches!(func.name, "rate" | "increase") {
|
||||
let raw_delta_function = if func.name == "rate" {
|
||||
"raw_delta_rate"
|
||||
} else {
|
||||
"raw_delta_increase"
|
||||
};
|
||||
let delta_sum = DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: Arc::new(MixedRange::float_udf(self.promql_annotations.clone())),
|
||||
args: vec![
|
||||
lit(raw_delta_function),
|
||||
timestamp_range,
|
||||
float_range,
|
||||
histogram_range,
|
||||
],
|
||||
});
|
||||
float_expr = self.select_delta_range_math(
|
||||
func.name,
|
||||
input_schema,
|
||||
self.ctx.range.context(ExpectRangeSelectorSnafu)?,
|
||||
delta_sum,
|
||||
float_expr,
|
||||
)?;
|
||||
}
|
||||
let exprs = if returns_histogram {
|
||||
self.ctx.field_columns = vec![float_field.to_string(), histogram_field.to_string()];
|
||||
vec![
|
||||
@@ -3348,6 +3419,7 @@ impl PromPlanner {
|
||||
other_input_exprs.clone(),
|
||||
&float_field,
|
||||
&histogram_field,
|
||||
input_schema,
|
||||
)?
|
||||
{
|
||||
return Ok((exprs, vec![]));
|
||||
@@ -3870,21 +3942,38 @@ impl PromPlanner {
|
||||
let _ = other_input_exprs.remove(field_column_pos + 1);
|
||||
let _ = other_input_exprs.remove(field_column_pos);
|
||||
}
|
||||
ScalarFunc::ExtrapolateUdf(func, range_length) => {
|
||||
ScalarFunc::ExtrapolateUdf(udf, range_length) => {
|
||||
let ts_range_expr = DfExpr::Column(Column::from_name(
|
||||
RangeManipulate::build_timestamp_range_name(
|
||||
self.ctx.time_index_column.as_ref().unwrap(),
|
||||
),
|
||||
));
|
||||
other_input_exprs.insert(field_column_pos, ts_range_expr);
|
||||
other_input_exprs.insert(field_column_pos + 1, col_expr);
|
||||
other_input_exprs.insert(field_column_pos, ts_range_expr.clone());
|
||||
other_input_exprs.insert(field_column_pos + 1, col_expr.clone());
|
||||
other_input_exprs
|
||||
.insert(field_column_pos + 2, self.create_time_index_column_expr()?);
|
||||
other_input_exprs.push_back(lit(range_length));
|
||||
let fn_expr = DfExpr::ScalarFunction(ScalarFunction {
|
||||
func,
|
||||
func: udf,
|
||||
args: other_input_exprs.clone().into(),
|
||||
});
|
||||
let fn_expr = if matches!(func.name, "rate" | "increase")
|
||||
&& !all_field_columns_are_native_histogram_ranges
|
||||
{
|
||||
let delta_sum = DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: Arc::new(SumOverTime::scalar_udf()),
|
||||
args: vec![ts_range_expr, col_expr],
|
||||
});
|
||||
self.select_delta_range_math(
|
||||
func.name,
|
||||
input_schema,
|
||||
range_length,
|
||||
delta_sum,
|
||||
fn_expr,
|
||||
)?
|
||||
} else {
|
||||
fn_expr
|
||||
};
|
||||
exprs.push(fn_expr);
|
||||
let _ = other_input_exprs.pop_back();
|
||||
let _ = other_input_exprs.remove(field_column_pos + 2);
|
||||
@@ -3917,6 +4006,49 @@ impl PromPlanner {
|
||||
Ok((exprs, new_tags))
|
||||
}
|
||||
|
||||
fn select_delta_range_math(
|
||||
&self,
|
||||
function: &str,
|
||||
input_schema: &DFSchemaRef,
|
||||
range_length: Millisecond,
|
||||
delta_sum: DfExpr,
|
||||
cumulative: DfExpr,
|
||||
) -> Result<DfExpr> {
|
||||
let marker_is_delta = if self
|
||||
.ctx
|
||||
.tag_columns
|
||||
.iter()
|
||||
.any(|tag| tag == OTLP_AGGREGATION_TEMPORALITY_LABEL)
|
||||
{
|
||||
Self::field_column_type(input_schema, OTLP_AGGREGATION_TEMPORALITY_LABEL)
|
||||
.filter(|data_type| Self::string_value_data_type(data_type).is_some())
|
||||
.map(|_| {
|
||||
DfExpr::Column(Column::from_name(OTLP_AGGREGATION_TEMPORALITY_LABEL))
|
||||
.eq(lit(GREPTIME_TEMPORALITY_DELTA))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(marker_is_delta) = marker_is_delta else {
|
||||
return Ok(cumulative);
|
||||
};
|
||||
|
||||
let delta = if function == "rate" {
|
||||
DfExpr::BinaryExpr(BinaryExpr {
|
||||
left: Box::new(delta_sum),
|
||||
op: Operator::Divide,
|
||||
right: Box::new(lit(range_length as f64 / 1000.0)),
|
||||
})
|
||||
} else {
|
||||
delta_sum
|
||||
};
|
||||
let display_name = cumulative.schema_name().to_string();
|
||||
when(marker_is_delta, delta)
|
||||
.otherwise(cumulative)
|
||||
.context(DataFusionPlanningSnafu)
|
||||
.map(|expr| expr.alias(display_name))
|
||||
}
|
||||
|
||||
/// Validate label name according to Prometheus specification.
|
||||
/// Label names must match the regex: [a-zA-Z_][a-zA-Z0-9_]*
|
||||
/// Additionally, label names starting with double underscores are reserved for internal use.
|
||||
@@ -5441,6 +5573,10 @@ impl PromPlanner {
|
||||
.any(|field| field.name() == DATA_SCHEMA_TSID_COLUMN_NAME)
|
||||
}
|
||||
|
||||
fn is_empty_metric(plan: &LogicalPlan) -> bool {
|
||||
matches!(plan, LogicalPlan::Extension(Extension { node }) if node.as_any().is::<EmptyMetric>())
|
||||
}
|
||||
|
||||
fn native_histogram_arrow_type() -> ArrowDataType {
|
||||
native_histogram_value_type().as_arrow_type()
|
||||
}
|
||||
@@ -5706,7 +5842,7 @@ impl PromPlanner {
|
||||
left_context: &PromPlannerContext,
|
||||
right_context: &PromPlannerContext,
|
||||
) -> Result<LogicalPlan> {
|
||||
let (mut left_tag_columns, mut right_tag_columns, force_empty_join) = self
|
||||
let (mut left_tag_columns, mut right_tag_columns, mut force_empty_join) = self
|
||||
.binary_join_key_columns(
|
||||
left.schema(),
|
||||
right.schema(),
|
||||
@@ -5715,6 +5851,35 @@ impl PromPlanner {
|
||||
only_join_time_index,
|
||||
modifier,
|
||||
)?;
|
||||
let use_tsid_join = !only_join_time_index
|
||||
&& !force_empty_join
|
||||
&& left_tag_columns == BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()])
|
||||
&& right_tag_columns == BTreeSet::from([DATA_SCHEMA_TSID_COLUMN_NAME.to_string()]);
|
||||
let (left, right) = if !only_join_time_index
|
||||
&& !use_tsid_join
|
||||
&& Self::only_temporality_match_label_mismatches(left_context, right_context, modifier)
|
||||
{
|
||||
let mut aligned_left_context = left_context.clone();
|
||||
let mut aligned_right_context = right_context.clone();
|
||||
let (left, right, _) = Self::align_temporality_match_column(
|
||||
left,
|
||||
right,
|
||||
&mut aligned_left_context,
|
||||
&mut aligned_right_context,
|
||||
)?;
|
||||
(left_tag_columns, right_tag_columns, force_empty_join) = self
|
||||
.binary_join_key_columns(
|
||||
left.schema(),
|
||||
right.schema(),
|
||||
&aligned_left_context,
|
||||
&aligned_right_context,
|
||||
false,
|
||||
modifier,
|
||||
)?;
|
||||
(left, right)
|
||||
} else {
|
||||
(left, right)
|
||||
};
|
||||
|
||||
// push time index column if it exists
|
||||
if let (Some(left_time_index_column), Some(right_time_index_column)) =
|
||||
@@ -5755,6 +5920,145 @@ impl PromPlanner {
|
||||
.context(DataFusionPlanningSnafu)
|
||||
}
|
||||
|
||||
fn selected_binary_match_labels(
|
||||
left_context: &PromPlannerContext,
|
||||
right_context: &PromPlannerContext,
|
||||
modifier: &Option<BinModifier>,
|
||||
) -> BTreeSet<String> {
|
||||
let mut labels = left_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.chain(&right_context.tag_columns)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if let Some(matching) = modifier
|
||||
.as_ref()
|
||||
.and_then(|modifier| modifier.matching.as_ref())
|
||||
{
|
||||
match matching {
|
||||
LabelModifier::Include(on) => {
|
||||
labels = on
|
||||
.labels
|
||||
.iter()
|
||||
.filter(|label| {
|
||||
left_context.tag_columns.contains(label)
|
||||
|| right_context.tag_columns.contains(label)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
}
|
||||
LabelModifier::Exclude(ignoring) => {
|
||||
for label in &ignoring.labels {
|
||||
labels.remove(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
labels
|
||||
}
|
||||
|
||||
fn only_temporality_match_label_mismatches(
|
||||
left_context: &PromPlannerContext,
|
||||
right_context: &PromPlannerContext,
|
||||
modifier: &Option<BinModifier>,
|
||||
) -> bool {
|
||||
let mut mismatches =
|
||||
Self::selected_binary_match_labels(left_context, right_context, modifier)
|
||||
.into_iter()
|
||||
.filter(|label| {
|
||||
left_context.tag_columns.contains(label)
|
||||
!= right_context.tag_columns.contains(label)
|
||||
});
|
||||
matches!(
|
||||
(mismatches.next(), mismatches.next()),
|
||||
(Some(label), None) if label == OTLP_AGGREGATION_TEMPORALITY_LABEL
|
||||
)
|
||||
}
|
||||
|
||||
fn align_temporality_match_column(
|
||||
mut left: LogicalPlan,
|
||||
mut right: LogicalPlan,
|
||||
left_context: &mut PromPlannerContext,
|
||||
right_context: &mut PromPlannerContext,
|
||||
) -> Result<(LogicalPlan, LogicalPlan, bool)> {
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
let left_has_marker = left_context.tag_columns.iter().any(|tag| tag == marker);
|
||||
let (present, add_to_left) = if left_has_marker {
|
||||
(&left, false)
|
||||
} else {
|
||||
(&right, true)
|
||||
};
|
||||
let data_type = present
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.find(|field| field.name() == marker)
|
||||
.map(|field| field.data_type().clone())
|
||||
.with_context(|| ColumnNotFoundSnafu {
|
||||
col: marker.to_string(),
|
||||
})?;
|
||||
let null = Self::string_scalar_value(&data_type, None).with_context(|| {
|
||||
UnexpectedPlanExprSnafu {
|
||||
desc: format!("temporality match label {marker} must be a string"),
|
||||
}
|
||||
})?;
|
||||
let add_marker = |plan: LogicalPlan| {
|
||||
let visible = plan
|
||||
.schema()
|
||||
.iter()
|
||||
.map(|(qualifier, field)| {
|
||||
DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
LogicalPlanBuilder::from(plan)
|
||||
.project(
|
||||
visible
|
||||
.into_iter()
|
||||
.chain([DfExpr::Literal(null, None).alias(marker)]),
|
||||
)
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.build()
|
||||
.context(DataFusionPlanningSnafu)
|
||||
};
|
||||
|
||||
if add_to_left {
|
||||
left = add_marker(left)?;
|
||||
left_context.tag_columns.push(marker.to_string());
|
||||
} else {
|
||||
right = add_marker(right)?;
|
||||
right_context.tag_columns.push(marker.to_string());
|
||||
}
|
||||
Ok((left, right, add_to_left))
|
||||
}
|
||||
|
||||
fn normalized_match_key_expr(
|
||||
label: &str,
|
||||
field: Option<(Option<TableReference>, ArrowDataType)>,
|
||||
value_type: &ArrowDataType,
|
||||
internal_name: &str,
|
||||
) -> DfExpr {
|
||||
let empty = Self::string_scalar_value(value_type, Some(String::new()))
|
||||
.expect("match label value type is a string");
|
||||
let expr = if let Some((qualifier, data_type)) = field {
|
||||
let column = DfExpr::Column(Column::new(qualifier, label));
|
||||
let column = if &data_type == value_type {
|
||||
column
|
||||
} else {
|
||||
DfExpr::Cast(Cast {
|
||||
expr: Box::new(column),
|
||||
data_type: value_type.clone(),
|
||||
})
|
||||
};
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: coalesce(),
|
||||
args: vec![column, DfExpr::Literal(empty, None)],
|
||||
})
|
||||
} else {
|
||||
DfExpr::Literal(empty, None)
|
||||
};
|
||||
expr.alias(internal_name)
|
||||
}
|
||||
|
||||
fn is_zero_row_empty_relation(plan: &LogicalPlan) -> bool {
|
||||
// `produce_one_row` is used for input-free plans that still emit one row;
|
||||
// only the false case is a statically proven empty vector.
|
||||
@@ -5764,19 +6068,19 @@ impl PromPlanner {
|
||||
/// Build a set operator (AND/OR/UNLESS)
|
||||
fn set_op_on_non_field_columns(
|
||||
&mut self,
|
||||
left: LogicalPlan,
|
||||
mut left: LogicalPlan,
|
||||
mut right: LogicalPlan,
|
||||
left_context: PromPlannerContext,
|
||||
right_context: PromPlannerContext,
|
||||
op: TokenType,
|
||||
modifier: &Option<BinModifier>,
|
||||
) -> Result<LogicalPlan> {
|
||||
let mut left_tag_col_set = left_context
|
||||
let left_tag_col_set = left_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
let mut right_tag_col_set = right_context
|
||||
let right_tag_col_set = right_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -5794,9 +6098,7 @@ impl PromPlanner {
|
||||
);
|
||||
}
|
||||
|
||||
// apply modifier
|
||||
if let Some(modifier) = modifier {
|
||||
// one-to-many and many-to-one are not supported
|
||||
ensure!(
|
||||
matches!(
|
||||
modifier.card,
|
||||
@@ -5806,44 +6108,68 @@ impl PromPlanner {
|
||||
name: modifier.card.clone(),
|
||||
},
|
||||
);
|
||||
// apply label modifier
|
||||
if let Some(matching) = &modifier.matching {
|
||||
match matching {
|
||||
// keeps columns mentioned in `on`
|
||||
LabelModifier::Include(on) => {
|
||||
let mask = on.labels.iter().cloned().collect::<HashSet<_>>();
|
||||
left_tag_col_set = left_tag_col_set.intersection(&mask).cloned().collect();
|
||||
right_tag_col_set =
|
||||
right_tag_col_set.intersection(&mask).cloned().collect();
|
||||
}
|
||||
// removes columns memtioned in `ignoring`
|
||||
LabelModifier::Exclude(ignoring) => {
|
||||
// doesn't check existence of label
|
||||
for label in &ignoring.labels {
|
||||
let _ = left_tag_col_set.remove(label);
|
||||
let _ = right_tag_col_set.remove(label);
|
||||
}
|
||||
}
|
||||
|
||||
let output_context = left_context.clone();
|
||||
let visible_left_schema = left.schema().clone();
|
||||
let mut left_context = left_context;
|
||||
let mut right_context = right_context;
|
||||
let added_marker_to_left = if Self::only_temporality_match_label_mismatches(
|
||||
&left_context,
|
||||
&right_context,
|
||||
modifier,
|
||||
) {
|
||||
let aligned = Self::align_temporality_match_column(
|
||||
left,
|
||||
right,
|
||||
&mut left_context,
|
||||
&mut right_context,
|
||||
)?;
|
||||
left = aligned.0;
|
||||
right = aligned.1;
|
||||
aligned.2
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut left_tag_col_set = left_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut right_tag_col_set = right_context
|
||||
.tag_columns
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if let Some(matching) = modifier
|
||||
.as_ref()
|
||||
.and_then(|modifier| modifier.matching.as_ref())
|
||||
{
|
||||
match matching {
|
||||
LabelModifier::Include(on) => {
|
||||
let mask = on.labels.iter().cloned().collect::<BTreeSet<_>>();
|
||||
left_tag_col_set = left_tag_col_set.intersection(&mask).cloned().collect();
|
||||
right_tag_col_set = right_tag_col_set.intersection(&mask).cloned().collect();
|
||||
}
|
||||
LabelModifier::Exclude(ignoring) => {
|
||||
for label in &ignoring.labels {
|
||||
let _ = left_tag_col_set.remove(label);
|
||||
let _ = right_tag_col_set.remove(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure two sides have the same tag columns
|
||||
if !matches!(op.id(), token::T_LOR) {
|
||||
ensure!(
|
||||
left_tag_col_set == right_tag_col_set,
|
||||
CombineTableColumnMismatchSnafu {
|
||||
left: left_tag_col_set.into_iter().collect::<Vec<_>>(),
|
||||
right: right_tag_col_set.into_iter().collect::<Vec<_>>(),
|
||||
}
|
||||
)
|
||||
};
|
||||
ensure!(
|
||||
left_tag_col_set == right_tag_col_set,
|
||||
CombineTableColumnMismatchSnafu {
|
||||
left: left_tag_col_set.iter().cloned().collect::<Vec<_>>(),
|
||||
right: right_tag_col_set.iter().cloned().collect::<Vec<_>>(),
|
||||
}
|
||||
);
|
||||
|
||||
let left_time_index = left_context.time_index_column.clone().unwrap();
|
||||
let right_time_index = right_context.time_index_column.clone().unwrap();
|
||||
let join_keys = left_tag_col_set
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain([left_time_index.clone()])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// alias right time index column if necessary
|
||||
if left_context.time_index_column != right_context.time_index_column {
|
||||
@@ -5867,6 +6193,11 @@ impl PromPlanner {
|
||||
.context(DataFusionPlanningSnafu)?;
|
||||
}
|
||||
|
||||
let join_keys = left_tag_col_set
|
||||
.into_iter()
|
||||
.chain([left_time_index])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ensure!(
|
||||
left_context.field_columns.len() == 1
|
||||
|| Self::field_columns_are_alternative_samples(
|
||||
@@ -5913,9 +6244,20 @@ impl PromPlanner {
|
||||
}
|
||||
_ => UnexpectedTokenSnafu { token: op }.fail(),
|
||||
}?;
|
||||
let result = if added_marker_to_left {
|
||||
LogicalPlanBuilder::from(result)
|
||||
.project(visible_left_schema.iter().map(|(qualifier, field)| {
|
||||
DfExpr::Column(Column::new(qualifier.cloned(), field.name().clone()))
|
||||
}))
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
.build()
|
||||
.context(DataFusionPlanningSnafu)?
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
// AND/UNLESS preserve the complete left operand schema and metadata.
|
||||
self.ctx = left_context;
|
||||
self.ctx = output_context;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -6492,8 +6834,6 @@ impl PromPlanner {
|
||||
}
|
||||
.fail();
|
||||
};
|
||||
let empty = Self::string_scalar_value(&value_type, Some(String::new()))
|
||||
.expect("match label value type is a string");
|
||||
let internal_name = loop {
|
||||
let name = format!("__promql_or_match_{next_internal_column}");
|
||||
next_internal_column += 1;
|
||||
@@ -6501,28 +6841,18 @@ impl PromPlanner {
|
||||
break name;
|
||||
}
|
||||
};
|
||||
let normalize = |field: Option<(Option<TableReference>, ArrowDataType)>| {
|
||||
let expr = if let Some((qualifier, data_type)) = field {
|
||||
let column = DfExpr::Column(Column::new(qualifier, label.clone()));
|
||||
let column = if data_type == value_type {
|
||||
column
|
||||
} else {
|
||||
DfExpr::Cast(Cast {
|
||||
expr: Box::new(column),
|
||||
data_type: value_type.clone(),
|
||||
})
|
||||
};
|
||||
DfExpr::ScalarFunction(ScalarFunction {
|
||||
func: coalesce(),
|
||||
args: vec![column, DfExpr::Literal(empty.clone(), None)],
|
||||
})
|
||||
} else {
|
||||
DfExpr::Literal(empty.clone(), None)
|
||||
};
|
||||
expr.alias(internal_name.clone())
|
||||
};
|
||||
left_match_exprs.push(normalize(left_field));
|
||||
right_match_exprs.push(normalize(right_field));
|
||||
left_match_exprs.push(Self::normalized_match_key_expr(
|
||||
label,
|
||||
left_field,
|
||||
&value_type,
|
||||
&internal_name,
|
||||
));
|
||||
right_match_exprs.push(Self::normalized_match_key_expr(
|
||||
label,
|
||||
right_field,
|
||||
&value_type,
|
||||
&internal_name,
|
||||
));
|
||||
}
|
||||
|
||||
let left_augmented = LogicalPlanBuilder::from(left_projected)
|
||||
@@ -6850,6 +7180,8 @@ mod test {
|
||||
use crate::parser::QueryLanguageParser;
|
||||
use crate::query_engine::DefaultSerializer;
|
||||
|
||||
mod delta;
|
||||
|
||||
fn find_instant_manipulate(plan: &LogicalPlan) -> Option<&InstantManipulate> {
|
||||
if let LogicalPlan::Extension(Extension { node }) = plan
|
||||
&& let Some(instant_manipulate) = node.as_any().downcast_ref::<InstantManipulate>()
|
||||
@@ -7757,8 +8089,15 @@ mod test {
|
||||
}
|
||||
|
||||
async fn build_test_native_histogram_table_provider(table_name: &str) -> DfTableSourceProvider {
|
||||
build_test_native_histogram_table_provider_with_marker(table_name, false).await
|
||||
}
|
||||
|
||||
async fn build_test_native_histogram_table_provider_with_marker(
|
||||
table_name: &str,
|
||||
temporality_marker: bool,
|
||||
) -> DfTableSourceProvider {
|
||||
let catalog_list = MemoryCatalogManager::with_default_setup();
|
||||
let columns = vec![
|
||||
let mut columns = vec![
|
||||
ColumnSchema::new(
|
||||
"tag_0".to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
@@ -7769,6 +8108,16 @@ mod test {
|
||||
ConcreteDataType::string_datatype(),
|
||||
true,
|
||||
),
|
||||
];
|
||||
if temporality_marker {
|
||||
columns.push(ColumnSchema::new(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL.to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
true,
|
||||
));
|
||||
}
|
||||
let tag_count = columns.len();
|
||||
columns.extend([
|
||||
ColumnSchema::new(
|
||||
"timestamp".to_string(),
|
||||
ConcreteDataType::timestamp_millisecond_datatype(),
|
||||
@@ -7780,12 +8129,12 @@ mod test {
|
||||
native_histogram_value_type().clone(),
|
||||
true,
|
||||
),
|
||||
];
|
||||
]);
|
||||
let schema = Arc::new(Schema::new(columns));
|
||||
let table_meta = TableMetaBuilder::empty()
|
||||
.schema(schema)
|
||||
.primary_key_indices(vec![0, 1])
|
||||
.value_indices(vec![3])
|
||||
.primary_key_indices((0..tag_count).collect())
|
||||
.value_indices(vec![tag_count + 1])
|
||||
.next_column_id(1024)
|
||||
.build()
|
||||
.unwrap();
|
||||
@@ -7880,14 +8229,29 @@ mod test {
|
||||
|
||||
async fn build_test_mixed_native_histogram_table_provider(
|
||||
table_name: &str,
|
||||
) -> DfTableSourceProvider {
|
||||
build_test_mixed_native_histogram_table_provider_with_marker(table_name, false).await
|
||||
}
|
||||
|
||||
async fn build_test_mixed_native_histogram_table_provider_with_marker(
|
||||
table_name: &str,
|
||||
temporality_marker: bool,
|
||||
) -> DfTableSourceProvider {
|
||||
let catalog_list = MemoryCatalogManager::with_default_setup();
|
||||
let columns = vec![
|
||||
ColumnSchema::new(
|
||||
"tag_0".to_string(),
|
||||
let mut columns = vec![ColumnSchema::new(
|
||||
"tag_0".to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
false,
|
||||
)];
|
||||
if temporality_marker {
|
||||
columns.push(ColumnSchema::new(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL.to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
false,
|
||||
),
|
||||
true,
|
||||
));
|
||||
}
|
||||
let tag_count = columns.len();
|
||||
columns.extend([
|
||||
ColumnSchema::new(
|
||||
"timestamp".to_string(),
|
||||
ConcreteDataType::timestamp_millisecond_datatype(),
|
||||
@@ -7904,12 +8268,12 @@ mod test {
|
||||
ConcreteDataType::float64_datatype(),
|
||||
true,
|
||||
),
|
||||
];
|
||||
]);
|
||||
let schema = Arc::new(Schema::new(columns));
|
||||
let table_meta = TableMetaBuilder::empty()
|
||||
.schema(schema.clone())
|
||||
.primary_key_indices(vec![0])
|
||||
.value_indices(vec![2, 3])
|
||||
.primary_key_indices((0..tag_count).collect())
|
||||
.value_indices(vec![tag_count + 1, tag_count + 2])
|
||||
.next_column_id(1024)
|
||||
.build()
|
||||
.unwrap();
|
||||
@@ -7920,16 +8284,20 @@ mod test {
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
let batch = RecordBatch::try_new(
|
||||
schema.arrow_schema().clone(),
|
||||
vec![
|
||||
Arc::new(StringArray::from(vec!["float", "histogram"])),
|
||||
Arc::new(TimestampMillisecondArray::from(vec![1_000, 1_000])),
|
||||
build_histogram_array(&[None, Some(direct_or_histogram())]),
|
||||
Arc::new(Float64Array::from(vec![Some(2.0), None])),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let mut arrays: Vec<Arc<dyn Array>> =
|
||||
vec![Arc::new(StringArray::from(vec!["float", "histogram"]))];
|
||||
if temporality_marker {
|
||||
arrays.push(Arc::new(StringArray::from(vec![
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
])));
|
||||
}
|
||||
arrays.extend([
|
||||
Arc::new(TimestampMillisecondArray::from(vec![1_000, 1_000])) as Arc<dyn Array>,
|
||||
build_histogram_array(&[None, Some(direct_or_histogram())]),
|
||||
Arc::new(Float64Array::from(vec![Some(2.0), None])),
|
||||
]);
|
||||
let batch = RecordBatch::try_new(schema.arrow_schema().clone(), arrays).unwrap();
|
||||
let backing = GreptimeMemTable::new_with_catalog(
|
||||
table_name,
|
||||
GreptimeRecordBatch::from_df_record_batch(schema, batch),
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use common_query::logical_plan::SubstraitPlanDecoder;
|
||||
use common_query::prelude::set_default_prefix;
|
||||
use datafusion::catalog::SchemaProvider;
|
||||
|
||||
use super::*;
|
||||
use crate::query_engine::DefaultPlanDecoder;
|
||||
|
||||
fn delta_temporality_table_provider() -> (DfTableSourceProvider, QueryEngineState, Arc<MemTable>) {
|
||||
let catalog = MemoryCatalogManager::with_default_setup();
|
||||
let schema = Arc::new(Schema::new(vec![
|
||||
ColumnSchema::new(
|
||||
"series".to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
false,
|
||||
),
|
||||
ColumnSchema::new(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL.to_string(),
|
||||
ConcreteDataType::string_datatype(),
|
||||
true,
|
||||
),
|
||||
ColumnSchema::new(
|
||||
greptime_timestamp().to_string(),
|
||||
ConcreteDataType::timestamp_millisecond_datatype(),
|
||||
false,
|
||||
)
|
||||
.with_time_index(true),
|
||||
ColumnSchema::new(
|
||||
greptime_value().to_string(),
|
||||
ConcreteDataType::float64_datatype(),
|
||||
true,
|
||||
),
|
||||
]));
|
||||
let batch = RecordBatch::try_new(
|
||||
schema.arrow_schema().clone(),
|
||||
vec![
|
||||
Arc::new(StringArray::from(vec![
|
||||
"delta",
|
||||
"delta",
|
||||
"delta",
|
||||
"single",
|
||||
"stale",
|
||||
"stale",
|
||||
"cumulative",
|
||||
"cumulative",
|
||||
"cumulative",
|
||||
])),
|
||||
Arc::new(StringArray::from(vec![
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
])),
|
||||
Arc::new(TimestampMillisecondArray::from(vec![
|
||||
60_000, 120_000, 180_000, 180_000, 120_000, 180_000, 60_000, 120_000, 180_000,
|
||||
])),
|
||||
Arc::new(Float64Array::from(vec![
|
||||
10.0,
|
||||
20.0,
|
||||
15.0,
|
||||
7.0,
|
||||
7.0,
|
||||
f64::from_bits(PROMETHEUS_STALE_NAN_BITS),
|
||||
10.0,
|
||||
20.0,
|
||||
30.0,
|
||||
])),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let datafusion_table =
|
||||
Arc::new(MemTable::try_new(batch.schema(), vec![vec![batch.clone()]]).unwrap());
|
||||
let table_meta = TableMetaBuilder::empty()
|
||||
.schema(schema.clone())
|
||||
.primary_key_indices(vec![0, 1])
|
||||
.value_indices(vec![3])
|
||||
.next_column_id(4)
|
||||
.build()
|
||||
.unwrap();
|
||||
let table_info = Arc::new(
|
||||
TableInfoBuilder::default()
|
||||
.name("delta_metric")
|
||||
.meta(table_meta)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
let backing = GreptimeMemTable::new_with_catalog(
|
||||
"delta_metric",
|
||||
GreptimeRecordBatch::from_df_record_batch(schema, batch),
|
||||
4_001,
|
||||
DEFAULT_CATALOG_NAME.to_string(),
|
||||
DEFAULT_SCHEMA_NAME.to_string(),
|
||||
);
|
||||
let table = Arc::new(Table::new(
|
||||
table_info,
|
||||
FilterPushDownType::Unsupported,
|
||||
backing.data_source(),
|
||||
));
|
||||
catalog
|
||||
.register_table_sync(RegisterTableRequest {
|
||||
catalog: DEFAULT_CATALOG_NAME.to_string(),
|
||||
schema: DEFAULT_SCHEMA_NAME.to_string(),
|
||||
table_name: "delta_metric".to_string(),
|
||||
table_id: 4_001,
|
||||
table,
|
||||
})
|
||||
.unwrap();
|
||||
let state = QueryEngineState::new(
|
||||
catalog.clone(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
Plugins::default(),
|
||||
QueryOptions::default(),
|
||||
);
|
||||
let provider = DfTableSourceProvider::new(
|
||||
catalog,
|
||||
false,
|
||||
QueryContext::arc(),
|
||||
DummyDecoder::arc(),
|
||||
false,
|
||||
);
|
||||
(provider, state, datafusion_table)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rate_and_increase_select_raw_delta_math_per_series() {
|
||||
set_default_prefix(Some("custom")).unwrap();
|
||||
assert_eq!(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
"otlp_aggregation_temporality"
|
||||
);
|
||||
|
||||
let eval_time = UNIX_EPOCH.checked_add(Duration::from_secs(180)).unwrap();
|
||||
for (function, expected_delta, expected_single) in
|
||||
[("increase", 45.0, 7.0), ("rate", 0.25, 7.0 / 180.0)]
|
||||
{
|
||||
let eval_stmt = EvalStmt {
|
||||
expr: parser::parse(&format!("{function}(delta_metric[3m])")).unwrap(),
|
||||
start: eval_time,
|
||||
end: eval_time,
|
||||
interval: Duration::from_secs(60),
|
||||
lookback_delta: Duration::from_secs(300),
|
||||
};
|
||||
let (provider, state, datafusion_table) = delta_temporality_table_provider();
|
||||
let raw = PromPlanner::stmt_to_plan(provider, &eval_stmt, &state)
|
||||
.await
|
||||
.unwrap();
|
||||
let plan = raw.display_indent_schema().to_string();
|
||||
assert!(plan.contains("prom_sum_over_time"), "{plan}");
|
||||
assert!(plan.contains(OTLP_AGGREGATION_TEMPORALITY_LABEL), "{plan}");
|
||||
let value_field = raw
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.find(|field| field.data_type() == &ArrowDataType::Float64)
|
||||
.unwrap()
|
||||
.name()
|
||||
.clone();
|
||||
assert!(value_field.starts_with(&format!("prom_{function}")));
|
||||
|
||||
let executable = if function == "increase" {
|
||||
let context = SessionContext::new_with_state(state.session_state());
|
||||
let catalog = Arc::new(MemoryCatalogProvider::new());
|
||||
let schema = Arc::new(MemorySchemaProvider::new());
|
||||
schema
|
||||
.register_table("delta_metric".to_string(), datafusion_table)
|
||||
.unwrap();
|
||||
catalog
|
||||
.register_schema(DEFAULT_SCHEMA_NAME, schema)
|
||||
.unwrap();
|
||||
context.register_catalog("datafusion", catalog);
|
||||
let decoder = DefaultPlanDecoder::new(context.state(), &QueryContext::arc()).unwrap();
|
||||
decoder
|
||||
.decode(
|
||||
DFLogicalSubstraitConvertor
|
||||
.encode(&raw, DefaultSerializer)
|
||||
.unwrap(),
|
||||
context.state().catalog_list().clone(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let (_, batches) = execute(executable, &state).await;
|
||||
let mut results = HashMap::new();
|
||||
for batch in batches {
|
||||
let series = batch
|
||||
.column_by_name("series")
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
let temporality = batch
|
||||
.column_by_name(OTLP_AGGREGATION_TEMPORALITY_LABEL)
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
let values = batch
|
||||
.column_by_name(&value_field)
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<Float64Array>()
|
||||
.unwrap();
|
||||
for row in 0..batch.num_rows() {
|
||||
results.insert(
|
||||
series.value(row).to_string(),
|
||||
(
|
||||
(!temporality.is_null(row)).then(|| temporality.value(row).to_string()),
|
||||
values.value(row),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
Some(&(Some(GREPTIME_TEMPORALITY_DELTA.to_string()), expected_delta)),
|
||||
results.get("delta")
|
||||
);
|
||||
assert_eq!(
|
||||
Some(&(
|
||||
Some(GREPTIME_TEMPORALITY_DELTA.to_string()),
|
||||
expected_single
|
||||
)),
|
||||
results.get("single")
|
||||
);
|
||||
assert_eq!(
|
||||
Some(&(
|
||||
Some(GREPTIME_TEMPORALITY_DELTA.to_string()),
|
||||
expected_single
|
||||
)),
|
||||
results.get("stale")
|
||||
);
|
||||
assert!(results.contains_key("cumulative"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_metric_broadcasts_over_temporality_marker() {
|
||||
let eval_time = UNIX_EPOCH.checked_add(Duration::from_secs(180)).unwrap();
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
for query in [
|
||||
"vector(2) * delta_metric".to_string(),
|
||||
format!("vector(2) * ignoring({marker}) delta_metric"),
|
||||
"delta_metric * vector(2)".to_string(),
|
||||
format!("delta_metric * ignoring({marker}) vector(2)"),
|
||||
] {
|
||||
let eval_stmt = EvalStmt {
|
||||
expr: parser::parse(&query).unwrap(),
|
||||
start: eval_time,
|
||||
end: eval_time,
|
||||
interval: Duration::from_secs(60),
|
||||
lookback_delta: Duration::from_secs(300),
|
||||
};
|
||||
let (provider, state, _) = delta_temporality_table_provider();
|
||||
let plan = PromPlanner::stmt_to_plan(provider, &eval_stmt, &state)
|
||||
.await
|
||||
.unwrap();
|
||||
let plan_text = plan.display_indent_schema().to_string();
|
||||
assert!(
|
||||
!plan_text.contains("Filter: Boolean(false)"),
|
||||
"{query}: {plan_text}"
|
||||
);
|
||||
let value_field = plan
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.find(|field| field.data_type() == &ArrowDataType::Float64)
|
||||
.unwrap()
|
||||
.name()
|
||||
.clone();
|
||||
let (_, batches) = execute(plan, &state).await;
|
||||
let mut results = HashMap::new();
|
||||
for batch in batches {
|
||||
let series = batch
|
||||
.column_by_name("series")
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
let values = batch
|
||||
.column_by_name(&value_field)
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<Float64Array>()
|
||||
.unwrap();
|
||||
let temporality = batch
|
||||
.column_by_name(marker)
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
for row in 0..batch.num_rows() {
|
||||
results.insert(
|
||||
series.value(row).to_string(),
|
||||
(
|
||||
(!temporality.is_null(row)).then(|| temporality.value(row).to_string()),
|
||||
values.value(row),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(3, results.len(), "{query}");
|
||||
assert_eq!(
|
||||
Some(&(Some(GREPTIME_TEMPORALITY_DELTA.to_string()), 30.0)),
|
||||
results.get("delta"),
|
||||
"{query}"
|
||||
);
|
||||
assert_eq!(
|
||||
Some(&(Some(GREPTIME_TEMPORALITY_DELTA.to_string()), 14.0)),
|
||||
results.get("single"),
|
||||
"{query}"
|
||||
);
|
||||
assert_eq!(Some(&(None, 60.0)), results.get("cumulative"), "{query}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_histogram_rate_ignores_delta_marker() {
|
||||
let provider =
|
||||
build_test_native_histogram_table_provider_with_marker("native_delta_metric", true).await;
|
||||
let plan = PromPlanner::stmt_to_plan(
|
||||
provider,
|
||||
&build_eval_stmt("rate(native_delta_metric[5m])"),
|
||||
&build_query_engine_state(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.display_indent_schema()
|
||||
.to_string();
|
||||
|
||||
assert!(plan.contains("prom_native_histogram_rate"), "{plan}");
|
||||
assert!(!plan.contains("prom_sum_over_time"), "{plan}");
|
||||
assert!(!plan.contains("CASE WHEN"), "{plan}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mixed_range_rate_selects_delta_math_only_for_float_samples() {
|
||||
let plan = PromPlanner::stmt_to_plan(
|
||||
build_test_mixed_native_histogram_table_provider_with_marker("some_metric", true).await,
|
||||
&build_eval_stmt("rate(some_metric[5m])"),
|
||||
&build_query_engine_state(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.display_indent_schema()
|
||||
.to_string();
|
||||
|
||||
assert!(plan.contains("CASE WHEN"), "{plan}");
|
||||
assert!(plan.contains("prom_mixed_range_float"), "{plan}");
|
||||
assert!(plan.contains("prom_mixed_range_histogram"), "{plan}");
|
||||
assert!(plan.contains(OTLP_AGGREGATION_TEMPORALITY_LABEL), "{plan}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delta_mixed_ranges_drop_and_float_ranges_sum() {
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
for function in ["rate", "increase"] {
|
||||
for histogram_present in [true, false] {
|
||||
let float = source(
|
||||
"lhs",
|
||||
false,
|
||||
1_000,
|
||||
vec![
|
||||
("job", Some("job")),
|
||||
(marker, Some(GREPTIME_TEMPORALITY_DELTA)),
|
||||
],
|
||||
DirectOrValue::Float64(1.0),
|
||||
);
|
||||
let histogram = source(
|
||||
"rhs",
|
||||
!histogram_present,
|
||||
2_000,
|
||||
vec![
|
||||
("job", Some("job")),
|
||||
(marker, Some(GREPTIME_TEMPORALITY_DELTA)),
|
||||
],
|
||||
DirectOrValue::NativeHistogram(direct_or_histogram()),
|
||||
);
|
||||
let collector = PromqlAnnotationCollector::default();
|
||||
let mut planner = PromPlanner {
|
||||
table_provider: build_test_table_provider_with_fields(
|
||||
&[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
|
||||
&[],
|
||||
)
|
||||
.await,
|
||||
ctx: PromPlannerContext::default(),
|
||||
promql_annotations: Some(collector.clone()),
|
||||
};
|
||||
let left_context = direct_or_context("lhs", &["job", marker], "v");
|
||||
let right_context = direct_or_context("rhs", &["job", marker], "v");
|
||||
let input = planner
|
||||
.or_operator(
|
||||
scan(&float),
|
||||
scan(&histogram),
|
||||
left_context.tag_columns.iter().cloned().collect(),
|
||||
right_context.tag_columns.iter().cloned().collect(),
|
||||
left_context,
|
||||
right_context,
|
||||
&or_modifier("lhs or rhs"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut sort_exprs = planner
|
||||
.ctx
|
||||
.tag_columns
|
||||
.iter()
|
||||
.map(|tag| DfExpr::Column(Column::from_name(tag)).sort(true, true))
|
||||
.collect_vec();
|
||||
sort_exprs.push(DfExpr::Column(Column::from_name("ts")).sort(true, true));
|
||||
let input = LogicalPlanBuilder::from(input)
|
||||
.sort(sort_exprs)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let input = LogicalPlan::Extension(Extension {
|
||||
node: Arc::new(SeriesDivide::new(
|
||||
planner.ctx.tag_columns.clone(),
|
||||
"ts".to_string(),
|
||||
input,
|
||||
)),
|
||||
});
|
||||
planner.ctx.start = 2_000;
|
||||
planner.ctx.end = 2_000;
|
||||
planner.ctx.interval = 1_000;
|
||||
planner.ctx.range = Some(2_000);
|
||||
let input = LogicalPlan::Extension(Extension {
|
||||
node: Arc::new(
|
||||
RangeManipulate::new(
|
||||
2_000,
|
||||
2_000,
|
||||
1_000,
|
||||
2_000,
|
||||
"ts".to_string(),
|
||||
planner.ctx.field_columns.clone(),
|
||||
input,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
});
|
||||
|
||||
let PromExpr::Call(call) = parser::parse(&format!("{function}(mixed[2s])")).unwrap()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let preserve_any_value = PromPlanner::field_columns_are_alternative_samples(
|
||||
input.schema(),
|
||||
&planner.ctx.field_columns,
|
||||
);
|
||||
let state = build_query_engine_state();
|
||||
let (mut exprs, _) = planner
|
||||
.create_function_expr(&call.func, vec![], input.schema(), &state)
|
||||
.unwrap();
|
||||
exprs.insert(0, planner.create_time_index_column_expr().unwrap());
|
||||
let plan = LogicalPlanBuilder::from(input)
|
||||
.project(exprs)
|
||||
.unwrap()
|
||||
.filter(
|
||||
planner
|
||||
.create_empty_values_filter_expr(preserve_any_value)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let value_field = plan
|
||||
.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.find(|field| field.data_type() == &ArrowDataType::Float64)
|
||||
.unwrap()
|
||||
.name()
|
||||
.clone();
|
||||
|
||||
let (_, batches) = execute(plan, &state).await;
|
||||
let row_count = batches.iter().map(RecordBatch::num_rows).sum::<usize>();
|
||||
if histogram_present {
|
||||
assert_eq!(0, row_count, "{function}");
|
||||
} else {
|
||||
assert_eq!(1, row_count, "{function}");
|
||||
let expected = if function == "rate" { 0.5 } else { 1.0 };
|
||||
assert_eq!(vec![expected], values(&batches, &value_field), "{function}");
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
let mut infos = Vec::new();
|
||||
collector.append_to(&mut warnings, &mut infos);
|
||||
let expected_warnings = if histogram_present {
|
||||
vec![format!(
|
||||
"{function}: encountered a mix of float and native histogram samples"
|
||||
)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
assert_eq!(expected_warnings, warnings);
|
||||
assert!(infos.is_empty(), "{function}: {infos:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn temporality_matchers_treat_null_as_absent() {
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
let schema = Arc::new(ArrowSchema::new(vec![Field::new(
|
||||
marker,
|
||||
ArrowDataType::Utf8,
|
||||
true,
|
||||
)]));
|
||||
let batch = RecordBatch::try_new(
|
||||
schema.clone(),
|
||||
vec![Arc::new(StringArray::from(vec![
|
||||
None,
|
||||
Some(GREPTIME_TEMPORALITY_DELTA),
|
||||
]))],
|
||||
)
|
||||
.unwrap();
|
||||
let table = Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap());
|
||||
|
||||
for (matcher, expected) in [
|
||||
(r#"="""#, vec![None]),
|
||||
(r#"!="delta""#, vec![None]),
|
||||
(r#"=~".*""#, vec![None, Some(GREPTIME_TEMPORALITY_DELTA)]),
|
||||
(r#"!~"delta""#, vec![None]),
|
||||
(r#"="delta""#, vec![Some(GREPTIME_TEMPORALITY_DELTA)]),
|
||||
] {
|
||||
let query = format!(r#"metric{{{marker}{matcher}}}"#);
|
||||
let scan = LogicalPlanBuilder::scan("labels", provider_as_source(table.clone()), None)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let PromExpr::VectorSelector(selector) = parser::parse(&query).unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
let expressions = PromPlanner::matchers_to_expr(selector.matchers, scan.schema()).unwrap();
|
||||
let display = expressions.iter().map(ToString::to_string).join(" AND ");
|
||||
if matcher == r#"="delta""# {
|
||||
assert!(!display.contains("coalesce"), "{display}");
|
||||
} else if !expressions.is_empty() {
|
||||
assert!(display.contains("coalesce"), "{display}");
|
||||
}
|
||||
let plan = if let Some(filter) = conjunction(expressions) {
|
||||
LogicalPlanBuilder::from(scan)
|
||||
.filter(filter)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap()
|
||||
} else {
|
||||
scan
|
||||
};
|
||||
let (_, batches) = execute(plan, &build_query_engine_state()).await;
|
||||
let actual = batches
|
||||
.iter()
|
||||
.flat_map(|batch| {
|
||||
let labels = batch
|
||||
.column_by_name(marker)
|
||||
.unwrap()
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
(0..batch.num_rows())
|
||||
.map(move |row| (!labels.is_null(row)).then(|| labels.value(row).to_string()))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
expected
|
||||
.into_iter()
|
||||
.map(|value| value.map(str::to_string))
|
||||
.collect::<Vec<_>>(),
|
||||
actual,
|
||||
"{query}"
|
||||
);
|
||||
}
|
||||
|
||||
let ordinary_schema = Arc::new(ArrowSchema::new(vec![Field::new(
|
||||
"label",
|
||||
ArrowDataType::Utf8,
|
||||
true,
|
||||
)]));
|
||||
let ordinary_scan = LogicalPlanBuilder::scan(
|
||||
"ordinary_labels",
|
||||
provider_as_source(Arc::new(
|
||||
MemTable::try_new(ordinary_schema, vec![vec![]]).unwrap(),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let PromExpr::VectorSelector(selector) = parser::parse(r#"metric{label!="delta"}"#).unwrap()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let expressions = PromPlanner::matchers_to_expr(selector.matchers, ordinary_scan.schema())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.join(" AND ");
|
||||
assert!(!expressions.contains("coalesce"), "{expressions}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn binary_joins_align_only_the_temporality_marker() {
|
||||
let marker = OTLP_AGGREGATION_TEMPORALITY_LABEL;
|
||||
for (left_marker, expected_rows) in [(Some(GREPTIME_TEMPORALITY_DELTA), 0), (None, 1)] {
|
||||
let left = source(
|
||||
"lhs",
|
||||
false,
|
||||
1,
|
||||
vec![("job", Some("job")), (marker, left_marker)],
|
||||
DirectOrValue::Float64(1.0),
|
||||
);
|
||||
let right = source(
|
||||
"rhs",
|
||||
false,
|
||||
1,
|
||||
vec![("job", Some("job"))],
|
||||
DirectOrValue::Float64(2.0),
|
||||
);
|
||||
let left_context = direct_or_context("lhs", &["job", marker], "v");
|
||||
let right_context = direct_or_context("rhs", &["job"], "v");
|
||||
let planner = PromPlanner {
|
||||
table_provider: build_test_table_provider_with_fields(
|
||||
&[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
|
||||
&[],
|
||||
)
|
||||
.await,
|
||||
ctx: PromPlannerContext::default(),
|
||||
promql_annotations: None,
|
||||
};
|
||||
let joined = planner
|
||||
.join_on_non_field_columns(
|
||||
scan(&left),
|
||||
scan(&right),
|
||||
TableReference::bare("lhs"),
|
||||
TableReference::bare("rhs"),
|
||||
Some("ts".to_string()),
|
||||
Some("ts".to_string()),
|
||||
false,
|
||||
&None,
|
||||
&left_context,
|
||||
&right_context,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!joined
|
||||
.display_indent_schema()
|
||||
.to_string()
|
||||
.contains("__promql_match_"),
|
||||
"{joined:?}"
|
||||
);
|
||||
let (_, batches) = execute(joined, &build_query_engine_state()).await;
|
||||
assert_eq!(
|
||||
expected_rows,
|
||||
batches.iter().map(RecordBatch::num_rows).sum::<usize>()
|
||||
);
|
||||
|
||||
let PromExpr::Binary(and_expr) = parser::parse("lhs and rhs").unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut planner = PromPlanner {
|
||||
table_provider: build_test_table_provider_with_fields(
|
||||
&[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
|
||||
&[],
|
||||
)
|
||||
.await,
|
||||
ctx: PromPlannerContext::default(),
|
||||
promql_annotations: None,
|
||||
};
|
||||
let set = planner
|
||||
.set_op_on_non_field_columns(
|
||||
scan(&left),
|
||||
scan(&right),
|
||||
left_context,
|
||||
right_context,
|
||||
and_expr.op,
|
||||
&and_expr.modifier,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
set.schema()
|
||||
.fields()
|
||||
.iter()
|
||||
.all(|field| !field.name().starts_with("__promql_match_"))
|
||||
);
|
||||
assert!(
|
||||
!set.display_indent_schema()
|
||||
.to_string()
|
||||
.contains("__promql_match_"),
|
||||
"{set:?}"
|
||||
);
|
||||
let (_, batches) = execute(set, &build_query_engine_state()).await;
|
||||
assert_eq!(
|
||||
expected_rows,
|
||||
batches.iter().map(RecordBatch::num_rows).sum::<usize>()
|
||||
);
|
||||
}
|
||||
|
||||
let left = source(
|
||||
"lhs",
|
||||
false,
|
||||
1,
|
||||
vec![("job", Some("job"))],
|
||||
DirectOrValue::Float64(1.0),
|
||||
);
|
||||
let right = source(
|
||||
"rhs",
|
||||
false,
|
||||
1,
|
||||
vec![("job", Some("job")), (marker, None)],
|
||||
DirectOrValue::Float64(2.0),
|
||||
);
|
||||
let PromExpr::Binary(and_expr) = parser::parse("lhs and rhs").unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut planner = PromPlanner {
|
||||
table_provider: build_test_table_provider_with_fields(
|
||||
&[(DEFAULT_SCHEMA_NAME.to_string(), "dummy".to_string())],
|
||||
&[],
|
||||
)
|
||||
.await,
|
||||
ctx: PromPlannerContext::default(),
|
||||
promql_annotations: None,
|
||||
};
|
||||
let set = planner
|
||||
.set_op_on_non_field_columns(
|
||||
scan(&left),
|
||||
scan(&right),
|
||||
direct_or_context("lhs", &["job"], "v"),
|
||||
direct_or_context("rhs", &["job", marker], "v"),
|
||||
and_expr.op,
|
||||
&and_expr.modifier,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(set.schema().field_with_unqualified_name(marker).is_err());
|
||||
let (_, batches) = execute(set, &build_query_engine_state()).await;
|
||||
assert_eq!(1, batches.iter().map(RecordBatch::num_rows).sum::<usize>());
|
||||
}
|
||||
@@ -177,6 +177,9 @@ pub enum Error {
|
||||
location: Location,
|
||||
},
|
||||
|
||||
#[snafu(display("Invalid OTLP metric input: {}", reason))]
|
||||
InvalidOtlpMetricInput { reason: String },
|
||||
|
||||
#[snafu(display(
|
||||
"Too many concurrent large requests, limit: {}, request size: {}",
|
||||
ReadableSize(*limit as u64),
|
||||
@@ -755,6 +758,7 @@ impl ErrorExt for Error {
|
||||
|
||||
NotSupported { .. }
|
||||
| InvalidParameter { .. }
|
||||
| InvalidOtlpMetricInput { .. }
|
||||
| InvalidQuery { .. }
|
||||
| InfluxdbLineProtocol { .. }
|
||||
| InvalidOpentsdbJsonRequest { .. }
|
||||
|
||||
@@ -126,15 +126,24 @@ pub async fn metrics(
|
||||
}));
|
||||
let query_ctx = Arc::new(query_ctx);
|
||||
|
||||
handler.metrics(request, query_ctx).await.map(|outcome| {
|
||||
if outcome.accepted_data_points == 0 && outcome.rejected_data_points > 0 {
|
||||
OtlpMetricsResponse::Failure(outcome)
|
||||
} else if outcome.rejected_data_points > 0 || outcome.error_message.is_some() {
|
||||
OtlpMetricsResponse::PartialSuccess(outcome)
|
||||
} else {
|
||||
OtlpMetricsResponse::FullSuccess(outcome)
|
||||
match handler.metrics(request, query_ctx).await {
|
||||
Ok(outcome) => {
|
||||
if outcome.accepted_data_points == 0 && outcome.rejected_data_points > 0 {
|
||||
Ok(OtlpMetricsResponse::Failure(outcome))
|
||||
} else if outcome.rejected_data_points > 0 || outcome.error_message.is_some() {
|
||||
Ok(OtlpMetricsResponse::PartialSuccess(outcome))
|
||||
} else {
|
||||
Ok(OtlpMetricsResponse::FullSuccess(outcome))
|
||||
}
|
||||
}
|
||||
})
|
||||
Err(error::Error::InvalidOtlpMetricInput { reason }) => {
|
||||
Ok(OtlpMetricsResponse::Failure(MetricsIngestOutcome {
|
||||
error_message: Some(reason),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
@@ -365,3 +374,6 @@ impl IntoResponse for OtlpTraceResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use axum::body::to_bytes;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn metric_failure_is_a_protobuf_invalid_argument() {
|
||||
let response = OtlpMetricsResponse::Failure(MetricsIngestOutcome {
|
||||
error_message: Some("reserved temporality label".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response();
|
||||
|
||||
assert_eq!(StatusCode::BAD_REQUEST, response.status());
|
||||
assert_eq!(
|
||||
&CONTENT_TYPE_PROTOBUF,
|
||||
response.headers().get(header::CONTENT_TYPE).unwrap()
|
||||
);
|
||||
let body = to_bytes(response.into_body(), 1024).await.unwrap();
|
||||
let status = GoogleRpcStatus::decode(body).unwrap();
|
||||
assert_eq!(tonic::Code::InvalidArgument as i32, status.code);
|
||||
assert_eq!("reserved temporality label", status.message);
|
||||
}
|
||||
@@ -66,7 +66,10 @@ use store_api::metric_engine_consts::{
|
||||
};
|
||||
use table::TableRef;
|
||||
use table::metadata::TableInfo;
|
||||
use table::requests::{SEMANTIC_METRIC_TEMPORALITY, SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT};
|
||||
use table::requests::{
|
||||
METRIC_TEMPORALITY_DELTA, SEMANTIC_METRIC_TEMPORALITY, SEMANTIC_METRIC_TYPE,
|
||||
SEMANTIC_METRIC_UNIT, SEMANTIC_VALUE_MIXED,
|
||||
};
|
||||
|
||||
pub use super::result::prometheus_resp::PrometheusJsonResponse;
|
||||
use crate::error::{
|
||||
@@ -2024,7 +2027,12 @@ fn prometheus_metadata_from_table(table_info: &TableInfo) -> PromMetadata {
|
||||
Some(metric_type)
|
||||
if options
|
||||
.get(SEMANTIC_METRIC_TEMPORALITY)
|
||||
.is_some_and(|temporality| temporality == "delta")
|
||||
.is_some_and(|temporality| {
|
||||
matches!(
|
||||
temporality.as_str(),
|
||||
METRIC_TEMPORALITY_DELTA | SEMANTIC_VALUE_MIXED
|
||||
)
|
||||
})
|
||||
&& matches!(
|
||||
metric_type.as_str(),
|
||||
"counter" | "histogram" | "updown_counter"
|
||||
@@ -3504,6 +3512,9 @@ mod tests {
|
||||
("mixed", None, "unknown"),
|
||||
("counter", Some("delta"), "unknown"),
|
||||
("histogram", Some("delta"), "unknown"),
|
||||
("counter", Some("mixed"), "unknown"),
|
||||
("histogram", Some("mixed"), "unknown"),
|
||||
("updown_counter", Some("mixed"), "unknown"),
|
||||
] {
|
||||
let mut table_info = table_info.clone();
|
||||
table_info
|
||||
|
||||
@@ -146,6 +146,16 @@ impl ArrowMetricsService for OtelArrowServiceHandler<OpenTelemetryProtocolHandle
|
||||
});
|
||||
let outcome = match handler.metrics(request, query_ctx.clone()).await {
|
||||
Ok(outcome) => outcome,
|
||||
Err(error::Error::InvalidOtlpMetricInput { reason }) => {
|
||||
let _ = sender
|
||||
.send(Ok(BatchStatus {
|
||||
batch_id,
|
||||
status_code: ArrowStatusCode::InvalidArgument as i32,
|
||||
status_message: reason,
|
||||
}))
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = sender
|
||||
.send(Err(Status::new(
|
||||
|
||||
+213
-69
@@ -23,7 +23,10 @@ use common_grpc::precision::Precision;
|
||||
use common_query::native_histogram::{
|
||||
encode_native_histogram, native_histogram_column_schema, native_histogram_value_type,
|
||||
};
|
||||
use common_query::prelude::{GREPTIME_COUNT, greptime_timestamp, greptime_value};
|
||||
use common_query::prelude::{
|
||||
GREPTIME_COUNT, GREPTIME_TEMPORALITY_DELTA, OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
greptime_timestamp, greptime_value,
|
||||
};
|
||||
use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
|
||||
use common_telemetry::warn;
|
||||
use lazy_static::lazy_static;
|
||||
@@ -32,8 +35,9 @@ use otel_arrow_rust::proto::opentelemetry::common::v1::{AnyValue, KeyValue, any_
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::{metric, number_data_point, *};
|
||||
use session::protocol_ctx::{MetricType, OtlpMetricCtx};
|
||||
use table::requests::{
|
||||
METADATA_QUALITY_DECLARED, SEMANTIC_METRIC_METADATA_QUALITY, SEMANTIC_METRIC_ORIGINAL_NAME,
|
||||
SEMANTIC_METRIC_TEMPORALITY, SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT,
|
||||
METADATA_QUALITY_DECLARED, METRIC_TEMPORALITY_CUMULATIVE, METRIC_TEMPORALITY_DELTA,
|
||||
SEMANTIC_METRIC_METADATA_QUALITY, SEMANTIC_METRIC_ORIGINAL_NAME, SEMANTIC_METRIC_TEMPORALITY,
|
||||
SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT,
|
||||
};
|
||||
|
||||
use crate::error::{self, Result};
|
||||
@@ -285,8 +289,8 @@ fn temporality_value(data: &metric::Data) -> Option<&'static str> {
|
||||
_ => return None,
|
||||
};
|
||||
match AggregationTemporality::try_from(raw) {
|
||||
Ok(AggregationTemporality::Delta) => Some("delta"),
|
||||
Ok(AggregationTemporality::Cumulative) => Some("cumulative"),
|
||||
Ok(AggregationTemporality::Delta) => Some(METRIC_TEMPORALITY_DELTA),
|
||||
Ok(AggregationTemporality::Cumulative) => Some(METRIC_TEMPORALITY_CUMULATIVE),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -509,18 +513,15 @@ fn encode_metrics(
|
||||
add_accepted_data_points(outcome, summary.data_points.len())?;
|
||||
!summary.data_points.is_empty()
|
||||
}
|
||||
metric::Data::Histogram(hist) => {
|
||||
encode_histogram(
|
||||
table_writer,
|
||||
&name,
|
||||
hist,
|
||||
resource_attrs,
|
||||
scope_attrs,
|
||||
metric_ctx,
|
||||
)?;
|
||||
add_accepted_data_points(outcome, hist.data_points.len())?;
|
||||
!hist.data_points.is_empty()
|
||||
}
|
||||
metric::Data::Histogram(hist) => encode_histogram(
|
||||
table_writer,
|
||||
&name,
|
||||
hist,
|
||||
resource_attrs,
|
||||
scope_attrs,
|
||||
metric_ctx,
|
||||
outcome,
|
||||
)?,
|
||||
metric::Data::ExponentialHistogram(hist) => encode_exponential_histogram(
|
||||
table_writer,
|
||||
&name,
|
||||
@@ -932,9 +933,12 @@ fn write_attributes(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let tags = attrs.iter().filter_map(|attr| {
|
||||
let mut tags = Vec::with_capacity(attrs.len());
|
||||
for attr in attrs {
|
||||
// TODO(sunng87): allow different type of values
|
||||
let value = scalar_value_string(attr.value.as_ref())?;
|
||||
let Some(value) = scalar_value_string(attr.value.as_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let key = match attribute_type {
|
||||
AttributeType::Resource | AttributeType::DataPoint => {
|
||||
translate_label_name(&attr.key, metric_ctx.metric_translation_strategy)
|
||||
@@ -947,9 +951,18 @@ fn write_attributes(
|
||||
}
|
||||
AttributeType::Legacy => legacy_normalize_otlp_name(&attr.key),
|
||||
};
|
||||
Some((key, value))
|
||||
});
|
||||
row_writer::write_tags(writer, tags, row)?;
|
||||
if key == OTLP_AGGREGATION_TEMPORALITY_LABEL {
|
||||
return Err(error::InvalidOtlpMetricInputSnafu {
|
||||
reason: format!(
|
||||
"OTLP attribute `{}` resolves to reserved label `{}`",
|
||||
attr.key, OTLP_AGGREGATION_TEMPORALITY_LABEL
|
||||
),
|
||||
}
|
||||
.build());
|
||||
}
|
||||
tags.push((key, value));
|
||||
}
|
||||
row_writer::write_tags(writer, tags.into_iter(), row)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -998,6 +1011,26 @@ fn write_data_point_value(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_temporality_tag(
|
||||
table: &mut TableData,
|
||||
row: &mut Vec<Value>,
|
||||
is_delta: bool,
|
||||
) -> Result<()> {
|
||||
if is_delta {
|
||||
row_writer::write_tag(
|
||||
table,
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
GREPTIME_TEMPORALITY_DELTA,
|
||||
row,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_no_recorded_value(flags: u32) -> bool {
|
||||
flags & DataPointFlags::NoRecordedValueMask as u32 != 0
|
||||
}
|
||||
|
||||
fn write_tags_and_timestamp(
|
||||
table: &mut TableData,
|
||||
row: &mut Vec<Value>,
|
||||
@@ -1086,7 +1119,6 @@ fn encode_gauge(
|
||||
|
||||
/// encode this sum metric
|
||||
///
|
||||
/// `aggregation_temporality` and `monotonic` are ignored for now
|
||||
fn encode_sum(
|
||||
table_writer: &mut MultiTableData,
|
||||
name: &str,
|
||||
@@ -1095,6 +1127,10 @@ fn encode_sum(
|
||||
scope_attrs: Option<&Vec<KeyValue>>,
|
||||
metric_ctx: &OtlpMetricCtx,
|
||||
) -> Result<()> {
|
||||
let is_delta = matches!(
|
||||
AggregationTemporality::try_from(sum.aggregation_temporality),
|
||||
Ok(AggregationTemporality::Delta)
|
||||
);
|
||||
let table = table_writer.get_or_default_table_data(
|
||||
name,
|
||||
APPROXIMATE_COLUMN_COUNT,
|
||||
@@ -1112,7 +1148,17 @@ fn encode_sum(
|
||||
data_point.time_unix_nano as i64,
|
||||
metric_ctx,
|
||||
)?;
|
||||
write_data_point_value(table, &mut row, greptime_value(), &data_point.value)?;
|
||||
write_temporality_tag(table, &mut row, is_delta)?;
|
||||
if has_no_recorded_value(data_point.flags) {
|
||||
row_writer::write_f64(
|
||||
table,
|
||||
greptime_value(),
|
||||
f64::from_bits(PROMETHEUS_STALE_NAN_BITS),
|
||||
&mut row,
|
||||
)?;
|
||||
} else {
|
||||
write_data_point_value(table, &mut row, greptime_value(), &data_point.value)?;
|
||||
}
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
@@ -1139,22 +1185,71 @@ fn encode_histogram(
|
||||
resource_attrs: Option<&Vec<KeyValue>>,
|
||||
scope_attrs: Option<&Vec<KeyValue>>,
|
||||
metric_ctx: &OtlpMetricCtx,
|
||||
) -> Result<()> {
|
||||
outcome: &mut MetricsIngestOutcome,
|
||||
) -> Result<bool> {
|
||||
let normalized_name = name;
|
||||
|
||||
let bucket_table_name = format!("{}{}", normalized_name, BUCKET_TABLE_SUFFIX);
|
||||
let sum_table_name = format!("{}{}", normalized_name, SUM_TABLE_SUFFIX);
|
||||
let count_table_name = format!("{}{}", normalized_name, COUNT_TABLE_SUFFIX);
|
||||
|
||||
let data_points_len = hist.data_points.len();
|
||||
for data_point in &hist.data_points {
|
||||
let bucket_table = table_writer.get_or_default_table_data(
|
||||
&bucket_table_name,
|
||||
APPROXIMATE_COLUMN_COUNT,
|
||||
data_points_len * 3,
|
||||
);
|
||||
let mut accumulated_count = 0;
|
||||
for (idx, count) in data_point.bucket_counts.iter().enumerate() {
|
||||
let is_delta = matches!(
|
||||
AggregationTemporality::try_from(hist.aggregation_temporality),
|
||||
Ok(AggregationTemporality::Delta)
|
||||
);
|
||||
let stale_value = f64::from_bits(PROMETHEUS_STALE_NAN_BITS);
|
||||
let mut emitted = false;
|
||||
for (index, data_point) in hist.data_points.iter().enumerate() {
|
||||
if let Some(reason) = histogram_data_point_rejection(data_point, is_delta) {
|
||||
reject_data_points(outcome, 1, || {
|
||||
format!("metric `{name}` data point {index}: {reason}")
|
||||
})?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let bucket_table =
|
||||
table_writer.get_or_default_table_data(&bucket_table_name, APPROXIMATE_COLUMN_COUNT, 0);
|
||||
let no_recorded_value = has_no_recorded_value(data_point.flags);
|
||||
if no_recorded_value {
|
||||
bucket_table.reserve_rows(data_point.explicit_bounds.len());
|
||||
bucket_table.reserve_rows(1);
|
||||
} else {
|
||||
bucket_table.reserve_rows(data_point.bucket_counts.len().max(1));
|
||||
}
|
||||
let bucket_values = if no_recorded_value {
|
||||
data_point
|
||||
.explicit_bounds
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(f64::INFINITY))
|
||||
.map(|bound| (bound, stale_value))
|
||||
.collect::<Vec<_>>()
|
||||
} else if data_point.bucket_counts.is_empty() && data_point.explicit_bounds.is_empty() {
|
||||
// OTLP count-only histograms map to one implicit Prometheus infinity bucket.
|
||||
vec![(f64::INFINITY, data_point.count as f64)]
|
||||
} else {
|
||||
let mut accumulated_count = 0u64;
|
||||
let mut values = Vec::with_capacity(data_point.bucket_counts.len());
|
||||
for (idx, count) in data_point.bucket_counts.iter().enumerate() {
|
||||
accumulated_count = accumulated_count.checked_add(*count).ok_or_else(|| {
|
||||
error::InvalidParameterSnafu {
|
||||
reason: format!(
|
||||
"metric `{name}` data point {index}: bucket prefix overflows u64"
|
||||
),
|
||||
}
|
||||
.build()
|
||||
})?;
|
||||
let bound =
|
||||
data_point.explicit_bounds.get(idx).copied().or_else(|| {
|
||||
(idx == data_point.explicit_bounds.len()).then_some(f64::INFINITY)
|
||||
});
|
||||
if let Some(bound) = bound {
|
||||
values.push((bound, accumulated_count as f64));
|
||||
}
|
||||
}
|
||||
values
|
||||
};
|
||||
for (bound, value) in bucket_values {
|
||||
let mut bucket_row = bucket_table.alloc_one_row();
|
||||
write_tags_and_timestamp(
|
||||
bucket_table,
|
||||
@@ -1165,31 +1260,9 @@ fn encode_histogram(
|
||||
data_point.time_unix_nano as i64,
|
||||
metric_ctx,
|
||||
)?;
|
||||
|
||||
if let Some(upper_bounds) = data_point.explicit_bounds.get(idx) {
|
||||
row_writer::write_tag(
|
||||
bucket_table,
|
||||
HISTOGRAM_LE_COLUMN,
|
||||
upper_bounds,
|
||||
&mut bucket_row,
|
||||
)?;
|
||||
} else if idx == data_point.explicit_bounds.len() {
|
||||
// The last bucket
|
||||
row_writer::write_tag(
|
||||
bucket_table,
|
||||
HISTOGRAM_LE_COLUMN,
|
||||
f64::INFINITY,
|
||||
&mut bucket_row,
|
||||
)?;
|
||||
}
|
||||
|
||||
accumulated_count += count;
|
||||
row_writer::write_f64(
|
||||
bucket_table,
|
||||
greptime_value(),
|
||||
accumulated_count as f64,
|
||||
&mut bucket_row,
|
||||
)?;
|
||||
write_temporality_tag(bucket_table, &mut bucket_row, is_delta)?;
|
||||
row_writer::write_tag(bucket_table, HISTOGRAM_LE_COLUMN, bound, &mut bucket_row)?;
|
||||
row_writer::write_f64(bucket_table, greptime_value(), value, &mut bucket_row)?;
|
||||
|
||||
bucket_table.add_row(bucket_row);
|
||||
}
|
||||
@@ -1198,7 +1271,7 @@ fn encode_histogram(
|
||||
let sum_table = table_writer.get_or_default_table_data(
|
||||
&sum_table_name,
|
||||
APPROXIMATE_COLUMN_COUNT,
|
||||
data_points_len,
|
||||
hist.data_points.len(),
|
||||
);
|
||||
let mut sum_row = sum_table.alloc_one_row();
|
||||
write_tags_and_timestamp(
|
||||
@@ -1210,15 +1283,20 @@ fn encode_histogram(
|
||||
data_point.time_unix_nano as i64,
|
||||
metric_ctx,
|
||||
)?;
|
||||
|
||||
row_writer::write_f64(sum_table, greptime_value(), sum, &mut sum_row)?;
|
||||
write_temporality_tag(sum_table, &mut sum_row, is_delta)?;
|
||||
row_writer::write_f64(
|
||||
sum_table,
|
||||
greptime_value(),
|
||||
if no_recorded_value { stale_value } else { sum },
|
||||
&mut sum_row,
|
||||
)?;
|
||||
sum_table.add_row(sum_row);
|
||||
}
|
||||
|
||||
let count_table = table_writer.get_or_default_table_data(
|
||||
&count_table_name,
|
||||
APPROXIMATE_COLUMN_COUNT,
|
||||
data_points_len,
|
||||
hist.data_points.len(),
|
||||
);
|
||||
let mut count_row = count_table.alloc_one_row();
|
||||
write_tags_and_timestamp(
|
||||
@@ -1230,17 +1308,75 @@ fn encode_histogram(
|
||||
data_point.time_unix_nano as i64,
|
||||
metric_ctx,
|
||||
)?;
|
||||
|
||||
write_temporality_tag(count_table, &mut count_row, is_delta)?;
|
||||
row_writer::write_f64(
|
||||
count_table,
|
||||
greptime_value(),
|
||||
data_point.count as f64,
|
||||
if no_recorded_value {
|
||||
stale_value
|
||||
} else {
|
||||
data_point.count as f64
|
||||
},
|
||||
&mut count_row,
|
||||
)?;
|
||||
count_table.add_row(count_row);
|
||||
add_accepted_data_points(outcome, 1)?;
|
||||
emitted = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(emitted)
|
||||
}
|
||||
|
||||
pub(crate) fn histogram_data_point_rejection(
|
||||
data_point: &HistogramDataPoint,
|
||||
is_delta: bool,
|
||||
) -> Option<String> {
|
||||
if has_no_recorded_value(data_point.flags) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_delta {
|
||||
let valid_empty_layout =
|
||||
data_point.bucket_counts.is_empty() && data_point.explicit_bounds.is_empty();
|
||||
let expected_buckets = data_point.explicit_bounds.len().checked_add(1);
|
||||
if !valid_empty_layout && expected_buckets != Some(data_point.bucket_counts.len()) {
|
||||
return Some(format!(
|
||||
"bucket_counts length {} must equal explicit_bounds length {} plus one",
|
||||
data_point.bucket_counts.len(),
|
||||
data_point.explicit_bounds.len()
|
||||
));
|
||||
}
|
||||
if data_point
|
||||
.explicit_bounds
|
||||
.iter()
|
||||
.any(|bound| !bound.is_finite())
|
||||
|| data_point
|
||||
.explicit_bounds
|
||||
.windows(2)
|
||||
.any(|bounds| bounds[0] >= bounds[1])
|
||||
{
|
||||
return Some("explicit_bounds must be finite and strictly increasing".to_string());
|
||||
}
|
||||
if data_point.count == 0 && data_point.sum.is_some_and(|sum| sum != 0.0) {
|
||||
return Some("sum must be absent or zero when count is zero".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let bucket_total = data_point
|
||||
.bucket_counts
|
||||
.iter()
|
||||
.try_fold(0u64, |total, count| total.checked_add(*count));
|
||||
let Some(bucket_total) = bucket_total else {
|
||||
return Some("bucket prefix overflows u64".to_string());
|
||||
};
|
||||
if is_delta && !data_point.bucket_counts.is_empty() && bucket_total != data_point.count {
|
||||
return Some(format!(
|
||||
"buckets contain {bucket_total} observations, declared count is {}",
|
||||
data_point.count
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn encode_summary(
|
||||
@@ -1388,6 +1524,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
mod delta;
|
||||
|
||||
fn keyvalue(key: &str, value: &str) -> KeyValue {
|
||||
KeyValue {
|
||||
key: key.into(),
|
||||
@@ -1786,6 +1924,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_encode_histogram() {
|
||||
let mut tables = MultiTableData::default();
|
||||
let mut outcome = MetricsIngestOutcome::default();
|
||||
|
||||
let data_points = vec![HistogramDataPoint {
|
||||
attributes: vec![keyvalue("host", "testserver")],
|
||||
@@ -1811,15 +1950,17 @@ mod tests {
|
||||
Some(&vec![]),
|
||||
Some(&vec![keyvalue("scope", "otel")]),
|
||||
&OtlpMetricCtx::default(),
|
||||
&mut outcome,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(3, tables.num_tables());
|
||||
assert_eq!(1, outcome.accepted_data_points);
|
||||
|
||||
// bucket table
|
||||
let bucket_table = tables.get_or_default_table_data("histo_bucket", 0, 0);
|
||||
assert_eq!(bucket_table.num_rows(), 5);
|
||||
assert_eq!(bucket_table.num_columns(), 5);
|
||||
assert_eq!(bucket_table.num_columns(), 6);
|
||||
assert_eq!(
|
||||
bucket_table
|
||||
.columns()
|
||||
@@ -1830,6 +1971,7 @@ mod tests {
|
||||
"otel_scope_scope",
|
||||
"host",
|
||||
greptime_timestamp(),
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
"le",
|
||||
greptime_value(),
|
||||
]
|
||||
@@ -1837,7 +1979,7 @@ mod tests {
|
||||
|
||||
let sum_table = tables.get_or_default_table_data("histo_sum", 0, 0);
|
||||
assert_eq!(sum_table.num_rows(), 1);
|
||||
assert_eq!(sum_table.num_columns(), 4);
|
||||
assert_eq!(sum_table.num_columns(), 5);
|
||||
assert_eq!(
|
||||
sum_table
|
||||
.columns()
|
||||
@@ -1848,13 +1990,14 @@ mod tests {
|
||||
"otel_scope_scope",
|
||||
"host",
|
||||
greptime_timestamp(),
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
greptime_value()
|
||||
]
|
||||
);
|
||||
|
||||
let count_table = tables.get_or_default_table_data("histo_count", 0, 0);
|
||||
assert_eq!(count_table.num_rows(), 1);
|
||||
assert_eq!(count_table.num_columns(), 4);
|
||||
assert_eq!(count_table.num_columns(), 5);
|
||||
assert_eq!(
|
||||
count_table
|
||||
.columns()
|
||||
@@ -1865,6 +2008,7 @@ mod tests {
|
||||
"otel_scope_scope",
|
||||
"host",
|
||||
greptime_timestamp(),
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
greptime_value()
|
||||
]
|
||||
);
|
||||
|
||||
@@ -27,13 +27,16 @@ use common_catalog::consts::SEMANTIC_GRAPH_WINDOW_NANOS;
|
||||
use common_grpc::precision::Precision;
|
||||
use common_query::prelude::{greptime_timestamp, greptime_value};
|
||||
use otel_arrow_rust::proto::opentelemetry::common::v1::KeyValue;
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::{ResourceMetrics, metric};
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
|
||||
AggregationTemporality, ResourceMetrics, metric,
|
||||
};
|
||||
use session::protocol_ctx::OtlpMetricCtx;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::otlp::metrics::{
|
||||
INSTANCE_KEY, JOB_KEY, ServiceIdentity, exponential_histogram_gate,
|
||||
exponential_histogram_value, scalar_value_string, service_identity,
|
||||
exponential_histogram_value, histogram_data_point_rejection, scalar_value_string,
|
||||
service_identity,
|
||||
};
|
||||
use crate::otlp::trace::{
|
||||
KEY_CONTAINER_ID, KEY_CONTAINER_NAME, KEY_HOST_ID, KEY_HOST_NAME, KEY_K8S_CONTAINER_NAME,
|
||||
@@ -193,7 +196,19 @@ fn for_each_encoded_time(
|
||||
visit_all(s.data_points.iter().map(|p| p.time_unix_nano), &mut visit)
|
||||
}
|
||||
Some(metric::Data::Histogram(h)) => {
|
||||
visit_all(h.data_points.iter().map(|p| p.time_unix_nano), &mut visit)
|
||||
let is_delta = matches!(
|
||||
AggregationTemporality::try_from(h.aggregation_temporality),
|
||||
Ok(AggregationTemporality::Delta)
|
||||
);
|
||||
visit_all(
|
||||
h.data_points
|
||||
.iter()
|
||||
.filter(|point| {
|
||||
histogram_data_point_rejection(point, is_delta).is_none()
|
||||
})
|
||||
.map(|point| point.time_unix_nano),
|
||||
&mut visit,
|
||||
)
|
||||
}
|
||||
Some(metric::Data::Summary(s)) => {
|
||||
visit_all(s.data_points.iter().map(|p| p.time_unix_nano), &mut visit)
|
||||
@@ -227,6 +242,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
mod delta;
|
||||
|
||||
fn kv(key: &str, value: &str) -> KeyValue {
|
||||
KeyValue {
|
||||
key: key.into(),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
|
||||
DataPointFlags, Histogram, HistogramDataPoint,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn observe_ignores_rejected_classic_histogram_windows() {
|
||||
let window = SEMANTIC_GRAPH_WINDOW_NANOS;
|
||||
let resource = ResourceMetrics {
|
||||
scope_metrics: vec![ScopeMetrics {
|
||||
metrics: vec![Metric {
|
||||
data: Some(metric::Data::Histogram(Histogram {
|
||||
data_points: vec![
|
||||
HistogramDataPoint {
|
||||
time_unix_nano: (window + 1) as u64,
|
||||
count: 1,
|
||||
bucket_counts: vec![1],
|
||||
..Default::default()
|
||||
},
|
||||
HistogramDataPoint {
|
||||
time_unix_nano: (3 * window + 1) as u64,
|
||||
count: 1,
|
||||
bucket_counts: vec![1],
|
||||
explicit_bounds: vec![1.0, 2.0],
|
||||
..Default::default()
|
||||
},
|
||||
HistogramDataPoint {
|
||||
time_unix_nano: (4 * window + 1) as u64,
|
||||
count: u64::MAX,
|
||||
bucket_counts: vec![u64::MAX, 1],
|
||||
flags: DataPointFlags::NoRecordedValueMask as u32,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
aggregation_temporality: AggregationTemporality::Delta as i32,
|
||||
})),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let mut data = ResourceInfoData::default();
|
||||
data.observe(
|
||||
&[kv("service.name", "api")],
|
||||
&resource,
|
||||
&OtlpMetricCtx::default(),
|
||||
);
|
||||
|
||||
let windows = data.rows.values().next().unwrap();
|
||||
assert_eq!(
|
||||
vec![window + 1, 4 * window + 1],
|
||||
windows.values().copied().collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
// Copyright 2023 Greptime Team
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use otel_arrow_rust::proto::opentelemetry::common::v1::InstrumentationScope;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn table_rows<'a>(request: &'a RowInsertRequests, table: &str) -> &'a api::v1::Rows {
|
||||
request
|
||||
.inserts
|
||||
.iter()
|
||||
.find(|insert| insert.table_name == table)
|
||||
.unwrap_or_else(|| panic!("missing table {table}"))
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn column_index(rows: &api::v1::Rows, column: &str) -> usize {
|
||||
rows.schema
|
||||
.iter()
|
||||
.position(|schema| schema.column_name == column)
|
||||
.unwrap_or_else(|| panic!("missing column {column}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_delta_sum_identity_and_stale_marker() {
|
||||
set_default_prefix(Some("custom")).unwrap();
|
||||
assert_eq!(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
"otlp_aggregation_temporality"
|
||||
);
|
||||
|
||||
let points = vec![
|
||||
NumberDataPoint {
|
||||
attributes: vec![keyvalue("host", "a")],
|
||||
time_unix_nano: 1_000_000,
|
||||
value: Some(Value::AsInt(10)),
|
||||
..Default::default()
|
||||
},
|
||||
NumberDataPoint {
|
||||
attributes: vec![keyvalue("host", "a")],
|
||||
time_unix_nano: 2_000_000,
|
||||
value: Some(Value::AsDouble(20.5)),
|
||||
..Default::default()
|
||||
},
|
||||
NumberDataPoint {
|
||||
attributes: vec![keyvalue("host", "a")],
|
||||
time_unix_nano: 3_000_000,
|
||||
value: Some(Value::AsDouble(99.0)),
|
||||
flags: DataPointFlags::NoRecordedValueMask as u32,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let metric = Metric {
|
||||
name: "requests".to_string(),
|
||||
data: Some(metric::Data::Sum(Sum {
|
||||
data_points: points,
|
||||
aggregation_temporality: AggregationTemporality::Delta as i32,
|
||||
is_monotonic: true,
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let conversion =
|
||||
to_grpc_insert_requests(metrics_request(vec![metric]), &mut OtlpMetricCtx::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(3, conversion.outcome.accepted_data_points);
|
||||
assert_eq!(0, conversion.outcome.rejected_data_points);
|
||||
let rows = table_rows(&conversion.requests, "requests_total");
|
||||
let value = column_index(rows, greptime_value());
|
||||
let temporality = column_index(rows, OTLP_AGGREGATION_TEMPORALITY_LABEL);
|
||||
let host = column_index(rows, "host");
|
||||
let values = rows
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.values[value].value_data.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(Some(&ValueData::F64Value(10.0)), values.first().copied());
|
||||
assert_eq!(Some(&ValueData::F64Value(20.5)), values.get(1).copied());
|
||||
let ValueData::F64Value(stale) = values[2] else {
|
||||
panic!("expected stale float")
|
||||
};
|
||||
assert_eq!(PROMETHEUS_STALE_NAN_BITS, stale.to_bits());
|
||||
for row in &rows.rows {
|
||||
assert_eq!(
|
||||
Some(&ValueData::StringValue(
|
||||
GREPTIME_TEMPORALITY_DELTA.to_string()
|
||||
)),
|
||||
row.values[temporality].value_data.as_ref()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(&ValueData::StringValue("a".to_string())),
|
||||
row.values[host].value_data.as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
for temporality in [
|
||||
AggregationTemporality::Cumulative as i32,
|
||||
AggregationTemporality::Unspecified as i32,
|
||||
] {
|
||||
let metric = Metric {
|
||||
name: format!("sum_{temporality}"),
|
||||
data: Some(metric::Data::Sum(Sum {
|
||||
data_points: vec![NumberDataPoint::default()],
|
||||
aggregation_temporality: temporality,
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let conversion =
|
||||
to_grpc_insert_requests(metrics_request(vec![metric]), &mut OtlpMetricCtx::default())
|
||||
.unwrap();
|
||||
assert!(
|
||||
conversion.requests.inserts[0]
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.schema
|
||||
.iter()
|
||||
.all(|column| column.column_name != OTLP_AGGREGATION_TEMPORALITY_LABEL)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delta_histogram_partial_rejection_and_inline_tombstone() {
|
||||
let valid = HistogramDataPoint {
|
||||
time_unix_nano: 1_000_000,
|
||||
count: 10,
|
||||
sum: Some(12.0),
|
||||
bucket_counts: vec![2, 3, 5],
|
||||
explicit_bounds: vec![1.0, 2.0],
|
||||
..Default::default()
|
||||
};
|
||||
let malformed = HistogramDataPoint {
|
||||
time_unix_nano: 2_000_000,
|
||||
count: 10,
|
||||
bucket_counts: vec![10],
|
||||
explicit_bounds: vec![1.0, 2.0],
|
||||
..Default::default()
|
||||
};
|
||||
let tombstone = HistogramDataPoint {
|
||||
time_unix_nano: 3_000_000,
|
||||
count: u64::MAX,
|
||||
sum: Some(99.0),
|
||||
bucket_counts: vec![u64::MAX, 1],
|
||||
explicit_bounds: vec![3.0, 2.0],
|
||||
flags: DataPointFlags::NoRecordedValueMask as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let metric = Metric {
|
||||
name: "latency".to_string(),
|
||||
data: Some(metric::Data::Histogram(Histogram {
|
||||
data_points: vec![valid, malformed, tombstone],
|
||||
aggregation_temporality: AggregationTemporality::Delta as i32,
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let conversion =
|
||||
to_grpc_insert_requests(metrics_request(vec![metric]), &mut OtlpMetricCtx::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(2, conversion.outcome.accepted_data_points);
|
||||
assert_eq!(1, conversion.outcome.rejected_data_points);
|
||||
assert!(
|
||||
conversion
|
||||
.outcome
|
||||
.error_message
|
||||
.unwrap()
|
||||
.contains("bucket_counts length")
|
||||
);
|
||||
let buckets = table_rows(&conversion.requests, "latency_bucket");
|
||||
assert_eq!(6, buckets.rows.len());
|
||||
let value = column_index(buckets, greptime_value());
|
||||
let le = column_index(buckets, HISTOGRAM_LE_COLUMN);
|
||||
let temporality = column_index(buckets, OTLP_AGGREGATION_TEMPORALITY_LABEL);
|
||||
let ordinary = buckets.rows[..3]
|
||||
.iter()
|
||||
.map(|row| row.values[value].value_data.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
vec![
|
||||
Some(ValueData::F64Value(2.0)),
|
||||
Some(ValueData::F64Value(5.0)),
|
||||
Some(ValueData::F64Value(10.0)),
|
||||
],
|
||||
ordinary
|
||||
);
|
||||
let tombstone_bounds = buckets.rows[3..]
|
||||
.iter()
|
||||
.map(|row| row.values[le].value_data.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
vec![
|
||||
Some(ValueData::StringValue("3".to_string())),
|
||||
Some(ValueData::StringValue("2".to_string())),
|
||||
Some(ValueData::StringValue("inf".to_string())),
|
||||
],
|
||||
tombstone_bounds
|
||||
);
|
||||
for row in &buckets.rows {
|
||||
assert_eq!(
|
||||
Some(&ValueData::StringValue(
|
||||
GREPTIME_TEMPORALITY_DELTA.to_string()
|
||||
)),
|
||||
row.values[temporality].value_data.as_ref()
|
||||
);
|
||||
}
|
||||
for row in &buckets.rows[3..] {
|
||||
let Some(ValueData::F64Value(value)) = row.values[value].value_data else {
|
||||
panic!("expected stale float")
|
||||
};
|
||||
assert_eq!(PROMETHEUS_STALE_NAN_BITS, value.to_bits());
|
||||
}
|
||||
for table in ["latency_sum", "latency_count"] {
|
||||
let rows = table_rows(&conversion.requests, table);
|
||||
assert_eq!(2, rows.rows.len());
|
||||
let value = column_index(rows, greptime_value());
|
||||
let Some(ValueData::F64Value(stale)) = rows.rows[1].values[value].value_data else {
|
||||
panic!("expected stale float")
|
||||
};
|
||||
assert_eq!(PROMETHEUS_STALE_NAN_BITS, stale.to_bits());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_histogram_validation_preserves_supported_siblings() {
|
||||
for temporality in [
|
||||
AggregationTemporality::Delta,
|
||||
AggregationTemporality::Cumulative,
|
||||
] {
|
||||
let overflow = HistogramDataPoint {
|
||||
count: u64::MAX,
|
||||
bucket_counts: vec![u64::MAX, 1],
|
||||
explicit_bounds: vec![1.0],
|
||||
..Default::default()
|
||||
};
|
||||
let count_only = HistogramDataPoint {
|
||||
count: 2,
|
||||
sum: Some(3.0),
|
||||
..Default::default()
|
||||
};
|
||||
let metric = Metric {
|
||||
name: format!("hist_{temporality:?}"),
|
||||
data: Some(metric::Data::Histogram(Histogram {
|
||||
data_points: vec![overflow, count_only],
|
||||
aggregation_temporality: temporality as i32,
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let conversion =
|
||||
to_grpc_insert_requests(metrics_request(vec![metric]), &mut OtlpMetricCtx::default())
|
||||
.unwrap();
|
||||
assert_eq!(1, conversion.outcome.accepted_data_points);
|
||||
assert_eq!(1, conversion.outcome.rejected_data_points);
|
||||
assert!(
|
||||
conversion
|
||||
.outcome
|
||||
.error_message
|
||||
.unwrap()
|
||||
.contains("overflows u64")
|
||||
);
|
||||
let count = conversion
|
||||
.requests
|
||||
.inserts
|
||||
.iter()
|
||||
.find(|insert| insert.table_name.ends_with(COUNT_TABLE_SUFFIX))
|
||||
.unwrap()
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(1, count.rows.len());
|
||||
|
||||
let buckets = conversion
|
||||
.requests
|
||||
.inserts
|
||||
.iter()
|
||||
.find(|insert| insert.table_name.ends_with(BUCKET_TABLE_SUFFIX))
|
||||
.unwrap()
|
||||
.rows
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(1, buckets.rows.len());
|
||||
let le = column_index(buckets, HISTOGRAM_LE_COLUMN);
|
||||
let value = column_index(buckets, greptime_value());
|
||||
assert_eq!(
|
||||
Some(&ValueData::StringValue("inf".to_string())),
|
||||
buckets.rows[0].values[le].value_data.as_ref()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(&ValueData::F64Value(2.0)),
|
||||
buckets.rows[0].values[value].value_data.as_ref()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reserved_temporality_label_uses_final_persisted_key() {
|
||||
set_default_prefix(Some("custom")).unwrap();
|
||||
|
||||
let gauge = |attributes: Vec<KeyValue>| Metric {
|
||||
name: "gauge".to_string(),
|
||||
data: Some(metric::Data::Gauge(Gauge {
|
||||
data_points: vec![NumberDataPoint {
|
||||
attributes,
|
||||
..Default::default()
|
||||
}],
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let error = to_grpc_insert_requests(
|
||||
metrics_request(vec![gauge(vec![keyvalue(
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL,
|
||||
"user",
|
||||
)])]),
|
||||
&mut OtlpMetricCtx::default(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, error::Error::InvalidOtlpMetricInput { .. }));
|
||||
|
||||
let mut request = metrics_request(vec![gauge(vec![])]);
|
||||
request.resource_metrics[0].scope_metrics[0].scope = Some(InstrumentationScope {
|
||||
attributes: vec![keyvalue(OTLP_AGGREGATION_TEMPORALITY_LABEL, "safe")],
|
||||
..Default::default()
|
||||
});
|
||||
let mut ctx = OtlpMetricCtx {
|
||||
promote_scope_attrs: true,
|
||||
..Default::default()
|
||||
};
|
||||
let conversion = to_grpc_insert_requests(request, &mut ctx).unwrap();
|
||||
assert!(
|
||||
column_names(&conversion.requests, "gauge").contains(&format!(
|
||||
"otel_scope_{}",
|
||||
OTLP_AGGREGATION_TEMPORALITY_LABEL
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_histogram_semantics_follow_only_emitted_rows() {
|
||||
let cumulative = histogram_metric("latency");
|
||||
let mut delta = histogram_metric("latency");
|
||||
let Some(metric::Data::Histogram(histogram)) = delta.data.as_mut() else {
|
||||
unreachable!()
|
||||
};
|
||||
histogram.aggregation_temporality = AggregationTemporality::Delta as i32;
|
||||
|
||||
let conversion = to_grpc_insert_requests(
|
||||
metrics_request(vec![cumulative.clone(), delta.clone()]),
|
||||
&mut OtlpMetricCtx::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let semantics = decode(&conversion.semantic_index);
|
||||
for table in ["latency_bucket", "latency_sum", "latency_count"] {
|
||||
assert_eq!(
|
||||
Some("mixed"),
|
||||
semantics[table]
|
||||
.get(SEMANTIC_METRIC_TEMPORALITY)
|
||||
.map(String::as_str)
|
||||
);
|
||||
}
|
||||
|
||||
let Some(metric::Data::Histogram(histogram)) = delta.data.as_mut() else {
|
||||
unreachable!()
|
||||
};
|
||||
histogram.data_points[0].explicit_bounds = vec![1.0];
|
||||
let conversion = to_grpc_insert_requests(
|
||||
metrics_request(vec![cumulative, delta]),
|
||||
&mut OtlpMetricCtx::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(1, conversion.outcome.rejected_data_points);
|
||||
let semantics = decode(&conversion.semantic_index);
|
||||
for table in ["latency_bucket", "latency_sum", "latency_count"] {
|
||||
assert_eq!(
|
||||
Some(METRIC_TEMPORALITY_CUMULATIVE),
|
||||
semantics[table]
|
||||
.get(SEMANTIC_METRIC_TEMPORALITY)
|
||||
.map(String::as_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,8 @@ pub const SEMANTIC_METRIC_TYPE: &str = "greptime.semantic.metric.type";
|
||||
/// UCUM unit, e.g. `s`, `By`, `{request}`. Discarded by the row encoders, so it
|
||||
/// is unrecoverable once ingested.
|
||||
pub const SEMANTIC_METRIC_UNIT: &str = "greptime.semantic.metric.unit";
|
||||
/// `cumulative` / `delta` (OTel only). Invisible in the metric name, so it is
|
||||
/// unrecoverable from the table alone.
|
||||
/// Catalog-level `cumulative` / `delta` / `mixed` description for OTLP metrics.
|
||||
/// Per-series query behavior is determined from stored row identity instead.
|
||||
pub const SEMANTIC_METRIC_TEMPORALITY: &str = "greptime.semantic.metric.temporality";
|
||||
/// [`METADATA_QUALITY_DECLARED`] when the protocol stated the type, or
|
||||
/// [`METADATA_QUALITY_INFERRED`] when guessed from a name suffix.
|
||||
@@ -135,6 +135,9 @@ pub const SOURCE_ELASTICSEARCH: &str = "elasticsearch";
|
||||
pub const METADATA_QUALITY_DECLARED: &str = "declared";
|
||||
pub const METADATA_QUALITY_INFERRED: &str = "inferred";
|
||||
|
||||
pub const METRIC_TEMPORALITY_CUMULATIVE: &str = "cumulative";
|
||||
pub const METRIC_TEMPORALITY_DELTA: &str = "delta";
|
||||
|
||||
/// Sentinel for a key that cannot be determined at stamp time.
|
||||
pub const SEMANTIC_VALUE_UNKNOWN: &str = "unknown";
|
||||
/// Sentinel for a single-valued key that saw conflicting sources.
|
||||
@@ -277,7 +280,10 @@ pub fn validate_semantic_option(key: &str, value: &str) -> bool {
|
||||
| "unknown"
|
||||
),
|
||||
SEMANTIC_METRIC_TEMPORALITY => {
|
||||
matches!(value, "cumulative" | "delta" | "mixed" | "unknown")
|
||||
matches!(
|
||||
value,
|
||||
METRIC_TEMPORALITY_CUMULATIVE | METRIC_TEMPORALITY_DELTA | "mixed" | "unknown"
|
||||
)
|
||||
}
|
||||
SEMANTIC_METRIC_METADATA_QUALITY => matches!(value, "declared" | "inferred" | "unknown"),
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ mod test {
|
||||
};
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::number_data_point::Value;
|
||||
use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
|
||||
Gauge, Histogram, HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics,
|
||||
ScopeMetrics, metric,
|
||||
AggregationTemporality, DataPointFlags, Gauge, Histogram, HistogramDataPoint, Metric,
|
||||
NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, metric,
|
||||
};
|
||||
use otel_arrow_rust::proto::opentelemetry::resource::v1::Resource;
|
||||
use servers::query_handler::OpenTelemetryProtocolHandler;
|
||||
@@ -54,6 +54,51 @@ mod test {
|
||||
test_otlp(&instance.frontend()).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
pub async fn test_otlp_fixed_schema_rejects_missing_temporality_tag() {
|
||||
let standalone = GreptimeDbStandaloneBuilder::new("test_otlp_fixed_schema")
|
||||
.with_auto_create_table(false)
|
||||
.build()
|
||||
.await;
|
||||
let instance = standalone.fe_instance();
|
||||
let ctx = Arc::new(QueryContext::with(DEFAULT_CATALOG_NAME, "public"));
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"CREATE TABLE fixed_delta_total (\
|
||||
\"stream\" STRING, greptime_timestamp TIMESTAMP(3) NOT NULL, \
|
||||
greptime_value DOUBLE, TIME INDEX (greptime_timestamp), \
|
||||
PRIMARY KEY (\"stream\")) ENGINE=mito",
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let result = output.remove(0);
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
|
||||
let error = instance
|
||||
.metrics(
|
||||
build_sum_request("fixed.delta", AggregationTemporality::Delta, &[(60, 10)]),
|
||||
ctx.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{error:?}").contains("otlp_aggregation_temporality"));
|
||||
|
||||
let mut output = instance
|
||||
.do_query("SELECT COUNT(*) FROM fixed_delta_total", ctx)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(
|
||||
RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap()
|
||||
.contains("| 0 |")
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_otlp(instance: &Arc<Instance>) {
|
||||
let req = build_request();
|
||||
let db = "otlp";
|
||||
@@ -74,6 +119,272 @@ mod test {
|
||||
let resp = instance.metrics(req, ctx.clone()).await;
|
||||
assert!(resp.is_ok());
|
||||
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"CREATE TABLE raw_delta_mito_total (\
|
||||
\"stream\" STRING, greptime_timestamp TIMESTAMP(3) NOT NULL, \
|
||||
greptime_value DOUBLE, TIME INDEX (greptime_timestamp), \
|
||||
PRIMARY KEY (\"stream\")) ENGINE=mito",
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let result = output.remove(0);
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
|
||||
for (metric, table) in [
|
||||
("raw.delta", "raw_delta_total"),
|
||||
("raw.delta.mito", "raw_delta_mito_total"),
|
||||
] {
|
||||
for request in [
|
||||
build_sum_request(metric, AggregationTemporality::Cumulative, &[(60, 10)]),
|
||||
build_sum_request(
|
||||
metric,
|
||||
AggregationTemporality::Delta,
|
||||
&[(60, 10), (120, 20), (180, 15)],
|
||||
),
|
||||
build_sum_request(metric, AggregationTemporality::Cumulative, &[(180, 30)]),
|
||||
] {
|
||||
let result = instance.metrics(request, ctx.clone()).await;
|
||||
assert!(result.is_ok(), "{metric}: {result:?}");
|
||||
}
|
||||
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
&format!(
|
||||
"SELECT COALESCE(otlp_aggregation_temporality, '') AS temporality, \
|
||||
COUNT(*) AS samples, SUM(greptime_value) AS total \
|
||||
FROM {table} GROUP BY otlp_aggregation_temporality ORDER BY temporality"
|
||||
),
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(
|
||||
RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap(),
|
||||
"\
|
||||
+-------------+---------+-------+
|
||||
| temporality | samples | total |
|
||||
+-------------+---------+-------+
|
||||
| | 2 | 40.0 |
|
||||
| delta | 3 | 45.0 |
|
||||
+-------------+---------+-------+"
|
||||
);
|
||||
|
||||
for (function, expected) in [("increase", "45.0"), ("rate", "0.25")] {
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
&format!("TQL EVAL (180, 180, '1m') {function}({table}[3m])"),
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
let rendered = RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap();
|
||||
assert!(rendered.contains("delta"), "{rendered}");
|
||||
assert!(rendered.contains(expected), "{rendered}");
|
||||
}
|
||||
|
||||
let mut stale = build_sum_request(metric, AggregationTemporality::Delta, &[(240, 99)]);
|
||||
let Some(metric::Data::Sum(sum)) = stale.resource_metrics[0].scope_metrics[0].metrics
|
||||
[0]
|
||||
.data
|
||||
.as_mut() else {
|
||||
unreachable!()
|
||||
};
|
||||
sum.data_points[0].flags = DataPointFlags::NoRecordedValueMask as u32;
|
||||
assert!(instance.metrics(stale, ctx.clone()).await.is_ok());
|
||||
|
||||
for (matcher, expected_rows) in [
|
||||
("otlp_aggregation_temporality=\"delta\"", 0),
|
||||
("otlp_aggregation_temporality!=\"delta\"", 1),
|
||||
] {
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
&format!("TQL EVAL (240, 240, '1m') {table}{{{matcher}}}"),
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
let batches = RecordBatches::try_collect(stream).await.unwrap();
|
||||
assert_eq!(
|
||||
expected_rows,
|
||||
batches.iter().map(|batch| batch.num_rows()).sum::<usize>()
|
||||
);
|
||||
if expected_rows == 1 {
|
||||
assert!(batches.pretty_print().unwrap().contains("30.0"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let malformed = ExportMetricsServiceRequest {
|
||||
resource_metrics: vec![ResourceMetrics {
|
||||
scope_metrics: vec![ScopeMetrics {
|
||||
metrics: vec![Metric {
|
||||
name: "rejected.delta.histogram".to_string(),
|
||||
data: Some(metric::Data::Histogram(Histogram {
|
||||
data_points: vec![HistogramDataPoint {
|
||||
count: 1,
|
||||
bucket_counts: vec![1],
|
||||
explicit_bounds: vec![1.0, 2.0],
|
||||
..Default::default()
|
||||
}],
|
||||
aggregation_temporality: AggregationTemporality::Delta as i32,
|
||||
})),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
};
|
||||
let outcome = instance.metrics(malformed, ctx.clone()).await.unwrap();
|
||||
assert_eq!(0, outcome.accepted_data_points);
|
||||
assert_eq!(1, outcome.rejected_data_points);
|
||||
assert!(
|
||||
outcome
|
||||
.error_message
|
||||
.as_deref()
|
||||
.unwrap()
|
||||
.contains("bucket_counts length")
|
||||
);
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name IN \
|
||||
('rejected_delta_histogram_bucket', 'rejected_delta_histogram_sum', \
|
||||
'rejected_delta_histogram_count')",
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(
|
||||
RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap()
|
||||
.contains("| 0 |")
|
||||
);
|
||||
|
||||
let point = |stream: &str, seconds: u64, bounds: Vec<f64>, sum| HistogramDataPoint {
|
||||
attributes: vec![keyvalue("stream", stream)],
|
||||
time_unix_nano: seconds * 1_000_000_000,
|
||||
count: u64::try_from(bounds.len() + 1).unwrap(),
|
||||
sum,
|
||||
bucket_counts: vec![1; bounds.len() + 1],
|
||||
explicit_bounds: bounds,
|
||||
..Default::default()
|
||||
};
|
||||
let tombstone = |stream: &str, seconds: u64, bounds: Vec<f64>, sum| HistogramDataPoint {
|
||||
flags: DataPointFlags::NoRecordedValueMask as u32,
|
||||
..point(stream, seconds, bounds, sum)
|
||||
};
|
||||
for points in [
|
||||
vec![
|
||||
point("same", 300, vec![1.0, 2.0], Some(4.0)),
|
||||
tombstone("same", 360, vec![1.0, 2.0], None),
|
||||
],
|
||||
vec![
|
||||
point("changed", 420, vec![3.0, 5.0], Some(8.0)),
|
||||
tombstone("changed", 480, vec![1.0, 2.0], Some(99.0)),
|
||||
],
|
||||
vec![
|
||||
point("boundless", 420, vec![3.0, 5.0], Some(8.0)),
|
||||
tombstone("boundless", 480, vec![], None),
|
||||
],
|
||||
vec![tombstone("new", 480, vec![], None)],
|
||||
] {
|
||||
let outcome = instance
|
||||
.metrics(
|
||||
build_histogram_request("raw.delta.histogram", points),
|
||||
ctx.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(0, outcome.rejected_data_points);
|
||||
}
|
||||
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"TQL EVAL (300, 300, '1m') \
|
||||
histogram_quantile(0.5, rate(raw_delta_histogram_bucket[2m]))",
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
let rendered = RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap();
|
||||
assert!(rendered.contains("1.5"), "{rendered}");
|
||||
|
||||
for (query, expected_rows) in [
|
||||
("raw_delta_histogram_bucket{stream=\"same\",le=~\"1|2\"}", 0),
|
||||
("raw_delta_histogram_sum{stream=\"same\"}", 1),
|
||||
(
|
||||
"raw_delta_histogram_bucket{stream=\"changed\",le=~\"3|5\"}",
|
||||
2,
|
||||
),
|
||||
("raw_delta_histogram_sum{stream=\"changed\"}", 0),
|
||||
(
|
||||
"raw_delta_histogram_bucket{stream=\"boundless\",le=~\"3|5\"}",
|
||||
2,
|
||||
),
|
||||
] {
|
||||
let mut output = instance
|
||||
.do_query(&format!("TQL EVAL (480, 480, '1m') {query}"), ctx.clone())
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
let batches = RecordBatches::try_collect(stream).await.unwrap();
|
||||
assert_eq!(
|
||||
expected_rows,
|
||||
batches.iter().map(|batch| batch.num_rows()).sum::<usize>(),
|
||||
"{query}: {}",
|
||||
batches.pretty_print().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"SELECT \
|
||||
(SELECT COUNT(*) FROM raw_delta_histogram_bucket WHERE \"stream\" = 'new') AS buckets, \
|
||||
(SELECT COUNT(*) FROM raw_delta_histogram_count WHERE \"stream\" = 'new') AS counts, \
|
||||
(SELECT COUNT(*) FROM raw_delta_histogram_sum WHERE \"stream\" = 'new') AS sums",
|
||||
ctx.clone(),
|
||||
)
|
||||
.await;
|
||||
let OutputData::Stream(stream) = output.remove(0).unwrap().data else {
|
||||
unreachable!()
|
||||
};
|
||||
let rendered = RecordBatches::try_collect(stream)
|
||||
.await
|
||||
.unwrap()
|
||||
.pretty_print()
|
||||
.unwrap();
|
||||
assert!(
|
||||
rendered.contains("| 1 | 1 | 0 |"),
|
||||
"{rendered}"
|
||||
);
|
||||
|
||||
let mut output = instance
|
||||
.do_query(
|
||||
"SELECT * FROM my_test_metric_my_ignored_unit ORDER BY greptime_timestamp",
|
||||
@@ -232,6 +543,61 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sum_request(
|
||||
name: &str,
|
||||
temporality: AggregationTemporality,
|
||||
points: &[(u64, i64)],
|
||||
) -> ExportMetricsServiceRequest {
|
||||
let data_points = points
|
||||
.iter()
|
||||
.map(|(seconds, value)| NumberDataPoint {
|
||||
attributes: vec![keyvalue("stream", "same")],
|
||||
time_unix_nano: *seconds * 1_000_000_000,
|
||||
value: Some(Value::AsInt(*value)),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
ExportMetricsServiceRequest {
|
||||
resource_metrics: vec![ResourceMetrics {
|
||||
scope_metrics: vec![ScopeMetrics {
|
||||
metrics: vec![Metric {
|
||||
name: name.to_string(),
|
||||
data: Some(metric::Data::Sum(Sum {
|
||||
data_points,
|
||||
aggregation_temporality: temporality as i32,
|
||||
is_monotonic: true,
|
||||
})),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_histogram_request(
|
||||
name: &str,
|
||||
points: Vec<HistogramDataPoint>,
|
||||
) -> ExportMetricsServiceRequest {
|
||||
ExportMetricsServiceRequest {
|
||||
resource_metrics: vec![ResourceMetrics {
|
||||
scope_metrics: vec![ScopeMetrics {
|
||||
metrics: vec![Metric {
|
||||
name: name.to_string(),
|
||||
data: Some(metric::Data::Histogram(Histogram {
|
||||
data_points: points,
|
||||
aggregation_temporality: AggregationTemporality::Delta as i32,
|
||||
})),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn keyvalue(key: &str, value: &str) -> KeyValue {
|
||||
KeyValue {
|
||||
key: key.into(),
|
||||
|
||||
@@ -38,8 +38,9 @@ use common_runtime::Runtime;
|
||||
use common_runtime::runtime::{BuilderBuild, RuntimeTrait};
|
||||
use common_test_util::find_workspace_path;
|
||||
use datatypes::arrow::array::{
|
||||
Array, ArrayRef, Float64Array, Int32Array, ListBuilder, StringArray, StructArray,
|
||||
TimestampNanosecondArray, UInt8Array, UInt16Array, UInt32Array, UInt64Array, UInt64Builder,
|
||||
Array, ArrayRef, Float64Array, Float64Builder, Int32Array, ListBuilder, StringArray,
|
||||
StructArray, TimestampNanosecondArray, UInt8Array, UInt16Array, UInt32Array, UInt64Array,
|
||||
UInt64Builder,
|
||||
};
|
||||
use datatypes::arrow::datatypes::{DataType, Field};
|
||||
use datatypes::arrow::ipc::writer::StreamWriter;
|
||||
@@ -104,6 +105,7 @@ macro_rules! grpc_tests {
|
||||
test_private_system_tables_auto_create_table_with_global_disabled,
|
||||
test_private_system_tables_bypass_auto_create_hint,
|
||||
test_otel_arrow_auth,
|
||||
test_otel_arrow_delta_histogram,
|
||||
test_otel_arrow_exponential_histogram,
|
||||
test_insert_and_select,
|
||||
test_dbname,
|
||||
@@ -563,6 +565,268 @@ fn exponential_histogram_arrow_batch(batch_id: i64, scales: &[i32]) -> BatchArro
|
||||
}
|
||||
}
|
||||
|
||||
fn delta_histogram_arrow_batch(
|
||||
batch_id: i64,
|
||||
points: &[(u64, &[u64], &[f64])],
|
||||
) -> BatchArrowRecords {
|
||||
let resource = StructArray::from(vec![(
|
||||
Arc::new(Field::new(arrow_consts::ID, DataType::UInt16, true)),
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
)]);
|
||||
let scope = StructArray::from(vec![(
|
||||
Arc::new(Field::new(arrow_consts::ID, DataType::UInt16, true)),
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
)]);
|
||||
let metrics = ArrowRecordBatch::try_from_iter(vec![
|
||||
(
|
||||
arrow_consts::ID,
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
),
|
||||
(arrow_consts::RESOURCE, Arc::new(resource) as ArrayRef),
|
||||
(arrow_consts::SCOPE, Arc::new(scope) as ArrayRef),
|
||||
(
|
||||
arrow_consts::METRIC_TYPE,
|
||||
Arc::new(UInt8Array::from(vec![ArrowMetricType::Histogram as u8])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::NAME,
|
||||
Arc::new(StringArray::from(vec!["otel.arrow.delta.histogram"])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::AGGREGATION_TEMPORALITY,
|
||||
Arc::new(Int32Array::from(vec![AggregationTemporality::Delta as i32])) as ArrayRef,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let mut bucket_counts = ListBuilder::new(UInt64Builder::new());
|
||||
let mut explicit_bounds = ListBuilder::new(Float64Builder::new());
|
||||
for (_, counts, bounds) in points {
|
||||
bucket_counts.values().append_slice(counts);
|
||||
bucket_counts.append(true);
|
||||
explicit_bounds.values().append_slice(bounds);
|
||||
explicit_bounds.append(true);
|
||||
}
|
||||
let bucket_counts = bucket_counts.finish();
|
||||
let explicit_bounds = explicit_bounds.finish();
|
||||
let data_points = ArrowRecordBatch::try_from_iter(vec![
|
||||
(
|
||||
arrow_consts::ID,
|
||||
Arc::new(UInt32Array::from_iter_values(
|
||||
(0..points.len()).map(|id| u32::try_from(id).unwrap()),
|
||||
)) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::PARENT_ID,
|
||||
Arc::new(UInt16Array::from(vec![0_u16; points.len()])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::START_TIME_UNIX_NANO,
|
||||
Arc::new(TimestampNanosecondArray::from(vec![
|
||||
1_000_000_000;
|
||||
points.len()
|
||||
])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::TIME_UNIX_NANO,
|
||||
Arc::new(TimestampNanosecondArray::from_iter_values(
|
||||
(1..=points.len()).map(|second| i64::try_from(second).unwrap() * 1_000_000_000),
|
||||
)) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::HISTOGRAM_COUNT,
|
||||
Arc::new(UInt64Array::from(
|
||||
points
|
||||
.iter()
|
||||
.map(|(count, _, _)| *count)
|
||||
.collect::<Vec<_>>(),
|
||||
)) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::HISTOGRAM_SUM,
|
||||
Arc::new(Float64Array::from(vec![1.0; points.len()])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::HISTOGRAM_BUCKET_COUNTS,
|
||||
Arc::new(bucket_counts) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::HISTOGRAM_EXPLICIT_BOUNDS,
|
||||
Arc::new(explicit_bounds) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::FLAGS,
|
||||
Arc::new(UInt32Array::from(vec![0_u32; points.len()])) as ArrayRef,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
BatchArrowRecords {
|
||||
batch_id,
|
||||
arrow_payloads: vec![
|
||||
ArrowPayload {
|
||||
schema_id: format!("metrics-{batch_id}"),
|
||||
r#type: ArrowPayloadType::UnivariateMetrics as i32,
|
||||
record: serialize_arrow_record_batch(&metrics),
|
||||
},
|
||||
ArrowPayload {
|
||||
schema_id: format!("histogram-{batch_id}"),
|
||||
r#type: ArrowPayloadType::HistogramDataPoints as i32,
|
||||
record: serialize_arrow_record_batch(&data_points),
|
||||
},
|
||||
],
|
||||
headers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn gauge_arrow_batch(batch_id: i64, reserved_attr: bool) -> BatchArrowRecords {
|
||||
let resource = StructArray::from(vec![(
|
||||
Arc::new(Field::new(arrow_consts::ID, DataType::UInt16, true)),
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
)]);
|
||||
let scope = StructArray::from(vec![(
|
||||
Arc::new(Field::new(arrow_consts::ID, DataType::UInt16, true)),
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
)]);
|
||||
let metrics = ArrowRecordBatch::try_from_iter(vec![
|
||||
(
|
||||
arrow_consts::ID,
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
),
|
||||
(arrow_consts::RESOURCE, Arc::new(resource) as ArrayRef),
|
||||
(arrow_consts::SCOPE, Arc::new(scope) as ArrayRef),
|
||||
(
|
||||
arrow_consts::METRIC_TYPE,
|
||||
Arc::new(UInt8Array::from(vec![ArrowMetricType::Gauge as u8])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::NAME,
|
||||
Arc::new(StringArray::from(vec!["otel.arrow.gauge"])) as ArrayRef,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let data_points = ArrowRecordBatch::try_from_iter(vec![
|
||||
(
|
||||
arrow_consts::ID,
|
||||
Arc::new(UInt32Array::from(vec![0_u32])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::PARENT_ID,
|
||||
Arc::new(UInt16Array::from(vec![0_u16])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::START_TIME_UNIX_NANO,
|
||||
Arc::new(TimestampNanosecondArray::from(vec![1_000_000_000])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::TIME_UNIX_NANO,
|
||||
Arc::new(TimestampNanosecondArray::from(vec![2_000_000_000])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::DOUBLE_VALUE,
|
||||
Arc::new(Float64Array::from(vec![1.0])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::FLAGS,
|
||||
Arc::new(UInt32Array::from(vec![0_u32])) as ArrayRef,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
let mut arrow_payloads = vec![
|
||||
ArrowPayload {
|
||||
schema_id: format!("metrics-{batch_id}"),
|
||||
r#type: ArrowPayloadType::UnivariateMetrics as i32,
|
||||
record: serialize_arrow_record_batch(&metrics),
|
||||
},
|
||||
ArrowPayload {
|
||||
schema_id: format!("number-{batch_id}"),
|
||||
r#type: ArrowPayloadType::NumberDataPoints as i32,
|
||||
record: serialize_arrow_record_batch(&data_points),
|
||||
},
|
||||
];
|
||||
if reserved_attr {
|
||||
let attributes = ArrowRecordBatch::try_from_iter(vec![
|
||||
(
|
||||
arrow_consts::PARENT_ID,
|
||||
Arc::new(UInt32Array::from(vec![0_u32])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::ATTRIBUTE_KEY,
|
||||
Arc::new(StringArray::from(vec!["otlp_aggregation_temporality"])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::ATTRIBUTE_TYPE,
|
||||
Arc::new(UInt8Array::from(vec![1_u8])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
arrow_consts::ATTRIBUTE_STR,
|
||||
Arc::new(StringArray::from(vec!["user"])) as ArrayRef,
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
arrow_payloads.push(ArrowPayload {
|
||||
schema_id: format!("number-attrs-{batch_id}"),
|
||||
r#type: ArrowPayloadType::NumberDpAttrs as i32,
|
||||
record: serialize_arrow_record_batch(&attributes),
|
||||
});
|
||||
}
|
||||
BatchArrowRecords {
|
||||
batch_id,
|
||||
arrow_payloads,
|
||||
headers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_otel_arrow_delta_histogram(store_type: StorageType) {
|
||||
let (_instance, server) =
|
||||
setup_grpc_server(store_type, "test_otel_arrow_delta_histogram").await;
|
||||
let addr = server.bind_addr().unwrap().to_string();
|
||||
let mut client = ArrowMetricsServiceClient::connect(format!("http://{addr}"))
|
||||
.await
|
||||
.unwrap();
|
||||
let valid = (3, &[1, 2][..], &[1.0][..]);
|
||||
let malformed = (1, &[1][..], &[1.0][..]);
|
||||
let request = Request::new(futures::stream::iter([
|
||||
delta_histogram_arrow_batch(10, &[valid, malformed]),
|
||||
delta_histogram_arrow_batch(11, &[malformed]),
|
||||
delta_histogram_arrow_batch(12, &[valid]),
|
||||
gauge_arrow_batch(13, true),
|
||||
gauge_arrow_batch(14, false),
|
||||
]));
|
||||
let mut response = client.arrow_metrics(request).await.unwrap().into_inner();
|
||||
|
||||
let mixed = response.message().await.unwrap().unwrap();
|
||||
assert_eq!(10, mixed.batch_id);
|
||||
assert_eq!(ArrowStatusCode::Ok as i32, mixed.status_code);
|
||||
assert!(mixed.status_message.contains("bucket_counts length"));
|
||||
|
||||
let rejected = response.message().await.unwrap().unwrap();
|
||||
assert_eq!(11, rejected.batch_id);
|
||||
assert_eq!(
|
||||
ArrowStatusCode::InvalidArgument as i32,
|
||||
rejected.status_code
|
||||
);
|
||||
assert!(rejected.status_message.contains("bucket_counts length"));
|
||||
|
||||
let later_valid = response.message().await.unwrap().unwrap();
|
||||
assert_eq!(12, later_valid.batch_id);
|
||||
assert_eq!(ArrowStatusCode::Ok as i32, later_valid.status_code);
|
||||
assert!(later_valid.status_message.is_empty());
|
||||
|
||||
let collision = response.message().await.unwrap().unwrap();
|
||||
assert_eq!(13, collision.batch_id);
|
||||
assert_eq!(
|
||||
ArrowStatusCode::InvalidArgument as i32,
|
||||
collision.status_code
|
||||
);
|
||||
assert!(collision.status_message.contains("reserved label"));
|
||||
|
||||
let after_collision = response.message().await.unwrap().unwrap();
|
||||
assert_eq!(14, after_collision.batch_id);
|
||||
assert_eq!(ArrowStatusCode::Ok as i32, after_collision.status_code);
|
||||
assert!(after_collision.status_message.is_empty());
|
||||
let _ = server.shutdown().await;
|
||||
}
|
||||
|
||||
pub async fn test_otel_arrow_exponential_histogram(store_type: StorageType) {
|
||||
let (_instance, server) =
|
||||
setup_grpc_server(store_type, "test_otel_arrow_exponential_histogram").await;
|
||||
|
||||
+114
-12
@@ -6596,13 +6596,14 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// "otel_scope_name" STRING NULL,
|
||||
// "otel_scope_schema_url" STRING NULL,
|
||||
// "otel_scope_version" STRING NULL,
|
||||
// "otlp_aggregation_temporality" STRING NULL,
|
||||
// "service_name" STRING NULL,
|
||||
// "service_version" STRING NULL,
|
||||
// "session_id" STRING NULL,
|
||||
// "terminal_type" STRING NULL,
|
||||
// "user_id" STRING NULL,
|
||||
// TIME INDEX ("greptime_timestamp"),
|
||||
// PRIMARY KEY ("host_arch", "job", "model", "os_version", "otel_scope_name", "otel_scope_schema_url", "otel_scope_version", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// PRIMARY KEY ("host_arch", "job", "model", "os_version", "otel_scope_name", "otel_scope_schema_url", "otel_scope_version", "otlp_aggregation_temporality", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// )
|
||||
// ENGINE=metric
|
||||
// WITH(
|
||||
@@ -6610,7 +6611,7 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// on_physical_table = 'greptime_physical_table',
|
||||
// otlp_metric_compat = 'prom'
|
||||
// )
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"host_arch\\\" STRING NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"os_version\\\" STRING NULL,\\n \\\"otel_scope_name\\\" STRING NULL,\\n \\\"otel_scope_schema_url\\\" STRING NULL,\\n \\\"otel_scope_version\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"host_arch\\\", \\\"job\\\", \\\"model\\\", \\\"os_version\\\", \\\"otel_scope_name\\\", \\\"otel_scope_schema_url\\\", \\\"otel_scope_version\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"host_arch\\\" STRING NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"os_version\\\" STRING NULL,\\n \\\"otel_scope_name\\\" STRING NULL,\\n \\\"otel_scope_schema_url\\\" STRING NULL,\\n \\\"otel_scope_version\\\" STRING NULL,\\n \\\"otlp_aggregation_temporality\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"host_arch\\\", \\\"job\\\", \\\"model\\\", \\\"os_version\\\", \\\"otel_scope_name\\\", \\\"otel_scope_schema_url\\\", \\\"otel_scope_version\\\", \\\"otlp_aggregation_temporality\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_all_show_create_table",
|
||||
&client,
|
||||
@@ -6620,11 +6621,11 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
.await;
|
||||
|
||||
// select metrics data
|
||||
let expected = "[[1753780559836,2.244618,\"arm64\",\"claude-code\",\"claude-sonnet-4-20250514\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,0.0052544,\"arm64\",\"claude-code\",\"claude-3-5-haiku-20241022\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
let expected = "[[1753780559836,2.244618,\"arm64\",\"claude-code\",\"claude-sonnet-4-20250514\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,0.0052544,\"arm64\",\"claude-code\",\"claude-3-5-haiku-20241022\",\"25.0.0\",\"com.anthropic.claude_code\",\"\",\"1.0.62\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_all_select",
|
||||
&client,
|
||||
"select * from `claude_code_cost_usage_USD_total`;",
|
||||
"select * from `claude_code_cost_usage_USD_total` order by model desc;",
|
||||
expected,
|
||||
)
|
||||
.await;
|
||||
@@ -6701,13 +6702,14 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// "model" STRING NULL,
|
||||
// "os_type" STRING NULL,
|
||||
// "os_version" STRING NULL,
|
||||
// "otlp_aggregation_temporality" STRING NULL,
|
||||
// "service_name" STRING NULL,
|
||||
// "service_version" STRING NULL,
|
||||
// "session_id" STRING NULL,
|
||||
// "terminal_type" STRING NULL,
|
||||
// "user_id" STRING NULL,
|
||||
// TIME INDEX ("greptime_timestamp"),
|
||||
// PRIMARY KEY ("job", "model", "os_type", "os_version", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// PRIMARY KEY ("job", "model", "os_type", "os_version", "otlp_aggregation_temporality", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// )
|
||||
// ENGINE=metric
|
||||
// WITH(
|
||||
@@ -6715,7 +6717,7 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// on_physical_table = 'greptime_physical_table',
|
||||
// otlp_metric_compat = 'prom'
|
||||
// )
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"os_type\\\" STRING NULL,\\n \\\"os_version\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"os_type\\\", \\\"os_version\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"os_type\\\" STRING NULL,\\n \\\"os_version\\\" STRING NULL,\\n \\\"otlp_aggregation_temporality\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"os_type\\\", \\\"os_version\\\", \\\"otlp_aggregation_temporality\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_show_create_table",
|
||||
&client,
|
||||
@@ -6725,11 +6727,11 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
.await;
|
||||
|
||||
// select metrics data
|
||||
let expected = "[[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"darwin\",\"25.0.0\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"darwin\",\"25.0.0\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
let expected = "[[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"darwin\",\"25.0.0\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"darwin\",\"25.0.0\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_select",
|
||||
&client,
|
||||
"select * from `claude_code_cost_usage_USD_total`;",
|
||||
"select * from `claude_code_cost_usage_USD_total` order by model desc;",
|
||||
expected,
|
||||
)
|
||||
.await;
|
||||
@@ -6765,13 +6767,14 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// "greptime_value" DOUBLE NULL,
|
||||
// "job" STRING NULL,
|
||||
// "model" STRING NULL,
|
||||
// "otlp_aggregation_temporality" STRING NULL,
|
||||
// "service_name" STRING NULL,
|
||||
// "service_version" STRING NULL,
|
||||
// "session_id" STRING NULL,
|
||||
// "terminal_type" STRING NULL,
|
||||
// "user_id" STRING NULL,
|
||||
// TIME INDEX ("greptime_timestamp"),
|
||||
// PRIMARY KEY ("job", "model", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// PRIMARY KEY ("job", "model", "otlp_aggregation_temporality", "service_name", "service_version", "session_id", "terminal_type", "user_id")
|
||||
// )
|
||||
// ENGINE=metric
|
||||
// WITH(
|
||||
@@ -6779,7 +6782,7 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
// on_physical_table = 'greptime_physical_table',
|
||||
// otlp_metric_compat = 'prom'
|
||||
// )
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
let expected = "[[\"claude_code_cost_usage_USD_total\",\"CREATE TABLE IF NOT EXISTS \\\"claude_code_cost_usage_USD_total\\\" (\\n \\\"greptime_timestamp\\\" TIMESTAMP(3) NOT NULL,\\n \\\"greptime_value\\\" DOUBLE NULL,\\n \\\"job\\\" STRING NULL,\\n \\\"model\\\" STRING NULL,\\n \\\"otlp_aggregation_temporality\\\" STRING NULL,\\n \\\"service_name\\\" STRING NULL,\\n \\\"service_version\\\" STRING NULL,\\n \\\"session_id\\\" STRING NULL,\\n \\\"terminal_type\\\" STRING NULL,\\n \\\"user_id\\\" STRING NULL,\\n TIME INDEX (\\\"greptime_timestamp\\\"),\\n PRIMARY KEY (\\\"job\\\", \\\"model\\\", \\\"otlp_aggregation_temporality\\\", \\\"service_name\\\", \\\"service_version\\\", \\\"session_id\\\", \\\"terminal_type\\\", \\\"user_id\\\")\\n)\\n\\nENGINE=metric\\nWITH(\\n 'comment' = 'Created on insertion',\\n 'greptime.semantic.metric.metadata_quality' = 'declared',\\n 'greptime.semantic.metric.original_name' = 'claude_code.cost.usage',\\n 'greptime.semantic.metric.temporality' = 'delta',\\n 'greptime.semantic.metric.type' = 'counter',\\n 'greptime.semantic.metric.unit' = 'USD',\\n 'greptime.semantic.signal_type' = 'metric',\\n 'greptime.semantic.source' = 'opentelemetry',\\n on_physical_table = 'greptime_physical_table',\\n otlp_metric_compat = 'prom'\\n)\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_show_create_table_none",
|
||||
&client,
|
||||
@@ -6789,11 +6792,11 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
.await;
|
||||
|
||||
// select metrics data
|
||||
let expected = "[[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
let expected = "[[1753780559836,2.244618,\"claude-code\",\"claude-sonnet-4-20250514\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"],[1753780559836,0.0052544,\"claude-code\",\"claude-3-5-haiku-20241022\",\"delta\",\"claude-code\",\"1.0.62\",\"736525A3-F5D4-496B-933E-827AF23A5B97\",\"ghostty\",\"6DA02FD9-B5C5-4E61-9355-9FE8EC9A0CF4\"]]";
|
||||
validate_data(
|
||||
"otlp_metrics_select_none",
|
||||
&client,
|
||||
"select * from `claude_code_cost_usage_USD_total`;",
|
||||
"select * from `claude_code_cost_usage_USD_total` order by model desc;",
|
||||
expected,
|
||||
)
|
||||
.await;
|
||||
@@ -6858,6 +6861,105 @@ pub async fn test_otlp_metrics_new(store_type: StorageType) {
|
||||
)
|
||||
.await;
|
||||
|
||||
let content = r#"
|
||||
{"resourceMetrics":[{"scopeMetrics":[{"metrics":[{"name":"http.stale.sum","sum":{"dataPoints":[{"timeUnixNano":"1753780559836000000","flags":1,"asDouble":99.0}],"aggregationTemporality":1,"isMonotonic":true}},{"name":"http.stale.histogram","histogram":{"dataPoints":[{"timeUnixNano":"1753780559835000000","count":"3","sum":4.0,"bucketCounts":["1","1","1"],"explicitBounds":[1.0,2.0]},{"timeUnixNano":"1753780559836000000","flags":1,"count":"99","sum":99.0,"bucketCounts":["99"],"explicitBounds":[1.0,2.0]}],"aggregationTemporality":1}}]}]}]}
|
||||
"#;
|
||||
let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
|
||||
let res = send_req(
|
||||
&client,
|
||||
vec![(
|
||||
HeaderName::from_static("content-type"),
|
||||
HeaderValue::from_static("application/x-protobuf"),
|
||||
)],
|
||||
"/v1/otlp/v1/metrics",
|
||||
req.encode_to_vec(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::OK, res.status());
|
||||
let body = ExportMetricsServiceResponse::decode(res.bytes().await).unwrap();
|
||||
assert!(body.partial_success.is_none());
|
||||
validate_data(
|
||||
"otlp_http_stale_sum_identity",
|
||||
&client,
|
||||
"select otlp_aggregation_temporality, count(*) from http_stale_sum_total \
|
||||
group by otlp_aggregation_temporality;",
|
||||
"[[\"delta\",1]]",
|
||||
)
|
||||
.await;
|
||||
for (table, expected) in [
|
||||
("http_stale_histogram_bucket", "[[\"delta\",6]]"),
|
||||
("http_stale_histogram_sum", "[[\"delta\",2]]"),
|
||||
("http_stale_histogram_count", "[[\"delta\",2]]"),
|
||||
] {
|
||||
validate_data(
|
||||
"otlp_http_stale_histogram_identity",
|
||||
&client,
|
||||
&format!(
|
||||
"select otlp_aggregation_temporality, count(*) from {table} \
|
||||
group by otlp_aggregation_temporality;"
|
||||
),
|
||||
expected,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let content = r#"
|
||||
{"resourceMetrics":[{"scopeMetrics":[{"metrics":[{"name":"reserved_label_gauge","gauge":{"dataPoints":[{"timeUnixNano":"1753780559836000000","asDouble":1.0,"attributes":[{"key":"otlp_aggregation_temporality","value":{"stringValue":"user"}}]}]}}]}]}]}
|
||||
"#;
|
||||
let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
|
||||
let res = send_req(
|
||||
&client,
|
||||
vec![(
|
||||
HeaderName::from_static("content-type"),
|
||||
HeaderValue::from_static("application/x-protobuf"),
|
||||
)],
|
||||
"/v1/otlp/v1/metrics",
|
||||
req.encode_to_vec(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::BAD_REQUEST, res.status());
|
||||
let status = GoogleRpcStatus::decode(res.bytes().await.as_ref()).unwrap();
|
||||
assert_eq!(tonic::Code::InvalidArgument as i32, status.code);
|
||||
assert!(status.message.contains("reserved label"));
|
||||
validate_data(
|
||||
"otlp_metrics_reserved_label_stores_nothing",
|
||||
&client,
|
||||
"select count(*) from information_schema.tables where table_name = 'reserved_label_gauge';",
|
||||
"[[0]]",
|
||||
)
|
||||
.await;
|
||||
|
||||
let content = r#"
|
||||
{"resourceMetrics":[{"scopeMetrics":[{"metrics":[{"name":"malformed_delta_histogram","histogram":{"dataPoints":[{"timeUnixNano":"1753780559836000000","count":"1","sum":1.0,"bucketCounts":["1"],"explicitBounds":[1.0,2.0]}],"aggregationTemporality":1}}]}]}]}
|
||||
"#;
|
||||
let req: ExportMetricsServiceRequest = serde_json::from_str(content).unwrap();
|
||||
let res = send_req(
|
||||
&client,
|
||||
vec![(
|
||||
HeaderName::from_static("content-type"),
|
||||
HeaderValue::from_static("application/x-protobuf"),
|
||||
)],
|
||||
"/v1/otlp/v1/metrics",
|
||||
req.encode_to_vec(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(StatusCode::BAD_REQUEST, res.status());
|
||||
let status = GoogleRpcStatus::decode(res.bytes().await.as_ref()).unwrap();
|
||||
assert_eq!(tonic::Code::InvalidArgument as i32, status.code);
|
||||
assert!(status.message.contains("bucket_counts length"));
|
||||
validate_data(
|
||||
"otlp_metrics_malformed_delta_histogram_stores_nothing",
|
||||
&client,
|
||||
"select count(*) from information_schema.tables where table_name in \
|
||||
('malformed_delta_histogram_bucket', 'malformed_delta_histogram_sum', \
|
||||
'malformed_delta_histogram_count');",
|
||||
"[[0]]",
|
||||
)
|
||||
.await;
|
||||
|
||||
guard.remove_all().await;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
CREATE TABLE delta_temporality (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE,
|
||||
series STRING,
|
||||
otlp_aggregation_temporality STRING,
|
||||
PRIMARY KEY (series, otlp_aggregation_temporality)
|
||||
);
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
INSERT INTO delta_temporality VALUES
|
||||
(60000, 10, 'delta', 'delta'),
|
||||
(120000, 20, 'delta', 'delta'),
|
||||
(180000, 15, 'delta', 'delta'),
|
||||
(180000, 7, 'single', 'delta'),
|
||||
(60000, 10, 'cumulative', NULL),
|
||||
(120000, 20, 'cumulative', NULL),
|
||||
(180000, 30, 'cumulative', NULL);
|
||||
|
||||
Affected Rows: 7
|
||||
|
||||
-- Raw deltas are summed; untagged rows retain cumulative reset-aware math.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') increase(delta_temporality[3m]);
|
||||
|
||||
+---------------------+---------------------------------------------------------+------------+------------------------------+
|
||||
| ts | prom_increase(ts_range,greptime_value,ts,Int64(180000)) | series | otlp_aggregation_temporality |
|
||||
+---------------------+---------------------------------------------------------+------------+------------------------------+
|
||||
| 1970-01-01T00:03:00 | 30.0 | cumulative | |
|
||||
| 1970-01-01T00:03:00 | 45.0 | delta | delta |
|
||||
| 1970-01-01T00:03:00 | 7.0 | single | delta |
|
||||
+---------------------+---------------------------------------------------------+------------+------------------------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') rate(delta_temporality[3m]);
|
||||
|
||||
+---------------------+-----------------------------------------------------+------------+------------------------------+
|
||||
| ts | prom_rate(ts_range,greptime_value,ts,Int64(180000)) | series | otlp_aggregation_temporality |
|
||||
+---------------------+-----------------------------------------------------+------------+------------------------------+
|
||||
| 1970-01-01T00:03:00 | 0.03888888888888889 | single | delta |
|
||||
| 1970-01-01T00:03:00 | 0.16666666666666666 | cumulative | |
|
||||
| 1970-01-01T00:03:00 | 0.25 | delta | delta |
|
||||
+---------------------+-----------------------------------------------------+------------+------------------------------+
|
||||
|
||||
-- The physical plan selects raw-delta math per series while retaining cumulative rate.
|
||||
-- SQLNESS REPLACE (metrics.*) REDACTED
|
||||
-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED
|
||||
-- SQLNESS REPLACE (-+) -
|
||||
-- SQLNESS REPLACE (\s\s+) _
|
||||
-- SQLNESS REPLACE (?m)^\|\s1_\|\s0_\|_(?:Projection|Filter)Exec:.*\n
|
||||
-- SQLNESS REPLACE (?m)^\|_\|_\|_FilterExec:.*\n
|
||||
-- SQLNESS REPLACE END\sas\s.*,\sseries@2\sas\sseries END as RATE_RESULT, series@2 as series
|
||||
-- SQLNESS REPLACE (peers.*) REDACTED
|
||||
-- SQLNESS REPLACE input_partitions=\d+ input_partitions=REDACTED
|
||||
-- SQLNESS REPLACE "partition_count":\{(.*?)\} "partition_count":REDACTED
|
||||
-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED
|
||||
TQL ANALYZE (180, 180, '1m') rate(delta_temporality[3m]);
|
||||
|
||||
+-+-+-+
|
||||
| stage | node | plan_|
|
||||
+-+-+-+
|
||||
| 0_| 0_|_CooperativeExec REDACTED
|
||||
|_|_|_MergeScanExec: REDACTED
|
||||
|_|_|_|
|
||||
|_|_|_ProjectionExec: expr=[ts@0 as ts, CASE WHEN otlp_aggregation_temporality@3 = delta THEN prom_sum_over_time(ts_range@4, greptime_value@1) / 180 ELSE prom_rate(ts_range@4, greptime_value@1, ts@0, 180000) END as RATE_RESULT, series@2 as series, otlp_aggregation_temporality@3 as otlp_aggregation_temporality] REDACTED
|
||||
|_|_|_PromRangeManipulateExec: req range=[180000..180000], interval=[60000], eval range=[180000], time index=[ts] REDACTED
|
||||
|_|_|_PromSeriesNormalizeExec: offset=[0], time index=[ts], filter NaN: [true] REDACTED
|
||||
|_|_|_PromSeriesDivideExec: tags=["series", "otlp_aggregation_temporality"] REDACTED
|
||||
|_|_|_CooperativeExec REDACTED
|
||||
|_|_|_SeriesScan: region=REDACTED, "partition_count":REDACTED, "distribution":"PerSeries", "mode":"legacy" REDACTED
|
||||
|_|_|_|
|
||||
|_|_| Total rows: 3_|
|
||||
+-+-+-+
|
||||
|
||||
-- The reserved temporality marker treats NULL as the absent cumulative state.
|
||||
TQL EVAL (180, 180, '1m') delta_temporality{otlp_aggregation_temporality=""};
|
||||
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
| ts | greptime_value | series | otlp_aggregation_temporality |
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
| 1970-01-01T00:03:00 | 30.0 | cumulative | |
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
|
||||
TQL EVAL (180, 180, '1m') delta_temporality{otlp_aggregation_temporality!="delta"};
|
||||
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
| ts | greptime_value | series | otlp_aggregation_temporality |
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
| 1970-01-01T00:03:00 | 30.0 | cumulative | |
|
||||
+---------------------+----------------+------------+------------------------------+
|
||||
|
||||
-- Generated vector plans retain timestamp broadcast across the temporality marker.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') vector(2) * delta_temporality;
|
||||
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| series | otlp_aggregation_temporality | ts | .greptime_value * delta_temporality.greptime_value |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| cumulative | | 1970-01-01T00:03:00 | 60.0 |
|
||||
| delta | delta | 1970-01-01T00:03:00 | 30.0 |
|
||||
| single | delta | 1970-01-01T00:03:00 | 14.0 |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') vector(2) * ignoring(otlp_aggregation_temporality) delta_temporality;
|
||||
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| series | otlp_aggregation_temporality | ts | .greptime_value * delta_temporality.greptime_value |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| cumulative | | 1970-01-01T00:03:00 | 60.0 |
|
||||
| delta | delta | 1970-01-01T00:03:00 | 30.0 |
|
||||
| single | delta | 1970-01-01T00:03:00 | 14.0 |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') delta_temporality * vector(2);
|
||||
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| series | otlp_aggregation_temporality | ts | delta_temporality.greptime_value * .greptime_value |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| cumulative | | 1970-01-01T00:03:00 | 60.0 |
|
||||
| delta | delta | 1970-01-01T00:03:00 | 30.0 |
|
||||
| single | delta | 1970-01-01T00:03:00 | 14.0 |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') delta_temporality * ignoring(otlp_aggregation_temporality) vector(2);
|
||||
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| series | otlp_aggregation_temporality | ts | delta_temporality.greptime_value * .greptime_value |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
| cumulative | | 1970-01-01T00:03:00 | 60.0 |
|
||||
| delta | delta | 1970-01-01T00:03:00 | 30.0 |
|
||||
| single | delta | 1970-01-01T00:03:00 | 14.0 |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------+
|
||||
|
||||
-- Aggregation preserves or deliberately removes the visible stored marker.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') sum by (series, otlp_aggregation_temporality) (rate(delta_temporality[3m]));
|
||||
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------------+
|
||||
| series | otlp_aggregation_temporality | ts | sum(prom_rate(ts_range,greptime_value,ts,Int64(180000))) |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------------+
|
||||
| cumulative | | 1970-01-01T00:03:00 | 0.16666666666666666 |
|
||||
| delta | delta | 1970-01-01T00:03:00 | 0.25 |
|
||||
| single | delta | 1970-01-01T00:03:00 | 0.03888888888888889 |
|
||||
+------------+------------------------------+---------------------+----------------------------------------------------------+
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') sum by (series) (rate(delta_temporality[3m]));
|
||||
|
||||
+------------+---------------------+----------------------------------------------------------+
|
||||
| series | ts | sum(prom_rate(ts_range,greptime_value,ts,Int64(180000))) |
|
||||
+------------+---------------------+----------------------------------------------------------+
|
||||
| cumulative | 1970-01-01T00:03:00 | 0.16666666666666666 |
|
||||
| delta | 1970-01-01T00:03:00 | 0.25 |
|
||||
| single | 1970-01-01T00:03:00 | 0.03888888888888889 |
|
||||
+------------+---------------------+----------------------------------------------------------+
|
||||
|
||||
TQL EVAL (180, 180, '1m') round(sum(rate(delta_temporality[3m])), 0.000001);
|
||||
|
||||
+---------------------+----------------------------------------------------------------------------------------+
|
||||
| ts | prom_round(sum(prom_rate(ts_range,greptime_value,ts,Int64(180000))),Float64(0.000001)) |
|
||||
+---------------------+----------------------------------------------------------------------------------------+
|
||||
| 1970-01-01T00:03:00 | 0.45555599999999996 |
|
||||
+---------------------+----------------------------------------------------------------------------------------+
|
||||
|
||||
CREATE TABLE delta_marker_only (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE,
|
||||
otlp_aggregation_temporality STRING PRIMARY KEY
|
||||
);
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
INSERT INTO delta_marker_only VALUES
|
||||
(180000, 1, 'delta'),
|
||||
(180000, 2, NULL);
|
||||
|
||||
Affected Rows: 2
|
||||
|
||||
CREATE TABLE delta_tagless (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE
|
||||
);
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
INSERT INTO delta_tagless VALUES (180000, 10);
|
||||
|
||||
Affected Rows: 1
|
||||
|
||||
-- A missing temporality marker matches the NULL cumulative state; "delta" does not.
|
||||
TQL EVAL (180, 180, '1m') delta_marker_only + delta_tagless;
|
||||
|
||||
+---------------------+-----------------------------------------------------------------+
|
||||
| ts | delta_marker_only.greptime_value + delta_tagless.greptime_value |
|
||||
+---------------------+-----------------------------------------------------------------+
|
||||
| 1970-01-01T00:03:00 | 12.0 |
|
||||
+---------------------+-----------------------------------------------------------------+
|
||||
|
||||
TQL EVAL (180, 180, '1m') delta_marker_only AND delta_tagless;
|
||||
|
||||
+---------------------+----------------+------------------------------+
|
||||
| ts | greptime_value | otlp_aggregation_temporality |
|
||||
+---------------------+----------------+------------------------------+
|
||||
| 1970-01-01T00:03:00 | 2.0 | |
|
||||
+---------------------+----------------+------------------------------+
|
||||
|
||||
DROP TABLE delta_tagless;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
DROP TABLE delta_marker_only;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
DROP TABLE delta_temporality;
|
||||
|
||||
Affected Rows: 0
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
CREATE TABLE delta_temporality (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE,
|
||||
series STRING,
|
||||
otlp_aggregation_temporality STRING,
|
||||
PRIMARY KEY (series, otlp_aggregation_temporality)
|
||||
);
|
||||
|
||||
INSERT INTO delta_temporality VALUES
|
||||
(60000, 10, 'delta', 'delta'),
|
||||
(120000, 20, 'delta', 'delta'),
|
||||
(180000, 15, 'delta', 'delta'),
|
||||
(180000, 7, 'single', 'delta'),
|
||||
(60000, 10, 'cumulative', NULL),
|
||||
(120000, 20, 'cumulative', NULL),
|
||||
(180000, 30, 'cumulative', NULL);
|
||||
|
||||
-- Raw deltas are summed; untagged rows retain cumulative reset-aware math.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') increase(delta_temporality[3m]);
|
||||
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') rate(delta_temporality[3m]);
|
||||
|
||||
-- The physical plan selects raw-delta math per series while retaining cumulative rate.
|
||||
-- SQLNESS REPLACE (metrics.*) REDACTED
|
||||
-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED
|
||||
-- SQLNESS REPLACE (-+) -
|
||||
-- SQLNESS REPLACE (\s\s+) _
|
||||
-- SQLNESS REPLACE (?m)^\|\s1_\|\s0_\|_(?:Projection|Filter)Exec:.*\n
|
||||
-- SQLNESS REPLACE (?m)^\|_\|_\|_FilterExec:.*\n
|
||||
-- SQLNESS REPLACE END\sas\s.*,\sseries@2\sas\sseries END as RATE_RESULT, series@2 as series
|
||||
-- SQLNESS REPLACE (peers.*) REDACTED
|
||||
-- SQLNESS REPLACE input_partitions=\d+ input_partitions=REDACTED
|
||||
-- SQLNESS REPLACE "partition_count":\{(.*?)\} "partition_count":REDACTED
|
||||
-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED
|
||||
TQL ANALYZE (180, 180, '1m') rate(delta_temporality[3m]);
|
||||
|
||||
-- The reserved temporality marker treats NULL as the absent cumulative state.
|
||||
TQL EVAL (180, 180, '1m') delta_temporality{otlp_aggregation_temporality=""};
|
||||
TQL EVAL (180, 180, '1m') delta_temporality{otlp_aggregation_temporality!="delta"};
|
||||
|
||||
-- Generated vector plans retain timestamp broadcast across the temporality marker.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') vector(2) * delta_temporality;
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') vector(2) * ignoring(otlp_aggregation_temporality) delta_temporality;
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') delta_temporality * vector(2);
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') delta_temporality * ignoring(otlp_aggregation_temporality) vector(2);
|
||||
|
||||
-- Aggregation preserves or deliberately removes the visible stored marker.
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') sum by (series, otlp_aggregation_temporality) (rate(delta_temporality[3m]));
|
||||
-- SQLNESS SORT_RESULT 3 1
|
||||
TQL EVAL (180, 180, '1m') sum by (series) (rate(delta_temporality[3m]));
|
||||
TQL EVAL (180, 180, '1m') round(sum(rate(delta_temporality[3m])), 0.000001);
|
||||
|
||||
CREATE TABLE delta_marker_only (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE,
|
||||
otlp_aggregation_temporality STRING PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO delta_marker_only VALUES
|
||||
(180000, 1, 'delta'),
|
||||
(180000, 2, NULL);
|
||||
|
||||
CREATE TABLE delta_tagless (
|
||||
ts TIMESTAMP TIME INDEX,
|
||||
greptime_value DOUBLE
|
||||
);
|
||||
|
||||
INSERT INTO delta_tagless VALUES (180000, 10);
|
||||
|
||||
-- A missing temporality marker matches the NULL cumulative state; "delta" does not.
|
||||
TQL EVAL (180, 180, '1m') delta_marker_only + delta_tagless;
|
||||
TQL EVAL (180, 180, '1m') delta_marker_only AND delta_tagless;
|
||||
|
||||
DROP TABLE delta_tagless;
|
||||
DROP TABLE delta_marker_only;
|
||||
DROP TABLE delta_temporality;
|
||||
@@ -46,3 +46,13 @@ iterations = 15
|
||||
|
||||
[scenario.queries.thresholds]
|
||||
max_candidate_latency_regression_pct = 75
|
||||
|
||||
[[scenario.queries]]
|
||||
name = "binary_1d_ignoring_host"
|
||||
kind = "tql"
|
||||
query = "TQL ANALYZE VERBOSE (1704067200, 1704153600, '1h') prom_remote_write_seeded_random / ignoring(host) prom_remote_write_seeded_random"
|
||||
warmup = 2
|
||||
iterations = 9
|
||||
|
||||
[scenario.queries.thresholds]
|
||||
max_candidate_latency_regression_pct = 75
|
||||
|
||||
Reference in New Issue
Block a user