diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md index 721d3bd3e9ff..175c54975b72 100644 --- a/docs/en/changes/changes.md +++ b/docs/en/changes/changes.md @@ -23,6 +23,7 @@ * Fix the OpenTelemetry receiver folding a request that carries several minutes of points into one. A MAL rule folds every sample of an entity into one value at the first sample's time, so a delta exporter that batches, or a sender replaying history, landed as one minute. A request is now analysed a minute at a time, oldest first; a request whose points share one minute, a scrape relayed by a collector, is analysed exactly as before. * Fix the runtime-rule sync tearing down every bundled MAL and LAL rule file on its first pass after each boot: `StaticRuleLoader` seeds a bundled rule with a RUNNING state, but the gone-keys pass recognised a boot-seeded entry only by a null state, so every shipped rule file without an operator row was torn down and reloaded from the bundle one after another. A MAL teardown waits a persistence round for the metric drain, and for that round the OpenTelemetry receiver held no converter for the file and dropped its samples without a log line, so every restart lost up to a round of metrics from each rule set within its first minute or two. A rule that runs its bundled file as shipped is now left alone; only an operator override whose row is gone falls over to the bundle. * Open every port that takes requests only after the boot is complete. A new `ModuleProvider.notifyBootCompleted` hook runs once every module's `notifyAfterCompleted` has returned, and the core's gRPC and HTTP servers, the sharing server, the Zipkin, AWS Firehose, eBPF, Envoy metrics and Zabbix receivers, the admin server and the PromQL, LogQL, TraceQL and Zipkin query servers bind there. Before, the core opened 11800 and 12800 in its own `notifyAfterCompleted`, ahead of the modules started after it, so a request could arrive while the runtime-rule boot pass or any later boot-time work was still running; the readiness probes, which check the port, now mean what they say. +* Fix the Elasticsearch storage filtering events by a layer name against the numeric `layer` column, which made every event query carrying a layer condition fail with `number_format_exception` while the query was built, before any document was read. It is what Horizon UI's per-service events popout sends, so that popout failed for every service; on an empty index as well. The condition value is now resolved to its layer value, as the JDBC and BanyanDB storages already do. #### UI * Add a Virtual GenAI evaluation-record page and evaluation-score chart in Horizon UI, so operators can inspect evaluation result, level, reason, judge model, timestamp, trace linkage, and the `gen_ai_model_evaluation_score_ppm` trend for evaluated records. diff --git a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAO.java b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAO.java index 89369b770d86..5f255e498c41 100644 --- a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAO.java +++ b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/main/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAO.java @@ -121,7 +121,7 @@ private void buildMustQueryListByCondition(final EventQueryCondition condition, } if (!isNullOrEmpty(condition.getLayer())) { - query.must(Query.term(Event.LAYER, condition.getLayer())); + query.must(Query.term(Event.LAYER, Layer.nameOf(condition.getLayer()).value())); } } diff --git a/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAOTest.java b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAOTest.java new file mode 100644 index 000000000000..75962bd4f37f --- /dev/null +++ b/oap-server/server-storage-plugin/storage-elasticsearch-plugin/src/test/java/org/apache/skywalking/oap/server/storage/plugin/elasticsearch/query/ESEventQueryDAOTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + * + */ + +package org.apache.skywalking.oap.server.storage.plugin.elasticsearch.query; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.skywalking.library.elasticsearch.requests.search.SearchBuilder; +import org.apache.skywalking.oap.server.core.analysis.Layer; +import org.apache.skywalking.oap.server.core.query.type.Pagination; +import org.apache.skywalking.oap.server.core.query.type.event.EventQueryCondition; +import org.apache.skywalking.oap.server.library.client.elasticsearch.ElasticSearchClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +class ESEventQueryDAOTest { + + @Mock + private ElasticSearchClient client; + + private ESEventQueryDAO dao; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + dao = new ESEventQueryDAO(client); + } + + @Test + void buildQuery_withLayerName_shouldFilterByTheNumericLayerValue() throws Exception { + final EventQueryCondition condition = new EventQueryCondition(); + condition.setLayer(Layer.GENERAL.name()); + condition.setPaging(new Pagination(1, 10)); + + final SearchBuilder search = dao.buildQuery(condition); + final String json = objectMapper.writeValueAsString(search.build()); + + // The layer column is stored as a number, so the filter has to carry the layer value instead of + // its name. Sending the name makes Elasticsearch fail while building the query, before any + // document is read: number_format_exception: For input string: "GENERAL". + assertThat(json).contains("\"layer\":" + Layer.GENERAL.value()); + assertThat(json).doesNotContain(Layer.GENERAL.name()); + } +}