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

pvillard31 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 0ef04bc95c9 NIFI-15979 Wire Connector MDC and reporting-task 
visibility (#11330)
0ef04bc95c9 is described below

commit 0ef04bc95c9b3946d51bc76d948250f2d34f3318
Author: Kevin Doran <[email protected]>
AuthorDate: Fri Jun 12 23:19:09 2026 -0400

    NIFI-15979 Wire Connector MDC and reporting-task visibility (#11330)
    
    Surface Connector observability that was part of nifi-api 2.9.0:
    - ConnectorNode.getLoggingAttributes() plus 
StandardConnectorNode/StandardProcessGroup
      propagation of connector identity into descendant-component MDC.
    - StandardEventAccess.getConnectorStatuses() returns a ConnectorStatus (id, 
name, root
      group status) per connector; StatusHistoryRepository gains a capture 
overload accepting
      Collection<ConnectorStatus>; FlowController invokes it.
---
 .../src/main/asciidoc/administration-guide.adoc    |  16 ++-
 .../connector/ConnectorConfigurationProvider.java  |  29 ++++++
 .../status/history/StatusHistoryRepository.java    |  21 ++++
 .../EmbeddedQuestDbStatusHistoryRepository.java    |  27 +++++
 .../AbstractStatusHistoryRepositoryTest.java       |  21 ++++
 ...DbStatusHistoryRepositoryForComponentsTest.java |  19 ++++
 .../apache/nifi/groups/StandardProcessGroup.java   |  48 +++++++++
 .../nifi/groups/StandardProcessGroupTest.java      | 108 ++++++++++++++++++++
 .../nifi/components/connector/ConnectorNode.java   |  25 +++++
 .../java/org/apache/nifi/groups/ProcessGroup.java  |  22 ++++
 .../connector/ConnectorLoggingAttribute.java       |  62 ++++++++++++
 .../connector/StandardConnectorNode.java           | 112 ++++++++++++++++++++-
 .../connector/StandardConnectorRepository.java     |  42 ++++++++
 .../apache/nifi/controller/ExtensionBuilder.java   |  16 ++-
 .../org/apache/nifi/controller/FlowController.java |   5 +-
 .../history/VolatileComponentStatusRepository.java |  20 ++++
 .../apache/nifi/reporting/StandardEventAccess.java |  53 +++++++++-
 .../connector/TestStandardConnectorNode.java       |  82 +++++++++++++++
 .../connector/TestStandardConnectorRepository.java | 102 +++++++++++++++++++
 .../AbstractStatusHistoryRepositoryTest.java       |  21 ++++
 ...latileComponentStatusRepositoryForNodeTest.java |  29 ++++++
 21 files changed, 871 insertions(+), 9 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc 
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index d5e4234bc39..f7ed3cf5702 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -211,11 +211,25 @@ Component logs provide the following MDC named values:
 - `processGroupName` contains the name of the Process Group
 - `processGroupNamePath` contains of the hierarchy of names for Process Groups 
with separators
 
+Components that run inside a Connector-managed flow also carry 
framework-supplied MDC values that identify the owning
+Connector:
+
+- `connectorId` contains the UUID of the Connector
+- `connectorName` contains the user-visible name of the Connector
+- `connectorComponent` contains the fully qualified class name of the 
Connector implementation
+- `connectorBundleGroup`, `connectorBundleArtifact`, `connectorBundleVersion` 
identify the NAR bundle the Connector was
+  loaded from
+
+Additional implementation-specific MDC values may be supplied by the framework
+`ConnectorConfigurationProvider` extension via `getLoggingAttributes(String 
connectorId)`. The framework consults the
+provider at connector creation time and merges the result with the 
framework-managed keys above. Keys reserved by the
+framework (those listed above) cannot be overridden; attempts to do so are 
dropped and logged as a `WARN`.
+
 MDC named values can be added to a Logback pattern layout using the `mdc` 
conversion word.
 
 [source, xml]
 ----
-<pattern>%date %level [%thread] %mdc{processGroupId} %logger{40} 
%msg%n</pattern>
+<pattern>%date %level [%thread] %mdc{connectorId} %mdc{processGroupId} 
%logger{40} %msg%n</pattern>
 ----
 
 Logs from classes other than extension components do not have MDC named 
values. Logs formatted using the pattern layout
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
index a6327b2a8ba..16e5ae6e9b2 100644
--- 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
@@ -20,6 +20,7 @@ package org.apache.nifi.components.connector;
 import org.apache.nifi.flow.VersionedConnectorState;
 
 import java.io.InputStream;
+import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -212,6 +213,34 @@ public interface ConnectorConfigurationProvider {
         return true;
     }
 
+    /**
+     * Returns identity-shaped logging attributes the framework should include 
in the SLF4J {@code MDC} for
+     * every log line emitted by the Connector with the given identifier and 
by any component (Processor,
+     * Controller Service, etc.) running inside its managed flow. The same 
attributes are also snapshotted
+     * onto {@code ProcessGroupStatus} so that status-based reporting tasks 
(e.g. OpenTelemetry) can attach
+     * them as metric attributes.
+     *
+     * <p>The framework reserves a set of well-known keys that describe the 
Connector itself (identity,
+     * bundle coordinate, etc.). Any entry in the returned map whose key 
collides with a reserved
+     * framework-managed key is dropped by the framework and a {@code WARN} is 
logged for that entry;
+     * the remaining entries are accepted.</p>
+     *
+     * <p>The framework consults this method once, at connector create time. 
It is the provider's
+     * responsibility to ensure the returned values are stable for the 
lifetime of the connector
+     * instance or to publish updates through other means; mid-lifecycle 
refresh is not currently
+     * defined.</p>
+     *
+     * <p>The default returns {@link Map#of()} so that providers that do not 
need to publish additional
+     * attributes (and runtimes that have no provider configured) get only the 
framework-managed identity
+     * keys in MDC.</p>
+     *
+     * @param connectorId the identifier of the connector whose logging 
attributes are being requested
+     * @return an immutable map of attribute keys to values; never {@code null}
+     */
+    default Map<String, String> getLoggingAttributes(final String connectorId) 
{
+        return Map.of();
+    }
+
     /**
      * Ensures that local asset binaries are up to date with the external 
store. For each asset
      * tracked in the provider's local state, this method compares the 
external store's current
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/StatusHistoryRepository.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/StatusHistoryRepository.java
index 2b96feb6192..b0225855281 100644
--- 
a/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/StatusHistoryRepository.java
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/controller/status/history/StatusHistoryRepository.java
@@ -16,9 +16,11 @@
  */
 package org.apache.nifi.controller.status.history;
 
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.NodeStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 
@@ -48,6 +50,25 @@ public interface StatusHistoryRepository {
      */
     void capture(NodeStatus nodeStatus, ProcessGroupStatus rootGroupStatus, 
List<GarbageCollectionStatus> garbageCollectionStatus, Date timestamp);
 
+    /**
+     * Captures the status information provided in the given report, 
additionally providing the status of each
+     * Connector-managed flow. Connector-managed Process Groups are siblings 
of the root group and are not reachable
+     * from {@code rootGroupStatus}, so this overload allows implementations 
to observe them (for example, to export
+     * Connector-managed flow metrics). The default implementation ignores 
{@code connectorStatuses} and delegates to
+     * {@link #capture(NodeStatus, ProcessGroupStatus, List, Date)} so 
existing implementations continue to work
+     * unchanged.
+     *
+     * @param nodeStatus status of the node
+     * @param rootGroupStatus status of the root group and its content
+     * @param connectorStatuses status of each registered Connector, each 
carrying its managed root group status
+     * @param garbageCollectionStatus status of garbage collection
+     * @param timestamp timestamp of capture
+     */
+    default void capture(NodeStatus nodeStatus, ProcessGroupStatus 
rootGroupStatus, Collection<ConnectorStatus> connectorStatuses,
+                         List<GarbageCollectionStatus> 
garbageCollectionStatus, Date timestamp) {
+        capture(nodeStatus, rootGroupStatus, garbageCollectionStatus, 
timestamp);
+    }
+
     /**
      * @param connectionId the ID of the Connection for which the Status is
      * desired
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/main/java/org/apache/nifi/controller/status/history/questdb/EmbeddedQuestDbStatusHistoryRepository.java
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/main/java/org/apache/nifi/controller/status/history/questdb/EmbeddedQuestDbStatusHistoryRepository.java
index 81966b606e2..4afd292ddd3 100644
--- 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/main/java/org/apache/nifi/controller/status/history/questdb/EmbeddedQuestDbStatusHistoryRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/main/java/org/apache/nifi/controller/status/history/questdb/EmbeddedQuestDbStatusHistoryRepository.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.controller.status.history.questdb;
 
 import org.apache.nifi.controller.status.ConnectionStatus;
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.NodeStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 import org.apache.nifi.controller.status.ProcessorStatus;
@@ -126,6 +127,32 @@ public class EmbeddedQuestDbStatusHistoryRepository 
implements StatusHistoryRepo
         updateComponentDetails(rootGroupStatus);
     }
 
+    @Override
+    public void capture(final NodeStatus nodeStatus, final ProcessGroupStatus 
rootGroupStatus, final Collection<ConnectorStatus> connectorStatuses,
+                        final List<GarbageCollectionStatus> 
garbageCollectionStatus, final Date timestamp) {
+        final Instant captured = timestamp.toInstant();
+        captureNodeStatus(nodeStatus, captured);
+        captureGarbageCollectionStatus(garbageCollectionStatus, captured);
+        captureComponentStatus(rootGroupStatus, captured);
+
+        // Connector-managed flows are siblings of the root group and 
therefore are not reachable from
+        // rootGroupStatus. Capture each Connector's managed flow so that 
components running inside it have
+        // status history just like components in the primary flow. Component 
details for the root group and
+        // every connector-managed flow are accumulated into a single map so 
that one does not overwrite another.
+        final Map<String, ComponentDetails> componentDetails = new HashMap<>();
+        updateComponentDetails(rootGroupStatus, componentDetails);
+        if (connectorStatuses != null) {
+            for (final ConnectorStatus connectorStatus : connectorStatuses) {
+                final ProcessGroupStatus connectorRootGroupStatus = 
connectorStatus.getRootGroupStatus();
+                if (connectorRootGroupStatus != null) {
+                    captureComponentStatus(connectorRootGroupStatus, captured);
+                    updateComponentDetails(connectorRootGroupStatus, 
componentDetails);
+                }
+            }
+        }
+        componentDetailsProvider.setComponentDetails(componentDetails);
+    }
+
     private void captureNodeStatus(final NodeStatus nodeStatus, final Instant 
captured) {
         storage.storeNodeStatuses(Collections.singleton(new 
CapturedStatus<>(nodeStatus, captured)));
     }
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
index 62f865721c9..2b032e62b89 100644
--- 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.controller.status.history;
 
 import org.apache.nifi.controller.status.ConnectionStatus;
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.NodeStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 import org.apache.nifi.controller.status.ProcessorStatus;
@@ -41,6 +42,9 @@ public abstract class AbstractStatusHistoryRepositoryTest {
     protected static final String PROCESSOR_WITH_COUNTER_ID = 
"f09c5bb8-7b67-4d3b-81c5-5373e1c03ed1";
     protected static final String CONNECTION_ID = 
"d5452f70-a0d9-44c5-aeda-c2026482e4ee";
     protected static final String REMOTE_PROCESS_GROUP_ID = 
"f5f4fe2a-0209-4ba7-8f15-f33df942cde5";
+    protected static final String CONNECTOR_ID = 
"a1111111-1111-1111-1111-111111111111";
+    protected static final String CONNECTOR_ROOT_GROUP_ID = 
"a2222222-2222-2222-2222-222222222222";
+    protected static final String CONNECTOR_PROCESSOR_ID = 
"a3333333-3333-3333-3333-333333333333";
 
     protected ProcessGroupStatus givenSimpleRootProcessGroupStatus() {
         final ProcessGroupStatus status = new ProcessGroupStatus();
@@ -74,6 +78,23 @@ public abstract class AbstractStatusHistoryRepositoryTest {
         return status;
     }
 
+    protected ConnectorStatus givenConnectorStatus() {
+        final ProcessorStatus connectorProcessor = givenProcessorStatus();
+        connectorProcessor.setId(CONNECTOR_PROCESSOR_ID);
+        connectorProcessor.setName("ConnectorProcessor");
+
+        final ProcessGroupStatus connectorRootGroup = 
givenSimpleRootProcessGroupStatus();
+        connectorRootGroup.setId(CONNECTOR_ROOT_GROUP_ID);
+        connectorRootGroup.setName("Connector Root");
+        
connectorRootGroup.setProcessorStatus(Collections.singleton(connectorProcessor));
+
+        final ConnectorStatus connectorStatus = new ConnectorStatus();
+        connectorStatus.setId(CONNECTOR_ID);
+        connectorStatus.setName("Test Connector");
+        connectorStatus.setRootGroupStatus(connectorRootGroup);
+        return connectorStatus;
+    }
+
     protected ProcessGroupStatus givenChildProcessGroupStatus() {
         final ProcessGroupStatus status = new ProcessGroupStatus();
         status.setId(CHILD_GROUP_ID);
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/EmbeddedQuestDbStatusHistoryRepositoryForComponentsTest.java
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/EmbeddedQuestDbStatusHistoryRepositoryForComponentsTest.java
index e9e9db41b6d..95d1ce5096e 100644
--- 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/EmbeddedQuestDbStatusHistoryRepositoryForComponentsTest.java
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-questdb-bundle/nifi-questdb-status-history/src/test/java/org/apache/nifi/controller/status/history/EmbeddedQuestDbStatusHistoryRepositoryForComponentsTest.java
@@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -79,6 +80,24 @@ public class 
EmbeddedQuestDbStatusHistoryRepositoryForComponentsTest extends Abs
         assertStatusHistoryIsEmpty(rootGroupStatus2);
     }
 
+    @Test
+    public void testWritingThenReadingConnectorManagedFlowComponents() throws 
Exception {
+        repository.capture(new NodeStatus(), givenRootProcessGroupStatus(), 
List.of(givenConnectorStatus()), new ArrayList<>(), INSERTED_AT);
+        waitUntilPersisted();
+
+        // The Connector's managed root group is captured even though it is a 
sibling of (not reachable from) the root group
+        final StatusHistory connectorRootGroupStatus = 
repository.getProcessGroupStatusHistory(CONNECTOR_ROOT_GROUP_ID, START, END, 
PREFERRED_DATA_POINTS);
+        assertCorrectStatusHistory(connectorRootGroupStatus, 
CONNECTOR_ROOT_GROUP_ID, "Connector Root");
+
+        // A processor running inside the Connector-managed flow has status 
history just like a primary-flow processor
+        final StatusHistory connectorProcessorStatus = 
repository.getProcessorStatusHistory(CONNECTOR_PROCESSOR_ID, START, END, 
PREFERRED_DATA_POINTS, false);
+        assertCorrectStatusHistory(connectorProcessorStatus, 
CONNECTOR_PROCESSOR_ID, "ConnectorProcessor");
+
+        // The primary root flow is still captured alongside the connector flow
+        final StatusHistory rootGroupStatus = 
repository.getProcessGroupStatusHistory(ROOT_GROUP_ID, START, END, 
PREFERRED_DATA_POINTS);
+        assertCorrectStatusHistory(rootGroupStatus, ROOT_GROUP_ID, "Root");
+    }
+
     @Test
     public void testReadingLimitedByPreferredDataPoints() throws Exception {
         repository.capture(new NodeStatus(), 
givenSimpleRootProcessGroupStatus(), new ArrayList<>(), new 
Date(INSERTED_AT.getTime() - TimeUnit.MINUTES.toMillis(8)));
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
index 91f3bdbde4e..610eb3f6413 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java
@@ -234,6 +234,7 @@ public final class StandardProcessGroup implements 
ProcessGroup {
     private static final String UNREGISTERED_PATH_SEGMENT = "UNREGISTERED";
 
     private final Map<String, String> loggingAttributes = new 
ConcurrentHashMap<>();
+    private volatile Map<String, String> connectorLoggingAttributes = Map.of();
     private volatile String logFileSuffix;
 
     public StandardProcessGroup(final String id, final 
ControllerServiceProvider serviceProvider, final ProcessScheduler scheduler,
@@ -308,6 +309,12 @@ public final class StandardProcessGroup implements 
ProcessGroup {
     @Override
     public void setParent(final ProcessGroup newParent) {
         parent.set(newParent);
+        // Inherit connector-supplied MDC attributes from the parent so 
descendants of a connector's managed
+        // flow carry the same connector metadata (attributing their logs and 
status metrics to the connector).
+        // Runs on every re-parent (including initial attach), so PGs added 
later inherit automatically.
+        if (newParent instanceof StandardProcessGroup standardParent) {
+            this.connectorLoggingAttributes = 
standardParent.connectorLoggingAttributes;
+        }
         setLoggingAttributes();
     }
 
@@ -4829,6 +4836,47 @@ public final class StandardProcessGroup implements 
ProcessGroup {
             final String registeredFlowVersion = 
currentVersionControl.getVersion();
             
loggingAttributes.put(LoggingAttribute.REGISTERED_FLOW_VERSION.attribute, 
registeredFlowVersion);
         }
+
+        loggingAttributes.putAll(connectorLoggingAttributes);
+    }
+
+    /**
+     * Stores the connector-managed MDC attributes for this process group and 
cascades the same
+     * attributes to all descendant process groups so that components anywhere 
in the connector's
+     * managed flow log with consistent connectorId/connectorName/etc. context.
+     *
+     * <p>This method is called by {@code StandardConnectorNode} against its 
managed root process
+     * group whenever the connector's framework keys (e.g. {@code 
connectorName}) change or when the
+     * connector provides updated custom logging attributes. Newly created 
descendant groups will
+     * also inherit the attributes lazily via {@link 
#setParent(ProcessGroup)}.</p>
+     *
+     * @param attributes the merged set of connector logging attributes; an 
empty or {@code null}
+     *                   map clears any previously assigned attributes
+     */
+    @Override
+    public void setConnectorLoggingAttributes(final Map<String, String> 
attributes) {
+        final Map<String, String> snapshot = (attributes == null || 
attributes.isEmpty())
+            ? Map.of()
+            : Map.copyOf(attributes);
+        this.connectorLoggingAttributes = snapshot;
+        setLoggingAttributes();
+
+        for (final ProcessGroup child : getProcessGroups()) {
+            if (child instanceof StandardProcessGroup standardChild) {
+                standardChild.setConnectorLoggingAttributes(snapshot);
+            }
+        }
+    }
+
+    /**
+     * Returns the connector-managed MDC attributes currently assigned to this 
process group (inherited from
+     * its connector's managed flow root, or empty if this group is not part 
of a connector flow).
+     *
+     * @return an immutable map of connector logging attributes; never {@code 
null}
+     */
+    @Override
+    public Map<String, String> getConnectorLoggingAttributes() {
+        return connectorLoggingAttributes;
     }
 
     private void setGroupPath() {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
index 35051a30f9f..592721b466e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java
@@ -44,6 +44,7 @@ import java.util.Map;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.when;
@@ -238,4 +239,111 @@ class StandardProcessGroupTest {
 
         assertEquals(expected, loggingAttributes);
     }
+
+    @Test
+    void testSetConnectorLoggingAttributesMergesIntoLoggingAttributes() {
+        processGroup.setName(NAME);
+
+        final Map<String, String> connectorAttributes = Map.of(
+                "connectorId", "connector-1",
+                "connectorName", "My Connector",
+                "connectorComponent", "com.example.MyConnector"
+        );
+
+        processGroup.setConnectorLoggingAttributes(connectorAttributes);
+
+        final Map<String, String> loggingAttributes = 
processGroup.getLoggingAttributes();
+        assertEquals("connector-1", loggingAttributes.get("connectorId"));
+        assertEquals("My Connector", loggingAttributes.get("connectorName"));
+        assertEquals("com.example.MyConnector", 
loggingAttributes.get("connectorComponent"));
+        assertEquals(NAME, 
loggingAttributes.get(StandardProcessGroup.LoggingAttribute.PROCESS_GROUP_NAME.getAttribute()));
+        assertEquals(ID, 
loggingAttributes.get(StandardProcessGroup.LoggingAttribute.PROCESS_GROUP_ID.getAttribute()));
+    }
+
+    @Test
+    void testSetConnectorLoggingAttributesCascadesToChildProcessGroups() {
+        processGroup.setName(NAME);
+
+        final StandardProcessGroup child = 
createStandardProcessGroup("child-id");
+        child.setName("Child");
+        processGroup.addProcessGroup(child);
+
+        final Map<String, String> connectorAttributes = Map.of(
+                "connectorId", "connector-1",
+                "connectorName", "Postgres CDC"
+        );
+
+        processGroup.setConnectorLoggingAttributes(connectorAttributes);
+
+        assertEquals("connector-1", 
child.getLoggingAttributes().get("connectorId"));
+        assertEquals("Postgres CDC", 
child.getLoggingAttributes().get("connectorName"));
+    }
+
+    @Test
+    void testAddProcessGroupInheritsConnectorLoggingAttributesFromParent() {
+        processGroup.setName(NAME);
+        processGroup.setConnectorLoggingAttributes(Map.of(
+                "connectorId", "connector-1",
+                "connectorName", "Postgres CDC"
+        ));
+
+        final StandardProcessGroup lateChild = 
createStandardProcessGroup("late-child-id");
+        lateChild.setName("Late Child");
+        processGroup.addProcessGroup(lateChild);
+
+        assertEquals("connector-1", 
lateChild.getLoggingAttributes().get("connectorId"));
+        assertEquals("Postgres CDC", 
lateChild.getLoggingAttributes().get("connectorName"));
+        assertEquals(Map.copyOf(processGroup.getConnectorLoggingAttributes()), 
lateChild.getConnectorLoggingAttributes());
+    }
+
+    @Test
+    void testEmptyConnectorLoggingAttributesAddsNothing() {
+        processGroup.setName(NAME);
+        processGroup.setConnectorLoggingAttributes(Map.of());
+
+        final Map<String, String> loggingAttributes = 
processGroup.getLoggingAttributes();
+        assertFalse(loggingAttributes.containsKey("connectorId"));
+        assertTrue(processGroup.getConnectorLoggingAttributes().isEmpty());
+        // Verify the PG-level keys are still present.
+        assertEquals(NAME, 
loggingAttributes.get(StandardProcessGroup.LoggingAttribute.PROCESS_GROUP_NAME.getAttribute()));
+    }
+
+    @Test
+    void testSetConnectorLoggingAttributesReplacesPreviousValues() {
+        processGroup.setName(NAME);
+        processGroup.setConnectorLoggingAttributes(Map.of(
+                "connectorId", "connector-1",
+                "connectorName", "Old Name",
+                "customKey", "customValue"
+        ));
+
+        processGroup.setConnectorLoggingAttributes(Map.of(
+                "connectorId", "connector-1",
+                "connectorName", "New Name"
+        ));
+
+        final Map<String, String> loggingAttributes = 
processGroup.getLoggingAttributes();
+        assertEquals("New Name", loggingAttributes.get("connectorName"));
+        assertFalse(loggingAttributes.containsKey("customKey"));
+    }
+
+    private StandardProcessGroup createStandardProcessGroup(final String id) {
+        return new StandardProcessGroup(
+                id,
+                controllerServiceProvider,
+                processScheduler,
+                propertyEncryptor,
+                extensionManager,
+                stateManagerProvider,
+                flowManager,
+                reloadComponent,
+                nodeTypeProvider,
+                clusterTopologyProvider,
+                properties,
+                statelessGroupNodeFactory,
+                assetManager,
+                null
+        );
+    }
+
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
index 1c3c78073b6..fd994aa8e79 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/components/connector/ConnectorNode.java
@@ -35,6 +35,7 @@ import org.apache.nifi.logging.ComponentLog;
 import java.time.Duration;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Future;
 
@@ -67,6 +68,30 @@ public interface ConnectorNode extends 
ComponentAuthorizable, VersionedComponent
 
     BundleCoordinate getBundleCoordinate();
 
+    /**
+     * @return an immutable map of the Connector's logging attributes: the 
framework-managed identity keys
+     *         (such as connector identifier, name, component type, and bundle 
coordinate) merged with any
+     *         provider-supplied custom attributes. These are the attributes 
applied to the MDC of the Connector's
+     *         managed flow and surfaced on Connector metrics. Never {@code 
null}. The default implementation
+     *         returns an empty map.
+     */
+    default Map<String, String> getLoggingAttributes() {
+        return Map.of();
+    }
+
+    /**
+     * Replaces the provider-supplied custom logging attributes for this 
Connector, merging them with the
+     * framework-managed identity keys (see {@link #getLoggingAttributes()}). 
This is invoked by the framework
+     * when the Connector node is added or restored, using attributes sourced 
from the
+     * {@link ConnectorConfigurationProvider} (if one is configured). Keys 
reserved by the framework are ignored.
+     * The default implementation is a no-op.
+     *
+     * @param attributes the proposed custom attributes; {@code null} or empty 
clears any previously supplied
+     *                   custom attributes (the framework-managed keys remain)
+     */
+    default void setCustomLoggingAttributes(final Map<String, String> 
attributes) {
+    }
+
     /**
      * Returns whether or not the underlying extension is missing (i.e., the 
Connector is a GhostConnector).
      * @return true if the extension is missing, false otherwise
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
index 25bf1cd2b22..976004b514b 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/ProcessGroup.java
@@ -1235,6 +1235,28 @@ public interface ProcessGroup extends 
ComponentAuthorizable, Positionable, Versi
      */
     Map<String, String> getLoggingAttributes();
 
+    /**
+     * Returns the connector-managed MDC attributes assigned to this Process 
Group. When this group is part of a
+     * Connector's managed flow, these attributes (for example, connectorId 
and connectorName) are inherited from the
+     * Connector's managed root group so that logs and status metrics emitted 
by components in the flow can be
+     * attributed to the Connector. Returns an empty map when this group is 
not part of a Connector flow.
+     *
+     * @return an immutable map of connector logging attributes; never {@code 
null}
+     */
+    default Map<String, String> getConnectorLoggingAttributes() {
+        return Map.of();
+    }
+
+    /**
+     * Assigns the connector-managed MDC attributes for this Process Group and 
cascades them to all descendant Process
+     * Groups, so that components anywhere in a Connector's managed flow log 
with consistent connector context. An empty
+     * or {@code null} map clears any previously assigned attributes.
+     *
+     * @param attributes the connector logging attributes to assign
+     */
+    default void setConnectorLoggingAttributes(final Map<String, String> 
attributes) {
+    }
+
     /**
      * @return the log file suffix of the ProcessGroup for dedicated logging
      */
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/ConnectorLoggingAttribute.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/ConnectorLoggingAttribute.java
new file mode 100644
index 00000000000..46d936f8ded
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/ConnectorLoggingAttribute.java
@@ -0,0 +1,62 @@
+/*
+ * 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.components.connector;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Framework-managed MDC logging attribute keys for a connector. The framework 
computes these
+ * automatically and they cannot be overridden by a connector's custom 
attributes.
+ */
+public enum ConnectorLoggingAttribute {
+    CONNECTOR_ID("connectorId"),
+    CONNECTOR_NAME("connectorName"),
+    CONNECTOR_COMPONENT("connectorComponent"),
+    CONNECTOR_BUNDLE_GROUP("connectorBundleGroup"),
+    CONNECTOR_BUNDLE_ARTIFACT("connectorBundleArtifact"),
+    CONNECTOR_BUNDLE_VERSION("connectorBundleVersion");
+
+    private static final Set<String> RESERVED_KEYS;
+    static {
+        final Set<String> reserved = new HashSet<>();
+        for (final ConnectorLoggingAttribute attribute : values()) {
+            reserved.add(attribute.attribute);
+        }
+        RESERVED_KEYS = Collections.unmodifiableSet(reserved);
+    }
+
+    private final String attribute;
+
+    ConnectorLoggingAttribute(final String attribute) {
+        this.attribute = attribute;
+    }
+
+    public String getAttribute() {
+        return attribute;
+    }
+
+    public static boolean isReserved(final String key) {
+        return RESERVED_KEYS.contains(key);
+    }
+
+    public static Set<String> getReservedKeys() {
+        return RESERVED_KEYS;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
index 8f5ae6f123a..999471373b6 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorNode.java
@@ -53,6 +53,7 @@ import org.apache.nifi.flow.VersionedProcessor;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.logging.GroupedComponent;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.util.StringUtils;
@@ -84,7 +85,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-public class StandardConnectorNode implements ConnectorNode {
+public class StandardConnectorNode implements ConnectorNode, GroupedComponent {
     private static final Logger logger = 
LoggerFactory.getLogger(StandardConnectorNode.class);
 
     private static final Set<org.apache.nifi.controller.ScheduledState> 
STOPPED_STATES =
@@ -115,6 +116,10 @@ public class StandardConnectorNode implements 
ConnectorNode {
     private volatile String name;
     private volatile FrameworkConnectorInitializationContext 
initializationContext;
 
+    private final Object loggingAttributesLock = new Object();
+    private volatile Map<String, String> customLoggingAttributes = Map.of();
+    private volatile Map<String, String> mergedLoggingAttributes = Map.of();
+
 
     public StandardConnectorNode(final String identifier, final FlowManager 
flowManager, final ExtensionManager extensionManager,
         final Authorizable parentAuthorizable, final ConnectorDetails 
connectorDetails, final String componentType, final String 
componentCanonicalClass,
@@ -139,6 +144,8 @@ public class StandardConnectorNode implements ConnectorNode 
{
 
         final Bundle activeFlowBundle = new 
Bundle(bundleCoordinate.getGroup(), bundleCoordinate.getId(), 
bundleCoordinate.getVersion());
         this.activeFlowContext = 
flowContextFactory.createActiveFlowContext(identifier, 
connectorDetails.getComponentLog(), activeFlowBundle);
+
+        rebuildLoggingAttributes();
     }
 
     @Override
@@ -149,6 +156,109 @@ public class StandardConnectorNode implements 
ConnectorNode {
     @Override
     public void setName(final String name) {
         this.name = name;
+        rebuildLoggingAttributes();
+    }
+
+    /**
+     * Returns the {@link ProcessGroup} that holds the connector-managed flow 
(the connector's active
+     * managed root group), or {@code null} before the active flow context has 
been established.
+     *
+     * <p>This serves two purposes:
+     * <ul>
+     *   <li>It satisfies the {@link GroupedComponent} contract. The 
connector's own {@code ComponentLog}
+     *       uses a {@code StandardLoggingContext} bound to this node (see 
{@code ExtensionBuilder}), so the
+     *       connector's own log lines source their MDC from this group's
+     *       {@link ProcessGroup#getLoggingAttributes() logging 
attributes}.</li>
+     *   <li>It is the target onto which {@link #rebuildLoggingAttributes()} 
pushes the merged connector
+     *       logging attributes (via {@link 
ProcessGroup#setConnectorLoggingAttributes(Map)}); those then
+     *       cascade down the managed flow so components running inside it 
inherit the connector MDC through
+     *       their own logging contexts.</li>
+     * </ul>
+     *
+     * @return the connector's managed root process group, or {@code null} 
before the active flow context
+     *         has been established
+     */
+    @Override
+    public ProcessGroup getProcessGroup() {
+        final FrameworkFlowContext context = activeFlowContext;
+        return context == null ? null : context.getManagedProcessGroup();
+    }
+
+    /**
+     * Replaces the connector-supplied custom logging attributes. Reserved 
keys (those used by the
+     * framework, see {@link ConnectorLoggingAttribute}) are filtered out and 
a WARN is logged for
+     * each dropped entry.
+     *
+     * @param attributes the proposed custom attributes; {@code null} or empty 
clears the current set
+     */
+    @Override
+    public void setCustomLoggingAttributes(final Map<String, String> 
attributes) {
+        final Map<String, String> filtered = filterReservedKeys(attributes);
+
+        synchronized (loggingAttributesLock) {
+            this.customLoggingAttributes = filtered;
+        }
+        rebuildLoggingAttributes();
+    }
+
+    /**
+     * Returns an immutable snapshot of the merged framework + custom logging 
attributes currently
+     * advertised by this connector. The framework keys are populated by the 
framework from the
+     * connector's identifier, name, component type, and bundle coordinate.
+     *
+     * @return an immutable map of the connector's merged framework and custom 
logging attributes; never {@code null}
+     */
+    @Override
+    public Map<String, String> getLoggingAttributes() {
+        return mergedLoggingAttributes;
+    }
+
+    private Map<String, String> filterReservedKeys(final Map<String, String> 
attributes) {
+        if (attributes == null || attributes.isEmpty()) {
+            return Map.of();
+        }
+        final Map<String, String> filtered = new HashMap<>(attributes.size());
+        for (final Map.Entry<String, String> entry : attributes.entrySet()) {
+            final String key = entry.getKey();
+            if (key == null || key.isEmpty()) {
+                continue;
+            }
+            if (ConnectorLoggingAttribute.isReserved(key)) {
+                logger.warn("{} attempted to set reserved logging attribute 
[{}]; dropping the entry. Reserved keys are managed by the framework.", this, 
key);
+                continue;
+            }
+            filtered.put(key, entry.getValue());
+        }
+        return Collections.unmodifiableMap(filtered);
+    }
+
+    private void rebuildLoggingAttributes() {
+        final Map<String, String> merged;
+        final Map<String, String> custom;
+        synchronized (loggingAttributesLock) {
+            custom = customLoggingAttributes;
+            merged = new HashMap<>(custom.size() + 
ConnectorLoggingAttribute.values().length);
+            merged.putAll(custom);
+            // Framework keys are applied last so they always win against any 
not-yet-filtered overlap.
+            merged.put(ConnectorLoggingAttribute.CONNECTOR_ID.getAttribute(), 
identifier);
+            
merged.put(ConnectorLoggingAttribute.CONNECTOR_NAME.getAttribute(), name == 
null ? "" : name);
+            
merged.put(ConnectorLoggingAttribute.CONNECTOR_COMPONENT.getAttribute(), 
componentCanonicalClass == null ? "" : componentCanonicalClass);
+            if (bundleCoordinate != null) {
+                
merged.put(ConnectorLoggingAttribute.CONNECTOR_BUNDLE_GROUP.getAttribute(), 
bundleCoordinate.getGroup());
+                
merged.put(ConnectorLoggingAttribute.CONNECTOR_BUNDLE_ARTIFACT.getAttribute(), 
bundleCoordinate.getId());
+                
merged.put(ConnectorLoggingAttribute.CONNECTOR_BUNDLE_VERSION.getAttribute(), 
bundleCoordinate.getVersion());
+            }
+            this.mergedLoggingAttributes = Collections.unmodifiableMap(merged);
+        }
+
+        pushLoggingAttributesToManagedFlow(merged);
+    }
+
+    private void pushLoggingAttributesToManagedFlow(final Map<String, String> 
attributes) {
+        final ProcessGroup managedProcessGroup = getProcessGroup();
+        if (managedProcessGroup != null) {
+            managedProcessGroup.setConnectorLoggingAttributes(attributes);
+        }
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
index a345edb554f..cfe0544075e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
@@ -63,6 +63,13 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
     private static final Logger logger = 
LoggerFactory.getLogger(StandardConnectorRepository.class);
     private static final Duration SYNC_POLL_INTERVAL = Duration.ofSeconds(2);
 
+    /**
+     * Soft cardinality limit beyond which a WARN is emitted when a {@link 
ConnectorConfigurationProvider}
+     * supplies logging attributes for a connector. Exceeding this threshold 
signals a potential metric/MDC
+     * cardinality risk for downstream observability backends but does not 
reject the attributes.
+     */
+    private static final int 
CUSTOM_LOGGING_ATTRIBUTE_CARDINALITY_WARN_THRESHOLD = 5;
+
     private final Map<String, ConnectorNode> connectors = new 
ConcurrentHashMap<>();
     private final FlowEngine lifecycleExecutor = new FlowEngine(8, "NiFi 
Connector Lifecycle");
 
@@ -100,12 +107,14 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
 
     @Override
     public void addConnector(final ConnectorNode connector) {
+        applyProviderLoggingAttributes(connector);
         syncFromProvider(connector);
         connectors.put(connector.getIdentifier(), connector);
     }
 
     @Override
     public void restoreConnector(final ConnectorNode connector) {
+        applyProviderLoggingAttributes(connector);
         connectors.put(connector.getIdentifier(), connector);
         logger.debug("Successfully restored {}", connector);
     }
@@ -832,6 +841,39 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
         return secretsManager;
     }
 
+    /**
+     * Sources provider-supplied logging attributes for the given connector 
and applies them to the node so that
+     * they are merged into the MDC of the connector's {@link 
org.apache.nifi.logging.ComponentLog} and the loggers
+     * of every component running inside its managed flow (and surfaced on 
connector metrics). Invoked when the
+     * connector node is added or restored. The framework-managed identity 
keys are always present regardless of
+     * what the provider supplies.
+     */
+    private void applyProviderLoggingAttributes(final ConnectorNode connector) 
{
+        
connector.setCustomLoggingAttributes(resolveProviderLoggingAttributes(connector.getIdentifier()));
+    }
+
+    private Map<String, String> resolveProviderLoggingAttributes(final String 
connectorId) {
+        if (configurationProvider == null) {
+            return Map.of();
+        }
+        try {
+            final Map<String, String> attributes = 
configurationProvider.getLoggingAttributes(connectorId);
+            if (attributes == null || attributes.isEmpty()) {
+                return Map.of();
+            }
+            if (attributes.size() > 
CUSTOM_LOGGING_ATTRIBUTE_CARDINALITY_WARN_THRESHOLD) {
+                logger.warn("ConnectorConfigurationProvider [{}] supplied {} 
logging attributes for connector [{}] which exceeds the soft threshold of {}; "
+                                + "high-cardinality attributes can degrade MDC 
logging and OpenTelemetry metric backends",
+                        configurationProvider.getClass().getSimpleName(), 
attributes.size(), connectorId, 
CUSTOM_LOGGING_ATTRIBUTE_CARDINALITY_WARN_THRESHOLD);
+            }
+            return attributes;
+        } catch (final Exception e) {
+            logger.warn("ConnectorConfigurationProvider [{}] threw while 
computing logging attributes for connector [{}]; using empty map: {}",
+                    configurationProvider.getClass().getSimpleName(), 
connectorId, e.getMessage(), e);
+            return Map.of();
+        }
+    }
+
     @Override
     public ConnectorStateTransition createStateTransition(final String type, 
final String id) {
         final String componentDescription = "StandardConnectorNode[id=" + id + 
", type=" + type + "]";
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
index 45ef9980d4b..103a47bee19 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
@@ -521,10 +521,11 @@ public class ExtensionBuilder {
         }
 
         final String componentType = connector.getClass().getSimpleName();
-        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
connector, new StandardLoggingContext());
+        final StandardLoggingContext loggingContext = new 
StandardLoggingContext();
+        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
connector, loggingContext);
         final ConnectorDetails connectorDetails = new 
ConnectorDetails(connector, bundleCoordinate, componentLog);
 
-        final ConnectorNode connectorNode = new StandardConnectorNode(
+        final StandardConnectorNode connectorNode = new StandardConnectorNode(
             identifier,
             flowController.getFlowManager(),
             extensionManager,
@@ -538,6 +539,11 @@ public class ExtensionBuilder {
             connectorValidationTrigger,
             false
         );
+        // Late-bind the logging context to the connector node so that MDC 
attributes assembled by
+        // the node (framework keys + provider-supplied custom keys) flow 
through the connector's
+        // own ComponentLog as well as the managed flow's nested components. 
Provider-supplied custom
+        // attributes are sourced and applied by the ConnectorRepository when 
the node is added/restored.
+        loggingContext.setComponent(connectorNode);
 
         try {
             initializeDefaultValues(connector, 
connectorNode.getActiveFlowContext());
@@ -565,13 +571,14 @@ public class ExtensionBuilder {
         final GhostConnector ghostConnector = new GhostConnector(identifier, 
type, cause);
         final String simpleClassName = type.contains(".") ? 
StringUtils.substringAfterLast(type, ".") : type;
         final String componentType = "(Missing) " + simpleClassName;
-        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
ghostConnector, new StandardLoggingContext());
+        final StandardLoggingContext loggingContext = new 
StandardLoggingContext();
+        final ComponentLog componentLog = new SimpleProcessLogger(identifier, 
ghostConnector, loggingContext);
         final ConnectorDetails connectorDetails = new 
ConnectorDetails(ghostConnector, bundleCoordinate, componentLog);
 
         // If an instance class loader has been created for this connector, 
remove it because it's no longer necessary.
         extensionManager.removeInstanceClassLoader(identifier);
 
-        final ConnectorNode connectorNode = new StandardConnectorNode(
+        final StandardConnectorNode connectorNode = new StandardConnectorNode(
             identifier,
             flowController.getFlowManager(),
             extensionManager,
@@ -585,6 +592,7 @@ public class ExtensionBuilder {
             connectorValidationTrigger,
             true
         );
+        loggingContext.setComponent(connectorNode);
 
         // Initialize the ghost connector so that it can be properly 
configured during flow synchronization
         final FrameworkConnectorInitializationContext initContext = 
createConnectorInitializationContext(managedProcessGroup, componentLog);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 0804b1d5662..804de6818a4 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -810,11 +810,12 @@ public class FlowController implements 
ReportingTaskProvider, FlowAnalysisRulePr
         }
 
         eventAccess = new StandardEventAccess(flowManager, 
flowFileEventRepository, processScheduler, authorizer, provenanceRepository,
-                auditService, analyticsEngine, flowFileRepository, 
contentRepository);
+                auditService, analyticsEngine, flowFileRepository, 
contentRepository, connectorRepository);
 
         timerDrivenEngineRef.get().scheduleWithFixedDelay(() -> {
             try {
-                statusHistoryRepository.capture(getNodeStatusSnapshot(), 
eventAccess.getControllerStatus(), getGarbageCollectionStatus(), new Date());
+                statusHistoryRepository.capture(getNodeStatusSnapshot(), 
eventAccess.getControllerStatus(), eventAccess.getConnectorStatuses(),
+                        getGarbageCollectionStatus(), new Date());
             } catch (final Exception e) {
                 LOG.error("Failed to capture component stats for Stats 
History", e);
             }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepository.java
index 2b84217fa4e..c03cc077f88 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepository.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.controller.status.history;
 
 import org.apache.nifi.controller.status.ConnectionStatus;
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.NodeStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 import org.apache.nifi.controller.status.ProcessorStatus;
@@ -28,6 +29,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -110,6 +112,24 @@ public class VolatileComponentStatusRepository implements 
StatusHistoryRepositor
         lastCaptureTime = Math.max(lastCaptureTime, timestamp.getTime());
     }
 
+    @Override
+    public synchronized void capture(final NodeStatus nodeStatus, final 
ProcessGroupStatus rootGroupStatus, final Collection<ConnectorStatus> 
connectorStatuses,
+                                     final List<GarbageCollectionStatus> 
gcStatus, final Date timestamp) {
+        capture(nodeStatus, rootGroupStatus, gcStatus, timestamp);
+
+        // Connector-managed flows are siblings of the root group and 
therefore are not reachable from
+        // rootGroupStatus. Capture each Connector's managed flow so that 
components running inside it have
+        // status history just like components in the primary flow.
+        if (connectorStatuses != null) {
+            for (final ConnectorStatus connectorStatus : connectorStatuses) {
+                final ProcessGroupStatus connectorRootGroupStatus = 
connectorStatus.getRootGroupStatus();
+                if (connectorRootGroupStatus != null) {
+                    capture(connectorRootGroupStatus, timestamp);
+                }
+            }
+        }
+    }
+
     private void capture(final ProcessGroupStatus groupStatus, final Date 
timestamp) {
         // Capture status for the ProcessGroup
         final ComponentDetails groupDetails = 
ComponentDetails.forProcessGroup(groupStatus);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/reporting/StandardEventAccess.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/reporting/StandardEventAccess.java
index ec233778b65..3e12745328e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/reporting/StandardEventAccess.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/reporting/StandardEventAccess.java
@@ -22,6 +22,10 @@ import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.components.connector.ConnectorNode;
+import org.apache.nifi.components.connector.ConnectorRepository;
+import org.apache.nifi.components.connector.ConnectorSyncMode;
+import org.apache.nifi.components.connector.FrameworkFlowContext;
 import org.apache.nifi.controller.ProcessScheduler;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.flow.FlowManager;
@@ -31,6 +35,7 @@ import 
org.apache.nifi.controller.repository.FlowFileEventRepository;
 import org.apache.nifi.controller.repository.FlowFileRepository;
 import org.apache.nifi.controller.repository.RepositoryStatusReport;
 import org.apache.nifi.controller.repository.metrics.EmptyFlowFileEvent;
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 import org.apache.nifi.controller.status.ProcessorStatus;
 import org.apache.nifi.controller.status.analytics.StatusAnalyticsEngine;
@@ -41,6 +46,8 @@ import org.apache.nifi.provenance.ProvenanceRepository;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -55,10 +62,11 @@ public class StandardEventAccess extends 
AbstractEventAccess implements UserAwar
     private final AuditService auditService;
     private final FlowFileRepository flowFileRepository;
     private final ContentRepository contentRepository;
+    private final ConnectorRepository connectorRepository;
 
     public StandardEventAccess(final FlowManager flowManager, final 
FlowFileEventRepository flowFileEventRepository, final ProcessScheduler 
processScheduler,
                                final Authorizer authorizer, final 
ProvenanceRepository provenanceRepository, final AuditService auditService, 
final StatusAnalyticsEngine statusAnalyticsEngine,
-                               final FlowFileRepository flowFileRepository, 
final ContentRepository contentRepository) {
+                               final FlowFileRepository flowFileRepository, 
final ContentRepository contentRepository, final ConnectorRepository 
connectorRepository) {
         super(processScheduler, statusAnalyticsEngine, flowManager, 
flowFileEventRepository);
         this.flowFileEventRepository = flowFileEventRepository;
         this.flowManager = flowManager;
@@ -67,6 +75,49 @@ public class StandardEventAccess extends AbstractEventAccess 
implements UserAwar
         this.auditService = auditService;
         this.flowFileRepository = flowFileRepository;
         this.contentRepository = contentRepository;
+        this.connectorRepository = connectorRepository;
+    }
+
+    /**
+     * Returns a {@link ConnectorStatus} for each registered connector, each 
carrying the connector identity along with
+     * the {@link ProcessGroupStatus} of the connector's managed root process 
group. Connector-managed flows live
+     * outside the controller's root process group, so they are invisible to 
{@link #getControllerStatus()} and would
+     * otherwise be skipped by standard reporting tasks.
+     */
+    @Override
+    public Collection<ConnectorStatus> getConnectorStatuses() {
+        if (connectorRepository == null) {
+            return Collections.emptyList();
+        }
+
+        final List<ConnectorNode> connectors = 
connectorRepository.getConnectors(ConnectorSyncMode.LOCAL_ONLY);
+        if (connectors == null || connectors.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        final RepositoryStatusReport statusReport = 
generateRepositoryStatusReport();
+        final List<ConnectorStatus> statuses = new 
ArrayList<>(connectors.size());
+        for (final ConnectorNode connector : connectors) {
+            final FrameworkFlowContext context = 
connector.getActiveFlowContext();
+            if (context == null) {
+                continue;
+            }
+            final ProcessGroup managedGroup = context.getManagedProcessGroup();
+            if (managedGroup == null) {
+                continue;
+            }
+            final ProcessGroupStatus rootGroupStatus = 
getGroupStatus(managedGroup, statusReport,
+                authorizable -> true, Integer.MAX_VALUE, 1, true);
+            if (rootGroupStatus == null) {
+                continue;
+            }
+            final ConnectorStatus connectorStatus = new ConnectorStatus();
+            connectorStatus.setId(connector.getIdentifier());
+            connectorStatus.setName(connector.getName());
+            connectorStatus.setRootGroupStatus(rootGroupStatus);
+            statuses.add(connectorStatus);
+        }
+        return statuses;
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
index 8f4ad44d0aa..bbd0497a257 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorNode.java
@@ -829,6 +829,88 @@ public class TestStandardConnectorNode {
         assertEquals(ConnectorState.STOPPED, connectorNode.getCurrentState());
     }
 
+    @Test
+    public void testLoggingAttributesIncludeFrameworkKeys() throws Exception {
+        final SleepingConnector connector = new 
SleepingConnector(Duration.ofMillis(1));
+        final StandardConnectorNode connectorNode = 
createConnectorNode(connector);
+
+        final Map<String, String> attributes = 
connectorNode.getLoggingAttributes();
+        assertEquals("test-connector-id", attributes.get("connectorId"));
+        assertEquals(connector.getClass().getCanonicalName(), 
attributes.get("connectorComponent"));
+        assertEquals("org.apache.nifi", 
attributes.get("connectorBundleGroup"));
+        assertEquals("test-standard-connector-node", 
attributes.get("connectorBundleArtifact"));
+        assertEquals("1.0.0", attributes.get("connectorBundleVersion"));
+        assertEquals(connector.getClass().getSimpleName(), 
attributes.get("connectorName"));
+    }
+
+    @Test
+    public void testSetNameRefreshesConnectorNameLoggingAttribute() throws 
Exception {
+        final StandardConnectorNode connectorNode = createConnectorNode();
+        connectorNode.setName("Renamed Connector");
+
+        assertEquals("Renamed Connector", 
connectorNode.getLoggingAttributes().get("connectorName"));
+    }
+
+    @Test
+    public void testSetCustomLoggingAttributesMergesWithFrameworkKeys() throws 
Exception {
+        final StandardConnectorNode connectorNode = createConnectorNode();
+        connectorNode.setCustomLoggingAttributes(Map.of(
+            "customA", "valueA",
+            "customB", "valueB"
+        ));
+
+        final Map<String, String> attributes = 
connectorNode.getLoggingAttributes();
+        assertEquals("valueA", attributes.get("customA"));
+        assertEquals("valueB", attributes.get("customB"));
+        assertEquals("test-connector-id", attributes.get("connectorId"));
+    }
+
+    @Test
+    public void testSetCustomLoggingAttributesFiltersReservedKeys() throws 
Exception {
+        final StandardConnectorNode connectorNode = createConnectorNode();
+        connectorNode.setCustomLoggingAttributes(Map.of(
+            "connectorId", "attempted-override",
+            "connectorBundleVersion", "9.9.9",
+            "customA", "valueA"
+        ));
+
+        final Map<String, String> attributes = 
connectorNode.getLoggingAttributes();
+        // Reserved keys are owned by the framework: connectorId stays as the 
framework value.
+        assertEquals("test-connector-id", attributes.get("connectorId"));
+        assertEquals("1.0.0", attributes.get("connectorBundleVersion"));
+        // Non-reserved custom keys are preserved.
+        assertEquals("valueA", attributes.get("customA"));
+    }
+
+    @Test
+    public void testSetCustomLoggingAttributesNullClears() throws Exception {
+        final StandardConnectorNode connectorNode = createConnectorNode();
+        connectorNode.setCustomLoggingAttributes(Map.of("customA", "valueA"));
+        
assertTrue(connectorNode.getLoggingAttributes().containsKey("customA"));
+
+        connectorNode.setCustomLoggingAttributes(null);
+        
assertFalse(connectorNode.getLoggingAttributes().containsKey("customA"));
+        // Framework keys remain.
+        assertEquals("test-connector-id", 
connectorNode.getLoggingAttributes().get("connectorId"));
+    }
+
+    @Test
+    public void testReservedKeysExposeFrameworkAttributeNames() {
+        final Set<String> reserved = 
ConnectorLoggingAttribute.getReservedKeys();
+        assertTrue(reserved.contains("connectorId"));
+        assertTrue(reserved.contains("connectorName"));
+        assertTrue(reserved.contains("connectorComponent"));
+        assertTrue(reserved.contains("connectorBundleGroup"));
+        assertTrue(reserved.contains("connectorBundleArtifact"));
+        assertTrue(reserved.contains("connectorBundleVersion"));
+    }
+
+    @Test
+    public void testGetProcessGroupReturnsManagedFlowRoot() throws Exception {
+        final StandardConnectorNode connectorNode = createConnectorNode();
+        assertEquals(managedProcessGroup, connectorNode.getProcessGroup());
+    }
+
     private StandardConnectorNode createConnectorNode() throws 
FlowUpdateException {
         final SleepingConnector sleepingConnector = new 
SleepingConnector(Duration.ofMillis(1));
         return createConnectorNode(sleepingConnector);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
index 4396b8a1ec9..68020bb8677 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/TestStandardConnectorRepository.java
@@ -308,6 +308,10 @@ public class TestStandardConnectorRepository {
         final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Local Name");
         repository.restoreConnector(connector);
 
+        // Restoring the connector sources provider-supplied logging 
attributes; reset so that the assertions
+        // below verify only that the LOCAL_ONLY get path does not consult the 
provider.
+        reset(provider);
+
         final ConnectorNode result = repository.getConnector("connector-1", 
ConnectorSyncMode.LOCAL_ONLY);
 
         assertNotNull(result);
@@ -325,6 +329,10 @@ public class TestStandardConnectorRepository {
         repository.restoreConnector(connector1);
         repository.restoreConnector(connector2);
 
+        // Restoring the connectors sources provider-supplied logging 
attributes; reset so that the assertions
+        // below verify only that the LOCAL_ONLY get path does not consult the 
provider.
+        reset(provider);
+
         final List<ConnectorNode> results = 
repository.getConnectors(ConnectorSyncMode.LOCAL_ONLY);
 
         assertEquals(2, results.size());
@@ -1586,6 +1594,100 @@ public class TestStandardConnectorRepository {
         return node;
     }
 
+    // --- provider-sourced logging attribute application (add/restore) tests 
---
+
+    @Test
+    public void testRestoreConnectorAppliesEmptyAttributesWhenNoProvider() {
+        final StandardConnectorRepository repository = new 
StandardConnectorRepository();
+        final ConnectorRepositoryInitializationContext initContext = 
mock(ConnectorRepositoryInitializationContext.class);
+        
when(initContext.getExtensionManager()).thenReturn(mock(ExtensionManager.class));
+        when(initContext.getConnectorConfigurationProvider()).thenReturn(null);
+        repository.initialize(initContext);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.restoreConnector(connector);
+
+        verify(connector).setCustomLoggingAttributes(Map.of());
+    }
+
+    @Test
+    public void testRestoreConnectorAppliesProviderLoggingAttributes() {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        final Map<String, String> expected = Map.of("connectorDefinitionId", 
"snowflake.runtime.postgres-cdc");
+        
when(provider.getLoggingAttributes("connector-1")).thenReturn(expected);
+
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.restoreConnector(connector);
+
+        verify(provider).getLoggingAttributes("connector-1");
+        verify(connector).setCustomLoggingAttributes(expected);
+    }
+
+    @Test
+    public void testAddConnectorAppliesProviderLoggingAttributes() {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        final Map<String, String> expected = Map.of("connectorDefinitionId", 
"snowflake.runtime.postgres-cdc");
+        
when(provider.getLoggingAttributes("connector-1")).thenReturn(expected);
+
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.addConnector(connector);
+
+        verify(provider).getLoggingAttributes("connector-1");
+        verify(connector).setCustomLoggingAttributes(expected);
+    }
+
+    @Test
+    public void 
testRestoreConnectorAppliesEmptyAttributesWhenProviderReturnsNull() {
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getLoggingAttributes(anyString())).thenReturn(null);
+
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.restoreConnector(connector);
+
+        verify(connector).setCustomLoggingAttributes(Map.of());
+    }
+
+    @Test
+    public void testRestoreConnectorAppliesEmptyAttributesWhenProviderThrows() 
{
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        when(provider.getLoggingAttributes(anyString())).thenThrow(new 
RuntimeException("provider boom"));
+
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.restoreConnector(connector);
+
+        verify(connector).setCustomLoggingAttributes(Map.of());
+    }
+
+    @Test
+    public void 
testRestoreConnectorAppliesAllAttributesWhenAboveCardinalityThreshold() {
+        // More than the soft cardinality threshold (5): the framework logs a 
WARN but must not drop attributes.
+        final Map<String, String> expected = Map.of(
+                "attr1", "v1",
+                "attr2", "v2",
+                "attr3", "v3",
+                "attr4", "v4",
+                "attr5", "v5",
+                "attr6", "v6"
+        );
+        final ConnectorConfigurationProvider provider = 
mock(ConnectorConfigurationProvider.class);
+        
when(provider.getLoggingAttributes("connector-1")).thenReturn(expected);
+
+        final StandardConnectorRepository repository = 
createRepositoryWithProvider(provider);
+
+        final ConnectorNode connector = 
createSimpleConnectorNode("connector-1", "Connector 1");
+        repository.restoreConnector(connector);
+
+        verify(connector).setCustomLoggingAttributes(expected);
+    }
+
     // --- Helper Methods ---
 
     private StandardConnectorRepository 
createRepositoryWithProviderAndAssetManager(
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
index 62f865721c9..2b032e62b89 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/AbstractStatusHistoryRepositoryTest.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.controller.status.history;
 
 import org.apache.nifi.controller.status.ConnectionStatus;
+import org.apache.nifi.controller.status.ConnectorStatus;
 import org.apache.nifi.controller.status.NodeStatus;
 import org.apache.nifi.controller.status.ProcessGroupStatus;
 import org.apache.nifi.controller.status.ProcessorStatus;
@@ -41,6 +42,9 @@ public abstract class AbstractStatusHistoryRepositoryTest {
     protected static final String PROCESSOR_WITH_COUNTER_ID = 
"f09c5bb8-7b67-4d3b-81c5-5373e1c03ed1";
     protected static final String CONNECTION_ID = 
"d5452f70-a0d9-44c5-aeda-c2026482e4ee";
     protected static final String REMOTE_PROCESS_GROUP_ID = 
"f5f4fe2a-0209-4ba7-8f15-f33df942cde5";
+    protected static final String CONNECTOR_ID = 
"a1111111-1111-1111-1111-111111111111";
+    protected static final String CONNECTOR_ROOT_GROUP_ID = 
"a2222222-2222-2222-2222-222222222222";
+    protected static final String CONNECTOR_PROCESSOR_ID = 
"a3333333-3333-3333-3333-333333333333";
 
     protected ProcessGroupStatus givenSimpleRootProcessGroupStatus() {
         final ProcessGroupStatus status = new ProcessGroupStatus();
@@ -74,6 +78,23 @@ public abstract class AbstractStatusHistoryRepositoryTest {
         return status;
     }
 
+    protected ConnectorStatus givenConnectorStatus() {
+        final ProcessorStatus connectorProcessor = givenProcessorStatus();
+        connectorProcessor.setId(CONNECTOR_PROCESSOR_ID);
+        connectorProcessor.setName("ConnectorProcessor");
+
+        final ProcessGroupStatus connectorRootGroup = 
givenSimpleRootProcessGroupStatus();
+        connectorRootGroup.setId(CONNECTOR_ROOT_GROUP_ID);
+        connectorRootGroup.setName("Connector Root");
+        
connectorRootGroup.setProcessorStatus(Collections.singleton(connectorProcessor));
+
+        final ConnectorStatus connectorStatus = new ConnectorStatus();
+        connectorStatus.setId(CONNECTOR_ID);
+        connectorStatus.setName("Test Connector");
+        connectorStatus.setRootGroupStatus(connectorRootGroup);
+        return connectorStatus;
+    }
+
     protected ProcessGroupStatus givenChildProcessGroupStatus() {
         final ProcessGroupStatus status = new ProcessGroupStatus();
         status.setId(CHILD_GROUP_ID);
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepositoryForNodeTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepositoryForNodeTest.java
index 30681f4305c..0b5f363fea4 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepositoryForNodeTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/status/history/VolatileComponentStatusRepositoryForNodeTest.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.fail;
 
 public class VolatileComponentStatusRepositoryForNodeTest extends 
AbstractStatusHistoryRepositoryTest {
@@ -129,6 +130,34 @@ public class VolatileComponentStatusRepositoryForNodeTest 
extends AbstractStatus
         assertEquals(3L, getMetricAtOrdinal(snapshot2, g1CountDiffOrdinal));
     }
 
+    @Test
+    public void testConnectorManagedFlowStatusHistory() {
+        final NiFiProperties niFiProperties = 
Mockito.mock(NiFiProperties.class);
+        
Mockito.when(niFiProperties.getIntegerProperty(VolatileComponentStatusRepository.NUM_DATA_POINTS_PROPERTY,
 VolatileComponentStatusRepository.DEFAULT_NUM_DATA_POINTS)).thenReturn(10);
+        final VolatileComponentStatusRepository testSubject = new 
VolatileComponentStatusRepository(niFiProperties);
+
+        final Date capturedAt = new Date();
+        testSubject.capture(givenNodeStatus(0), givenRootProcessGroupStatus(), 
List.of(givenConnectorStatus()), givenGarbageCollectionStatuses(capturedAt), 
capturedAt);
+
+        // The Connector's managed root group is captured even though it is a 
sibling of (not reachable from) the root group
+        final StatusHistory connectorRootGroupStatus = 
testSubject.getProcessGroupStatusHistory(CONNECTOR_ROOT_GROUP_ID, new Date(0), 
new Date(), Integer.MAX_VALUE);
+        assertFalse(connectorRootGroupStatus.getStatusSnapshots().isEmpty());
+        assertEquals(CONNECTOR_ROOT_GROUP_ID, 
connectorRootGroupStatus.getComponentDetails().get("Id"));
+        assertEquals("Connector Root", 
connectorRootGroupStatus.getComponentDetails().get("Name"));
+
+        // A processor running inside the Connector-managed flow has status 
history just like a primary-flow processor
+        final StatusHistory connectorProcessorStatus = 
testSubject.getProcessorStatusHistory(CONNECTOR_PROCESSOR_ID, new Date(0), new 
Date(), Integer.MAX_VALUE, false);
+        assertFalse(connectorProcessorStatus.getStatusSnapshots().isEmpty());
+        assertEquals(CONNECTOR_PROCESSOR_ID, 
connectorProcessorStatus.getComponentDetails().get("Id"));
+        assertEquals("ConnectorProcessor", 
connectorProcessorStatus.getComponentDetails().get("Name"));
+
+        // The primary root flow is still captured alongside the connector flow
+        final StatusHistory rootGroupStatus = 
testSubject.getProcessGroupStatusHistory(ROOT_GROUP_ID, new Date(0), new 
Date(), Integer.MAX_VALUE);
+        assertFalse(rootGroupStatus.getStatusSnapshots().isEmpty());
+        assertEquals(ROOT_GROUP_ID, 
rootGroupStatus.getComponentDetails().get("Id"));
+        assertEquals("Root", 
rootGroupStatus.getComponentDetails().get("Name"));
+    }
+
     private static long getMetricAtOrdinal(final StatusSnapshot snapshot, 
final long ordinal) {
         final Set<MetricDescriptor<?>> metricDescriptors = 
snapshot.getMetricDescriptors();
 

Reply via email to