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

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new c7148f9197 NIFI-15104 Populate Bulletin stackTrace and for Bulletin 
Board (#10431)
c7148f9197 is described below

commit c7148f9197c721f4ed9b7eff0c77be8ebf1014dc
Author: Pierre Villard <[email protected]>
AuthorDate: Fri Oct 17 18:40:43 2025 +0200

    NIFI-15104 Populate Bulletin stackTrace and for Bulletin Board (#10431)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../org/apache/nifi/web/api/dto/BulletinDTO.java   | 10 ++++
 .../nifi/logging/ConnectableLogObserver.java       |  4 +-
 .../nifi/logging/ControllerServiceLogObserver.java |  2 +-
 .../logging/FlowRegistryClientLogObserver.java     |  2 +-
 .../apache/nifi/logging/ProcessorLogObserver.java  |  4 +-
 .../nifi/logging/ReportingTaskLogObserver.java     |  2 +-
 .../org/apache/nifi/events/BulletinFactory.java    | 64 +++++++++++++++++++++
 .../nifi/events/BulletinFactoryStackTraceTest.java | 61 ++++++++++++++++++++
 .../java/org/apache/nifi/jaxb/AdaptedBulletin.java |  9 +++
 .../java/org/apache/nifi/jaxb/BulletinAdapter.java |  8 ++-
 .../nifi/logging/FlowAnalysisRuleLogObserver.java  |  2 +-
 .../nifi/logging/ParameterProviderLogObserver.java |  2 +-
 .../nifi/jaxb/BulletinAdapterStackTraceTest.java   | 46 +++++++++++++++
 .../apache/nifi/web/StandardNiFiServiceFacade.java | 18 +++---
 .../org/apache/nifi/web/api/dto/DtoFactory.java    | 13 +++--
 .../api/dto/DtoFactoryBulletinStackTraceTest.java  | 67 ++++++++++++++++++++++
 16 files changed, 290 insertions(+), 24 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
index 6f5cc39547..51ee6b6388 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/BulletinDTO.java
@@ -39,6 +39,7 @@ public class BulletinDTO {
     private String message;
     private Date timestamp;
     private String sourceType;
+    private String stackTrace;
 
     /**
      * @return id of this message
@@ -168,4 +169,13 @@ public class BulletinDTO {
     public void setSourceType(String sourceType) {
         this.sourceType = sourceType;
     }
+
+    @Schema(description = "The stack trace associated with the bulletin, if 
any.")
+    public String getStackTrace() {
+        return stackTrace;
+    }
+
+    public void setStackTrace(String stackTrace) {
+        this.stackTrace = stackTrace;
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ConnectableLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ConnectableLogObserver.java
index 8ed66b9323..d05b04b6ea 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ConnectableLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ConnectableLogObserver.java
@@ -38,7 +38,9 @@ public class ConnectableLogObserver implements LogObserver {
         // Map LogLevel.WARN to Severity.WARNING so that we are consistent 
with the Severity enumeration. Else, just use whatever
         // the LogLevel is (INFO and ERROR map directly and all others we will 
just accept as they are).
         final String bulletinLevel = (message.getLogLevel() == LogLevel.WARN) 
? Severity.WARNING.name() : message.getLogLevel().toString();
-        
bulletinRepository.addBulletin(BulletinFactory.createBulletin(connectable, 
CATEGORY, bulletinLevel, message.getMessage(), message.getFlowFileUuid()));
+        bulletinRepository.addBulletin(
+            BulletinFactory.createBulletin(connectable, CATEGORY, 
bulletinLevel, message.getMessage(), message.getFlowFileUuid(), 
message.getThrowable())
+        );
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ControllerServiceLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ControllerServiceLogObserver.java
index 85ca37caee..0dfaa9aebf 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ControllerServiceLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ControllerServiceLogObserver.java
@@ -45,7 +45,7 @@ public class ControllerServiceLogObserver implements 
LogObserver {
         final String groupName = pg == null ? null : pg.getName();
 
         final Bulletin bulletin = BulletinFactory.createBulletin(groupId, 
groupName, serviceNode.getIdentifier(), ComponentType.CONTROLLER_SERVICE,
-                serviceNode.getName(), "Log Message", bulletinLevel, 
message.getMessage());
+                serviceNode.getName(), "Log Message", bulletinLevel, 
message.getMessage(), message.getThrowable());
         bulletinRepository.addBulletin(bulletin);
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/FlowRegistryClientLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/FlowRegistryClientLogObserver.java
index d7b0d5adbf..80376f3ab2 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/FlowRegistryClientLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/FlowRegistryClientLogObserver.java
@@ -39,7 +39,7 @@ public class FlowRegistryClientLogObserver implements 
LogObserver {
         final String bulletinLevel = message.getLogLevel() == LogLevel.WARN ? 
Severity.WARNING.name() : message.getLogLevel().toString();
 
         final Bulletin bulletin = BulletinFactory.createBulletin(null, 
clientNode.getIdentifier(), ComponentType.FLOW_REGISTRY_CLIENT,
-                clientNode.getName(), "Log Message", bulletinLevel, 
message.getMessage());
+                clientNode.getName(), "Log Message", bulletinLevel, 
message.getMessage(), message.getThrowable());
         bulletinRepository.addBulletin(bulletin);
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ProcessorLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ProcessorLogObserver.java
index 18b99b4bfb..7209492ca3 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ProcessorLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ProcessorLogObserver.java
@@ -41,7 +41,9 @@ public class ProcessorLogObserver implements LogObserver {
         // Map LogLevel.WARN to Severity.WARNING so that we are consistent 
with the Severity enumeration. Else, just use whatever
         // the LogLevel is (INFO and ERROR map directly and all others we will 
just accept as they are).
         final String bulletinLevel = (message.getLogLevel() == LogLevel.WARN) 
? Severity.WARNING.name() : message.getLogLevel().toString();
-        
bulletinRepository.addBulletin(BulletinFactory.createBulletin(processorNode, 
CATEGORY, bulletinLevel, message.getMessage(), message.getFlowFileUuid()));
+        bulletinRepository.addBulletin(
+            BulletinFactory.createBulletin(processorNode, CATEGORY, 
bulletinLevel, message.getMessage(), message.getFlowFileUuid(), 
message.getThrowable())
+        );
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ReportingTaskLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ReportingTaskLogObserver.java
index 8631b4fb02..5df50132e7 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ReportingTaskLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/logging/ReportingTaskLogObserver.java
@@ -39,7 +39,7 @@ public class ReportingTaskLogObserver implements LogObserver {
         final String bulletinLevel = message.getLogLevel() == LogLevel.WARN ? 
Severity.WARNING.name() : message.getLogLevel().toString();
 
         final Bulletin bulletin = BulletinFactory.createBulletin(null, 
taskNode.getIdentifier(), ComponentType.REPORTING_TASK,
-            taskNode.getName(), "Log Message", bulletinLevel, 
message.getMessage());
+            taskNode.getName(), "Log Message", bulletinLevel, 
message.getMessage(), message.getThrowable());
         bulletinRepository.addBulletin(bulletin);
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/events/BulletinFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/events/BulletinFactory.java
index e9c14885df..443ac18e8e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/events/BulletinFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/events/BulletinFactory.java
@@ -21,6 +21,8 @@ import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.reporting.Bulletin;
 import org.apache.nifi.reporting.ComponentType;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.concurrent.atomic.AtomicLong;
 
 public final class BulletinFactory {
@@ -50,6 +52,22 @@ public final class BulletinFactory {
         return createBulletin(groupId, groupName, connectable.getIdentifier(), 
type, connectable.getName(), category, severity, message, groupPath, 
flowFileUUID);
     }
 
+    public static Bulletin createBulletin(final Connectable connectable, final 
String category, final String severity, final String message, final String 
flowFileUUID, final Throwable t) {
+        final Bulletin bulletin = createBulletin(connectable, category, 
severity, message, flowFileUUID);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
+    public static Bulletin createBulletin(final Connectable connectable, final 
String category, final String severity, final String message, final Throwable 
t) {
+        final Bulletin bulletin = createBulletin(connectable, category, 
severity, message);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
     private static String buildGroupPath(ProcessGroup group) {
         if (group == null) {
             return null;
@@ -78,6 +96,15 @@ public final class BulletinFactory {
         return bulletin;
     }
 
+    public static Bulletin createBulletin(final String groupId, final String 
sourceId, final ComponentType sourceType, final String sourceName,
+        final String category, final String severity, final String message, 
final Throwable t) {
+        final Bulletin bulletin = createBulletin(groupId, sourceId, 
sourceType, sourceName, category, severity, message);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
     public static Bulletin createBulletin(final String groupId, final String 
groupName, final String sourceId, final ComponentType sourceType,
             final String sourceName, final String category, final String 
severity, final String message) {
         final Bulletin bulletin = new 
ComponentBulletin(currentId.getAndIncrement());
@@ -92,6 +119,15 @@ public final class BulletinFactory {
         return bulletin;
     }
 
+    public static Bulletin createBulletin(final String groupId, final String 
groupName, final String sourceId, final ComponentType sourceType,
+            final String sourceName, final String category, final String 
severity, final String message, final Throwable t) {
+        final Bulletin bulletin = createBulletin(groupId, groupName, sourceId, 
sourceType, sourceName, category, severity, message);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
     public static Bulletin createBulletin(final String groupId, final String 
groupName, final String sourceId, final ComponentType sourceType,
             final String sourceName, final String category, final String 
severity, final String message, final String groupPath, final String 
flowFileUUID) {
         final Bulletin bulletin = new 
ComponentBulletin(currentId.getAndIncrement());
@@ -108,6 +144,15 @@ public final class BulletinFactory {
         return bulletin;
     }
 
+    public static Bulletin createBulletin(final String groupId, final String 
groupName, final String sourceId, final ComponentType sourceType,
+            final String sourceName, final String category, final String 
severity, final String message, final String groupPath, final String 
flowFileUUID, final Throwable t) {
+        final Bulletin bulletin = createBulletin(groupId, groupName, sourceId, 
sourceType, sourceName, category, severity, message, groupPath, flowFileUUID);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
     public static Bulletin createBulletin(final String category, final String 
severity, final String message) {
         final Bulletin bulletin = new 
SystemBulletin(currentId.getAndIncrement());
         bulletin.setCategory(category);
@@ -117,6 +162,14 @@ public final class BulletinFactory {
         return bulletin;
     }
 
+    public static Bulletin createBulletin(final String category, final String 
severity, final String message, final Throwable t) {
+        final Bulletin bulletin = createBulletin(category, severity, message);
+        if (t != null) {
+            bulletin.setStackTrace(formatStackTrace(t));
+        }
+        return bulletin;
+    }
+
     private static ComponentType getComponentType(final Connectable 
connectable) {
         return switch (connectable.getConnectableType()) {
             case REMOTE_INPUT_PORT, REMOTE_OUTPUT_PORT -> 
ComponentType.REMOTE_PROCESS_GROUP;
@@ -126,4 +179,15 @@ public final class BulletinFactory {
             default -> ComponentType.PROCESSOR;
         };
     }
+
+    private static String formatStackTrace(final Throwable t) {
+        try (final StringWriter sw = new StringWriter(); final PrintWriter pw 
= new PrintWriter(sw)) {
+            t.printStackTrace(pw);
+            pw.flush();
+            return sw.toString();
+        } catch (final Exception e) {
+            // Fallback to Throwable#toString if printing fails for any reason
+            return t.toString();
+        }
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/events/BulletinFactoryStackTraceTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/events/BulletinFactoryStackTraceTest.java
new file mode 100644
index 0000000000..7b4e94da09
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/events/BulletinFactoryStackTraceTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.nifi.events;
+
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.ComponentType;
+import org.junit.jupiter.api.Test;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class BulletinFactoryStackTraceTest {
+
+    private static String toStackTrace(final Throwable t) {
+        final StringWriter sw = new StringWriter();
+        try (PrintWriter pw = new PrintWriter(sw)) {
+            t.printStackTrace(pw);
+        }
+        return sw.toString();
+    }
+
+    @Test
+    void testCreateBulletinWithThrowableIncludesPrintableStackTrace() {
+        final Exception cause = new IllegalStateException("inner");
+        final RuntimeException ex = new RuntimeException("outer", cause);
+
+        final Bulletin bulletin = BulletinFactory.createBulletin(
+                "pg1", "Process Group 1", "proc1", ComponentType.PROCESSOR, 
"MyProcessor",
+                "Log Message", "ERROR", "Something failed", "/root / Process 
Group 1", null, ex);
+
+        assertNotNull(bulletin);
+        final String stackTrace = bulletin.getStackTrace();
+        assertNotNull(stackTrace, "Stack trace should be set on bulletin");
+
+        final String expected = toStackTrace(ex);
+        assertEquals(expected, stackTrace, "Stack trace should match 
Throwable.printStackTrace output exactly");
+        assertTrue(stackTrace.contains(cause.getClass().getSimpleName()));
+        assertTrue(stackTrace.contains(ex.getClass().getSimpleName()));
+        assertTrue(stackTrace.contains("\n"), "Stack trace should contain 
newlines for multiline formatting");
+        assertTrue(stackTrace.contains("\tat ") || stackTrace.contains("\n\tat 
"), "Stack trace should include frame lines");
+    }
+}
+
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/AdaptedBulletin.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/AdaptedBulletin.java
index 9fc0626dc5..623bd45ac9 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/AdaptedBulletin.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/AdaptedBulletin.java
@@ -30,6 +30,7 @@ public class AdaptedBulletin {
     private String level;
     private String category;
     private String message;
+    private String stackTrace;
 
     private String groupId;
     private String groupName;
@@ -85,6 +86,14 @@ public class AdaptedBulletin {
         this.message = message;
     }
 
+    public String getStackTrace() {
+        return stackTrace;
+    }
+
+    public void setStackTrace(String stackTrace) {
+        this.stackTrace = stackTrace;
+    }
+
     public String getSourceId() {
         return sourceId;
     }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/BulletinAdapter.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/BulletinAdapter.java
index 8a111f1cb0..9e6c17cc28 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/BulletinAdapter.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/jaxb/BulletinAdapter.java
@@ -31,12 +31,15 @@ public class BulletinAdapter extends 
XmlAdapter<AdaptedBulletin, Bulletin> {
             return null;
         }
         // TODO - timestamp is overridden here with a new timestamp... address?
+        final Bulletin bulletin;
         if (b.getSourceId() == null) {
-            return BulletinFactory.createBulletin(b.getCategory(), 
b.getLevel(), b.getMessage());
+            bulletin = BulletinFactory.createBulletin(b.getCategory(), 
b.getLevel(), b.getMessage());
         } else {
-            return BulletinFactory.createBulletin(b.getGroupId(), 
b.getGroupName(), b.getSourceId(), b.getSourceType(),
+            bulletin = BulletinFactory.createBulletin(b.getGroupId(), 
b.getGroupName(), b.getSourceId(), b.getSourceType(),
                     b.getSourceName(), b.getCategory(), b.getLevel(), 
b.getMessage());
         }
+        bulletin.setStackTrace(b.getStackTrace());
+        return bulletin;
     }
 
     @Override
@@ -55,6 +58,7 @@ public class BulletinAdapter extends 
XmlAdapter<AdaptedBulletin, Bulletin> {
         aBulletin.setCategory(b.getCategory());
         aBulletin.setLevel(b.getLevel());
         aBulletin.setMessage(b.getMessage());
+        aBulletin.setStackTrace(b.getStackTrace());
         return aBulletin;
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
index bb4ad15611..cbcd85fcb8 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/FlowAnalysisRuleLogObserver.java
@@ -39,7 +39,7 @@ public class FlowAnalysisRuleLogObserver implements 
LogObserver {
         final String bulletinLevel = message.getLogLevel() == LogLevel.WARN ? 
Severity.WARNING.name() : message.getLogLevel().toString();
 
         final Bulletin bulletin = BulletinFactory.createBulletin(null, 
flowAnalysisRuleNode.getIdentifier(), ComponentType.FLOW_ANALYSIS_RULE,
-            flowAnalysisRuleNode.getName(), "Log Message", bulletinLevel, 
message.getMessage());
+            flowAnalysisRuleNode.getName(), "Log Message", bulletinLevel, 
message.getMessage(), message.getThrowable());
         bulletinRepository.addBulletin(bulletin);
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/ParameterProviderLogObserver.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/ParameterProviderLogObserver.java
index e4aa3e0037..210866ab7c 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/ParameterProviderLogObserver.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/logging/ParameterProviderLogObserver.java
@@ -39,7 +39,7 @@ public class ParameterProviderLogObserver implements 
LogObserver {
         final String bulletinLevel = message.getLogLevel() == LogLevel.WARN ? 
Severity.WARNING.name() : message.getLogLevel().toString();
 
         final Bulletin bulletin = BulletinFactory.createBulletin(null, 
parameterProviderNode.getIdentifier(), ComponentType.PARAMETER_PROVIDER,
-            parameterProviderNode.getName(), "Log Message", bulletinLevel, 
message.getMessage());
+            parameterProviderNode.getName(), "Log Message", bulletinLevel, 
message.getMessage(), message.getThrowable());
         bulletinRepository.addBulletin(bulletin);
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/jaxb/BulletinAdapterStackTraceTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/jaxb/BulletinAdapterStackTraceTest.java
new file mode 100644
index 0000000000..6854d4f325
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/jaxb/BulletinAdapterStackTraceTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.nifi.jaxb;
+
+import org.apache.nifi.events.BulletinFactory;
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.ComponentType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class BulletinAdapterStackTraceTest {
+
+    @Test
+    void testMarshalUnmarshalCarriesStackTrace() throws Exception {
+        final Throwable t = new NullPointerException("npe");
+        final Bulletin original = BulletinFactory.createBulletin(
+                "g", "G", "id", ComponentType.PROCESSOR, "Name",
+                "Category", "ERROR", "msg", "/G", null, t);
+
+        final BulletinAdapter adapter = new BulletinAdapter();
+        final AdaptedBulletin adapted = adapter.marshal(original);
+        assertNotNull(adapted);
+        assertEquals(original.getStackTrace(), adapted.getStackTrace(), 
"AdaptedBulletin must copy stackTrace");
+
+        final Bulletin roundTrip = adapter.unmarshal(adapted);
+        assertNotNull(roundTrip);
+        assertEquals(original.getStackTrace(), roundTrip.getStackTrace(), 
"Unmarshalled Bulletin must preserve stackTrace");
+    }
+}
+
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index e4b234dd82..0d8685ce1b 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -2573,7 +2573,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
     public BulletinEntity createBulletin(final BulletinDTO bulletinDTO, final 
Boolean canRead) {
         final Bulletin bulletin = 
BulletinFactory.createBulletin(bulletinDTO.getCategory(), 
bulletinDTO.getLevel(), bulletinDTO.getMessage());
         bulletinRepository.addBulletin(bulletin);
-        return 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
canRead);
+        return 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), canRead);
     }
 
     @Override
@@ -4109,7 +4109,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         final List<BulletinEntity> bulletinEntities = new ArrayList<>();
         for (final ListIterator<Bulletin> bulletinIter = 
results.listIterator(results.size()); bulletinIter.hasPrevious(); ) {
             final Bulletin bulletin = bulletinIter.previous();
-            
bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin),
 authorizeBulletin(bulletin)));
+            
bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin,
 true), authorizeBulletin(bulletin)));
         }
 
         // create the bulletin board
@@ -4465,7 +4465,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 final Authorizable controllerServiceAuthorizable = 
authorizableLookup.getControllerService(bulletin.getSourceId()).getAuthorizable();
                 final boolean controllerServiceAuthorized = 
controllerServiceAuthorizable.isAuthorized(authorizer, RequestAction.READ, 
user);
 
-                final BulletinEntity controllerServiceBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
controllerServiceAuthorized);
+                final BulletinEntity controllerServiceBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), controllerServiceAuthorized);
                 
controllerServiceBulletinEntities.add(controllerServiceBulletin);
                 controllerBulletinEntities.add(controllerServiceBulletin);
             } catch (final ResourceNotFoundException ignored) {
@@ -4483,7 +4483,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 final Authorizable reportingTaskAuthorizable = 
authorizableLookup.getReportingTask(bulletin.getSourceId()).getAuthorizable();
                 final boolean reportingTaskAuthorizableAuthorized = 
reportingTaskAuthorizable.isAuthorized(authorizer, RequestAction.READ, user);
 
-                final BulletinEntity reportingTaskBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
reportingTaskAuthorizableAuthorized);
+                final BulletinEntity reportingTaskBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), reportingTaskAuthorizableAuthorized);
                 reportingTaskBulletinEntities.add(reportingTaskBulletin);
                 controllerBulletinEntities.add(reportingTaskBulletin);
             } catch (final ResourceNotFoundException ignored) {
@@ -4501,7 +4501,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 final Authorizable flowAnalysisRuleAuthorizable = 
authorizableLookup.getFlowAnalysisRule(bulletin.getSourceId()).getAuthorizable();
                 final boolean flowAnalysisRuleAuthorizableAuthorized = 
flowAnalysisRuleAuthorizable.isAuthorized(authorizer, RequestAction.READ, user);
 
-                final BulletinEntity flowAnalysisRuleBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
flowAnalysisRuleAuthorizableAuthorized);
+                final BulletinEntity flowAnalysisRuleBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), flowAnalysisRuleAuthorizableAuthorized);
                 flowAnalysisRuleBulletinEntities.add(flowAnalysisRuleBulletin);
                 controllerBulletinEntities.add(flowAnalysisRuleBulletin);
             } catch (final ResourceNotFoundException ignored) {
@@ -4519,7 +4519,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 final Authorizable parameterProviderAuthorizable = 
authorizableLookup.getParameterProvider(bulletin.getSourceId()).getAuthorizable();
                 final boolean parameterProviderAuthorizableAuthorized = 
parameterProviderAuthorizable.isAuthorized(authorizer, RequestAction.READ, 
user);
 
-                final BulletinEntity parameterProviderBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
parameterProviderAuthorizableAuthorized);
+                final BulletinEntity parameterProviderBulletin = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), parameterProviderAuthorizableAuthorized);
                 
parameterProviderBulletinEntities.add(parameterProviderBulletin);
                 controllerBulletinEntities.add(parameterProviderBulletin);
             } catch (final ResourceNotFoundException ignored) {
@@ -4537,7 +4537,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 final Authorizable flowRegistryClientAuthorizable = 
authorizableLookup.getFlowRegistryClient(bulletin.getSourceId()).getAuthorizable();
                 final boolean flowRegistryClientkAuthorizableAuthorized = 
flowRegistryClientAuthorizable.isAuthorized(authorizer, RequestAction.READ, 
user);
 
-                final BulletinEntity flowRegistryClient = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin), 
flowRegistryClientkAuthorizableAuthorized);
+                final BulletinEntity flowRegistryClient = 
entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin, 
false), flowRegistryClientkAuthorizableAuthorized);
                 flowRegistryClientBulletinEntities.add(flowRegistryClient);
                 controllerBulletinEntities.add(flowRegistryClient);
             } catch (final ResourceNotFoundException ignored) {
@@ -4795,7 +4795,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
 
         List<BulletinEntity> bulletinEntities = new ArrayList<>();
         for (final Bulletin bulletin : bulletins) {
-            
bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin),
 authorizeBulletin(bulletin)));
+            
bulletinEntities.add(entityFactory.createBulletinEntity(dtoFactory.createBulletinDto(bulletin,
 false), authorizeBulletin(bulletin)));
         }
 
         return pruneAndSortBulletins(bulletinEntities, 
BulletinRepository.MAX_BULLETINS_PER_COMPONENT);
@@ -6599,7 +6599,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         final RevisionDTO revisionDto = dtoFactory.createRevisionDTO(revision);
         final PermissionsDTO permissionsDto = 
dtoFactory.createPermissionsDto(processor);
         final List<BulletinEntity> bulletins = 
bulletinRepository.findBulletinsForSource(id).stream()
-                .map(bulletin -> dtoFactory.createBulletinDto(bulletin))
+                .map(bulletin -> dtoFactory.createBulletinDto(bulletin, false))
                 .map(bulletin -> entityFactory.createBulletinEntity(bulletin, 
permissionsDto.getCanRead()))
                 .collect(Collectors.toList());
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 737410f066..1c401ba690 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.web.api.dto;
 
+import jakarta.ws.rs.WebApplicationException;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -119,9 +120,9 @@ import org.apache.nifi.diagnostics.DiagnosticLevel;
 import org.apache.nifi.diagnostics.GarbageCollection;
 import org.apache.nifi.diagnostics.StorageUsage;
 import org.apache.nifi.diagnostics.SystemDiagnostics;
-import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.flow.VersionedComponent;
 import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flowanalysis.FlowAnalysisRule;
 import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.groups.ProcessGroup;
@@ -255,8 +256,6 @@ import 
org.apache.nifi.web.api.entity.RemoteProcessGroupStatusSnapshotEntity;
 import org.apache.nifi.web.api.entity.TenantEntity;
 import org.apache.nifi.web.controller.ControllerFacade;
 import org.apache.nifi.web.revision.RevisionManager;
-
-import jakarta.ws.rs.WebApplicationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -3466,18 +3465,19 @@ public final class DtoFactory {
    public List<BulletinDTO> createBulletinDtos(final List<Bulletin> bulletins) 
{
        final List<BulletinDTO> bulletinDtos = new 
ArrayList<>(bulletins.size());
        for (final Bulletin bulletin : bulletins) {
-           bulletinDtos.add(createBulletinDto(bulletin));
+           bulletinDtos.add(createBulletinDto(bulletin, false));
        }
        return bulletinDtos;
    }
 
    /**
-    * Creates a BulletinDTO for the specified Bulletin.
+    * Creates a BulletinDTO for the specified Bulletin with optional stack 
trace inclusion.
     *
     * @param bulletin bulletin
+    * @param includeStackTrace whether to include stack trace
     * @return dto
     */
-   public BulletinDTO createBulletinDto(final Bulletin bulletin) {
+   public BulletinDTO createBulletinDto(final Bulletin bulletin, final boolean 
includeStackTrace) {
        final BulletinDTO dto = new BulletinDTO();
        dto.setId(bulletin.getId());
        dto.setNodeAddress(bulletin.getNodeAddress());
@@ -3489,6 +3489,7 @@ public final class DtoFactory {
        dto.setLevel(bulletin.getLevel());
        dto.setMessage(bulletin.getMessage());
        dto.setSourceType(bulletin.getSourceType().name());
+       dto.setStackTrace(includeStackTrace ? bulletin.getStackTrace() : null);
        return dto;
    }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryBulletinStackTraceTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryBulletinStackTraceTest.java
new file mode 100644
index 0000000000..dc06fcd62c
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryBulletinStackTraceTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.nifi.web.api.dto;
+
+import org.apache.nifi.events.BulletinFactory;
+import org.apache.nifi.reporting.Bulletin;
+import org.apache.nifi.reporting.ComponentType;
+import org.junit.jupiter.api.Test;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class DtoFactoryBulletinStackTraceTest {
+
+    private static String toStackTrace(final Throwable t) {
+        final StringWriter sw = new StringWriter();
+        try (PrintWriter pw = new PrintWriter(sw)) {
+            t.printStackTrace(pw);
+        }
+        return sw.toString();
+    }
+
+    @Test
+    void testBulletinDtoDoesNotIncludeStackTraceByDefault() {
+        final Throwable ex = new IllegalArgumentException("invalid");
+        final Bulletin bulletin = BulletinFactory.createBulletin(
+                "pg", "PG", "svc1", ComponentType.CONTROLLER_SERVICE, "CS",
+                "Log Message", "ERROR", "boom", "/PG", null, ex);
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        final BulletinDTO dto = dtoFactory.createBulletinDto(bulletin, false);
+
+        assertNotNull(dto);
+        assertEquals(null, dto.getStackTrace(), "DTO must not include 
stackTrace by default");
+    }
+
+    @Test
+    void testBulletinDtoIncludesStackTraceWhenRequested() {
+        final Throwable ex = new IllegalArgumentException("invalid");
+        final Bulletin bulletin = BulletinFactory.createBulletin(
+                "pg", "PG", "svc1", ComponentType.CONTROLLER_SERVICE, "CS",
+                "Log Message", "ERROR", "boom", "/PG", null, ex);
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        final BulletinDTO dto = dtoFactory.createBulletinDto(bulletin, true);
+
+        assertNotNull(dto);
+        assertEquals(toStackTrace(ex), dto.getStackTrace(), "DTO must carry 
stackTrace when requested");
+    }
+}


Reply via email to