Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/changes/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
* Fix: `envoy-ai-gateway` metrics rules, make the metrics value return `0` when the divisor is `0`.
* Custom `Layer`s can be declared without modifying the OAP source — via an operator-managed `layer-extensions.yml`, inline `layerDefinitions:` block in a MAL or LAL rule file, or a plugin extension. UI dashboard templates for new layers are auto-discovered from the `ui-initialized-templates/` directory. Recommended ordinal range for external layers is `>= 1000`; conflicting names or ordinals are reported at boot.
* LAL: support full arithmetic (`+`, `-`, `*`, `/`) on numeric operands and fix the original bug where `(tag("x") as Integer) + (tag("y") as Integer)` was treated as string concatenation — expressions like `input_tokens + output_tokens < 10000` produced the concatenated string `"2589115"` rather than the integer sum `2704`, so token-threshold conditions never triggered `abort {}`. Operand types are now inferred from explicit casts (`as Integer` / `as Long` / `as Float` / `as Double`), typed proto fields, or numeric literal shape (with `L` / `F` / `D` suffix support, e.g. `1000L`). The compiler honours JLS-style binary numeric promotion and emits Java arithmetic in the declared primitive type — `(x as Integer) + (y as Integer)` compiles to `int + int` (not widened to `long`). `+` with any String operand falls back to string concatenation; `-` / `*` / `/` against non-numeric operands produces a compile-time error. The `as Double` and `as Float` casts are accepted in `typeCast` clauses, including in `def` declarations. Numeric comparisons honour declared casts on both sides (no more universal `h.toLong()` wrapper).
* Fix: `avgHistogramPercentile` / `sumHistogramPercentile` meter functions reported the smallest finite bucket boundary (e.g. `10` for OTel `gen_ai_server_request_duration` whose `le` is rewritten from `0.01s` → `10ms`) for every rank when no samples were observed in any bucket. The percentile loop's `count >= roof` check matched on the first sorted bucket because both sides were `0`. `calculate()` now short-circuits to `0` for every rank when the windowed total is `0`.

#### UI
* Add mobile menu icon and i18n labels for the iOS layer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ public void calculate() {
long total;
total = subDataset.sumOfValues();

if (total <= 0) {
for (int rankIdx = 0; rankIdx < ranks.size(); rankIdx++) {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, 0L);
}
return;
}

int[] roofs = new int[ranks.size()];
for (int i = 0; i < ranks.size(); i++) {
roofs[i] = Math.round(total * ranks.get(i) * 1.0f / 100);
Expand All @@ -253,13 +261,8 @@ public void calculate() {
int roof = roofs[rankIdx];

if (count >= roof) {
if (labels.isEmpty()) {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
} else {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
}
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
loopIndex++;
} else {
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ public void calculate() {
long total;
total = subDataset.sumOfValues();

if (total <= 0) {
for (int rankIdx = 0; rankIdx < ranks.size(); rankIdx++) {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, 0L);
}
return;
}

int[] roofs = new int[ranks.size()];
for (int i = 0; i < ranks.size(); i++) {
roofs[i] = Math.round(total * ranks.get(i) * 1.0f / 100);
Expand All @@ -219,13 +227,8 @@ public void calculate() {
int roof = roofs[rankIdx];

if (count >= roof) {
if (labels.isEmpty()) {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
} else {
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
}
labels.put(PERCENTILE_LABEL_NAME, String.valueOf(ranks.get(rankIdx)));
percentileValues.put(labels, Long.parseLong(key));
loopIndex++;
} else {
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,59 @@ public void testFunctionWithLabel() {
);
}

@Test
public void testFunctionWithNoData() {
PercentileFunctionInst inst = new PercentileFunctionInst();
inst.accept(
MeterEntity.newService("service-test", Layer.GENERAL),
new PercentileArgument(
new BucketedValues(
BUCKETS,
new long[] {
0,
0,
0,
0
}
),
RANKS
)
);

inst.calculate();
// No samples observed in any bucket — every rank should report 0
// rather than collapsing to the smallest bucket boundary.
Assertions.assertEquals(new DataTable("{p=50},0|{p=90},0"), inst.getValue());
}

@Test
public void testFunctionWithNoDataAndLabels() {
BucketedValues bucketedValues = new BucketedValues(
BUCKETS,
new long[] {
0,
0,
0,
0
}
);
bucketedValues.getLabels().put("service_name", "ai-gateway");
PercentileFunctionInst inst = new PercentileFunctionInst();
inst.accept(
MeterEntity.newService("service-test", Layer.GENERAL),
new PercentileArgument(
bucketedValues,
RANKS
)
);

inst.calculate();
Assertions.assertEquals(
new DataTable("{service_name=ai-gateway,p=50},0|{service_name=ai-gateway,p=90},0"),
inst.getValue()
);
}

private static class PercentileFunctionInst extends AvgHistogramPercentileFunction {
@Override
public AcceptableValue<PercentileArgument> createNew() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,59 @@ public void testFunctionWithLabel() {
);
}

@Test
public void testFunctionWithNoData() {
PercentileFunctionInst inst = new PercentileFunctionInst();
inst.accept(
MeterEntity.newService("service-test", Layer.GENERAL),
new PercentileArgument(
new BucketedValues(
BUCKETS,
new long[] {
0,
0,
0,
0
}
),
RANKS
)
);

inst.calculate();
// No samples observed in any bucket — every rank should report 0
// rather than collapsing to the smallest bucket boundary.
Assertions.assertEquals(new DataTable("{p=50},0|{p=90},0"), inst.getValue());
}

@Test
public void testFunctionWithNoDataAndLabels() {
BucketedValues bucketedValues = new BucketedValues(
BUCKETS,
new long[] {
0,
0,
0,
0
}
);
bucketedValues.getLabels().put("service_name", "ai-gateway");
PercentileFunctionInst inst = new PercentileFunctionInst();
inst.accept(
MeterEntity.newService("service-test", Layer.GENERAL),
new PercentileArgument(
bucketedValues,
RANKS
)
);

inst.calculate();
Assertions.assertEquals(
new DataTable("{service_name=ai-gateway,p=50},0|{service_name=ai-gateway,p=90},0"),
inst.getValue()
);
}

private static class PercentileFunctionInst extends SumHistogramPercentileFunction {
@Override
public AcceptableValue<PercentileArgument> createNew() {
Expand Down
Loading