You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/04/15 10:14:57 UTC

(skywalking) 02/05: LAL: add layer:auto mode for dynamic layer assignment

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch feature/swip11-general-enhancements
in repository https://gitbox.apache.org/repos/asf/skywalking.git

commit 6786f833743b08d2ad59a13d5fb3947003378ae6
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Wed Apr 15 17:27:22 2026 +0800

    LAL: add layer:auto mode for dynamic layer assignment
    
    When a LAL rule declares layer: auto, it matches logs where
    service.layer is absent. The script determines the layer via
    the extractor (e.g., layer "IOS"). If the script does not set
    a layer, the log is warned and dropped at sink time.
    
    - LALConfig: add LAYER_AUTO = "auto" constant
    - ExecutionContext: add autoLayerMode flag
    - LogFilterListener.Factory: store auto rules separately, route
      logs with null layer to auto rules
    - LogAnalyzer: empty layer → createAnalysisListeners(null)
      instead of defaulting to Layer.GENERAL
    - FilterSpec.doSink(): enforce layer-must-be-set in auto mode
    - Tests: 4 data-driven execution tests (set, not-set, abort)
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <no...@anthropic.com>
---
 .../oap/log/analyzer/v2/dsl/ExecutionContext.java  | 11 +++++
 .../analyzer/v2/dsl/spec/filter/FilterSpec.java    | 10 +++++
 .../oap/log/analyzer/v2/provider/LALConfig.java    |  6 +++
 .../log/analyzer/v2/provider/log/LogAnalyzer.java  | 15 +++----
 .../provider/log/listener/LogFilterListener.java   | 47 ++++++++++++++++------
 .../feature-cases/execution-basic.data.yaml        | 32 +++++++++++++++
 .../test-lal/feature-cases/execution-basic.yaml    | 38 +++++++++++++++++
 7 files changed, 138 insertions(+), 21 deletions(-)

diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
index 04154077a2..2039bae36b 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/ExecutionContext.java
@@ -49,6 +49,7 @@ public class ExecutionContext {
     public static final String KEY_ABORT = "abort";
     public static final String KEY_METRICS_CONTAINER = "metrics_container";
     public static final String KEY_DRY_RUN = "dry_run";
+    public static final String KEY_AUTO_LAYER = "auto_layer";
     public static final String KEY_OUTPUT = "output";
 
     private final Map<String, Object> properties = new HashMap<>();
@@ -147,6 +148,16 @@ public class ExecutionContext {
         return (boolean) getProperty(KEY_DRY_RUN);
     }
 
+    public ExecutionContext autoLayerMode(final boolean autoLayer) {
+        setProperty(KEY_AUTO_LAYER, autoLayer);
+        return this;
+    }
+
+    public boolean isAutoLayerMode() {
+        final Object val = getProperty(KEY_AUTO_LAYER);
+        return val != null && (boolean) val;
+    }
+
     public void setOutput(final Object output) {
         setProperty(KEY_OUTPUT, output);
     }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
index dcd72fa052..0b0de203eb 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/dsl/spec/filter/FilterSpec.java
@@ -184,6 +184,16 @@ public class FilterSpec extends AbstractSpec {
             return;
         }
 
+        // In auto-layer mode, the script must set the layer. If not set, drop the log.
+        if (ctx.isAutoLayerMode()) {
+            final String layer = metadata.getLayer();
+            if (layer == null || layer.isEmpty()) {
+                LOGGER.warn("Auto-layer LAL rule did not set layer for service={}, dropping log",
+                    metadata.getService());
+                return;
+            }
+        }
+
         sinkListenerFactories.stream()
                  .map(LogSinkListenerFactory::create)
                  .forEach(it -> it.parse(metadata, input, ctx).build());
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LALConfig.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LALConfig.java
index a8177cf341..b0098bacf3 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LALConfig.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LALConfig.java
@@ -22,6 +22,12 @@ import lombok.Data;
 
 @Data
 public class LALConfig {
+    /**
+     * Special layer value indicating the layer is determined by the LAL script at runtime.
+     * Rules with {@code layer: auto} match logs where {@code service.layer} is absent.
+     * The script is expected to set the layer in the extractor; if not set, the log is dropped.
+     */
+    public static final String LAYER_AUTO = "auto";
     private String name;
 
     private String dsl;
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
index 19d99541bd..36a14a94bd 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/LogAnalyzer.java
@@ -22,7 +22,6 @@ import java.util.List;
 import java.util.Objects;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.oap.log.analyzer.v2.provider.log.listener.LogAnalysisListener;
-import org.apache.skywalking.oap.server.core.UnexpectedException;
 import org.apache.skywalking.oap.server.core.analysis.Layer;
 import org.apache.skywalking.oap.server.core.source.LogMetadata;
 import org.apache.skywalking.oap.server.library.util.StringUtil;
@@ -62,19 +61,17 @@ public class LogAnalyzer {
             log.debug("The log is ignored because the Service name is empty");
             return;
         }
-        Layer layer;
         if (metadata.getLayer() == null || metadata.getLayer().isEmpty()) {
-            layer = Layer.GENERAL;
+            // Empty layer: route to auto-layer rules (pass null)
+            createAnalysisListeners(null);
         } else {
-            try {
-                layer = Layer.nameOf(metadata.getLayer());
-            } catch (UnexpectedException e) {
-                log.warn("The Layer {} is not found, abandon the log.", metadata.getLayer());
+            final Layer layer = Layer.nameOf(metadata.getLayer());
+            if (layer == Layer.UNDEFINED) {
+                log.warn("The Layer {} is not recognized, abandon the log.", metadata.getLayer());
                 return;
             }
+            createAnalysisListeners(layer);
         }
-
-        createAnalysisListeners(layer);
         if (metadata.getTimestamp() == 0) {
             metadata.setTimestamp(System.currentTimeMillis());
         }
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
index ab16b1f540..385668a617 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/log/listener/LogFilterListener.java
@@ -57,10 +57,12 @@ import org.apache.skywalking.oap.server.library.module.ModuleStartException;
 @Slf4j
 public class LogFilterListener implements LogAnalysisListener {
     private final List<DSL> dsls;
+    private final boolean autoMode;
     private List<ExecutionContext> contexts;
 
-    LogFilterListener(final Collection<DSL> dsls) {
+    LogFilterListener(final Collection<DSL> dsls, final boolean autoMode) {
         this.dsls = new ArrayList<>(dsls);
+        this.autoMode = autoMode;
     }
 
     @Override
@@ -79,7 +81,11 @@ public class LogFilterListener implements LogAnalysisListener {
                                      final Object input) {
         contexts = new ArrayList<>(dsls.size());
         for (int i = 0; i < dsls.size(); i++) {
-            contexts.add(new ExecutionContext().init(metadata, input));
+            final ExecutionContext ctx = new ExecutionContext().init(metadata, input);
+            if (autoMode) {
+                ctx.autoLayerMode(true);
+            }
+            contexts.add(ctx);
         }
         return this;
     }
@@ -105,9 +111,11 @@ public class LogFilterListener implements LogAnalysisListener {
      */
     public static class Factory implements LogAnalysisListenerFactory {
         private final Map<Layer, Map<String, DSL>> dsls;
+        private final Map<String, DSL> autoDsls;
 
         public Factory(final ModuleManager moduleManager, final LogAnalyzerModuleConfig config) throws Exception {
             dsls = new HashMap<>();
+            autoDsls = new HashMap<>();
 
             // Scan SPI providers for default inputType/outputType per layer
             final Map<Layer, LALSourceTypeProvider> spiProviders = new HashMap<>();
@@ -123,19 +131,30 @@ public class LogFilterListener implements LogAnalysisListener {
                                                          .flatMap(it -> it.getRules().stream())
                                                          .collect(Collectors.toList());
             for (final LALConfig c : configList) {
-                final Layer layer = Layer.nameOf(c.getLayer());
-                final LALSourceTypeProvider spiProvider = spiProviders.get(layer);
+                final boolean isAuto = LALConfig.LAYER_AUTO.equalsIgnoreCase(c.getLayer());
+                final Layer layer = isAuto ? null : Layer.nameOf(c.getLayer());
+                final LALSourceTypeProvider spiProvider = isAuto ? null : spiProviders.get(layer);
 
                 // Per-rule resolution: explicit YAML > SPI > null
                 final Class<?> resolvedInputType = resolveInputType(c, spiProvider);
                 final Class<?> resolvedOutputType = resolveOutputType(c, spiProvider);
 
-                final Map<String, DSL> layerDsls = this.dsls.computeIfAbsent(layer, k -> new HashMap<>());
-                if (layerDsls.put(c.getName(), DSL.of(
-                        moduleManager, config, c.getDsl(),
-                        resolvedInputType, resolvedOutputType,
-                        c.getName(), c.getSourceName())) != null) {
-                    throw new ModuleStartException("Layer " + layer.name() + " has already set " + c.getName() + " rule.");
+                final DSL dsl = DSL.of(
+                    moduleManager, config, c.getDsl(),
+                    resolvedInputType, resolvedOutputType,
+                    c.getName(), c.getSourceName());
+
+                if (isAuto) {
+                    if (autoDsls.put(c.getName(), dsl) != null) {
+                        throw new ModuleStartException(
+                            "Auto-layer rules have duplicate name: " + c.getName());
+                    }
+                } else {
+                    final Map<String, DSL> layerDsls = this.dsls.computeIfAbsent(layer, k -> new HashMap<>());
+                    if (layerDsls.put(c.getName(), dsl) != null) {
+                        throw new ModuleStartException(
+                            "Layer " + layer.name() + " has already set " + c.getName() + " rule.");
+                    }
                 }
             }
         }
@@ -221,13 +240,17 @@ public class LogFilterListener implements LogAnalysisListener {
         @Override
         public LogAnalysisListener create(Layer layer) {
             if (layer == null) {
-                return null;
+                // null layer → route to auto-layer rules
+                if (autoDsls.isEmpty()) {
+                    return null;
+                }
+                return new LogFilterListener(autoDsls.values(), true);
             }
             final Map<String, DSL> dsl = dsls.get(layer);
             if (dsl == null) {
                 return null;
             }
-            return new LogFilterListener(dsl.values());
+            return new LogFilterListener(dsl.values(), false);
         }
     }
 }
diff --git a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
index 62fe6ab839..7c67febff0 100644
--- a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
+++ b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.data.yaml
@@ -167,3 +167,35 @@ source-attribute-missing:
   expect:
     save: true
     tag.missing: ""
+
+auto-layer-set:
+  body-type: json
+  body: '{"service":"MyApp"}'
+  source-attributes:
+    os.name: "iOS"
+  expect:
+    save: true
+    layer: IOS
+    service: MyApp
+
+auto-layer-not-set:
+  body-type: json
+  body: '{"service":"MyApp"}'
+  expect:
+    save: true
+    service: MyApp
+
+auto-layer-abort:
+  - body-type: json
+    body: '{"msg":"hello"}'
+    source-attributes:
+      os.name: "iOS"
+    expect:
+      save: true
+      layer: IOS
+  - body-type: json
+    body: '{"msg":"hello"}'
+    source-attributes:
+      os.name: "Android"
+    expect:
+      abort: true
diff --git a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
index b15a9663e6..45a1398854 100644
--- a/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
+++ b/oap-server/analyzer/log-analyzer/src/test/resources/scripts/lal/test-lal/feature-cases/execution-basic.yaml
@@ -286,3 +286,41 @@ rules:
         }
         sink {}
       }
+
+  - name: auto-layer-set
+    layer: auto
+    dsl: |
+      filter {
+        json {}
+        if (sourceAttribute("os.name") == "iOS") {
+          extractor {
+            layer "IOS"
+            service parsed.service as String
+          }
+        }
+        sink {}
+      }
+
+  - name: auto-layer-not-set
+    layer: auto
+    dsl: |
+      filter {
+        json {}
+        extractor {
+          service parsed.service as String
+        }
+        sink {}
+      }
+
+  - name: auto-layer-abort
+    layer: auto
+    dsl: |
+      filter {
+        if (sourceAttribute("os.name") != "iOS") {
+          abort {}
+        }
+        extractor {
+          layer "IOS"
+        }
+        sink {}
+      }