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

dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 521dac6092 feat: Support structured logging configurations (#5198)
521dac6092 is described below

commit 521dac609250edf9840204aa35a22f2221468a27
Author: Dominik Riemer <[email protected]>
AuthorDate: Fri Nov 6 17:19:18 2026 +0100

    feat: Support structured logging configurations (#5198)
---
 pom.xml                                            | 12 ++++
 streampipes-service-base/pom.xml                   |  7 ++
 .../service/base/PreContextStartup.java            | 33 +++++++++
 .../service/base/PreContextStartupListener.java    | 58 +++++++++++++++
 .../service/base/StreamPipesServiceBase.java       | 14 ++--
 .../src/main/resources/logback.xml                 | 38 ----------
 .../base/PreContextStartupListenerTest.java        | 84 ++++++++++++++++++++++
 .../service/core/StreamPipesCoreApplication.java   | 11 +--
 .../src/main/resources/application.properties      |  3 +-
 .../StreamPipesExtensionsServiceBase.java          | 42 ++++++-----
 .../src/main/resources/application.properties      |  3 +-
 11 files changed, 231 insertions(+), 74 deletions(-)

diff --git a/pom.xml b/pom.xml
index 8b1588d00f..aab9a316a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -540,6 +540,10 @@
                         <groupId>ch.qos.reload4j</groupId>
                         <artifactId>reload4j</artifactId>
                     </exclusion>
+                    <exclusion>
+                        <groupId>org.slf4j</groupId>
+                        <artifactId>slf4j-reload4j</artifactId>
+                    </exclusion>
                     <exclusion>
                         <groupId>io.netty</groupId>
                         <artifactId>netty-transport-native-epoll</artifactId>
@@ -603,6 +607,14 @@
                         <groupId>com.google.inject</groupId>
                         <artifactId>guice</artifactId>
                     </exclusion>
+                    <exclusion>
+                        <groupId>ch.qos.reload4j</groupId>
+                        <artifactId>reload4j</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.slf4j</groupId>
+                        <artifactId>slf4j-reload4j</artifactId>
+                    </exclusion>
                 </exclusions>
             </dependency>
             <dependency>
diff --git a/streampipes-service-base/pom.xml b/streampipes-service-base/pom.xml
index 92bc8cd723..f5dc67693f 100644
--- a/streampipes-service-base/pom.xml
+++ b/streampipes-service-base/pom.xml
@@ -86,6 +86,13 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git 
a/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartup.java
 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartup.java
new file mode 100644
index 0000000000..af744385e0
--- /dev/null
+++ 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartup.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.service.base;
+
+import java.net.UnknownHostException;
+
+/**
+ * Startup work that has to finish before the Spring context is created. It 
runs after
+ * Spring Boot has initialized logging, so everything it logs uses the 
configured format.
+ */
+@FunctionalInterface
+public interface PreContextStartup {
+
+  /**
+   * @return the networking configuration the service is started with
+   */
+  BaseNetworkingConfig run() throws UnknownHostException;
+}
diff --git 
a/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartupListener.java
 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartupListener.java
new file mode 100644
index 0000000000..c68b513663
--- /dev/null
+++ 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/PreContextStartupListener.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.service.base;
+
+import 
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.env.PropertiesPropertySource;
+
+import java.net.UnknownHostException;
+import java.util.Properties;
+
+/**
+ * Has no explicit order on purpose: it must run after Spring Boot's 
LoggingApplicationListener,
+ * which handles the same event at a high precedence.
+ */
+class PreContextStartupListener implements 
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
+
+  private static final String PROPERTY_SOURCE_NAME = 
"streampipesServiceConfig";
+
+  private final PreContextStartup startup;
+
+  PreContextStartupListener(PreContextStartup startup) {
+    this.startup = startup;
+  }
+
+  @Override
+  public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+    BaseNetworkingConfig networkingConfig;
+    try {
+      networkingConfig = startup.run();
+    } catch (UnknownHostException e) {
+      throw new IllegalStateException(
+          "Could not auto-resolve host address - please manually provide the 
hostname"
+              + " using the `SP_HOST` environment variable", e);
+    }
+
+    Properties config = new Properties();
+    ServiceBaseConfig.addPrometheusConfig(config);
+    ServiceBaseConfig.addPortConfig(networkingConfig.getPort(), config);
+
+    event.getEnvironment().getPropertySources().addLast(new 
PropertiesPropertySource(PROPERTY_SOURCE_NAME, config));
+  }
+}
diff --git 
a/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/StreamPipesServiceBase.java
 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/StreamPipesServiceBase.java
index 6505e9ab20..a9fe7ef68e 100644
--- 
a/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/StreamPipesServiceBase.java
+++ 
b/streampipes-service-base/src/main/java/org/apache/streampipes/service/base/StreamPipesServiceBase.java
@@ -21,7 +21,6 @@ import org.apache.commons.lang3.RandomStringUtils;
 import org.springframework.boot.SpringApplication;
 
 import java.net.UnknownHostException;
-import java.util.Properties;
 
 public abstract class StreamPipesServiceBase {
 
@@ -29,18 +28,13 @@ public abstract class StreamPipesServiceBase {
 
   public void startStreamPipesService(Class<?> serviceClass,
                                          BaseNetworkingConfig 
networkingConfig) throws UnknownHostException {
-    runApplication(serviceClass, networkingConfig.getPort());
+    startStreamPipesService(serviceClass, () -> networkingConfig);
   }
 
-  private void runApplication(Class<?> serviceClass,
-                              Integer port) {
+  public void startStreamPipesService(Class<?> serviceClass,
+                                      PreContextStartup startup) {
     SpringApplication app = new SpringApplication(serviceClass);
-
-    Properties config = new Properties();
-    ServiceBaseConfig.addPrometheusConfig(config);
-    ServiceBaseConfig.addPortConfig(port, config);
-
-    app.setDefaultProperties(config);
+    app.addListeners(new PreContextStartupListener(startup));
     app.run();
   }
 
diff --git a/streampipes-service-base/src/main/resources/logback.xml 
b/streampipes-service-base/src/main/resources/logback.xml
deleted file mode 100644
index 1331d020df..0000000000
--- a/streampipes-service-base/src/main/resources/logback.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one or more
-  ~ contributor license agreements.  See the NOTICE file distributed with
-  ~ this work for additional information regarding copyright ownership.
-  ~ The ASF licenses this file to You under the Apache License, Version 2.0
-  ~ (the "License"); you may not use this file except in compliance with
-  ~ the License.  You may obtain a copy of the License at
-  ~
-  ~    http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  ~
-  -->
-
-<configuration>
-
-    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
-
-    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
-            <charset>utf8</charset>
-        </encoder>
-    </appender>
-
-    <logger name="org.lightcouch" level="ERROR"/>
-    <logger name="org.apache.kafka" level="ERROR"/>
-
-
-    <root level="INFO">
-        <appender-ref ref="CONSOLE"/>
-    </root>
-</configuration>
diff --git 
a/streampipes-service-base/src/test/java/org/apache/streampipes/service/base/PreContextStartupListenerTest.java
 
b/streampipes-service-base/src/test/java/org/apache/streampipes/service/base/PreContextStartupListenerTest.java
new file mode 100644
index 0000000000..8f7709c8a1
--- /dev/null
+++ 
b/streampipes-service-base/src/test/java/org/apache/streampipes/service/base/PreContextStartupListenerTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.streampipes.service.base;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.WebApplicationType;
+import org.springframework.context.annotation.Configuration;
+
+import java.net.UnknownHostException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PreContextStartupListenerTest {
+
+  @Configuration(proxyBeanMethods = false)
+  static class EmptyConfiguration {
+  }
+
+  private SpringApplication application(PreContextStartup startup) {
+    var application = new SpringApplication(EmptyConfiguration.class);
+    application.setWebApplicationType(WebApplicationType.NONE);
+    application.addListeners(new PreContextStartupListener(startup));
+    return application;
+  }
+
+  @Test
+  void startupRunsOnceLoggingIsInitialized() {
+    // Spring Boot suppresses all log output from the start of the run until 
logging is initialized
+    var loggingEnabled = new AtomicBoolean(false);
+
+    try (var ignored = application(() -> {
+      
loggingEnabled.set(LoggerFactory.getLogger(PreContextStartupListenerTest.class).isInfoEnabled());
+      return new BaseNetworkingConfig("localhost", 8090);
+    }).run()) {
+      assertTrue(loggingEnabled.get());
+    }
+  }
+
+  @Test
+  void portFromStartupIsAvailableToTheContext() {
+    try (var context = application(() -> new BaseNetworkingConfig("localhost", 
8123)).run()) {
+      assertEquals("8123", 
context.getEnvironment().getProperty("server.port"));
+    }
+  }
+
+  @Test
+  void configuredPortWinsOverPortFromStartup() {
+    try (var context = application(() -> new BaseNetworkingConfig("localhost", 
8123))
+        .run("--server.port=9000")) {
+      assertEquals("9000", 
context.getEnvironment().getProperty("server.port"));
+    }
+  }
+
+  @Test
+  void unresolvableHostFailsStartupWithHint() {
+    var application = application(() -> {
+      throw new UnknownHostException("unknown");
+    });
+
+    var exception = assertThrows(IllegalStateException.class, 
application::run);
+
+    assertTrue(exception.getMessage().contains("SP_HOST"));
+  }
+}
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
index 6edc4ab024..145ded42b8 100644
--- 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/StreamPipesCoreApplication.java
@@ -84,7 +84,6 @@ import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PreDestroy;
 
 import java.io.IOException;
-import java.net.UnknownHostException;
 import java.time.Duration;
 import java.util.List;
 import java.util.Objects;
@@ -162,15 +161,11 @@ public class StreamPipesCoreApplication extends 
StreamPipesServiceBase {
 
   public void initialize(SupportedProtocols supportedProtocols,
                          List<Supplier<TransformationEngine>> 
transformationEngines) {
-    try {
+    startStreamPipesService(StreamPipesCoreApplication.class, () -> {
       registerProtocols(supportedProtocols);
       registerTransformationEngines(transformationEngines);
-      BaseNetworkingConfig networkingConfig = 
BaseNetworkingConfig.defaultResolution(8030);
-      startStreamPipesService(StreamPipesCoreApplication.class, 
networkingConfig);
-    } catch (UnknownHostException e) {
-      LOG.error("Could not auto-resolve host address - please manually provide 
the hostname"
-          + " using the `SP_HOST` environment variable");
-    }
+      return BaseNetworkingConfig.defaultResolution(8030);
+    });
   }
 
   protected void 
registerTransformationEngines(List<Supplier<TransformationEngine>> 
transformationEngines) {
diff --git a/streampipes-service-core/src/main/resources/application.properties 
b/streampipes-service-core/src/main/resources/application.properties
index 26febd012f..63435cc25c 100644
--- a/streampipes-service-core/src/main/resources/application.properties
+++ b/streampipes-service-core/src/main/resources/application.properties
@@ -35,7 +35,8 @@ 
streampipes.storage.cache.core-configuration.enabled=${SP_STORAGE_CACHE_CORE_CON
 spring.cache.type=caffeine
 
spring.cache.cache-names=dataExplorerWidgets,permissions,adapters,dashboards,pipelines,dataLakeMeasures,roles,userGroups,privileges,users,coreConfiguration
 spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=10m,recordStats
-logging.config=classpath:logback.xml
+logging.level.org.lightcouch=ERROR
+logging.level.org.apache.kafka=ERROR
 spring.output.ansi.enabled=always
 springdoc.api-docs.enabled=${SP_API_DOCS_ENABLED:true}
 springdoc.api-docs.path=/apidocs
diff --git 
a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
 
b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
index 43b89eeaf5..f2a8aefe5b 100644
--- 
a/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
+++ 
b/streampipes-service-extensions/src/main/java/org/apache/streampipes/service/extensions/StreamPipesExtensionsServiceBase.java
@@ -62,6 +62,7 @@ import java.net.UnknownHostException;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -84,12 +85,19 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
   private volatile SpServiceRegistration currentServiceRegistration;
 
   public void init() {
-    SpServiceDefinition serviceDef = provideServiceDefinition();
-    init(serviceDef);
+    startService(this::provideServiceDefinition);
   }
 
   public void init(SpServiceDefinition serviceDef) {
-    try {
+    startService(() -> serviceDef);
+  }
+
+  private void startService(Supplier<SpServiceDefinition> serviceDefSupplier) {
+    var serviceDefinition = new AtomicReference<SpServiceDefinition>();
+
+    startStreamPipesService(this.getClass(), () -> {
+      SpServiceDefinition serviceDef = serviceDefSupplier.get();
+      serviceDefinition.set(serviceDef);
       BaseNetworkingConfig networkingConfig = 
BaseNetworkingConfig.defaultResolution(serviceDef.getDefaultPort());
       String serviceId = serviceDef.getServiceGroup() + "-" + 
AUTO_GENERATED_SERVICE_ID;
       serviceDef.setServiceId(serviceId);
@@ -100,14 +108,12 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
           GraalJsScriptEngine::new
       ));
 
-      startExtensionsService(this.getClass(), serviceDef, networkingConfig);
-      ServiceLoadDataReportGenerator.getInstance().initialize();
+      registerExtensionsService(serviceDef, networkingConfig);
+      return networkingConfig;
+    });
 
-    } catch (UnknownHostException e) {
-      LOG.error(
-          "Could not auto-resolve host address - "
-              + "please manually provide the hostname using the SP_HOST 
environment variable");
-    }
+    this.afterServiceRegistered(serviceDefinition.get(), 
this.currentServiceRegistration);
+    ServiceLoadDataReportGenerator.getInstance().initialize();
   }
 
   protected void 
registerTransformationEngines(List<Supplier<TransformationEngine>> 
transformationEngines) {
@@ -125,6 +131,16 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
   public void startExtensionsService(Class<?> serviceClass,
                                      SpServiceDefinition serviceDef,
                                      BaseNetworkingConfig networkingConfig) 
throws UnknownHostException {
+    this.startStreamPipesService(serviceClass, () -> {
+      registerExtensionsService(serviceDef, networkingConfig);
+      return networkingConfig;
+    });
+
+    this.afterServiceRegistered(serviceDef, this.currentServiceRegistration);
+  }
+
+  private void registerExtensionsService(SpServiceDefinition serviceDef,
+                                         BaseNetworkingConfig 
networkingConfig) {
     this.extensionTransportMode = ExtensionServiceTransportMode.from(
         
Environments.getEnvironment().getExtensionTransportMode().getValueOrDefault()
     );
@@ -164,12 +180,6 @@ public abstract class StreamPipesExtensionsServiceBase 
extends StreamPipesServic
     registerService(req);
 
     this.registerConfigs(serviceDef.getServiceGroup(), 
serviceDef.getServiceName(), serviceDef.getKvConfigs());
-    this.startStreamPipesService(
-        serviceClass,
-        networkingConfig
-    );
-
-    this.afterServiceRegistered(serviceDef, req);
   }
 
   private void registerService(SpServiceRegistration serviceRegistration) {
diff --git 
a/streampipes-service-extensions/src/main/resources/application.properties 
b/streampipes-service-extensions/src/main/resources/application.properties
index 8814c22a59..e0006c18f1 100644
--- a/streampipes-service-extensions/src/main/resources/application.properties
+++ b/streampipes-service-extensions/src/main/resources/application.properties
@@ -16,5 +16,6 @@
 #
 #
 
-logging.config=classpath:logback.xml
+logging.level.org.lightcouch=ERROR
+logging.level.org.apache.kafka=ERROR
 spring.output.ansi.enabled=always

Reply via email to