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/26 01:40:31 UTC
(skywalking) branch feature/runtime-rule-hot-update updated: log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch feature/runtime-rule-hot-update
in repository https://gitbox.apache.org/repos/asf/skywalking.git
The following commit(s) were added to refs/heads/feature/runtime-rule-hot-update by this push:
new 98073ffdc5 log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules
98073ffdc5 is described below
commit 98073ffdc5d3609bbdd3ccf0e75b608891e777f4
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Sun Apr 26 09:40:23 2026 +0800
log-analyzer: split Factory init into prepare-safe constructor + start-time loadStaticRules
LogAnalyzerModule.services() declared LogFilterListener.Factory and
MalConverterRegistry, but the provider was registering both inside
start() — BootstrapFlow runs requiredCheck BEFORE start(), and the
count-equals branch of requiredCheck (services.length must match
services.size) caused ServiceNotProvidedException at boot.
Moving the registration to prepare() needed a small refactor of the
Factory: its constructor was calling compile() in a loop, which builds
a RecordSinkListener.Factory that calls moduleManager.find() — illegal
during the prepare stage (ModuleManager asserts otherwise).
Two-phase init now mirrors the OTLP receiver's own pattern
(OpenTelemetryMetricRequestProcessor created in prepare(), populated in
start()):
- Constructor stores moduleManager + config, runs the SPI scan, and
publishes empty dsls / autoDsls maps. Safe in prepare() — no
cross-module lookups.
- New Factory.loadStaticRules() does the LALConfigs.load + per-rule
compile pass and replaces the empty maps with the populated set.
Provider calls this from start() once the manager is past prepare.
LogAnalyzerModuleProvider.prepare() now creates and registers the
Factory + MalConverterRegistry; start() lights up the Factory via
loadStaticRules and wires it into logAnalyzerService. requiredCheck
sees both declared services already registered; runtime-rule plugin's
existing provider().getService(LogFilterListener.Factory.class)
lookups continue to resolve unchanged.
134 LAL unit tests pass. Verified by booting OAP locally with the
runtime-rule e2e docker-compose — port 17128 + /runtime/rule/list
return the static rule snapshot cleanly.
---
.../v2/provider/LogAnalyzerModuleProvider.java | 50 ++++++++++++++--------
.../provider/log/listener/LogFilterListener.java | 41 +++++++++++++++---
2 files changed, 69 insertions(+), 22 deletions(-)
diff --git a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
index 1a2ebdf0b1..24f25d1c27 100644
--- a/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
+++ b/oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/provider/LogAnalyzerModuleProvider.java
@@ -58,6 +58,7 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
private final Object convertersWriteLock = new Object();
private LogAnalyzerServiceImpl logAnalyzerService;
+ private LogFilterListener.Factory factory;
@Override
public String name() {
@@ -88,26 +89,21 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
public void prepare() throws ServiceNotProvidedException, ModuleStartException {
logAnalyzerService = new LogAnalyzerServiceImpl(getManager(), moduleConfig);
this.registerServiceImplementation(ILogAnalyzerService.class, logAnalyzerService);
- }
- @Override
- public void start() throws ServiceNotProvidedException, ModuleStartException {
- MeterSystem meterSystem = getManager().find(CoreModule.NAME).provider().getService(MeterSystem.class);
- for (final var rule : moduleConfig.malConfigs()) {
- // Use the catalog:name key convention so a runtime-rule /addOrUpdate for the same
- // (catalog, name) replaces this static entry in place instead of running two
- // converters against the same sample stream.
- addOrReplaceMetricConvert("log-mal-rules:" + rule.getName(), new MetricConvert(rule, meterSystem));
- }
+ // Register both module-declared services in prepare(): {@link BootstrapFlow#start}
+ // runs {@code requiredCheck} against {@code services()} BEFORE this provider's
+ // {@code start()}, and the count-equals check ({@code requiredServices.length ==
+ // services.size()}) requires every declared service to be registered already.
+ // Both objects are config-only at construction — the Factory's heavy
+ // rule-compile pass is deferred to {@link LogFilterListener.Factory#loadStaticRules}
+ // (called from {@link #start()} where moduleManager.find is allowed); the
+ // MalConverterRegistry is a pure delegate to this provider's own volatile map and
+ // never reaches across modules.
try {
- final LogFilterListener.Factory factory = new LogFilterListener.Factory(getManager(), moduleConfig);
- logAnalyzerService.addListenerFactory(factory);
- // Expose the Factory as a module service so the runtime-rule plugin can compile +
- // addOrReplace / remove LAL rules at runtime against the same registry the runtime
- // log pipeline consults.
- this.registerServiceImplementation(LogFilterListener.Factory.class, factory);
+ this.factory = new LogFilterListener.Factory(getManager(), moduleConfig);
+ this.registerServiceImplementation(LogFilterListener.Factory.class, this.factory);
} catch (final Exception e) {
- throw new ModuleStartException("Failed to create LAL listener.", e);
+ throw new ModuleStartException("Failed to create LAL listener factory.", e);
}
// MalConverterRegistry for the log-mal-rules catalog. The runtime-rule plugin looks
// this up by module name + service class, so it does not need a compile-time dep on
@@ -126,6 +122,26 @@ public class LogAnalyzerModuleProvider extends ModuleProvider {
});
}
+ @Override
+ public void start() throws ServiceNotProvidedException, ModuleStartException {
+ MeterSystem meterSystem = getManager().find(CoreModule.NAME).provider().getService(MeterSystem.class);
+ for (final var rule : moduleConfig.malConfigs()) {
+ // Use the catalog:name key convention so a runtime-rule /addOrUpdate for the same
+ // (catalog, name) replaces this static entry in place instead of running two
+ // converters against the same sample stream.
+ addOrReplaceMetricConvert("log-mal-rules:" + rule.getName(), new MetricConvert(rule, meterSystem));
+ }
+ try {
+ // Light up the Factory now that all peer modules are past prepare:
+ // loadStaticRules calls compile() which constructs RecordSinkListener.Factory
+ // which calls moduleManager.find() — only safe outside prepare.
+ this.factory.loadStaticRules();
+ logAnalyzerService.addListenerFactory(this.factory);
+ } catch (final Exception e) {
+ throw new ModuleStartException("Failed to load static LAL rules.", e);
+ }
+ }
+
@Override
public void notifyAfterCompleted() throws ServiceNotProvidedException {
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 130ed510f3..c3f4af9265 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
@@ -156,13 +156,26 @@ public class LogFilterListener implements LogAnalysisListener {
private final LogAnalyzerModuleConfig analyzerConfig;
private final Map<Layer, LALSourceTypeProvider> spiProviders;
+ /**
+ * Two-phase init — the constructor wires fields and runs the SPI scan, leaving the
+ * {@code dsls} / {@code autoDsls} registry empty so this instance is safe to register
+ * as a module service from {@code prepare()}. The rule-compile pass — which calls
+ * {@code RecordSinkListener.Factory.<init>} → {@code moduleManager.find()} and is
+ * therefore illegal during the prepare stage — is deferred to
+ * {@link #loadStaticRules()}, which the provider invokes from {@code start()} once
+ * the manager is past prepare.
+ *
+ * <p>This shape mirrors {@code OpenTelemetryMetricRequestProcessor}: the receiver
+ * registers a config-only object in {@code prepare()} and lights it up in
+ * {@code start()}, so cross-module {@code requiredCheck} resolves the service name
+ * cleanly.
+ */
public Factory(final ModuleManager moduleManager, final LogAnalyzerModuleConfig config) throws Exception {
this.moduleManager = moduleManager;
this.analyzerConfig = config;
- final Map<Layer, Map<String, DSL>> initDsls = new HashMap<>();
- final Map<String, DSL> initAutoDsls = new HashMap<>();
- // Scan SPI providers for default inputType/outputType per layer
+ // Scan SPI providers for default inputType/outputType per layer. SPI lookup uses
+ // the JDK's {@code ServiceLoader} — no moduleManager.find required, safe in prepare.
this.spiProviders = new HashMap<>();
for (final LALSourceTypeProvider p : ServiceLoader.load(LALSourceTypeProvider.class)) {
spiProviders.put(p.layer(), p);
@@ -171,7 +184,26 @@ public class LogFilterListener implements LogAnalysisListener {
p.outputType() != null ? p.outputType().getName() : "default(Log)");
}
- final List<LALConfig> configList = LALConfigs.load(config.getLalPath(), config.lalFiles())
+ // Empty registry — populated by {@link #loadStaticRules}.
+ this.dsls = new HashMap<>();
+ this.autoDsls = new HashMap<>();
+ }
+
+ /**
+ * Compile every static LAL rule the {@link LogAnalyzerModuleConfig} configures and
+ * publish the resulting registry. Provider must call this from {@code start()} —
+ * never from {@code prepare()} — because {@code compile} reaches into
+ * {@code RecordSinkListener.Factory.<init>}, which calls {@code moduleManager.find()}
+ * and asserts the manager is past prepare.
+ *
+ * <p>Idempotent against re-entry within the same boot (the Factory only stays in
+ * its empty post-construct state until this fires once); calling it twice on the
+ * same instance is a programming error.
+ */
+ public void loadStaticRules() throws Exception {
+ final Map<Layer, Map<String, DSL>> initDsls = new HashMap<>();
+ final Map<String, DSL> initAutoDsls = new HashMap<>();
+ final List<LALConfig> configList = LALConfigs.load(analyzerConfig.getLalPath(), analyzerConfig.lalFiles())
.stream()
.flatMap(it -> it.getRules().stream())
.collect(Collectors.toList());
@@ -190,7 +222,6 @@ public class LogFilterListener implements LogAnalysisListener {
}
}
}
-
// Publish: readers from now on see the startup-complete registry.
this.dsls = initDsls;
this.autoDsls = initAutoDsls;