This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch groovy-replace in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit e57381850a5d9610515f286b54421d4356c7fc53 Author: Wu Sheng <[email protected]> AuthorDate: Sun Mar 1 17:31:06 2026 +0800 Fix LAL compiler gaps: tag(), safe nav, nested blocks, else-if chains, interpolated sampler IDs Address five critical gaps in the LAL v2 compiler that broke shipped production rules: 1. tag("LOG_KIND") in conditions now emits tagValue() helper instead of null 2. Safe navigation (?.) for method calls emits safeCall() helper to prevent NPE 3. Metrics, slowSql, sampledTrace, sampler/rateLimit blocks generate proper sub-consumer classes with BindingAware wiring 4. else-if chains build nested IfBlock AST nodes instead of dropping intermediate branches 5. GString interpolation in rateLimit IDs (e.g. "${log.service}:${parsed.code}") parsed into InterpolationPart segments and emitted as string concatenation Also fixes ProcessRegistry static calls to pass arguments through, and adds comprehensive tests (55 total: 35 generator + 20 parser) covering all gaps including production-like envoy-als, nginx, and k8s-service rule patterns. Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../log/analyzer/compiler/LALClassGenerator.java | 636 ++++++++++++++++++++- .../oap/log/analyzer/compiler/LALScriptModel.java | 77 ++- .../oap/log/analyzer/compiler/LALScriptParser.java | 193 ++++++- .../analyzer/compiler/LALClassGeneratorTest.java | 458 +++++++++++++++ .../log/analyzer/compiler/LALScriptParserTest.java | 337 +++++++++++ 5 files changed, 1653 insertions(+), 48 deletions(-) diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java index a5cdcab939..be5e78d9ef 100644 --- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java +++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGenerator.java @@ -52,6 +52,22 @@ public final class LALClassGenerator { "org.apache.skywalking.oap.log.analyzer.dsl.Binding"; private static final String BINDING_PARSED = "org.apache.skywalking.oap.log.analyzer.dsl.Binding.Parsed"; + private static final String EXTRACTOR_SPEC = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.extractor.ExtractorSpec"; + private static final String SLOW_SQL_SPEC = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.extractor.slowsql.SlowSqlSpec"; + private static final String SAMPLED_TRACE_SPEC = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.extractor.sampledtrace.SampledTraceSpec"; + private static final String SINK_SPEC = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.SinkSpec"; + private static final String SAMPLER_SPEC = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.SamplerSpec"; + private static final String RATE_LIMITING_SAMPLER = + "org.apache.skywalking.oap.log.analyzer.dsl.spec.sink.sampler.RateLimitingSampler"; + private static final String SAMPLE_BUILDER = + EXTRACTOR_SPEC + "$SampleBuilder"; + private static final String PROCESS_REGISTRY = + "org.apache.skywalking.oap.meter.analyzer.dsl.registry.ProcessRegistry"; private final ClassPool classPool; @@ -82,7 +98,7 @@ public final class LALClassGenerator { final List<ConsumerInfo> consumers = new ArrayList<>(); collectConsumers(model.getStatements(), consumers); - // Phase 2: Compile consumer classes + // Phase 2: Compile consumer classes (recursively handles sub-consumers) final List<Object> consumerInstances = new ArrayList<>(); for (int i = 0; i < consumers.size(); i++) { final String consumerName = className + "_C" + i; @@ -131,7 +147,7 @@ public final class LALClassGenerator { // ==================== Consumer info ==================== - private static final class ConsumerInfo { + private static class ConsumerInfo { final String body; final String castType; final List<ConsumerInfo> subConsumers; @@ -141,6 +157,13 @@ public final class LALClassGenerator { this.castType = castType; this.subConsumers = new ArrayList<>(); } + + ConsumerInfo(final String body, final String castType, + final List<ConsumerInfo> subConsumers) { + this.body = body; + this.castType = castType; + this.subConsumers = new ArrayList<>(subConsumers); + } } // ==================== Phase 1: Collect consumers ==================== @@ -184,19 +207,29 @@ public final class LALClassGenerator { } else if (stmt instanceof LALScriptModel.ExtractorBlock) { final LALScriptModel.ExtractorBlock block = (LALScriptModel.ExtractorBlock) stmt; + final ConsumerInfo info = new ConsumerInfo("", EXTRACTOR_SPEC); final StringBuilder sb = new StringBuilder(); - generateExtractorStatementsFlat(sb, block.getStatements()); - consumers.add(new ConsumerInfo(sb.toString(), - "org.apache.skywalking.oap.log.analyzer.dsl" - + ".spec.extractor.ExtractorSpec")); + final int[] subCounter = {0}; + final List<LALScriptModel.FilterStatement> extractorStmts = new ArrayList<>(); + for (final LALScriptModel.ExtractorStatement es : block.getStatements()) { + extractorStmts.add((LALScriptModel.FilterStatement) es); + } + generateExtractorBody(sb, extractorStmts, info, subCounter); + consumers.add(new ConsumerInfo(sb.toString(), EXTRACTOR_SPEC, + info.subConsumers)); } else if (stmt instanceof LALScriptModel.SinkBlock) { final LALScriptModel.SinkBlock sink = (LALScriptModel.SinkBlock) stmt; if (!sink.getStatements().isEmpty()) { + final ConsumerInfo info = new ConsumerInfo("", SINK_SPEC); final StringBuilder sb = new StringBuilder(); - generateSinkStatementsFlat(sb, sink.getStatements()); - consumers.add(new ConsumerInfo(sb.toString(), - "org.apache.skywalking.oap.log.analyzer.dsl" - + ".spec.sink.SinkSpec")); + final int[] subCounter = {0}; + final List<LALScriptModel.FilterStatement> sinkStmts = new ArrayList<>(); + for (final LALScriptModel.SinkStatement ss : sink.getStatements()) { + sinkStmts.add((LALScriptModel.FilterStatement) ss); + } + generateSinkBody(sb, sinkStmts, info, subCounter); + consumers.add(new ConsumerInfo(sb.toString(), SINK_SPEC, + info.subConsumers)); } } else if (stmt instanceof LALScriptModel.IfBlock) { final LALScriptModel.IfBlock ifBlock = (LALScriptModel.IfBlock) stmt; @@ -207,12 +240,14 @@ public final class LALClassGenerator { } } - // ==================== Flat code for consumer bodies ==================== + // ==================== Extractor body generation ==================== - private void generateExtractorStatementsFlat( + private void generateExtractorBody( final StringBuilder sb, - final List<LALScriptModel.ExtractorStatement> stmts) { - for (final LALScriptModel.ExtractorStatement stmt : stmts) { + final List<? extends LALScriptModel.FilterStatement> stmts, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.FilterStatement stmt : stmts) { if (stmt instanceof LALScriptModel.FieldAssignment) { final LALScriptModel.FieldAssignment field = (LALScriptModel.FieldAssignment) stmt; @@ -227,13 +262,423 @@ public final class LALClassGenerator { } sb.append(");\n"); } else if (stmt instanceof LALScriptModel.TagAssignment) { - final LALScriptModel.TagAssignment tag = - (LALScriptModel.TagAssignment) stmt; - generateTagAssignment(sb, tag); + generateTagAssignment(sb, (LALScriptModel.TagAssignment) stmt); + } else if (stmt instanceof LALScriptModel.IfBlock) { + generateIfBlockInBody(sb, (LALScriptModel.IfBlock) stmt, + parentInfo, subCounter, true); + } else if (stmt instanceof LALScriptModel.MetricsBlock) { + generateMetricsSubConsumer(sb, (LALScriptModel.MetricsBlock) stmt, + parentInfo, subCounter); + } else if (stmt instanceof LALScriptModel.SlowSqlBlock) { + generateSlowSqlSubConsumer(sb, (LALScriptModel.SlowSqlBlock) stmt, + parentInfo, subCounter); + } else if (stmt instanceof LALScriptModel.SampledTraceBlock) { + generateSampledTraceSubConsumer(sb, + (LALScriptModel.SampledTraceBlock) stmt, + parentInfo, subCounter); + } + } + } + + private void generateIfBlockInBody( + final StringBuilder sb, + final LALScriptModel.IfBlock ifBlock, + final ConsumerInfo parentInfo, + final int[] subCounter, + final boolean isExtractorContext) { + sb.append(" if ("); + generateCondition(sb, ifBlock.getCondition()); + sb.append(") {\n"); + if (isExtractorContext) { + generateExtractorBody(sb, ifBlock.getThenBranch(), parentInfo, subCounter); + } else { + generateSinkBody(sb, ifBlock.getThenBranch(), parentInfo, subCounter); + } + sb.append(" }\n"); + if (!ifBlock.getElseBranch().isEmpty()) { + sb.append(" else {\n"); + if (isExtractorContext) { + generateExtractorBody(sb, ifBlock.getElseBranch(), parentInfo, subCounter); + } else { + generateSinkBody(sb, ifBlock.getElseBranch(), parentInfo, subCounter); + } + sb.append(" }\n"); + } + } + + // ==================== Metrics sub-consumer ==================== + + private void generateMetricsSubConsumer( + final StringBuilder sb, + final LALScriptModel.MetricsBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + final int idx = subCounter[0]++; + final StringBuilder body = new StringBuilder(); + if (block.getName() != null) { + body.append(" _t.name(\"") + .append(escapeJava(block.getName())).append("\");\n"); + } + if (block.getTimestampValue() != null) { + body.append(" _t.timestamp("); + generateCastedValueAccess(body, block.getTimestampValue(), + block.getTimestampCast()); + body.append(");\n"); + } + if (!block.getLabels().isEmpty()) { + body.append(" { java.util.Map _labels = new java.util.LinkedHashMap();\n"); + for (final Map.Entry<String, LALScriptModel.TagValue> entry + : block.getLabels().entrySet()) { + body.append(" _labels.put(\"") + .append(escapeJava(entry.getKey())).append("\", "); + generateCastedValueAccess(body, entry.getValue().getValue(), + entry.getValue().getCastType()); + body.append(");\n"); + } + body.append(" _t.labels(_labels); }\n"); + } + if (block.getValue() != null) { + body.append(" _t.value("); + if ("Long".equals(block.getValueCast())) { + body.append("(double) toLong("); + generateValueAccess(body, block.getValue()); + body.append(")"); + } else if ("Integer".equals(block.getValueCast())) { + body.append("(double) toInt("); + generateValueAccess(body, block.getValue()); + body.append(")"); + } else { + // Number literal or untyped value — cast to double for Sample.value(double) + if (block.getValue().isNumberLiteral()) { + body.append("(double) ").append(block.getValue().getSegments().get(0)); + } else { + body.append("((Number) "); + generateValueAccess(body, block.getValue()); + body.append(").doubleValue()"); + } + } + body.append(");\n"); + } + + final ConsumerInfo sub = new ConsumerInfo(body.toString(), SAMPLE_BUILDER); + parentInfo.subConsumers.add(sub); + sb.append(" ((").append(PACKAGE_PREFIX) + .append("BindingAware) this._sub").append(idx) + .append(").setBinding(this.binding);\n"); + sb.append(" _t.metrics(this._sub").append(idx).append(");\n"); + } + + // ==================== SlowSql sub-consumer ==================== + + private void generateSlowSqlSubConsumer( + final StringBuilder sb, + final LALScriptModel.SlowSqlBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + final int idx = subCounter[0]++; + final StringBuilder body = new StringBuilder(); + if (block.getId() != null) { + body.append(" _t.id("); + generateCastedValueAccess(body, block.getId(), block.getIdCast()); + body.append(");\n"); + } + if (block.getStatement() != null) { + body.append(" _t.statement("); + generateCastedValueAccess(body, block.getStatement(), + block.getStatementCast()); + body.append(");\n"); + } + if (block.getLatency() != null) { + body.append(" _t.latency(Long.valueOf(toLong("); + generateValueAccess(body, block.getLatency()); + body.append(")));\n"); + } + + final ConsumerInfo sub = new ConsumerInfo(body.toString(), SLOW_SQL_SPEC); + parentInfo.subConsumers.add(sub); + sb.append(" ((").append(PACKAGE_PREFIX) + .append("BindingAware) this._sub").append(idx) + .append(").setBinding(this.binding);\n"); + sb.append(" _t.slowSql(this._sub").append(idx).append(");\n"); + } + + // ==================== SampledTrace sub-consumer ==================== + + private void generateSampledTraceSubConsumer( + final StringBuilder sb, + final LALScriptModel.SampledTraceBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + final int idx = subCounter[0]++; + final StringBuilder body = new StringBuilder(); + final ConsumerInfo sub = new ConsumerInfo("", SAMPLED_TRACE_SPEC); + final int[] innerSubCounter = {0}; + generateSampledTraceBody(body, block.getStatements(), sub, innerSubCounter); + + // Propagate any sub-sub-consumers + parentInfo.subConsumers.add(new ConsumerInfo(body.toString(), + SAMPLED_TRACE_SPEC, sub.subConsumers)); + sb.append(" ((").append(PACKAGE_PREFIX) + .append("BindingAware) this._sub").append(idx) + .append(").setBinding(this.binding);\n"); + sb.append(" _t.sampledTrace(this._sub").append(idx).append(");\n"); + } + + private void generateSampledTraceBody( + final StringBuilder sb, + final List<LALScriptModel.SampledTraceStatement> stmts, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.SampledTraceStatement stmt : stmts) { + if (stmt instanceof LALScriptModel.SampledTraceField) { + generateSampledTraceField(sb, (LALScriptModel.SampledTraceField) stmt); + } else if (stmt instanceof LALScriptModel.IfBlock) { + generateSampledTraceIfBlock(sb, (LALScriptModel.IfBlock) stmt, + parentInfo, subCounter); + } + } + } + + private void generateSampledTraceField( + final StringBuilder sb, + final LALScriptModel.SampledTraceField field) { + final String methodName; + switch (field.getFieldType()) { + case LATENCY: + methodName = "latency"; + sb.append(" _t.latency(Long.valueOf(toLong("); + generateValueAccess(sb, field.getValue()); + sb.append(")));\n"); + return; + case COMPONENT_ID: + methodName = "componentId"; + sb.append(" _t.componentId(toInt("); + generateValueAccess(sb, field.getValue()); + sb.append("));\n"); + return; + case URI: + methodName = "uri"; + break; + case REASON: + methodName = "reason"; + break; + case PROCESS_ID: + methodName = "processId"; + break; + case DEST_PROCESS_ID: + methodName = "destProcessId"; + break; + case DETECT_POINT: + methodName = "detectPoint"; + break; + case REPORT_SERVICE: + methodName = "reportService"; + break; + default: + return; + } + sb.append(" _t.").append(methodName).append("("); + generateCastedValueAccess(sb, field.getValue(), field.getCastType()); + sb.append(");\n"); + } + + private void generateSampledTraceIfBlock( + final StringBuilder sb, + final LALScriptModel.IfBlock ifBlock, + final ConsumerInfo parentInfo, + final int[] subCounter) { + sb.append(" if ("); + generateCondition(sb, ifBlock.getCondition()); + sb.append(") {\n"); + generateSampledTraceBodyFromFilterStmts(sb, ifBlock.getThenBranch(), + parentInfo, subCounter); + sb.append(" }\n"); + if (!ifBlock.getElseBranch().isEmpty()) { + sb.append(" else {\n"); + generateSampledTraceBodyFromFilterStmts(sb, ifBlock.getElseBranch(), + parentInfo, subCounter); + sb.append(" }\n"); + } + } + + private void generateSampledTraceBodyFromFilterStmts( + final StringBuilder sb, + final List<? extends LALScriptModel.FilterStatement> stmts, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.FilterStatement stmt : stmts) { + if (stmt instanceof LALScriptModel.FieldAssignment) { + // SampledTrace fields (processId, latency, etc.) are parsed as FieldAssignment + generateSampledTraceFieldFromAssignment(sb, + (LALScriptModel.FieldAssignment) stmt); + } else if (stmt instanceof LALScriptModel.IfBlock) { + generateSampledTraceIfBlock(sb, (LALScriptModel.IfBlock) stmt, + parentInfo, subCounter); + } + } + } + + private void generateSampledTraceFieldFromAssignment( + final StringBuilder sb, + final LALScriptModel.FieldAssignment fa) { + // Map FieldType to SampledTraceSpec methods + switch (fa.getFieldType()) { + case TIMESTAMP: + sb.append(" _t.latency(Long.valueOf(toLong("); + generateValueAccess(sb, fa.getValue()); + sb.append(")));\n"); + break; + default: + sb.append(" _t.").append(fa.getFieldType().name().toLowerCase()) + .append("("); + generateCastedValueAccess(sb, fa.getValue(), fa.getCastType()); + sb.append(");\n"); + break; + } + } + + // ==================== Sink body generation ==================== + + private void generateSinkBody( + final StringBuilder sb, + final List<? extends LALScriptModel.FilterStatement> stmts, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.FilterStatement stmt : stmts) { + if (stmt instanceof LALScriptModel.EnforcerStatement) { + sb.append(" _t.enforcer();\n"); + } else if (stmt instanceof LALScriptModel.DropperStatement) { + sb.append(" _t.dropper();\n"); + } else if (stmt instanceof LALScriptModel.SamplerBlock) { + generateSamplerSubConsumer(sb, (LALScriptModel.SamplerBlock) stmt, + parentInfo, subCounter); + } else if (stmt instanceof LALScriptModel.IfBlock) { + generateIfBlockInBody(sb, (LALScriptModel.IfBlock) stmt, + parentInfo, subCounter, false); + } + } + } + + // ==================== Sampler sub-consumer ==================== + + private void generateSamplerSubConsumer( + final StringBuilder sb, + final LALScriptModel.SamplerBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + final int idx = subCounter[0]++; + final StringBuilder body = new StringBuilder(); + final ConsumerInfo sub = new ConsumerInfo("", SAMPLER_SPEC); + final int[] innerSubCounter = {0}; + generateSamplerBody(body, block.getContents(), sub, innerSubCounter); + + parentInfo.subConsumers.add(new ConsumerInfo(body.toString(), + SAMPLER_SPEC, sub.subConsumers)); + sb.append(" ((").append(PACKAGE_PREFIX) + .append("BindingAware) this._sub").append(idx) + .append(").setBinding(this.binding);\n"); + sb.append(" _t.sampler(this._sub").append(idx).append(");\n"); + } + + private void generateSamplerBody( + final StringBuilder sb, + final List<LALScriptModel.SamplerContent> contents, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.SamplerContent content : contents) { + if (content instanceof LALScriptModel.RateLimitBlock) { + generateRateLimitSubConsumer(sb, (LALScriptModel.RateLimitBlock) content, + parentInfo, subCounter); + } else if (content instanceof LALScriptModel.IfBlock) { + generateSamplerIfBlock(sb, (LALScriptModel.IfBlock) content, + parentInfo, subCounter); + } + } + } + + private void generateSamplerIfBlock( + final StringBuilder sb, + final LALScriptModel.IfBlock ifBlock, + final ConsumerInfo parentInfo, + final int[] subCounter) { + sb.append(" if ("); + generateCondition(sb, ifBlock.getCondition()); + sb.append(") {\n"); + generateSamplerBodyFromFilterStmts(sb, ifBlock.getThenBranch(), + parentInfo, subCounter); + sb.append(" }\n"); + if (!ifBlock.getElseBranch().isEmpty()) { + sb.append(" else {\n"); + generateSamplerBodyFromFilterStmts(sb, ifBlock.getElseBranch(), + parentInfo, subCounter); + sb.append(" }\n"); + } + } + + private void generateSamplerBodyFromFilterStmts( + final StringBuilder sb, + final List<? extends LALScriptModel.FilterStatement> stmts, + final ConsumerInfo parentInfo, + final int[] subCounter) { + for (final LALScriptModel.FilterStatement stmt : stmts) { + if (stmt instanceof LALScriptModel.SamplerBlock) { + // SamplerBlock appears in if-branches inside a sampler + generateSamplerSubConsumerInline(sb, + (LALScriptModel.SamplerBlock) stmt, + parentInfo, subCounter); + } else if (stmt instanceof LALScriptModel.IfBlock) { + generateSamplerIfBlock(sb, (LALScriptModel.IfBlock) stmt, + parentInfo, subCounter); } } } + private void generateSamplerSubConsumerInline( + final StringBuilder sb, + final LALScriptModel.SamplerBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + // When a sampler block appears inside an if branch of a sampler, + // generate its contents inline + generateSamplerBody(sb, block.getContents(), parentInfo, subCounter); + } + + private void generateRateLimitSubConsumer( + final StringBuilder sb, + final LALScriptModel.RateLimitBlock block, + final ConsumerInfo parentInfo, + final int[] subCounter) { + final int idx = subCounter[0]++; + final String body = " _t.rpm(" + block.getRpm() + ");\n"; + final ConsumerInfo sub = new ConsumerInfo(body, RATE_LIMITING_SAMPLER); + parentInfo.subConsumers.add(sub); + sb.append(" ((").append(PACKAGE_PREFIX) + .append("BindingAware) this._sub").append(idx) + .append(").setBinding(this.binding);\n"); + + if (block.isIdInterpolated()) { + // Emit string concatenation for interpolated IDs + // e.g. "${log.service}:${parsed?.field}" → + // "" + String.valueOf(binding.log().getService()) + ":" + String.valueOf(...) + sb.append(" _t.rateLimit(\"\""); + for (final LALScriptModel.InterpolationPart part : block.getIdParts()) { + sb.append(" + "); + if (part.isLiteral()) { + sb.append("\"").append(escapeJava(part.getLiteral())).append("\""); + } else { + sb.append("String.valueOf("); + generateValueAccess(sb, part.getExpression()); + sb.append(")"); + } + } + sb.append(", this._sub").append(idx).append(");\n"); + } else { + sb.append(" _t.rateLimit(\"") + .append(escapeJava(block.getId())).append("\", this._sub") + .append(idx).append(");\n"); + } + } + private void generateTagAssignment(final StringBuilder sb, final LALScriptModel.TagAssignment tag) { final Map<String, LALScriptModel.TagValue> tags = tag.getTags(); @@ -262,18 +707,6 @@ public final class LALClassGenerator { } } - private void generateSinkStatementsFlat( - final StringBuilder sb, - final List<LALScriptModel.SinkStatement> stmts) { - for (final LALScriptModel.SinkStatement stmt : stmts) { - if (stmt instanceof LALScriptModel.EnforcerStatement) { - sb.append(" _t.enforcer();\n"); - } else if (stmt instanceof LALScriptModel.DropperStatement) { - sb.append(" _t.dropper();\n"); - } - } - } - // ==================== Phase 2: Compile consumer classes ==================== private Object compileConsumerClass(final String className, @@ -294,17 +727,39 @@ public final class LALClassGenerator { "public " + BINDING + " getBinding() {" + " return this.binding; }", ctClass)); + // Add sub-consumer fields + for (int i = 0; i < info.subConsumers.size(); i++) { + ctClass.addField(CtField.make( + "public java.util.function.Consumer _sub" + i + ";", + ctClass)); + } + addHelperMethods(ctClass); final String method = "public void accept(Object arg) {\n" + " " + info.castType + " _t = (" + info.castType + ") arg;\n" + info.body + "}\n"; + + if (log.isDebugEnabled()) { + log.debug("LAL compile consumer {} body:\n{}", className, method); + } + ctClass.addMethod(CtNewMethod.make(method, ctClass)); final Class<?> clazz = ctClass.toClass(LalExpressionPackageHolder.class); ctClass.detach(); - return clazz.getDeclaredConstructor().newInstance(); + final Object instance = clazz.getDeclaredConstructor().newInstance(); + + // Compile and wire sub-consumers + for (int i = 0; i < info.subConsumers.size(); i++) { + final String subName = className + "_S" + i; + final Object subInstance = compileConsumerClass( + subName, info.subConsumers.get(i)); + clazz.getField("_sub" + i).set(instance, subInstance); + } + + return instance; } // ==================== Phase 4: Generate execute method ==================== @@ -442,6 +897,32 @@ public final class LALClassGenerator { + " return ((Number) obj).doubleValue() != 0;" + " return true;" + "}", ctClass)); + + // tag() value lookup using Binding + ctClass.addMethod(CtNewMethod.make( + "private static String tagValue(" + + BINDING + " b, String key) {" + + " java.util.List dl = b.log().getTags().getDataList();" + + " for (int i = 0; i < dl.size(); i++) {" + + " org.apache.skywalking.apm.network.common.v3" + + ".KeyStringValuePair kv = " + + "(org.apache.skywalking.apm.network.common.v3" + + ".KeyStringValuePair) dl.get(i);" + + " if (key.equals(kv.getKey())) return kv.getValue();" + + " }" + + " return \"\";" + + "}", ctClass)); + + // Safe method call helper + ctClass.addMethod(CtNewMethod.make( + "private static Object safeCall(Object obj, String method) {" + + " if (obj == null) return null;" + + " if (\"toString\".equals(method)) return obj.toString();" + + " if (\"trim\".equals(method)) return obj.toString().trim();" + + " if (\"isEmpty\".equals(method))" + + " return Boolean.valueOf(obj.toString().isEmpty());" + + " return obj.toString();" + + "}", ctClass)); } // ==================== Conditions ==================== @@ -597,6 +1078,45 @@ public final class LALClassGenerator { private void generateValueAccess(final StringBuilder sb, final LALScriptModel.ValueAccess value) { + // Handle function call primaries (e.g., tag("LOG_KIND")) + if (value.getFunctionCallName() != null) { + if ("tag".equals(value.getFunctionCallName()) + && !value.getFunctionCallArgs().isEmpty()) { + // tag("KEY") → tagValue(binding, "KEY") + sb.append("tagValue(binding, \""); + final String key = value.getFunctionCallArgs().get(0) + .getValue().getSegments().get(0); + sb.append(escapeJava(key)).append("\")"); + } else { + // Unknown function call — emit null for safety + sb.append("null"); + } + return; + } + + // Handle string/number literals + if (value.isStringLiteral() && value.getChain().isEmpty()) { + sb.append("\"").append(escapeJava(value.getSegments().get(0))) + .append("\""); + return; + } + if (value.isNumberLiteral() && value.getChain().isEmpty()) { + final String num = value.getSegments().get(0); + // Box number literals so Javassist resolves Object-param methods + if (num.contains(".")) { + sb.append("Double.valueOf(").append(num).append(")"); + } else { + sb.append("Integer.valueOf(").append(num).append(")"); + } + return; + } + + // Handle ProcessRegistry static calls + if (value.isProcessRegistryRef()) { + generateProcessRegistryCall(sb, value); + return; + } + String current; if (value.isParsedRef()) { current = "binding.parsed()"; @@ -622,17 +1142,71 @@ public final class LALClassGenerator { if (seg instanceof LALScriptModel.FieldSegment) { final String name = ((LALScriptModel.FieldSegment) seg).getName(); + // getAt() already handles null → null, so safe nav is free current = "getAt(" + current + ", \"" + escapeJava(name) + "\")"; } else if (seg instanceof LALScriptModel.MethodSegment) { final LALScriptModel.MethodSegment ms = (LALScriptModel.MethodSegment) seg; - current = current + "." + ms.getName() + "()"; + if (ms.isSafeNav()) { + // Safe navigation: null-safe method call + current = "safeCall(" + current + ", \"" + + escapeJava(ms.getName()) + "\")"; + } else { + if (ms.getArguments().isEmpty()) { + current = current + "." + ms.getName() + "()"; + } else { + current = current + "." + ms.getName() + "(" + + generateMethodArgs(ms.getArguments()) + ")"; + } + } } } sb.append(current); } + private void generateProcessRegistryCall( + final StringBuilder sb, + final LALScriptModel.ValueAccess value) { + final List<LALScriptModel.ValueAccessSegment> chain = value.getChain(); + if (chain.isEmpty()) { + sb.append("null"); + return; + } + // Expect exactly one method segment: ProcessRegistry.methodName(args) + final LALScriptModel.ValueAccessSegment seg = chain.get(0); + if (seg instanceof LALScriptModel.MethodSegment) { + final LALScriptModel.MethodSegment ms = + (LALScriptModel.MethodSegment) seg; + sb.append(PROCESS_REGISTRY).append(".") + .append(ms.getName()).append("("); + final List<LALScriptModel.FunctionArg> args = ms.getArguments(); + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + sb.append(", "); + } + generateCastedValueAccess(sb, + args.get(i).getValue(), args.get(i).getCastType()); + } + sb.append(")"); + } else { + sb.append("null"); + } + } + + private String generateMethodArgs( + final List<LALScriptModel.FunctionArg> args) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + sb.append(", "); + } + generateCastedValueAccess(sb, + args.get(i).getValue(), args.get(i).getCastType()); + } + return sb.toString(); + } + // ==================== Utilities ==================== private static String escapeJava(final String s) { diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java index cc3d1d3df0..8609deb66d 100644 --- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java +++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptModel.java @@ -245,12 +245,44 @@ public final class LALScriptModel { @Getter public static final class RateLimitBlock implements SamplerContent { private final String id; + private final List<InterpolationPart> idParts; private final long rpm; - public RateLimitBlock(final String id, final long rpm) { + public RateLimitBlock(final String id, + final List<InterpolationPart> idParts, + final long rpm) { this.id = id; + this.idParts = idParts != null + ? Collections.unmodifiableList(idParts) : Collections.emptyList(); this.rpm = rpm; } + + public boolean isIdInterpolated() { + return !idParts.isEmpty(); + } + } + + @Getter + public static final class InterpolationPart { + private final String literal; + private final ValueAccess expression; + + private InterpolationPart(final String literal, final ValueAccess expression) { + this.literal = literal; + this.expression = expression; + } + + public static InterpolationPart ofLiteral(final String text) { + return new InterpolationPart(text, null); + } + + public static InterpolationPart ofExpression(final ValueAccess expr) { + return new InterpolationPart(null, expr); + } + + public boolean isLiteral() { + return literal != null; + } } public static final class EnforcerStatement implements SinkStatement, FilterStatement { @@ -341,17 +373,41 @@ public final class LALScriptModel { private final List<String> segments; private final boolean parsedRef; private final boolean logRef; + private final boolean processRegistryRef; + private final boolean stringLiteral; + private final boolean numberLiteral; private final List<ValueAccessSegment> chain; + private final String functionCallName; + private final List<FunctionArg> functionCallArgs; public ValueAccess(final List<String> segments, final boolean parsedRef, final boolean logRef, final List<ValueAccessSegment> chain) { + this(segments, parsedRef, logRef, false, false, false, + chain, null, Collections.emptyList()); + } + + public ValueAccess(final List<String> segments, + final boolean parsedRef, + final boolean logRef, + final boolean processRegistryRef, + final boolean stringLiteral, + final boolean numberLiteral, + final List<ValueAccessSegment> chain, + final String functionCallName, + final List<FunctionArg> functionCallArgs) { this.segments = Collections.unmodifiableList(segments); this.parsedRef = parsedRef; this.logRef = logRef; + this.processRegistryRef = processRegistryRef; + this.stringLiteral = stringLiteral; + this.numberLiteral = numberLiteral; this.chain = chain != null ? Collections.unmodifiableList(chain) : Collections.emptyList(); + this.functionCallName = functionCallName; + this.functionCallArgs = functionCallArgs != null + ? Collections.unmodifiableList(functionCallArgs) : Collections.emptyList(); } public String toPathString() { @@ -359,6 +415,17 @@ public final class LALScriptModel { } } + @Getter + public static final class FunctionArg { + private final ValueAccess value; + private final String castType; + + public FunctionArg(final ValueAccess value, final String castType) { + this.value = value; + this.castType = castType; + } + } + public interface ValueAccessSegment { } @@ -376,12 +443,14 @@ public final class LALScriptModel { @Getter public static final class MethodSegment implements ValueAccessSegment { private final String name; - private final List<String> arguments; + private final List<FunctionArg> arguments; private final boolean safeNav; - public MethodSegment(final String name, final List<String> arguments, final boolean safeNav) { + public MethodSegment(final String name, final List<FunctionArg> arguments, + final boolean safeNav) { this.name = name; - this.arguments = Collections.unmodifiableList(arguments); + this.arguments = arguments != null + ? Collections.unmodifiableList(arguments) : Collections.emptyList(); this.safeNav = safeNav; } } diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java index 9bd9d241c1..2793ed4dde 100644 --- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java +++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParser.java @@ -29,6 +29,7 @@ import org.antlr.v4.runtime.Recognizer; import org.apache.skywalking.lal.rt.grammar.LALLexer; import org.apache.skywalking.lal.rt.grammar.LALParser; import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.AbortStatement; +import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.InterpolationPart; import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.CompareOp; import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.ComparisonCondition; import org.apache.skywalking.oap.log.analyzer.compiler.LALScriptModel.Condition; @@ -428,7 +429,8 @@ public final class LALScriptParser { for (final LALParser.RateLimitBlockContext rlc : ctx.samplerContent().rateLimitBlock()) { final String id = stripQuotes(rlc.rateLimitId().getText()); final long rpm = Long.parseLong(rlc.rateLimitContent().NUMBER().getText()); - contents.add(new RateLimitBlock(id, rpm)); + final List<InterpolationPart> idParts = parseInterpolation(id); + contents.add(new RateLimitBlock(id, idParts, rpm)); } for (final LALParser.IfStatementContext isc : ctx.samplerContent().ifStatement()) { contents.add((SamplerContent) visitIfStatement(isc)); @@ -439,17 +441,45 @@ public final class LALScriptParser { // ==================== If statement ==================== private static IfBlock visitIfStatement(final LALParser.IfStatementContext ctx) { - final Condition condition = visitCondition(ctx.condition(0)); - final List<FilterStatement> thenBranch = visitIfBody(ctx.ifBody(0)); - - List<FilterStatement> elseBranch = null; - // Handle else-if and else branches + final int condCount = ctx.condition().size(); final int bodyCount = ctx.ifBody().size(); - if (bodyCount > 1) { - elseBranch = visitIfBody(ctx.ifBody(bodyCount - 1)); + // Whether there is a trailing else (no condition) block + final boolean hasElse = bodyCount > condCount; + + // Build the chain from the last else-if backwards. + // For: if(A){b0} else if(B){b1} else if(C){b2} else{b3} + // condCount=3, bodyCount=4, hasElse=true + // Result: IfBlock(A, b0, IfBlock(B, b1, IfBlock(C, b2, b3))) + + // Start from the innermost else-if (last condition) + List<FilterStatement> trailingElse = hasElse + ? visitIfBody(ctx.ifBody(bodyCount - 1)) : null; + + // Build from the last condition backwards to index 1 + IfBlock nested = null; + for (int i = condCount - 1; i >= 1; i--) { + final Condition cond = visitCondition(ctx.condition(i)); + final List<FilterStatement> body = visitIfBody(ctx.ifBody(i)); + final List<FilterStatement> elsePart; + if (nested != null) { + elsePart = List.of(nested); + } else { + elsePart = trailingElse; + } + nested = new IfBlock(cond, body, elsePart); + } + + // Build the outermost if block (index 0) + final Condition topCond = visitCondition(ctx.condition(0)); + final List<FilterStatement> topBody = visitIfBody(ctx.ifBody(0)); + final List<FilterStatement> topElse; + if (nested != null) { + topElse = List.of(nested); + } else { + topElse = trailingElse; } - return new IfBlock(condition, thenBranch, elseBranch); + return new IfBlock(topCond, topBody, topElse); } private static List<FilterStatement> visitIfBody(final LALParser.IfBodyContext ctx) { @@ -541,8 +571,11 @@ public final class LALScriptParser { if (leftCtx instanceof LALParser.CondFunctionCallContext) { final LALParser.FunctionInvocationContext fi = ((LALParser.CondFunctionCallContext) leftCtx).functionInvocation(); + final String funcName = fi.functionName().getText(); + final List<LALScriptModel.FunctionArg> funcArgs = visitFunctionArgs(fi); final ValueAccess left = new ValueAccess( - List.of(fi.getText()), false, false, List.of()); + List.of(fi.getText()), false, false, false, false, false, + List.of(), funcName, funcArgs); return new ComparisonCondition(left, null, op, visitConditionExprAsValue(rightCtx)); } @@ -582,6 +615,16 @@ public final class LALScriptParser { final String cast = va.typeCast() != null ? extractCastType(va.typeCast()) : null; return new ExprCondition(visitValueAccess(va.valueAccess()), cast); } + if (ctx instanceof LALParser.CondFunctionCallContext) { + final LALParser.FunctionInvocationContext fi = + ((LALParser.CondFunctionCallContext) ctx).functionInvocation(); + final String funcName = fi.functionName().getText(); + final List<LALScriptModel.FunctionArg> funcArgs = visitFunctionArgs(fi); + final ValueAccess va = new ValueAccess( + List.of(fi.getText()), false, false, false, false, false, + List.of(), funcName, funcArgs); + return new ExprCondition(va, null); + } return new ExprCondition( new ValueAccess(List.of(ctx.getText()), false, false, List.of()), null); } @@ -592,6 +635,11 @@ public final class LALScriptParser { final List<String> segments = new ArrayList<>(); boolean parsedRef = false; boolean logRef = false; + boolean processRegistryRef = false; + boolean stringLiteral = false; + boolean numberLiteral = false; + String functionCallName = null; + List<LALScriptModel.FunctionArg> functionCallArgs = null; final LALParser.ValueAccessPrimaryContext primary = ctx.valueAccessPrimary(); if (primary instanceof LALParser.ValueParsedContext) { @@ -601,17 +649,22 @@ public final class LALScriptParser { logRef = true; segments.add("log"); } else if (primary instanceof LALParser.ValueProcessRegistryContext) { + processRegistryRef = true; segments.add("ProcessRegistry"); } else if (primary instanceof LALParser.ValueIdentifierContext) { segments.add(((LALParser.ValueIdentifierContext) primary).IDENTIFIER().getText()); } else if (primary instanceof LALParser.ValueStringContext) { + stringLiteral = true; segments.add(stripQuotes( ((LALParser.ValueStringContext) primary).STRING().getText())); } else if (primary instanceof LALParser.ValueNumberContext) { + numberLiteral = true; segments.add(((LALParser.ValueNumberContext) primary).NUMBER().getText()); } else if (primary instanceof LALParser.ValueFunctionCallContext) { final LALParser.FunctionInvocationContext fi = ((LALParser.ValueFunctionCallContext) primary).functionInvocation(); + functionCallName = fi.functionName().getText(); + functionCallArgs = visitFunctionArgs(fi); segments.add(fi.getText()); } else { segments.add(primary.getText()); @@ -634,17 +687,57 @@ public final class LALScriptParser { ((LALParser.SegmentMethodContext) seg).functionInvocation(); segments.add(fi.functionName().getText() + "()"); chain.add(new LALScriptModel.MethodSegment( - fi.functionName().getText(), List.of(), false)); + fi.functionName().getText(), visitFunctionArgs(fi), false)); } else if (seg instanceof LALParser.SegmentSafeMethodContext) { final LALParser.FunctionInvocationContext fi = ((LALParser.SegmentSafeMethodContext) seg).functionInvocation(); segments.add(fi.functionName().getText() + "()"); chain.add(new LALScriptModel.MethodSegment( - fi.functionName().getText(), List.of(), true)); + fi.functionName().getText(), visitFunctionArgs(fi), true)); } } - return new ValueAccess(segments, parsedRef, logRef, chain); + return new ValueAccess(segments, parsedRef, logRef, + processRegistryRef, stringLiteral, numberLiteral, + chain, functionCallName, functionCallArgs); + } + + private static List<LALScriptModel.FunctionArg> visitFunctionArgs( + final LALParser.FunctionInvocationContext fi) { + if (fi.functionArgList() == null) { + return List.of(); + } + final List<LALScriptModel.FunctionArg> args = new ArrayList<>(); + for (final LALParser.FunctionArgContext fac : fi.functionArgList().functionArg()) { + if (fac.valueAccess() != null) { + final ValueAccess va = visitValueAccess(fac.valueAccess()); + final String cast = fac.typeCast() != null + ? extractCastType(fac.typeCast()) : null; + args.add(new LALScriptModel.FunctionArg(va, cast)); + } else if (fac.STRING() != null) { + final String val = stripQuotes(fac.STRING().getText()); + final ValueAccess va = new ValueAccess( + List.of(val), false, false, true, true, false, + List.of(), null, null); + args.add(new LALScriptModel.FunctionArg(va, null)); + } else if (fac.NUMBER() != null) { + final ValueAccess va = new ValueAccess( + List.of(fac.NUMBER().getText()), false, false, + false, false, true, List.of(), null, null); + args.add(new LALScriptModel.FunctionArg(va, null)); + } else if (fac.boolValue() != null) { + final ValueAccess va = new ValueAccess( + List.of(fac.boolValue().getText()), false, false, + false, false, false, List.of(), null, null); + args.add(new LALScriptModel.FunctionArg(va, null)); + } else { + // NULL + final ValueAccess va = new ValueAccess( + List.of("null"), false, false, List.of()); + args.add(new LALScriptModel.FunctionArg(va, null)); + } + } + return args; } private static String resolveValueAsString(final LALParser.ValueAccessContext ctx) { @@ -694,4 +787,78 @@ public final class LALScriptParser { } return s.substring(0, maxLen) + "..."; } + + // ==================== GString interpolation ==================== + + /** + * Parses Groovy-style GString interpolation in a string. + * E.g. {@code "${log.service}:${parsed?.field}"} produces + * [expr(log.service), literal(":"), expr(parsed?.field)]. + * + * @return list of parts, or {@code null} if no interpolation found + */ + static List<InterpolationPart> parseInterpolation(final String s) { + if (!s.contains("${")) { + return null; + } + final List<InterpolationPart> parts = new ArrayList<>(); + int pos = 0; + while (pos < s.length()) { + final int start = s.indexOf("${", pos); + if (start < 0) { + // Remaining literal text + if (pos < s.length()) { + parts.add(InterpolationPart.ofLiteral(s.substring(pos))); + } + break; + } + // Literal text before ${ + if (start > pos) { + parts.add(InterpolationPart.ofLiteral(s.substring(pos, start))); + } + // Find matching closing brace, respecting nesting + int depth = 1; + int i = start + 2; + while (i < s.length() && depth > 0) { + final char c = s.charAt(i); + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + } + i++; + } + if (depth != 0) { + throw new IllegalArgumentException( + "Unclosed interpolation in: " + s); + } + final String expr = s.substring(start + 2, i - 1); + // Parse the expression as a valueAccess through ANTLR + parts.add(InterpolationPart.ofExpression(parseValueAccessExpr(expr))); + pos = i; + } + return parts; + } + + /** + * Parses a standalone valueAccess expression string by wrapping it in + * a minimal LAL script and extracting the parsed ValueAccess. + */ + private static ValueAccess parseValueAccessExpr(final String expr) { + // Wrap in: filter { if (EXPR) { sink {} } } + // The expression becomes a condition, parsed as ExprCondition + // whose ValueAccess is what we want. + final String wrapper = "filter { if (" + expr + ") { sink {} } }"; + final LALScriptModel model = parse(wrapper); + final IfBlock ifBlock = (IfBlock) model.getStatements().get(0); + final LALScriptModel.Condition cond = ifBlock.getCondition(); + if (cond instanceof ExprCondition) { + return ((ExprCondition) cond).getExpr(); + } + if (cond instanceof ComparisonCondition) { + return ((ComparisonCondition) cond).getLeft(); + } + throw new IllegalArgumentException( + "Cannot parse interpolation expression: " + expr); + } } diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java index 11cee26a83..a1a10209e5 100644 --- a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java +++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALClassGeneratorTest.java @@ -22,8 +22,11 @@ import org.apache.skywalking.oap.log.analyzer.dsl.LalExpression; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class LALClassGeneratorTest { @@ -127,4 +130,459 @@ class LALClassGeneratorTest { assertThrows(Exception.class, () -> generator.compile("filter { invalid {} }")); } + + // ==================== tag() function in conditions ==================== + + @Test + void compileTagFunctionInCondition() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n" + + " sink {}\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void generateSourceTagFunctionEmitsTagValue() { + final String source = generator.generateSource( + "filter {\n" + + " if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n" + + " sink {}\n" + + " }\n" + + "}"); + // Should use tagValue helper, not emit null + assertTrue(source.contains("tagValue(binding, \"LOG_KIND\")"), + "Expected tagValue call but got: " + source); + assertTrue(source.contains("SLOW_SQL")); + } + + @Test + void compileTagFunctionNestedInExtractor() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " if (tag(\"LOG_KIND\") == \"NET_PROFILING_SAMPLED_TRACE\") {\n" + + " service parsed.service as String\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== Safe navigation ==================== + + @Test + void compileSafeNavigationFieldAccess() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service parsed?.response?.service as String\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileSafeNavigationMethodCalls() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service parsed?.flags?.toString()?.trim() as String\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void generateSourceSafeNavMethodEmitsSafeCall() { + final String source = generator.generateSource( + "filter {\n" + + " if (parsed?.flags?.toString()) {\n" + + " sink {}\n" + + " }\n" + + "}"); + // Safe method calls should use safeCall helper + assertTrue(source.contains("safeCall("), + "Expected safeCall for safe nav method but got: " + source); + } + + // ==================== ProcessRegistry static calls ==================== + + @Test + void compileProcessRegistryCall() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service ProcessRegistry.generateVirtualLocalProcess(" + + "parsed.service as String, parsed.serviceInstance as String" + + ") as String\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileProcessRegistryWithThreeArgs() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service ProcessRegistry.generateVirtualRemoteProcess(" + + "parsed.service as String, parsed.serviceInstance as String, " + + "parsed.address as String) as String\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== Metrics block ==================== + + @Test + void compileMetricsBlock() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " metrics {\n" + + " timestamp log.timestamp as Long\n" + + " labels level: parsed.level, service: log.service\n" + + " name \"nginx_error_log_count\"\n" + + " value 1\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== SlowSql block ==================== + + @Test + void compileSlowSqlBlock() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " slowSql {\n" + + " id parsed.id as String\n" + + " statement parsed.statement as String\n" + + " latency parsed.query_time as Long\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== SampledTrace block ==================== + + @Test + void compileSampledTraceBlock() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " sampledTrace {\n" + + " latency parsed.latency as Long\n" + + " uri parsed.uri as String\n" + + " reason parsed.reason as String\n" + + " detectPoint parsed.detect_point as String\n" + + " componentId 49\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileSampledTraceWithIfBlocks() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " sampledTrace {\n" + + " latency parsed.latency as Long\n" + + " if (parsed.client_process.process_id as String != \"\") {\n" + + " processId parsed.client_process.process_id as String\n" + + " } else {\n" + + " processId parsed.fallback as String\n" + + " }\n" + + " detectPoint parsed.detect_point as String\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== Sampler / rateLimit ==================== + + @Test + void compileSamplerWithRateLimit() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit('service:error') {\n" + + " rpm 6000\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileSamplerWithInterpolatedId() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit(\"${log.service}:${parsed.code}\") {\n" + + " rpm 6000\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void parseInterpolatedIdParts() { + // Verify the parser correctly splits interpolated strings + final java.util.List<LALScriptModel.InterpolationPart> parts = + LALScriptParser.parseInterpolation( + "${log.service}:${parsed.code}"); + assertNotNull(parts); + // expr, literal ":", expr + assertEquals(3, parts.size()); + assertFalse(parts.get(0).isLiteral()); + assertTrue(parts.get(0).getExpression().isLogRef()); + assertTrue(parts.get(1).isLiteral()); + assertEquals(":", parts.get(1).getLiteral()); + assertFalse(parts.get(2).isLiteral()); + assertTrue(parts.get(2).getExpression().isParsedRef()); + } + + @Test + void compileSamplerWithSafeNavInterpolatedId() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit(\"${log.service}:${parsed?.commonProperties?.responseFlags?.toString()}\") {\n" + + " rpm 6000\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileSamplerWithIfAndRateLimit() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " sink {\n" + + " sampler {\n" + + " if (parsed?.error) {\n" + + " rateLimit('svc:err') {\n" + + " rpm 6000\n" + + " }\n" + + " } else {\n" + + " rateLimit('svc:ok') {\n" + + " rpm 3000\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + // ==================== If blocks in extractor/sink ==================== + + @Test + void compileIfInsideExtractor() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " if (parsed?.status) {\n" + + " tag 'http.status_code': parsed.status\n" + + " }\n" + + " tag 'response.flag': parsed.flags\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileIfInsideExtractorWithTagCondition() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " if (tag(\"LOG_KIND\") == \"NET_PROFILING\") {\n" + + " service parsed.service as String\n" + + " layer parsed.layer as String\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== Complex production-like rules ==================== + + @Test + void compileNginxAccessLogRule() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " if (tag(\"LOG_KIND\") == \"NGINX_ACCESS_LOG\") {\n" + + " text {\n" + + " regexp '.+\"(?<request>.+)\"(?<status>\\\\d{3}).+'\n" + + " }\n" + + " extractor {\n" + + " if (parsed.status) {\n" + + " tag 'http.status_code': parsed.status\n" + + " }\n" + + " }\n" + + " sink {}\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileSlowSqlProductionRule() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n" + + " layer parsed.layer as String\n" + + " service parsed.service as String\n" + + " timestamp parsed.time as String\n" + + " slowSql {\n" + + " id parsed.id as String\n" + + " statement parsed.statement as String\n" + + " latency parsed.query_time as Long\n" + + " }\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileEnvoyAlsAbortRule() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " if (parsed?.response?.responseCode?.value as Integer < 400" + + " && !parsed?.commonProperties?.responseFlags?.toString()?.trim()) {\n" + + " abort {}\n" + + " }\n" + + " extractor {\n" + + " if (parsed?.response?.responseCode) {\n" + + " tag 'status.code': parsed?.response?.responseCode?.value\n" + + " }\n" + + " tag 'response.flag': parsed?.commonProperties?.responseFlags\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + // ==================== Else-if chain ==================== + + @Test + void compileElseIfChain() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " if (parsed.a) {\n" + + " sink {}\n" + + " } else if (parsed.b) {\n" + + " sink {}\n" + + " } else if (parsed.c) {\n" + + " sink {}\n" + + " } else {\n" + + " sink {}\n" + + " }\n" + + "}"); + assertNotNull(expr); + } + + @Test + void compileElseIfInSampledTrace() throws Exception { + final LalExpression expr = generator.compile( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " sampledTrace {\n" + + " latency parsed.latency as Long\n" + + " if (parsed.client_process.process_id as String != \"\") {\n" + + " processId parsed.client_process.process_id as String\n" + + " } else if (parsed.client_process.local as Boolean) {\n" + + " processId ProcessRegistry.generateVirtualLocalProcess(" + + "parsed.service as String, parsed.serviceInstance as String) as String\n" + + " } else {\n" + + " processId ProcessRegistry.generateVirtualRemoteProcess(" + + "parsed.service as String, parsed.serviceInstance as String, " + + "parsed.client_process.address as String) as String\n" + + " }\n" + + " detectPoint parsed.detect_point as String\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + assertNotNull(expr); + } + + @Test + void generateSourceElseIfEmitsNestedBranches() { + final String source = generator.generateSource( + "filter {\n" + + " if (parsed.a) {\n" + + " sink {}\n" + + " } else if (parsed.b) {\n" + + " sink {}\n" + + " } else {\n" + + " sink {}\n" + + " }\n" + + "}"); + // The else-if should produce a nested if inside else + assertTrue(source.contains("else"), + "Expected else branch but got: " + source); + // Both condition branches should appear + int ifCount = 0; + for (int i = 0; i < source.length() - 2; i++) { + if (source.substring(i, i + 3).equals("if ")) { + ifCount++; + } + } + assertTrue(ifCount >= 2, + "Expected at least 2 if-conditions for else-if chain but got " + + ifCount + " in: " + source); + } } diff --git a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java index 5c29397f8c..8760ca4f6c 100644 --- a/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java +++ b/oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/compiler/LALScriptParserTest.java @@ -20,6 +20,7 @@ package org.apache.skywalking.oap.log.analyzer.compiler; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -165,6 +166,65 @@ class LALScriptParserTest { assertEquals(6000, rateLimit.getRpm()); } + @Test + void parseInterpolatedRateLimitId() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit(\"${log.service}:${parsed.code}\") {\n" + + " rpm 3000\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + + final LALScriptModel.SinkBlock sink = + (LALScriptModel.SinkBlock) model.getStatements().get(0); + final LALScriptModel.SamplerBlock sampler = + (LALScriptModel.SamplerBlock) sink.getStatements().get(0); + final LALScriptModel.RateLimitBlock rl = + (LALScriptModel.RateLimitBlock) sampler.getContents().get(0); + + assertTrue(rl.isIdInterpolated()); + assertEquals(3, rl.getIdParts().size()); + + // Part 0: expression ${log.service} + assertFalse(rl.getIdParts().get(0).isLiteral()); + assertTrue(rl.getIdParts().get(0).getExpression().isLogRef()); + + // Part 1: literal ":" + assertTrue(rl.getIdParts().get(1).isLiteral()); + assertEquals(":", rl.getIdParts().get(1).getLiteral()); + + // Part 2: expression ${parsed.code} + assertFalse(rl.getIdParts().get(2).isLiteral()); + assertTrue(rl.getIdParts().get(2).getExpression().isParsedRef()); + } + + @Test + void parsePlainRateLimitIdNotInterpolated() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " sink {\n" + + " sampler {\n" + + " rateLimit('service:error') {\n" + + " rpm 6000\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + + final LALScriptModel.SinkBlock sink = + (LALScriptModel.SinkBlock) model.getStatements().get(0); + final LALScriptModel.SamplerBlock sampler = + (LALScriptModel.SamplerBlock) sink.getStatements().get(0); + final LALScriptModel.RateLimitBlock rl = + (LALScriptModel.RateLimitBlock) sampler.getContents().get(0); + + assertFalse(rl.isIdInterpolated()); + } + @Test void parseIfCondition() { final LALScriptModel model = LALScriptParser.parse( @@ -184,9 +244,286 @@ class LALScriptParserTest { assertEquals(2, ifBlock.getThenBranch().size()); } + @Test + void parseElseIfChain() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " if (parsed.a) {\n" + + " sink {}\n" + + " } else if (parsed.b) {\n" + + " sink {}\n" + + " } else if (parsed.c) {\n" + + " sink {}\n" + + " } else {\n" + + " sink {}\n" + + " }\n" + + "}"); + + assertEquals(1, model.getStatements().size()); + final LALScriptModel.IfBlock top = + (LALScriptModel.IfBlock) model.getStatements().get(0); + assertNotNull(top.getCondition()); + assertEquals(1, top.getThenBranch().size()); + + // else branch contains a nested IfBlock for "else if (parsed.b)" + assertEquals(1, top.getElseBranch().size()); + final LALScriptModel.IfBlock elseIf1 = + (LALScriptModel.IfBlock) top.getElseBranch().get(0); + assertNotNull(elseIf1.getCondition()); + assertEquals(1, elseIf1.getThenBranch().size()); + + // nested else branch contains another IfBlock for "else if (parsed.c)" + assertEquals(1, elseIf1.getElseBranch().size()); + final LALScriptModel.IfBlock elseIf2 = + (LALScriptModel.IfBlock) elseIf1.getElseBranch().get(0); + assertNotNull(elseIf2.getCondition()); + assertEquals(1, elseIf2.getThenBranch().size()); + + // innermost else branch is the final else body + assertEquals(1, elseIf2.getElseBranch().size()); + assertInstanceOf(LALScriptModel.SinkBlock.class, elseIf2.getElseBranch().get(0)); + } + + @Test + void parseElseIfWithoutFinalElse() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " if (parsed.a) {\n" + + " sink {}\n" + + " } else if (parsed.b) {\n" + + " sink {}\n" + + " }\n" + + "}"); + + final LALScriptModel.IfBlock top = + (LALScriptModel.IfBlock) model.getStatements().get(0); + assertEquals(1, top.getElseBranch().size()); + final LALScriptModel.IfBlock elseIf = + (LALScriptModel.IfBlock) top.getElseBranch().get(0); + assertNotNull(elseIf.getCondition()); + assertTrue(elseIf.getElseBranch().isEmpty()); + } + @Test void parseSyntaxErrorThrows() { assertThrows(IllegalArgumentException.class, () -> LALScriptParser.parse("filter {")); } + + // ==================== Function call parsing ==================== + + @Test + void parseTagFunctionCallInCondition() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " if (tag(\"LOG_KIND\") == \"SLOW_SQL\") {\n" + + " sink {}\n" + + " }\n" + + "}"); + + final LALScriptModel.IfBlock ifBlock = + (LALScriptModel.IfBlock) model.getStatements().get(0); + final LALScriptModel.ComparisonCondition cond = + (LALScriptModel.ComparisonCondition) ifBlock.getCondition(); + + // Left side should be a function call + final LALScriptModel.ValueAccess left = cond.getLeft(); + assertEquals("tag", left.getFunctionCallName()); + assertEquals(1, left.getFunctionCallArgs().size()); + assertEquals("LOG_KIND", + left.getFunctionCallArgs().get(0).getValue().getSegments().get(0)); + + // Right side should be a string value (parsed as ValueAccess with stringLiteral flag) + assertInstanceOf(LALScriptModel.ValueAccessConditionValue.class, cond.getRight()); + final LALScriptModel.ValueAccessConditionValue rightVal = + (LALScriptModel.ValueAccessConditionValue) cond.getRight(); + assertTrue(rightVal.getValue().isStringLiteral()); + assertEquals("SLOW_SQL", rightVal.getValue().getSegments().get(0)); + } + + @Test + void parseTagFunctionCallAsSingleCondition() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " if (tag(\"LOG_KIND\")) {\n" + + " sink {}\n" + + " }\n" + + "}"); + + final LALScriptModel.IfBlock ifBlock = + (LALScriptModel.IfBlock) model.getStatements().get(0); + final LALScriptModel.ExprCondition cond = + (LALScriptModel.ExprCondition) ifBlock.getCondition(); + assertEquals("tag", cond.getExpr().getFunctionCallName()); + assertEquals(1, cond.getExpr().getFunctionCallArgs().size()); + } + + // ==================== Safe navigation parsing ==================== + + @Test + void parseSafeNavigationFields() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " extractor {\n" + + " service parsed?.response?.service as String\n" + + " }\n" + + " sink {}\n" + + "}"); + + final LALScriptModel.ExtractorBlock extractor = + (LALScriptModel.ExtractorBlock) model.getStatements().get(0); + final LALScriptModel.FieldAssignment field = + (LALScriptModel.FieldAssignment) extractor.getStatements().get(0); + + assertTrue(field.getValue().isParsedRef()); + assertEquals(2, field.getValue().getChain().size()); + assertTrue(((LALScriptModel.FieldSegment) field.getValue().getChain().get(0)) + .isSafeNav()); + assertTrue(((LALScriptModel.FieldSegment) field.getValue().getChain().get(1)) + .isSafeNav()); + } + + @Test + void parseSafeNavigationMethods() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " extractor {\n" + + " service parsed?.flags?.toString()?.trim() as String\n" + + " }\n" + + " sink {}\n" + + "}"); + + final LALScriptModel.ExtractorBlock extractor = + (LALScriptModel.ExtractorBlock) model.getStatements().get(0); + final LALScriptModel.FieldAssignment field = + (LALScriptModel.FieldAssignment) extractor.getStatements().get(0); + + assertEquals(3, field.getValue().getChain().size()); + // flags is a safe field + assertInstanceOf(LALScriptModel.FieldSegment.class, + field.getValue().getChain().get(0)); + assertTrue(((LALScriptModel.FieldSegment) field.getValue().getChain().get(0)) + .isSafeNav()); + // toString() is a safe method + assertInstanceOf(LALScriptModel.MethodSegment.class, + field.getValue().getChain().get(1)); + assertTrue(((LALScriptModel.MethodSegment) field.getValue().getChain().get(1)) + .isSafeNav()); + assertEquals("toString", + ((LALScriptModel.MethodSegment) field.getValue().getChain().get(1)).getName()); + // trim() is a safe method + assertTrue(((LALScriptModel.MethodSegment) field.getValue().getChain().get(2)) + .isSafeNav()); + assertEquals("trim", + ((LALScriptModel.MethodSegment) field.getValue().getChain().get(2)).getName()); + } + + // ==================== Method argument parsing ==================== + + @Test + void parseMethodWithArguments() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " service ProcessRegistry.generateVirtualLocalProcess(" + + "parsed.service as String, parsed.instance as String) as String\n" + + " }\n" + + " sink {}\n" + + "}"); + + final LALScriptModel.ExtractorBlock extractor = + (LALScriptModel.ExtractorBlock) model.getStatements().get(1); + final LALScriptModel.FieldAssignment field = + (LALScriptModel.FieldAssignment) extractor.getStatements().get(0); + + assertTrue(field.getValue().isProcessRegistryRef()); + assertEquals(1, field.getValue().getChain().size()); + + final LALScriptModel.MethodSegment method = + (LALScriptModel.MethodSegment) field.getValue().getChain().get(0); + assertEquals("generateVirtualLocalProcess", method.getName()); + assertEquals(2, method.getArguments().size()); + assertTrue(method.getArguments().get(0).getValue().isParsedRef()); + assertEquals("String", method.getArguments().get(0).getCastType()); + assertTrue(method.getArguments().get(1).getValue().isParsedRef()); + assertEquals("String", method.getArguments().get(1).getCastType()); + } + + // ==================== Sampled trace parsing ==================== + + @Test + void parseSampledTrace() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " sampledTrace {\n" + + " latency parsed.latency as Long\n" + + " uri parsed.uri as String\n" + + " reason parsed.reason as String\n" + + " detectPoint parsed.detect_point as String\n" + + " componentId 49\n" + + " }\n" + + " }\n" + + " sink {}\n" + + "}"); + + final LALScriptModel.ExtractorBlock extractor = + (LALScriptModel.ExtractorBlock) model.getStatements().get(1); + final LALScriptModel.SampledTraceBlock st = + (LALScriptModel.SampledTraceBlock) extractor.getStatements().get(0); + assertEquals(5, st.getStatements().size()); + } + + // ==================== If in extractor/sink parsing ==================== + + @Test + void parseIfInsideExtractor() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " json {}\n" + + " extractor {\n" + + " if (parsed.status) {\n" + + " tag 'http.status_code': parsed.status\n" + + " }\n" + + " tag 'response.flag': parsed.flags\n" + + " }\n" + + " sink {}\n" + + "}"); + + final LALScriptModel.ExtractorBlock extractor = + (LALScriptModel.ExtractorBlock) model.getStatements().get(1); + assertEquals(2, extractor.getStatements().size()); + assertInstanceOf(LALScriptModel.IfBlock.class, extractor.getStatements().get(0)); + assertInstanceOf(LALScriptModel.TagAssignment.class, extractor.getStatements().get(1)); + } + + @Test + void parseIfInsideSink() { + final LALScriptModel model = LALScriptParser.parse( + "filter {\n" + + " sink {\n" + + " sampler {\n" + + " if (parsed.error) {\n" + + " rateLimit('svc:err') {\n" + + " rpm 6000\n" + + " }\n" + + " } else {\n" + + " rateLimit('svc:ok') {\n" + + " rpm 3000\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"); + + final LALScriptModel.SinkBlock sink = + (LALScriptModel.SinkBlock) model.getStatements().get(0); + final LALScriptModel.SamplerBlock sampler = + (LALScriptModel.SamplerBlock) sink.getStatements().get(0); + // The sampler has one if-block as content + assertEquals(1, sampler.getContents().size()); + assertInstanceOf(LALScriptModel.IfBlock.class, sampler.getContents().get(0)); + } }
