This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/master by this push:
new 32f611881d Fix MAL List.of() varargs for >10 elements and split test
class by scope (#13746)
32f611881d is described below
commit 32f611881d94d340a19dbd640c6a645a4382b0cb
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue Mar 17 12:24:11 2026 +0800
Fix MAL List.of() varargs for >10 elements and split test class by scope
(#13746)
**Bug**: Javassist cannot resolve `List.of()` varargs overload for >10
elements (JDK 11 only has fixed-arity overloads for 0–10). MAL expressions with
`sum()` over 12 label keys (e.g., serviceRelation histogram percentile rules)
fail to compile.
**Fix**: Changed `StringListArgument` and `NumberListArgument` codegen from
`List.of(...)` to `Arrays.asList(new T[]{...})`, which works for any number of
elements.
**Test refactoring**: Split `MALClassGeneratorTest` (41 tests) into 3
classes by scope:
- `MALClassGeneratorTest` (18) — basic compilation, error handling,
bytecode verification
- `MALClassGeneratorClosureTest` (14) — tag/forEach closures, regex,
network-profiling patterns
- `MALClassGeneratorScopeTest` (9) — full-expression scope tests (endpoint,
service, instance, serviceRelation)
All 68 tests pass.
---
.../analyzer/v2/compiler/MALClassGenerator.java | 10 +-
.../v2/compiler/MALClassGeneratorClosureTest.java | 238 ++++++++++++++++++++
.../v2/compiler/MALClassGeneratorScopeTest.java | 243 +++++++++++++++++++++
.../v2/compiler/MALClassGeneratorTest.java | 240 +-------------------
4 files changed, 495 insertions(+), 236 deletions(-)
diff --git
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
index cfaff7d829..465cbddbac 100644
---
a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
+++
b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java
@@ -919,18 +919,20 @@ public final class MALClassGenerator {
} else if (arg instanceof MALExpressionModel.StringListArgument) {
final List<String> vals =
((MALExpressionModel.StringListArgument) arg).getValues();
- sb.append("java.util.List.of(");
+ // Use Arrays.asList — Javassist cannot resolve List.of() varargs
for >10 elements.
+ sb.append("java.util.Arrays.asList(new String[]{");
for (int i = 0; i < vals.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append('"').append(MALCodegenHelper.escapeJava(vals.get(i))).append('"');
}
- sb.append(')');
+ sb.append("})");
} else if (arg instanceof MALExpressionModel.NumberListArgument) {
final List<Double> vals =
((MALExpressionModel.NumberListArgument) arg).getValues();
- sb.append("java.util.List.of(");
+ // Use Arrays.asList — Javassist cannot resolve List.of() varargs
for >10 elements.
+ sb.append("java.util.Arrays.asList(new Number[]{");
for (int i = 0; i < vals.size(); i++) {
if (i > 0) {
sb.append(", ");
@@ -942,7 +944,7 @@ public final class MALClassGenerator {
sb.append("Double.valueOf(").append(v).append(')');
}
}
- sb.append(')');
+ sb.append("})");
} else if (arg instanceof MALExpressionModel.BoolArgument) {
sb.append(((MALExpressionModel.BoolArgument) arg).isValue());
} else if (arg instanceof MALExpressionModel.NullArgument) {
diff --git
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
new file mode 100644
index 0000000000..3dcbce0f6a
--- /dev/null
+++
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.meter.analyzer.v2.compiler;
+
+import javassist.ClassPool;
+import org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Closure-related tests for MALClassGenerator: tag closures, forEach closures,
+ * ProcessRegistry FQCN resolution, network-profiling patterns, string
concatenation,
+ * regex match, and time() scalar.
+ */
+class MALClassGeneratorClosureTest {
+
+ private MALClassGenerator generator;
+
+ @BeforeEach
+ void setUp() {
+ generator = new MALClassGenerator(new ClassPool(true));
+ }
+
+ // ==================== Closure key extraction tests ====================
+
+ @Test
+ void tagClosurePutsCorrectKey() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_key",
+ "metric.tag({tags -> tags.cluster = 'activemq::' +
tags.cluster})");
+ assertNotNull(expr);
+ final String source = generator.generateSource(
+ "metric.tag({tags -> tags.cluster = 'activemq::' +
tags.cluster})");
+ assertTrue(source.contains("this._tag"),
+ "Generated source should reference pre-compiled closure");
+ }
+
+ @Test
+ void tagClosureKeyExtractionViaGeneratedCode() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_key_gen",
+ "metric.tag({tags -> tags.service_name = 'svc1'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void tagClosureBracketAssignment() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_bracket",
+ "metric.tag({tags -> tags['my_key'] = 'my_value'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ // ==================== forEach closure tests ====================
+
+ @Test
+ void forEachClosureCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_foreach",
+ "metric.forEach(['client', 'server'], {prefix, tags ->"
+ + " tags[prefix + '_name'] = 'value'})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void forEachClosureWithBareReturn() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_foreach_return",
+ "metric.forEach(['x'], {prefix, tags ->\n"
+ + " if (tags[prefix + '_id'] != null) {\n"
+ + " return\n"
+ + " }\n"
+ + " tags[prefix + '_id'] = 'default'\n"
+ + "})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void forEachClosureWithVarDeclAndElseIf() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_foreach_vars",
+ "metric.forEach(['component'], {key, tags ->\n"
+ + " String result = \"\"\n"
+ + " String protocol = tags['protocol']\n"
+ + " String ssl = tags['is_ssl']\n"
+ + " if (protocol == 'http' && ssl == 'true') {\n"
+ + " result = '129'\n"
+ + " } else if (protocol == 'http') {\n"
+ + " result = '49'\n"
+ + " } else if (ssl == 'true') {\n"
+ + " result = '130'\n"
+ + " } else {\n"
+ + " result = '110'\n"
+ + " }\n"
+ + " tags[key] = result\n"
+ + "})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ // ==================== ProcessRegistry FQCN resolution tests
====================
+
+ @Test
+ void processRegistryResolvedToFQCN() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_registry",
+ "metric.forEach(['client'], {prefix, tags ->\n"
+ + " tags[prefix + '_process_id'] = "
+ + "ProcessRegistry.generateVirtualLocalProcess(tags.service,
tags.instance)\n"
+ + "})");
+ assertNotNull(expr);
+ }
+
+ // ==================== Network-profiling full expression tests
====================
+
+ @Test
+ void networkProfilingFirstClosureCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_np1",
+ "metric.forEach(['client', 'server'], { prefix, tags ->\n"
+ + " if (tags[prefix + '_process_id'] != null) {\n"
+ + " return\n"
+ + " }\n"
+ + " if (tags[prefix + '_local'] == 'true') {\n"
+ + " tags[prefix + '_process_id'] = ProcessRegistry"
+ + ".generateVirtualLocalProcess(tags.service, tags.instance)\n"
+ + " return\n"
+ + " }\n"
+ + " tags[prefix + '_process_id'] = ProcessRegistry"
+ + ".generateVirtualRemoteProcess(tags.service, tags.instance,"
+ + " tags[prefix + '_address'])\n"
+ + " })");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void networkProfilingSecondClosureCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_np2",
+ "metric.forEach(['component'], { key, tags ->\n"
+ + " String result = \"\"\n"
+ + " // protocol are defined in the component-libraries.yml\n"
+ + " String protocol = tags['protocol']\n"
+ + " String ssl = tags['is_ssl']\n"
+ + " if (protocol == 'http' && ssl == 'true') {\n"
+ + " result = '129'\n"
+ + " } else if (protocol == 'http') {\n"
+ + " result = '49'\n"
+ + " } else if (ssl == 'true') {\n"
+ + " result = '130'\n"
+ + " } else {\n"
+ + " result = '110'\n"
+ + " }\n"
+ + " tags[key] = result\n"
+ + " })");
+ assertNotNull(expr);
+ }
+
+ // ==================== String concatenation and regex in closures
====================
+
+ @Test
+ void apisixExpressionCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_apisix",
+ "metric.tag({tags -> tags.service_name = 'APISIX::'"
+ + "+(tags['skywalking_service']?.trim()?:'APISIX')})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void closureStringConcatenation() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_concat",
+ "metric.tag({tags -> tags.service_name = 'APISIX::' +
tags.service})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void regexMatchWithDefCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_regex",
+ "metric.tag({tags ->\n"
+ + " def matcher = (tags.metrics_name =~
/\\.ssl\\.certificate\\.([^.]+)\\.expiration/)\n"
+ + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
+ + "})");
+ assertNotNull(expr);
+ assertNotNull(expr.run(java.util.Map.of()));
+ }
+
+ @Test
+ void envoyCAExpressionCompiles() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_envoy_ca",
+ "(metric.tagMatch('metrics_name',
'.*ssl.*expiration_unix_time_seconds')"
+ + ".tag({tags ->\n"
+ + " def matcher = (tags.metrics_name =~
/\\.ssl\\.certificate\\.([^.]+)"
+ + "\\.expiration_unix_time_seconds/)\n"
+ + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
+ + "}).min(['app', 'secret_name']) - time())"
+ + ".downsampling(MIN).service(['app'], Layer.MESH_DP)");
+ assertNotNull(expr);
+ }
+
+ @Test
+ void timeScalarFunctionHandledInMetadata() throws Exception {
+ final MalExpression expr = generator.compile(
+ "test_time",
+ "(metric.sum(['app']) - time()).service(['app'], Layer.GENERAL)");
+ assertNotNull(expr);
+ assertNotNull(expr.metadata());
+ assertTrue(expr.metadata().getSamples().contains("metric"));
+ assertTrue(expr.metadata().getSamples().size() == 1);
+ }
+}
diff --git
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java
new file mode 100644
index 0000000000..2c994920d7
--- /dev/null
+++
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.meter.analyzer.v2.compiler;
+
+import javassist.ClassPool;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Full-expression compilation tests combining expPrefix + exp + expSuffix via
formatExp.
+ * Covers endpoint, service, instance, serviceRelation scope suffixes,
+ * forEach closures with if/else-if/else chains, sum() with >10 label keys,
+ * and tag() closure chained with service() suffix.
+ */
+class MALClassGeneratorScopeTest {
+
+ private MALClassGenerator generator;
+
+ @BeforeEach
+ void setUp() {
+ generator = new MALClassGenerator(new ClassPool(true));
+ }
+
+ /**
+ * Simulates MetricConvert.formatExp() to build the full expression
+ * from expPrefix + exp + expSuffix.
+ */
+ private static String formatExp(String expPrefix, String expSuffix, String
exp) {
+ String ret = exp;
+ if (expPrefix != null && !expPrefix.isEmpty()) {
+ int dotIdx = exp.indexOf('.');
+ if (dotIdx > 0) {
+ ret = String.format("(%s.%s)", exp.substring(0, dotIdx),
expPrefix);
+ String after = exp.substring(dotIdx + 1);
+ if (!after.isEmpty()) {
+ ret = String.format("(%s.%s)", ret, after);
+ }
+ }
+ }
+ if (expSuffix != null && !expSuffix.isEmpty()) {
+ ret = String.format("(%s).%s", ret, expSuffix);
+ }
+ return ret;
+ }
+
+ private void compileRule(String name, String expPrefix, String expSuffix,
String exp)
+ throws Exception {
+ final String full = formatExp(expPrefix, expSuffix, exp);
+ assertNotNull(generator.compile(name, full));
+ }
+
+ // --- Shared constants ---
+
+ private static final String FOREACH_COMPONENT_PREFIX =
+ "forEach(['component'], { key, tags ->\n"
+ + " String result = \"\"\n"
+ + " String protocol = tags['protocol']\n"
+ + " String ssl = tags['is_ssl']\n"
+ + " if (protocol == 'http' && ssl == 'true') {\n"
+ + " result = '129'\n"
+ + " } else if (protocol == 'http') {\n"
+ + " result = '49'\n"
+ + " } else if (ssl == 'true') {\n"
+ + " result = '130'\n"
+ + " } else {\n"
+ + " result = '110'\n"
+ + " }\n"
+ + " tags[key] = result\n"
+ + " })";
+
+ private static final String RELATION_SERVER_SUFFIX =
+ "serviceRelation(DetectPoint.SERVER, "
+ + "[\"client_service_subset\", \"client_service_name\",
\"client_namespace\", \"client_service_cluster\", \"client_service_env\"], "
+ + "[\"server_service_subset\", \"server_service_name\",
\"server_namespace\", \"server_service_cluster\", \"server_service_env\"], "
+ + "'|', Layer.GENERAL, 'component')";
+
+ private static final String RELATION_CLIENT_SUFFIX =
+ "serviceRelation(DetectPoint.CLIENT, "
+ + "[\"client_service_subset\", \"client_service_name\",
\"client_namespace\", \"client_service_cluster\", \"client_service_env\"], "
+ + "[\"server_service_subset\", \"server_service_name\",
\"server_namespace\", \"server_service_cluster\", \"server_service_env\"], "
+ + "'|', Layer.GENERAL, 'component')";
+
+ private static final String HISTOGRAM_12_LABELS =
+ ".sum([\"le\", \"client_service_subset\", \"client_service_name\",
\"client_namespace\", "
+ + "\"client_service_cluster\", \"client_service_env\",
\"server_service_subset\", "
+ + "\"server_service_name\", \"server_namespace\",
\"server_service_cluster\", "
+ + "\"server_service_env\", 'component'])"
+ + ".downsampling(SUM).histogram().histogram_percentile([50, 75, 90,
95, 99])";
+
+ // ==================== endpoint scope ====================
+
+ @Test
+ void endpointScopeWithHistogramPercentile() throws Exception {
+ final String suffix = "endpoint([\"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\"], [\"api\"], \"|\",
Layer.GENERAL)";
+ compileRule("ep_cpm", null, suffix,
+ "api_agent_api_total_count.downsampling(SUM_PER_MIN)");
+ compileRule("ep_duration", null, suffix,
+ "api_agent_api_total_duration.downsampling(SUM_PER_MIN)");
+ compileRule("ep_success", null, suffix,
+ "api_agent_api_http_success_count.downsampling(SUM_PER_MIN)");
+ compileRule("ep_percentile", null, suffix,
+ "api_agent_api_response_time_histogram"
+ + ".sum([\"le\", \"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\", \"api\"])"
+ + ".downsampling(SUM).histogram().histogram_percentile([50, 75,
90, 95, 99])");
+ }
+
+ // ==================== service scope ====================
+
+ @Test
+ void serviceScopeWithDelimiter() throws Exception {
+ final String suffix = "service([\"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\",
Layer.GENERAL)";
+ compileRule("svc_api_cpm", null, suffix,
+ "api_agent_api_total_count.downsampling(SUM_PER_MIN)");
+ compileRule("svc_api_duration", null, suffix,
+ "api_agent_api_total_duration.downsampling(SUM_PER_MIN)");
+ compileRule("svc_api_success", null, suffix,
+ "api_agent_api_http_success_count.downsampling(SUM_PER_MIN)");
+ compileRule("svc_api_percentile", null, suffix,
+ "api_agent_api_response_time_histogram"
+ + ".sum([\"le\", \"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\"])"
+ + ".downsampling(SUM).histogram().histogram_percentile([50, 75,
90, 95, 99])");
+ }
+
+ @Test
+ void serviceScopeWithTcpMetrics() throws Exception {
+ final String suffix = "service([\"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\",
Layer.GENERAL)";
+ for (final String metric : new String[]{
+ "write_bytes", "write_count", "write_execute_time",
+ "read_bytes", "read_count", "read_execute_time",
+ "connect_connection_count", "connect_connection_time",
+ "accept_connection_count", "accept_connection_time",
+ "close_connection_count", "close_connection_time"}) {
+ compileRule("svc_tcp_" + metric, null, suffix,
+ "metric_tcp_" + metric + ".downsampling(SUM_PER_MIN)");
+ }
+ }
+
+ @Test
+ void tagClosureChainedWithServiceSuffix() throws Exception {
+ final String suffix = "tag({tags -> tags.service = 'satellite::' +
tags.service}).service(['service'], Layer.SO11Y_SATELLITE)";
+ compileRule("sat_receive", null, suffix,
+ "sw_stl_gatherer_receive_count.sum([\"pipe\", \"status\",
\"service\"]).increase(\"PT1M\")");
+ compileRule("sat_fetch", null, suffix,
+ "sw_stl_gatherer_fetch_count.sum([\"pipe\", \"status\",
\"service\"]).increase(\"PT1M\")");
+ compileRule("sat_queue_in", null, suffix,
+ "sw_stl_queue_output_count.sum([\"pipe\", \"status\",
\"service\"]).increase(\"PT1M\")");
+ compileRule("sat_send", null, suffix,
+ "sw_stl_sender_output_count.sum([\"pipe\", \"status\",
\"service\"]).increase(\"PT1M\")");
+ compileRule("sat_queue_cap", null, suffix,
+ "sw_stl_pipeline_queue_total_capacity.sum([\"pipeline\",
\"service\"])");
+ compileRule("sat_queue_used", null, suffix,
+ "sw_stl_pipeline_queue_partition_size.sum([\"pipeline\",
\"service\"])");
+ compileRule("sat_cpu", null, suffix,
+ "sw_stl_grpc_server_cpu_gauge.downsampling(LATEST)");
+ compileRule("sat_conn", null, suffix,
+ "sw_stl_grpc_server_connection_count.downsampling(LATEST)");
+ }
+
+ // ==================== instance scope ====================
+
+ @Test
+ void instanceScopeWithNullLayerKey() throws Exception {
+ final String suffix = "instance([\"service_subset\", \"service_name\",
\"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\",
[\"instance_name\"], \"|\", Layer.GENERAL, null)";
+ for (final String metric : new String[]{
+ "write_bytes", "write_count", "write_execute_time",
+ "read_bytes", "read_count", "read_execute_time",
+ "connect_connection_count", "connect_connection_time",
+ "accept_connection_count", "accept_connection_time",
+ "close_connection_count", "close_connection_time"}) {
+ compileRule("inst_tcp_" + metric, null, suffix,
+ "api_agent_tcp_" + metric + ".downsampling(SUM_PER_MIN)");
+ }
+ }
+
+ // ==================== serviceRelation scope ====================
+
+ @Test
+ void serviceRelation6ArgWithForEachServer() throws Exception {
+ compileRule("rel_api_srv_cpm", FOREACH_COMPONENT_PREFIX,
RELATION_SERVER_SUFFIX,
+ "metric_server_api_total_count.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_srv_dur", FOREACH_COMPONENT_PREFIX,
RELATION_SERVER_SUFFIX,
+ "metric_server_api_total_duration.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_srv_suc", FOREACH_COMPONENT_PREFIX,
RELATION_SERVER_SUFFIX,
+ "metric_server_api_http_success_count.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_srv_pct", FOREACH_COMPONENT_PREFIX,
RELATION_SERVER_SUFFIX,
+ "metric_server_api_response_time_histogram" + HISTOGRAM_12_LABELS);
+ }
+
+ @Test
+ void serviceRelation6ArgWithForEachClient() throws Exception {
+ compileRule("rel_api_cli_cpm", FOREACH_COMPONENT_PREFIX,
RELATION_CLIENT_SUFFIX,
+ "metric_client_api_total_count.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_cli_dur", FOREACH_COMPONENT_PREFIX,
RELATION_CLIENT_SUFFIX,
+ "metric_client_api_total_duration.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_cli_suc", FOREACH_COMPONENT_PREFIX,
RELATION_CLIENT_SUFFIX,
+ "metric_client_api_http_success_count.downsampling(SUM_PER_MIN)");
+ compileRule("rel_api_cli_pct", FOREACH_COMPONENT_PREFIX,
RELATION_CLIENT_SUFFIX,
+ "metric_client_api_response_time_histogram" + HISTOGRAM_12_LABELS);
+ }
+
+ @Test
+ void serviceRelationTcpWithForEachServer() throws Exception {
+ for (final String metric : new String[]{
+ "write_bytes", "write_count", "write_execute_time",
+ "read_bytes", "read_count", "read_execute_time",
+ "connect_connection_count", "connect_connection_time",
+ "accept_connection_count", "accept_connection_time",
+ "close_connection_count", "close_connection_time"}) {
+ compileRule("rel_tcp_srv_" + metric, FOREACH_COMPONENT_PREFIX,
RELATION_SERVER_SUFFIX,
+ "metric_server_tcp_" + metric + ".downsampling(SUM_PER_MIN)");
+ }
+ }
+
+ @Test
+ void serviceRelationTcpWithForEachClient() throws Exception {
+ for (final String metric : new String[]{
+ "write_bytes", "write_count", "write_execute_time",
+ "read_bytes", "read_count", "read_execute_time",
+ "connect_connection_count", "connect_connection_time",
+ "accept_connection_count", "accept_connection_time",
+ "close_connection_count", "close_connection_time"}) {
+ compileRule("rel_tcp_cli_" + metric, FOREACH_COMPONENT_PREFIX,
RELATION_CLIENT_SUFFIX,
+ "metric_client_tcp_" + metric + ".downsampling(SUM_PER_MIN)");
+ }
+ }
+}
diff --git
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
index a850977a01..8bd2d4c179 100644
---
a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
+++
b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java
@@ -26,6 +26,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+/**
+ * Basic compilation, error handling, source generation, and bytecode
verification
+ * tests for MALClassGenerator.
+ */
class MALClassGeneratorTest {
private MALClassGenerator generator;
@@ -35,12 +39,13 @@ class MALClassGeneratorTest {
generator = new MALClassGenerator(new ClassPool(true));
}
+ // ==================== Basic compilation tests ====================
+
@Test
void compileSimpleMetric() throws Exception {
final MalExpression expr = generator.compile(
"test_metric", "instance_jvm_cpu");
assertNotNull(expr);
- // Run returns SampleFamily.EMPTY since no samples are provided
assertNotNull(expr.run(java.util.Map.of()));
}
@@ -110,9 +115,7 @@ class MALClassGeneratorTest {
final String source = generator.generateSource(
"instance_jvm_cpu.sum(['service'])");
assertNotNull(source);
- // Generated source should contain getOrDefault for the metric
- org.junit.jupiter.api.Assertions.assertTrue(
- source.contains("getOrDefault"));
+ assertTrue(source.contains("getOrDefault"));
}
@Test
@@ -149,260 +152,37 @@ class MALClassGeneratorTest {
@Test
void emptyExpressionThrows() {
- // Demo error: MAL expression parsing failed: 1:0 mismatched input
'<EOF>'
- // expecting {IDENTIFIER, NUMBER, '(', '-'}
assertThrows(Exception.class, () -> generator.compile("test", ""));
}
@Test
void malformedExpressionThrows() {
- // Demo error: MAL expression parsing failed: 1:7 token recognition
error at: '@'
assertThrows(Exception.class,
() -> generator.compile("test", "metric.@invalid"));
}
@Test
void unclosedParenthesisThrows() {
- // Demo error: MAL expression parsing failed: 1:8 mismatched input
'<EOF>'
- // expecting {')', '+', '-', '*', '/'}
assertThrows(Exception.class,
() -> generator.compile("test", "(metric1 "));
}
@Test
void invalidFilterClosureThrows() {
- // Demo error: MAL filter parsing failed: 1:0 mismatched input
'invalid'
- // expecting '{'
assertThrows(Exception.class,
() -> generator.compileFilter("invalid filter"));
}
@Test
void emptyFilterBodyThrows() {
- // Demo error: MAL filter parsing failed: 1:1 mismatched input '}'
- // expecting {IDENTIFIER, ...}
assertThrows(Exception.class,
() -> generator.compileFilter("{ }"));
}
- // ==================== Closure key extraction tests ====================
-
- @Test
- void tagClosurePutsCorrectKey() throws Exception {
- // Issue: tags.cluster = expr should generate tags.put("cluster", ...)
- // NOT tags.put("tags.cluster", ...)
- final MalExpression expr = generator.compile(
- "test_key",
- "metric.tag({tags -> tags.cluster = 'activemq::' +
tags.cluster})");
- assertNotNull(expr);
- final String source = generator.generateSource(
- "metric.tag({tags -> tags.cluster = 'activemq::' +
tags.cluster})");
- assertTrue(source.contains("this._tag"),
- "Generated source should reference pre-compiled closure");
- }
-
- @Test
- void tagClosureKeyExtractionViaGeneratedCode() throws Exception {
- // Verify the closure generates correct put("cluster", ...) not
put("tags.cluster", ...)
- final MalExpression expr = generator.compile(
- "test_key_gen",
- "metric.tag({tags -> tags.service_name = 'svc1'})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void tagClosureBracketAssignment() throws Exception {
- // tags['key_name'] = 'value' should also use correct key
- final MalExpression expr = generator.compile(
- "test_bracket",
- "metric.tag({tags -> tags['my_key'] = 'my_value'})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- // ==================== forEach closure tests ====================
-
- @Test
- void forEachClosureCompiles() throws Exception {
- // forEach requires ForEachFunction.accept(String, Map), not
TagFunction.apply(Map)
- final MalExpression expr = generator.compile(
- "test_foreach",
- "metric.forEach(['client', 'server'], {prefix, tags ->"
- + " tags[prefix + '_name'] = 'value'})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void forEachClosureWithBareReturn() throws Exception {
- // forEach with bare return (void method) — should not throw
- final MalExpression expr = generator.compile(
- "test_foreach_return",
- "metric.forEach(['x'], {prefix, tags ->\n"
- + " if (tags[prefix + '_id'] != null) {\n"
- + " return\n"
- + " }\n"
- + " tags[prefix + '_id'] = 'default'\n"
- + "})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void forEachClosureWithVarDeclAndElseIf() throws Exception {
- // Full pattern from network-profiling.yaml second closure
- final MalExpression expr = generator.compile(
- "test_foreach_vars",
- "metric.forEach(['component'], {key, tags ->\n"
- + " String result = \"\"\n"
- + " String protocol = tags['protocol']\n"
- + " String ssl = tags['is_ssl']\n"
- + " if (protocol == 'http' && ssl == 'true') {\n"
- + " result = '129'\n"
- + " } else if (protocol == 'http') {\n"
- + " result = '49'\n"
- + " } else if (ssl == 'true') {\n"
- + " result = '130'\n"
- + " } else {\n"
- + " result = '110'\n"
- + " }\n"
- + " tags[key] = result\n"
- + "})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- // ==================== ProcessRegistry FQCN resolution tests
====================
-
- @Test
- void processRegistryResolvedToFQCN() throws Exception {
- // ProcessRegistry.generateVirtualLocalProcess() should resolve to FQCN
- final MalExpression expr = generator.compile(
- "test_registry",
- "metric.forEach(['client'], {prefix, tags ->\n"
- + " tags[prefix + '_process_id'] = "
- + "ProcessRegistry.generateVirtualLocalProcess(tags.service,
tags.instance)\n"
- + "})");
- assertNotNull(expr);
- // We can't easily execute this (needs ProcessRegistry runtime) but
compile should succeed
- }
-
- // ==================== Network-profiling full expression tests
====================
-
- @Test
- void networkProfilingFirstClosureCompiles() throws Exception {
- // Full first closure from network-profiling.yaml expPrefix
- final MalExpression expr = generator.compile(
- "test_np1",
- "metric.forEach(['client', 'server'], { prefix, tags ->\n"
- + " if (tags[prefix + '_process_id'] != null) {\n"
- + " return\n"
- + " }\n"
- + " if (tags[prefix + '_local'] == 'true') {\n"
- + " tags[prefix + '_process_id'] = ProcessRegistry"
- + ".generateVirtualLocalProcess(tags.service, tags.instance)\n"
- + " return\n"
- + " }\n"
- + " tags[prefix + '_process_id'] = ProcessRegistry"
- + ".generateVirtualRemoteProcess(tags.service, tags.instance,"
- + " tags[prefix + '_address'])\n"
- + " })");
- assertNotNull(expr);
- }
-
- @Test
- void networkProfilingSecondClosureCompiles() throws Exception {
- // Full second closure from network-profiling.yaml expPrefix
- final MalExpression expr = generator.compile(
- "test_np2",
- "metric.forEach(['component'], { key, tags ->\n"
- + " String result = \"\"\n"
- + " // protocol are defined in the component-libraries.yml\n"
- + " String protocol = tags['protocol']\n"
- + " String ssl = tags['is_ssl']\n"
- + " if (protocol == 'http' && ssl == 'true') {\n"
- + " result = '129'\n"
- + " } else if (protocol == 'http') {\n"
- + " result = '49'\n"
- + " } else if (ssl == 'true') {\n"
- + " result = '130'\n"
- + " } else {\n"
- + " result = '110'\n"
- + " }\n"
- + " tags[key] = result\n"
- + " })");
- assertNotNull(expr);
- }
-
- // ==================== String concatenation in closures
====================
-
- @Test
- void apisixExpressionCompiles() throws Exception {
- // The APISIX expression that originally triggered the E2E failure:
- // safe navigation + elvis + bracket access + string concat
- final MalExpression expr = generator.compile(
- "test_apisix",
- "metric.tag({tags -> tags.service_name = 'APISIX::'"
- + "+(tags['skywalking_service']?.trim()?:'APISIX')})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void closureStringConcatenation() throws Exception {
- // APISIX-style: tags.service_name = 'APISIX::' + tags.service
- final MalExpression expr = generator.compile(
- "test_concat",
- "metric.tag({tags -> tags.service_name = 'APISIX::' +
tags.service})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void regexMatchWithDefCompiles() throws Exception {
- // envoy-ca pattern: def + regex match + ternary with chained indexing
- final MalExpression expr = generator.compile(
- "test_regex",
- "metric.tag({tags ->\n"
- + " def matcher = (tags.metrics_name =~
/\\.ssl\\.certificate\\.([^.]+)\\.expiration/)\n"
- + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
- + "})");
- assertNotNull(expr);
- assertNotNull(expr.run(java.util.Map.of()));
- }
-
- @Test
- void envoyCAExpressionCompiles() throws Exception {
- // Full envoy-ca.yaml expression with regex closure, subtraction of
time(), and service
- final MalExpression expr = generator.compile(
- "test_envoy_ca",
- "(metric.tagMatch('metrics_name',
'.*ssl.*expiration_unix_time_seconds')"
- + ".tag({tags ->\n"
- + " def matcher = (tags.metrics_name =~
/\\.ssl\\.certificate\\.([^.]+)"
- + "\\.expiration_unix_time_seconds/)\n"
- + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n"
- + "}).min(['app', 'secret_name']) - time())"
- + ".downsampling(MIN).service(['app'], Layer.MESH_DP)");
- assertNotNull(expr);
- }
-
- @Test
- void timeScalarFunctionHandledInMetadata() throws Exception {
- // time() should not appear as a sample name and should be treated as
scalar
- final MalExpression expr = generator.compile(
- "test_time",
- "(metric.sum(['app']) - time()).service(['app'], Layer.GENERAL)");
- assertNotNull(expr);
- assertNotNull(expr.metadata());
- // time() should not be in sample names
- assertTrue(expr.metadata().getSamples().contains("metric"));
- assertTrue(expr.metadata().getSamples().size() == 1);
- }
+ // ==================== Bytecode verification ====================
@Test
void runMethodHasLocalVariableTable() throws Exception {
- // Compile a class that writes its .class file for inspection
final java.io.File tmpDir =
java.nio.file.Files.createTempDirectory("mal-lvt").toFile();
try {
final ClassPool pool = new ClassPool(true);
@@ -411,11 +191,9 @@ class MALClassGeneratorTest {
final MalExpression expr = gen.compile(
"test_lvt", "instance_jvm_cpu.sum(['service', 'instance'])");
assertNotNull(expr);
- // Read the .class file bytecode and verify LVT
final java.io.File[] classFiles = tmpDir.listFiles((d, n) ->
n.endsWith(".class"));
assertNotNull(classFiles);
assertTrue(classFiles.length > 0, "Should have generated .class
file");
- // Use javassist to read back and check for LocalVariableTable
final javassist.bytecode.ClassFile cf =
new javassist.bytecode.ClassFile(
new java.io.DataInputStream(
@@ -428,7 +206,6 @@ class MALClassGeneratorTest {
(javassist.bytecode.LocalVariableAttribute)
code.getAttribute(javassist.bytecode.LocalVariableAttribute.tag);
assertNotNull(lva, "run() should have LocalVariableTable
attribute");
- // Check that slot 1 has name "samples"
boolean foundSamples = false;
boolean foundSf = false;
for (int i = 0; i < lva.tableLength(); i++) {
@@ -449,5 +226,4 @@ class MALClassGeneratorTest {
tmpDir.delete();
}
}
-
}