Repository: antrea-io/antrea
Pull request #4047 (2022-07-21 17:56:35)

Audit Logging support K8s Networkpolicy
Previously audit logging only works for Antrea NetworkPolicy and ClusterNetworkPolicy when setting the `enableLogging` field in the rule. This PR is to address this [issue](https://github.com/antrea-io/antrea/issues/3640).

Method overview of this PR is at this [design doc](https://docs.google.com/document/d/1lgPxhhAw8INUxE6ulUo_RngmWd5X4IrLkwtx744CwG4/edit?usp=sharing).

To enable logging for K8s NetworkPolicy, set `annotations` in `metadata` for the Namespace as `policy.antrea.io/enable-np-logging: "true"`. Then K8s NetworkPolicy applied to this Namespace will be logged in the directory `/var/log/antrea/networkpolicy/np.log`.

codecov[bot]: # [Codecov](https://codecov.io/gh/antrea-io/antrea/pull/4047?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) Report
> Merging [#4047](https://codecov.io/gh/antrea-io/antrea/pull/4047?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (6c3a475) into [main](https://codecov.io/gh/antrea-io/antrea/commit/23ef6f71a46f504b5d4ac189f4dfaf95862b724e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) (23ef6f7) will **decrease** coverage by `0.39%`.
> The diff coverage is `81.63%`.

[![Impacted file tree graph](https://codecov.io/gh/antrea-io/antrea/pull/4047/graphs/tree.svg?width=650&height=150&src=pr&token=TjWaLL2GoC&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)](https://codecov.io/gh/antrea-io/antrea/pull/4047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io)

```diff
@@            Coverage Diff             @@
##             main    #4047      +/-   ##
==========================================
- Coverage   64.56%   64.16%   -0.40%     
==========================================
  Files         295      293       -2     
  Lines       43838    44285     +447     
==========================================
+ Hits        28303    28415     +112     
- Misses      13248    13589     +341     
+ Partials     2287     2281       -6     
```

| Flag | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| kind-e2e-tests | `51.23% <81.39%> (ø)` | | Carriedforward from [23ef6f7](https://codecov.io/gh/antrea-io/antrea/commit/23ef6f71a46f504b5d4ac189f4dfaf95862b724e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) |
| unit-tests | `44.20% <51.02%> (-0.08%)` | :arrow_down: | |

*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) to find out more.

| [Impacted Files](https://codecov.io/gh/antrea-io/antrea/pull/4047?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | Coverage Δ | |
|---|---|---|
| [...kg/agent/controller/networkpolicy/audit\_logging.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9hdWRpdF9sb2dnaW5nLmdv) | `76.42% <40.00%> (-7.61%)` | :arrow_down: |
| [pkg/agent/openflow/pipeline.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L29wZW5mbG93L3BpcGVsaW5lLmdv) | `68.90% <83.33%> (-4.42%)` | :arrow_down: |
| [pkg/agent/openflow/network\_policy.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L29wZW5mbG93L25ldHdvcmtfcG9saWN5Lmdv) | `79.52% <100.00%> (ø)` | |
| [...ntroller/networkpolicy/networkpolicy\_controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2NvbnRyb2xsZXIvbmV0d29ya3BvbGljeS9uZXR3b3JrcG9saWN5X2NvbnRyb2xsZXIuZ28=) | `78.51% <100.00%> (-3.82%)` | :arrow_down: |
| [pkg/agent/agent.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2FnZW50Lmdv) | `50.91% <0.00%> (-2.32%)` | :arrow_down: |
| [pkg/controller/grouping/controller.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2NvbnRyb2xsZXIvZ3JvdXBpbmcvY29udHJvbGxlci5nbw==) | `65.13% <0.00%> (-1.98%)` | :arrow_down: |
| [...g/agent/cniserver/interface\_configuration\_linux.go](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io#diff-cGtnL2FnZW50L2NuaXNlcnZlci9pbnRlcmZhY2VfY29uZmlndXJhdGlvbl9saW51eC5nbw==) | `15.40% <0.00%> (-1.80%)` | :arrow_down: |
| ... and [12 more](https://codecov.io/gh/antrea-io/antrea/pull/4047/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=antrea-io) | |

qiyueyao: Reviewers, there's a problem for the Namespace UPDATE event. When modifying the `drop flow` in an UPDATE event, we might need to compare the previous `drop flow` and the newly generated one to determine if a modification is necessary. [Code here](https://github.com/antrea-io/antrea/blob/ac04bab158e2c37a672657dfcc2f82894586959b/pkg/agent/openflow/network_policy.go#L654).
Currently the method is to always modify the `drop flow` due to lack of available information. If we want to compare the flows, we need to change the `Flow` struct to add useful methods, which involves much more changes to the OVS struct.
Any suggestions or thoughts? Thanks.
tnqn: > Reviewers, there's a problem for the Namespace UPDATE event. When modifying the `drop flow` in an UPDATE event, we might need to compare the previous `drop flow` and the newly generated one to determine if a modification is necessary. [Code here](https://github.com/antrea-io/antrea/blob/ac04bab158e2c37a672657dfcc2f82894586959b/pkg/agent/openflow/network_policy.go#L654). Currently the method is to always modify the `drop flow` due to lack of available information. If we want to compare the flows, we need to change the `Flow` struct to add useful methods, which involves much more changes to the OVS struct. Any suggestions or thoughts? Thanks.

If enableLogging changes, the ID of rule also changes, the original rule would be deleted and the new rule would be installed:
https://github.com/antrea-io/antrea/blob/2a37aec1e4df5e976601342a72eb76a42058e335/pkg/agent/controller/networkpolicy/cache.go#L650-L665
So it's already handled. Otherwise current code won't work because reconciler doesn't call functions like `AddPolicyRuleAddress` for a previously install rule if there is no address change.

I guess [Code](https://github.com/antrea-io/antrea/blob/ac04bab158e2c37a672657dfcc2f82894586959b/pkg/agent/openflow/network_policy.go#L654) is not needed?
qiyueyao: > So it's already handled. Otherwise current code won't work because reconciler doesn't call functions like `AddPolicyRuleAddress` for a previously install rule if there is no address change.
> I guess [Code](https://github.com/antrea-io/antrea/blob/ac04bab158e2c37a672657dfcc2f82894586959b/pkg/agent/openflow/network_policy.go#L654) is not needed?

Probably I didn't express it clearly, for the flow generated by the rule directly, it indeed changes due to ruleID update, so the `IngressRuleTable` is updated correspondingly.
However, for K8s NetworkPolicies, the default drop flow in `IngressDefaultTable` would not update since address was not changed. The pathway is `Reconcile`->add->`installOFRule`->`InstallPolicyRuleFlows`->`calculateMatchFlowChangesForRule, calculateChangesForRuleCreation`->`addAddrFlows`->`addConjunctiveMatchFlow`. In this function, it searches for `globalConjMatchFlowCache` where the key only depends on `tableID, priority and matchPairs`, it does not depend on `enableLogging`.
I did some tests as well, [Code](https://github.com/antrea-io/antrea/blob/ac04bab158e2c37a672657dfcc2f82894586959b/pkg/agent/openflow/network_policy.go#L654) here is the place to update default drop flow by K8s NP, but the problem remains whether to compare the flows so that we only update when it is necessary. If we need to compare but there is no easy way to modify `Flow`, then I believe the UPDATE event should be marked TODO, to solve together with the race condition. Thanks.

tnqn: @qiyueyao thanks for the explanation. Now I understand the problem is the dropFlow is not specific to a rule but specific to a match condition. Then does it work if we store `enableLogging` in `conjMatchFlowContext` and update the dropFlow when it changes?
GraysonWu: I think Quan's suggestion could work. Since we only use it for `dropFlow`, maybe name it as something like: `dropFlowEnableLogging`?

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 0cff40dc2..27c2fd728 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -104,6 +104,48 @@ jobs:
           flags: unit-tests
           name: codecov-unit-test
 
+  test-integration:
+    needs: check-changes
+    if: ${{ needs.check-changes.outputs.has_changes == 'yes' }}
+    name: Integration test
+    runs-on: [ubuntu-latest]
+    steps:
+      - name: Set up Go 1.17
+        uses: actions/setup-go@v3
+        with:
+          go-version: 1.17
+      - name: Check-out code
+        uses: actions/checkout@v3
+      - uses: actions/cache@v3
+        with:
+          # In order:
+          # * Module download cache
+          # * Build cache (Linux)
+          # * Build cache (Mac)
+          path: |
+            ~/go/pkg/mod
+            ~/.cache/go-build
+            ~/Library/Caches/go-build
+            %LocalAppData%\go-build
+          key: ${{ runner.os }}-${{ env.go-cache-name }}-${{ hashFiles('**/go.sum') }}
+          restore-keys: |
+            ${{ runner.os }}-${{ env.go-cache-name }}-
+      - name: Run integration tests
+        run: |
+          ./build/images/ovs/build.sh
+          NO_PULL=1 make docker-test-integration
+      - name: Run integration tests for multicluster
+        run: |
+          cd multicluster
+          make test-integration
+      - name: Codecov
+        uses: codecov/codecov-action@v3
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          files: .coverage/coverage-integration.txt,multicluster/.coverage/coverage-integration.txt
+          flags: integration-tests
+          name: codecov-integration-test
+
   # golangci-lint-ubuntu and golangci-lint-macos are intentionally not merged into one job with os matrix, otherwise the
   # job wouldn't be expanded if it's skipped and the report of the required check would be missing.
   # See https://github.com/antrea-io/antrea/issues/3563.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 369a5f5ea..3c5caa9dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -161,7 +161,6 @@ the repo. It's a Github security mechanism.
 
 Here are the trigger phrases for individual checks:
 
-* `/test-integration`: Integration tests
 * `/test-e2e`: Linux IPv4 e2e tests
 * `/test-conformance`: Linux IPv4 conformance tests
 * `/test-networkpolicy`: Linux IPv4 networkpolicy tests
@@ -187,7 +186,7 @@ Here are the trigger phrases for groups of checks:
 * `/test-ipv6-only-all`: Linux IPv6 only tests
 
 Besides, you can skip a check with `/skip-*`, e.g. `/skip-e2e`: skip Linux IPv4
-e2e tests. Integration check cannot be skipped.
+e2e tests.
 
 Skipping a check should be used only when the change doesn't influence the
 specific function. For example:
diff --git a/Makefile b/Makefile
index 64f92785d..7961c0a79 100644
--- a/Makefile
+++ b/Makefile
@@ -241,7 +241,7 @@ tidy:
 	@echo
 	@echo "==> Running integration tests <=="
 	@echo "SOME TESTS WILL FAIL IF NOT RUN AS ROOT!"
-	$(GO) test -coverpkg=antrea.io/antrea/pkg/... -coverprofile=.coverage/coverage-integration.txt -covermode=atomic -cover antrea.io/antrea/test/integration/...
+	$(GO) test -v -coverpkg=antrea.io/antrea/pkg/... -coverprofile=.coverage/coverage-integration.txt -covermode=atomic -cover antrea.io/antrea/test/integration/...
 
 test-tidy:
 	@echo
diff --git a/build/charts/antrea/crds/ippool.yaml b/build/charts/antrea/crds/ippool.yaml
index ae0d4b8f9..ff1f3a4af 100644
--- a/build/charts/antrea/crds/ippool.yaml
+++ b/build/charts/antrea/crds/ippool.yaml
@@ -108,6 +108,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml
index a5f95f9e5..02f8d5344 100644
--- a/build/yamls/antrea-aks.yml
+++ b/build/yamls/antrea-aks.yml
@@ -1398,6 +1398,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea-crds.yml b/build/yamls/antrea-crds.yml
index ddfecc65e..f90cdd946 100644
--- a/build/yamls/antrea-crds.yml
+++ b/build/yamls/antrea-crds.yml
@@ -1383,6 +1383,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml
index 942173bcf..42001d9c5 100644
--- a/build/yamls/antrea-eks.yml
+++ b/build/yamls/antrea-eks.yml
@@ -1398,6 +1398,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml
index dd180753c..fa2f02d27 100644
--- a/build/yamls/antrea-gke.yml
+++ b/build/yamls/antrea-gke.yml
@@ -1398,6 +1398,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml
index 421aea111..bb735b76f 100644
--- a/build/yamls/antrea-ipsec.yml
+++ b/build/yamls/antrea-ipsec.yml
@@ -1398,6 +1398,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml
index 3947f26d7..c1f3d3782 100644
--- a/build/yamls/antrea.yml
+++ b/build/yamls/antrea.yml
@@ -1398,6 +1398,18 @@ spec:
                       type: integer
                   type: object
               type: object
+      additionalPrinterColumns:
+        - description: The number of total IPs
+          jsonPath: .status.usage.total
+          name: Total
+          type: integer
+        - description: The number of allocated IPs
+          jsonPath: .status.usage.used
+          name: Used
+          type: integer
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
       subresources:
         status: {}
   scope: Cluster
diff --git a/docs/antrea-network-policy.md b/docs/antrea-network-policy.md
index ff8186eb2..fcc18d6ea 100644
--- a/docs/antrea-network-policy.md
+++ b/docs/antrea-network-policy.md
@@ -647,11 +647,33 @@ The rules are logged in the following format:
     Deduplication:
     <yyyy/mm/dd> <time> <ovs-table-name> <antrea-native-policy-reference> <action> <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
 
-    Example:
+    Examples:
     2020/11/02 22:21:21.148395 AntreaPolicyAppTierIngressRule AntreaNetworkPolicy:default/test-anp Allow 61800 10.10.1.65 35402 10.0.0.5 80 TCP 60
     2021/06/24 23:56:41.346165 AntreaPolicyEgressRule AntreaNetworkPolicy:default/test-anp Drop 44900 10.10.1.65 35402 10.0.0.5 80 TCP 60 [3 packets in 1.011379442s]
 ```
 
+Kubernetes NetworkPolicies can also be audited using Antrea logging to the same file
+(`/var/log/antrea/networkpolicy/np.log`). Add Annotation
+`networkpolicy.antrea.io/enable-logging: "true` on a Namespace to enable logging
+for all NetworkPolicies in the Namespace. Packets of any connection that match
+a NetworkPolicy rule will be logged with a reference to the NetworkPolicy name,
+but packets dropped by the implicit "default drop" (not allowed by any NetworkPolicy)
+will only be logged with consistent name `K8sNetworkPolicy` for reference.
+Note that currently, Antrea only retrieves the logging Annotation once when adding
+NetworkPolicies and in case of agent restart, users should not update Namespace
+logging Annotations, otherwise it would risk NetworkPolicies working in a stale state.
+The rules are logged in the following format:
+
+```text
+    <yyyy/mm/dd> <time> <ovs-table-name> <k8s-network-policy-reference> Allow <openflow-priority> <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length>
+    Default dropped traffic:
+    <yyyy/mm/dd> <time> <ovs-table-name> K8sNetworkPolicy Drop -1 <source-ip> <source-port> <destination-ip> <destination-port> <protocol> <packet-length> [<num of packets> packets in <duplicate duration>]
+
+    Examples:
+    2022/07/26 06:55:56.170456 IngressRule K8sNetworkPolicy:default/test-np-log Allow 190 10.10.1.82 49518 10.10.1.84 80 TCP 60
+    2022/07/26 06:55:57.142206 IngressDefaultRule K8sNetworkPolicy Drop -1 10.10.1.83 38608 10.10.1.84 80 TCP 60
+```
+
 Fluentd can be used to assist with collecting and analyzing the logs. Refer to the
 [Fluentd cookbook](cookbooks/fluentd) for documentation.
 
diff --git a/multicluster/Makefile b/multicluster/Makefile
index df9168b77..58c49881c 100644
--- a/multicluster/Makefile
+++ b/multicluster/Makefile
@@ -71,15 +71,8 @@ test-unit: fmt vet .coverage
 	go test -race -coverprofile=.coverage/coverage-unit.txt -covermode=atomic -cover antrea.io/antrea/multicluster/controllers/multicluster/...
 test-integration: .coverage
 	@echo "==> Running Multi-cluster integration tests <=="
-ifneq ($(NO_LOCAL),)
-	@echo "===> Building Antrea Multi-cluster Integration Test Docker image <==="
-	docker build -t antrea/mc-test -f build/images/test/Dockerfile $(DOCKER_BUILD_ARGS) .
-	$(CURDIR)/hack/verify-tools.sh "/home/jenkins"
-	$(CURDIR)/hack/mc-integration-test.sh "/home/jenkins"
-else
 	$(CURDIR)/hack/verify-tools.sh
 	$(CURDIR)/hack/mc-integration-test.sh
-endif
 ##@ Build
 
 build: fmt vet ## Build manager binary.
diff --git a/multicluster/cmd/multicluster-controller/leader.go b/multicluster/cmd/multicluster-controller/leader.go
index 2321622d1..6ed3593e2 100644
--- a/multicluster/cmd/multicluster-controller/leader.go
+++ b/multicluster/cmd/multicluster-controller/leader.go
@@ -27,6 +27,7 @@ import (
 	multiclusterv1alpha1 "antrea.io/antrea/multicluster/apis/multicluster/v1alpha1"
 	multiclustercontrollers "antrea.io/antrea/multicluster/controllers/multicluster"
 	"antrea.io/antrea/pkg/log"
+	"antrea.io/antrea/pkg/signals"
 	"antrea.io/antrea/pkg/util/env"
 )
 
@@ -93,6 +94,16 @@ func runLeader(o *Options) error {
 	if err = (&multiclusterv1alpha1.ResourceExport{}).SetupWebhookWithManager(mgr); err != nil {
 		return fmt.Errorf("error creating ResourceExport webhook: %v", err)
 	}
+	stopCh := signals.RegisterSignalHandlers()
+	staleController := multiclustercontrollers.NewStaleResCleanupController(
+		mgr.GetClient(),
+		mgr.GetScheme(),
+		env.GetPodNamespace(),
+		nil,
+		multiclustercontrollers.LeaderCluster,
+	)
+
+	go staleController.Run(stopCh)
 
 	klog.InfoS("Leader MC Controller Starting Manager")
 	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
diff --git a/multicluster/cmd/multicluster-controller/member.go b/multicluster/cmd/multicluster-controller/member.go
index 5315d72a9..dcfeec082 100644
--- a/multicluster/cmd/multicluster-controller/member.go
+++ b/multicluster/cmd/multicluster-controller/member.go
@@ -95,7 +95,9 @@ func runMember(o *Options) error {
 		mgr.GetClient(),
 		mgr.GetScheme(),
 		env.GetPodNamespace(),
-		commonAreaGetter)
+		commonAreaGetter,
+		multiclustercontrollers.MemberCluster,
+	)
 
 	go staleController.Run(stopCh)
 	// Member runs ResourceImportReconciler from RemoteCommonArea only
diff --git a/multicluster/controllers/multicluster/commonarea/remote_common_area.go b/multicluster/controllers/multicluster/commonarea/remote_common_area.go
index 2282f2b9e..353dd05b6 100644
--- a/multicluster/controllers/multicluster/commonarea/remote_common_area.go
+++ b/multicluster/controllers/multicluster/commonarea/remote_common_area.go
@@ -234,7 +234,7 @@ func (r *remoteCommonArea) SendMemberAnnounce() error {
 		}
 		// Add timestamp to force update on MemberClusterAnnounce. Leader cluster requires
 		// periodic updates to detect connectivity. Without this, no-op updates will be ignored.
-		localClusterMemberAnnounce.Annotations[TimestampAnnotationKey] = time.Now().String()
+		localClusterMemberAnnounce.Annotations[TimestampAnnotationKey] = time.Now().Format(time.RFC3339)
 		if err := r.Update(context.TODO(), &localClusterMemberAnnounce, &client.UpdateOptions{}); err != nil {
 			klog.ErrorS(err, "Error updating MemberClusterAnnounce", "cluster", r.GetClusterID())
 			return err
diff --git a/multicluster/controllers/multicluster/stale_controller.go b/multicluster/controllers/multicluster/stale_controller.go
index 1cf1c4d91..a0512d64a 100644
--- a/multicluster/controllers/multicluster/stale_controller.go
+++ b/multicluster/controllers/multicluster/stale_controller.go
@@ -35,16 +35,26 @@ import (
 	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
 )
 
+const (
+	LeaderCluster = "leader"
+	MemberCluster = "member"
+
+	memberClusterAnnounceStaleTime = 5 * time.Minute
+)
+
 // StaleResCleanupController will clean up ServiceImport, MC Service, ACNP, ClusterInfoImport resources
 // if no corresponding ResourceImports in the leader cluster and remove stale ResourceExports
-// in the leader cluster if no corresponding ServiceExport or Gateway in the member cluster.
-// It will only run in the member cluster.
+// in the leader cluster if no corresponding ServiceExport or Gateway in the member cluster when it runs in
+// the member cluster.
+// It will clean up stale MemberClusterAnnounce resources in the leader cluster if no corresponding member
+// cluster in the ClusterSet.Spec.Members when it runs in the leader cluster.
 type StaleResCleanupController struct {
 	client.Client
 	Scheme           *runtime.Scheme
 	localClusterID   string
 	commonAreaGetter RemoteCommonAreaGetter
 	namespace        string
+	clusterRole      string
 	// queue only ever has one item, but it has nice error handling backoff/retry semantics
 	queue workqueue.RateLimitingInterface
 }
@@ -53,12 +63,15 @@ func NewStaleResCleanupController(
 	Client client.Client,
 	Scheme *runtime.Scheme,
 	namespace string,
-	commonAreaGetter RemoteCommonAreaGetter) *StaleResCleanupController {
+	commonAreaGetter RemoteCommonAreaGetter,
+	clusterRole string,
+) *StaleResCleanupController {
 	reconciler := &StaleResCleanupController{
 		Client:           Client,
 		Scheme:           Scheme,
 		namespace:        namespace,
 		commonAreaGetter: commonAreaGetter,
+		clusterRole:      clusterRole,
 		queue:            workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "StaleResCleanupController"),
 	}
 	return reconciler
@@ -70,6 +83,20 @@ func NewStaleResCleanupController(
 //+kubebuilder:rbac:groups=multicluster.crd.antrea.io,resources=resourceexports,verbs=get;list;watch;delete
 
 func (c *StaleResCleanupController) cleanup() error {
+	switch c.clusterRole {
+	case LeaderCluster:
+		return c.cleanupStaleResourcesOnLeader()
+	case MemberCluster:
+		return c.cleanupStaleResourcesOnMember()
+	}
+	return nil
+}
+
+func (c *StaleResCleanupController) cleanupStaleResourcesOnLeader() error {
+	return c.cleanupMemberClusterAnnounces()
+}
+
+func (c *StaleResCleanupController) cleanupStaleResourcesOnMember() error {
 	var err error
 	var commonArea commonarea.RemoteCommonArea
 	commonArea, c.localClusterID, err = c.commonAreaGetter.GetRemoteCommonAreaAndLocalID()
@@ -270,6 +297,32 @@ func (c *StaleResCleanupController) cleanupClusterInfoResourceExport(commonArea
 	return nil
 }
 
+func (c *StaleResCleanupController) cleanupMemberClusterAnnounces() error {
+	memberClusterAnnounceList := &mcsv1alpha1.MemberClusterAnnounceList{}
+	if err := c.List(ctx, memberClusterAnnounceList, &client.ListOptions{}); err != nil {
+		return err
+	}
+
+	for _, m := range memberClusterAnnounceList.Items {
+		memberClusterAnnounce := m
+		lastUpdateTime, err := time.Parse(time.RFC3339, memberClusterAnnounce.Annotations[commonarea.TimestampAnnotationKey])
+		if err == nil && time.Now().Sub(lastUpdateTime) < memberClusterAnnounceStaleTime {
+			continue
+		}
+		if err == nil {
+			klog.InfoS("Cleaning up stale MemberClusterAnnounce. It has not been updated within the agreed period", "MemberClusterAnnounce", klog.KObj(&memberClusterAnnounce), "agreedPeriod", memberClusterAnnounceStaleTime)
+		} else {
+			klog.InfoS("Cleaning up stale MemberClusterAnnounce. The latest update time is not in RFC3339 format", "MemberClusterAnnounce", klog.KObj(&memberClusterAnnounce))
+		}
+
+		if err := c.Client.Delete(ctx, &memberClusterAnnounce, &client.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
+			klog.ErrorS(err, "Failed to delete stale MemberClusterAnnounce", "MemberClusterAnnounce", klog.KObj(&memberClusterAnnounce))
+			return err
+		}
+	}
+	return nil
+}
+
 // Enqueue will be called after StaleResCleanupController is initialized.
 func (c *StaleResCleanupController) Enqueue() {
 	// The key can be anything as we only have single item.
diff --git a/pkg/agent/controller/networkpolicy/audit_logging.go b/pkg/agent/controller/networkpolicy/audit_logging.go
index 97265d12b..c29190374 100644
--- a/pkg/agent/controller/networkpolicy/audit_logging.go
+++ b/pkg/agent/controller/networkpolicy/audit_logging.go
@@ -28,6 +28,7 @@ import (
 	"k8s.io/klog/v2"
 
 	"antrea.io/antrea/pkg/agent/openflow"
+	"antrea.io/antrea/pkg/apis/controlplane/v1beta2"
 	binding "antrea.io/antrea/pkg/ovs/openflow"
 	"antrea.io/antrea/pkg/util/ip"
 	"antrea.io/antrea/pkg/util/logdir"
@@ -196,13 +197,17 @@ func getNetworkPolicyInfo(pktIn *ofctrl.PacketIn, c *Controller, ob *logInfo) er
 	// Set match to corresponding ingress/egress reg according to disposition.
 	match = getMatch(matchers, tableID, info)
 
-	// Get Network Policy full name and OF priority of the conjunction.
-	info, err = getInfoInReg(match, nil)
-	if err != nil {
-		return fmt.Errorf("received error while unloading conjunction id from reg: %v", err)
+	if match != nil {
+		// Get NetworkPolicy full name and OF priority of the conjunction.
+		info, err = getInfoInReg(match, nil)
+		if err != nil {
+			return fmt.Errorf("received error while unloading conjunction id from reg: %v", err)
+		}
+		ob.npRef, ob.ofPriority = c.ofClient.GetPolicyInfoFromConjunction(info)
+	} else {
+		// For K8s NetworkPolicy implicit drop action, we cannot get Namespace/name.
+		ob.npRef, ob.ofPriority = string(v1beta2.K8sNetworkPolicy), "-1"
 	}
-	ob.npRef, ob.ofPriority = c.ofClient.GetPolicyInfoFromConjunction(info)
-
 	return nil
 }
 
diff --git a/pkg/agent/controller/networkpolicy/packetin.go b/pkg/agent/controller/networkpolicy/packetin.go
index b14942149..7730641f1 100644
--- a/pkg/agent/controller/networkpolicy/packetin.go
+++ b/pkg/agent/controller/networkpolicy/packetin.go
@@ -180,7 +180,7 @@ func (c *Controller) storeDenyConnection(pktIn *ofctrl.PacketIn) error {
 			denyConn.EgressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
 		}
 	} else {
-		// For K8s NetworkPolicy implicit drop action, we cannot get name/namespace.
+		// For K8s NetworkPolicy implicit drop action, we cannot get Namespace/name.
 		if tableID == openflow.IngressDefaultTable.GetID() {
 			denyConn.IngressNetworkPolicyType = registry.PolicyTypeK8sNetworkPolicy
 			denyConn.IngressNetworkPolicyRuleAction = flowexporter.RuleActionToUint8(disposition)
diff --git a/pkg/agent/controller/networkpolicy/reconciler.go b/pkg/agent/controller/networkpolicy/reconciler.go
index f08718d77..f8a0d2cf3 100644
--- a/pkg/agent/controller/networkpolicy/reconciler.go
+++ b/pkg/agent/controller/networkpolicy/reconciler.go
@@ -734,7 +734,7 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 		} else {
 			addedTo := ofPortsToOFAddresses(newOFPorts.Difference(lastRealized.podOFPorts[igmpServicesKey]))
 			deletedTo := ofPortsToOFAddresses(lastRealized.podOFPorts[igmpServicesKey].Difference(newOFPorts))
-			if err := r.updateOFRule(ofID, nil, addedTo, nil, deletedTo, ofPriority); err != nil {
+			if err := r.updateOFRule(ofID, nil, addedTo, nil, deletedTo, ofPriority, newRule.EnableLogging); err != nil {
 				return err
 			}
 		}
@@ -803,7 +803,7 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 					addedTo = ofPortsToOFAddresses(newOFPorts.Difference(originalOfPortsSet))
 					deletedTo = ofPortsToOFAddresses(originalOfPortsSet.Difference(newOFPorts))
 				}
-				if err := r.updateOFRule(ofID, addedFrom, addedTo, deletedFrom, deletedTo, ofPriority); err != nil {
+				if err := r.updateOFRule(ofID, addedFrom, addedTo, deletedFrom, deletedTo, ofPriority, newRule.EnableLogging); err != nil {
 					return err
 				}
 				// Delete valid servicesKey from staleOFIDs.
@@ -893,7 +893,7 @@ func (r *reconciler) update(lastRealized *lastRealized, newRule *CompletedRule,
 					addedTo = svcGroupIDsToOFAddresses(newGroupIDSet.Difference(originalGroupIDSet))
 					deletedTo = svcGroupIDsToOFAddresses(originalGroupIDSet.Difference(newGroupIDSet))
 				}
-				if err := r.updateOFRule(ofID, addedFrom, addedTo, deletedFrom, deletedTo, ofPriority); err != nil {
+				if err := r.updateOFRule(ofID, addedFrom, addedTo, deletedFrom, deletedTo, ofPriority, newRule.EnableLogging); err != nil {
 					return err
 				}
 				if r.fqdnController != nil {
@@ -930,17 +930,17 @@ func (r *reconciler) installOFRule(ofRule *types.PolicyRule) error {
 	return nil
 }
 
-func (r *reconciler) updateOFRule(ofID uint32, addedFrom []types.Address, addedTo []types.Address, deletedFrom []types.Address, deletedTo []types.Address, priority *uint16) error {
+func (r *reconciler) updateOFRule(ofID uint32, addedFrom []types.Address, addedTo []types.Address, deletedFrom []types.Address, deletedTo []types.Address, priority *uint16, enableLogging bool) error {
 	klog.V(2).Infof("Updating ofRule %d (addedFrom: %d, addedTo: %d, deleteFrom: %d, deletedTo: %d)",
 		ofID, len(addedFrom), len(addedTo), len(deletedFrom), len(deletedTo))
 	// TODO: This might be unnecessarily complex and hard for error handling, consider revising the Openflow interfaces.
 	if len(addedFrom) > 0 {
-		if err := r.ofClient.AddPolicyRuleAddress(ofID, types.SrcAddress, addedFrom, priority); err != nil {
+		if err := r.ofClient.AddPolicyRuleAddress(ofID, types.SrcAddress, addedFrom, priority, enableLogging); err != nil {
 			return fmt.Errorf("error adding policy rule source addresses for ofRule %v: %v", ofID, err)
 		}
 	}
 	if len(addedTo) > 0 {
-		if err := r.ofClient.AddPolicyRuleAddress(ofID, types.DstAddress, addedTo, priority); err != nil {
+		if err := r.ofClient.AddPolicyRuleAddress(ofID, types.DstAddress, addedTo, priority, enableLogging); err != nil {
 			return fmt.Errorf("error adding policy rule destination addresses for ofRule %v: %v", ofID, err)
 		}
 	}
diff --git a/pkg/agent/openflow/client.go b/pkg/agent/openflow/client.go
index cd0cea0b9..f3a791744 100644
--- a/pkg/agent/openflow/client.go
+++ b/pkg/agent/openflow/client.go
@@ -125,7 +125,7 @@ type Client interface {
 
 	// AddPolicyRuleAddress adds one or multiple addresses to the specified NetworkPolicy rule. If addrType is true, the
 	// addresses are added to PolicyRule.From, else to PolicyRule.To.
-	AddPolicyRuleAddress(ruleID uint32, addrType types.AddressType, addresses []types.Address, priority *uint16) error
+	AddPolicyRuleAddress(ruleID uint32, addrType types.AddressType, addresses []types.Address, priority *uint16, enableLogging bool) error
 
 	// DeletePolicyRuleAddress removes addresses from the specified NetworkPolicy rule. If addrType is srcAddress, the addresses
 	// are removed from PolicyRule.From, else from PolicyRule.To.
diff --git a/pkg/agent/openflow/network_policy.go b/pkg/agent/openflow/network_policy.go
index 20624b95e..489a956dc 100644
--- a/pkg/agent/openflow/network_policy.go
+++ b/pkg/agent/openflow/network_policy.go
@@ -422,6 +422,8 @@ type conjMatchFlowContext struct {
 	// empty, and uninstalled when both two are empty. When the dropFlow is uninstalled from the switch, the
 	// conjMatchFlowContext is removed from the cache.
 	dropFlow binding.Flow
+	// dropFlowEnableLogging describes the logging requirement of the dropFlow.
+	dropFlowEnableLogging bool
 }
 
 // createOrUpdateConjunctiveMatchFlow creates or updates the conjunctive match flow with the latest actions. It returns
@@ -684,7 +686,7 @@ func (c *client) NewDNSpacketInConjunction(id uint32) error {
 
 	c.featureNetworkPolicy.conjMatchFlowLock.Lock()
 	defer c.featureNetworkPolicy.conjMatchFlowLock.Unlock()
-	ctxChanges := conj.serviceClause.addServiceFlows(c.featureNetworkPolicy, []v1beta2.Service{udpService}, &dnsPriority, true)
+	ctxChanges := conj.serviceClause.addServiceFlows(c.featureNetworkPolicy, []v1beta2.Service{udpService}, &dnsPriority, true, false)
 	if err := c.featureNetworkPolicy.applyConjunctiveMatchFlows(ctxChanges); err != nil {
 		return err
 	}
@@ -695,7 +697,7 @@ func (c *client) NewDNSpacketInConjunction(id uint32) error {
 
 func (c *client) AddAddressToDNSConjunction(id uint32, addrs []types.Address) error {
 	dnsPriority := priorityDNSIntercept
-	return c.AddPolicyRuleAddress(id, types.DstAddress, addrs, &dnsPriority)
+	return c.AddPolicyRuleAddress(id, types.DstAddress, addrs, &dnsPriority, false)
 }
 
 func (c *client) DeleteAddressFromDNSConjunction(id uint32, addrs []types.Address) error {
@@ -703,7 +705,7 @@ func (c *client) DeleteAddressFromDNSConjunction(id uint32, addrs []types.Addres
 	return c.DeletePolicyRuleAddress(id, types.DstAddress, addrs, &dnsPriority)
 }
 
-func (c *clause) addConjunctiveMatchFlow(featureNetworkPolicy *featureNetworkPolicy, match *conjunctiveMatch) *conjMatchFlowContextChange {
+func (c *clause) addConjunctiveMatchFlow(featureNetworkPolicy *featureNetworkPolicy, match *conjunctiveMatch, enableLogging bool) *conjMatchFlowContextChange {
 	matcherKey := match.generateGlobalMapKey()
 	_, found := c.matches[matcherKey]
 	if found {
@@ -718,19 +720,29 @@ func (c *clause) addConjunctiveMatchFlow(featureNetworkPolicy *featureNetworkPol
 	context, found = featureNetworkPolicy.globalConjMatchFlowCache[matcherKey]
 	if !found {
 		context = &conjMatchFlowContext{
-			conjunctiveMatch:     match,
-			actions:              make(map[uint32]*conjunctiveAction),
-			featureNetworkPolicy: featureNetworkPolicy,
+			conjunctiveMatch:      match,
+			actions:               make(map[uint32]*conjunctiveAction),
+			featureNetworkPolicy:  featureNetworkPolicy,
+			dropFlowEnableLogging: enableLogging,
 		}
 		ctxType = insertion
 
 		// Generate the default drop flow if dropTable is not nil and the default drop flow is not set yet.
 		if c.dropTable != nil && context.dropFlow == nil {
 			dropFlow = &flowChange{
-				flow:       context.featureNetworkPolicy.defaultDropFlow(c.dropTable, match.matchPairs),
+				flow:       context.featureNetworkPolicy.defaultDropFlow(c.dropTable, match.matchPairs, enableLogging),
 				changeType: insertion,
 			}
 		}
+	} else if context.dropFlowEnableLogging != enableLogging {
+		// Logging requirement of the rule has changed, modify default drop flow accordingly.
+		context.dropFlowEnableLogging = enableLogging
+		if c.dropTable != nil && context.dropFlow != nil {
+			dropFlow = &flowChange{
+				flow:       context.featureNetworkPolicy.defaultDropFlow(c.dropTable, match.matchPairs, enableLogging),
+				changeType: modification,
+			}
+		}
 	}
 
 	// Calculate the change on the conjMatchFlowContext.
@@ -923,12 +935,12 @@ func serviceToBitRanges(service v1beta2.Service) []types.BitRange {
 
 // addAddrFlows translates the specified addresses to conjunctiveMatchFlows, and returns the corresponding changes on the
 // conjunctiveMatchFlows.
-func (c *clause) addAddrFlows(featureNetworkPolicy *featureNetworkPolicy, addrType types.AddressType, addresses []types.Address, priority *uint16) []*conjMatchFlowContextChange {
+func (c *clause) addAddrFlows(featureNetworkPolicy *featureNetworkPolicy, addrType types.AddressType, addresses []types.Address, priority *uint16, enableLogging bool) []*conjMatchFlowContextChange {
 	var conjMatchFlowContextChanges []*conjMatchFlowContextChange
 	// Calculate Openflow changes for the added addresses.
 	for _, addr := range addresses {
 		match := generateAddressConjMatch(c.ruleTable.GetID(), addr, addrType, priority)
-		ctxChange := c.addConjunctiveMatchFlow(featureNetworkPolicy, match)
+		ctxChange := c.addConjunctiveMatchFlow(featureNetworkPolicy, match, enableLogging)
 		if ctxChange != nil {
 			conjMatchFlowContextChanges = append(conjMatchFlowContextChanges, ctxChange)
 		}
@@ -938,12 +950,12 @@ func (c *clause) addAddrFlows(featureNetworkPolicy *featureNetworkPolicy, addrTy
 
 // addServiceFlows translates the specified Antrea Service to conjunctiveMatchFlow,
 // and returns corresponding conjMatchFlowContextChange.
-func (c *clause) addServiceFlows(featureNetworkPolicy *featureNetworkPolicy, services []v1beta2.Service, priority *uint16, matchSrc bool) []*conjMatchFlowContextChange {
+func (c *clause) addServiceFlows(featureNetworkPolicy *featureNetworkPolicy, services []v1beta2.Service, priority *uint16, matchSrc bool, enableLogging bool) []*conjMatchFlowContextChange {
 	var conjMatchFlowContextChanges []*conjMatchFlowContextChange
 	for _, service := range services {
 		matches := generateServiceConjMatches(c.ruleTable.GetID(), service, priority, featureNetworkPolicy.ipProtocols, matchSrc)
 		for _, match := range matches {
-			ctxChange := c.addConjunctiveMatchFlow(featureNetworkPolicy, match)
+			ctxChange := c.addConjunctiveMatchFlow(featureNetworkPolicy, match, enableLogging)
 			conjMatchFlowContextChanges = append(conjMatchFlowContextChanges, ctxChange)
 		}
 	}
@@ -1143,20 +1155,20 @@ func (f *featureNetworkPolicy) addRuleToConjunctiveMatch(conj *policyRuleConjunc
 	if conj.fromClause != nil {
 		for _, addr := range rule.From {
 			match := generateAddressConjMatch(conj.fromClause.ruleTable.GetID(), addr, types.SrcAddress, rule.Priority)
-			f.addActionToConjunctiveMatch(conj.fromClause, match)
+			f.addActionToConjunctiveMatch(conj.fromClause, match, rule.EnableLogging)
 		}
 	}
 	if conj.toClause != nil {
 		for _, addr := range rule.To {
 			match := generateAddressConjMatch(conj.toClause.ruleTable.GetID(), addr, types.DstAddress, rule.Priority)
-			f.addActionToConjunctiveMatch(conj.toClause, match)
+			f.addActionToConjunctiveMatch(conj.toClause, match, rule.EnableLogging)
 		}
 	}
 	if conj.serviceClause != nil {
 		for _, eachService := range rule.Service {
 			matches := generateServiceConjMatches(conj.serviceClause.ruleTable.GetID(), eachService, rule.Priority, f.ipProtocols, false)
 			for _, match := range matches {
-				f.addActionToConjunctiveMatch(conj.serviceClause, match)
+				f.addActionToConjunctiveMatch(conj.serviceClause, match, rule.EnableLogging)
 			}
 		}
 	}
@@ -1165,7 +1177,7 @@ func (f *featureNetworkPolicy) addRuleToConjunctiveMatch(conj *policyRuleConjunc
 // addActionToConjunctiveMatch adds a clause to corresponding conjunctive match context.
 // It updates the context status directly and doesn't calculate the match flow, which is supposed to be calculated after
 // all actions are added. It's used in initial batch install only.
-func (f *featureNetworkPolicy) addActionToConjunctiveMatch(clause *clause, match *conjunctiveMatch) {
+func (f *featureNetworkPolicy) addActionToConjunctiveMatch(clause *clause, match *conjunctiveMatch, enableLogging bool) {
 	matcherKey := match.generateGlobalMapKey()
 	_, found := clause.matches[matcherKey]
 	if found {
@@ -1178,13 +1190,14 @@ func (f *featureNetworkPolicy) addActionToConjunctiveMatch(clause *clause, match
 	context, found = f.globalConjMatchFlowCache[matcherKey]
 	if !found {
 		context = &conjMatchFlowContext{
-			conjunctiveMatch:     match,
-			actions:              make(map[uint32]*conjunctiveAction),
-			featureNetworkPolicy: f,
+			conjunctiveMatch:      match,
+			actions:               make(map[uint32]*conjunctiveAction),
+			featureNetworkPolicy:  f,
+			dropFlowEnableLogging: enableLogging,
 		}
 		// Generate the default drop flow if dropTable is not nil.
 		if clause.dropTable != nil {
-			context.dropFlow = context.featureNetworkPolicy.defaultDropFlow(clause.dropTable, match.matchPairs)
+			context.dropFlow = context.featureNetworkPolicy.defaultDropFlow(clause.dropTable, match.matchPairs, enableLogging)
 		}
 		f.globalConjMatchFlowCache[matcherKey] = context
 	}
@@ -1371,13 +1384,13 @@ func (c *policyRuleConjunction) calculateClauses(rule *types.PolicyRule) (uint8,
 func (c *policyRuleConjunction) calculateChangesForRuleCreation(featureNetworkPolicy *featureNetworkPolicy, rule *types.PolicyRule) []*conjMatchFlowContextChange {
 	var ctxChanges []*conjMatchFlowContextChange
 	if c.fromClause != nil {
-		ctxChanges = append(ctxChanges, c.fromClause.addAddrFlows(featureNetworkPolicy, types.SrcAddress, rule.From, rule.Priority)...)
+		ctxChanges = append(ctxChanges, c.fromClause.addAddrFlows(featureNetworkPolicy, types.SrcAddress, rule.From, rule.Priority, rule.EnableLogging)...)
 	}
 	if c.toClause != nil {
-		ctxChanges = append(ctxChanges, c.toClause.addAddrFlows(featureNetworkPolicy, types.DstAddress, rule.To, rule.Priority)...)
+		ctxChanges = append(ctxChanges, c.toClause.addAddrFlows(featureNetworkPolicy, types.DstAddress, rule.To, rule.Priority, rule.EnableLogging)...)
 	}
 	if c.serviceClause != nil {
-		ctxChanges = append(ctxChanges, c.serviceClause.addServiceFlows(featureNetworkPolicy, rule.Service, rule.Priority, false)...)
+		ctxChanges = append(ctxChanges, c.serviceClause.addServiceFlows(featureNetworkPolicy, rule.Service, rule.Priority, false, rule.EnableLogging)...)
 	}
 	return ctxChanges
 }
@@ -1550,7 +1563,7 @@ func (f *featureNetworkPolicy) replayFlows() []binding.Flow {
 
 // AddPolicyRuleAddress adds one or multiple addresses to the specified NetworkPolicy rule. If addrType is srcAddress, the
 // addresses are added to PolicyRule.From, else to PolicyRule.To.
-func (c *client) AddPolicyRuleAddress(ruleID uint32, addrType types.AddressType, addresses []types.Address, priority *uint16) error {
+func (c *client) AddPolicyRuleAddress(ruleID uint32, addrType types.AddressType, addresses []types.Address, priority *uint16, enableLogging bool) error {
 	c.replayMutex.RLock()
 	defer c.replayMutex.RUnlock()
 
@@ -1569,7 +1582,7 @@ func (c *client) AddPolicyRuleAddress(ruleID uint32, addrType types.AddressType,
 
 	c.featureNetworkPolicy.conjMatchFlowLock.Lock()
 	defer c.featureNetworkPolicy.conjMatchFlowLock.Unlock()
-	flowChanges := clause.addAddrFlows(c.featureNetworkPolicy, addrType, addresses, priority)
+	flowChanges := clause.addAddrFlows(c.featureNetworkPolicy, addrType, addresses, priority, enableLogging)
 	return c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChanges)
 }
 
diff --git a/pkg/agent/openflow/pipeline.go b/pkg/agent/openflow/pipeline.go
index 0abfae8c1..0bf44bb7e 100644
--- a/pkg/agent/openflow/pipeline.go
+++ b/pkg/agent/openflow/pipeline.go
@@ -2118,22 +2118,28 @@ func (f *featureNetworkPolicy) conjunctiveMatchFlow(tableID uint8, matchPairs []
 }
 
 // defaultDropFlow generates the flow to drop packets if the match condition is matched.
-func (f *featureNetworkPolicy) defaultDropFlow(table binding.Table, matchPairs []matchPair) binding.Flow {
+func (f *featureNetworkPolicy) defaultDropFlow(table binding.Table, matchPairs []matchPair, enableLogging bool) binding.Flow {
 	cookieID := f.cookieAllocator.Request(f.category).Raw()
 	fb := table.BuildFlow(priorityNormal)
 	for _, eachMatchPair := range matchPairs {
 		fb = f.addFlowMatch(fb, eachMatchPair.matchKey, eachMatchPair.matchValue)
 	}
+	fb = fb.Action().Drop()
+
+	var customReason int
 	if f.enableDenyTracking {
-		return fb.Action().Drop().
-			Action().LoadRegMark(DispositionDropRegMark, CustomReasonDenyRegMark).
-			Action().SendToController(uint8(PacketInReasonNP)).
-			Cookie(cookieID).
-			Done()
+		customReason += CustomReasonDeny
 	}
-	return fb.Action().Drop().
-		Cookie(cookieID).
-		Done()
+	if enableLogging {
+		customReason += CustomReasonLogging
+	}
+
+	if enableLogging || f.enableDenyTracking {
+		fb = fb.Action().LoadRegMark(DispositionDropRegMark).
+			Action().LoadToRegField(CustomReasonField, uint32(customReason)).
+			Action().SendToController(uint8(PacketInReasonNP))
+	}
+	return fb.Cookie(cookieID).Done()
 }
 
 // dnsPacketInFlow generates the flow to send dns response packets of fqdn policy selected Pods to the fqdnController for
diff --git a/pkg/controller/networkpolicy/clusternetworkpolicy.go b/pkg/controller/networkpolicy/clusternetworkpolicy.go
index cd6ce44a9..886c5abdf 100644
--- a/pkg/controller/networkpolicy/clusternetworkpolicy.go
+++ b/pkg/controller/networkpolicy/clusternetworkpolicy.go
@@ -214,10 +214,12 @@ func (n *NetworkPolicyController) addNamespace(obj interface{}) {
 
 // updateNamespace receives Namespace UPDATE events and triggers all ClusterNetworkPolicies that have a
 // per-namespace rule applied to either the original or the new Namespace to be re-processed.
+// It also triggers all K8s NetworkPolicies in the new Namespace to be re-processed
+// if the logging Annotation changes.
 func (n *NetworkPolicyController) updateNamespace(oldObj, curObj interface{}) {
 	defer n.heartbeat("updateNamespace")
 	oldNamespace, curNamespace := oldObj.(*v1.Namespace), curObj.(*v1.Namespace)
-	klog.V(2).Infof("Processing Namespace %s UPDATE event, labels: %v", curNamespace.Name, curNamespace.Labels)
+	klog.V(2).Infof("Processing Namespace %s UPDATE event, labels: %v, annotations: %v", curNamespace.Name, curNamespace.Labels, curNamespace.Annotations)
 	oldLabelSet, curLabelSet := labels.Set(oldNamespace.Labels), labels.Set(curNamespace.Labels)
 	affectedACNPsByOldLabels := n.filterPerNamespaceRuleACNPsByNSLabels(oldLabelSet)
 	affectedACNPsByCurLabels := n.filterPerNamespaceRuleACNPsByNSLabels(curLabelSet)
@@ -227,6 +229,12 @@ func (n *NetworkPolicyController) updateNamespace(oldObj, curObj interface{}) {
 			n.reprocessCNP(cnp, false)
 		}
 	}
+	if oldNamespace.Annotations[EnableNPLoggingAnnotationKey] != curNamespace.Annotations[EnableNPLoggingAnnotationKey] {
+		affectedNPs, _ := n.networkPolicyLister.NetworkPolicies(curNamespace.Name).List(labels.Everything())
+		for _, np := range affectedNPs {
+			n.updateNetworkPolicy(np, np)
+		}
+	}
 }
 
 // deleteNamespace receives Namespace DELETE events and triggers all ClusterNetworkPolicies that have a
diff --git a/pkg/controller/networkpolicy/networkpolicy_controller.go b/pkg/controller/networkpolicy/networkpolicy_controller.go
index 02d7856f8..ce5e819ea 100644
--- a/pkg/controller/networkpolicy/networkpolicy_controller.go
+++ b/pkg/controller/networkpolicy/networkpolicy_controller.go
@@ -81,6 +81,8 @@ const (
 	PriorityIndex = "priority"
 	// ClusterGroupIndex is used to index ClusterNetworkPolicies by ClusterGroup names.
 	ClusterGroupIndex = "clustergroup"
+	// EnableNPLoggingAnnotationKey can be added to Namespace to enable logging K8s NP.
+	EnableNPLoggingAnnotationKey = "networkpolicy.antrea.io/enable-logging"
 
 	appliedToGroupType grouping.GroupType = "appliedToGroup"
 	addressGroupType   grouping.GroupType = "addressGroup"
@@ -566,6 +568,12 @@ func (n *NetworkPolicyController) processNetworkPolicy(np *networkingv1.NetworkP
 	appliedToGroupKey := n.createAppliedToGroup(np.Namespace, &np.Spec.PodSelector, nil, nil)
 	appliedToGroupNames := []string{appliedToGroupKey}
 	rules := make([]controlplane.NetworkPolicyRule, 0, len(np.Spec.Ingress)+len(np.Spec.Egress))
+	// Retrieve Namespace logging annotation.
+	enableLogging := false
+	namespace, err := n.namespaceLister.Get(np.Namespace)
+	if err == nil {
+		enableLogging, _ = strconv.ParseBool(namespace.Annotations[EnableNPLoggingAnnotationKey])
+	}
 	var ingressRuleExists, egressRuleExists bool
 	// Compute NetworkPolicyRule for Ingress Rule.
 	for _, ingressRule := range np.Spec.Ingress {
@@ -577,7 +585,7 @@ func (n *NetworkPolicyController) processNetworkPolicy(np *networkingv1.NetworkP
 			Services:      services,
 			Priority:      defaultRulePriority,
 			Action:        &defaultAction,
-			EnableLogging: false,
+			EnableLogging: enableLogging,
 		})
 	}
 	// Compute NetworkPolicyRule for Egress Rule.
@@ -590,7 +598,7 @@ func (n *NetworkPolicyController) processNetworkPolicy(np *networkingv1.NetworkP
 			Services:      services,
 			Priority:      defaultRulePriority,
 			Action:        &defaultAction,
-			EnableLogging: false,
+			EnableLogging: enableLogging,
 		})
 	}
 


Test patch:
diff --git a/ci/jenkins/test-vmc.sh b/ci/jenkins/test-vmc.sh
index b7601fc87..4515db2bb 100755
--- a/ci/jenkins/test-vmc.sh
+++ b/ci/jenkins/test-vmc.sh
@@ -40,16 +40,16 @@ DOCKER_REGISTRY=""
 CONTROL_PLANE_NODE_ROLE="master|control-plane"
 
 _usage="Usage: $0 [--cluster-name <VMCClusterNameToUse>] [--kubeconfig <KubeconfigSavePath>] [--workdir <HomePath>]
-                  [--log-mode <SonobuoyResultLogLevel>] [--testcase <e2e|integration|conformance|all-features-conformance|whole-conformance|networkpolicy>]
+                  [--log-mode <SonobuoyResultLogLevel>] [--testcase <e2e|conformance|all-features-conformance|whole-conformance|networkpolicy>]
                   [--garbage-collection] [--setup-only] [--cleanup-only] [--coverage] [--test-only] [--codecov-token] [--registry]
 
-Setup a VMC cluster to run K8s e2e community tests (E2e, Integration, Conformance, all features Conformance, whole Conformance & Network Policy).
+Setup a VMC cluster to run K8s e2e community tests (E2e, Conformance, all features Conformance, whole Conformance & Network Policy).
 
         --cluster-name           The cluster name to be used for the generated VMC cluster.
         --kubeconfig             Path to save kubeconfig of generated VMC cluster.
         --workdir                Home path for Go, vSphere information and antrea_logs during cluster setup. Default is $WORKDIR.
         --log-mode               Use the flag to set either 'report', 'detail', or 'dump' level data for sonobouy results.
-        --testcase               The testcase to run: e2e, integration, conformance, all-features-conformance, whole-conformance or networkpolicy.
+        --testcase               The testcase to run: e2e, conformance, all-features-conformance, whole-conformance or networkpolicy.
         --garbage-collection     Do garbage collection to clean up some unused testbeds.
         --setup-only             Only perform setting up the cluster and run test.
         --cleanup-only           Only perform cleaning up the cluster.
@@ -466,26 +466,6 @@ function deliver_antrea {
     done
 }
 
-function run_integration {
-    flag=$1
-    VM_NAME="antrea-integration-0"
-    export GOVC_INSECURE=1
-    export GOVC_URL=${GOVC_URL}
-    export GOVC_USERNAME=${GOVC_USERNAME}
-    export GOVC_PASSWORD=${GOVC_PASSWORD}
-    VM_IP=$(govc vm.ip ${VM_NAME})
-    govc snapshot.revert -vm.ip ${VM_IP} initial
-    VM_IP=$(govc vm.ip ${VM_NAME}) # wait for VM to be on
-
-    set -x
-    echo "===== Run Integration tests ====="
-    # umask ensures that files are cloned with the correct permissions so that Docker caching can be leveraged
-    ${SSH_WITH_UTILS_KEY} -n jenkins@${VM_IP} "umask 0022 && git clone ${ghprbAuthorRepoGitUrl} antrea && cd antrea && git checkout ${GIT_BRANCH} && DOCKER_REGISTRY=${DOCKER_REGISTRY} ./build/images/ovs/build.sh --pull && NO_PULL=${NO_PULL} make docker-test-integration;cd multicluster && NO_LOCAL=true make test-integration"
-    if [[ "$COVERAGE" == true ]]; then
-      run_codecov "integration-tests" "coverage-integration.txt" "" true ${VM_IP}
-    fi
-}
-
 function run_e2e {
     echo "====== Running Antrea E2E Tests ======"
 
@@ -707,8 +687,8 @@ if [[ "$RUN_CLEANUP_ONLY" == true ]]; then
     exit 0
 fi
 
-if [[ "$TESTCASE" != "e2e" && "$TESTCASE" != "conformance" && "$TESTCASE" != "all-features-conformance" && "$TESTCASE" != "whole-conformance" && "$TESTCASE" != "networkpolicy" && "$TESTCASE" != "integration" ]]; then
-    echoerr "testcase should be e2e, integration, conformance, all-features-conformance, whole-conformance or networkpolicy"
+if [[ "$TESTCASE" != "e2e" && "$TESTCASE" != "conformance" && "$TESTCASE" != "all-features-conformance" && "$TESTCASE" != "whole-conformance" && "$TESTCASE" != "networkpolicy" ]]; then
+    echoerr "testcase should be e2e, conformance, all-features-conformance, whole-conformance or networkpolicy"
     exit 1
 fi
 
@@ -724,11 +704,6 @@ if [[ "$RUN_TEST_ONLY" == true ]]; then
     exit 0
 fi
 
-if [[ "$TESTCASE" == "integration" ]]; then
-    run_integration
-    exit 0
-fi
-
 trap cleanup_cluster EXIT
 if [[ "$TESTCASE" == "e2e" ]]; then
     setup_cluster
diff --git a/multicluster/build/images/test/Dockerfile b/multicluster/build/images/test/Dockerfile
deleted file mode 100644
index 7683e1f5e..000000000
--- a/multicluster/build/images/test/Dockerfile
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2022 Antrea Authors
-#
-# Licensed 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.
-
-FROM ubuntu:20.04
-
-LABEL maintainer="Antrea <projectantrea-dev@googlegroups.com>"
-LABEL description="A Docker image for Antrea Multi-cluster integration tests."
-
-RUN apt-get update && \
-    apt-get install -y --no-install-recommends iproute2 iptables ipset make wget gcc libc6-dev ca-certificates git && \
-    rm -rf /var/cache/apt/* /var/lib/apt/lists/*
-
-ARG GO_VERSION
-ENV GOPATH /go
-
-RUN wget -q -O - https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz | tar xz -C /usr/local/ && \
-    export PATH="/usr/local/go/bin:$PATH" && \
-    mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
-
-ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
-
-WORKDIR $GOPATH
-
-COPY build/images/test/test-integration /usr/local/bin/
diff --git a/multicluster/build/images/test/test-integration b/multicluster/build/images/test/test-integration
deleted file mode 100755
index 5c2aa2ff1..000000000
--- a/multicluster/build/images/test/test-integration
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env bash
-
-go test -coverpkg=antrea.io/antrea/multicluster/controllers/multicluster/... -coverprofile=multicluster/.coverage/coverage-integration.txt -covermode=atomic -cover antrea.io/antrea/multicluster/test/integration/...
diff --git a/multicluster/controllers/multicluster/stale_controller_test.go b/multicluster/controllers/multicluster/stale_controller_test.go
index 43f8d16e8..efdf83527 100644
--- a/multicluster/controllers/multicluster/stale_controller_test.go
+++ b/multicluster/controllers/multicluster/stale_controller_test.go
@@ -19,7 +19,9 @@ package multicluster
 import (
 	"context"
 	"testing"
+	"time"
 
+	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -95,7 +97,7 @@ func TestStaleController_CleanupService(t *testing.T) {
 			commonArea := commonarea.NewFakeRemoteCommonArea(fakeRemoteClient, "leader-cluster", localClusterID, "default")
 			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
 			mcReconciler.SetRemoteCommonArea(commonArea)
-			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler)
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, MemberCluster)
 			if err := c.cleanup(); err != nil {
 				t.Errorf("StaleController.cleanup() should clean up all stale Service and ServiceImport but got err = %v", err)
 			}
@@ -190,7 +192,7 @@ func TestStaleController_CleanupACNP(t *testing.T) {
 
 			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
 			mcReconciler.SetRemoteCommonArea(commonArea)
-			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler)
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, MemberCluster)
 			if err := c.cleanup(); err != nil {
 				t.Errorf("StaleController.cleanup() should clean up all stale ACNPs but got err = %v", err)
 			}
@@ -319,7 +321,7 @@ func TestStaleController_CleanupResourceExport(t *testing.T) {
 
 			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
 			mcReconciler.SetRemoteCommonArea(commonArea)
-			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler)
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, MemberCluster)
 			if err := c.cleanup(); err != nil {
 				t.Errorf("StaleController.cleanup() should clean up all stale ResourceExports but got err = %v", err)
 			}
@@ -400,7 +402,7 @@ func TestStaleController_CleanupClusterInfoImport(t *testing.T) {
 
 			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
 			mcReconciler.SetRemoteCommonArea(commonarea)
-			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler)
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, MemberCluster)
 			if err := c.cleanup(); err != nil {
 				t.Errorf("StaleController.cleanup() should clean up all stale ClusterInfoImport but got err = %v", err)
 			}
@@ -418,3 +420,118 @@ func TestStaleController_CleanupClusterInfoImport(t *testing.T) {
 		})
 	}
 }
+
+func TestStaleController_CleanupMemberClusterAnnounce(t *testing.T) {
+	tests := []struct {
+		name                              string
+		memberClusterAnnounceList         *mcsv1alpha1.MemberClusterAnnounceList
+		clusterSet                        *mcsv1alpha1.ClusterSetList
+		exceptMemberClusterAnnounceNumber int
+	}{
+		{
+			name:                              "no MemberClusterAnnounce to clean up when there is no resource",
+			clusterSet:                        &mcsv1alpha1.ClusterSetList{},
+			memberClusterAnnounceList:         &mcsv1alpha1.MemberClusterAnnounceList{},
+			exceptMemberClusterAnnounceNumber: 0,
+		},
+		{
+			name:                              "no MemberClusterAnnounce to clean up when the resource has a valid update time",
+			exceptMemberClusterAnnounceNumber: 1,
+			clusterSet: &mcsv1alpha1.ClusterSetList{
+				Items: []mcsv1alpha1.ClusterSet{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "clusterset",
+						},
+						Spec: mcsv1alpha1.ClusterSetSpec{
+							Members: []mcsv1alpha1.MemberCluster{
+								{
+									ClusterID: "cluster-a",
+								},
+							},
+						},
+					},
+				},
+			},
+			memberClusterAnnounceList: &mcsv1alpha1.MemberClusterAnnounceList{
+				Items: []mcsv1alpha1.MemberClusterAnnounce{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "member-cluster-from-cluster-a",
+							Annotations: map[string]string{
+								commonarea.TimestampAnnotationKey: time.Now().Format(time.RFC3339),
+							},
+						},
+						ClusterID: "cluster-a",
+					},
+				},
+			},
+		},
+		{
+			name:                              "clean up outdated MemberClusterAnnounce",
+			exceptMemberClusterAnnounceNumber: 1,
+			clusterSet: &mcsv1alpha1.ClusterSetList{
+				Items: []mcsv1alpha1.ClusterSet{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "clusterset",
+						},
+						Spec: mcsv1alpha1.ClusterSetSpec{
+							Members: []mcsv1alpha1.MemberCluster{
+								{
+									ClusterID: "cluster-a",
+								},
+								{
+									ClusterID: "cluster-outdated",
+								},
+							},
+						},
+					},
+				},
+			},
+			memberClusterAnnounceList: &mcsv1alpha1.MemberClusterAnnounceList{
+				Items: []mcsv1alpha1.MemberClusterAnnounce{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "member-cluster-from-cluster-a",
+							Annotations: map[string]string{
+								commonarea.TimestampAnnotationKey: time.Now().Format(time.RFC3339),
+							},
+						},
+						ClusterID: "cluster-a",
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "default",
+							Name:      "member-cluster-from-cluster-outdated",
+							Annotations: map[string]string{
+								commonarea.TimestampAnnotationKey: time.Now().Add(-1 * time.Hour).Format(time.RFC3339),
+							},
+						},
+						ClusterID: "cluster-outdated",
+					},
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(tt.memberClusterAnnounceList).WithLists(tt.clusterSet).Build()
+
+			mcReconciler := NewMemberClusterSetReconciler(fakeClient, scheme, "default")
+			c := NewStaleResCleanupController(fakeClient, scheme, "default", mcReconciler, LeaderCluster)
+			assert.Equal(t, nil, c.cleanup())
+
+			memberClusterAnnounceList := &mcsv1alpha1.MemberClusterAnnounceList{}
+			if err := fakeClient.List(context.TODO(), memberClusterAnnounceList, &client.ListOptions{}); err != nil {
+				t.Errorf("Should list MemberClusterAnnounce successfully but got err = %v", err)
+			}
+
+			assert.Equal(t, tt.exceptMemberClusterAnnounceNumber, len(memberClusterAnnounceList.Items))
+		})
+	}
+}
diff --git a/multicluster/hack/mc-integration-test.sh b/multicluster/hack/mc-integration-test.sh
index 1ea2eaef7..89b0adde8 100755
--- a/multicluster/hack/mc-integration-test.sh
+++ b/multicluster/hack/mc-integration-test.sh
@@ -36,16 +36,4 @@ kubectl create namespace leader-ns
 # Here we simply create a cluster-admin user for member to access leader
 kubectl apply -f test/integration/cluster-admin.yml
 
-if [[ $NO_LOCAL == "true" ]];then
-  # Run go test in a Docker container
-  container_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' antrea-integration-control-plane)
-  sed -i "s|server: https://.*|server: https://${container_ip}:6443|" /tmp/mc-integration-kubeconfig
-  docker run --network kind --privileged --rm \
-	  -w /usr/src/antrea.io/antrea \
-    -v /tmp/mc-integration-kubeconfig:/tmp/mc-integration-kubeconfig \
-	  -v ${BASEDIR}/../.coverage:/usr/src/antrea.io/antrea/multicluster/.coverage \
-	  -v ${BASEDIR}/../..:/usr/src/antrea.io/antrea:ro \
-	  antrea/mc-test test-integration
-else
-    go test -coverpkg=antrea.io/antrea/multicluster/controllers/multicluster/... -coverprofile=../.coverage/coverage-integration.txt -covermode=atomic -cover antrea.io/antrea/multicluster/test/integration/...
-fi
+go test -coverpkg=antrea.io/antrea/multicluster/controllers/multicluster/... -coverprofile=.coverage/coverage-integration.txt -covermode=atomic -cover antrea.io/antrea/multicluster/test/integration/...
diff --git a/multicluster/test/integration/suite_test.go b/multicluster/test/integration/suite_test.go
index fed3a652d..60a76e41d 100644
--- a/multicluster/test/integration/suite_test.go
+++ b/multicluster/test/integration/suite_test.go
@@ -158,7 +158,9 @@ var _ = BeforeSuite(func() {
 		k8sManager.GetClient(),
 		k8sManager.GetScheme(),
 		"default",
-		clusterSetReconciler)
+		clusterSetReconciler,
+		multiclustercontrollers.MemberCluster,
+	)
 
 	go staleController.Run(stopCh)
 	// Make sure to trigger clean up process every 5 seconds
diff --git a/pkg/agent/controller/networkpolicy/reconciler_test.go b/pkg/agent/controller/networkpolicy/reconciler_test.go
index 0560113ba..9cfb32266 100644
--- a/pkg/agent/controller/networkpolicy/reconciler_test.go
+++ b/pkg/agent/controller/networkpolicy/reconciler_test.go
@@ -1256,10 +1256,10 @@ func TestReconcilerUpdate(t *testing.T) {
 				mockOFClient.EXPECT().UninstallPolicyRuleFlows(gomock.Any())
 			}
 			if len(tt.expectedAddedFrom) > 0 {
-				mockOFClient.EXPECT().AddPolicyRuleAddress(gomock.Any(), types.SrcAddress, gomock.InAnyOrder(tt.expectedAddedFrom), priority)
+				mockOFClient.EXPECT().AddPolicyRuleAddress(gomock.Any(), types.SrcAddress, gomock.InAnyOrder(tt.expectedAddedFrom), priority, false)
 			}
 			if len(tt.expectedAddedTo) > 0 {
-				mockOFClient.EXPECT().AddPolicyRuleAddress(gomock.Any(), types.DstAddress, gomock.InAnyOrder(tt.expectedAddedTo), priority)
+				mockOFClient.EXPECT().AddPolicyRuleAddress(gomock.Any(), types.DstAddress, gomock.InAnyOrder(tt.expectedAddedTo), priority, false)
 			}
 			if len(tt.expectedDeletedFrom) > 0 {
 				mockOFClient.EXPECT().DeletePolicyRuleAddress(gomock.Any(), types.SrcAddress, gomock.InAnyOrder(tt.expectedDeletedFrom), priority)
diff --git a/pkg/agent/openflow/client_test.go b/pkg/agent/openflow/client_test.go
index d698ab6fe..b3043a52b 100644
--- a/pkg/agent/openflow/client_test.go
+++ b/pkg/agent/openflow/client_test.go
@@ -437,7 +437,7 @@ func prepareTraceflowFlow(ctrl *gomock.Controller) *client {
 	c.bridge = ovsoftest.NewMockBridge(ctrl)
 
 	mFlow := ovsoftest.NewMockFlow(ctrl)
-	ctx := &conjMatchFlowContext{dropFlow: mFlow}
+	ctx := &conjMatchFlowContext{dropFlow: mFlow, dropFlowEnableLogging: false}
 	mFlow.EXPECT().FlowProtocol().Return(binding.Protocol("ip"))
 	mFlow.EXPECT().CopyToBuilder(priorityNormal+2, false).Return(EgressDefaultTable.ofTable.BuildFlow(priorityNormal + 2)).Times(1)
 	c.featureNetworkPolicy.globalConjMatchFlowCache["mockContext"] = ctx
diff --git a/pkg/agent/openflow/network_policy_test.go b/pkg/agent/openflow/network_policy_test.go
index 89eb37fea..614941811 100644
--- a/pkg/agent/openflow/network_policy_test.go
+++ b/pkg/agent/openflow/network_policy_test.go
@@ -105,7 +105,7 @@ func TestPolicyRuleConjunction(t *testing.T) {
 
 	var addedAddrs = parseAddresses([]string{"192.168.1.3", "192.168.1.30", "192.168.2.0/24", "103", "104"})
 	expectConjunctionsCount([]*expectConjunctionTimes{{5, ruleID1, clauseID, nClause}})
-	flowChanges1 := clause1.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs, nil)
+	flowChanges1 := clause1.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs, nil, false)
 	err := c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChanges1)
 	require.Nil(t, err, "Failed to invoke addAddrFlows")
 	checkFlowCount(t, len(addedAddrs))
@@ -130,7 +130,7 @@ func TestPolicyRuleConjunction(t *testing.T) {
 	var addedAddrs2 = parseAddresses([]string{"192.168.1.30", "192.168.1.50"})
 	expectConjunctionsCount([]*expectConjunctionTimes{{2, ruleID2, clauseID2, nClause}})
 	expectConjunctionsCount([]*expectConjunctionTimes{{1, ruleID1, clauseID, nClause}})
-	flowChanges3 := clause2.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs2, nil)
+	flowChanges3 := clause2.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs2, nil, false)
 	err = c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChanges3)
 	require.Nil(t, err, "Failed to invoke addAddrFlows")
 	testAddr := NewIPAddress(net.ParseIP("192.168.1.30"))
@@ -146,7 +146,7 @@ func TestPolicyRuleConjunction(t *testing.T) {
 	nClause3 := uint8(1)
 	clause3 := conj3.newClause(clauseID3, nClause3, mockEgressRuleTable, mockEgressDefaultTable)
 	var addedAddrs3 = parseAddresses([]string{"192.168.1.30"})
-	flowChanges4 := clause3.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs3, nil)
+	flowChanges4 := clause3.addAddrFlows(c.featureNetworkPolicy, types.SrcAddress, addedAddrs3, nil, false)
 	err = c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChanges4)
 	require.Nil(t, err, "Failed to invoke addAddrFlows")
 	checkConjMatchFlowActions(t, c, clause3, testAddr, types.SrcAddress, 2, 1)
@@ -643,7 +643,7 @@ func TestConjMatchFlowContextKeyConflict(t *testing.T) {
 		id: ruleID1,
 	}
 	clause1 := conj1.newClause(1, 3, mockEgressRuleTable, mockEgressDefaultTable)
-	flowChange1 := clause1.addAddrFlows(c.featureNetworkPolicy, types.DstAddress, parseAddresses([]string{ip.String()}), nil)
+	flowChange1 := clause1.addAddrFlows(c.featureNetworkPolicy, types.DstAddress, parseAddresses([]string{ip.String()}), nil, false)
 	err := c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChange1)
 	require.Nil(t, err, "no error expect in applyConjunctiveMatchFlows")
 
@@ -652,7 +652,7 @@ func TestConjMatchFlowContextKeyConflict(t *testing.T) {
 		id: ruleID2,
 	}
 	clause2 := conj2.newClause(1, 3, mockEgressRuleTable, mockEgressDefaultTable)
-	flowChange2 := clause2.addAddrFlows(c.featureNetworkPolicy, types.DstAddress, parseAddresses([]string{ipNet.String()}), nil)
+	flowChange2 := clause2.addAddrFlows(c.featureNetworkPolicy, types.DstAddress, parseAddresses([]string{ipNet.String()}), nil, false)
 	err = c.featureNetworkPolicy.applyConjunctiveMatchFlows(flowChange2)
 	require.Nil(t, err, "no error expect in applyConjunctiveMatchFlows")
 	expectedMatchKey := fmt.Sprintf("table:%d,priority:%s,matchPair:%s", EgressRuleTable.GetID(), strconv.Itoa(int(priorityNormal)), singleMatchPair.KeyString())
diff --git a/pkg/agent/openflow/testing/mock_openflow.go b/pkg/agent/openflow/testing/mock_openflow.go
index cfb9be05f..d3e7d0ea6 100644
--- a/pkg/agent/openflow/testing/mock_openflow.go
+++ b/pkg/agent/openflow/testing/mock_openflow.go
@@ -71,17 +71,17 @@ func (mr *MockClientMockRecorder) AddAddressToDNSConjunction(arg0, arg1 interfac
 }
 
 // AddPolicyRuleAddress mocks base method
-func (m *MockClient) AddPolicyRuleAddress(arg0 uint32, arg1 types.AddressType, arg2 []types.Address, arg3 *uint16) error {
+func (m *MockClient) AddPolicyRuleAddress(arg0 uint32, arg1 types.AddressType, arg2 []types.Address, arg3 *uint16, arg4 bool) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "AddPolicyRuleAddress", arg0, arg1, arg2, arg3)
+	ret := m.ctrl.Call(m, "AddPolicyRuleAddress", arg0, arg1, arg2, arg3, arg4)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // AddPolicyRuleAddress indicates an expected call of AddPolicyRuleAddress
-func (mr *MockClientMockRecorder) AddPolicyRuleAddress(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+func (mr *MockClientMockRecorder) AddPolicyRuleAddress(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPolicyRuleAddress", reflect.TypeOf((*MockClient)(nil).AddPolicyRuleAddress), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddPolicyRuleAddress", reflect.TypeOf((*MockClient)(nil).AddPolicyRuleAddress), arg0, arg1, arg2, arg3, arg4)
 }
 
 // BatchInstallPolicyRuleFlows mocks base method
diff --git a/pkg/controller/networkpolicy/networkpolicy_controller_test.go b/pkg/controller/networkpolicy/networkpolicy_controller_test.go
index b7b4f1ca1..76a85c8a0 100644
--- a/pkg/controller/networkpolicy/networkpolicy_controller_test.go
+++ b/pkg/controller/networkpolicy/networkpolicy_controller_test.go
@@ -158,6 +158,7 @@ func newControllerWithoutEventHandler(k8sObjects, crdObjects []runtime.Object) (
 	addressGroupStore := store.NewAddressGroupStore()
 	internalNetworkPolicyStore := store.NewNetworkPolicyStore()
 	internalGroupStore := store.NewGroupStore()
+	namespaceInformer := informerFactory.Core().V1().Namespaces()
 	networkPolicyInformer := informerFactory.Networking().V1().NetworkPolicies()
 	tierInformer := crdInformerFactory.Crd().V1alpha1().Tiers()
 	cnpInformer := crdInformerFactory.Crd().V1alpha1().ClusterNetworkPolicies()
@@ -167,6 +168,7 @@ func newControllerWithoutEventHandler(k8sObjects, crdObjects []runtime.Object) (
 	npController := &NetworkPolicyController{
 		kubeClient:                 client,
 		crdClient:                  crdClient,
+		namespaceLister:            namespaceInformer.Lister(),
 		networkPolicyInformer:      networkPolicyInformer,
 		networkPolicyLister:        networkPolicyInformer.Lister(),
 		networkPolicyListerSynced:  networkPolicyInformer.Informer().HasSynced,
@@ -2532,11 +2534,12 @@ func TestProcessNetworkPolicy(t *testing.T) {
 					UID:       "uidA",
 				},
 				Rules: []controlplane.NetworkPolicyRule{{
-					Direction: controlplane.DirectionIn,
-					From:      matchAllPeer,
-					Services:  nil,
-					Priority:  defaultRulePriority,
-					Action:    &defaultAction,
+					Direction:     controlplane.DirectionIn,
+					From:          matchAllPeer,
+					Services:      nil,
+					Priority:      defaultRulePriority,
+					Action:        &defaultAction,
+					EnableLogging: false,
 				}},
 				AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("nsA", &metav1.LabelSelector{}, nil, nil, nil).NormalizedName)},
 			},
@@ -2626,8 +2629,9 @@ func TestProcessNetworkPolicy(t *testing.T) {
 								Port:     &int80,
 							},
 						},
-						Priority: defaultRulePriority,
-						Action:   &defaultAction,
+						Priority:      defaultRulePriority,
+						Action:        &defaultAction,
+						EnableLogging: false,
 					},
 					{
 						Direction: controlplane.DirectionOut,
@@ -2640,8 +2644,9 @@ func TestProcessNetworkPolicy(t *testing.T) {
 								Port:     &int81,
 							},
 						},
-						Priority: defaultRulePriority,
-						Action:   &defaultAction,
+						Priority:      defaultRulePriority,
+						Action:        &defaultAction,
+						EnableLogging: false,
 					},
 				},
 				AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("nsA", &selectorA, nil, nil, nil).NormalizedName)},
@@ -2704,8 +2709,9 @@ func TestProcessNetworkPolicy(t *testing.T) {
 								Port:     &int80,
 							},
 						},
-						Priority: defaultRulePriority,
-						Action:   &defaultAction,
+						Priority:      defaultRulePriority,
+						Action:        &defaultAction,
+						EnableLogging: false,
 					},
 					{
 						Direction: controlplane.DirectionIn,
@@ -2718,8 +2724,9 @@ func TestProcessNetworkPolicy(t *testing.T) {
 								Port:     &int81,
 							},
 						},
-						Priority: defaultRulePriority,
-						Action:   &defaultAction,
+						Priority:      defaultRulePriority,
+						Action:        &defaultAction,
+						EnableLogging: false,
 					},
 				},
 				AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("nsA", &selectorA, nil, nil, nil).NormalizedName)},
@@ -2747,6 +2754,69 @@ func TestProcessNetworkPolicy(t *testing.T) {
 	}
 }
 
+func TestProcessNetworkPolicyLogging(t *testing.T) {
+	tests := []struct {
+		name                    string
+		inputPolicy             *networkingv1.NetworkPolicy
+		expectedPolicy          *antreatypes.NetworkPolicy
+		expectedAppliedToGroups int
+		expectedAddressGroups   int
+	}{
+		{
+			name: "default-allow-ingress",
+			inputPolicy: &networkingv1.NetworkPolicy{
+				ObjectMeta: metav1.ObjectMeta{Namespace: "nsA", Name: "npA", UID: "uidA"},
+				Spec: networkingv1.NetworkPolicySpec{
+					PodSelector: metav1.LabelSelector{},
+					PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress},
+					Ingress:     []networkingv1.NetworkPolicyIngressRule{{}},
+				},
+			},
+			expectedPolicy: &antreatypes.NetworkPolicy{
+				UID:  "uidA",
+				Name: "uidA",
+				SourceRef: &controlplane.NetworkPolicyReference{
+					Type:      controlplane.K8sNetworkPolicy,
+					Namespace: "nsA",
+					Name:      "npA",
+					UID:       "uidA",
+				},
+				Rules: []controlplane.NetworkPolicyRule{{
+					Direction:     controlplane.DirectionIn,
+					From:          matchAllPeer,
+					Services:      nil,
+					Priority:      defaultRulePriority,
+					Action:        &defaultAction,
+					EnableLogging: true,
+				}},
+				AppliedToGroups: []string{getNormalizedUID(antreatypes.NewGroupSelector("nsA", &metav1.LabelSelector{}, nil, nil, nil).NormalizedName)},
+			},
+			expectedAppliedToGroups: 1,
+			expectedAddressGroups:   0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, c := newController()
+			nsA := corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:        "nsA",
+					Annotations: map[string]string{"networkpolicy.antrea.io/enable-logging": "true"},
+				},
+			}
+			c.namespaceStore.Add(&nsA)
+
+			if actualPolicy := c.processNetworkPolicy(tt.inputPolicy); !reflect.DeepEqual(actualPolicy, tt.expectedPolicy) {
+				t.Errorf("processNetworkPolicy() got %v, want %v", actualPolicy, tt.expectedPolicy)
+			}
+
+			if actualAppliedToGroups := len(c.appliedToGroupStore.List()); actualAppliedToGroups != tt.expectedAppliedToGroups {
+				t.Errorf("len(appliedToGroupStore.List()) got %v, want %v", actualAppliedToGroups, tt.expectedAppliedToGroups)
+			}
+		})
+	}
+}
+
 func TestPodToGroupMember(t *testing.T) {
 	namedPod := getPod("", "", "", "", true)
 	unNamedPod := getPod("", "", "", "", false)
diff --git a/test/e2e/antreapolicy_test.go b/test/e2e/antreapolicy_test.go
index 9667d10af..36e548a3f 100644
--- a/test/e2e/antreapolicy_test.go
+++ b/test/e2e/antreapolicy_test.go
@@ -40,6 +40,7 @@ import (
 	crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1"
 	crdv1alpha2 "antrea.io/antrea/pkg/apis/crd/v1alpha2"
 	crdv1alpha3 "antrea.io/antrea/pkg/apis/crd/v1alpha3"
+	"antrea.io/antrea/pkg/controller/networkpolicy"
 	"antrea.io/antrea/pkg/features"
 	. "antrea.io/antrea/test/e2e/utils"
 )
@@ -1946,7 +1947,7 @@ func testANPMultipleAppliedTo(t *testing.T, data *TestData, singleRule bool) {
 	failOnError(k8sUtils.DeleteANP(builder.Namespace, builder.Name), t)
 }
 
-// testAuditLoggingBasic tests that a audit log is generated when egress drop applied
+// testAuditLoggingBasic tests that audit logs are generated when egress drop applied
 func testAuditLoggingBasic(t *testing.T, data *TestData) {
 	builder := &ClusterNetworkPolicySpecBuilder{}
 	builder = builder.SetName("test-log-acnp-deny").
@@ -2034,6 +2035,99 @@ func testAuditLoggingBasic(t *testing.T, data *TestData) {
 	failOnError(k8sUtils.CleanACNPs(), t)
 }
 
+// testAuditLoggingEnableNP tests that audit logs are generated when K8s NP is applied
+// tests both Allow traffic by K8s NP and Drop traffic by implicit K8s policy drop
+func testAuditLoggingEnableNP(t *testing.T, data *TestData) {
+	data.updateNamespaceWithAnnotations(namespaces["x"], map[string]string{networkpolicy.EnableNPLoggingAnnotationKey: "true"})
+	// Add a K8s namespaced NetworkPolicy in ns x that allow ingress traffic from
+	// Pod x/b to x/a which default denies other ingress including from Pod x/c to x/a
+	k8sNPBuilder := &NetworkPolicySpecBuilder{}
+	k8sNPBuilder = k8sNPBuilder.SetName(namespaces["x"], "allow-x-b-to-x-a").
+		SetPodSelector(map[string]string{"pod": "a"}).
+		SetTypeIngress().
+		AddIngress(v1.ProtocolTCP, &p80, nil, nil, nil,
+			map[string]string{"pod": "b"}, nil, nil, nil)
+
+	knp, err := k8sUtils.CreateOrUpdateNetworkPolicy(k8sNPBuilder.Get())
+	failOnError(err, t)
+	failOnError(waitForResourceReady(t, timeout, knp), t)
+
+	// generate some traffic that will be dropped by implicit K8s policy drop
+	var wg sync.WaitGroup
+	oneProbe := func(ns1, pod1, ns2, pod2 string) {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			k8sUtils.Probe(ns1, pod1, ns2, pod2, p80, ProtocolTCP)
+		}()
+	}
+	oneProbe(namespaces["x"], "b", namespaces["x"], "a")
+	oneProbe(namespaces["x"], "c", namespaces["x"], "a")
+	wg.Wait()
+
+	podXA, err := k8sUtils.GetPodByLabel(namespaces["x"], "a")
+	if err != nil {
+		t.Errorf("Failed to get Pod in Namespace x with label 'pod=a': %v", err)
+	}
+	// nodeName is guaranteed to be set at this stage, since the framework waits for all Pods to be in Running phase
+	nodeName := podXA.Spec.NodeName
+	antreaPodName, err := data.getAntreaPodOnNode(nodeName)
+	if err != nil {
+		t.Errorf("Error occurred when trying to get the Antrea Agent Pod running on Node %s: %v", nodeName, err)
+	}
+	cmd := []string{"cat", logDir + logfileName}
+
+	if err := wait.Poll(1*time.Second, 10*time.Second, func() (bool, error) {
+		stdout, stderr, err := data.RunCommandFromPod(antreaNamespace, antreaPodName, "antrea-agent", cmd)
+		if err != nil || stderr != "" {
+			// file may not exist yet
+			t.Logf("Error when printing the audit log file, err: %v, stderr: %v", err, stderr)
+			return false, nil
+		}
+		if !strings.Contains(stdout, "K8sNetworkPolicy") {
+			t.Logf("Audit log file does not contain entries for 'test-log-acnp-deny' yet")
+			return false, nil
+		}
+
+		var expectedNumEntries, actualNumEntries int
+		srcPods := []string{namespaces["x"] + "/b", namespaces["x"] + "/c"}
+		expectedLogPrefix := []string{"allow-x-b-to-x-a Allow [0-9]+ ", "K8sNetworkPolicy Drop -1 "}
+		destIPs, _ := podIPs[namespaces["x"]+"/a"]
+		for i := 0; i < len(srcPods); i++ {
+			srcIPs, _ := podIPs[srcPods[i]]
+			for _, srcIP := range srcIPs {
+				for _, destIP := range destIPs {
+					// only look for an entry in the audit log file if srcIP and
+					// dstIP are of the same family
+					if strings.Contains(srcIP, ".") != strings.Contains(destIP, ".") {
+						continue
+					}
+					expectedNumEntries += 1
+					// The audit log should contain log entry `... Drop <ofPriority> <x/a IP> <z/* IP> ...`
+					re := regexp.MustCompile(expectedLogPrefix[i] + srcIP + ` [0-9]+ ` + destIP + ` ` + strconv.Itoa(int(p80)))
+					if re.MatchString(stdout) {
+						actualNumEntries += 1
+					} else {
+						t.Logf("Audit log does not contain expected entry from %s (%s) to x/a (%s)", srcPods[i], srcIP, destIP)
+					}
+					break
+				}
+			}
+		}
+		if actualNumEntries != expectedNumEntries {
+			t.Logf("Missing entries in audit log with K8s NP: expected %d but found %d", expectedNumEntries, actualNumEntries)
+			return false, nil
+		}
+		return true, nil
+	}); err != nil {
+		t.Errorf("Error when polling audit log files for required entries: %v", err)
+	}
+	failOnError(k8sUtils.DeleteNetworkPolicy(namespaces["x"], "allow-x-b-to-x-a"), t)
+	data.UpdateNamespace(namespaces["x"], func(namespace *v1.Namespace) {
+		delete(namespace.Annotations, networkpolicy.EnableNPLoggingAnnotationKey)
+	})
+}
+
 func testAppliedToPerRule(t *testing.T) {
 	builder := &AntreaNetworkPolicySpecBuilder{}
 	builder = builder.SetName(namespaces["y"], "np1").SetPriority(1.0)
@@ -3556,6 +3650,7 @@ func TestAntreaPolicy(t *testing.T) {
 
 	t.Run("TestGroupAuditLogging", func(t *testing.T) {
 		t.Run("Case=AuditLoggingBasic", func(t *testing.T) { testAuditLoggingBasic(t, data) })
+		t.Run("Case=AuditLoggingEnableNP", func(t *testing.T) { testAuditLoggingEnableNP(t, data) })
 	})
 
 	t.Run("TestMulticastNP", func(t *testing.T) {
diff --git a/test/e2e/framework.go b/test/e2e/framework.go
index 40d8ee35c..e6a3f8627 100644
--- a/test/e2e/framework.go
+++ b/test/e2e/framework.go
@@ -600,11 +600,11 @@ func (data *TestData) CreateNamespace(namespace string, mutateFunc func(*corev1.
 		mutateFunc(ns)
 	}
 	if ns, err := data.clientset.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}); err != nil {
-		// Ignore error if the namespace already exists
+		// Ignore error if the Namespace already exists
 		if !errors.IsAlreadyExists(err) {
 			return fmt.Errorf("error when creating '%s' Namespace: %v", namespace, err)
 		}
-		// When namespace already exists, check phase
+		// When Namespace already exists, check phase
 		if ns.Status.Phase == corev1.NamespaceTerminating {
 			return fmt.Errorf("error when creating '%s' Namespace: namespace exists but is in 'Terminating' phase", namespace)
 		}
@@ -612,8 +612,35 @@ func (data *TestData) CreateNamespace(namespace string, mutateFunc func(*corev1.
 	return nil
 }
 
-// createNamespaceWithAnnotations creates the namespace with Annotations.
+func (data *TestData) UpdateNamespace(namespace string, mutateFunc func(*corev1.Namespace)) error {
+	ns, _ := data.clientset.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
+	if mutateFunc != nil {
+		mutateFunc(ns)
+	}
+	if ns, err := data.clientset.CoreV1().Namespaces().Update(context.TODO(), ns, metav1.UpdateOptions{}); err != nil {
+		// Check Namespace phase
+		if ns.Status.Phase == corev1.NamespaceTerminating {
+			return fmt.Errorf("error when updating '%s' Namespace: namespace is in 'Terminating' phase", namespace)
+		}
+		return fmt.Errorf("error when updating '%s' Namespace: %v", namespace, err)
+	}
+	return nil
+}
+
+// createNamespaceWithAnnotations creates the Namespace with Annotations.
 func (data *TestData) createNamespaceWithAnnotations(namespace string, annotations map[string]string) error {
+	mutateFunc := data.generateNamespaceAnnotationsMutateFunc(annotations)
+	return data.CreateNamespace(namespace, mutateFunc)
+}
+
+// updateNamespaceWithAnnotations updates the given Namespace with Annotations.
+func (data *TestData) updateNamespaceWithAnnotations(namespace string, annotations map[string]string) error {
+	mutateFunc := data.generateNamespaceAnnotationsMutateFunc(annotations)
+	return data.UpdateNamespace(namespace, mutateFunc)
+}
+
+// generateAnnotationsMutateFunc generates a mutate function to add given Annotations to a Namespace.
+func (data *TestData) generateNamespaceAnnotationsMutateFunc(annotations map[string]string) func(*corev1.Namespace) {
 	var mutateFunc func(*corev1.Namespace)
 	if annotations != nil {
 		mutateFunc = func(namespace *corev1.Namespace) {
@@ -625,7 +652,7 @@ func (data *TestData) createNamespaceWithAnnotations(namespace string, annotatio
 			}
 		}
 	}
-	return data.CreateNamespace(namespace, mutateFunc)
+	return mutateFunc
 }
 
 // DeleteNamespace deletes the provided Namespace, and waits for deletion to actually complete if timeout>=0
diff --git a/test/integration/agent/openflow_test.go b/test/integration/agent/openflow_test.go
index 12cbd83be..34d2536f2 100644
--- a/test/integration/agent/openflow_test.go
+++ b/test/integration/agent/openflow_test.go
@@ -313,10 +313,10 @@ func TestReplayFlowsNetworkPolicyFlows(t *testing.T) {
 	err = c.InstallPolicyRuleFlows(rule)
 	require.Nil(t, err, "Failed to InstallPolicyRuleFlows")
 
-	err = c.AddPolicyRuleAddress(ruleID, types.SrcAddress, prepareIPNetAddresses([]string{"192.168.5.0/24", "192.169.1.0/24"}), nil)
+	err = c.AddPolicyRuleAddress(ruleID, types.SrcAddress, prepareIPNetAddresses([]string{"192.168.5.0/24", "192.169.1.0/24"}), nil, false)
 	require.Nil(t, err, "Failed to AddPolicyRuleAddress")
 	ofport := int32(100)
-	err = c.AddPolicyRuleAddress(ruleID, types.DstAddress, []types.Address{ofClient.NewOFPortAddress(ofport)}, nil)
+	err = c.AddPolicyRuleAddress(ruleID, types.DstAddress, []types.Address{ofClient.NewOFPortAddress(ofport)}, nil, false)
 	require.Nil(t, err, "Failed to AddPolicyRuleAddress")
 
 	testReplayFlows(t)
@@ -496,7 +496,7 @@ func TestNetworkPolicyFlows(t *testing.T) {
 	checkDeleteAddress(t, ingressRuleTable, priorityNormal, ruleID, addedFrom, types.SrcAddress)
 
 	ofport := int32(100)
-	err = c.AddPolicyRuleAddress(ruleID, types.DstAddress, []types.Address{ofClient.NewOFPortAddress(ofport)}, nil)
+	err = c.AddPolicyRuleAddress(ruleID, types.DstAddress, []types.Address{ofClient.NewOFPortAddress(ofport)}, nil, false)
 	require.Nil(t, err, "Failed to AddPolicyRuleAddress")
 
 	// Dump flows.
@@ -795,7 +795,7 @@ func checkDefaultDropFlows(t *testing.T, table string, priority int, addrType ty
 }
 
 func checkAddAddress(t *testing.T, ruleTable string, priority int, ruleID uint32, addedAddress []types.Address, addrType types.AddressType) {
-	err := c.AddPolicyRuleAddress(ruleID, addrType, addedAddress, nil)
+	err := c.AddPolicyRuleAddress(ruleID, addrType, addedAddress, nil, false)
 	require.Nil(t, err, "Failed to AddPolicyRuleAddress")
 
 	// dump flows