3434import org .apache .logging .log4j .core .appender .RollingFileAppender ;
3535import org .apache .logging .log4j .core .config .Configuration ;
3636import org .apache .shiro .subject .Subject ;
37+ import org .graylog .plugins .pipelineprocessor .rest .ProcessingLoadResponse ;
38+ import org .graylog .plugins .pipelineprocessor .rest .ProcessingLoadBuilder ;
3739import org .graylog .security .certutil .KeyStoreDto ;
3840import org .graylog2 .cluster .NodeService ;
3941import org .graylog2 .cluster .nodes .DataNodeDto ;
4244import org .graylog2 .log4j .MemoryAppender ;
4345import org .graylog2 .plugin .system .SimpleNodeId ;
4446import org .graylog2 .rest .RemoteInterfaceProvider ;
47+ import org .graylog2 .rest .models .system .metrics .requests .MetricsReadRequest ;
4548import org .graylog2 .rest .models .system .metrics .responses .MetricsSummaryResponse ;
4649import org .graylog2 .rest .models .system .plugins .responses .PluginList ;
4750import org .graylog2 .rest .models .system .responses .SystemJVMResponse ;
@@ -134,6 +137,7 @@ public class SupportBundleService {
134137 private final List <URI > elasticsearchHosts ;
135138 private final ClusterAdapter searchDbClusterAdapter ;
136139 private final DatanodeRestApiProxy datanodeProxy ;
140+ private final ProcessingLoadBuilder processingLoadBuilder ;
137141
138142 @ Inject
139143 public SupportBundleService (@ Named ("proxiedRequestsExecutorService" ) ExecutorService executor ,
@@ -145,7 +149,9 @@ public SupportBundleService(@Named("proxiedRequestsExecutorService") ExecutorSer
145149 ClusterStatsService clusterStatsService ,
146150 VersionProbeFactory searchDbProbeFactory ,
147151 @ IndexerHosts List <URI > searchDbHosts ,
148- ClusterAdapter searchDbClusterAdapter , DatanodeRestApiProxy datanodeProxy ) {
152+ ClusterAdapter searchDbClusterAdapter ,
153+ DatanodeRestApiProxy datanodeProxy ,
154+ ProcessingLoadBuilder processingLoadBuilder ) {
149155 this .executor = executor ;
150156 this .nodeService = nodeService ;
151157 this .datanodeService = datanodeService ;
@@ -157,6 +163,7 @@ public SupportBundleService(@Named("proxiedRequestsExecutorService") ExecutorSer
157163 this .elasticsearchHosts = searchDbHosts ;
158164 this .searchDbClusterAdapter = searchDbClusterAdapter ;
159165 this .datanodeProxy = datanodeProxy ;
166+ this .processingLoadBuilder = processingLoadBuilder ;
160167 }
161168
162169 public void buildBundle (HttpHeaders httpHeaders , Subject currentSubject ) {
@@ -190,7 +197,9 @@ private void collectBundleData(ProxiedResourceHelper proxiedResourceHelper,
190197
191198 // fetchClusterInfos runs concurrently with per-node collection — they are independent.
192199 // Its requestOnAllNodes wrappers run on their own orchestrationExecutor (created inside
193- // the method), so only 4 simple leaf tasks land on `executor` — no starvation risk.
200+ // the method), so only 4 simple leaf tasks land on `executor`.
201+ // tryFetchProcessingLoadSnapshot adds one more `executor` task that runs at most one fan-out inline
202+ // (none when debug metrics are off).
194203 final List <CompletableFuture <Void >> futures = new ArrayList <>();
195204
196205 nodeManifests .entrySet ().stream ()
@@ -204,6 +213,11 @@ private void collectBundleData(ProxiedResourceHelper proxiedResourceHelper,
204213 futures .add (CompletableFuture .runAsync (
205214 () -> tryFetchClusterInfos (proxiedResourceHelper , nodeManifests , spoolDir , errors ), executor ));
206215
216+ futures .add (CompletableFuture .runAsync (
217+ () -> tryFetchProcessingLoadSnapshot (proxiedResourceHelper , spoolDir , errors ),
218+ executor
219+ ));
220+
207221 CompletableFuture .allOf (futures .toArray (CompletableFuture []::new )).get ();
208222
209223 writeErrors (errors , spoolDir );
@@ -231,6 +245,52 @@ private void tryFetchClusterInfos(ProxiedResourceHelper proxiedResourceHelper,
231245 }
232246 }
233247
248+ private void tryFetchProcessingLoadSnapshot (ProxiedResourceHelper proxiedResourceHelper ,
249+ Path spoolDir , List <BundleError > errors ) {
250+ try {
251+ if (!processingLoadBuilder .metricsEnabled ()) {
252+ return ;
253+ }
254+ fetchProcessingLoadSnapshot (proxiedResourceHelper , spoolDir , errors );
255+ } catch (Exception e ) {
256+ LOG .warn ("Failed to collect pipeline processing-load snapshot for support bundle, skipping" , e );
257+ errors .add (BundleError .of ("cluster/processing-load" , e ));
258+ }
259+ }
260+
261+ private void fetchProcessingLoadSnapshot (ProxiedResourceHelper proxiedResourceHelper , Path spoolDir ,
262+ List <BundleError > errors ) throws IOException {
263+ final ProcessingLoadResponse snapshot = processingLoadBuilder .buildUnfiltered (
264+ timerNames -> fetchPerNodeMetrics (proxiedResourceHelper , timerNames , errors ));
265+
266+ if (!snapshot .available ()) {
267+ errors .add (new BundleError ("cluster/processing-load" ,
268+ "Debug metrics are enabled but the Processing Load snapshot had no usable data "
269+ + "(no active pipeline rules, no traffic in the last collection window, or "
270+ + "per-node metric collection failed. See any node-level processing-load "
271+ + "errors). Snapshot omitted." , null ));
272+ return ;
273+ }
274+
275+ try (var snapshotFile = new FileOutputStream (spoolDir .resolve ("pipeline-processing-load.json" ).toFile ())) {
276+ objectMapper .writerWithDefaultPrettyPrinter ().writeValue (snapshotFile , snapshot );
277+ }
278+ }
279+
280+ private Map <String , MetricsSummaryResponse > fetchPerNodeMetrics (
281+ ProxiedResourceHelper proxiedResourceHelper , List <String > timerNames , List <BundleError > errors ) {
282+ final MetricsReadRequest request = MetricsReadRequest .create (timerNames );
283+ return stripCallResult (
284+ proxiedResourceHelper .requestOnAllNodes (
285+ RemoteMetricsResource .class ,
286+ r -> r .multipleMetrics (request ),
287+ CALL_TIMEOUT
288+ ),
289+ errors ,
290+ "processing-load"
291+ );
292+ }
293+
234294 private void fetchClusterInfos (ProxiedResourceHelper proxiedResourceHelper , Map <String , SupportBundleNodeManifest > nodeManifests , Path tmpDir , List <BundleError > errors ) throws IOException {
235295 // requestOnAllNodes submits per-node tasks to `executor` then blocks on Future#get.
236296 // A separate short-lived orchestration executor runs these blocking fan-outs so they
0 commit comments