You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hudi.apache.org by si...@apache.org on 2026/02/12 04:51:56 UTC

(hudi) branch branch-0.x updated: [HUDI-9159] S3 implementation of StorageLock for StorageBasedLockProvider (#17955)

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

sivabalan pushed a commit to branch branch-0.x
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/branch-0.x by this push:
     new abe727df66ce [HUDI-9159] S3 implementation of StorageLock for StorageBasedLockProvider (#17955)
abe727df66ce is described below

commit abe727df66ce500df37f0b71aafd65e8b9ee71f1
Author: Lokesh Jain <lj...@apache.org>
AuthorDate: Thu Feb 12 10:21:50 2026 +0530

    [HUDI-9159] S3 implementation of StorageLock for StorageBasedLockProvider (#17955)
    
    
    ---------
    
    Co-authored-by: Alex R <ar...@gmail.com>
    Co-authored-by: Y Ethan Guo <et...@gmail.com>
    Co-authored-by: sivabalan <n....@gmail.com>
    Co-authored-by: Lokesh Jain <lj...@192.168.1.11>
    Co-authored-by: Jon Vexler <jb...@gmail.com>
    Co-authored-by: Lokesh Jain <lj...@Lokeshs-MacBook-Pro.local>
    Co-authored-by: Lokesh Jain <lj...@192.168.0.234>
    Co-authored-by: Danny Chan <yu...@gmail.com>
    Co-authored-by: Pavithran Ravichandiran <pa...@gmail.com>
---
 hudi-aws/pom.xml                                   |  24 +
 .../hudi/aws/sync/AWSGlueCatalogSyncClient.java    |  22 +-
 .../aws/transaction/lock/S3StorageLockClient.java  | 330 +++++++
 .../lock/TestS3StorageBasedLockProvider.java       | 160 ++++
 .../transaction/lock/TestS3StorageLockClient.java  | 336 ++++++++
 .../TestS3StorageLockClientAuditOperations.java    | 272 ++++++
 .../hudi/cli/commands/LockAuditingCommand.java     | 663 +++++++++++++++
 .../hudi/cli/commands/TestLockAuditingCommand.java | 943 +++++++++++++++++++++
 .../apache/hudi/async/AsyncClusteringService.java  |   4 +-
 .../org/apache/hudi/async/HoodieAsyncService.java  |   2 +-
 .../http/HoodieWriteCommitHttpCallbackClient.java  |   2 +-
 .../hudi/client/BaseHoodieTableServiceClient.java  |  10 +-
 .../apache/hudi/client/BaseHoodieWriteClient.java  |   2 +-
 .../org/apache/hudi/client/RunsTableService.java   |   2 +-
 .../client/heartbeat/HoodieHeartbeatClient.java    |   8 +-
 .../lock/FileSystemBasedLockProvider.java          |   5 +
 .../transaction/lock/InProcessLockProvider.java    |   2 +-
 .../hudi/client/transaction/lock/LockManager.java  |  17 +-
 .../transaction/lock/StorageBasedLockProvider.java | 178 +++-
 .../client/transaction/lock/StorageLockClient.java |  75 +-
 .../lock/audit/AuditOperationState.java}           |  37 +-
 .../transaction/lock/audit/AuditService.java       |  41 +
 .../lock/audit/AuditServiceFactory.java            | 119 +++
 .../audit/StorageLockProviderAuditService.java     | 176 ++++
 .../lock/metrics/HoodieLockMetrics.java            | 108 ++-
 .../lock/models/LockProviderHeartbeatManager.java  |  10 +-
 .../apache/hudi/client/utils/ArchivalUtils.java    |   2 +-
 .../org/apache/hudi/config/HoodieIndexConfig.java  |   4 +-
 .../apache/hudi/config/StorageBasedLockConfig.java |  17 +-
 .../hudi/execution/CopyOnWriteInsertHandler.java   |   2 +-
 .../apache/hudi/index/bloom/HoodieBloomIndex.java  |   3 +-
 .../index/bucket/ConsistentBucketIndexUtils.java   |   2 +-
 .../metadata/HoodieBackedTableMetadataWriter.java  |   8 +-
 .../table/action/clean/CleanActionExecutor.java    |   4 +-
 .../hudi/table/action/clean/CleanPlanner.java      |   2 +-
 .../cluster/ClusteringPlanActionExecutor.java      |  12 +-
 .../cluster/strategy/ClusteringPlanStrategy.java   |   8 +-
 .../PartitionAwareClusteringPlanStrategy.java      |   2 +-
 .../compact/ScheduleCompactionActionExecutor.java  |   2 +-
 .../BaseHoodieCompactionPlanGenerator.java         |   6 +-
 .../action/index/AbstractIndexingCatchupTask.java  |   2 +-
 .../table/action/index/RunIndexActionExecutor.java |   4 +-
 .../action/restore/BaseRestoreActionExecutor.java  |   2 +-
 .../rollback/BaseRollbackActionExecutor.java       |   6 +-
 .../table/action/rollback/BaseRollbackHelper.java  |   2 +-
 .../rollback/BaseRollbackPlanActionExecutor.java   |   2 +-
 .../hudi/table/marker/DirectWriteMarkers.java      |   2 +-
 .../table/marker/MarkerBasedRollbackUtils.java     |   4 +-
 .../SimpleDirectMarkerBasedDetectionStrategy.java  |   2 +-
 ...nsactionDirectMarkerBasedDetectionStrategy.java |   2 +-
 .../org/apache/hudi/table/marker/WriteMarkers.java |   2 +-
 .../client/http/TestCallbackHttpClient.java        |   2 +-
 .../client/transaction/TestHoodieLockMetrics.java  | 361 ++++++++
 .../hudi/client/transaction/TestLockManager.java   |  47 +
 .../lock/StorageBasedLockProviderTestBase.java     | 290 +++++++
 .../lock/TestStorageBasedLockProvider.java         | 301 ++++++-
 .../lock/audit/TestAuditServiceFactory.java        | 160 ++++
 .../audit/TestStorageLockProviderAuditService.java | 379 +++++++++
 .../lock/models/StorageLockClientFileTest.java     |  58 ++
 .../models/TestLockProviderHeartbeatManager.java   |   8 +-
 .../hudi/config/TestStorageBasedLockConfig.java    |  14 +-
 .../metrics/datadog/TestDatadogHttpClient.java     |   2 +-
 ...inkSizeBasedClusteringPlanStrategyRecently.java |   4 +-
 .../java/org/apache/hudi/io/FlinkAppendHandle.java |   2 +-
 .../java/org/apache/hudi/io/FlinkCreateHandle.java |   8 +-
 .../java/org/apache/hudi/io/FlinkMergeHandle.java  |   4 +-
 .../hudi/client/utils/SparkValidatorUtils.java     |   3 +-
 .../index/hfile/HFileBootstrapIndexReader.java     |   2 +-
 .../DirectMarkerBasedDetectionStrategy.java        |   2 +-
 .../hudi/common/fs/FailSafeConsistencyGuard.java   |   2 +-
 .../common/heartbeat/HoodieHeartbeatUtils.java     |   2 +-
 .../hudi/common/model/HoodiePartitionMetadata.java |   4 +-
 .../hudi/common/table/timeline/TimelineUtils.java  |   5 +-
 .../org/apache/hudi/common/util/ConfigUtils.java   |  13 +-
 .../org/apache/hudi/common/util/MarkerUtils.java   |   5 +-
 .../common/util/collection/LazyFileIterable.java   |   2 +-
 .../apache/hudi/metadata/BaseTableMetadata.java    |   5 +-
 .../hudi/metadata/HoodieTableMetadataUtil.java     |   5 +-
 .../main/java/org/apache/hudi/metrics/Metrics.java |   2 +-
 .../hudi/metrics/MetricsReporterFactory.java       |   4 +-
 .../hudi/metrics/datadog/DatadogHttpClient.java    |   2 +-
 .../metrics/prometheus/PushGatewayReporter.java    |   2 +-
 .../apache/hudi/common/util/TestHoodieTimer.java   |  41 +
 .../hudi/metrics/FlinkClusteringMetrics.java       |   2 +-
 .../hudi/metrics/FlinkCompactionMetrics.java       |   2 +-
 .../hudi/metrics/FlinkStreamReadMetrics.java       |   4 +-
 .../org/apache/hudi/metrics/FlinkWriteMetrics.java |   2 +-
 .../apache/hudi/metrics/HoodieFlinkMetrics.java    |   4 +-
 .../java/org/apache/hudi/sink/CleanFunction.java   |   2 +-
 .../hudi/sink/StreamWriteOperatorCoordinator.java  |   4 +-
 .../hudi/sink/bootstrap/BootstrapOperator.java     |   2 +-
 .../clustering/ClusteringPlanSourceFunction.java   |   2 +-
 .../sink/compact/CompactionPlanSourceFunction.java |   4 +-
 .../apache/hudi/source/ExpressionEvaluators.java   |   2 +-
 .../apache/hudi/source/ExpressionPredicates.java   |   8 +-
 .../java/org/apache/hudi/source/FileIndex.java     |   2 +-
 .../apache/hudi/source/IncrementalInputSplits.java |  16 +-
 .../hudi/source/StreamReadMonitoringFunction.java  |   1 +
 .../org/apache/hudi/table/HoodieTableFactory.java  |   2 +-
 .../org/apache/hudi/table/HoodieTableSource.java   |   6 +-
 .../apache/hudi/table/catalog/HoodieCatalog.java   |   2 +-
 .../hudi/table/catalog/HoodieHiveCatalog.java      |   2 +-
 .../table/format/cow/CopyOnWriteInputFormat.java   |   6 +-
 .../main/java/org/apache/hudi/util/ClientIds.java  |   2 +-
 .../apache/hudi/util/ImplicitTypeConverter.java    |   2 +-
 .../java/org/apache/hudi/util/StreamerUtil.java    |   2 +-
 .../gcp/transaction/lock/GCSStorageLockClient.java | 111 ++-
 .../TestGCSStorageLockClientAuditOperations.java   | 273 ++++++
 .../common/table/log/HoodieLogFormatWriter.java    |   2 +-
 .../apache/hudi/common/util/HadoopConfigUtils.java |   6 +-
 .../apache/hudi/hadoop/SchemaEvolutionContext.java |   2 +-
 .../realtime/AbstractRealtimeRecordReader.java     |   2 +-
 .../org/apache/hudi/common/util/FileIOUtils.java   |   8 +-
 .../org/apache/hudi/common/util/HoodieTimer.java   |  11 +
 .../org/apache/hudi/common/util/RetryHelper.java   |   2 +-
 .../connect/kafka/KafkaConnectControlAgent.java    |   6 +-
 .../transaction/ConnectTransactionCoordinator.java |  14 +-
 .../transaction/ConnectTransactionParticipant.java |  43 +-
 .../main/java/org/apache/hudi/DataSourceUtils.java |   2 +-
 .../scala/org/apache/hudi/HoodieFileIndex.scala    |   2 +-
 .../org/apache/hudi/HoodieSparkSqlWriter.scala     |   9 +-
 .../org/apache/hudi/HoodieStreamingSink.scala      |   8 +-
 .../scala/org/apache/hudi/cdc/HoodieCDCRDD.scala   |   2 +-
 .../apache/hudi/cli/BootstrapExecutorUtils.java    |   4 +-
 .../procedures/CleanupAuditLockProcedure.scala     | 191 +++++
 .../procedures/CreateSavepointProcedure.scala      |   2 +-
 .../command/procedures/DeleteMarkerProcedure.scala |   2 +-
 .../procedures/DeleteSavepointProcedure.scala      |   2 +-
 .../hudi/command/procedures/HoodieProcedures.scala |   4 +
 .../procedures/RollbackToSavepointProcedure.scala  |   4 +-
 .../command/procedures/SetAuditLockProcedure.scala | 188 ++++
 .../procedures/ShowAuditLockStatusProcedure.scala  | 169 ++++
 .../procedures/UpgradeOrDowngradeProcedure.scala   |   2 +-
 .../procedures/ValidateAuditLockProcedure.scala    | 315 +++++++
 .../procedure/TestCleanupAuditLockProcedure.scala  | 296 +++++++
 .../hudi/procedure/TestSetAuditLockProcedure.scala | 169 ++++
 .../TestShowAuditLockStatusProcedure.scala         | 223 +++++
 .../procedure/TestValidateAuditLockProcedure.scala | 454 ++++++++++
 .../java/org/apache/hudi/hive/HiveSyncTool.java    |   2 +-
 .../org/apache/hudi/hive/HoodieHiveSyncClient.java |   2 +-
 .../org/apache/hudi/hive/ddl/JDBCExecutor.java     |   4 +-
 .../hudi/hive/replication/GlobalHiveSyncTool.java  |   4 +-
 .../org/apache/hudi/hive/util/HiveSchemaUtil.java  |   2 +-
 .../hudi/timeline/service/RequestHandler.java      |   4 +-
 .../timeline/service/handlers/MarkerHandler.java   |   4 +-
 .../MarkerBasedEarlyConflictDetectionRunnable.java |   4 +-
 .../service/handlers/marker/MarkerDirState.java    |   4 +-
 .../hudi/utilities/HiveIncrementalPuller.java      |  11 +-
 .../org/apache/hudi/utilities/HoodieCompactor.java |   4 +-
 .../hudi/utilities/HoodieDataTableValidator.java   |   2 +-
 .../utilities/HoodieMetadataTableValidator.java    |   4 +-
 .../apache/hudi/utilities/HoodieRepairTool.java    |  11 +-
 .../org/apache/hudi/utilities/TableSizeStats.java  |   6 +-
 .../hudi/utilities/perf/TimelineServerPerf.java    |   2 +-
 .../DeleteSupportSchemaPostProcessor.java          |   2 +-
 .../DropColumnSchemaPostProcessor.java             |   4 +-
 .../sources/S3EventsHoodieIncrSource.java          |   4 +-
 .../helpers/CloudObjectsSelectorCommon.java        |   2 +-
 .../hudi/utilities/streamer/BootstrapExecutor.java |   2 +-
 .../hudi/utilities/streamer/HoodieStreamer.java    |  10 +-
 .../utilities/streamer/SchedulerConfGenerator.java |   2 +-
 .../apache/hudi/utilities/streamer/StreamSync.java |   2 +-
 pom.xml                                            |  15 +-
 163 files changed, 7709 insertions(+), 406 deletions(-)

diff --git a/hudi-aws/pom.xml b/hudi-aws/pom.xml
index 7dc22525a8af..9454c2951718 100644
--- a/hudi-aws/pom.xml
+++ b/hudi-aws/pom.xml
@@ -174,6 +174,13 @@
             <version>${aws.sdk.version}</version>
         </dependency>
 
+        <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <version>${aws.sdk.version}</version>
+        </dependency>
+
         <!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-sqs -->
         <dependency>
             <groupId>software.amazon.awssdk</groupId>
@@ -199,6 +206,23 @@
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.hudi</groupId>
+            <artifactId>hudi-client-common</artifactId>
+            <version>${project.version}</version>
+            <classifier>tests</classifier>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>localstack</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java b/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
index 681607117c65..53389f3e463f 100644
--- a/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
+++ b/hudi-aws/src/main/java/org/apache/hudi/aws/sync/AWSGlueCatalogSyncClient.java
@@ -315,7 +315,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
         if (response.errors().stream()
             .allMatch(
                 (error) -> "AlreadyExistsException".equals(error.errorDetail().errorCode()))) {
-          LOG.warn("Partitions already exist in glue: " + response.errors());
+          LOG.info("Partitions already exist in glue: {}", response.errors());
         } else {
           throw new HoodieGlueSyncException("Fail to add partitions to " + tableId(databaseName, table.name())
               + " with error(s): " + response.errors());
@@ -593,14 +593,14 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
     if (!config.getBooleanOrDefault(META_SYNC_PARTITION_INDEX_FIELDS_ENABLE)) {
       // deactivate indexing if enabled
       if (getPartitionIndexEnable(tableName)) {
-        LOG.warn("Deactivating partition indexing");
+        LOG.info("Deactivating partition indexing");
         updatePartitionIndexEnable(tableName, false);
       }
       // also drop all existing indexes
       GetPartitionIndexesRequest indexesRequest = GetPartitionIndexesRequest.builder().databaseName(databaseName).tableName(tableName).build();
       GetPartitionIndexesResponse existingIdxsResp = awsGlue.getPartitionIndexes(indexesRequest).get();
       for (PartitionIndexDescriptor idsToDelete : existingIdxsResp.partitionIndexDescriptorList()) {
-        LOG.warn("Dropping partition index: " + idsToDelete.indexName());
+        LOG.info("Dropping partition index: {}", idsToDelete.indexName());
         DeletePartitionIndexRequest idxToDelete = DeletePartitionIndexRequest.builder()
                 .databaseName(databaseName).tableName(tableName).indexName(idsToDelete.indexName()).build();
         awsGlue.deletePartitionIndex(idxToDelete).get();
@@ -608,7 +608,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
     } else {
       // activate indexing usage if disabled
       if (!getPartitionIndexEnable(tableName)) {
-        LOG.warn("Activating partition indexing");
+        LOG.info("Activating partition indexing");
         updatePartitionIndexEnable(tableName, true);
       }
 
@@ -633,7 +633,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
           indexesChanges = true;
           DeletePartitionIndexRequest idxToDelete = DeletePartitionIndexRequest.builder()
                   .databaseName(databaseName).tableName(tableName).indexName(existingIdx.indexName()).build();
-          LOG.warn("Dropping irrelevant index: " + existingIdx.indexName());
+          LOG.info("Dropping irrelevant index: {}", existingIdx.indexName());
           awsGlue.deletePartitionIndex(idxToDelete).get();
         }
       }
@@ -655,7 +655,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
           PartitionIndex newIdx = PartitionIndex.builder()
                   .indexName(newIdxName)
                   .keys(neededIdx).build();
-          LOG.warn("Creating new partition index: " + newIdxName);
+          LOG.info("Creating new partition index: {}", newIdxName);
           CreatePartitionIndexRequest creationRequest = CreatePartitionIndexRequest.builder()
                   .databaseName(databaseName).tableName(tableName).partitionIndex(newIdx).build();
           awsGlue.createPartitionIndex(creationRequest).get();
@@ -671,7 +671,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
                                        .map(idx -> Arrays.stream(idx.split(";"))
                                                          .collect(Collectors.toList())).collect(Collectors.toList());
     if (indexes.size() > PARTITION_INDEX_MAX_NUMBER) {
-      LOG.warn(String.format("Only considering first %s indexes", PARTITION_INDEX_MAX_NUMBER));
+      LOG.warn("Only considering first {} indexes", PARTITION_INDEX_MAX_NUMBER);
       return indexes.subList(0, PARTITION_INDEX_MAX_NUMBER);
     }
     return indexes;
@@ -733,7 +733,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
       return Objects.nonNull(awsGlue.getTable(request).get().table());
     } catch (ExecutionException e) {
       if (e.getCause() instanceof EntityNotFoundException) {
-        LOG.info("Table not found: " + tableId(databaseName, tableName), e);
+        LOG.info("Table not found: {}.{}", databaseName, tableName, e);
         return false;
       } else {
         throw new HoodieGlueSyncException("Fail to get table: " + tableId(databaseName, tableName), e);
@@ -750,7 +750,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
       return Objects.nonNull(awsGlue.getDatabase(request).get().database());
     } catch (ExecutionException e) {
       if (e.getCause() instanceof EntityNotFoundException) {
-        LOG.info("Database not found: " + databaseName, e);
+        LOG.info("Database not found: {}", databaseName, e);
         return false;
       } else {
         throw new HoodieGlueSyncException("Fail to check if database exists " + databaseName, e);
@@ -777,7 +777,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
       CreateDatabaseResponse result = awsGlue.createDatabase(request).get();
       LOG.info("Successfully created database in AWS Glue: " + result.toString());
     } catch (AlreadyExistsException e) {
-      LOG.warn("AWS Glue Database " + databaseName + " already exists", e);
+      LOG.info("AWS Glue Database {} already exists", databaseName, e);
     } catch (Exception e) {
       throw new HoodieGlueSyncException("Fail to create database " + databaseName, e);
     }
@@ -801,7 +801,7 @@ public class AWSGlueCatalogSyncClient extends HoodieSyncClient {
   @Override
   public void updateLastCommitTimeSynced(String tableName) {
     if (!getActiveTimeline().lastInstant().isPresent()) {
-      LOG.warn("No commit in active timeline.");
+      LOG.info("No commit in active timeline.");
       return;
     }
     final String lastCommitTimestamp = getActiveTimeline().lastInstant().get().getTimestamp();
diff --git a/hudi-aws/src/main/java/org/apache/hudi/aws/transaction/lock/S3StorageLockClient.java b/hudi-aws/src/main/java/org/apache/hudi/aws/transaction/lock/S3StorageLockClient.java
new file mode 100644
index 000000000000..c6034ca80783
--- /dev/null
+++ b/hudi-aws/src/main/java/org/apache/hudi/aws/transaction/lock/S3StorageLockClient.java
@@ -0,0 +1,330 @@
+/*
+ * 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.hudi.aws.transaction.lock;
+
+import org.apache.hudi.aws.credentials.HoodieAWSCredentialsProviderFactory;
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.client.transaction.lock.models.LockGetResult;
+import org.apache.hudi.client.transaction.lock.models.LockUpsertResult;
+import org.apache.hudi.client.transaction.lock.models.StorageLockData;
+import org.apache.hudi.client.transaction.lock.models.StorageLockFile;
+import org.apache.hudi.common.util.Functions;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.common.util.VisibleForTesting;
+import org.apache.hudi.common.util.collection.Pair;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.exception.SdkClientException;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetBucketLocationRequest;
+import software.amazon.awssdk.services.s3.model.GetBucketLocationResponse;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.time.Duration;
+import java.util.Properties;
+
+import static org.apache.hudi.config.StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS;
+
+/**
+ * S3-based distributed lock client using ETag checks (AWS SDK v2).
+ * See RFC: <a href="https://github.com/apache/hudi/blob/master/rfc/rfc-91/rfc-91.md">RFC-91</a>
+ */
+@ThreadSafe
+public class S3StorageLockClient implements StorageLockClient {
+
+  private static final Logger LOG = LoggerFactory.getLogger(S3StorageLockClient.class);
+  private static final int PRECONDITION_FAILURE_ERROR_CODE = 412;
+  private static final int NOT_FOUND_ERROR_CODE = 404;
+  private static final int CONDITIONAL_REQUEST_CONFLICT_ERROR_CODE = 409;
+  private static final int RATE_LIMIT_ERROR_CODE = 429;
+  private static final int INTERNAL_SERVER_ERROR_CODE_MIN = 500;
+
+  private final Logger logger;
+  private final S3Client s3Client;
+  private final String bucketName;
+  private final String lockFilePath;
+  private final String ownerId;
+
+  /**
+   * Constructor that is used by reflection to instantiate an S3-based storage locking client.
+   *
+   * @param ownerId     The owner id.
+   * @param lockFileUri The full table base path where the lock will be written.
+   * @param props       The properties for the lock config, can be used to customize client.
+   */
+  public S3StorageLockClient(String ownerId, String lockFileUri, Properties props) {
+    this(ownerId, lockFileUri, props, createDefaultS3Client(), LOG);
+  }
+
+  @VisibleForTesting
+  S3StorageLockClient(String ownerId, String lockFileUri, Properties props, Functions.Function2<String, Properties, S3Client> s3ClientSupplier, Logger logger) {
+    Pair<String, String> bucketAndPath = StorageLockClient.parseBucketAndPath(lockFileUri);
+    this.bucketName = bucketAndPath.getLeft();
+    this.lockFilePath = bucketAndPath.getRight();
+
+    this.s3Client = s3ClientSupplier.apply(bucketName, props);
+    this.ownerId = ownerId;
+    this.logger = logger;
+  }
+
+  @Override
+  public Pair<LockGetResult, Option<StorageLockFile>> readCurrentLockFile() {
+    try (ResponseInputStream<GetObjectResponse> in = s3Client.getObject(
+        GetObjectRequest.builder()
+            .bucket(bucketName)
+            .key(lockFilePath)
+            .build())) {
+      String eTag = in.response().eTag();
+      return Pair.of(LockGetResult.SUCCESS, Option.of(StorageLockFile.createFromStream(in, eTag)));
+    } catch (S3Exception e) {
+      int status = e.statusCode();
+      // Default to unknown error
+      LockGetResult result = LockGetResult.UNKNOWN_ERROR;
+      if (status == NOT_FOUND_ERROR_CODE) {
+        logger.info("OwnerId: {}, Object not found: {}", ownerId, lockFilePath);
+        result = LockGetResult.NOT_EXISTS;
+      } else if (status == CONDITIONAL_REQUEST_CONFLICT_ERROR_CODE) {
+        logger.info("OwnerId: {}, Conflicting operation has occurred: {}", ownerId, lockFilePath);
+      } else if (status == RATE_LIMIT_ERROR_CODE) {
+        logger.warn("OwnerId: {}, Rate limit exceeded: {}", ownerId, lockFilePath);
+      } else if (status >= INTERNAL_SERVER_ERROR_CODE_MIN) {
+        logger.warn("OwnerId: {}, S3 internal server error: {}", ownerId, lockFilePath, e);
+      } else {
+        throw e;
+      }
+      return Pair.of(result, Option.empty());
+    } catch (IOException e) {
+      throw new UncheckedIOException("Failed reading lock file from S3: " + lockFilePath, e);
+    }
+  }
+
+  @Override
+  public Pair<LockUpsertResult, Option<StorageLockFile>> tryUpsertLockFile(StorageLockData newLockData, Option<StorageLockFile> previousLockFile) {
+    boolean isLockRenewal = previousLockFile.isPresent();
+    String currentEtag = isLockRenewal ? previousLockFile.get().getVersionId() : null;
+    LockUpsertResult result = LockUpsertResult.UNKNOWN_ERROR;
+    try {
+      StorageLockFile updated = createOrUpdateLockFileInternal(newLockData, currentEtag);
+      return Pair.of(LockUpsertResult.SUCCESS, Option.of(updated));
+    } catch (S3Exception e) {
+      result = handleUpsertS3Exception(e);
+    } catch (AwsServiceException | SdkClientException e) {
+      logger.error("OwnerId: {}, Unexpected SDK error while writing lock file: {}", ownerId, lockFilePath, e);
+      if (!isLockRenewal) {
+        // We should always throw errors early when we are creating the lock file.
+        // This is likely indicative of a larger issue that should bubble up sooner.
+        throw e;
+      }
+    }
+
+    return Pair.of(result, Option.empty());
+  }
+
+  /**
+   * Internal helper to create or update the lock file with optional ETag precondition.
+   */
+  private StorageLockFile createOrUpdateLockFileInternal(StorageLockData lockData, String expectedEtag) {
+    byte[] bytes = StorageLockFile.toByteArray(lockData);
+    PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder()
+        .bucket(bucketName)
+        .key(lockFilePath);
+
+    // ETag-based constraints:
+    // - If expectedEtag is not null:
+    //    We assume that the file already exists on S3 with the ETag "expectedEtag".
+    //    The update operation will include an ifMatch(expectedEtag) condition, meaning the update will only
+    //    succeed if the current file's ETag exactly matches expectedEtag.
+    //    If the actual ETag of the file on S3 differs from expectedEtag, the update attempt will fail.
+    // - If expectedEtag is null:
+    //    We assume that the file does not currently exist on S3.
+    //    The operation will use ifNoneMatch("*"), which instructs S3 to create the file only if it doesn't already exist.
+    //    If a file with the same name is present (i.e., there is an existing ETag), the creation attempt will fail.
+    if (expectedEtag == null) {
+      putRequestBuilder.ifNoneMatch("*");
+    } else {
+      putRequestBuilder.ifMatch(expectedEtag);
+    }
+
+    PutObjectResponse response = s3Client.putObject(putRequestBuilder.build(), RequestBody.fromBytes(bytes));
+    String newEtag = response.eTag();
+
+    return new StorageLockFile(lockData, newEtag);
+  }
+
+  private LockUpsertResult handleUpsertS3Exception(S3Exception e) {
+    int status = e.statusCode();
+    if (status == PRECONDITION_FAILURE_ERROR_CODE) {
+      logger.info("OwnerId: {}, Lockfile modified by another process: {}", ownerId, lockFilePath);
+      return LockUpsertResult.ACQUIRED_BY_OTHERS;
+    } else if (status == CONDITIONAL_REQUEST_CONFLICT_ERROR_CODE) {
+      logger.info("OwnerId: {}, Retriable conditional request conflict error: {}", ownerId, lockFilePath);
+    } else if (status == RATE_LIMIT_ERROR_CODE) {
+      logger.warn("OwnerId: {}, Rate limit exceeded for: {}", ownerId, lockFilePath);
+    } else if (status >= INTERNAL_SERVER_ERROR_CODE_MIN) {
+      logger.warn("OwnerId: {}, internal server error for: {}", ownerId, lockFilePath, e);
+    } else {
+      logger.warn("OwnerId: {}, Error writing lock file: {}", ownerId, lockFilePath, e);
+    }
+
+    return LockUpsertResult.UNKNOWN_ERROR;
+  }
+
+  private static Functions.Function2<String, Properties, S3Client> createDefaultS3Client() {
+    return (bucketName, props) -> {
+      Region region;
+      boolean requiredFallbackRegion = false;
+      try {
+        region = DefaultAwsRegionProviderChain.builder().build().getRegion();
+      } catch (SdkClientException e) {
+        // Fallback to us-east-1 if no region is found
+        region = Region.US_EAST_1;
+        requiredFallbackRegion = true;
+      }
+
+      // Set all request timeouts to be 1/5 of the default validity.
+      // Each call to acquire a lock requires 2 requests.
+      // Each renewal requires 1 request.
+      long validityTimeoutSecs =
+          ((Number) props.getOrDefault(
+              VALIDITY_TIMEOUT_SECONDS.key(),
+              VALIDITY_TIMEOUT_SECONDS.defaultValue()
+          )).longValue();
+      long s3CallTimeoutSecs = validityTimeoutSecs / 5;
+      S3Client s3Client = createS3Client(region, s3CallTimeoutSecs, props);
+      if (requiredFallbackRegion) {
+        GetBucketLocationResponse bucketLocationResponse = s3Client.getBucketLocation(
+            GetBucketLocationRequest.builder().bucket(bucketName).build());
+        // This is null when the region is US_EAST_1, so we do not need to worry about duplicate logic.
+        String regionString = bucketLocationResponse.locationConstraintAsString();
+        if (!StringUtils.isNullOrEmpty(regionString)) {
+          // Close existing client and create another.
+          s3Client.close();
+          return createS3Client(
+              Region.of(regionString),
+              s3CallTimeoutSecs,
+              props);
+        }
+      }
+
+      return s3Client;
+    };
+  }
+
+  private static S3Client createS3Client(Region region, long timeoutSecs, Properties props) {
+    // Set the timeout, credentials, and region
+    return S3Client.builder()
+        .overrideConfiguration(
+            b -> b.apiCallTimeout(Duration.ofSeconds(timeoutSecs)))
+        .credentialsProvider(HoodieAWSCredentialsProviderFactory.getAwsCredentialsProvider(props))
+        .region(region).build();
+  }
+
+  @Override
+  public Option<String> readObject(String filePath, boolean checkExistsFirst) {
+    try {
+      // Parse the file path to get bucket and key
+      Pair<String, String> bucketAndKey = StorageLockClient.parseBucketAndPath(filePath);
+      String bucket = bucketAndKey.getLeft();
+      String key = bucketAndKey.getRight();
+
+      if (checkExistsFirst) {
+        // First check if the file exists (lightweight HEAD request)
+        try {
+          s3Client.headObject(HeadObjectRequest.builder()
+              .bucket(bucket)
+              .key(key)
+              .build());
+        } catch (S3Exception e) {
+          if (e.statusCode() == NOT_FOUND_ERROR_CODE) {
+            // File doesn't exist - this is the common case for optional configs
+            logger.debug("JSON config file not found: {}", filePath);
+            return Option.empty();
+          }
+          throw e; // Re-throw other errors
+        }
+      }
+
+      // Read the file (either after existence check or directly)
+      String content = s3Client.getObjectAsBytes(
+          GetObjectRequest.builder()
+              .bucket(bucket)
+              .key(key)
+              .build()).asUtf8String();
+
+      return Option.of(content);
+    } catch (S3Exception e) {
+      if (e.statusCode() == NOT_FOUND_ERROR_CODE) {
+        logger.debug("JSON config file not found: {}", filePath);
+        return Option.empty();
+      }
+      logger.error("Error reading JSON config file: {}", filePath, e);
+      return Option.empty();
+    } catch (Exception e) {
+      logger.error("Error reading JSON config file: {}", filePath, e);
+      return Option.empty();
+    }
+  }
+
+  @Override
+  public boolean writeObject(String filePath, String content) {
+    try {
+      // Parse the file path to get bucket and key
+      Pair<String, String> bucketAndPath = StorageLockClient.parseBucketAndPath(filePath);
+      String bucket = bucketAndPath.getLeft();
+      String key = bucketAndPath.getRight();
+
+      // Write the content to S3
+      s3Client.putObject(
+          PutObjectRequest.builder()
+              .bucket(bucket)
+              .key(key)
+              .build(),
+          RequestBody.fromString(content));
+
+      logger.debug("Successfully wrote object to: {}", filePath);
+      return true;
+    } catch (Exception e) {
+      logger.error("Error writing object to: {}", filePath, e);
+      return false;
+    }
+  }
+
+  @Override
+  public void close() {
+    s3Client.close();
+  }
+}
diff --git a/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageBasedLockProvider.java b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageBasedLockProvider.java
new file mode 100644
index 000000000000..4b7e083ef1fc
--- /dev/null
+++ b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageBasedLockProvider.java
@@ -0,0 +1,160 @@
+/*
+ * 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.hudi.aws.transaction.lock;
+
+import org.apache.hudi.client.transaction.lock.StorageBasedLockProviderTestBase;
+import org.apache.hudi.client.transaction.lock.StorageBasedLockProvider;
+import org.apache.hudi.common.config.LockConfiguration;
+import org.apache.hudi.config.HoodieAWSConfig;
+import org.apache.hudi.storage.StorageConfiguration;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.testcontainers.containers.localstack.LocalStackContainer;
+import org.testcontainers.utility.DockerImageName;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.ServerSideEncryption;
+
+import static org.apache.hudi.common.config.HoodieCommonConfig.BASE_PATH;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mockStatic;
+import static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
+
+/**
+ * Tests S3-based StorageBasedLockProvider using a LocalStack container
+ * to emulate S3.
+ */
+@Disabled("HUDI-9159 The tests do not work. Disabling them to unblock Azure CI")
+public class TestS3StorageBasedLockProvider extends StorageBasedLockProviderTestBase {
+
+  private static final DockerImageName LOCALSTACK_IMAGE =
+          DockerImageName.parse("localstack/localstack:4.1.0");
+
+  private static LocalStackContainer S3_CONTAINER;
+
+  private static final String TEST_BUCKET = "test-bucket";
+  private static S3Client s3Client;
+
+  @BeforeAll
+  static void initContainer() {
+    // Spin up LocalStack with S3 service
+    S3_CONTAINER = new LocalStackContainer(LOCALSTACK_IMAGE)
+            .withServices(S3);
+    S3_CONTAINER.start();
+
+    s3Client = S3Client.builder()
+            .endpointOverride(S3_CONTAINER.getEndpointOverride(S3))
+            .credentialsProvider(
+                    StaticCredentialsProvider.create(
+                            AwsBasicCredentials.create(
+                                    S3_CONTAINER.getAccessKey(),
+                                    S3_CONTAINER.getSecretKey()
+                            )
+                    )
+            )
+            .region(Region.of(S3_CONTAINER.getRegion()))
+            .build();
+    s3Client.createBucket(CreateBucketRequest.builder()
+            .bucket(TEST_BUCKET)
+            .build());
+  }
+
+  @AfterAll
+  static void stopContainer() {
+    if (S3_CONTAINER != null) {
+      S3_CONTAINER.stop();
+    }
+  }
+
+  /**
+   * Creates a real S3-based StorageBasedLockProvider for testing.
+   */
+  @Override
+  protected StorageBasedLockProvider createLockProvider() {
+    LockConfiguration lockConf = new LockConfiguration(providerProperties);
+
+    try (MockedStatic<S3Client> s3ClientStaticMock =
+                 mockStatic(S3Client.class, Mockito.CALLS_REAL_METHODS)) {
+      s3ClientStaticMock.when(S3Client::builder).thenAnswer(invocation -> {
+        S3ClientBuilder builder = (S3ClientBuilder) invocation.callRealMethod();
+        // Always add the endpoint override from S3_CONTAINER
+        builder.endpointOverride(S3_CONTAINER.getEndpointOverride(S3));
+        return builder;
+      });
+      return new StorageBasedLockProvider(
+          lockConf,
+          (StorageConfiguration<?>) null);
+    }
+  }
+
+  @BeforeEach
+  void setupLockProvider() {
+    // Provide base path for the test
+    providerProperties.put(BASE_PATH.key(), String.format("s3://%s/lake/db/tbl-default", TEST_BUCKET));
+    providerProperties.put(HoodieAWSConfig.AWS_SECRET_KEY.key(), S3_CONTAINER.getSecretKey());
+    providerProperties.put(HoodieAWSConfig.AWS_ACCESS_KEY.key(), S3_CONTAINER.getAccessKey());
+    lockProvider = createLockProvider();
+  }
+
+  @Test
+  void testS3Preconditions() {
+    // Similar to GCS test, just to show we can put and head objects
+    String key = "my-test-object";
+    // Put an object
+    PutObjectResponse put1 = s3Client.putObject(
+            PutObjectRequest.builder()
+                    .bucket(TEST_BUCKET)
+                    .key(key)
+                    .serverSideEncryption(ServerSideEncryption.AES256)
+                    .ifNoneMatch("*")
+                    .build(),
+            RequestBody.fromString("Hello1"));
+
+    // Put a second object with different contents
+    PutObjectResponse put2 = s3Client.putObject(
+            PutObjectRequest.builder()
+                    .bucket(TEST_BUCKET)
+                    .key(key)
+                    .serverSideEncryption(ServerSideEncryption.AES256)
+                    .ifMatch(put1.eTag())
+                    .build(),
+            RequestBody.fromString("Hello2"));
+    s3Client.close();
+    // ETags should differ
+    assertNotNull(put1.eTag());
+    assertNotNull(put2.eTag());
+    assertNotEquals(put1.eTag(), put2.eTag(), "ETags should differ if the content changed");
+  }
+}
diff --git a/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClient.java b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClient.java
new file mode 100644
index 000000000000..71d2011a88c2
--- /dev/null
+++ b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClient.java
@@ -0,0 +1,336 @@
+/*
+ * 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.hudi.aws.transaction.lock;
+
+import org.apache.hudi.client.transaction.lock.models.LockGetResult;
+import org.apache.hudi.client.transaction.lock.models.LockUpsertResult;
+import org.apache.hudi.client.transaction.lock.models.StorageLockData;
+import org.apache.hudi.client.transaction.lock.models.StorageLockFile;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieIOException;
+
+import org.apache.hudi.exception.HoodieLockException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.slf4j.Logger;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import static org.apache.hudi.client.transaction.lock.models.LockUpsertResult.UNKNOWN_ERROR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TestS3StorageLockClient {
+
+  private static final String OWNER_ID = "ownerId";
+  private static final String LOCK_FILE_URI = "s3://bucket/lockFilePath";
+  private static final String LOCK_FILE_PATH = "lockFilePath";
+
+  @Mock
+  private S3Client mockS3Client;
+
+  @Mock
+  private Logger mockLogger;
+
+  private S3StorageLockClient lockService;
+
+  @BeforeEach
+  void setUp() {
+    lockService = new S3StorageLockClient(
+        OWNER_ID,
+        LOCK_FILE_URI,
+        new Properties(),
+        (a,b) -> mockS3Client,
+        mockLogger
+    );
+  }
+
+  private void mockS3ObjectWithLockData(StorageLockData lockData, String eTag) {
+    byte[] bytes = StorageLockFile.toByteArray(lockData);
+    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+
+    ResponseInputStream<GetObjectResponse> mockResponseStream = new ResponseInputStream<>(
+            GetObjectResponse.builder().eTag(eTag).build(),
+            new MockAbortableInputStream(bais)
+    );
+
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockResponseStream);
+  }
+
+  private static class MockAbortableInputStream extends java.io.FilterInputStream {
+    protected MockAbortableInputStream(ByteArrayInputStream in) {
+      super(in);
+    }
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_noPreviousLock_success() {
+    StorageLockData lockData = new StorageLockData(false, System.currentTimeMillis(), "myTxOwner");
+    PutObjectResponse putResp = PutObjectResponse.builder().eTag("new-etag-123").build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenReturn(putResp);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result = lockService.tryUpsertLockFile(lockData, Option.empty());
+
+    assertEquals(LockUpsertResult.SUCCESS, result.getLeft());
+    assertTrue(result.getRight().isPresent());
+    assertEquals("new-etag-123", result.getRight().get().getVersionId());
+    verify(mockS3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class));
+    verifyNoMoreInteractions(mockLogger);
+  }
+
+  @Test
+  void testInitializeWithInvalidUri() {
+    assertThrows(HoodieLockException.class, () -> new S3StorageLockClient(
+        OWNER_ID,
+        "\\",
+        new Properties(),
+        (a, b) -> mockS3Client,
+        mockLogger
+    ));
+  }
+
+  @Test
+  void testInitializeWithNoLockFilePath() {
+    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new S3StorageLockClient(
+        OWNER_ID,
+        "s3://bucket/",
+        new Properties(),
+        (a, b) -> mockS3Client,
+        mockLogger
+    ));
+    assertTrue(ex.getMessage().contains("path"));
+  }
+
+  @Test
+  void testInitializeWithNoBucketName() {
+    IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> new S3StorageLockClient(
+        OWNER_ID,
+        "s3:///path",
+        new Properties(),
+        (a, b) -> mockS3Client,
+        mockLogger
+    ));
+    assertTrue(ex.getMessage().contains("bucket name"));
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_withPreviousLock_success() {
+    StorageLockData lockData = new StorageLockData(false, 1000L, "myTxOwner");
+    StorageLockFile prevLockFile = new StorageLockFile(lockData, "old-etag-999");
+    PutObjectResponse putResp = PutObjectResponse.builder().eTag("new-etag-456").build();
+    when(mockS3Client.putObject(
+            eq(PutObjectRequest.builder().bucket("bucket")
+                    .key(LOCK_FILE_PATH)
+                    .ifMatch("old-etag-999")
+                    .build()),
+            any(RequestBody.class)
+    )).thenReturn(putResp);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result =
+            lockService.tryUpsertLockFile(lockData, Option.of(prevLockFile));
+
+    assertEquals(LockUpsertResult.SUCCESS, result.getLeft());
+    assertTrue(result.getRight().isPresent());
+    assertEquals("new-etag-456", result.getRight().get().getVersionId());
+    verify(mockS3Client, times(1)).putObject(any(PutObjectRequest.class), any(RequestBody.class));
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_preconditionFailed() {
+    StorageLockData lockData = new StorageLockData(false, 2000L, "txOwner");
+    StorageLockFile prevLockFile = new StorageLockFile(lockData, "some-etag");
+
+    AwsServiceException ex412 = S3Exception.builder().statusCode(412).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(ex412);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result =
+            lockService.tryUpsertLockFile(lockData, Option.of(prevLockFile));
+
+    assertEquals(LockUpsertResult.ACQUIRED_BY_OTHERS, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).info(contains("Lockfile modified by another process"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_conflict409() {
+    StorageLockData lockData = new StorageLockData(false, 3000L, "myTxOwner");
+    StorageLockFile prevLockFile = new StorageLockFile(lockData, "some-etag-409");
+
+    AwsServiceException ex409 = S3Exception.builder().statusCode(409).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(ex409);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result =
+            lockService.tryUpsertLockFile(lockData, Option.of(prevLockFile));
+
+    // This error is not necessarily unknown, but we don't know the state of the lockfile,
+    // which is why the return result is UNKNOWN_ERROR
+    assertEquals(UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).info(contains("Retriable conditional request conflict error"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_rateLimitExceeded() {
+    StorageLockData lockData = new StorageLockData(false, 4000L, "myTxOwner");
+    AwsServiceException ex429 = S3Exception.builder().statusCode(429).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(ex429);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result =
+            lockService.tryUpsertLockFile(lockData, Option.empty());
+
+    assertEquals(UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).warn(contains("Rate limit exceeded"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testTryCreateOrUpdateLockFile_serverError() {
+    StorageLockData lockData = new StorageLockData(false, 5000L, "myTxOwner");
+    AwsServiceException ex503 = S3Exception.builder().statusCode(503).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(ex503);
+
+    Pair<LockUpsertResult, Option<StorageLockFile>> result =
+            lockService.tryUpsertLockFile(lockData, Option.empty());
+
+    assertEquals(UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).warn(contains("internal server error"), eq(OWNER_ID), eq(LOCK_FILE_PATH), eq(ex503));
+  }
+
+  @Test
+  void testTryCreateLockFile_unexpectedError() {
+    StorageLockData lockData = new StorageLockData(false, 8000L, "myTxOwner");
+    AwsServiceException ex400 = AwsServiceException.builder().statusCode(400).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(ex400);
+
+    assertThrows(AwsServiceException.class, () -> lockService.tryUpsertLockFile(lockData, Option.empty()));
+  }
+
+  @Test
+  void testGetCurrentLockFile_404Error() {
+    AwsServiceException ex404 = S3Exception.builder().statusCode(404).build();
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex404);
+
+    Pair<LockGetResult, Option<StorageLockFile>> result = lockService.readCurrentLockFile();
+    assertEquals(LockGetResult.NOT_EXISTS, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).info(contains("Object not found"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testGetCurrentLockFile_409Error() {
+    AwsServiceException ex409 = S3Exception.builder().statusCode(409).build();
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex409);
+
+    Pair<LockGetResult, Option<StorageLockFile>> result = lockService.readCurrentLockFile();
+    assertEquals(LockGetResult.UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).info(contains("Conflicting operation has occurred"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testGetCurrentLockFile_objectFound() {
+    StorageLockData lockData = new StorageLockData(false, 9999L, "myTxOwner");
+    mockS3ObjectWithLockData(lockData, "abc-etag");
+
+    Pair<LockGetResult, Option<StorageLockFile>> result = lockService.readCurrentLockFile();
+    assertEquals(LockGetResult.SUCCESS, result.getLeft());
+    assertTrue(result.getRight().isPresent());
+    assertEquals("abc-etag", result.getRight().get().getVersionId());
+    assertEquals("myTxOwner", result.getRight().get().getOwner());
+  }
+
+  @Test
+  void testGetCurrentLockFile_rateLimit() {
+    AwsServiceException ex429 = S3Exception.builder().statusCode(429).build();
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex429);
+
+    Pair<LockGetResult, Option<StorageLockFile>> result = lockService.readCurrentLockFile();
+    assertEquals(LockGetResult.UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).warn(contains("Rate limit exceeded"), eq(OWNER_ID), eq(LOCK_FILE_PATH));
+  }
+
+  @Test
+  void testGetCurrentLockFile_serverError() {
+    AwsServiceException ex500 = S3Exception.builder().statusCode(500).build();
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex500);
+
+    Pair<LockGetResult, Option<StorageLockFile>> result = lockService.readCurrentLockFile();
+    assertEquals(LockGetResult.UNKNOWN_ERROR, result.getLeft());
+    assertTrue(result.getRight().isEmpty());
+    verify(mockLogger).warn(contains("S3 internal server error"), eq(OWNER_ID), eq(LOCK_FILE_PATH), eq(ex500));
+  }
+
+  @Test
+  void testGetCurrentLockFile_unexpectedError() {
+    AwsServiceException ex400 = S3Exception.builder().statusCode(400).build();
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenThrow(ex400);
+
+    assertThrows(S3Exception.class, () -> lockService.readCurrentLockFile());
+  }
+
+  @Test
+  void testGetCurrentLockFile_readFailure() {
+    GetObjectResponse getObjectResponse = GetObjectResponse.builder().eTag("some-etag").build();
+    @SuppressWarnings("unchecked")
+    InputStream mockInputStream = new InputStream() {
+      @Override
+      public int read() throws IOException {
+        throw new IOException("Read error");
+      }
+    };
+    ResponseInputStream mockStream = new ResponseInputStream(getObjectResponse, mockInputStream);
+    when(mockS3Client.getObject(any(GetObjectRequest.class))).thenReturn(mockStream);
+    assertThrows(HoodieIOException.class, () -> lockService.readCurrentLockFile());
+  }
+
+  @Test
+  void testClose() throws Exception {
+    lockService.close();
+    verify(mockS3Client).close();
+  }
+}
diff --git a/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClientAuditOperations.java b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClientAuditOperations.java
new file mode 100644
index 000000000000..dda5cdb99880
--- /dev/null
+++ b/hudi-aws/src/test/java/org/apache/hudi/aws/transaction/lock/TestS3StorageLockClientAuditOperations.java
@@ -0,0 +1,272 @@
+/*
+ * 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.hudi.aws.transaction.lock;
+
+import org.apache.hudi.common.util.Option;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import software.amazon.awssdk.awscore.exception.AwsServiceException;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
+import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for S3StorageLockClient audit operations (readObject and writeObject methods)
+ */
+public class TestS3StorageLockClientAuditOperations {
+
+  private S3Client mockS3Client;
+  private Logger mockLogger;
+  private S3StorageLockClient lockClient;
+
+  @BeforeEach
+  void setUp() {
+    mockS3Client = mock(S3Client.class);
+    mockLogger = mock(Logger.class);
+    String ownerId = "test-owner";
+    String lockFileUri = "s3://test-bucket/table/.hoodie/.locks/table_lock.json";
+    lockClient = new S3StorageLockClient(
+        ownerId,
+        lockFileUri,
+        new Properties(),
+        (bucket, props) -> mockS3Client,
+        mockLogger);
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstFileNotFound() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // HEAD request returns 404
+    S3Exception notFoundException = (S3Exception) S3Exception.builder()
+        .statusCode(404)
+        .message("Not Found")
+        .build();
+    when(mockS3Client.headObject(any(HeadObjectRequest.class)))
+        .thenThrow(notFoundException);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isEmpty());
+    // Should only call HEAD, not GET
+    verify(mockS3Client, times(1)).headObject(any(HeadObjectRequest.class));
+    verify(mockS3Client, never()).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstFileExists() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+    String expectedContent = "{\"STORAGE_LP_AUDIT_SERVICE_ENABLED\": true}";
+
+    // HEAD request succeeds
+    HeadObjectResponse headResponse = HeadObjectResponse.builder().build();
+    when(mockS3Client.headObject(any(HeadObjectRequest.class)))
+        .thenReturn(headResponse);
+
+    // GET request returns content
+    ResponseBytes<GetObjectResponse> responseBytes = ResponseBytes.fromByteArray(
+        GetObjectResponse.builder().build(),
+        expectedContent.getBytes(StandardCharsets.UTF_8));
+    when(mockS3Client.getObjectAsBytes(any(GetObjectRequest.class)))
+        .thenReturn(responseBytes);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isPresent());
+    assertEquals(expectedContent, result.get());
+    // Should call both HEAD and GET
+    verify(mockS3Client, times(1)).headObject(any(HeadObjectRequest.class));
+    verify(mockS3Client, times(1)).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithoutCheckExistsFirstFileNotFound() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // Direct GET request returns 404
+    S3Exception notFoundException = (S3Exception) S3Exception.builder()
+        .statusCode(404)
+        .message("Not Found")
+        .build();
+    when(mockS3Client.getObjectAsBytes(any(GetObjectRequest.class)))
+        .thenThrow(notFoundException);
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isEmpty());
+    // Should not call HEAD, only GET
+    verify(mockS3Client, never()).headObject(any(HeadObjectRequest.class));
+    verify(mockS3Client, times(1)).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithoutCheckExistsFirstFileExists() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+    String expectedContent = "{\"STORAGE_LP_AUDIT_SERVICE_ENABLED\": false}";
+
+    // Direct GET request returns content
+    ResponseBytes<GetObjectResponse> responseBytes = ResponseBytes.fromByteArray(
+        GetObjectResponse.builder().build(),
+        expectedContent.getBytes(StandardCharsets.UTF_8));
+    when(mockS3Client.getObjectAsBytes(any(GetObjectRequest.class)))
+        .thenReturn(responseBytes);
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isPresent());
+    assertEquals(expectedContent, result.get());
+    // Should not call HEAD, only GET
+    verify(mockS3Client, never()).headObject(any(HeadObjectRequest.class));
+    verify(mockS3Client, times(1)).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstOtherS3Error() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // HEAD request returns non-404 error
+    S3Exception serverError = (S3Exception) S3Exception.builder()
+        .statusCode(500)
+        .message("Internal Server Error")
+        .build();
+    when(mockS3Client.headObject(any(HeadObjectRequest.class)))
+        .thenThrow(serverError);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isEmpty());
+    verify(mockS3Client, times(1)).headObject(any(HeadObjectRequest.class));
+    verify(mockS3Client, never()).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithInvalidUri() {
+    String invalidPath = "not-a-valid-uri";
+
+    Option<String> result = lockClient.readObject(invalidPath, false);
+
+    assertTrue(result.isEmpty());
+    // Should not make any S3 calls due to URI parsing error
+    verify(mockS3Client, never()).headObject(any(HeadObjectRequest.class));
+  }
+
+  @Test
+  void testReadConfigWithRateLimitError() {
+    String configPath = "s3://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // GET request returns rate limit error
+    S3Exception rateLimitException = (S3Exception) S3Exception.builder()
+        .statusCode(429)
+        .message("Too Many Requests")
+        .build();
+    when(mockS3Client.getObjectAsBytes(any(GetObjectRequest.class)))
+        .thenThrow(rateLimitException);
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isEmpty());
+    verify(mockS3Client, times(1)).getObjectAsBytes(any(GetObjectRequest.class));
+  }
+
+  // ================================
+  // writeObject() tests
+  // ================================
+
+  @Test
+  void testWriteObject_success() {
+    String filePath = "s3://test-bucket/audit/test-audit.jsonl";
+    String content = "{\"test\": \"data\"}\n";
+    PutObjectResponse putResp = PutObjectResponse.builder().eTag("write-etag-123").build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenReturn(putResp);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertTrue(result);
+    verify(mockS3Client, times(1)).putObject(
+        eq(PutObjectRequest.builder().bucket("test-bucket").key("audit/test-audit.jsonl").build()),
+        any(RequestBody.class)
+    );
+    verify(mockLogger).debug("Successfully wrote object to: {}", filePath);
+  }
+
+  @Test
+  void testWriteObject_s3Exception() {
+    String filePath = "s3://test-bucket/audit/test-audit.jsonl";
+    String content = "{\"test\": \"data\"}\n";
+    AwsServiceException s3Exception = S3Exception.builder().statusCode(500).build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenThrow(s3Exception);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertFalse(result);
+    verify(mockLogger).error(contains("Error writing object to"), eq(filePath), eq(s3Exception));
+  }
+
+  @Test
+  void testWriteObject_invalidPath() {
+    String invalidPath = "invalid-path";
+    String content = "{\"test\": \"data\"}\n";
+
+    boolean result = lockClient.writeObject(invalidPath, content);
+
+    assertFalse(result);
+    verify(mockLogger).error(contains("Error writing object to"), eq(invalidPath), any(Exception.class));
+  }
+
+  @Test
+  void testWriteObject_emptyContent() {
+    String filePath = "s3://test-bucket/audit/empty-content.jsonl";
+    String content = "";
+    PutObjectResponse putResp = PutObjectResponse.builder().eTag("empty-etag-456").build();
+    when(mockS3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).thenReturn(putResp);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertTrue(result);
+    verify(mockS3Client, times(1)).putObject(
+        eq(PutObjectRequest.builder().bucket("test-bucket").key("audit/empty-content.jsonl").build()),
+        any(RequestBody.class)
+    );
+    verify(mockLogger).debug("Successfully wrote object to: {}", filePath);
+  }
+}
diff --git a/hudi-cli/src/main/java/org/apache/hudi/cli/commands/LockAuditingCommand.java b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/LockAuditingCommand.java
new file mode 100644
index 000000000000..18d3862aff63
--- /dev/null
+++ b/hudi-cli/src/main/java/org/apache/hudi/cli/commands/LockAuditingCommand.java
@@ -0,0 +1,663 @@
+/*
+ * 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.hudi.cli.commands;
+
+import org.apache.hudi.cli.HoodieCLI;
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService;
+import org.apache.hudi.common.util.FileIOUtils;
+import org.apache.hudi.common.util.Option;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.StoragePathInfo;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.shell.standard.ShellComponent;
+import org.springframework.shell.standard.ShellMethod;
+import org.springframework.shell.standard.ShellOption;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * CLI commands for managing Hudi table lock auditing functionality.
+ */
+@ShellComponent
+public class LockAuditingCommand {
+
+  private static final Logger LOG = LoggerFactory.getLogger(LockAuditingCommand.class);
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  /**
+   * Represents a single audit log entry in the JSONL file.
+   * Maps to the structure written by StorageLockProviderAuditService.
+   */
+  public static class AuditRecord {
+    private final String ownerId;
+    private final long transactionStartTime;
+    private final long timestamp;
+    private final String state;
+    private final long lockExpiration;
+    private final boolean lockHeld;
+
+    @JsonCreator
+    public AuditRecord(
+        @JsonProperty("ownerId") String ownerId,
+        @JsonProperty("transactionStartTime") long transactionStartTime,
+        @JsonProperty("timestamp") long timestamp,
+        @JsonProperty("state") String state,
+        @JsonProperty("lockExpiration") long lockExpiration,
+        @JsonProperty("lockHeld") boolean lockHeld) {
+      this.ownerId = ownerId;
+      this.transactionStartTime = transactionStartTime;
+      this.timestamp = timestamp;
+      this.state = state;
+      this.lockExpiration = lockExpiration;
+      this.lockHeld = lockHeld;
+    }
+
+    public String getOwnerId() {
+      return ownerId;
+    }
+
+    public long getTransactionStartTime() {
+      return transactionStartTime;
+    }
+
+    public long getTimestamp() {
+      return timestamp;
+    }
+
+    public String getState() {
+      return state;
+    }
+
+    public long getLockExpiration() {
+      return lockExpiration;
+    }
+
+    public boolean isLockHeld() {
+      return lockHeld;
+    }
+  }
+
+  /**
+   * Represents a transaction window with start time, end time, and metadata.
+   */
+  static class TransactionWindow {
+    final String ownerId;
+    final long transactionCreationTime;
+    final long lockAcquisitionTime;
+    final Option<Long> lockReleaseTime;
+    final Option<Long> lockExpirationTime;
+    final String filename;
+
+    TransactionWindow(String ownerId, long transactionCreationTime, long lockAcquisitionTime,
+                      Option<Long> lockReleaseTime, Option<Long> lockExpirationTime, String filename) {
+      this.ownerId = ownerId;
+      this.transactionCreationTime = transactionCreationTime;
+      this.lockAcquisitionTime = lockAcquisitionTime;
+      this.lockReleaseTime = lockReleaseTime;
+      this.lockExpirationTime = lockExpirationTime;
+      this.filename = filename;
+    }
+
+    long getEffectiveEndTime() {
+      return lockReleaseTime.orElse(lockExpirationTime.orElse(lockAcquisitionTime));
+    }
+  }
+
+  /**
+   * Holds validation results.
+   */
+  static class ValidationResults {
+    final List<String> errors;
+    final List<String> warnings;
+
+    ValidationResults(List<String> errors, List<String> warnings) {
+      this.errors = errors;
+      this.warnings = warnings;
+    }
+  }
+
+  /**
+   * Holds cleanup operation results.
+   */
+  static class CleanupResult {
+    final boolean success;
+    final int deletedCount;
+    final int failedCount;
+    final int totalFiles;
+    final String message;
+    final boolean isDryRun;
+
+    CleanupResult(boolean success, int deletedCount, int failedCount, int totalFiles, String message, boolean isDryRun) {
+      this.success = success;
+      this.deletedCount = deletedCount;
+      this.failedCount = failedCount;
+      this.totalFiles = totalFiles;
+      this.message = message;
+      this.isDryRun = isDryRun;
+    }
+
+    boolean hasErrors() {
+      return failedCount > 0;
+    }
+
+    boolean hasFiles() {
+      return totalFiles > 0;
+    }
+
+    @Override
+    public String toString() {
+      return message;
+    }
+  }
+
+  /**
+   * Enables lock audit logging for the currently connected Hudi table.
+   * This command creates or updates the audit configuration file to enable
+   * audit logging for storage lock operations.
+   * 
+   * @return Status message indicating success or failure
+   */
+  @ShellMethod(key = "locks audit enable", value = "Enable storage lock audit service for the current table")
+  public String enableLockAudit() {
+    
+    if (HoodieCLI.basePath == null) {
+      return "No Hudi table loaded. Please connect to a table first.";
+    }
+
+    try {
+      // Create the audit config file path using utility method
+      String auditConfigPath = StorageLockProviderAuditService.getAuditConfigPath(HoodieCLI.basePath);
+      
+      // Create the JSON content
+      ObjectNode configJson = OBJECT_MAPPER.createObjectNode();
+      configJson.put(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD, true);
+      String jsonContent = OBJECT_MAPPER.writeValueAsString(configJson);
+      
+      // Write the config file using HoodieStorage
+      StoragePath configPath = new StoragePath(auditConfigPath);
+      try (OutputStream outputStream = HoodieCLI.storage.create(configPath, true)) {
+        outputStream.write(jsonContent.getBytes());
+      }
+      
+      return String.format("Lock audit enabled successfully.\nAudit config written to: %s\n"
+          + "Audit files will be stored at: %s", auditConfigPath, StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath));
+      
+    } catch (Exception e) {
+      LOG.error("Error enabling lock audit", e);
+      return String.format("Failed to enable lock audit: %s", e.getMessage());
+    }
+  }
+
+  /**
+   * Disables lock audit logging for the currently connected Hudi table.
+   * This command updates the audit configuration file to disable audit logging.
+   * 
+   * @param keepAuditFiles Whether to preserve existing audit files when disabling
+   * @return Status message indicating success or failure
+   */
+  @ShellMethod(key = "locks audit disable", value = "Disable storage lock audit service for the current table")
+  public String disableLockAudit(
+      @ShellOption(value = {"--keepAuditFiles"}, defaultValue = "true",
+          help = "Keep existing audit files when disabling") final boolean keepAuditFiles) {
+    
+    if (HoodieCLI.basePath == null) {
+      return "No Hudi table loaded. Please connect to a table first.";
+    }
+
+    try {
+      // Create the audit config file path
+      String auditConfigPath = StorageLockProviderAuditService.getAuditConfigPath(HoodieCLI.basePath);
+      StoragePath configPath = new StoragePath(auditConfigPath);
+
+      // Check if config file exists by attempting to get its info
+      try {
+        HoodieCLI.storage.getPathInfo(configPath);
+      } catch (FileNotFoundException e) {
+        return "Lock audit is already disabled (no configuration file found).";
+      }
+
+      // Create the JSON content with audit disabled
+      ObjectNode configJson = OBJECT_MAPPER.createObjectNode();
+      configJson.put(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD, false);
+      String jsonContent = OBJECT_MAPPER.writeValueAsString(configJson);
+
+      // Write the config file
+      try (OutputStream outputStream = HoodieCLI.storage.create(configPath, true)) {
+        outputStream.write(jsonContent.getBytes());
+      }
+      
+      String message = String.format("Lock audit disabled successfully.\nAudit config updated at: %s", auditConfigPath);
+      
+      if (keepAuditFiles) {
+        message += String.format("\nExisting audit files preserved at: %s", StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath));
+      } else {
+        // Call the cleanup method to prune all old files
+        CleanupResult cleanupResult = performAuditCleanup(false, 0);
+        if (cleanupResult.success && cleanupResult.deletedCount > 0) {
+          message += String.format("\nAudit files cleaned up at: %s", StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath));
+        } else if (cleanupResult.success && cleanupResult.deletedCount == 0) {
+          message += String.format("\nNo audit files to clean up at: %s", StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath));
+        } else {
+          message += String.format("\nWarning: Some audit files may not have been cleaned up: %s", cleanupResult.message);
+        }
+      }
+      
+      return message;
+      
+    } catch (Exception e) {
+      LOG.error("Error disabling lock audit", e);
+      return String.format("Failed to disable lock audit: %s", e.getMessage());
+    }
+  }
+
+  /**
+   * Shows the current status of lock audit logging for the connected table.
+   * This command checks the audit configuration file and reports whether
+   * auditing is currently enabled or disabled.
+   * 
+   * @return Status information about the current audit configuration
+   */
+  @ShellMethod(key = "locks audit status", value = "Show the current status of lock audit service")
+  public String showLockAuditStatus() {
+    
+    if (HoodieCLI.basePath == null) {
+      return "No Hudi table loaded. Please connect to a table first.";
+    }
+
+    try {
+      // Create the audit config file path
+      String auditConfigPath = StorageLockProviderAuditService.getAuditConfigPath(HoodieCLI.basePath);
+      StoragePath configPath = new StoragePath(auditConfigPath);
+
+      // Read and parse the configuration
+      String configContent;
+      try (InputStream inputStream = HoodieCLI.storage.open(configPath)) {
+        configContent = new String(FileIOUtils.readAsByteArray(inputStream));
+      } catch (FileNotFoundException e) {
+        return String.format("Lock Audit Status: DISABLED\n"
+            + "Table: %s\n"
+            + "Config file: %s (not found)\n"
+            + "Use 'locks audit enable' to enable audit logging.",
+            HoodieCLI.basePath, auditConfigPath);
+      }
+      JsonNode rootNode = OBJECT_MAPPER.readTree(configContent);
+      JsonNode enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD);
+      boolean isEnabled = enabledNode != null && enabledNode.asBoolean(false);
+      
+      String status = isEnabled ? "ENABLED" : "DISABLED";
+      
+      return String.format("Lock Audit Status: %s\n"
+          + "Table: %s\n"
+          + "Config file: %s\n"
+          + "Audit files location: %s",
+          status, HoodieCLI.basePath, auditConfigPath, StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath));
+      
+    } catch (Exception e) {
+      LOG.error("Error checking lock audit status", e);
+      return String.format("Failed to check lock audit status: %s", e.getMessage());
+    }
+  }
+
+  /**
+   * Validates the audit lock files for consistency and integrity.
+   * This command checks for issues such as corrupted files, invalid format,
+   * incorrect state transitions, and orphaned locks.
+   * 
+   * @return Validation results including any issues found
+   */
+  @ShellMethod(key = "locks audit validate", value = "Validate audit lock files for consistency and integrity")
+  public String validateAuditLocks() {
+    
+    if (HoodieCLI.basePath == null) {
+      return "No Hudi table loaded. Please connect to a table first.";
+    }
+
+    try {
+      String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+      StoragePath auditFolder = new StoragePath(auditFolderPath);
+
+      // Get all audit files
+      List<StoragePathInfo> allFiles;
+      try {
+        allFiles = HoodieCLI.storage.listDirectEntries(auditFolder);
+      } catch (FileNotFoundException e) {
+        return "Validation Result: PASSED\n"
+            + "Transactions Validated: 0\n"
+            + "Issues Found: 0\n"
+            + "Details: No audit folder found - nothing to validate";
+      }
+      List<StoragePathInfo> auditFiles = new ArrayList<>();
+      for (StoragePathInfo pathInfo : allFiles) {
+        if (pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl")) {
+          auditFiles.add(pathInfo);
+        }
+      }
+      
+      if (auditFiles.isEmpty()) {
+        return "Validation Result: PASSED\n"
+            + "Transactions Validated: 0\n"
+            + "Issues Found: 0\n"
+            + "Details: No audit files found - nothing to validate";
+      }
+      
+      // Parse all audit files into transaction windows
+      List<TransactionWindow> windows = new ArrayList<>();
+      List<String> parseErrors = new ArrayList<>();
+      for (StoragePathInfo pathInfo : auditFiles) {
+        Option<TransactionWindow> window = parseAuditFile(pathInfo);
+        if (window.isPresent()) {
+          windows.add(window.get());
+        } else {
+          parseErrors.add(String.format("[ERROR] Failed to parse audit file: %s", pathInfo.getPath().getName()));
+        }
+      }
+
+      if (windows.isEmpty()) {
+        return String.format("Validation Result: FAILED\n"
+            + "Transactions Validated: 0\n"
+            + "Issues Found: %d\n"
+            + "Details: Failed to parse any audit files", auditFiles.size());
+      }
+
+      // Validate transactions
+      ValidationResults validationResults = validateTransactionWindows(windows);
+
+      // Add parse errors to the validation results
+      validationResults.errors.addAll(parseErrors);
+      
+      // Generate result
+      int totalIssues = validationResults.errors.size() + validationResults.warnings.size();
+      String result;
+      String details;
+      
+      if (totalIssues == 0) {
+        result = "PASSED";
+        details = "All audit lock transactions validated successfully";
+      } else {
+        result = validationResults.errors.isEmpty() ? "WARNING" : "FAILED";
+        List<String> allIssues = new ArrayList<>();
+        allIssues.addAll(validationResults.errors);
+        allIssues.addAll(validationResults.warnings);
+        details = String.join(", ", allIssues);
+      }
+      
+      return String.format("Validation Result: %s\n"
+          + "Audit Files: %d total, %d parsed successfully, %d failed to parse\n"
+          + "Transactions Validated: %d\n"
+          + "Issues Found: %d\n"
+          + "Details: %s", result, auditFiles.size(), windows.size(), parseErrors.size(), windows.size(), totalIssues, details);
+      
+    } catch (Exception e) {
+      LOG.error("Error validating audit locks", e);
+      return String.format("Validation Result: ERROR\n"
+          + "Transactions Validated: 0\n"
+          + "Issues Found: -1\n"
+          + "Details: Validation failed: %s", e.getMessage());
+    }
+  }
+
+  /**
+   * Cleans up old audit lock files based on age threshold.
+   * This command removes audit files that are older than the specified number of days.
+   * 
+   * @param dryRun Whether to perform a dry run (preview changes without deletion)
+   * @param ageDays Number of days to keep audit files (default 7)
+   * @return Status message indicating files cleaned or to be cleaned
+   */
+  @ShellMethod(key = "locks audit cleanup", value = "Clean up old audit lock files")
+  public String cleanupAuditLocks(
+      @ShellOption(value = {"--dryRun"}, defaultValue = "false",
+          help = "Preview changes without actually deleting files") final boolean dryRun,
+      @ShellOption(value = {"--ageDays"}, defaultValue = "7",
+          help = "Delete audit files older than this many days") final String ageDaysStr) {
+
+    try {
+      if (HoodieCLI.basePath == null) {
+        return "No Hudi table loaded. Please connect to a table first.";
+      }
+
+      // Parse ageDays manually to handle validation properly
+      int ageDays;
+      try {
+        ageDays = Integer.parseInt(ageDaysStr);
+      } catch (NumberFormatException e) {
+        return "Error: ageDays must be a value greater than 0.";
+      }
+
+      if (ageDays < 0) {
+        return "Error: ageDays must be non-negative (>= 0).";
+      }
+
+      return performAuditCleanup(dryRun, ageDays).toString();
+    } catch (Exception e) {
+      LOG.error("Error during audit cleanup", e);
+      return String.format("Error during cleanup: %s", e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
+    }
+  }
+
+  /**
+   * Internal method to perform audit cleanup. Used by both the CLI command and disable method.
+   *
+   * @param dryRun Whether to perform a dry run (preview changes without deletion)
+   * @param ageDays Number of days to keep audit files (0 means delete all, must be non-negative)
+   * @return CleanupResult containing cleanup operation details
+   */
+  private CleanupResult performAuditCleanup(boolean dryRun, int ageDays) {
+    try {
+      if (HoodieCLI.storage == null) {
+        String message = "Storage not initialized.";
+        return new CleanupResult(false, 0, 0, 0, message, dryRun);
+      }
+
+      String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+      StoragePath auditFolder = new StoragePath(auditFolderPath);
+
+      // Calculate cutoff timestamp (ageDays ago)
+      long cutoffTime = System.currentTimeMillis() - (ageDays * 24L * 60L * 60L * 1000L);
+
+      // List all files in audit folder and filter by modification time
+      List<StoragePathInfo> allFiles;
+      try {
+        allFiles = HoodieCLI.storage.listDirectEntries(auditFolder);
+      } catch (FileNotFoundException e) {
+        String message = "No audit folder found - nothing to cleanup.";
+        return new CleanupResult(true, 0, 0, 0, message, dryRun);
+      }
+      List<StoragePathInfo> auditFiles = new ArrayList<>();
+      List<StoragePathInfo> oldFiles = new ArrayList<>();
+      
+      // Filter to get only .jsonl files
+      for (StoragePathInfo pathInfo : allFiles) {
+        if (pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl")) {
+          auditFiles.add(pathInfo);
+          if (pathInfo.getModificationTime() < cutoffTime) {
+            oldFiles.add(pathInfo);
+          }
+        }
+      }
+
+      if (oldFiles.isEmpty()) {
+        String message = String.format("No audit files older than %d days found.", ageDays);
+        return new CleanupResult(true, 0, 0, auditFiles.size(), message, dryRun);
+      }
+
+      int fileCount = oldFiles.size();
+
+      if (dryRun) {
+        String message = String.format("Dry run: Would delete %d audit files older than %d days.", fileCount, ageDays);
+        return new CleanupResult(true, 0, 0, fileCount, message, dryRun);
+      } else {
+        // Actually delete the files
+        int deletedCount = 0;
+        int failedCount = 0;
+
+        for (StoragePathInfo pathInfo : oldFiles) {
+          try {
+            HoodieCLI.storage.deleteFile(pathInfo.getPath());
+            deletedCount++;
+          } catch (Exception e) {
+            failedCount++;
+            LOG.warn("Failed to delete audit file: " + pathInfo.getPath(), e);
+          }
+        }
+        
+        if (failedCount == 0) {
+          String message = String.format("Successfully deleted %d audit files older than %d days.", deletedCount, ageDays);
+          return new CleanupResult(true, deletedCount, failedCount, fileCount, message, dryRun);
+        } else {
+          String message = String.format("Deleted %d audit files, failed to delete %d files.", deletedCount, failedCount);
+          return new CleanupResult(false, deletedCount, failedCount, fileCount, message, dryRun);
+        }
+      }
+      
+    } catch (Exception e) {
+      LOG.error("Error cleaning up audit locks", e);
+      String message = String.format("Failed to cleanup audit locks: %s", e.getMessage());
+      return new CleanupResult(false, 0, 0, 0, message, dryRun);
+    }
+  }
+
+  /**
+   * Parses an audit file and extracts transaction window information.
+   */
+  private Option<TransactionWindow> parseAuditFile(StoragePathInfo pathInfo) {
+    String filename = pathInfo.getPath().getName();
+
+    try {
+      // Read and parse JSONL content
+      List<AuditRecord> entries = new ArrayList<>();
+      try (InputStream inputStream = HoodieCLI.storage.open(pathInfo.getPath());
+           BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+        String line;
+        while ((line = reader.readLine()) != null) {
+          if (line.trim().isEmpty()) {
+            continue;
+          }
+          try {
+            AuditRecord entry = OBJECT_MAPPER.readValue(line, AuditRecord.class);
+            entries.add(entry);
+          } catch (Exception e) {
+            LOG.warn("Failed to parse JSON line in file " + filename + ": " + line, e);
+          }
+        }
+      }
+
+      if (entries.isEmpty()) {
+        return Option.empty();
+      }
+
+      // Extract transaction metadata from first entry
+      AuditRecord firstEntry = entries.get(0);
+      String ownerId = firstEntry.getOwnerId();
+      long transactionCreationTime = firstEntry.getTransactionStartTime();
+
+      // Find first START timestamp
+      long lockAcquisitionTime = transactionCreationTime; // default to transaction creation time
+      for (AuditRecord entry : entries) {
+        if ("START".equals(entry.getState())) {
+          lockAcquisitionTime = entry.getTimestamp();
+          break;
+        }
+      }
+
+      // Find last END timestamp
+      Option<Long> lockReleaseTime = Option.empty();
+      for (int i = entries.size() - 1; i >= 0; i--) {
+        AuditRecord entry = entries.get(i);
+        if ("END".equals(entry.getState())) {
+          lockReleaseTime = Option.of(entry.getTimestamp());
+          break;
+        }
+      }
+
+      // Find last expiration time as fallback
+      Option<Long> lockExpirationTime = Option.empty();
+      if (!entries.isEmpty()) {
+        AuditRecord lastEntry = entries.get(entries.size() - 1);
+        lockExpirationTime = Option.of(lastEntry.getLockExpiration());
+      }
+
+      return Option.of(new TransactionWindow(
+          ownerId,
+          transactionCreationTime,
+          lockAcquisitionTime,
+          lockReleaseTime,
+          lockExpirationTime,
+          filename
+      ));
+    } catch (Exception e) {
+      LOG.warn("Failed to parse audit file: " + filename, e);
+      return Option.empty();
+    }
+  }
+
+  /**
+   * Validates transaction windows for overlaps and proper closure.
+   */
+  private ValidationResults validateTransactionWindows(List<TransactionWindow> windows) {
+    List<String> errors = new ArrayList<>();
+    List<String> warnings = new ArrayList<>();
+    
+    // Check for transactions without proper END
+    for (TransactionWindow window : windows) {
+      if (!window.lockReleaseTime.isPresent()) {
+        warnings.add(String.format("[WARNING] %s => transaction did not end gracefully. This could be due to driver OOM or non-graceful shutdown.",
+            window.filename));
+      }
+    }
+
+    // Sort windows by start time for overlap detection
+    List<TransactionWindow> sortedWindows = new ArrayList<>(windows);
+    sortedWindows.sort(Comparator.comparingLong(w -> w.lockAcquisitionTime));
+
+    // Check for overlaps
+    for (int i = 0; i < sortedWindows.size(); i++) {
+      TransactionWindow currentWindow = sortedWindows.get(i);
+      long currentEnd = currentWindow.getEffectiveEndTime();
+
+      // Check all subsequent windows for overlaps
+      for (int j = i + 1; j < sortedWindows.size(); j++) {
+        TransactionWindow otherWindow = sortedWindows.get(j);
+        long otherStart = otherWindow.lockAcquisitionTime;
+
+        // Check if windows overlap
+        if (otherStart < currentEnd) {
+          errors.add(String.format("[ERROR] %s => overlaps with %s",
+              currentWindow.filename, otherWindow.filename));
+        }
+      }
+    }
+    
+    return new ValidationResults(errors, warnings);
+  }
+}
\ No newline at end of file
diff --git a/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestLockAuditingCommand.java b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestLockAuditingCommand.java
new file mode 100644
index 000000000000..a8987ba96a8f
--- /dev/null
+++ b/hudi-cli/src/test/java/org/apache/hudi/cli/commands/TestLockAuditingCommand.java
@@ -0,0 +1,943 @@
+/*
+ * 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.hudi.cli.commands;
+
+import org.apache.hudi.cli.HoodieCLI;
+import org.apache.hudi.cli.functional.CLIFunctionalTestHarness;
+import org.apache.hudi.cli.testutils.ShellEvaluationResultUtil;
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.model.HoodieTableType;
+import org.apache.hudi.common.util.FileIOUtils;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.StoragePathInfo;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.shell.Shell;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+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;
+
+/**
+ * Test class for {@link org.apache.hudi.cli.commands.LockAuditingCommand}.
+ */
+@Tag("functional")
+@SpringBootTest(properties = {"spring.shell.interactive.enabled=false", "spring.shell.command.script.enabled=false"})
+public class TestLockAuditingCommand extends CLIFunctionalTestHarness {
+
+  @Autowired
+  private Shell shell;
+
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  /**
+   * Helper method to create AuditRecord for tests with lockHeld defaulting to true.
+   */
+  private static LockAuditingCommand.AuditRecord createAuditRecord(
+      String ownerId, long transactionStartTime, long timestamp, String state, long lockExpiration) {
+    return new LockAuditingCommand.AuditRecord(ownerId, transactionStartTime, timestamp, state, lockExpiration, true);
+  }
+
+  /**
+   * Represents a transaction scenario with its audit records
+   */
+  static class TransactionScenario {
+    final String filename; // e.g., "1234567890_owner1.jsonl"
+    final List<LockAuditingCommand.AuditRecord> records;
+
+    TransactionScenario(String filename, List<LockAuditingCommand.AuditRecord> records) {
+      this.filename = filename;
+      this.records = records;
+    }
+  }
+
+  /**
+   * Helper method to create audit files from scenarios
+   */
+  private void createAuditFiles(List<TransactionScenario> scenarios) throws IOException {
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditDir = new StoragePath(auditFolderPath);
+
+    // Create audit directory if it doesn't exist
+    if (!HoodieCLI.storage.exists(auditDir)) {
+      HoodieCLI.storage.createDirectory(auditDir);
+    }
+
+    for (TransactionScenario scenario : scenarios) {
+      StoragePath filePath = new StoragePath(auditDir, scenario.filename);
+      StringBuilder jsonLines = new StringBuilder();
+      for (LockAuditingCommand.AuditRecord record : scenario.records) {
+        if (jsonLines.length() > 0) {
+          jsonLines.append("\n");
+        }
+        jsonLines.append(OBJECT_MAPPER.writeValueAsString(record));
+      }
+
+      try (OutputStream outputStream = HoodieCLI.storage.create(filePath, true)) {
+        outputStream.write(jsonLines.toString().getBytes());
+      }
+    }
+  }
+
+  @BeforeEach
+  public void setUp() throws Exception {
+    HoodieCLI.conf = storageConf();
+    String tableName = tableName();
+    String tablePath = tablePath(tableName);
+    HoodieCLI.basePath = tablePath;
+    
+    // Initialize table
+    HoodieTableMetaClient.withPropertyBuilder()
+        .setTableType(HoodieTableType.COPY_ON_WRITE.name())
+        .setTableName(tableName)
+        .setRecordKeyFields("key")
+        .initTable(storageConf(), tablePath);
+        
+    // Initialize storage
+    HoodieCLI.initFS(true);
+  }
+
+  /**
+   * Test enabling lock audit when no table is loaded.
+   */
+  @Test
+  public void testEnableLockAuditNoTable() {
+    // Clear the base path to simulate no table loaded
+    String originalBasePath = HoodieCLI.basePath;
+    HoodieCLI.basePath = null;
+
+    try {
+      Object result = shell.evaluate(() -> "locks audit enable");
+      assertAll("Command runs with no table loaded",
+          () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+          () -> assertNotNull(result.toString()),
+          () -> assertEquals("No Hudi table loaded. Please connect to a table first.", result.toString()));
+    } finally {
+      HoodieCLI.basePath = originalBasePath;
+    }
+  }
+
+  /**
+   * Test disabling lock audit when no table is loaded.
+   */
+  @Test
+  public void testDisableLockAuditNoTable() {
+    // Clear the base path to simulate no table loaded
+    String originalBasePath = HoodieCLI.basePath;
+    HoodieCLI.basePath = null;
+
+    try {
+      Object result = shell.evaluate(() -> "locks audit disable");
+      assertAll("Command runs with no table loaded",
+          () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+          () -> assertNotNull(result.toString()),
+          () -> assertEquals("No Hudi table loaded. Please connect to a table first.", result.toString()));
+    } finally {
+      HoodieCLI.basePath = originalBasePath;
+    }
+  }
+
+  /**
+   * Test showing lock audit status when no table is loaded.
+   */
+  @Test
+  public void testShowLockAuditStatusNoTable() {
+    // Clear the base path to simulate no table loaded
+    String originalBasePath = HoodieCLI.basePath;
+    HoodieCLI.basePath = null;
+
+    try {
+      Object result = shell.evaluate(() -> "locks audit status");
+      assertAll("Command runs with no table loaded",
+          () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+          () -> assertNotNull(result.toString()),
+          () -> assertEquals("No Hudi table loaded. Please connect to a table first.", result.toString()));
+    } finally {
+      HoodieCLI.basePath = originalBasePath;
+    }
+  }
+
+  /**
+   * Test enabling lock audit successfully.
+   */
+  @Test
+  public void testEnableLockAuditSuccess() throws Exception {
+    Object result = shell.evaluate(() -> "locks audit enable");
+    
+    assertAll("Enable command runs successfully",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock audit enabled successfully")));
+
+    // Verify the config file was created with correct content
+    String lockFolderPath = String.format("%s%s.hoodie%s.locks", HoodieCLI.basePath, StoragePath.SEPARATOR, StoragePath.SEPARATOR);
+    String auditConfigPath = String.format("%s%s%s", lockFolderPath, StoragePath.SEPARATOR, StorageLockProviderAuditService.AUDIT_CONFIG_FILE_NAME);
+    StoragePath configPath = new StoragePath(auditConfigPath);
+    
+    assertTrue(HoodieCLI.storage.exists(configPath), "Config file should exist");
+    
+    String configContent;
+    try (InputStream inputStream = HoodieCLI.storage.open(configPath)) {
+      configContent = new String(FileIOUtils.readAsByteArray(inputStream));
+    }
+    JsonNode rootNode = OBJECT_MAPPER.readTree(configContent);
+    JsonNode enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD);
+    
+    assertNotNull(enabledNode, "Config should contain enabled field");
+    assertTrue(enabledNode.asBoolean(), "Audit should be enabled");
+  }
+
+  /**
+   * Test showing lock audit status when disabled (no config file).
+   */
+  @Test
+  public void testShowLockAuditStatusDisabled() {
+    Object result = shell.evaluate(() -> "locks audit status");
+    
+    assertAll("Status command shows disabled state",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock Audit Status: DISABLED")),
+        () -> assertTrue(result.toString().contains("(not found)")));
+  }
+
+  /**
+   * Test the complete workflow: enable, check status, disable, check status.
+   */
+  @Test
+  public void testCompleteAuditWorkflow() throws Exception {
+    // 1. Enable audit
+    Object enableResult = shell.evaluate(() -> "locks audit enable");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(enableResult));
+    assertTrue(enableResult.toString().contains("Lock audit enabled successfully"));
+
+    // 2. Check status - should be enabled
+    Object statusEnabledResult = shell.evaluate(() -> "locks audit status");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(statusEnabledResult));
+    assertTrue(statusEnabledResult.toString().contains("Lock Audit Status: ENABLED"));
+
+    // 3. Disable audit with default keepAuditFiles=true
+    Object disableResult = shell.evaluate(() -> "locks audit disable");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(disableResult));
+    assertTrue(disableResult.toString().contains("Lock audit disabled successfully"));
+    assertTrue(disableResult.toString().contains("Existing audit files preserved"));
+
+    // 4. Check status - should be disabled
+    Object statusDisabledResult = shell.evaluate(() -> "locks audit status");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(statusDisabledResult));
+    assertTrue(statusDisabledResult.toString().contains("Lock Audit Status: DISABLED"));
+
+    // Verify the config file still exists but with audit disabled
+    String lockFolderPath = String.format("%s%s.hoodie%s.locks", HoodieCLI.basePath, StoragePath.SEPARATOR, StoragePath.SEPARATOR);
+    String auditConfigPath = String.format("%s%s%s", lockFolderPath, StoragePath.SEPARATOR, StorageLockProviderAuditService.AUDIT_CONFIG_FILE_NAME);
+    StoragePath configPath = new StoragePath(auditConfigPath);
+    
+    assertTrue(HoodieCLI.storage.exists(configPath), "Config file should still exist");
+    
+    String configContent;
+    try (InputStream inputStream = HoodieCLI.storage.open(configPath)) {
+      configContent = new String(FileIOUtils.readAsByteArray(inputStream));
+    }
+    JsonNode rootNode = OBJECT_MAPPER.readTree(configContent);
+    JsonNode enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD);
+    
+    assertNotNull(enabledNode, "Config should contain enabled field");
+    assertFalse(enabledNode.asBoolean(), "Audit should be disabled");
+  }
+
+  /**
+   * Test disabling audit when no config file exists.
+   */
+  @Test
+  public void testDisableLockAuditNoConfigFile() {
+    Object result = shell.evaluate(() -> "locks audit disable");
+    
+    assertAll("Disable command handles missing config file",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertEquals("Lock audit is already disabled (no configuration file found).", result.toString()));
+  }
+
+  /**
+   * Test disabling audit with keepAuditFiles=false option.
+   */
+  @Test
+  public void testDisableLockAuditWithoutKeepingFiles() throws Exception {
+    // First enable audit
+    shell.evaluate(() -> "locks audit enable");
+
+    // Create some audit files to be cleaned up
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records));
+    createAuditFiles(scenarios);
+
+    // Disable with keepAuditFiles=false
+    Object result = shell.evaluate(() -> "locks audit disable --keepAuditFiles false");
+    
+    assertAll("Disable command with keepAuditFiles=false",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock audit disabled successfully")),
+        () -> assertTrue(result.toString().contains("Audit files cleaned up at:")
+                        || result.toString().contains("Some audit files may not have been cleaned up")));
+  }
+
+  /**
+   * Test that disable with keepAuditFiles=false actually cleans up files.
+   */
+  @Test
+  public void testDisableLockAuditCleansUpFiles() throws Exception {
+    // First enable audit
+    shell.evaluate(() -> "locks audit enable");
+
+    // Create some audit files
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records));
+    createAuditFiles(scenarios);
+
+    // Verify files exist before disable
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditFolder = new StoragePath(auditFolderPath);
+    List<StoragePathInfo> filesBefore = HoodieCLI.storage.listDirectEntries(auditFolder);
+    long jsonlFilesBefore = filesBefore.stream()
+        .filter(pathInfo -> pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl"))
+        .count();
+    assertTrue(jsonlFilesBefore > 0, "Should have audit files before disable");
+
+    // Disable with keepAuditFiles=false - this should trigger cleanup
+    Object result = shell.evaluate(() -> "locks audit disable --keepAuditFiles false");
+    
+    assertAll("Cleanup triggered by disable",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock audit disabled successfully")));
+  }
+
+  /**
+   * Test disable with keepAuditFiles=false when no audit files exist.
+   */
+  @Test
+  public void testDisableLockAuditWithoutKeepingFilesNoFiles() {
+    // First enable audit but don't create any audit files
+    shell.evaluate(() -> "locks audit enable");
+
+    // Disable with keepAuditFiles=false when no files exist
+    Object result = shell.evaluate(() -> "locks audit disable --keepAuditFiles false");
+    
+    assertAll("Disable with cleanup when no files exist",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock audit disabled successfully")),
+        // Should handle the case where no files exist to clean up
+        () -> assertTrue(result.toString().contains("Audit files cleaned up at:")
+                        || result.toString().contains("No audit files")
+                        || result.toString().contains("nothing to cleanup")));
+  }
+
+  /**
+   * Test enabling audit multiple times (should overwrite).
+   */
+  @Test
+  public void testEnableLockAuditMultipleTimes() throws Exception {
+    // Enable first time
+    Object result1 = shell.evaluate(() -> "locks audit enable");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result1));
+    assertTrue(result1.toString().contains("Lock audit enabled successfully"));
+
+    // Enable second time (should succeed and overwrite)
+    Object result2 = shell.evaluate(() -> "locks audit enable");
+    assertTrue(ShellEvaluationResultUtil.isSuccess(result2));
+    assertTrue(result2.toString().contains("Lock audit enabled successfully"));
+
+    // Verify config is still correct
+    String lockFolderPath = String.format("%s%s.hoodie%s.locks", HoodieCLI.basePath, StoragePath.SEPARATOR, StoragePath.SEPARATOR);
+    String auditConfigPath = String.format("%s%s%s", lockFolderPath, StoragePath.SEPARATOR, StorageLockProviderAuditService.AUDIT_CONFIG_FILE_NAME);
+    StoragePath configPath = new StoragePath(auditConfigPath);
+    
+    String configContent;
+    try (InputStream inputStream = HoodieCLI.storage.open(configPath)) {
+      configContent = new String(FileIOUtils.readAsByteArray(inputStream));
+    }
+    JsonNode rootNode = OBJECT_MAPPER.readTree(configContent);
+    JsonNode enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD);
+    
+    assertTrue(enabledNode.asBoolean(), "Audit should still be enabled");
+  }
+
+  // ==================== Validation Tests ====================
+
+  /**
+   * Test validation when no audit folder exists.
+   */
+  @Test
+  public void testValidateAuditLocksNoAuditFolder() {
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Validation handles missing audit folder",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: PASSED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 0")),
+        () -> assertTrue(result.toString().contains("Issues Found: 0")),
+        () -> assertTrue(result.toString().contains("No audit folder found")));
+  }
+
+  /**
+   * Test validation when audit folder exists but no audit files.
+   */
+  @Test
+  public void testValidateAuditLocksNoAuditFiles() throws IOException {
+    // Create audit folder but no files
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditDir = new StoragePath(auditFolderPath);
+    HoodieCLI.storage.createDirectory(auditDir);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Validation handles no audit files",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: PASSED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 0")),
+        () -> assertTrue(result.toString().contains("Issues Found: 0")),
+        () -> assertTrue(result.toString().contains("No audit files found")));
+  }
+
+  /**
+   * Test validation - No Issues (PASSED)
+   */
+  @Test
+  public void testValidateAuditLocksNoIssues() throws IOException {
+    long baseTime = System.currentTimeMillis();
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // Transaction 1: Complete transaction
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 100, "START", baseTime + 60000));
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 200, "RENEW", baseTime + 60000));
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 300, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records1));
+    
+    // Transaction 2: Complete transaction starting after first one ends
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner2", baseTime + 500, baseTime + 600, "START", baseTime + 60000));
+    records2.add(createAuditRecord("owner2", baseTime + 500, baseTime + 700, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario((baseTime + 500) + "_owner2.jsonl", records2));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Validation passes with no issues",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: PASSED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 2")),
+        () -> assertTrue(result.toString().contains("Issues Found: 0")),
+        () -> assertTrue(result.toString().contains("successfully")));
+  }
+
+  /**
+   * Test validation - Single Unclosed Transaction (WARNING)
+   */
+  @Test
+  public void testValidateAuditLocksSingleUnclosedTransaction() throws IOException {
+    long baseTime = 1000000L;
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // Single audit file without END record
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 100, "START", baseTime + 200));
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records1));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Single unclosed transaction shows warning",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: WARNING")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 1")),
+        () -> assertTrue(result.toString().contains("Issues Found: 1")));
+  }
+
+  /**
+   * Test validation - Unclosed Transactions (WARNING)
+   */
+  @Test
+  public void testValidateAuditLocksUnclosedTransactions() throws IOException {
+    long baseTime = 1000000L;
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // Transaction 1: Unclosed (effective end at expiration = baseTime + 200)
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 100, "START", baseTime + 200));
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records1));
+    
+    // Transaction 2: Complete, starts after owner1's expiration
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner2", baseTime + 300, baseTime + 400, "START", baseTime + 60000));
+    records2.add(createAuditRecord("owner2", baseTime + 300, baseTime + 500, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario((baseTime + 300) + "_owner2.jsonl", records2));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Unclosed transactions show warning",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: WARNING")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 2")),
+        () -> assertTrue(result.toString().contains("Issues Found: 1")),
+        () -> assertTrue(result.toString().contains("[WARNING]")),
+        () -> assertTrue(result.toString().contains("owner1.jsonl")),
+        () -> assertTrue(result.toString().contains("did not end gracefully")));
+  }
+
+  /**
+   * Test validation - Overlapping Transactions (FAILED)
+   */
+  @Test
+  public void testValidateAuditLocksOverlappingTransactions() throws IOException {
+    long baseTime = System.currentTimeMillis();
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // Transaction 1: Ends after owner2 starts (overlapping)
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 100, "START", baseTime + 60000));
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 500, "END", baseTime + 60000)); // Ends after owner2 starts
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records1));
+    
+    // Transaction 2: Starts before owner1 ends (overlapping)
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner2", baseTime + 200, baseTime + 300, "START", baseTime + 60000)); // Starts before owner1 ends
+    records2.add(createAuditRecord("owner2", baseTime + 200, baseTime + 400, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario((baseTime + 200) + "_owner2.jsonl", records2));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Overlapping transactions show failure",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: FAILED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 2")),
+        () -> assertTrue(result.toString().contains("Issues Found: 1")),
+        () -> assertTrue(result.toString().contains("[ERROR]")),
+        () -> assertTrue(result.toString().contains("owner1.jsonl")),
+        () -> assertTrue(result.toString().contains("overlaps with")),
+        () -> assertTrue(result.toString().contains("owner2.jsonl")));
+  }
+
+  /**
+   * Test validation - Mixed Issues (FAILED)
+   */
+  @Test
+  public void testValidateAuditLocksMixedIssues() throws IOException {
+    long baseTime = System.currentTimeMillis();
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // Transaction 1: Unclosed transaction
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", baseTime, baseTime + 100, "START", baseTime + 60000));
+    // No END - unclosed
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records1));
+    
+    // Transaction 2: Overlaps with owner1
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner2", baseTime + 50, baseTime + 150, "START", baseTime + 60000)); // Overlaps with owner1
+    records2.add(createAuditRecord("owner2", baseTime + 50, baseTime + 250, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario((baseTime + 50) + "_owner2.jsonl", records2));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Mixed issues show failure",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: FAILED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 2")),
+        () -> assertTrue(result.toString().contains("Issues Found: 2")),
+        () -> assertTrue(result.toString().contains("[ERROR]")),
+        () -> assertTrue(result.toString().contains("[WARNING]")),
+        () -> assertTrue(result.toString().contains("overlaps with")),
+        () -> assertTrue(result.toString().contains("did not end gracefully")));
+  }
+
+  /**
+   * Test validation - Out of Order Filenames but Valid Transactions (PASSED)
+   */
+  @Test
+  public void testValidateAuditLocksOutOfOrderFilenames() throws IOException {
+    long baseTime = System.currentTimeMillis();
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // File with later timestamp in name but contains earlier transaction
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner2", baseTime + 100, baseTime + 200, "START", baseTime + 60000)); // Actually starts first
+    records1.add(createAuditRecord("owner2", baseTime + 100, baseTime + 300, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario((baseTime + 2000) + "_owner2.jsonl", records1)); // Filename suggests later time
+    
+    // File with earlier timestamp in name but contains later transaction
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner1", baseTime + 500, baseTime + 600, "START", baseTime + 60000)); // Actually starts second
+    records2.add(createAuditRecord("owner1", baseTime + 500, baseTime + 700, "END", baseTime + 60000));
+    scenarios.add(new TransactionScenario(baseTime + "_owner1.jsonl", records2)); // Filename suggests earlier time
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Out of order filenames but valid transactions pass",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: PASSED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 2")),
+        () -> assertTrue(result.toString().contains("Issues Found: 0")),
+        () -> assertTrue(result.toString().contains("successfully")));
+  }
+
+  /**
+   * Test validation when no table is loaded.
+   */
+  @Test
+  public void testValidateAuditLocksNoTable() {
+    // Clear the base path to simulate no table loaded
+    String originalBasePath = HoodieCLI.basePath;
+    HoodieCLI.basePath = null;
+
+    try {
+      Object result = shell.evaluate(() -> "locks audit validate");
+      assertAll("Validation handles no table loaded",
+          () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+          () -> assertNotNull(result.toString()),
+          () -> assertEquals("No Hudi table loaded. Please connect to a table first.", result.toString()));
+    } finally {
+      HoodieCLI.basePath = originalBasePath;
+    }
+  }
+
+  /**
+   * Test validation with malformed audit files.
+   */
+  @Test
+  public void testValidateAuditLocksMalformedFiles() throws IOException {
+    // Create audit directory
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditDir = new StoragePath(auditFolderPath);
+    HoodieCLI.storage.createDirectory(auditDir);
+    
+    // Create a malformed audit file
+    StoragePath filePath = new StoragePath(auditDir, "malformed_file.jsonl");
+    try (OutputStream outputStream = HoodieCLI.storage.create(filePath, true)) {
+      outputStream.write("invalid json content".getBytes());
+    }
+    
+    Object result = shell.evaluate(() -> "locks audit validate");
+    
+    assertAll("Validation handles malformed files",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Validation Result: FAILED")),
+        () -> assertTrue(result.toString().contains("Transactions Validated: 0")),
+        () -> assertTrue(result.toString().contains("Failed to parse any audit files")));
+  }
+
+  // ==================== Cleanup Tests ====================
+
+  /**
+   * Test cleanup when no audit folder exists.
+   */
+  @Test
+  public void testCleanupAuditLocksNoAuditFolder() {
+    Object result = shell.evaluate(() -> "locks audit cleanup");
+    
+    assertAll("Cleanup handles missing audit folder",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertEquals("No audit folder found - nothing to cleanup.", result.toString()));
+  }
+
+  /**
+   * Test cleanup when audit folder exists but no audit files.
+   */
+  @Test
+  public void testCleanupAuditLocksNoAuditFiles() throws IOException {
+    // Create audit folder but no files
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditDir = new StoragePath(auditFolderPath);
+    HoodieCLI.storage.createDirectory(auditDir);
+    
+    Object result = shell.evaluate(() -> "locks audit cleanup");
+    
+    assertAll("Cleanup handles no audit files",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("No audit files older than 7 days found")));
+  }
+
+  /**
+   * Test cleanup with dry run - test basic functionality.
+   */
+  @Test
+  public void testCleanupAuditLocksDryRun() throws IOException {
+    // Create some audit files (they will have current modification time)
+    long oldTime = System.currentTimeMillis() - (10 * 24 * 60 * 60 * 1000L); // 10 days ago
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", oldTime, oldTime + 100, "START", oldTime + 60000));
+    records.add(createAuditRecord("owner1", oldTime, oldTime + 200, "END", oldTime + 60000));
+    scenarios.add(new TransactionScenario(oldTime + "_owner1.jsonl", records));
+    
+    createAuditFiles(scenarios);
+    
+    // Test default cleanup with dry run
+    Object result = shell.evaluate(() -> "locks audit cleanup --dryRun true");
+    
+    assertAll("Dry run executes successfully",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        // Since files are created with current time, they should be too recent to delete
+        () -> assertTrue(result.toString().contains("No audit files older than") 
+                        || result.toString().contains("Dry run: Would delete")));
+    
+    // Verify files still exist
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditFolder = new StoragePath(auditFolderPath);
+    List<StoragePathInfo> filesAfter = HoodieCLI.storage.listDirectEntries(auditFolder);
+    long jsonlFiles = filesAfter.stream()
+        .filter(pathInfo -> pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl"))
+        .count();
+    assertEquals(1, jsonlFiles, "Files should still exist after dry run");
+  }
+
+  /**
+   * Test cleanup with custom age threshold.
+   */
+  @Test
+  public void testCleanupAuditLocksCustomAge() throws IOException {
+    // Create some audit files
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records));
+    
+    createAuditFiles(scenarios);
+    
+    // Test with custom age threshold - files will be recent so won't be deleted
+    Object result = shell.evaluate(() -> "locks audit cleanup --ageDays 30 --dryRun true");
+    
+    assertAll("Custom age threshold works",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("No audit files older than 30 days found")));
+  }
+
+  /**
+   * Test actual cleanup (not dry run).
+   */
+  @Test
+  public void testCleanupAuditLocksActualDelete() throws IOException {
+    // Create some audit files
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records));
+    
+    createAuditFiles(scenarios);
+    
+    // Test actual cleanup - files will be recent so shouldn't be deleted
+    Object result = shell.evaluate(() -> "locks audit cleanup --dryRun false");
+    
+    assertAll("Actual cleanup executes successfully",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        // Recent files shouldn't be deleted
+        () -> assertTrue(result.toString().contains("No audit files older than 7 days found")));
+  }
+
+  /**
+   * Test cleanup with recent files (should not delete).
+   */
+  @Test
+  public void testCleanupAuditLocksRecentFiles() throws IOException {
+    // Create recent audit files
+    long recentTime = System.currentTimeMillis() - (2 * 24 * 60 * 60 * 1000L); // 2 days ago
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", recentTime, recentTime + 100, "START", recentTime + 60000));
+    records.add(createAuditRecord("owner1", recentTime, recentTime + 200, "END", recentTime + 60000));
+    scenarios.add(new TransactionScenario(recentTime + "_owner1.jsonl", records));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit cleanup");
+    
+    assertAll("Recent files are not deleted",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("No audit files older than 7 days found")));
+  }
+
+  /**
+   * Test cleanup parameter validation - invalid age days.
+   */
+  @Test
+  public void testCleanupAuditLocksInvalidAgeDays() {
+    // Test with negative ageDays (Spring Shell treats this as invalid integer format)
+    Object resultNegative = shell.evaluate(() -> "locks audit cleanup --ageDays -1");
+
+    assertAll("Negative ageDays should be rejected",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(resultNegative)),
+        () -> assertNotNull(resultNegative.toString()),
+        () -> assertEquals("Error: ageDays must be a value greater than 0.", resultNegative.toString()));
+
+    // Test with invalid string to verify our parsing validation
+    Object resultInvalid = shell.evaluate(() -> "locks audit cleanup --ageDays abc");
+
+    assertAll("Invalid string ageDays should be rejected",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(resultInvalid)),
+        () -> assertNotNull(resultInvalid.toString()),
+        () -> assertEquals("Error: ageDays must be a value greater than 0.", resultInvalid.toString()));
+  }
+
+  /**
+   * Test cleanup when no table is loaded.
+   */
+  @Test
+  public void testCleanupAuditLocksNoTable() {
+    // Clear the base path to simulate no table loaded
+    String originalBasePath = HoodieCLI.basePath;
+    HoodieCLI.basePath = null;
+
+    Object result = shell.evaluate(() -> "locks audit cleanup");
+    assertAll("Cleanup handles no table loaded",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertEquals("No Hudi table loaded. Please connect to a table first.", result.toString()));
+    HoodieCLI.basePath = originalBasePath;
+  }
+
+  /**
+   * Test cleanup with multiple files.
+   */
+  @Test
+  public void testCleanupAuditLocksMixedFiles() throws IOException {
+    // Create multiple audit files
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    
+    // File 1
+    List<LockAuditingCommand.AuditRecord> records1 = new ArrayList<>();
+    records1.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records1.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records1));
+    
+    // File 2
+    List<LockAuditingCommand.AuditRecord> records2 = new ArrayList<>();
+    records2.add(createAuditRecord("owner2", 2000L, 2100L, "START", 62000L));
+    records2.add(createAuditRecord("owner2", 2000L, 2200L, "END", 62000L));
+    scenarios.add(new TransactionScenario("2000_owner2.jsonl", records2));
+    
+    createAuditFiles(scenarios);
+    
+    Object result = shell.evaluate(() -> "locks audit cleanup --dryRun true");
+    System.out.println("DEBUG - Mixed files result: " + result.toString());
+    
+    assertAll("Multiple files handled correctly",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("No audit files older than")
+                        || result.toString().contains("cleanup")
+                        || result.toString().contains("found")));
+  }
+
+  /**
+   * Test that disable method can call performAuditCleanup with ageDays=0 to delete all files.
+   * This validates that the internal validation allows ageDays >= 0 (not just > 0).
+   */
+  @Test
+  public void testDisableLockAuditWithAgeDaysZero() throws Exception {
+    // First enable audit
+    shell.evaluate(() -> "locks audit enable");
+
+    // Create some audit files to be cleaned up
+    List<TransactionScenario> scenarios = new ArrayList<>();
+    List<LockAuditingCommand.AuditRecord> records = new ArrayList<>();
+    records.add(createAuditRecord("owner1", 1000L, 1100L, "START", 61000L));
+    records.add(createAuditRecord("owner1", 1000L, 1200L, "END", 61000L));
+    scenarios.add(new TransactionScenario("1000_owner1.jsonl", records));
+    createAuditFiles(scenarios);
+
+    // Verify files exist before disable
+    String auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(HoodieCLI.basePath);
+    StoragePath auditFolder = new StoragePath(auditFolderPath);
+    List<StoragePathInfo> filesBefore = HoodieCLI.storage.listDirectEntries(auditFolder);
+    long jsonlFilesBefore = filesBefore.stream()
+        .filter(pathInfo -> pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl"))
+        .count();
+    assertTrue(jsonlFilesBefore > 0, "Should have audit files before disable");
+
+    // Disable with keepAuditFiles=false, which internally calls performAuditCleanup(false, 0)
+    Object result = shell.evaluate(() -> "locks audit disable --keepAuditFiles false");
+
+    assertAll("Disable with ageDays=0 deletes all files",
+        () -> assertTrue(ShellEvaluationResultUtil.isSuccess(result)),
+        () -> assertNotNull(result.toString()),
+        () -> assertTrue(result.toString().contains("Lock audit disabled successfully")),
+        () -> assertTrue(result.toString().contains("cleaned up") || result.toString().contains("No audit files to clean up")));
+
+    // Verify files were cleaned up (ageDays=0 should delete all files)
+    List<StoragePathInfo> filesAfter = HoodieCLI.storage.listDirectEntries(auditFolder);
+    long jsonlFilesAfter = filesAfter.stream()
+        .filter(pathInfo -> pathInfo.isFile() && pathInfo.getPath().getName().endsWith(".jsonl"))
+        .count();
+    assertEquals(0, jsonlFilesAfter, "All audit files should be deleted when ageDays=0");
+  }
+}
\ No newline at end of file
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/AsyncClusteringService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/AsyncClusteringService.java
index 763150d5429f..613a7d1b7e2c 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/AsyncClusteringService.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/AsyncClusteringService.java
@@ -78,9 +78,9 @@ public abstract class AsyncClusteringService extends HoodieAsyncTableService {
         while (!isShutdownRequested()) {
           final HoodieInstant instant = fetchNextAsyncServiceInstant();
           if (null != instant) {
-            LOG.info("Starting clustering for instant " + instant);
+            LOG.info("Starting clustering for instant {}", instant);
             clusteringClient.cluster(instant);
-            LOG.info("Finished clustering for instant " + instant);
+            LOG.info("Finished clustering for instant {}", instant);
           }
         }
         LOG.info("Clustering executor shutting down properly");
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/HoodieAsyncService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/HoodieAsyncService.java
index f022e7104568..9634f8008d62 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/HoodieAsyncService.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/async/HoodieAsyncService.java
@@ -141,7 +141,7 @@ public abstract class HoodieAsyncService implements Serializable {
    */
   public void start(Function<Boolean, Boolean> onShutdownCallback) {
     if (started) {
-      LOG.warn("The async service already started.");
+      LOG.info("The async service already started.");
       return;
     }
     Pair<CompletableFuture, ExecutorService> res = startService();
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/callback/client/http/HoodieWriteCommitHttpCallbackClient.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/callback/client/http/HoodieWriteCommitHttpCallbackClient.java
index 037e84b3d004..3036cfef5a30 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/callback/client/http/HoodieWriteCommitHttpCallbackClient.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/callback/client/http/HoodieWriteCommitHttpCallbackClient.java
@@ -81,7 +81,7 @@ public class HoodieWriteCommitHttpCallbackClient implements Closeable {
     try (CloseableHttpResponse response = client.execute(request)) {
       int statusCode = response.getStatusLine().getStatusCode();
       if (statusCode >= 300) {
-        LOG.warn("Failed to send callback message. Response was {}", response);
+        LOG.error("Failed to send callback message. Response was {}", response);
       } else {
         LOG.info("Sent Callback data with {} custom headers to {} successfully !", customHeaders.size(), url);
       }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieTableServiceClient.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieTableServiceClient.java
index 62fa6f98d4a4..e4b4c8de9f53 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieTableServiceClient.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieTableServiceClient.java
@@ -756,7 +756,7 @@ public abstract class BaseHoodieTableServiceClient<I, T, O> extends BaseHoodieCl
       }
 
       if (shouldDelegateToTableServiceManager(config, ActionType.clean)) {
-        LOG.warn("Cleaning is not yet supported with Table Service Manager.");
+        LOG.info("Cleaning is not yet supported with Table Service Manager.");
         return null;
       }
     }
@@ -849,17 +849,17 @@ public abstract class BaseHoodieTableServiceClient<I, T, O> extends BaseHoodieCl
         rollbackPlan = RollbackUtils.getRollbackPlan(metaClient, rollbackInstant);
       } catch (Exception e) {
         if (rollbackInstant.isRequested()) {
-          LOG.warn("Fetching rollback plan failed for " + rollbackInstant + ", deleting the plan since it's in REQUESTED state", e);
+          LOG.warn("Fetching rollback plan failed for {}, deleting the plan since it's in REQUESTED state", rollbackInstant, e);
           try {
             metaClient.getActiveTimeline().deletePending(rollbackInstant);
           } catch (HoodieIOException he) {
-            LOG.warn("Cannot delete " + rollbackInstant, he);
+            LOG.warn("Cannot delete {}", rollbackInstant, he);
             continue;
           }
         } else {
           // Here we assume that if the rollback is inflight, the rollback plan is intact
           // in instant.rollback.requested.  The exception here can be due to other reasons.
-          LOG.warn("Fetching rollback plan failed for " + rollbackInstant + ", skip the plan", e);
+          LOG.error("Fetching rollback plan failed for {}, skip the plan", rollbackInstant, e);
         }
         continue;
       }
@@ -879,7 +879,7 @@ public abstract class BaseHoodieTableServiceClient<I, T, O> extends BaseHoodieCl
           infoMap.putIfAbsent(instantToRollback, Option.of(new HoodiePendingRollbackInfo(rollbackInstant, rollbackPlan)));
         }
       } catch (Exception e) {
-        LOG.warn("Processing rollback plan failed for " + rollbackInstant + ", skip the plan", e);
+        LOG.warn("Processing rollback plan failed for {}, skip the plan", rollbackInstant, e);
       }
     }
     return infoMap;
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
index e585b28bf13f..a3c1f31479cd 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/BaseHoodieWriteClient.java
@@ -1362,7 +1362,7 @@ public abstract class BaseHoodieWriteClient<T, I, K, O> extends BaseHoodieClient
           throw new HoodieIOException("Latest commit does not have any schema in commit metadata");
         }
       } else {
-        LOG.warn("None rows are deleted because the table is empty");
+        LOG.debug("No rows are deleted because the table is empty");
       }
     } catch (IOException e) {
       throw new HoodieIOException("IOException thrown while reading last commit metadata", e);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java
index 8af2542616e1..8d32d4c36874 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java
@@ -31,7 +31,7 @@ public interface RunsTableService {
   default boolean tableServicesEnabled(HoodieWriteConfig config) {
     boolean enabled = config.areTableServicesEnabled();
     if (!enabled) {
-      LOG.warn(String.format("Table services are disabled. Set `%s` to enable.", HoodieWriteConfig.TABLE_SERVICES_ENABLED));
+      LOG.warn("Table services are disabled. Set `{}` to enable.", HoodieWriteConfig.TABLE_SERVICES_ENABLED);
     }
     return enabled;
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java
index 0238f6e7f45f..552118ef0ff4 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/heartbeat/HoodieHeartbeatClient.java
@@ -243,8 +243,8 @@ public class HoodieHeartbeatClient implements AutoCloseable, Serializable {
       lastHeartbeatForWriter.getTimer().cancel();
     }
     if (currentTime - lastHeartbeatForWriter.getLastHeartbeatTime() > this.maxAllowableHeartbeatIntervalInMs) {
-      LOG.warn("Heartbeat expired, currentTime = " + currentTime + ", last heartbeat = " + lastHeartbeatForWriter
-          + ", heartbeat interval = " + this.heartbeatIntervalInMs);
+      LOG.warn("Heartbeat expired, currentTime = {}, last heartbeat = {}, heartbeat interval = {}", currentTime,
+          lastHeartbeatForWriter, this.heartbeatIntervalInMs);
       return true;
     }
     return false;
@@ -259,7 +259,7 @@ public class HoodieHeartbeatClient implements AutoCloseable, Serializable {
       outputStream.close();
       Heartbeat heartbeat = instantToHeartbeatMap.get(instantTime);
       if (heartbeat.getLastHeartbeatTime() != null && isHeartbeatExpired(instantTime)) {
-        LOG.error("Aborting, missed generating heartbeat within allowable interval " + this.maxAllowableHeartbeatIntervalInMs);
+        LOG.error("Aborting, missed generating heartbeat within allowable interval {} ms", this.maxAllowableHeartbeatIntervalInMs);
         // Since TimerTask allows only java.lang.Runnable, cannot throw an exception and bubble to the caller thread, hence
         // explicitly interrupting the timer thread.
         Thread.currentThread().interrupt();
@@ -270,7 +270,7 @@ public class HoodieHeartbeatClient implements AutoCloseable, Serializable {
     } catch (IOException io) {
       Boolean isHeartbeatStopped = instantToHeartbeatMap.get(instantTime).isHeartbeatStopped;
       if (isHeartbeatStopped) {
-        LOG.warn(String.format("update heart beat failed, because the instant time %s was stopped ? : %s", instantTime, isHeartbeatStopped));
+        LOG.info("update heart beat failed, because the instant time {} was stopped", instantTime);
         return;
       }
       throw new HoodieHeartbeatException("Unable to generate heartbeat for instant " + instantTime, io);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
index f05e5c6e47a9..7d2fafa9de89 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/FileSystemBasedLockProvider.java
@@ -19,6 +19,7 @@
 
 package org.apache.hudi.client.transaction.lock;
 
+import org.apache.hudi.client.transaction.lock.metrics.HoodieLockMetrics;
 import org.apache.hudi.common.config.HoodieCommonConfig;
 import org.apache.hudi.common.config.LockConfiguration;
 import org.apache.hudi.common.config.TypedProperties;
@@ -91,6 +92,10 @@ public class FileSystemBasedLockProvider implements LockProvider<String>, Serial
     }
   }
 
+  public FileSystemBasedLockProvider(final LockConfiguration lockConfiguration, final StorageConfiguration<?> configuration, final HoodieLockMetrics lockMetrics) {
+    this(lockConfiguration, configuration);
+  }
+
   @Override
   public void close() {
     synchronized (LOCK_FILE_NAME) {
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/InProcessLockProvider.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/InProcessLockProvider.java
index 51d02dc4aea8..5922c54d2dfb 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/InProcessLockProvider.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/InProcessLockProvider.java
@@ -106,7 +106,7 @@ public class InProcessLockProvider implements LockProvider<ReentrantReadWriteLoc
         lock.writeLock().unlock();
         LOG.info(getLogMessage(LockState.RELEASED));
       } else {
-        LOG.warn("Cannot unlock because the current thread does not hold the lock.");
+        LOG.info("Cannot unlock because the current thread does not hold the lock.");
       }
     } catch (Exception e) {
       throw new HoodieLockException(getLogMessage(LockState.FAILED_TO_RELEASE), e);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/LockManager.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/LockManager.java
index b114069b4f76..13b7c0ba268d 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/LockManager.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/LockManager.java
@@ -115,6 +115,7 @@ public class LockManager implements Serializable, AutoCloseable {
     } catch (HoodieException e) {
       LOG.error(String.format("Exception encountered when updating lock metrics: %s", e));
     }
+    metrics.updateLockReleaseSuccessMetric();
     close();
   }
 
@@ -122,9 +123,19 @@ public class LockManager implements Serializable, AutoCloseable {
     // Perform lazy initialization of lock provider only if needed
     if (lockProvider == null) {
       LOG.info("LockProvider " + writeConfig.getLockProviderClass());
-      lockProvider = (LockProvider) ReflectionUtils.loadClass(writeConfig.getLockProviderClass(),
-          new Class<?>[] {LockConfiguration.class, StorageConfiguration.class},
-          lockConfiguration, storageConf);
+      
+      // Try to load lock provider with HoodieLockMetrics constructor first
+      Class<?>[] metricsConstructorTypes = {LockConfiguration.class, StorageConfiguration.class, HoodieLockMetrics.class};
+      if (ReflectionUtils.hasConstructor(writeConfig.getLockProviderClass(), metricsConstructorTypes)) {
+        lockProvider = (LockProvider) ReflectionUtils.loadClass(writeConfig.getLockProviderClass(),
+            metricsConstructorTypes, lockConfiguration, storageConf, metrics);
+      } else {
+        LOG.debug("LockProvider does not support HoodieLockMetrics param in constructor, falling back to standard constructor");
+        // Fallback to original constructor without metrics
+        lockProvider = (LockProvider) ReflectionUtils.loadClass(writeConfig.getLockProviderClass(),
+                new Class<?>[] {LockConfiguration.class, StorageConfiguration.class},
+                lockConfiguration, storageConf);
+      }
     }
     return lockProvider;
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java
index 9c3d1e3fdc43..9349ecc7b597 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProvider.java
@@ -19,6 +19,10 @@
 package org.apache.hudi.client.transaction.lock;
 
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hudi.client.transaction.lock.audit.AuditOperationState;
+import org.apache.hudi.client.transaction.lock.audit.AuditService;
+import org.apache.hudi.client.transaction.lock.audit.AuditServiceFactory;
+import org.apache.hudi.client.transaction.lock.metrics.HoodieLockMetrics;
 import org.apache.hudi.client.transaction.lock.models.HeartbeatManager;
 import org.apache.hudi.client.transaction.lock.models.LockGetResult;
 import org.apache.hudi.client.transaction.lock.models.LockProviderHeartbeatManager;
@@ -62,7 +66,6 @@ import static org.apache.hudi.common.lock.LockState.FAILED_TO_ACQUIRE;
 import static org.apache.hudi.common.lock.LockState.FAILED_TO_RELEASE;
 import static org.apache.hudi.common.lock.LockState.RELEASED;
 import static org.apache.hudi.common.lock.LockState.RELEASING;
-import static org.apache.hudi.common.table.HoodieTableMetaClient.LOCKS_FOLDER_NAME;
 
 /**
  * A distributed filesystem storage based lock provider. This {@link LockProvider} implementation
@@ -96,6 +99,9 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   private final String lockFilePath;
   private final HeartbeatManager heartbeatManager;
   private final transient Thread shutdownThread;
+  private final Option<HoodieLockMetrics> hoodieLockMetrics;
+  private Option<AuditService> auditService;
+  private final String basePath;
 
   @GuardedBy("this")
   private StorageLockFile currentLockObj = null;
@@ -113,24 +119,44 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   /**
    * Default constructor for StorageBasedLockProvider, required by LockManager
    * to instantiate it using reflection.
-   * 
+   *
    * @param lockConfiguration The lock configuration, should be transformable into
    *                          StorageBasedLockConfig
    * @param conf              Storage config, ignored.
    */
   public StorageBasedLockProvider(final LockConfiguration lockConfiguration, final StorageConfiguration<?> conf) {
     this(
-            UUID.randomUUID().toString(),
-            lockConfiguration.getConfig(),
-            LockProviderHeartbeatManager::new,
-            getStorageLockClientClassName(),
-            LOGGER);
+        UUID.randomUUID().toString(),
+        lockConfiguration.getConfig(),
+        LockProviderHeartbeatManager::new,
+        getStorageLockClientClassName(),
+        LOGGER,
+        null);
+  }
+
+  /**
+   * Constructor for StorageBasedLockProvider with HoodieLockMetrics support.
+   * This constructor allows lock providers to access metrics for fine-grained metrics collection.
+   *
+   * @param lockConfiguration The lock configuration, should be transformable into
+   *                          StorageBasedLockConfig
+   * @param conf              Storage config, ignored.
+   * @param metrics           HoodieLockMetrics instance for metrics collection
+   */
+  public StorageBasedLockProvider(final LockConfiguration lockConfiguration, final StorageConfiguration<?> conf, final HoodieLockMetrics metrics) {
+    this(
+        UUID.randomUUID().toString(),
+        lockConfiguration.getConfig(),
+        LockProviderHeartbeatManager::new,
+        getStorageLockClientClassName(),
+        LOGGER,
+        metrics);
   }
 
   /**
    * Default constructor for StorageBasedLockProvider, required by LockManager
    * to instantiate it using reflection, supports 0.14.1 LP format.
-   * 
+   *
    * @param lockConfiguration The lock configuration, should be transformable into
    *                          StorageBasedLockConfig
    * @param conf              Storage config, ignored.
@@ -141,16 +167,17 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
             lockConfiguration.getConfig(),
             LockProviderHeartbeatManager::new,
             getStorageLockClientClassName(),
-            LOGGER);
+            LOGGER,
+        null);
   }
 
   private static Functions.Function3<String, String, TypedProperties, StorageLockClient> getStorageLockClientClassName() {
     return (ownerId, lockFilePath, lockConfig) -> {
       try {
         return (StorageLockClient) ReflectionUtils.loadClass(
-                getLockServiceClassName(new URI(lockFilePath).getScheme()),
-                new Class<?>[]{String.class, String.class, Properties.class},
-                new Object[]{ownerId, lockFilePath, lockConfig});
+            getLockServiceClassName(new URI(lockFilePath).getScheme()),
+            new Class<?>[] {String.class, String.class, Properties.class},
+            new Object[] {ownerId, lockFilePath, lockConfig});
       } catch (Throwable e) {
         throw new HoodieLockException("Failed to load and initialize StorageLock", e);
       }
@@ -172,21 +199,20 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
       TypedProperties properties,
       Functions.Function3<String, Long, Supplier<Boolean>, HeartbeatManager> heartbeatManagerLoader,
       Functions.Function3<String, String, TypedProperties, StorageLockClient> storageLockClientLoader,
-      Logger logger) {
+      Logger logger,
+      HoodieLockMetrics hoodieLockMetrics) {
     StorageBasedLockConfig config = new StorageBasedLockConfig.Builder().fromProperties(properties).build();
-    long heartbeatPollSeconds = config.getHeartbeatPollSeconds();
+    long heartbeatPollSeconds = config.getRenewIntervalSecs();
     this.lockValiditySecs = config.getValiditySeconds();
-    this.lockFilePath = String.format(
-            "%s%s%s%s%s",
-            config.getHudiTableBasePath(),
-            StoragePath.SEPARATOR,
-            LOCKS_FOLDER_NAME,
-            StoragePath.SEPARATOR,
-            DEFAULT_TABLE_LOCK_FILE_NAME);
+    this.basePath = config.getHudiTableBasePath();
+    String lockFolderPath = StorageLockClient.getLockFolderPath(basePath);
+    this.lockFilePath = new StoragePath(lockFolderPath, DEFAULT_TABLE_LOCK_FILE_NAME).toString();
     this.heartbeatManager = heartbeatManagerLoader.apply(ownerId, TimeUnit.SECONDS.toMillis(heartbeatPollSeconds), this::renewLock);
     this.storageLockClient = storageLockClientLoader.apply(ownerId, lockFilePath, properties);
     this.ownerId = ownerId;
     this.logger = logger;
+    this.hoodieLockMetrics = Option.ofNullable(hoodieLockMetrics);
+    this.auditService = Option.empty(); // Will be created lazily on first lock acquisition
     shutdownThread = new Thread(() -> shutdown(true));
     Runtime.getRuntime().addShutdownHook(shutdownThread);
     logger.info("Instantiated new storage-based lock provider, owner: {}, lockfilePath: {}", ownerId, lockFilePath);
@@ -217,6 +243,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
         Thread.sleep(Long.parseLong(DEFAULT_LOCK_ACQUIRE_RETRY_WAIT_TIME_IN_MILLIS));
       } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
+        hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockInterruptedMetric);
         throw new HoodieLockException(generateLockStateMessage(LockState.FAILED_TO_ACQUIRE), e);
       }
     }
@@ -261,6 +288,18 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
     } catch (Exception e) {
       logger.error("Owner {}: Heartbeat manager failed to close.", ownerId, e);
     }
+    try {
+      this.auditService.ifPresent(auditService -> {
+        try {
+          auditService.close();
+        } catch (Exception e) {
+          logger.error("Owner {}: Audit service failed to close.", ownerId, e);
+        }
+      });
+      this.auditService = Option.empty();
+    } catch (Exception e) {
+      logger.error("Owner {}: Failed to close audit service.", ownerId, e);
+    }
 
     this.isClosed = true;
   }
@@ -271,12 +310,20 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   }
 
   private synchronized boolean isLockStillValid(StorageLockFile lock) {
-    return !lock.isExpired() && !isCurrentTimeCertainlyOlderThanDistributedTime(lock.getValidUntilMs());
+    if (lock.isExpired()) {
+      return false;
+    } else if (isCurrentTimeCertainlyOlderThanDistributedTime(lock.getValidUntilMs())) {
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockDanglingMetric);
+      logWarnLockState(ACQUIRING, "Found a dangling expired lock with owner " + lock.getOwner());
+      return false;
+    }
+
+    return true;
   }
 
   /**
    * Attempts a single pass to acquire the lock (non-blocking).
-   * 
+   *
    * @return true if lock acquired, false otherwise
    */
   @Override
@@ -293,12 +340,14 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
       logger.error("Detected broken invariant: there is an active heartbeat without a lock being held.");
       // Breach of object invariant - we should never have an active heartbeat without
       // holding a lock.
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockProviderFatalErrorMetric);
       throw new HoodieLockException(generateLockStateMessage(FAILED_TO_ACQUIRE));
     }
 
     Pair<LockGetResult, Option<StorageLockFile>> latestLock = this.storageLockClient.readCurrentLockFile();
     if (latestLock.getLeft() == LockGetResult.UNKNOWN_ERROR) {
       logInfoLockState(FAILED_TO_ACQUIRE, "Failed to get the latest lock status");
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockStateUnknownMetric);
       // We were not able to determine whether a lock was present.
       return false;
     }
@@ -311,16 +360,21 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
     }
 
     // Try to acquire the lock
-    StorageLockData newLockData = new StorageLockData(false, getCurrentEpochMs() + TimeUnit.SECONDS.toMillis(lockValiditySecs), ownerId);
+    long acquisitionTimestamp = getCurrentEpochMs();
+    long lockExpirationMs = calculateLockExpiration(acquisitionTimestamp);
+    StorageLockData newLockData = new StorageLockData(false, lockExpirationMs, ownerId);
     Pair<LockUpsertResult, Option<StorageLockFile>> lockUpdateStatus = this.storageLockClient.tryUpsertLockFile(
         newLockData,
         latestLock.getRight());
     if (lockUpdateStatus.getLeft() != LockUpsertResult.SUCCESS) {
       // failed to acquire the lock, indicates concurrent contention
       logInfoLockState(FAILED_TO_ACQUIRE);
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockAcquirePreconditionFailureMetric);
       return false;
     }
     this.setLock(lockUpdateStatus.getRight().get());
+    hoodieLockMetrics.ifPresent(metrics -> metrics.updateLockExpirationDeadlineMetric(
+        (int) (lockUpdateStatus.getRight().get().getValidUntilMs() - getCurrentEpochMs())));
 
     // There is a remote chance that
     // - after lock is acquired but before heartbeat starts the lock is expired.
@@ -334,11 +388,21 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
       // startHeartbeatForThread returns false,
       // we are confident no heartbeat thread is running.
       logErrorLockState(RELEASING, "We were unable to start the heartbeat!");
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockProviderFatalErrorMetric);
       tryExpireCurrentLock(false);
       return false;
     }
 
     logInfoLockState(ACQUIRED);
+
+    // Create audit service lazily on first successful lock acquisition if auditing is enabled
+    if (auditService.isEmpty()) {
+      auditService = AuditServiceFactory.createLockProviderAuditService(
+          ownerId, basePath, storageLockClient, acquisitionTimestamp,
+          this::calculateLockExpiration, this::actuallyHoldsLock);
+    }
+
+    recordAuditOperation(AuditOperationState.START, acquisitionTimestamp);
     return true;
   }
 
@@ -351,7 +415,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
    * valid only if it exists and has not expired according to its timestamp.
    *
    * @return {@code true} if this provider holds a valid lock, {@code false}
-   *         otherwise
+   * otherwise
    */
   private boolean actuallyHoldsLock() {
     return believesLockMightBeHeld() && isLockStillValid(getLock());
@@ -370,7 +434,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
    * StorageBasedLockProvider#actuallyHoldsLock should be used.
    *
    * @return {@code true} if this provider has a non-null lock object,
-   *         {@code false} otherwise
+   * {@code false} otherwise
    * @see StorageBasedLockProvider#actuallyHoldsLock()
    */
   private boolean believesLockMightBeHeld() {
@@ -397,6 +461,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
     // Then expire the current lock.
     believesNoLongerHoldsLock &= tryExpireCurrentLock(false);
     if (!believesNoLongerHoldsLock) {
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockReleaseFailureMetric);
       throw new HoodieLockException(generateLockStateMessage(FAILED_TO_RELEASE));
     }
   }
@@ -404,18 +469,21 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   private void assertHeartbeatManagerExists() {
     if (heartbeatManager == null) {
       // broken function precondition.
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockProviderFatalErrorMetric);
       throw new HoodieLockException("Unexpected null heartbeatManager");
     }
   }
 
   private void assertUnclosed() {
     if (this.isClosed) {
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockProviderFatalErrorMetric);
       throw new HoodieLockException("Lock provider already closed");
     }
   }
 
   /**
    * Tries to expire the currently held lock.
+   *
    * @param fromShutdownHook Whether we are attempting best effort quick unlock from shutdown hook.
    * @return True if we were successfully able to upload an expired lock.
    */
@@ -431,23 +499,27 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
     // Upload metadata that will unlock this lock.
     StorageLockData expiredLockData = new StorageLockData(true, this.getLock().getValidUntilMs(), ownerId);
     Pair<LockUpsertResult, Option<StorageLockFile>> result;
+    long lockExpirationTimeMs = System.currentTimeMillis();
     result = this.storageLockClient.tryUpsertLockFile(expiredLockData, Option.of(this.getLock()));
     switch (result.getLeft()) {
       case UNKNOWN_ERROR:
         // Here we do not know the state of the lock.
         logErrorLockState(FAILED_TO_RELEASE, "Lock state is unknown.");
+        hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockStateUnknownMetric);
         return false;
       case SUCCESS:
         logInfoLockState(RELEASED);
+        recordAuditOperation(AuditOperationState.END, lockExpirationTimeMs);
         setLock(null);
         return true;
       case ACQUIRED_BY_OTHERS:
-        // As we are confident no lock is held by itself, clean up the cached lock object.
-        // However, this is an edge case, so warn.
-        logWarnLockState(RELEASED, "lock should not have been acquired by others.");
+        // Lock was acquired by others, indicating heartbeat failure during lock hold period.
+        logErrorLockState(FAILED_TO_RELEASE, "lock was acquired by others, indicating heartbeat failure.");
         setLock(null);
-        return true;
+        hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockAcquiredByOthersErrorMetric);
+        return false;
       default:
+        hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockReleaseFailureMetric);
         throw new HoodieLockException("Unexpected lock update result: " + result.getLeft());
     }
   }
@@ -456,6 +528,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
    * Renews (heartbeats) the current lock if we are the holder, it forcefully set
    * the expiration flag
    * to false and the lock expiration time to a later time in the future.
+   *
    * @return True if we successfully renewed the lock, false if not.
    */
   @VisibleForTesting
@@ -480,12 +553,15 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
       // Action taken for corner case 2 is just a best effort mitigation. At least it
       // prevents further data corruption by
       // letting someone else acquire the lock.
+      long acquisitionTimestamp = getCurrentEpochMs();
+      long lockExpirationMs = calculateLockExpiration(acquisitionTimestamp);
       Pair<LockUpsertResult, Option<StorageLockFile>> currentLock = this.storageLockClient.tryUpsertLockFile(
-          new StorageLockData(false, getCurrentEpochMs() + TimeUnit.SECONDS.toMillis(lockValiditySecs), ownerId),
+          new StorageLockData(false, lockExpirationMs, ownerId),
           Option.of(getLock()));
       switch (currentLock.getLeft()) {
         case ACQUIRED_BY_OTHERS:
           logger.error("Owner {}: Unable to renew lock as it is acquired by others.", ownerId);
+          hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockAcquiredByOthersErrorMetric);
           // No need to extend lock lease anymore.
           return false;
         case UNKNOWN_ERROR:
@@ -493,13 +569,17 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
           // normally.
           // If the next heartbeat run identifies our lock has expired we will error out.
           logger.warn("Owner {}: Unable to renew lock due to unknown error, could be transient.", ownerId);
+          hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockStateUnknownMetric);
           // Let heartbeat retry later.
           return true;
         case SUCCESS:
           // Only positive outcome
           this.setLock(currentLock.getRight().get());
+          hoodieLockMetrics.ifPresent(metrics -> metrics.updateLockExpirationDeadlineMetric(
+              (int) (oldExpirationMs - getCurrentEpochMs())));
           logger.info("Owner {}: Lock renewal successful. The renewal completes {} ms before expiration for lock {}.",
               ownerId, oldExpirationMs - getCurrentEpochMs(), lockFilePath);
+          recordAuditOperation(AuditOperationState.RENEW, acquisitionTimestamp);
           // Let heartbeat continue to renew lock lease again later.
           return true;
         default:
@@ -507,6 +587,7 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
       }
     } catch (Exception e) {
       logger.error("Owner {}: Exception occurred while renewing lock", ownerId, e);
+      hoodieLockMetrics.ifPresent(HoodieLockMetrics::updateLockProviderFatalErrorMetric);
       return false;
     }
   }
@@ -526,11 +607,11 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   private String generateLockStateMessage(LockState state) {
     String threadName = Thread.currentThread().getName();
     return String.format(
-            "Owner %s: Lock file path %s, Thread %s, Storage based lock state %s",
-            ownerId,
-            lockFilePath,
-            threadName,
-            state.toString());
+        "Owner %s: Lock file path %s, Thread %s, Storage based lock state %s",
+        ownerId,
+        lockFilePath,
+        threadName,
+        state.toString());
   }
 
   private static final String LOCK_STATE_LOGGER_MSG = "Owner {}: Lock file path {}, Thread {}, Storage based lock state {}";
@@ -560,4 +641,29 @@ public class StorageBasedLockProvider implements LockProvider<StorageLockFile> {
   long getCurrentEpochMs() {
     return System.currentTimeMillis();
   }
+
+  /**
+   * Calculates the lock expiration time based on given timestamp and validity period.
+   * This is the shared function used by both the lock provider and audit service.
+   *
+   * @param timestamp The base timestamp to calculate expiration from
+   * @return Lock expiration time in milliseconds
+   */
+  private long calculateLockExpiration(long timestamp) {
+    return timestamp + TimeUnit.SECONDS.toMillis(lockValiditySecs);
+  }
+
+  /**
+   * Helper method to record audit operations.
+   */
+  private void recordAuditOperation(AuditOperationState state, long timestamp) {
+    auditService.ifPresent(service -> {
+      try {
+        service.recordOperation(state, timestamp);
+      } catch (Exception e) {
+        // Log but don't fail the lock operation due to recording failures
+        logger.warn("Owner {}: Failed to record audit operation {}: {}", ownerId, state, e.getMessage());
+      }
+    });
+  }
 }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageLockClient.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageLockClient.java
index ac8ff2ed9d0d..0ae66a58dbb8 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageLockClient.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/StorageLockClient.java
@@ -23,7 +23,15 @@ import org.apache.hudi.client.transaction.lock.models.StorageLockData;
 import org.apache.hudi.client.transaction.lock.models.StorageLockFile;
 import org.apache.hudi.client.transaction.lock.models.LockGetResult;
 import org.apache.hudi.common.util.Option;
+import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.common.util.collection.Pair;
+import org.apache.hudi.exception.HoodieLockException;
+import org.apache.hudi.storage.StoragePath;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import static org.apache.hudi.common.table.HoodieTableMetaClient.LOCKS_FOLDER_NAME;
 
 /**
  * Defines a contract for a service which should be able to perform conditional writes to object storage.
@@ -34,7 +42,8 @@ public interface StorageLockClient extends AutoCloseable {
   /**
    * Tries to create or update a lock file.
    * All non pre-condition failure related errors should be returned as UNKNOWN_ERROR.
-   * @param newLockData The new data to write to the lock file
+   *
+   * @param newLockData      The new data to write to the lock file
    * @param previousLockFile The previous lock file
    * @return A pair containing the result state and the new lock file (if successful)
    */
@@ -44,7 +53,69 @@ public interface StorageLockClient extends AutoCloseable {
 
   /**
    * Reads the current lock file.
+   *
    * @return The lock retrieve result and the current lock file if successfully retrieved.
-   * */
+   */
   Pair<LockGetResult, Option<StorageLockFile>> readCurrentLockFile();
+
+  /**
+   * Reads an object from the specified path.
+   * This method is intended for reading small files (e.g., audit config, feature flags, JSON objects)
+   * and loads the entire file into memory. Do not use for large files.
+   *
+   * @param filePath         The path to the object file to read
+   * @param checkExistsFirst If true, performs a lightweight existence check before attempting to read.
+   *                         Recommended when the file is unlikely to exist (e.g., optional configs).
+   * @return An Option containing the content as a string if successful, Option.empty() otherwise
+   */
+  Option<String> readObject(String filePath, boolean checkExistsFirst);
+
+  /**
+   * Writes an object to the specified path.
+   * This method is intended for writing small files (e.g., audit logs, configuration files)
+   * and should not be used for large files.
+   *
+   * @param filePath The path where the object should be written
+   * @param content  The content to write as a string
+   * @return true if the write was successful, false otherwise
+   */
+  boolean writeObject(String filePath, String content);
+
+  /**
+   * Gets the lock folder path for the given base path.
+   * This is a static utility method that can be used without creating an instance.
+   *
+   * @param basePath The base path of the Hudi table
+   * @return The lock folder path (e.g., "s3://bucket/table/.hoodie/.locks")
+   */
+  static String getLockFolderPath(String basePath) {
+    return new StoragePath(basePath, LOCKS_FOLDER_NAME).toString();
+  }
+
+  /**
+   * Parses a URI and returns bucket name and path as a Pair.
+   * This is a shared utility method for all storage lock client implementations.
+   *
+   * @param uriString The URI string to parse
+   * @return A Pair containing bucket name (left) and path (right)
+   * @throws HoodieLockException if URI parsing fails or components are invalid
+   */
+  static Pair<String, String> parseBucketAndPath(String uriString) {
+    try {
+      URI uri = new URI(uriString);
+      String bucketName = uri.getAuthority();
+      String path = uri.getPath().replaceFirst("/", "");
+
+      if (StringUtils.isNullOrEmpty(bucketName)) {
+        throw new IllegalArgumentException("URI does not contain a valid bucket name.");
+      }
+      if (StringUtils.isNullOrEmpty(path)) {
+        throw new IllegalArgumentException("URI does not contain a valid path.");
+      }
+
+      return Pair.of(bucketName, path);
+    } catch (URISyntaxException e) {
+      throw new HoodieLockException("Failed to parse URI: " + uriString, e);
+    }
+  }
 }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditOperationState.java
similarity index 50%
copy from hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java
copy to hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditOperationState.java
index 8af2542616e1..700a8dd79be7 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/RunsTableService.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditOperationState.java
@@ -16,27 +16,24 @@
  * limitations under the License.
  */
 
-package org.apache.hudi.client;
+package org.apache.hudi.client.transaction.lock.audit;
 
-import org.apache.hudi.common.model.ActionType;
-import org.apache.hudi.config.HoodieWriteConfig;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public interface RunsTableService {
-
-  Logger LOG = LoggerFactory.getLogger(RunsTableService.class);
+/**
+ * Enumeration of audit operation states for different operations.
+ */
+public enum AuditOperationState {
+  /**
+   * Operation started.
+   */
+  START,
 
-  default boolean tableServicesEnabled(HoodieWriteConfig config) {
-    boolean enabled = config.areTableServicesEnabled();
-    if (!enabled) {
-      LOG.warn(String.format("Table services are disabled. Set `%s` to enable.", HoodieWriteConfig.TABLE_SERVICES_ENABLED));
-    }
-    return enabled;
-  }
+  /**
+   * Operation renewal/heartbeat.
+   */
+  RENEW,
 
-  default boolean shouldDelegateToTableServiceManager(HoodieWriteConfig config, ActionType actionType) {
-    return config.getTableServiceManagerConfig().isEnabledAndActionSupported(actionType);
-  }
+  /**
+   * Operation end.
+   */
+  END
 }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditService.java
new file mode 100644
index 000000000000..bdc062f519af
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditService.java
@@ -0,0 +1,41 @@
+/*
+ * 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.hudi.client.transaction.lock.audit;
+
+import org.apache.hudi.ApiMaturityLevel;
+import org.apache.hudi.PublicAPIClass;
+import org.apache.hudi.PublicAPIMethod;
+
+/**
+ * Generic audit service interface for tracking operation lifecycles.
+ * Provides a single method for recording all types of audit operations.
+ */
+@PublicAPIClass(maturity = ApiMaturityLevel.EVOLVING)
+public interface AuditService extends AutoCloseable {
+
+  /**
+   * Records an audit operation with the given state and timestamp.
+   *
+   * @param state     The type of operation (START, RENEW, END)
+   * @param timestamp When the operation occurred
+   * @throws Exception if the operation cannot be recorded
+   */
+  @PublicAPIMethod(maturity = ApiMaturityLevel.EVOLVING)
+  void recordOperation(AuditOperationState state, long timestamp) throws Exception;
+}
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditServiceFactory.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditServiceFactory.java
new file mode 100644
index 000000000000..871330886916
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/AuditServiceFactory.java
@@ -0,0 +1,119 @@
+/*
+ * 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.hudi.client.transaction.lock.audit;
+
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.common.util.Option;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Generic factory for creating audit services.
+ * This factory determines whether auditing is enabled by checking configuration files.
+ */
+public class AuditServiceFactory {
+
+  private static final Logger LOG = LoggerFactory.getLogger(AuditServiceFactory.class);
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  /**
+   * Creates a lock provider audit service instance by checking the audit configuration file.
+   * This method reads the audit configuration to determine if auditing is enabled.
+   * If auditing is enabled, it returns a StorageLockProviderAuditService instance.
+   *
+   * @param ownerId The owner ID for the lock provider
+   * @param basePath The base path of the Hudi table
+   * @param storageLockClient The storage lock client to use for reading configuration
+   * @param transactionStartTime The timestamp when the transaction started (lock acquired)
+   * @param lockExpirationFunction Function that takes a timestamp and returns the lock expiration time
+   * @param lockHeldSupplier Supplier that provides whether the lock is currently held
+   * @return An Option containing the audit service if enabled, Option.empty() otherwise
+   */
+  public static Option<AuditService> createLockProviderAuditService(
+      String ownerId,
+      String basePath,
+      StorageLockClient storageLockClient,
+      long transactionStartTime,
+      Function<Long, Long> lockExpirationFunction,
+      Supplier<Boolean> lockHeldSupplier) {
+
+    if (!isAuditEnabled(basePath, storageLockClient)) {
+      return Option.empty();
+    }
+    
+    return Option.of(new StorageLockProviderAuditService(
+        basePath,
+        ownerId,
+        transactionStartTime,
+        storageLockClient,
+        lockExpirationFunction,
+        lockHeldSupplier));
+  }
+
+  /**
+   * Checks if audit is enabled by reading the audit configuration file.
+   * This method looks for the audit configuration file at the expected location
+   * and parses it to determine if auditing is enabled. Returns false if the
+   * configuration file doesn't exist or cannot be read.
+   *
+   * @param basePath The base path of the Hudi table
+   * @param storageLockClient The storage lock client to use for reading configuration
+   * @return true if audit is enabled, false otherwise
+   */
+  private static boolean isAuditEnabled(String basePath, StorageLockClient storageLockClient) {
+    try {
+      // Construct the audit config path using the utility method
+      String auditConfigPath = StorageLockProviderAuditService.getAuditConfigPath(basePath);
+
+      LOG.debug("Checking for audit configuration at: {}", auditConfigPath);
+
+      // Use the readObject method to read the config file
+      // Pass true for checkExistsFirst since audit config is rarely present (99% miss rate)
+      Option<String> jsonContent = storageLockClient.readObject(auditConfigPath, true);
+
+      if (jsonContent.isPresent()) {
+        LOG.debug("Audit configuration file found, parsing content");
+        JsonNode rootNode = OBJECT_MAPPER.readTree(jsonContent.get());
+        JsonNode enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD);
+
+        boolean isEnabled = enabledNode != null && enabledNode.asBoolean(false);
+
+        if (isEnabled) {
+          LOG.info("Audit logging is ENABLED for lock operations at base path: {}", basePath);
+        } else {
+          LOG.info("Audit configuration present but audit logging is DISABLED");
+        }
+
+        return isEnabled;
+      }
+
+      LOG.debug("No audit configuration file found at: {}", auditConfigPath);
+      return false;
+    } catch (Exception e) {
+      LOG.error("Error reading audit configuration from base path: {}. Audit will be disabled.", basePath, e);
+      return false;
+    }
+  }
+}
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/StorageLockProviderAuditService.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/StorageLockProviderAuditService.java
new file mode 100644
index 000000000000..cc93f9f7388a
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/audit/StorageLockProviderAuditService.java
@@ -0,0 +1,176 @@
+/*
+ * 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.hudi.client.transaction.lock.audit;
+
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.storage.StoragePath;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Storage-based audit service implementation for lock provider operations.
+ * Writes audit records to a single JSONL file per transaction to track lock lifecycle events.
+ */
+public class StorageLockProviderAuditService implements AuditService {
+  
+  private static final Logger LOG = LoggerFactory.getLogger(StorageLockProviderAuditService.class);
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+  
+  // Audit configuration constants
+  public static final String AUDIT_FOLDER_NAME = "audit";
+  public static final String AUDIT_CONFIG_FILE_NAME = "audit_enabled.json";
+  public static final String STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD = "STORAGE_LOCK_AUDIT_SERVICE_ENABLED";
+  
+  /**
+   * Constructs the full path to the audit configuration file for a given table.
+   *
+   * @param basePath The base path of the Hudi table
+   * @return The full path to the audit_enabled.json configuration file
+   */
+  public static String getAuditConfigPath(String basePath) {
+    String lockFolderPath = StorageLockClient.getLockFolderPath(basePath);
+    return new StoragePath(lockFolderPath, AUDIT_CONFIG_FILE_NAME).toString();
+  }
+  
+  /**
+   * Constructs the full path to the audit folder for a given table.
+   *
+   * @param basePath The base path of the Hudi table
+   * @return The full path to the audit folder where audit files are stored
+   */
+  public static String getAuditFolderPath(String basePath) {
+    String lockFolderPath = StorageLockClient.getLockFolderPath(basePath);
+    return new StoragePath(lockFolderPath, AUDIT_FOLDER_NAME).toString();
+  }
+  
+  private final String ownerId;
+  private final long transactionStartTime;
+  private final String auditFilePath;
+  private final StorageLockClient storageLockClient;
+  private final Function<Long, Long> lockExpirationFunction;
+  private final Supplier<Boolean> lockHeldSupplier;
+  private final StringBuilder auditBuffer;
+  
+  /**
+   * Creates a new StorageLockProviderAuditService instance.
+   * 
+   * @param basePath The base path where audit files will be written
+   * @param ownerId The full owner ID for the lock
+   * @param transactionStartTime The timestamp when the transaction started (lock acquired)
+   * @param storageLockClient The storage client for writing audit files
+   * @param lockExpirationFunction Function that takes a timestamp and returns the lock expiration time
+   * @param lockHeldSupplier Supplier that provides whether the lock is currently held
+   */
+  public StorageLockProviderAuditService(
+      String basePath,
+      String ownerId,
+      long transactionStartTime,
+      StorageLockClient storageLockClient,
+      Function<Long, Long> lockExpirationFunction,
+      Supplier<Boolean> lockHeldSupplier) {
+    this.ownerId = ownerId;
+    this.transactionStartTime = transactionStartTime;
+    this.storageLockClient = storageLockClient;
+    this.lockExpirationFunction = lockExpirationFunction;
+    this.lockHeldSupplier = lockHeldSupplier;
+    this.auditBuffer = new StringBuilder();
+    
+    // Generate audit file path: <txn-start>_<full-owner-id>.jsonl
+    String filename = String.format("%d_%s.jsonl", transactionStartTime, ownerId);
+    this.auditFilePath = new StoragePath(getAuditFolderPath(basePath), filename).toString();
+    
+    LOG.debug("Initialized audit service for transaction starting at {} with file: {}", 
+        transactionStartTime, auditFilePath);
+  }
+  
+  /**
+   * Records an audit operation with the current timestamp and state.
+   * 
+   * @param state The audit operation state to record
+   * @param timestamp The timestamp when the operation occurred
+   * @throws Exception if there's an error creating or writing the audit record
+   */
+  @Override
+  public synchronized void recordOperation(AuditOperationState state, long timestamp) throws Exception {
+    // Create audit record
+    Map<String, Object> auditRecord = new HashMap<>();
+    auditRecord.put("ownerId", ownerId);
+    auditRecord.put("transactionStartTime", transactionStartTime);
+    auditRecord.put("timestamp", timestamp);
+    auditRecord.put("state", state.name());
+    auditRecord.put("lockExpiration", lockExpirationFunction.apply(timestamp));
+    auditRecord.put("lockHeld", lockHeldSupplier.get());
+    
+    // Convert to JSON and append newline for JSONL format
+    String jsonLine = OBJECT_MAPPER.writeValueAsString(auditRecord) + "\n";
+    
+    // Append to buffer
+    auditBuffer.append(jsonLine);
+    
+    // Write the accumulated audit records to file
+    writeAuditFile();
+    
+    LOG.debug("Recorded audit operation: state={}, timestamp={}, file={}", 
+        state, timestamp, auditFilePath);
+  }
+  
+  /**
+   * Writes the accumulated audit records to the audit file.
+   * This method is called after each recordOperation to persist the audit log.
+   * Failures to write audit records are logged but do not throw exceptions
+   * to avoid breaking lock operations.
+   */
+  private void writeAuditFile() {
+    try {
+      // Write the entire buffer content to the audit file
+      // This overwrites the file with all accumulated records
+      String content = auditBuffer.toString();
+      boolean success = storageLockClient.writeObject(auditFilePath, content);
+      if (success) {
+        LOG.debug("Successfully wrote audit records to: {}", auditFilePath);
+      } else {
+        LOG.warn("Failed to write audit records to: {}", auditFilePath);
+      }
+    } catch (Exception e) {
+      LOG.warn("Failed to write audit records to: {}", auditFilePath, e);
+      // Don't throw exception - audit failures should not break lock operations
+    }
+  }
+  
+  /**
+   * Closes the audit service. Since all audit records are written after each operation,
+   * no additional cleanup is required during close.
+   * 
+   * @throws Exception if there's an error during cleanup (not expected in current implementation)
+   */
+  @Override
+  public synchronized void close() throws Exception {
+    // All audit records are already written after each recordOperation()
+    // No additional writes needed during close
+    LOG.debug("Closed StorageLockProviderAuditService for transaction: {}, owner: {}", 
+        transactionStartTime, ownerId);
+  }
+}
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/metrics/HoodieLockMetrics.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/metrics/HoodieLockMetrics.java
index a67b6abbc98c..9d8246a87687 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/metrics/HoodieLockMetrics.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/metrics/HoodieLockMetrics.java
@@ -19,6 +19,7 @@
 package org.apache.hudi.client.transaction.lock.metrics;
 
 import org.apache.hudi.common.util.HoodieTimer;
+import org.apache.hudi.common.util.Option;
 import org.apache.hudi.config.HoodieWriteConfig;
 import org.apache.hudi.metrics.Metrics;
 import org.apache.hudi.storage.HoodieStorage;
@@ -27,16 +28,28 @@ import com.codahale.metrics.Counter;
 import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.SlidingWindowReservoir;
 import com.codahale.metrics.Timer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.concurrent.TimeUnit;
 
 public class HoodieLockMetrics {
+  private static final Logger LOG = LoggerFactory.getLogger(HoodieLockMetrics.class);
 
   public static final String LOCK_ACQUIRE_ATTEMPTS_COUNTER_NAME = "lock.acquire.attempts";
   public static final String LOCK_ACQUIRE_SUCCESS_COUNTER_NAME = "lock.acquire.success";
   public static final String LOCK_ACQUIRE_FAILURES_COUNTER_NAME = "lock.acquire.failure";
+  public static final String LOCK_RELEASE_SUCCESS_COUNTER_NAME = "lock.release.success";
   public static final String LOCK_ACQUIRE_DURATION_TIMER_NAME = "lock.acquire.duration";
   public static final String LOCK_REQUEST_LATENCY_TIMER_NAME = "lock.request.latency";
+  public static final String LOCK_ACQUIRED_BY_OTHERS_ERROR_COUNTER_NAME = "lock.acquired.others.error";
+  public static final String LOCK_STATE_UNKNOWN_COUNTER_NAME = "lock.state.unknown";
+  public static final String LOCK_ACQUIRE_PRECONDITION_FAILURE_COUNTER_NAME = "lock.acquire.precondition.failure";
+  public static final String LOCK_PROVIDER_FATAL_ERROR_COUNTER_NAME = "lock.provider.fatal.error";
+  public static final String LOCK_RELEASE_FAILURE_COUNTER_NAME = "lock.release.failure";
+  public static final String LOCK_EXPIRATION_DEADLINE_COUNTER_NAME = "lock.expiration.deadline";
+  public static final String LOCK_DANGLING_COUNTER_NAME = "lock.dangling";
+  public static final String LOCK_INTERRUPTED_COUNTER_NAME = "lock.interrupted";
   private final HoodieWriteConfig writeConfig;
   private final boolean isMetricsEnabled;
   private final int keepLastNtimes = 100;
@@ -45,6 +58,14 @@ public class HoodieLockMetrics {
   private transient Counter lockAttempts;
   private transient Counter successfulLockAttempts;
   private transient Counter failedLockAttempts;
+  private transient Counter lockReleaseSuccess;
+  private transient Counter lockAcquiredByOthersError;
+  private transient Counter lockStateUnknown;
+  private transient Counter lockAcquirePreconditionFailure;
+  private transient Counter lockProviderFatalError;
+  private transient Counter lockReleaseFailure;
+  private transient Counter lockDangling;
+  private transient Counter lockInterrupted;
   private transient Timer lockDuration;
   private transient Timer lockApiRequestDuration;
   private static final Object REGISTRY_LOCK = new Object();
@@ -61,7 +82,14 @@ public class HoodieLockMetrics {
       lockAttempts = registry.counter(getMetricsName(LOCK_ACQUIRE_ATTEMPTS_COUNTER_NAME));
       successfulLockAttempts = registry.counter(getMetricsName(LOCK_ACQUIRE_SUCCESS_COUNTER_NAME));
       failedLockAttempts = registry.counter(getMetricsName(LOCK_ACQUIRE_FAILURES_COUNTER_NAME));
-
+      lockReleaseSuccess = registry.counter(getMetricsName(LOCK_RELEASE_SUCCESS_COUNTER_NAME));
+      lockAcquiredByOthersError = registry.counter(getMetricsName(LOCK_ACQUIRED_BY_OTHERS_ERROR_COUNTER_NAME));
+      lockStateUnknown = registry.counter(getMetricsName(LOCK_STATE_UNKNOWN_COUNTER_NAME));
+      lockAcquirePreconditionFailure = registry.counter(getMetricsName(LOCK_ACQUIRE_PRECONDITION_FAILURE_COUNTER_NAME));
+      lockProviderFatalError = registry.counter(getMetricsName(LOCK_PROVIDER_FATAL_ERROR_COUNTER_NAME));
+      lockReleaseFailure = registry.counter(getMetricsName(LOCK_RELEASE_FAILURE_COUNTER_NAME));
+      lockDangling = registry.counter(getMetricsName(LOCK_DANGLING_COUNTER_NAME));
+      lockInterrupted = registry.counter(getMetricsName(LOCK_INTERRUPTED_COUNTER_NAME));
       lockDuration = createTimerForMetrics(registry, LOCK_ACQUIRE_DURATION_TIMER_NAME);
       lockApiRequestDuration = createTimerForMetrics(registry, LOCK_REQUEST_LATENCY_TIMER_NAME);
     }
@@ -76,9 +104,9 @@ public class HoodieLockMetrics {
     if (registry.getMetrics().get(metricName) == null) {
       synchronized (REGISTRY_LOCK) {
         if (registry.getMetrics().get(metricName) == null) {
-          lockDuration = new Timer(new SlidingWindowReservoir(keepLastNtimes));
-          registry.register(metricName, lockDuration);
-          return lockDuration;
+          Timer timer = new Timer(new SlidingWindowReservoir(keepLastNtimes));
+          registry.register(metricName, timer);
+          return timer;
         }
       }
     }
@@ -91,10 +119,18 @@ public class HoodieLockMetrics {
     }
   }
 
+  private static void updateMetric(HoodieTimer timer, Timer metric, String lockName) {
+    Option<Long> durationMs = timer.tryEndTimer();
+    if (durationMs.isPresent()) {
+      metric.update(durationMs.get(), TimeUnit.MILLISECONDS);
+    } else {
+      LOG.info("Unable to get lock {} duration", lockName);
+    }
+  }
+
   public void updateLockAcquiredMetric() {
     if (isMetricsEnabled) {
-      long durationMs = lockApiRequestDurationTimer.endTimer();
-      lockApiRequestDuration.update(durationMs, TimeUnit.MILLISECONDS);
+      updateMetric(lockApiRequestDurationTimer, lockApiRequestDuration, "acquired");
       lockAttempts.inc();
       successfulLockAttempts.inc();
       lockDurationTimer.startTimer();
@@ -103,16 +139,68 @@ public class HoodieLockMetrics {
 
   public void updateLockNotAcquiredMetric() {
     if (isMetricsEnabled) {
-      long durationMs = lockApiRequestDurationTimer.endTimer();
-      lockApiRequestDuration.update(durationMs, TimeUnit.MILLISECONDS);
+      updateMetric(lockApiRequestDurationTimer, lockApiRequestDuration, "acquired");
       failedLockAttempts.inc();
     }
   }
 
   public void updateLockHeldTimerMetrics() {
     if (isMetricsEnabled && lockDurationTimer != null) {
-      long lockDurationInMs = lockDurationTimer.endTimer();
-      lockDuration.update(lockDurationInMs, TimeUnit.MILLISECONDS);
+      updateMetric(lockDurationTimer, lockDuration, "held");
+    }
+  }
+
+  public void updateLockReleaseSuccessMetric() {
+    if (isMetricsEnabled) {
+      lockReleaseSuccess.inc();
+    }
+  }
+
+  public void updateLockAcquiredByOthersErrorMetric() {
+    if (isMetricsEnabled) {
+      lockAcquiredByOthersError.inc();
+    }
+  }
+
+  public void updateLockStateUnknownMetric() {
+    if (isMetricsEnabled) {
+      lockStateUnknown.inc();
+    }
+  }
+
+  public void updateLockAcquirePreconditionFailureMetric() {
+    if (isMetricsEnabled) {
+      lockAcquirePreconditionFailure.inc();
+    }
+  }
+
+  public void updateLockProviderFatalErrorMetric() {
+    if (isMetricsEnabled) {
+      lockProviderFatalError.inc();
+    }
+  }
+
+  public void updateLockReleaseFailureMetric() {
+    if (isMetricsEnabled) {
+      lockReleaseFailure.inc();
+    }
+  }
+
+  public void updateLockExpirationDeadlineMetric(int deadlineMs) {
+    if (isMetricsEnabled) {
+      metrics.registerGauge(getMetricsName(LOCK_EXPIRATION_DEADLINE_COUNTER_NAME), deadlineMs >= 0 ? (long)deadlineMs : 0);
+    }
+  }
+
+  public void updateLockDanglingMetric() {
+    if (isMetricsEnabled) {
+      this.lockDangling.inc();
+    }
+  }
+
+  public void updateLockInterruptedMetric() {
+    if (isMetricsEnabled) {
+      this.lockInterrupted.inc();
     }
   }
 }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/models/LockProviderHeartbeatManager.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/models/LockProviderHeartbeatManager.java
index a91193d5dc05..17fd37e4badc 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/models/LockProviderHeartbeatManager.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/lock/models/LockProviderHeartbeatManager.java
@@ -179,7 +179,7 @@ public class LockProviderHeartbeatManager implements HeartbeatManager {
     }
 
     if (this.hasActiveHeartbeat()) {
-      logger.warn("Owner {}: Heartbeat is already running.", ownerId);
+      logger.error("Owner {}: Heartbeat is already running.", ownerId);
       return false;
     }
     try {
@@ -213,7 +213,7 @@ public class LockProviderHeartbeatManager implements HeartbeatManager {
 
     // Call synchronized method after releasing the semaphore
     if (!heartbeatExecutionSuccessful) {
-      logger.warn("Owner {}: Heartbeat function did not succeed.", ownerId);
+      logger.error("Owner {}: Heartbeat function did not succeed.", ownerId);
       // Unschedule self from further execution if heartbeat was unsuccessful.
       heartbeatTaskUnscheduleItself();
     }
@@ -268,7 +268,7 @@ public class LockProviderHeartbeatManager implements HeartbeatManager {
     if (heartbeatStillInflight) {
       // If waiting for cancellation was interrupted, do not log an error.
       if (Thread.currentThread().isInterrupted()) {
-        logger.warn("Owner {}: Heartbeat is still in flight due to interruption!", ownerId);
+        logger.info("Owner {}: Heartbeat is still in flight due to interruption!", ownerId);
       } else {
         logger.error("Owner {}: Heartbeat is still in flight!", ownerId);
       }
@@ -290,7 +290,7 @@ public class LockProviderHeartbeatManager implements HeartbeatManager {
    */
   private synchronized boolean cancelRecurringHeartbeatTask(boolean mayInterruptIfRunning) {
     if (!this.hasActiveHeartbeat()) {
-      logger.warn("Owner {}: No active heartbeat task to stop.", ownerId);
+      logger.info("Owner {}: No active heartbeat task to stop.", ownerId);
       return true;
     }
 
@@ -310,7 +310,7 @@ public class LockProviderHeartbeatManager implements HeartbeatManager {
       // means we wait any inflight task execution to finish synchronously.
       heartbeatStillInflight = !heartbeatSemaphore.tryAcquire(stopHeartbeatTimeoutMs, TimeUnit.MILLISECONDS);
       if (heartbeatStillInflight) {
-        logger.warn("Owner {}: Timed out while waiting for heartbeat termination.", ownerId);
+        logger.error("Owner {}: Timed out while waiting for heartbeat termination.", ownerId);
       }
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/utils/ArchivalUtils.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/utils/ArchivalUtils.java
index 3a6d2509ad9b..34a8f17b04ce 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/utils/ArchivalUtils.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/utils/ArchivalUtils.java
@@ -130,7 +130,7 @@ public class ArchivalUtils {
           metaClient.getTableConfig().getTimelineTimezone());
     } catch (ParseException e) {
       if (NOT_PARSABLE_TIMESTAMPS.stream().noneMatch(ts -> latestCommit.get().getTimestamp().startsWith(ts))) {
-        LOG.warn("Error parsing instant time: " + latestCommit.get().getTimestamp());
+        LOG.info("Error parsing instant time: " + latestCommit.get().getTimestamp());
       }
     }
     return earliestCommitToRetain;
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieIndexConfig.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieIndexConfig.java
index ffe902f7d4e0..305443b0267c 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieIndexConfig.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/HoodieIndexConfig.java
@@ -754,14 +754,14 @@ public class HoodieIndexConfig extends HoodieConfig {
         if (StringUtils.isNullOrEmpty(hoodieIndexConfig.getString(BUCKET_INDEX_MAX_NUM_BUCKETS))) {
           hoodieIndexConfig.setValue(BUCKET_INDEX_MAX_NUM_BUCKETS, Integer.toString(bucketNum));
         } else if (hoodieIndexConfig.getInt(BUCKET_INDEX_MAX_NUM_BUCKETS) < bucketNum) {
-          LOG.warn("Maximum bucket number is smaller than bucket number, maximum: " + hoodieIndexConfig.getInt(BUCKET_INDEX_MAX_NUM_BUCKETS) + ", bucketNum: " + bucketNum);
+          LOG.warn("Maximum bucket number is smaller than bucket number, maximum: {}, bucketNum: {}", hoodieIndexConfig.getInt(BUCKET_INDEX_MAX_NUM_BUCKETS), bucketNum);
           hoodieIndexConfig.setValue(BUCKET_INDEX_MAX_NUM_BUCKETS, Integer.toString(bucketNum));
         }
 
         if (StringUtils.isNullOrEmpty(hoodieIndexConfig.getString(BUCKET_INDEX_MIN_NUM_BUCKETS))) {
           hoodieIndexConfig.setValue(BUCKET_INDEX_MIN_NUM_BUCKETS, Integer.toString(bucketNum));
         } else if (hoodieIndexConfig.getInt(BUCKET_INDEX_MIN_NUM_BUCKETS) > bucketNum) {
-          LOG.warn("Minimum bucket number is larger than the bucket number, minimum: " + hoodieIndexConfig.getInt(BUCKET_INDEX_MIN_NUM_BUCKETS) + ", bucketNum: " + bucketNum);
+          LOG.warn("Minimum bucket number is larger than the bucket number, minimum: {}, bucketNum: {}", hoodieIndexConfig.getInt(BUCKET_INDEX_MIN_NUM_BUCKETS), bucketNum);
           hoodieIndexConfig.setValue(BUCKET_INDEX_MIN_NUM_BUCKETS, Integer.toString(bucketNum));
         }
       }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/StorageBasedLockConfig.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/StorageBasedLockConfig.java
index 8f4d4779ad1c..4cfa9f27a3a1 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/StorageBasedLockConfig.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/config/StorageBasedLockConfig.java
@@ -42,11 +42,12 @@ public class StorageBasedLockConfig extends HoodieConfig {
               + "The lock provider will attempt to renew its lock until it successfully extends the lock lease period "
               + "or the validity timeout is reached.");
 
-  public static final ConfigProperty<Long> HEARTBEAT_POLL_SECONDS = ConfigProperty
-      .key(STORAGE_BASED_LOCK_PROPERTY_PREFIX + "heartbeat.poll.secs")
+  public static final ConfigProperty<Long> RENEW_INTERVAL_SECS = ConfigProperty
+      .key(STORAGE_BASED_LOCK_PROPERTY_PREFIX + "renew.interval.secs")
       .defaultValue(30L)
       .markAdvanced()
       .sinceVersion(SINCE_VERSION_1_0_2)
+      .withAlternatives(STORAGE_BASED_LOCK_PROPERTY_PREFIX + "heartbeat.poll.secs")
       .withDocumentation(
           "For storage-based lock provider, the amount of time in seconds to wait before renewing the lock. "
               + "Defaults to 30 seconds.");
@@ -55,8 +56,8 @@ public class StorageBasedLockConfig extends HoodieConfig {
     return getLong(VALIDITY_TIMEOUT_SECONDS);
   }
 
-  public long getHeartbeatPollSeconds() {
-    return getLong(HEARTBEAT_POLL_SECONDS);
+  public long getRenewIntervalSecs() {
+    return getLong(RENEW_INTERVAL_SECS);
   }
 
   public String getHudiTableBasePath() {
@@ -82,18 +83,18 @@ public class StorageBasedLockConfig extends HoodieConfig {
       if (!lockConfig.contains(BASE_PATH)) {
         throw new IllegalArgumentException(BASE_PATH.key() + notExistsMsg);
       }
-      if (lockConfig.getLongOrDefault(VALIDITY_TIMEOUT_SECONDS) < lockConfig.getLongOrDefault(HEARTBEAT_POLL_SECONDS)
+      if (lockConfig.getLongOrDefault(VALIDITY_TIMEOUT_SECONDS) < lockConfig.getLongOrDefault(RENEW_INTERVAL_SECS)
           * 10) {
         throw new IllegalArgumentException(
-            VALIDITY_TIMEOUT_SECONDS.key() + " should be greater than or equal to 10x " + HEARTBEAT_POLL_SECONDS.key());
+            VALIDITY_TIMEOUT_SECONDS.key() + " should be greater than or equal to 10x " + RENEW_INTERVAL_SECS.key());
       }
       if (lockConfig.getLongOrDefault(VALIDITY_TIMEOUT_SECONDS) < 10) {
         throw new IllegalArgumentException(
             VALIDITY_TIMEOUT_SECONDS.key() + " should be greater than or equal to 10 seconds.");
       }
-      if (lockConfig.getLongOrDefault(HEARTBEAT_POLL_SECONDS) < 1) {
+      if (lockConfig.getLongOrDefault(RENEW_INTERVAL_SECS) < 1) {
         throw new IllegalArgumentException(
-            HEARTBEAT_POLL_SECONDS.key() + " should be greater than or equal to 1 second.");
+            RENEW_INTERVAL_SECS.key() + " should be greater than or equal to 1 second.");
       }
     }
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/execution/CopyOnWriteInsertHandler.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/execution/CopyOnWriteInsertHandler.java
index 0191b8f9d3a8..4511bf99aea6 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/execution/CopyOnWriteInsertHandler.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/execution/CopyOnWriteInsertHandler.java
@@ -87,7 +87,7 @@ public class CopyOnWriteInsertHandler<T>
         return;
       }
     } catch (IOException e) {
-      LOG.warn("Writing record should be ignore " + record, e);
+      LOG.warn("Writing record should be ignore {}", record, e);
     }
     HoodieWriteHandle<?,?,?,?> handle = handles.get(partitionPath);
     if (handle == null) {
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bloom/HoodieBloomIndex.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bloom/HoodieBloomIndex.java
index 99fc4a33b07e..739e0130a342 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bloom/HoodieBloomIndex.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bloom/HoodieBloomIndex.java
@@ -149,6 +149,7 @@ public class HoodieBloomIndex extends HoodieIndex<Object, Object> {
       }
       // fallback to loading column ranges from files
       if (isNullOrEmpty(fileInfoList)) {
+        LOG.info("fallback to loading column ranges from files");
         fileInfoList = loadColumnRangesFromFiles(affectedPartitionPathList, context, hoodieTable);
       }
     } else {
@@ -175,7 +176,7 @@ public class HoodieBloomIndex extends HoodieIndex<Object, Object> {
         String[] minMaxKeys = rangeInfoHandle.getMinMaxKeys(pf.getValue().getValue());
         return Pair.of(pf.getKey(), new BloomIndexFileInfo(pf.getValue().getKey(), minMaxKeys[0], minMaxKeys[1]));
       } catch (MetadataNotFoundException me) {
-        LOG.warn("Unable to find range metadata in file :" + pf);
+        LOG.warn("Unable to find range metadata in file :{}", pf);
         return Pair.of(pf.getKey(), new BloomIndexFileInfo(pf.getValue().getKey()));
       }
     }, Math.max(partitionPathFileIDList.size(), 1));
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bucket/ConsistentBucketIndexUtils.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bucket/ConsistentBucketIndexUtils.java
index 7473e0d66329..0fc14c167826 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bucket/ConsistentBucketIndexUtils.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/index/bucket/ConsistentBucketIndexUtils.java
@@ -225,7 +225,7 @@ public class ConsistentBucketIndexUtils {
       if (!storage.exists(fullPath)) {
         throw e;
       }
-      LOG.warn("Failed to create marker but " + fullPath + " exists", e);
+      LOG.warn("Failed to create marker but {} exists", fullPath, e);
     }
   }
 
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/HoodieBackedTableMetadataWriter.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/HoodieBackedTableMetadataWriter.java
index a69d8ebadbaf..68f93a7dcfbf 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/HoodieBackedTableMetadataWriter.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/metadata/HoodieBackedTableMetadataWriter.java
@@ -794,10 +794,10 @@ public abstract class HoodieBackedTableMetadataWriter<I> implements HoodieTableM
       String partitionPath = partitionType.getPartitionPath();
       // first update table config
       dataMetaClient.getTableConfig().setMetadataPartitionState(dataMetaClient, partitionType, false);
-      LOG.warn("Deleting Metadata Table partition: " + partitionPath);
+      LOG.info("Deleting Metadata Table partition: " + partitionPath);
       dataMetaClient.getStorage().deleteDirectory(new StoragePath(metadataWriteConfig.getBasePath(), partitionPath));
       // delete corresponding pending indexing instant file in the timeline
-      LOG.warn("Deleting pending indexing instant from the timeline for partition: {}", partitionPath);
+      LOG.info("Deleting pending indexing instant from the timeline for partition: {}", partitionPath);
       deletePendingIndexingInstant(dataMetaClient, partitionPath);
     }
     closeInternal();
@@ -882,7 +882,7 @@ public abstract class HoodieBackedTableMetadataWriter<I> implements HoodieTableM
 
   public void buildMetadataPartitions(HoodieEngineContext engineContext, List<HoodieIndexPartitionInfo> indexPartitionInfos) throws IOException {
     if (indexPartitionInfos.isEmpty()) {
-      LOG.warn("No partition to index in the plan");
+      LOG.debug("No partition to index in the plan");
       return;
     }
     String indexUptoInstantTime = indexPartitionInfos.get(0).getIndexUptoInstant();
@@ -1378,7 +1378,7 @@ public abstract class HoodieBackedTableMetadataWriter<I> implements HoodieTableM
     Option<HoodieInstant> pendingCompactionInstant =
         metadataMetaClient.getActiveTimeline().filterPendingCompactionTimeline().firstInstant();
     if (pendingLogCompactionInstant.isPresent() || pendingCompactionInstant.isPresent()) {
-      LOG.warn(String.format("Not scheduling compaction or logCompaction, since a pending compaction instant %s or logCompaction %s instant is present",
+      LOG.info(String.format("Not scheduling compaction or logCompaction, since a pending compaction instant %s or logCompaction %s instant is present",
           pendingCompactionInstant, pendingLogCompactionInstant));
       return false;
     }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanActionExecutor.java
index 6973d76c5d06..52537d0df2bd 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanActionExecutor.java
@@ -258,7 +258,7 @@ public class CleanActionExecutor<T, I, K, O> extends BaseActionExecutor<T, I, K,
         FileBasedInternalSchemaStorageManager fss = new FileBasedInternalSchemaStorageManager(table.getMetaClient());
         fss.cleanOldFiles(pendingCleanInstants.stream().map(is -> is.getTimestamp()).collect(Collectors.toList()));
       } catch (Exception e) {
-        // we should not affect original clean logic. Swallow exception and log warn.
+        // we should not affect original clean logic. Swallow exception and log.
         LOG.warn("failed to clean old history schema");
       }
 
@@ -292,7 +292,7 @@ public class CleanActionExecutor<T, I, K, O> extends BaseActionExecutor<T, I, K,
   private void checkIfOtherWriterCommitted(HoodieInstant hoodieInstant, HoodieIOException e) {
     table.getMetaClient().reloadActiveTimeline();
     if (table.getCleanTimeline().filterCompletedInstants().containsInstant(hoodieInstant.getTimestamp())) {
-      LOG.warn("Clean operation was completed by another writer for instant: " + hoodieInstant);
+      LOG.info("Clean operation was completed by another writer for instant: " + hoodieInstant);
     } else {
       LOG.error("Failed to perform previous clean operation, instant: " + hoodieInstant, e);
       throw e;
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanPlanner.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanPlanner.java
index 04e1601b558e..9e94913334ed 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanPlanner.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/clean/CleanPlanner.java
@@ -473,7 +473,7 @@ public class CleanPlanner<T, I, K, O> implements Serializable {
       return fsView.getAllFileGroups(partitionPath).findAny().isPresent();
     } catch (Exception ex) {
       // if any exception throws, assume there are existing pending files
-      LOG.warn("Error while checking the pending files under partition: " + partitionPath + ", assumes the files exist", ex);
+      LOG.warn("Error while checking the pending files under partition: {}, we will assume that pending files exist", partitionPath, ex);
       return true;
     }
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/ClusteringPlanActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/ClusteringPlanActionExecutor.java
index 91f64b1cee9f..634b42c1acc1 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/ClusteringPlanActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/ClusteringPlanActionExecutor.java
@@ -56,7 +56,7 @@ public class ClusteringPlanActionExecutor<T, I, K, O> extends BaseActionExecutor
   }
 
   protected Option<HoodieClusteringPlan> createClusteringPlan() {
-    LOG.info("Checking if clustering needs to be run on " + config.getBasePath());
+    LOG.info("Checking if clustering needs to be run on {}", config.getBasePath());
     Option<HoodieInstant> lastClusteringInstant =
         table.getActiveTimeline().getLastClusteringInstant();
 
@@ -65,20 +65,20 @@ public class ClusteringPlanActionExecutor<T, I, K, O> extends BaseActionExecutor
         .countInstants();
 
     if (config.inlineClusteringEnabled() && config.getInlineClusterMaxCommits() > commitsSinceLastClustering) {
-      LOG.warn("Not scheduling inline clustering as only " + commitsSinceLastClustering
-          + " commits was found since last clustering " + lastClusteringInstant + ". Waiting for "
-          + config.getInlineClusterMaxCommits());
+      LOG.info("Not scheduling inline clustering as only {} commits was found since last clustering {}. Waiting for {}",
+          commitsSinceLastClustering, lastClusteringInstant,
+          config.getInlineClusterMaxCommits());
       return Option.empty();
     }
 
     if (config.isAsyncClusteringEnabled() && config.getAsyncClusterMaxCommits() > commitsSinceLastClustering) {
-      LOG.warn("Not scheduling async clustering as only " + commitsSinceLastClustering
+      LOG.info("Not scheduling async clustering as only " + commitsSinceLastClustering
           + " commits was found since last clustering " + lastClusteringInstant + ". Waiting for "
           + config.getAsyncClusterMaxCommits());
       return Option.empty();
     }
 
-    LOG.info("Generating clustering plan for table " + config.getBasePath());
+    LOG.info("Generating clustering plan for table {}", config.getBasePath());
     ClusteringPlanStrategy strategy = (ClusteringPlanStrategy) ReflectionUtils.loadClass(
         ClusteringPlanStrategy.checkAndGetClusteringPlanStrategy(config),
             new Class<?>[] {HoodieTable.class, HoodieEngineContext.class, HoodieWriteConfig.class}, table, context, config);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/ClusteringPlanStrategy.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/ClusteringPlanStrategy.java
index a6894388f6d2..f43011f18633 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/ClusteringPlanStrategy.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/ClusteringPlanStrategy.java
@@ -72,18 +72,18 @@ public abstract class ClusteringPlanStrategy<T,I,K,O> implements Serializable {
     String javaSelectedPartitionClassName = "org.apache.hudi.client.clustering.plan.strategy.JavaRecentDaysClusteringPlanStrategy";
     String javaSizeBasedClassName = HoodieClusteringConfig.JAVA_SIZED_BASED_CLUSTERING_PLAN_STRATEGY;
 
-    String logStr = "The clustering plan '%s' is deprecated. Please set the plan as '%s' and set '%s' as '%s' to achieve the same behaviour";
+    String logStr = "The clustering plan '{}' is deprecated. Please set the plan as '{}' and set '{}' as '{}' to achieve the same behaviour";
     if (sparkRecentDaysClassName.equals(className)) {
       config.setValue(HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME, ClusteringPlanPartitionFilterMode.RECENT_DAYS.name());
-      LOG.warn(String.format(logStr, className, sparkSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.RECENT_DAYS.name()));
+      LOG.info(logStr, className, sparkSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.RECENT_DAYS.name());
       return sparkSizeBasedClassName;
     } else if (sparkSelectedPartitionsClassName.equals(className)) {
       config.setValue(HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME, ClusteringPlanPartitionFilterMode.SELECTED_PARTITIONS.name());
-      LOG.warn(String.format(logStr, className, sparkSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.SELECTED_PARTITIONS.name()));
+      LOG.info(logStr, className, sparkSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.SELECTED_PARTITIONS.name());
       return sparkSizeBasedClassName;
     } else if (javaSelectedPartitionClassName.equals(className)) {
       config.setValue(HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME, ClusteringPlanPartitionFilterMode.RECENT_DAYS.name());
-      LOG.warn(String.format(logStr, className, javaSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.SELECTED_PARTITIONS.name()));
+      LOG.info(logStr, className, javaSizeBasedClassName, HoodieClusteringConfig.PLAN_PARTITION_FILTER_MODE_NAME.key(), ClusteringPlanPartitionFilterMode.SELECTED_PARTITIONS.name());
       return javaSizeBasedClassName;
     }
     return className;
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/PartitionAwareClusteringPlanStrategy.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/PartitionAwareClusteringPlanStrategy.java
index 515d65328715..776f1578cd92 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/PartitionAwareClusteringPlanStrategy.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/cluster/strategy/PartitionAwareClusteringPlanStrategy.java
@@ -163,7 +163,7 @@ public abstract class PartitionAwareClusteringPlanStrategy<T,I,K,O> extends Clus
         .collect(Collectors.toList());
 
     if (clusteringGroups.isEmpty()) {
-      LOG.warn("No data available to cluster");
+      LOG.info("No data available to cluster");
       return Option.empty();
     }
 
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/ScheduleCompactionActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/ScheduleCompactionActionExecutor.java
index e28c1ccd1439..cbe4af9ae540 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/ScheduleCompactionActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/ScheduleCompactionActionExecutor.java
@@ -94,7 +94,7 @@ public class ScheduleCompactionActionExecutor<T, I, K, O> extends BaseActionExec
         // if there are inflight writes, their instantTime must not be less than that of compaction instant time
         Option<HoodieInstant> earliestInflightOpt = table.getActiveTimeline().getCommitsTimeline().filterPendingExcludingMajorAndMinorCompaction().firstInstant();
         if (earliestInflightOpt.isPresent() && !HoodieTimeline.compareTimestamps(earliestInflightOpt.get().getTimestamp(), HoodieTimeline.GREATER_THAN, instantTime)) {
-          LOG.warn("Earliest write inflight instant time must be later than compaction time. Earliest :" + earliestInflightOpt.get()
+          LOG.info("Earliest write inflight instant time must be later than compaction time. Earliest :" + earliestInflightOpt.get()
               + ", Compaction scheduled at " + instantTime + ". Hence skipping to schedule compaction");
           return Option.empty();
         }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/plan/generators/BaseHoodieCompactionPlanGenerator.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/plan/generators/BaseHoodieCompactionPlanGenerator.java
index 4024cf23394e..a1685fddebfb 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/plan/generators/BaseHoodieCompactionPlanGenerator.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/compact/plan/generators/BaseHoodieCompactionPlanGenerator.java
@@ -136,12 +136,12 @@ public abstract class BaseHoodieCompactionPlanGenerator<T extends HoodieRecordPa
     LOG.info("Total number of file slices " + totalFileSlices.value());
 
     if (operations.isEmpty()) {
-      LOG.warn("No operations are retrieved for {}", metaClient.getBasePath());
+      LOG.info("No operations are retrieved for {} for table {}", metaClient.getBasePath(), hoodieTable.getConfig().getBasePath());
       return null;
     }
 
     if (totalLogFiles.value() <= 0) {
-      LOG.warn("No log files are retrieved for {}", metaClient.getBasePath());
+      LOG.info("No log files are retrieved for {} for table {}", metaClient.getBasePath(), hoodieTable.getConfig().getBasePath());
       return null;
     }
 
@@ -154,7 +154,7 @@ public abstract class BaseHoodieCompactionPlanGenerator<T extends HoodieRecordPa
             + "Please fix your strategy implementation. FileIdsWithPendingCompactions :" + fgIdsInPendingCompactionAndClustering
             + ", Selected workload :" + compactionPlan);
     if (compactionPlan.getOperations().isEmpty()) {
-      LOG.warn("After filtering, Nothing to compact for {}", metaClient.getBasePath());
+      LOG.info("After filtering, Nothing to compact for {} for table {}", metaClient.getBasePath(), hoodieTable.getConfig().getBasePath());
     }
     return compactionPlan;
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/AbstractIndexingCatchupTask.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/AbstractIndexingCatchupTask.java
index 70be1b76f91b..ae5efd27164b 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/AbstractIndexingCatchupTask.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/AbstractIndexingCatchupTask.java
@@ -156,7 +156,7 @@ public abstract class AbstractIndexingCatchupTask implements IndexingCatchupTask
     }
     if (!instant.isCompleted()) {
       try {
-        LOG.warn("instant not completed, reloading timeline " + instant);
+        LOG.info("instant not completed, reloading timeline {}", instant);
         reloadTimelineWithWait(instant);
       } catch (InterruptedException e) {
         throw new HoodieIndexException(String.format("Thread interrupted while running indexing check for instant: %s", instant), e);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/RunIndexActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/RunIndexActionExecutor.java
index 4fc3974cfb93..12118b8a13c0 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/RunIndexActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/index/RunIndexActionExecutor.java
@@ -145,14 +145,14 @@ public class RunIndexActionExecutor<T, I, K, O> extends BaseActionExecutor<T, I,
                 "Could not get metadata writer to run index action for instant: %s", instantTime)))) {
           // this will only build index upto base instant as generated by the plan, we will be doing catchup later
           String indexUptoInstant = indexPartitionInfos.get(0).getIndexUptoInstant();
-          LOG.info("Starting Index Building with base instant: " + indexUptoInstant);
+          LOG.info("Starting Index Building with base instant: {}", indexUptoInstant);
           HoodieTimer timer = HoodieTimer.start();
           metadataWriter.buildMetadataPartitions(context, indexPartitionInfos);
           metrics.ifPresent(m -> m.updateMetrics(HoodieMetadataMetrics.INITIALIZE_STR, timer.endTimer()));
 
           // get remaining instants to catchup
           List<HoodieInstant> instantsToCatchup = getInstantsToCatchup(indexUptoInstant);
-          LOG.info("Total remaining instants to index: " + instantsToCatchup.size());
+          LOG.info("Total remaining instants to index: {}", instantsToCatchup.size());
 
           // reconcile with metadata table timeline
           String metadataBasePath = getMetadataTableBasePath(table.getMetaClient().getBasePath().toString());
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/restore/BaseRestoreActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/restore/BaseRestoreActionExecutor.java
index 30a02f2b458e..b0bac3574cd9 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/restore/BaseRestoreActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/restore/BaseRestoreActionExecutor.java
@@ -110,7 +110,7 @@ public abstract class BaseRestoreActionExecutor<T, I, K, O> extends BaseActionEx
       if (rollbackInstantOpt.isPresent()) {
         instantsToRollback.add(rollbackInstantOpt.get());
       } else {
-        LOG.warn("Ignoring already rolledback instant " + instantInfo.toString());
+        LOG.info("Ignoring already rolledback instant {}", instantInfo.toString());
       }
     }
     return instantsToRollback;
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackActionExecutor.java
index 868b4e2a7189..a97814172141 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackActionExecutor.java
@@ -288,7 +288,7 @@ public abstract class BaseRollbackActionExecutor<T, I, K, O> extends BaseActionE
                                                    HoodieInstant instantToBeDeleted) {
     // Remove the rolled back inflight commits
     if (deleteInstant) {
-      LOG.info("Deleting instant=" + instantToBeDeleted);
+      LOG.info("Deleting instant={}", instantToBeDeleted);
       activeTimeline.deletePending(instantToBeDeleted);
       if (instantToBeDeleted.isInflight() && !table.getMetaClient().getTimelineLayoutVersion().isNullVersion()) {
         // Delete corresponding requested instant
@@ -296,9 +296,9 @@ public abstract class BaseRollbackActionExecutor<T, I, K, O> extends BaseActionE
             instantToBeDeleted.getTimestamp());
         activeTimeline.deletePending(instantToBeDeleted);
       }
-      LOG.info("Deleted pending commit " + instantToBeDeleted);
+      LOG.info("Deleted pending commit {}", instantToBeDeleted);
     } else {
-      LOG.warn("Rollback finished without deleting inflight instant file. Instant=" + instantToBeDeleted);
+      LOG.info("Rollback finished without deleting inflight instant file. Instant={}", instantToBeDeleted);
     }
   }
 
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackHelper.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackHelper.java
index 2380b5120d68..26ebfee25deb 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackHelper.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackHelper.java
@@ -107,7 +107,7 @@ public class BaseRollbackHelper implements Serializable {
     try {
       logPaths = markers.getAppendedLogPaths(context, config.getFinalizeWriteParallelism());
     } catch (FileNotFoundException fnf) {
-      LOG.warn("Rollback never failed and hence no marker dir was found. Safely moving on");
+      LOG.info("Rollback never failed and hence no marker dir was found. Safely moving on");
     } catch (IOException e) {
       throw new HoodieRollbackException("Failed to list log file markers for previous attempt of rollback ", e);
     }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackPlanActionExecutor.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackPlanActionExecutor.java
index cfda47115586..67c3604ea80d 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackPlanActionExecutor.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/action/rollback/BaseRollbackPlanActionExecutor.java
@@ -118,7 +118,7 @@ public class BaseRollbackPlanActionExecutor<T, I, K, O> extends BaseActionExecut
           return Option.empty();
         }
         if (table.getRollbackTimeline().filterInflightsAndRequested().containsInstant(rollbackInstant.getTimestamp())) {
-          LOG.warn("Request Rollback found with instant time " + rollbackInstant + ", hence skipping scheduling rollback");
+          LOG.info("Request Rollback found with instant time " + rollbackInstant + ", hence skipping scheduling rollback");
         } else {
           table.getActiveTimeline().saveToRollbackRequested(rollbackInstant, TimelineMetadataUtils.serializeRollbackPlan(rollbackPlan));
           table.getMetaClient().reloadActiveTimeline();
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/DirectWriteMarkers.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/DirectWriteMarkers.java
index 961c0f0cea67..6aaf2d496e2f 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/DirectWriteMarkers.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/DirectWriteMarkers.java
@@ -255,7 +255,7 @@ public class DirectWriteMarkers extends WriteMarkers {
     }
     try {
       if (checkIfExists && storage.exists(markerPath)) {
-        LOG.warn("Marker Path=" + markerPath + " already exists, cancel creation");
+        LOG.info("Marker Path={} already exists, cancel creation", markerPath);
         return Option.empty();
       }
       LOG.info("Creating Marker Path=" + markerPath);
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/MarkerBasedRollbackUtils.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/MarkerBasedRollbackUtils.java
index e3caacf402e5..99e30d5ec1fc 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/MarkerBasedRollbackUtils.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/MarkerBasedRollbackUtils.java
@@ -73,8 +73,8 @@ public class MarkerBasedRollbackUtils {
       try {
         return new ArrayList<>(writeMarkers.allMarkerFilePaths());
       } catch (IOException | IllegalArgumentException e) {
-        LOG.warn(String.format("%s not present and %s marker failed with error: %s. So, falling back to %s marker",
-            MARKER_TYPE_FILENAME, DIRECT, e.getMessage(), TIMELINE_SERVER_BASED));
+        LOG.warn("{} not present and {} marker failed with error: {}. Falling back to {} marker",
+            MARKER_TYPE_FILENAME, DIRECT, e.getMessage(), TIMELINE_SERVER_BASED);
         return getTimelineServerBasedMarkers(context, parallelism, markerDir, storage);
       }
     }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleDirectMarkerBasedDetectionStrategy.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleDirectMarkerBasedDetectionStrategy.java
index 8a0c5f4220f4..5628c831623c 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleDirectMarkerBasedDetectionStrategy.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleDirectMarkerBasedDetectionStrategy.java
@@ -66,7 +66,7 @@ public class SimpleDirectMarkerBasedDetectionStrategy extends DirectMarkerBasedD
       return checkMarkerConflict(basePath, maxAllowableHeartbeatIntervalInMs)
           || (checkCommitConflict && MarkerUtils.hasCommitConflict(activeTimeline, Stream.of(fileId).collect(Collectors.toSet()), completedCommitInstants));
     } catch (IOException e) {
-      LOG.warn("Exception occurs during create marker file in eager conflict detection mode.");
+      LOG.error("Exception occurs during create marker file in eager conflict detection mode.", e);
       throw new HoodieIOException("Exception occurs during create marker file in eager conflict detection mode.", e);
     }
   }
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleTransactionDirectMarkerBasedDetectionStrategy.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleTransactionDirectMarkerBasedDetectionStrategy.java
index 3d984ba781cf..440d587a2c0c 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleTransactionDirectMarkerBasedDetectionStrategy.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/SimpleTransactionDirectMarkerBasedDetectionStrategy.java
@@ -56,7 +56,7 @@ public class SimpleTransactionDirectMarkerBasedDetectionStrategy
       super.detectAndResolveConflictIfNecessary();
 
     } catch (Exception e) {
-      LOG.warn("Exception occurs during create marker file in early conflict detection mode within transaction.");
+      LOG.error("Exception occurs during create marker file in early conflict detection mode within transaction.", e);
       throw e;
     } finally {
       // End transaction after created marker file.
diff --git a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkers.java b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkers.java
index cd9f67b5b203..aef9d75ba5ac 100644
--- a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkers.java
+++ b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/table/marker/WriteMarkers.java
@@ -148,7 +148,7 @@ public abstract class WriteMarkers implements Serializable {
       context.setJobStatus(this.getClass().getSimpleName(), "Deleting marker directory: " + basePath);
       deleteMarkerDir(context, parallelism);
     } catch (Exception e) {
-      LOG.warn("Error deleting marker directory for instant " + instantTime, e);
+      LOG.warn("Error deleting marker directory for instant {}", instantTime, e);
     }
   }
 
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/callback/client/http/TestCallbackHttpClient.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/callback/client/http/TestCallbackHttpClient.java
index 2de4ed08524c..5196bfce1c92 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/callback/client/http/TestCallbackHttpClient.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/callback/client/http/TestCallbackHttpClient.java
@@ -131,7 +131,7 @@ class TestCallbackHttpClient {
 
     verify(appender).append(logCaptor.capture());
     assertEquals("Failed to send callback message. Response was " + RESPONSE_UNAUTHORIZED, logCaptor.getValue().getMessage().getFormattedMessage());
-    assertEquals(Level.WARN, logCaptor.getValue().getLevel());
+    assertEquals(Level.ERROR, logCaptor.getValue().getLevel());
   }
 
   @Test
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestHoodieLockMetrics.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestHoodieLockMetrics.java
new file mode 100644
index 000000000000..73b863e80230
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestHoodieLockMetrics.java
@@ -0,0 +1,361 @@
+/*
+ * 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.hudi.client.transaction;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.hudi.client.transaction.lock.metrics.HoodieLockMetrics;
+import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.config.metrics.HoodieMetricsConfig;
+import org.apache.hudi.metrics.MetricsReporterType;
+import org.apache.hudi.storage.HoodieStorage;
+import org.apache.hudi.metrics.Metrics;
+
+import com.codahale.metrics.Gauge;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+public class TestHoodieLockMetrics {
+
+  @Test
+  public void testMetricsHappyPath() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/gdsafsd")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+        .forTable("idk").withPath("/dsfasdf/asdf")
+        .withMetricsConfig(metricsConfig)
+        .build(), storage);
+
+    //lock acquired
+    assertDoesNotThrow(lockMetrics::startLockApiTimerContext);
+    assertDoesNotThrow(lockMetrics::updateLockAcquiredMetric);
+    assertDoesNotThrow(lockMetrics::updateLockHeldTimerMetrics);
+
+    //lock not acquired
+    assertDoesNotThrow(lockMetrics::startLockApiTimerContext);
+    assertDoesNotThrow(lockMetrics::updateLockNotAcquiredMetric);
+  }
+
+  @Test
+  public void testMetricsMisses() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/gdsafsd")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+        .forTable("idk").withPath("/dsfasdf/asdf")
+        .withMetricsConfig(metricsConfig)
+        .build(), storage);
+
+    assertDoesNotThrow(lockMetrics::updateLockHeldTimerMetrics);
+    assertDoesNotThrow(lockMetrics::updateLockNotAcquiredMetric);
+    assertDoesNotThrow(lockMetrics::updateLockAcquiredMetric);
+  }
+
+  @Test
+  public void testLockReleaseSuccessMetric() {
+    // Test that lock release success metric is properly tracked
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .forTable("testTable").withPath("/test/path")
+        .withMetricsConfig(metricsConfig)
+        .build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(writeConfig, storage);
+
+    // Get the metrics registry to verify counter values
+    Metrics metrics = Metrics.getInstance(metricsConfig, storage);
+    MetricRegistry registry = metrics.getRegistry();
+
+    // Verify the lock release success counter exists
+    String metricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_RELEASE_SUCCESS_COUNTER_NAME;
+    Counter lockReleaseSuccessCounter = registry.getCounters().get(metricName);
+    assertNotNull(lockReleaseSuccessCounter, "Lock release success counter should exist");
+    
+    long initialCount = lockReleaseSuccessCounter.getCount();
+
+    // Simulate successful lock release
+    lockMetrics.updateLockReleaseSuccessMetric();
+    assertEquals(initialCount + 1, lockReleaseSuccessCounter.getCount(), "Lock release success counter should increment by 1");
+
+    // Simulate multiple successful lock releases
+    lockMetrics.updateLockReleaseSuccessMetric();
+    lockMetrics.updateLockReleaseSuccessMetric();
+    assertEquals(initialCount + 3, lockReleaseSuccessCounter.getCount(), "Lock release success counter should increment by 3 total");
+  }
+
+  @Test
+  public void testLockLifecycleWithReleaseSuccess() {
+    // Test complete lock lifecycle including acquisition and successful release
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .forTable("testTable").withPath("/test/path")
+        .withMetricsConfig(metricsConfig)
+        .build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(writeConfig, storage);
+
+    // Get the metrics registry to verify counter values
+    Metrics metrics = Metrics.getInstance(metricsConfig, storage);
+    MetricRegistry registry = metrics.getRegistry();
+
+    String acquireMetricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_ACQUIRE_SUCCESS_COUNTER_NAME;
+    String releaseMetricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_RELEASE_SUCCESS_COUNTER_NAME;
+    Counter lockAcquiredCounter = registry.getCounters().get(acquireMetricName);
+    Counter lockReleaseSuccessCounter = registry.getCounters().get(releaseMetricName);
+    
+    long initialAcquireCount = lockAcquiredCounter.getCount();
+    long initialReleaseCount = lockReleaseSuccessCounter.getCount();
+    
+    // Simulate complete lock lifecycle
+    lockMetrics.startLockApiTimerContext();
+    lockMetrics.updateLockAcquiredMetric();
+    assertEquals(initialAcquireCount + 1, lockAcquiredCounter.getCount(), "Lock acquired counter should increment by 1");
+    assertEquals(initialReleaseCount, lockReleaseSuccessCounter.getCount(), "Lock release success counter should not change yet");
+
+    // Now release the lock successfully
+    lockMetrics.updateLockReleaseSuccessMetric();
+    lockMetrics.updateLockHeldTimerMetrics();
+    assertEquals(initialAcquireCount + 1, lockAcquiredCounter.getCount(), "Lock acquired counter should still be incremented by 1");
+    assertEquals(initialReleaseCount + 1, lockReleaseSuccessCounter.getCount(), "Lock release success counter should increment by 1");
+
+    // Verify metrics balance for multiple cycles
+    for (int i = 0; i < 5; i++) {
+      lockMetrics.startLockApiTimerContext();
+      lockMetrics.updateLockAcquiredMetric();
+      lockMetrics.updateLockReleaseSuccessMetric();
+      lockMetrics.updateLockHeldTimerMetrics();
+    }
+    
+    assertEquals(initialAcquireCount + 6, lockAcquiredCounter.getCount(), "Lock acquired counter should increment by 6 total");
+    assertEquals(initialReleaseCount + 6, lockReleaseSuccessCounter.getCount(), "Lock release success counter should increment by 6 total");
+  }
+
+  @Test
+  public void testNewErrorMetrics() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+            .forTable("idk").withPath("/dsfasdf/asdf")
+            .withMetricsConfig(metricsConfig)
+            .build(), storage);
+
+    // Test all the new error metrics methods
+    assertDoesNotThrow(lockMetrics::updateLockAcquiredByOthersErrorMetric, 
+        "updateLockAcquiredByOthersErrorMetric should not throw");
+    assertDoesNotThrow(lockMetrics::updateLockStateUnknownMetric,
+        "updateLockStateUnknownMetric should not throw");
+    assertDoesNotThrow(lockMetrics::updateLockAcquirePreconditionFailureMetric,
+        "updateLockAcquirePreconditionFailureMetric should not throw");
+    assertDoesNotThrow(lockMetrics::updateLockProviderFatalErrorMetric,
+        "updateLockProviderFatalErrorMetric should not throw");
+  }
+
+  @Test
+  public void testNewErrorMetricsWithDisabledMetrics() {
+    // Test that the new metrics methods work safely when metrics are disabled
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(false).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+            .forTable("idk").withPath("/dsfasdf/asdf")
+            .withMetricsConfig(metricsConfig)
+            .build(), storage);
+
+    // All methods should be safe to call even when metrics are disabled
+    assertDoesNotThrow(lockMetrics::updateLockAcquiredByOthersErrorMetric,
+        "updateLockAcquiredByOthersErrorMetric should not throw when metrics disabled");
+    assertDoesNotThrow(lockMetrics::updateLockStateUnknownMetric,
+        "updateLockStateUnknownMetric should not throw when metrics disabled");
+    assertDoesNotThrow(lockMetrics::updateLockAcquirePreconditionFailureMetric,
+        "updateLockAcquirePreconditionFailureMetric should not throw when metrics disabled");
+    assertDoesNotThrow(lockMetrics::updateLockProviderFatalErrorMetric,
+        "updateLockProviderFatalErrorMetric should not throw when metrics disabled");
+  }
+
+  @Test
+  public void testCombinedMetricsScenario() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+            .forTable("idk").withPath("/dsfasdf/asdf")
+            .withMetricsConfig(metricsConfig)
+            .build(), storage);
+
+    // Test a realistic scenario combining old and new metrics
+    assertDoesNotThrow(() -> {
+      // Start lock acquisition
+      lockMetrics.startLockApiTimerContext();
+
+      // Lock acquisition failed due to unknown state
+      lockMetrics.updateLockStateUnknownMetric();
+      lockMetrics.updateLockNotAcquiredMetric();
+
+      // Retry - start another acquisition attempt
+      lockMetrics.startLockApiTimerContext();
+
+      // This time failed due to precondition failure
+      lockMetrics.updateLockAcquirePreconditionFailureMetric();
+      lockMetrics.updateLockNotAcquiredMetric();
+
+      // Final attempt - lock acquired by others
+      lockMetrics.startLockApiTimerContext();
+      lockMetrics.updateLockAcquiredByOthersErrorMetric();
+      lockMetrics.updateLockNotAcquiredMetric();
+
+    }, "Combined metrics scenario should not throw");
+  }
+
+  @Test
+  public void testLockInterruptedMetric() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+            .forTable("testTable").withPath("/test/path")
+            .withMetricsConfig(metricsConfig)
+            .build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(writeConfig, storage);
+
+    // Get the metrics registry to verify counter values
+    Metrics metrics = Metrics.getInstance(metricsConfig, storage);
+    MetricRegistry registry = metrics.getRegistry();
+    String metricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_INTERRUPTED_COUNTER_NAME;
+
+    // Test that the interrupted metric can be called
+    assertDoesNotThrow(lockMetrics::updateLockInterruptedMetric, 
+        "updateLockInterruptedMetric should not throw");
+
+    // Verify the counter exists and increments
+    Counter interruptedCounter = registry.getCounters().get(metricName);
+    assertNotNull(interruptedCounter, "Lock interrupted counter should exist");
+
+    long initialCount = interruptedCounter.getCount();
+
+    // Call the metric multiple times
+    lockMetrics.updateLockInterruptedMetric();
+    lockMetrics.updateLockInterruptedMetric();
+    lockMetrics.updateLockInterruptedMetric();
+
+    // Verify the counter incremented
+    assertEquals(initialCount + 3, interruptedCounter.getCount(), 
+        "Lock interrupted counter should increment by 3");
+  }
+
+  @Test
+  public void testLockExpirationDeadlineGauge() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .forTable("testTable").withPath("/test/path")
+        .withMetricsConfig(metricsConfig)
+        .build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(writeConfig, storage);
+
+    // Test that the method doesn't throw
+    assertDoesNotThrow(() -> lockMetrics.updateLockExpirationDeadlineMetric(5000), 
+        "updateLockExpirationDeadlineMetric should not throw");
+
+    // Test multiple updates
+    assertDoesNotThrow(() -> {
+      lockMetrics.updateLockExpirationDeadlineMetric(5000);
+      lockMetrics.updateLockExpirationDeadlineMetric(15000);
+      lockMetrics.updateLockExpirationDeadlineMetric(500);
+      lockMetrics.updateLockExpirationDeadlineMetric(-1000);
+      lockMetrics.updateLockExpirationDeadlineMetric(1);
+    }, "Multiple updateLockExpirationDeadlineMetric calls should not throw");
+
+    // Get the metrics registry to verify gauge exists
+    Metrics metrics = Metrics.getInstance(metricsConfig, storage);
+    MetricRegistry registry = metrics.getRegistry();
+    String metricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_EXPIRATION_DEADLINE_COUNTER_NAME;
+
+    // Verify the gauge exists
+    Gauge<?> deadlineGaugeRaw = registry.getGauges().get(metricName);
+    assertNotNull(deadlineGaugeRaw, "Lock expiration deadline gauge should exist");
+
+    // Test final value (should be 0 from last update)
+    assertEquals(1, ((Number) deadlineGaugeRaw.getValue()).intValue(), "Final gauge value should be 1");
+  }
+
+  @Test
+  public void testLockDanglingMetric() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(true).build();
+    HoodieWriteConfig writeConfig = HoodieWriteConfig.newBuilder()
+        .forTable("testTable").withPath("/test/path")
+        .withMetricsConfig(metricsConfig)
+        .build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(writeConfig, storage);
+
+    // Get the metrics registry to verify counter values
+    Metrics metrics = Metrics.getInstance(metricsConfig, storage);
+    MetricRegistry registry = metrics.getRegistry();
+    String metricName = writeConfig.getMetricReporterMetricsNamePrefix() + "." + HoodieLockMetrics.LOCK_DANGLING_COUNTER_NAME;
+
+    // Test that the dangling metric can be called
+    assertDoesNotThrow(lockMetrics::updateLockDanglingMetric, 
+        "updateLockDanglingMetric should not throw");
+
+    // Verify the counter exists and increments
+    Counter danglingCounter = registry.getCounters().get(metricName);
+    assertNotNull(danglingCounter, "Lock dangling counter should exist");
+
+    long initialCount = danglingCounter.getCount();
+
+    // Call the metric multiple times
+    lockMetrics.updateLockDanglingMetric();
+    lockMetrics.updateLockDanglingMetric();
+
+    // Verify the counter incremented
+    assertEquals(initialCount + 2, danglingCounter.getCount(), 
+        "Lock dangling counter should increment by 2");
+  }
+
+  @Test
+  public void testNewMetricsWithDisabledLocking() {
+    HoodieStorage storage = mock(HoodieStorage.class);
+    // Test that the new metrics methods work safely when locking metrics are disabled
+    HoodieMetricsConfig metricsConfig = HoodieMetricsConfig.newBuilder().withPath("/test")
+        .withReporterType(MetricsReporterType.INMEMORY.name()).withLockingMetrics(false).build();
+    HoodieLockMetrics lockMetrics = new HoodieLockMetrics(HoodieWriteConfig.newBuilder()
+        .forTable("testTable").withPath("/test/path")
+        .withMetricsConfig(metricsConfig)
+        .build(), storage);
+
+    // All new methods should be safe to call even when locking metrics are disabled
+    assertDoesNotThrow(lockMetrics::updateLockInterruptedMetric,
+        "updateLockInterruptedMetric should not throw when locking metrics disabled");
+    assertDoesNotThrow(() -> lockMetrics.updateLockExpirationDeadlineMetric(5000),
+        "updateLockExpirationDeadlineMetric should not throw when locking metrics disabled");
+    assertDoesNotThrow(lockMetrics::updateLockDanglingMetric,
+        "updateLockDanglingMetric should not throw when locking metrics disabled");
+  }
+}
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestLockManager.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestLockManager.java
index 398ce60f8117..fd587d2f6ddf 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestLockManager.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestLockManager.java
@@ -19,6 +19,7 @@
 package org.apache.hudi.client.transaction;
 
 import org.apache.hudi.client.transaction.lock.LockManager;
+import org.apache.hudi.client.transaction.lock.StorageBasedLockProvider;
 import org.apache.hudi.client.transaction.lock.ZookeeperBasedLockProvider;
 import org.apache.hudi.common.model.HoodieFailedWritesCleaningPolicy;
 import org.apache.hudi.common.model.WriteConcurrencyMode;
@@ -26,12 +27,14 @@ import org.apache.hudi.common.testutils.HoodieCommonTestHarness;
 import org.apache.hudi.config.HoodieCleanConfig;
 import org.apache.hudi.config.HoodieLockConfig;
 import org.apache.hudi.config.HoodieWriteConfig;
+import org.apache.hudi.exception.HoodieException;
 
 import org.apache.curator.test.TestingServer;
 import org.apache.hadoop.fs.FileSystem;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
@@ -41,6 +44,8 @@ import org.slf4j.LoggerFactory;
 import java.io.IOException;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class TestLockManager extends HoodieCommonTestHarness {
 
@@ -121,4 +126,46 @@ public class TestLockManager extends HoodieCommonTestHarness {
             .build())
         .build();
   }
+
+  @Test
+  void testLockManagerTriesMetricsConstructorFirst() {
+    HoodieWriteConfig writeConfig = getStorageBasedLockWriteConfig();
+    LockManager lockManager = new LockManager(writeConfig, (FileSystem) this.metaClient.getStorage().getFileSystem());
+
+    // The getLockProvider() call should fail due to invalid scheme in basePath, 
+    // but this validates that we successfully found and tried the metrics constructor
+    // (rather than falling back to the standard constructor due to reflection issues)
+    HoodieException exception = assertThrows(HoodieException.class, lockManager::getLockProvider,
+        "StorageBasedLockProvider should fail with HoodieLockException due to invalid scheme, but this confirms metrics constructor was found");
+    
+    // Assert that the exception message references the scheme failure, confirming we got to the 
+    // StorageBasedLockProvider instantiation (not a reflection failure)
+    assertTrue(exception.getCause().getCause().getCause().getMessage().startsWith("Unsupported scheme"),
+        "Exception should mention scheme failure, confirming metrics constructor was successfully found and used");
+
+    lockManager.close();
+  }
+
+  @Test
+  void testLockManagerFallbackToStandardConstructor() {
+    // Use ZookeeperBasedLockProvider which doesn't have the metrics constructor
+    HoodieWriteConfig writeConfig = getSingleWriterWriteConfig();
+    LockManager lockManager = new LockManager(writeConfig, (FileSystem) this.metaClient.getStorage().getFileSystem());
+
+    // Get the lock provider to trigger instantiation and fallback
+    assertDoesNotThrow(() -> {
+      lockManager.getLockProvider();
+    });
+
+    lockManager.close();
+  }
+
+  private HoodieWriteConfig getStorageBasedLockWriteConfig() {
+    return HoodieWriteConfig.newBuilder()
+        .withPath(basePath)
+        .withLockConfig(HoodieLockConfig.newBuilder()
+            .withLockProvider(StorageBasedLockProvider.class)
+            .build())
+        .build();
+  }
 }
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProviderTestBase.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProviderTestBase.java
new file mode 100644
index 000000000000..ec8e96db964f
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/StorageBasedLockProviderTestBase.java
@@ -0,0 +1,290 @@
+/*
+ * 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.hudi.client.transaction.lock;
+
+import org.apache.hudi.common.config.TypedProperties;
+import org.apache.hudi.config.StorageBasedLockConfig;
+import org.apache.hudi.exception.HoodieLockException;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class StorageBasedLockProviderTestBase {
+
+  protected StorageBasedLockProvider lockProvider;
+  protected static TypedProperties providerProperties;
+  // A method that subclasses must implement to instantiate the correct provider.
+  protected abstract StorageBasedLockProvider createLockProvider();
+
+  @BeforeEach
+  void setUp() {
+    // Create a fresh instance before each test
+    // This will run first
+    providerProperties = new TypedProperties();
+  }
+
+  @AfterEach
+  void tearDown() {
+    // Optionally unlock or close
+    if (lockProvider != null) {
+      lockProvider.unlock();
+      lockProvider.close();
+    }
+  }
+
+  // ----------------------------------------------------
+  // Basic Tests
+  // ----------------------------------------------------
+
+  @Test
+  void testTryLockSuccess() {
+    boolean lockAcquired = lockProvider.tryLock(5, TimeUnit.SECONDS);
+    assertTrue(lockAcquired, "Lock should be successfully acquired within the time limit.");
+  }
+
+  @Test
+  void testTryLockReleasesLock() {
+    assertTrue(lockProvider.tryLock(3, TimeUnit.SECONDS), "Lock should be successfully acquired.");
+    lockProvider.unlock();
+    assertTrue(lockProvider.tryLock(1, TimeUnit.SECONDS), "Lock should be reacquired after being released.");
+  }
+
+  @Test
+  void testTryLockEdgeCaseZeroTimeout() {
+    boolean lockAcquired = lockProvider.tryLock(0, TimeUnit.SECONDS);
+    assertFalse(lockAcquired, "Lock should not be acquired with a zero timeout.");
+  }
+
+  @Test
+  void testLockThreadKilledShouldNotCauseOrphanedHeartbeat() throws InterruptedException {
+    providerProperties.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), 10);
+    providerProperties.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), 1);
+
+    AtomicReference<StorageBasedLockProvider> lp = new AtomicReference<>();
+    // Create a thread with a new lock provider to acquire the lock
+    Thread lockingThread = new Thread(() -> {
+      lp.set(createLockProvider());
+      lp.get().tryLock();
+    });
+    lockingThread.start();
+
+    // Wait for the thread to acquire the lock
+    // Ensure the locking thread is dead.
+    lockingThread.join(2000);
+    assertFalse(lockingThread.isAlive());
+
+    // After the validity expires, should be able to reacquire
+    boolean lockAcquired = lockProvider.tryLock(15, TimeUnit.SECONDS);
+    assertTrue(lockAcquired, "Lock should be reacquired after expiration");
+    assertNotNull(lockProvider.getLock(), "Lock should be reacquired and getLock() should return non-null");
+    lockProvider.unlock();
+    lockProvider.close();
+    lp.get().close();
+  }
+
+  @Test
+  void testTwoLockProvidersCloseAndUnlock() throws InterruptedException {
+    // We had an issue with tryLock(30, TimeUnit.SECONDS) where it was not synchronized
+    // and this created a race condition where closing it would result in improper
+    // execution with the heartbeat manager.
+    StorageBasedLockProvider provider1 = createLockProvider();
+    assertTrue(provider1.tryLock(), "Provider1 should acquire the lock immediately");
+
+    StorageBasedLockProvider provider2 = createLockProvider();
+
+    CountDownLatch finishLatch = new CountDownLatch(1);
+
+    Thread provider2Thread = new Thread(() -> {
+      // We cannot Thread.sleep in a synchronized method, therefore
+      // this will throw while waiting.
+      assertThrows(HoodieLockException.class, () -> provider2.tryLock(10, TimeUnit.SECONDS));
+      finishLatch.countDown();
+    });
+    provider2Thread.start();
+    // We just need the tryLock code path to enter before close
+    provider2.close();
+    provider1.unlock();
+
+    assertTrue(finishLatch.await(5000, TimeUnit.MILLISECONDS));
+  }
+
+  @Test
+  void testLockAfterClosing() {
+    StorageBasedLockProvider provider1 = createLockProvider();
+    assertTrue(provider1.tryLock(), "Provider1 should acquire the lock immediately");
+    provider1.unlock();
+    provider1.close();
+    assertThrows(HoodieLockException.class, provider1::tryLock);
+  }
+
+  @Test
+  void testCloseBeforeUnlocking() {
+    StorageBasedLockProvider provider1 = createLockProvider();
+    assertTrue(provider1.tryLock(), "Provider1 should acquire the lock immediately");
+    provider1.close();
+    provider1.unlock();
+    assertThrows(HoodieLockException.class, provider1::tryLock);
+  }
+
+  @Test
+  void testUnlockWhenNoLockPresent() {
+    lockProvider.unlock();
+    assertNull(lockProvider.getLock());
+  }
+
+  @Test
+  void testTryLockHappyPath() {
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+    assertNotNull(lockProvider.getLock(), "Lock should be held after tryLock");
+  }
+
+  @Test
+  void testTryLockReentrancy() {
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed again");
+  }
+
+  @Test
+  void testUnlockAndLock() {
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+    assertNotNull(lockProvider.getLock());
+    lockProvider.unlock();
+    assertNull(lockProvider.getLock(), "Lock should be null after unlock");
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+    assertNotNull(lockProvider.getLock());
+  }
+
+  @Test
+  void testIdempotentUnlock() {
+    assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+    lockProvider.unlock();
+
+    // Calling this again should no op, but will warn.
+    lockProvider.unlock();
+  }
+
+  @Test
+  void testLockReacquisitionInLoop() {
+    for (int i = 0; i < 100; i++) {
+      assertTrue(lockProvider.tryLock(), "tryLock should succeed if lock not held");
+      assertNotNull(lockProvider.getLock(), "Lock should be held after acquisition in iteration " + i);
+      lockProvider.unlock();
+      assertNull(lockProvider.getLock(), "Lock should be null after unlock in iteration " + i);
+    }
+  }
+
+  @Test
+  void testMultipleLockProvidersContention() throws InterruptedException {
+    final int NUM_THREADS = 10;
+    List<StorageBasedLockProvider> providers = new ArrayList<>();
+    List<Thread> threads = new ArrayList<>();
+    AtomicBoolean stopFlag = new AtomicBoolean(false);
+    CountDownLatch startLatch = new CountDownLatch(1);
+    CountDownLatch threadsCompleteLatch = new CountDownLatch(NUM_THREADS);
+
+    // Shared ArrayList that will detect concurrent modifications
+    ArrayList<Integer> sharedList = new ArrayList<>();
+    // Pre-populate the list to allow modifications
+    for (int i = 0; i < 1000; i++) {
+      sharedList.add(i);
+    }
+
+    for (int i = 0; i < NUM_THREADS; i++) {
+      providers.add(createLockProvider());
+    }
+
+    for (int i = 0; i < NUM_THREADS; i++) {
+      final StorageBasedLockProvider provider = providers.get(i);
+      final int threadId = i;
+      Thread t = new Thread(() -> {
+        try {
+          startLatch.await();
+          while (!stopFlag.get()) {
+            if (provider.tryLock()) {
+              // Leveraging java arraylist built in best effort concurrent modification detection
+              // mechanism, if lock failed to process the list, there is a chance of throwing concurrent modification
+              // exception.
+              try {
+                // Multiple operations on ArrayList that must be atomic
+                sharedList.removeIf(val -> val % (threadId + 1) == 0);
+
+                // Add numbers back based on threadId
+                for (int j = 0; j < 1000; j++) {
+                  if (j % (threadId + 1) == 0) {
+                    sharedList.add(j);
+                  }
+                }
+
+                // Sort the list - this will fail if another thread modifies the list
+                sharedList.sort(Integer::compareTo);
+
+              } finally {
+                provider.unlock();
+              }
+            }
+            Thread.yield();
+          }
+        } catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+          throw new RuntimeException(e);
+        }
+        threadsCompleteLatch.countDown();
+      }, "LockProviderThread-" + i);
+      threads.add(t);
+    }
+
+    threads.forEach(Thread::start);
+    startLatch.countDown();
+
+    // Run for 3 seconds
+    Thread.sleep(3000);
+    stopFlag.set(true);
+
+    for (Thread t : threads) {
+      t.join();
+    }
+
+    providers.forEach(StorageBasedLockProvider::close);
+    assertTrue(threadsCompleteLatch.await(6, TimeUnit.SECONDS));
+
+    // Validate the invariant of the list.
+    assertEquals(1000, sharedList.size(), "List should contain 1000 elements");
+    for (int i = 0; i < 1000; i++) {
+      assertEquals(i, sharedList.get(i), "List should contain all numbers 0 to 999 in order");
+    }
+  }
+}
\ No newline at end of file
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestStorageBasedLockProvider.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestStorageBasedLockProvider.java
index e29ad17e551b..9dd0ef4dacc9 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestStorageBasedLockProvider.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/TestStorageBasedLockProvider.java
@@ -19,6 +19,7 @@
 package org.apache.hudi.client.transaction.lock;
 
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hudi.client.transaction.lock.metrics.HoodieLockMetrics;
 import org.apache.hudi.client.transaction.lock.models.LockUpsertResult;
 import org.apache.hudi.client.transaction.lock.models.StorageLockData;
 import org.apache.hudi.client.transaction.lock.models.StorageLockFile;
@@ -49,12 +50,16 @@ import java.util.concurrent.atomic.AtomicInteger;
 import static org.apache.hudi.common.config.HoodieCommonConfig.BASE_PATH;
 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.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.refEq;
 import static org.mockito.Mockito.atLeastOnce;
@@ -64,6 +69,7 @@ import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -84,17 +90,20 @@ class TestStorageBasedLockProvider {
     mockHeartbeatManager = mock(HeartbeatManager.class);
     mockLogger = mock(Logger.class);
     when(mockHeartbeatManager.stopHeartbeat(true)).thenReturn(true);
+    // Mock the readObject method to return Option.empty() to prevent NPE in audit service creation
+    when(mockLockService.readObject(anyString(), anyBoolean())).thenReturn(Option.empty());
     TypedProperties props = new TypedProperties();
     props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
-    props.put(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "1");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
     props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-default");
 
     lockProvider = spy(new StorageBasedLockProvider(
         ownerId,
         props,
-        (a,b,c) -> mockHeartbeatManager,
-        (a,b,c) -> mockLockService,
-        mockLogger));
+        (a, b, c) -> mockHeartbeatManager,
+        (a, b, c) -> mockLockService,
+        mockLogger,
+        null));
   }
 
   @AfterEach
@@ -120,7 +129,7 @@ class TestStorageBasedLockProvider {
     TypedProperties props = new TypedProperties();
     props.put(BASE_PATH.key(), "file:///tmp/lake/db/tbl-default");
     props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "20");
-    props.put(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "2");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "2");
 
     LockConfiguration lockConf = new LockConfiguration(props);
 
@@ -131,8 +140,8 @@ class TestStorageBasedLockProvider {
   }
 
   @ParameterizedTest
-  @ValueSource(strings = { "gs://bucket/lake/db/tbl-default", "s3://bucket/lake/db/tbl-default",
-      "s3a://bucket/lake/db/tbl-default" })
+  @ValueSource(strings = {"gs://bucket/lake/db/tbl-default", "s3://bucket/lake/db/tbl-default",
+      "s3a://bucket/lake/db/tbl-default"})
   void testNonExistentWriteServiceWithDefaults(String tableBasePathString) {
     TypedProperties props = new TypedProperties();
     props.put(BASE_PATH.key(), tableBasePathString);
@@ -395,6 +404,28 @@ class TestStorageBasedLockProvider {
     when(mockHeartbeatManager.hasActiveHeartbeat()).thenReturn(false);
   }
 
+  @Test
+  void testUnlockThrowsExceptionWhenLockAcquiredByOthers() {
+    when(mockLockService.readCurrentLockFile()).thenReturn(Pair.of(LockGetResult.NOT_EXISTS, Option.empty()));
+    StorageLockData data = new StorageLockData(false, System.currentTimeMillis() + DEFAULT_LOCK_VALIDITY_MS, ownerId);
+    StorageLockFile realLockFile = new StorageLockFile(data, "v1");
+    when(mockLockService.tryUpsertLockFile(any(), eq(Option.empty())))
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(realLockFile)));
+    when(mockHeartbeatManager.startHeartbeatForThread(any())).thenReturn(true);
+    assertTrue(lockProvider.tryLock());
+
+    when(mockHeartbeatManager.stopHeartbeat(true)).thenReturn(true);
+    when(mockHeartbeatManager.hasActiveHeartbeat())
+        .thenReturn(true)
+        .thenReturn(false);
+    when(mockLockService.tryUpsertLockFile(any(), eq(Option.of(realLockFile))))
+        .thenReturn(Pair.of(LockUpsertResult.ACQUIRED_BY_OTHERS, Option.empty()));
+
+    HoodieLockException exception = assertThrows(HoodieLockException.class, () -> lockProvider.unlock());
+    assertTrue(exception.getMessage().contains("FAILED_TO_RELEASE"));
+    when(mockHeartbeatManager.hasActiveHeartbeat()).thenReturn(false);
+  }
+
   @Test
   void testCloseFailsToStopHeartbeat() {
     when(mockLockService.readCurrentLockFile()).thenReturn(Pair.of(LockGetResult.NOT_EXISTS, Option.empty()));
@@ -545,7 +576,7 @@ class TestStorageBasedLockProvider {
     StorageLockData data = new StorageLockData(false, System.currentTimeMillis() + DEFAULT_LOCK_VALIDITY_MS, ownerId);
     StorageLockFile realLockFile = new StorageLockFile(data, "v1");
     when(mockLockService.tryUpsertLockFile(any(), eq(Option.empty())))
-            .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(realLockFile)));
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(realLockFile)));
     when(mockHeartbeatManager.startHeartbeatForThread(any())).thenReturn(true);
 
     boolean acquired = lockProvider.tryLock();
@@ -554,7 +585,7 @@ class TestStorageBasedLockProvider {
     verify(mockLockService, atLeastOnce()).tryUpsertLockFile(any(), any());
 
     when(mockLockService.tryUpsertLockFile(any(StorageLockData.class), eq(Option.of(realLockFile))))
-            .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(realLockFile)));
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(realLockFile)));
 
     // Mock shutdown
     lockProvider.shutdown(true);
@@ -587,7 +618,7 @@ class TestStorageBasedLockProvider {
 
   @Test
   void testShutdownHookFiresDuringTryLockWithTimeout() throws Exception {
-    // This test simulates the scenario where the shutdown hook fires while tryLock(long time, TimeUnit unit) 
+    // This test simulates the scenario where the shutdown hook fires while tryLock(long time, TimeUnit unit)
     // is in progress, and expects that HoodieLockException is thrown when tryLock is called after shutdown.
 
     // Setup mocks to simulate lock being held by another owner (to keep tryLock looping)
@@ -659,6 +690,242 @@ class TestStorageBasedLockProvider {
     verify(lockProvider, atLeastOnce()).tryLock();
   }
 
+  @Test
+  public void testStorageBasedLockProviderWithMetricsConstructor() {
+    // Create test configuration
+    TypedProperties props = new TypedProperties();
+    props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
+    props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-default");
+    LockConfiguration lockConfiguration = new LockConfiguration(props);
+    StorageConfiguration<?> storageConf = HoodieTestUtils.getDefaultStorageConf();
+
+    // Create a mock HoodieLockMetrics object  
+    HoodieLockMetrics mockMetrics = mock(HoodieLockMetrics.class);
+
+    // Test that constructor with metrics works by using the internal constructor to avoid scheme issues
+    StorageBasedLockProvider lockProviderWithMetrics = null;
+    try {
+      // First test that the public constructor with metrics compiles and can be called
+      // We expect this to fail due to lock client instantiation, but it validates the constructor exists
+      assertThrows(Exception.class, () -> {
+        new StorageBasedLockProvider(lockConfiguration, storageConf, mockMetrics);
+      }, "Constructor should exist but fail during lock client instantiation");
+
+      // Now create a working instance using the internal constructor for proper validation
+      lockProviderWithMetrics = new StorageBasedLockProvider(
+          UUID.randomUUID().toString(),
+          props,
+          (a, b, c) -> mock(HeartbeatManager.class),
+          (a, b, c) -> new StubStorageLockClient(a, b, new Properties()),
+          mock(Logger.class),
+          mockMetrics);
+
+      // Verify the lock provider was created successfully
+      assertNotNull(lockProviderWithMetrics, "StorageBasedLockProvider should be created successfully");
+
+      // Verify that it can perform basic operations
+      assertNull(lockProviderWithMetrics.getLock(), "Initially should have no lock");
+
+    } catch (Exception e) {
+      fail("StorageBasedLockProvider creation should not throw unexpected exception: " + e.getMessage());
+    } finally {
+      if (lockProviderWithMetrics != null) {
+        lockProviderWithMetrics.close();
+      }
+    }
+  }
+
+  @Test
+  public void testStorageBasedLockProviderStandardConstructor() {
+    // Create test configuration
+    TypedProperties props = new TypedProperties();
+    props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
+    props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-default");
+    LockConfiguration lockConfiguration = new LockConfiguration(props);
+    StorageConfiguration<?> storageConf = HoodieTestUtils.getDefaultStorageConf();
+
+    // Test that standard constructor works by using the internal constructor to avoid scheme issues
+    StorageBasedLockProvider lockProviderStandard = null;
+    try {
+      // First test that the public standard constructor compiles and can be called
+      // We expect this to fail due to lock client instantiation, but it validates the constructor exists
+      assertThrows(Exception.class, () -> {
+        new StorageBasedLockProvider(lockConfiguration, storageConf);
+      }, "Standard constructor should exist but fail during lock client instantiation");
+
+      // Now create a working instance using the internal constructor for proper validation
+      lockProviderStandard = new StorageBasedLockProvider(
+          UUID.randomUUID().toString(),
+          props,
+          (a, b, c) -> mock(HeartbeatManager.class),
+          (a, b, c) -> new StubStorageLockClient(a, b, new Properties()),
+          mock(Logger.class),
+          null);  // No metrics for standard constructor test
+
+      // Verify the lock provider was created successfully
+      assertNotNull(lockProviderStandard, "StorageBasedLockProvider should be created successfully");
+
+      // Verify that it can perform basic operations  
+      assertNull(lockProviderStandard.getLock(), "Initially should have no lock");
+
+    } catch (Exception e) {
+      fail("StorageBasedLockProvider creation should not throw unexpected exception: " + e.getMessage());
+    } finally {
+      if (lockProviderStandard != null) {
+        lockProviderStandard.close();
+      }
+    }
+  }
+
+  @Test
+  void testAuditServiceIntegrationWhenConfigNotPresent() {
+    // Test that lock provider works correctly when audit config is not present
+    TypedProperties props = new TypedProperties();
+    props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
+    props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-audit-test");
+
+    // Mock client that returns empty for audit config
+    StorageLockClient auditMockClient = mock(StorageLockClient.class);
+    when(auditMockClient.readObject(anyString(), eq(true)))
+        .thenReturn(Option.empty());
+    when(auditMockClient.readCurrentLockFile())
+        .thenReturn(Pair.of(LockGetResult.NOT_EXISTS, Option.empty()));
+
+    StorageBasedLockProvider auditLockProvider = new StorageBasedLockProvider(
+        ownerId,
+        props,
+        (a, b, c) -> mockHeartbeatManager,
+        (a, b, c) -> auditMockClient,
+        mockLogger,
+        null);
+
+    // Lock provider should work normally even without audit
+    StorageLockData data = new StorageLockData(false, System.currentTimeMillis() + DEFAULT_LOCK_VALIDITY_MS, ownerId);
+    StorageLockFile lockFile = new StorageLockFile(data, "v1");
+    when(auditMockClient.tryUpsertLockFile(any(), eq(Option.empty())))
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(lockFile)));
+    when(mockHeartbeatManager.startHeartbeatForThread(any())).thenReturn(true);
+
+    // tryLock should trigger audit service creation (lazily)
+    assertTrue(auditLockProvider.tryLock());
+
+    // Verify audit config was checked during tryLock
+    verify(auditMockClient, times(1)).readObject(
+        contains(".locks/audit_enabled.json"), eq(true));
+
+    // No audit writes should happen since audit is not present
+    verify(auditMockClient, never()).writeObject(
+        contains(".locks/audit"), anyString());
+
+    auditLockProvider.close();
+  }
+
+  @Test
+  void testAuditServiceIntegrationWhenConfigDisabled() {
+    // Test that lock provider works correctly when audit is explicitly disabled
+    TypedProperties props = new TypedProperties();
+    props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
+    props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-audit-disabled");
+
+    // Mock client that returns disabled config
+    StorageLockClient auditMockClient = mock(StorageLockClient.class);
+    String disabledConfig = "{\"STORAGE_LOCK_AUDIT_SERVICE_ENABLED\": false}";
+    when(auditMockClient.readObject(anyString(), eq(true)))
+        .thenReturn(Option.of(disabledConfig));
+    when(auditMockClient.readCurrentLockFile())
+        .thenReturn(Pair.of(LockGetResult.NOT_EXISTS, Option.empty()));
+
+    StorageBasedLockProvider auditLockProvider = new StorageBasedLockProvider(
+        ownerId,
+        props,
+        (a, b, c) -> mockHeartbeatManager,
+        (a, b, c) -> auditMockClient,
+        mockLogger,
+        null);
+
+    // Set up lock acquisition
+    StorageLockData data = new StorageLockData(false, System.currentTimeMillis() + DEFAULT_LOCK_VALIDITY_MS, ownerId);
+    StorageLockFile lockFile = new StorageLockFile(data, "v1");
+    when(auditMockClient.tryUpsertLockFile(any(), eq(Option.empty())))
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(lockFile)));
+    when(mockHeartbeatManager.startHeartbeatForThread(any())).thenReturn(true);
+
+    // tryLock should trigger audit service creation check
+    assertTrue(auditLockProvider.tryLock());
+
+    // Verify audit config was checked during tryLock
+    verify(auditMockClient, times(1)).readObject(
+        contains(".locks/audit_enabled.json"), eq(true));
+
+    // No audit writes should happen since audit is disabled
+    verify(auditMockClient, never()).writeObject(
+        contains(".locks/audit"), anyString());
+
+    auditLockProvider.close();
+  }
+
+  @Test
+  void testAuditServiceIntegrationWhenConfigEnabled() {
+    // Test that lock provider works correctly when audit is enabled
+    TypedProperties props = new TypedProperties();
+    props.put(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
+    props.put(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
+    props.put(BASE_PATH.key(), "gs://bucket/lake/db/tbl-audit-enabled");
+
+    // Mock client that returns enabled config
+    StorageLockClient auditMockClient = mock(StorageLockClient.class);
+    String enabledConfig = "{\"STORAGE_LOCK_AUDIT_SERVICE_ENABLED\": true}";
+    when(auditMockClient.readObject(anyString(), eq(true)))
+        .thenReturn(Option.of(enabledConfig));
+    when(auditMockClient.readCurrentLockFile())
+        .thenReturn(Pair.of(LockGetResult.NOT_EXISTS, Option.empty()));
+    // Mock writeObject method to return true for audit file writes
+    when(auditMockClient.writeObject(anyString(), anyString()))
+        .thenReturn(true);
+
+    StorageBasedLockProvider auditLockProvider = new StorageBasedLockProvider(
+        ownerId,
+        props,
+        (a, b, c) -> mockHeartbeatManager,
+        (a, b, c) -> auditMockClient,
+        mockLogger,
+        null);
+
+    // Set up lock acquisition
+    StorageLockData data = new StorageLockData(false, System.currentTimeMillis() + DEFAULT_LOCK_VALIDITY_MS, ownerId);
+    StorageLockFile lockFile = new StorageLockFile(data, "v1");
+    when(auditMockClient.tryUpsertLockFile(any(), eq(Option.empty())))
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(lockFile)));
+    when(mockHeartbeatManager.startHeartbeatForThread(any())).thenReturn(true);
+
+    // tryLock should trigger audit service creation and START audit
+    assertTrue(auditLockProvider.tryLock());
+
+    // Verify audit config was checked during tryLock
+    verify(auditMockClient, times(1)).readObject(
+        contains(".locks/audit_enabled.json"), eq(true));
+
+    // Verify audit START operation was written
+    verify(auditMockClient, times(1)).writeObject(
+        contains(".locks/audit/"), anyString());
+
+    // Unlock should trigger END audit
+    when(auditMockClient.tryUpsertLockFile(any(), any()))
+        .thenReturn(Pair.of(LockUpsertResult.SUCCESS, Option.of(lockFile)));
+    when(mockHeartbeatManager.stopHeartbeat(anyBoolean())).thenReturn(true);
+    auditLockProvider.unlock();
+
+    // Verify audit END operation was written (total 2 writes: START and END)
+    verify(auditMockClient, times(2)).writeObject(
+        contains(".locks/audit/"), anyString());
+
+    auditLockProvider.close();
+  }
+
   public static class StubStorageLockClient implements StorageLockClient {
     public StubStorageLockClient(String ownerId, String lockFileUri, Properties props) {
       assertTrue(lockFileUri.endsWith("table_lock.json"));
@@ -676,6 +943,18 @@ class TestStorageBasedLockProvider {
       return null;
     }
 
+    @Override
+    public Option<String> readObject(String filePath, boolean checkExistsFirst) {
+      // Stub implementation for testing
+      return Option.empty();
+    }
+
+    @Override
+    public boolean writeObject(String filePath, String content) {
+      // Stub implementation for testing
+      return true;
+    }
+
     @Override
     public void close() throws Exception {
       // stub, no-op
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestAuditServiceFactory.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestAuditServiceFactory.java
new file mode 100644
index 000000000000..a53c829e8d75
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestAuditServiceFactory.java
@@ -0,0 +1,160 @@
+/*
+ * 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.hudi.client.transaction.lock.audit;
+
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.common.util.Option;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+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.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test class for AuditServiceFactory
+ */
+public class TestAuditServiceFactory {
+
+  private StorageLockClient mockStorageLockClient;
+  private final String ownerId = "test-owner";
+  private final String basePath = "s3://bucket/path/to/table";
+
+  @BeforeEach
+  void setUp() {
+    mockStorageLockClient = mock(StorageLockClient.class);
+  }
+
+  @Test
+  void testCreateAuditServiceWhenConfigNotFound() {
+    // Config file doesn't exist
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.empty());
+
+    Option<AuditService> result = AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000,
+        () -> true);
+
+    // Should return empty for now (no concrete implementation yet)
+    // When implementation is added, this should return a present Option
+    assertTrue(result.isEmpty());
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+
+  @Test
+  void testCreateAuditServiceWhenDisabledInConfig() {
+    // Config file exists but audit is disabled
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    String configJson = "{\"STORAGE_LOCK_AUDIT_SERVICE_ENABLED\": false}";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.of(configJson));
+
+    Option<AuditService> result = AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000,
+        () -> true);
+
+    // Should return empty when audit is disabled
+    assertTrue(result.isEmpty());
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+
+  @Test
+  void testCreateAuditServiceWhenEnabledInConfig() {
+    // Config file exists and audit is enabled
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    String configJson = "{\"STORAGE_LOCK_AUDIT_SERVICE_ENABLED\": true}";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.of(configJson));
+    // Mock writeObject method to return true for audit file writes
+    when(mockStorageLockClient.writeObject(anyString(), anyString()))
+        .thenReturn(true);
+    Option<AuditService> result = AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000, // lockExpirationFunction
+        () -> true); // lockHeldSupplier
+
+    assertTrue(result.isPresent());
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+
+  @Test
+  void testCreateAuditServiceWithMalformedJson() {
+    // Config file exists but contains invalid JSON
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    String malformedJson = "{invalid json}";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.of(malformedJson));
+
+    Option<AuditService> result = AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000,
+        () -> true);
+
+    // Should return empty when JSON is malformed
+    assertTrue(result.isEmpty());
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+
+  @Test
+  void testCreateAuditServiceWithEmptyConfig() {
+    // Config file exists but doesn't contain the expected field
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    String configJson = "{\"some_other_field\": true}";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.of(configJson));
+
+    Option<AuditService> result = AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000,
+        () -> true);
+
+    // Should return empty when field is missing (defaults to false)
+    assertTrue(result.isEmpty());
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+
+  @Test
+  void testCreateAuditServiceUsesCheckExistsFirst() {
+    // Verify that the factory passes true for checkExistsFirst
+    String expectedPath = basePath + "/.hoodie/.locks/audit_enabled.json";
+    when(mockStorageLockClient.readObject(eq(expectedPath), eq(true)))
+        .thenReturn(Option.empty());
+
+    AuditServiceFactory.createLockProviderAuditService(
+        ownerId, basePath, mockStorageLockClient,
+        System.currentTimeMillis(),
+        timestamp -> timestamp + 10000,
+        () -> true);
+
+    // Should pass true for checkExistsFirst since audit config is rarely present
+    verify(mockStorageLockClient).readObject(expectedPath, true);
+  }
+}
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestStorageLockProviderAuditService.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestStorageLockProviderAuditService.java
new file mode 100644
index 000000000000..d5946a6ae410
--- /dev/null
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/audit/TestStorageLockProviderAuditService.java
@@ -0,0 +1,379 @@
+/*
+ * 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.hudi.client.transaction.lock.audit;
+
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.storage.StoragePath;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+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.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for StorageLockProviderAuditService.
+ */
+public class TestStorageLockProviderAuditService {
+
+  private static final String BASE_PATH = "s3://bucket/table";
+  private static final String OWNER_ID = "writer-12345678-9abc-def0-1234-567890abcdef";
+  private static final long TRANSACTION_START_TIME = 1234567890000L;
+  private static final long LOCK_EXPIRATION = 1000000L;
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  private StorageLockClient storageLockClient;
+  private Supplier<Boolean> lockHeldSupplier;
+  private StorageLockProviderAuditService auditService;
+
+  @BeforeEach
+  void setUp() {
+    storageLockClient = mock(StorageLockClient.class);
+    lockHeldSupplier = mock(Supplier.class);
+
+    when(lockHeldSupplier.get()).thenReturn(true);
+
+    auditService = new StorageLockProviderAuditService(
+        BASE_PATH,
+        OWNER_ID,
+        TRANSACTION_START_TIME,
+        storageLockClient,
+        timestamp -> LOCK_EXPIRATION,
+        lockHeldSupplier);
+  }
+
+  /**
+   * Helper method to validate audit record structure and content.
+   * 
+   * @param auditRecord The audit record to validate
+   * @param expectedState Expected operation state
+   * @param expectedTimestamp Expected timestamp
+   * @param expectedLockHeld Expected lock held status
+   */
+  private void validateAuditRecord(Map<String, Object> auditRecord, AuditOperationState expectedState, 
+                                  long expectedTimestamp, boolean expectedLockHeld) {
+    assertNotNull(auditRecord, "Audit record should not be null");
+    assertEquals(6, auditRecord.size(), "Audit record should contain exactly 6 fields");
+    
+    // Validate all required fields are present
+    assertNotNull(auditRecord.get("ownerId"), "ownerId should be present");
+    assertNotNull(auditRecord.get("transactionStartTime"), "transactionStartTime should be present");
+    assertNotNull(auditRecord.get("timestamp"), "timestamp should be present");
+    assertNotNull(auditRecord.get("state"), "state should be present");
+    assertNotNull(auditRecord.get("lockExpiration"), "lockExpiration should be present");
+    assertNotNull(auditRecord.get("lockHeld"), "lockHeld should be present");
+    
+    // Validate field values
+    assertEquals(OWNER_ID, auditRecord.get("ownerId"));
+    assertEquals(TRANSACTION_START_TIME, ((Number) auditRecord.get("transactionStartTime")).longValue());
+    assertEquals(expectedTimestamp, ((Number) auditRecord.get("timestamp")).longValue());
+    assertEquals(expectedState.name(), auditRecord.get("state"));
+    assertEquals(LOCK_EXPIRATION, ((Number) auditRecord.get("lockExpiration")).longValue());
+    assertEquals(expectedLockHeld, auditRecord.get("lockHeld"));
+  }
+
+  /**
+   * Helper method to verify expected file path generation.
+   * 
+   * @param actualPath The actual file path captured
+   * @param ownerId The owner ID used
+   * @param transactionStartTime The transaction start time used
+   */
+  private void validateFilePath(String actualPath, String ownerId, long transactionStartTime) {
+    String expectedPath = String.format("%s%s.hoodie%s.locks%saudit%s%d_%s.jsonl",
+        BASE_PATH,
+        StoragePath.SEPARATOR,
+        StoragePath.SEPARATOR,
+        StoragePath.SEPARATOR,
+        StoragePath.SEPARATOR,
+        transactionStartTime,
+        ownerId);
+    assertEquals(expectedPath, actualPath);
+  }
+
+  /**
+   * Comprehensive test for complete audit lifecycle: START -> RENEW -> END.
+   * This consolidates individual operation tests into a single comprehensive test
+   * that validates the entire audit flow.
+   */
+  @Test
+  void testCompleteAuditLifecycle() throws Exception {
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+
+    long startTime = System.currentTimeMillis();
+    long renewTime = startTime + 1000;
+    long endTime = startTime + 2000;
+
+    // Configure lock held status for each operation
+    when(lockHeldSupplier.get()).thenReturn(true, true, false);
+
+    // Execute complete lifecycle
+    auditService.recordOperation(AuditOperationState.START, startTime);
+    auditService.recordOperation(AuditOperationState.RENEW, renewTime);
+    auditService.recordOperation(AuditOperationState.END, endTime);
+
+    // Capture all write calls
+    ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
+    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
+    verify(storageLockClient, times(3)).writeObject(pathCaptor.capture(), contentCaptor.capture());
+
+    // Validate file path (should be consistent across all operations)
+    validateFilePath(pathCaptor.getValue(), OWNER_ID, TRANSACTION_START_TIME);
+
+    // Parse final content - should contain all three records
+    String finalContent = contentCaptor.getValue();
+    String[] lines = finalContent.trim().split("\n");
+    assertEquals(3, lines.length, "Should have three JSON lines");
+
+    // Validate each audit record
+    @SuppressWarnings("unchecked")
+    Map<String, Object> startRecord = OBJECT_MAPPER.readValue(lines[0], Map.class);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> renewRecord = OBJECT_MAPPER.readValue(lines[1], Map.class);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> endRecord = OBJECT_MAPPER.readValue(lines[2], Map.class);
+
+    validateAuditRecord(startRecord, AuditOperationState.START, startTime, true);
+    validateAuditRecord(renewRecord, AuditOperationState.RENEW, renewTime, true);
+    validateAuditRecord(endRecord, AuditOperationState.END, endTime, false);
+  }
+
+  /**
+   * Test data provider for different owner ID formats.
+   * 
+   * @return Stream of owner ID test cases
+   */
+  static java.util.stream.Stream<Arguments> ownerIdTestCases() {
+    return java.util.stream.Stream.of(
+        Arguments.of("12345678-9abc-def0-1234-567890abcdef", "Full UUID format"),
+        Arguments.of("abc123", "Short owner ID"),
+        Arguments.of("regular-owner-id-without-uuid", "Regular owner ID"),
+        Arguments.of("writer-12345678-9abc-def0-1234-567890abcdef", "Writer with UUID")
+    );
+  }
+
+  /**
+   * Test audit file naming with different owner ID formats.
+   * Consolidated test that verifies file path generation for various owner ID formats.
+   * 
+   * @param ownerId The owner ID to test
+   * @param description Description of the test case
+   */
+  @ParameterizedTest(name = "{1}: {0}")
+  @MethodSource("ownerIdTestCases")
+  void testFileNameWithDifferentOwnerIds(String ownerId, String description) throws Exception {
+    long txnStartTime = System.currentTimeMillis();
+    StorageLockProviderAuditService service = new StorageLockProviderAuditService(
+        BASE_PATH,
+        ownerId,
+        txnStartTime,
+        storageLockClient,
+        timestamp -> LOCK_EXPIRATION,
+        lockHeldSupplier);
+
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+    service.recordOperation(AuditOperationState.START, System.currentTimeMillis());
+
+    ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
+    verify(storageLockClient, times(1)).writeObject(pathCaptor.capture(), anyString());
+    
+    validateFilePath(pathCaptor.getValue(), ownerId, txnStartTime);
+  }
+
+  /**
+   * Test data provider for write failure scenarios.
+   * 
+   * @return Stream of write failure test cases
+   */
+  static Stream<Arguments> writeFailureTestCases() {
+    return Stream.of(
+        Arguments.of(false, "Write returns false"),
+        Arguments.of(new RuntimeException("Storage error"), "Write throws exception")
+    );
+  }
+
+  /**
+   * Test handling of write failures using parameterized test.
+   * Consolidates multiple write failure scenarios into a single parameterized test.
+   * 
+   * @param failureCondition The failure condition (Boolean false or Exception)
+   * @param description Description of the test case
+   */
+  @ParameterizedTest(name = "{1}")
+  @MethodSource("writeFailureTestCases")
+  void testWriteFailureHandling(Object failureCondition, String description) throws Exception {
+    long timestamp = System.currentTimeMillis();
+    
+    if (failureCondition instanceof Boolean) {
+      when(storageLockClient.writeObject(anyString(), anyString())).thenReturn((Boolean) failureCondition);
+    } else if (failureCondition instanceof Exception) {
+      when(storageLockClient.writeObject(anyString(), anyString())).thenThrow((Exception) failureCondition);
+    }
+
+    // Should not throw exception - audit failures should be handled gracefully
+    auditService.recordOperation(AuditOperationState.START, timestamp);
+
+    verify(storageLockClient, times(1)).writeObject(anyString(), anyString());
+  }
+
+  @Test
+  void testDynamicFunctionValues() throws Exception {
+    long timestamp = System.currentTimeMillis();
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+
+    // Create a service with dynamic expiration calculation
+    StorageLockProviderAuditService dynamicService = new StorageLockProviderAuditService(
+        BASE_PATH,
+        OWNER_ID,
+        TRANSACTION_START_TIME,
+        storageLockClient,
+        ts -> ts + 1000L, // Adds 1000ms to timestamp
+        lockHeldSupplier);
+
+    when(lockHeldSupplier.get()).thenReturn(true, false);
+
+    dynamicService.recordOperation(AuditOperationState.START, timestamp);
+    dynamicService.recordOperation(AuditOperationState.END, timestamp + 1000);
+
+    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
+    verify(storageLockClient, times(2)).writeObject(anyString(), contentCaptor.capture());
+
+    // Validate dynamic expiration calculation
+    String[] firstLines = contentCaptor.getAllValues().get(0).trim().split("\n");
+    @SuppressWarnings("unchecked")
+    Map<String, Object> firstRecord = OBJECT_MAPPER.readValue(firstLines[0], Map.class);
+
+    String[] secondLines = contentCaptor.getAllValues().get(1).trim().split("\n");
+    @SuppressWarnings("unchecked")
+    Map<String, Object> secondRecord = OBJECT_MAPPER.readValue(secondLines[1], Map.class);
+
+    assertTrue((Boolean) firstRecord.get("lockHeld"));
+    assertEquals(timestamp + 1000L, ((Number) firstRecord.get("lockExpiration")).longValue());
+
+    assertFalse((Boolean) secondRecord.get("lockHeld"));
+    assertEquals(timestamp + 1000L + 1000L, ((Number) secondRecord.get("lockExpiration")).longValue());
+  }
+
+  @Test
+  void testFilePathGeneration() throws Exception {
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+
+    auditService.recordOperation(AuditOperationState.START, System.currentTimeMillis());
+
+    String expectedPath = String.format("%s/.hoodie/.locks/audit/%d_%s.jsonl", 
+        BASE_PATH, TRANSACTION_START_TIME, OWNER_ID);
+
+    verify(storageLockClient).writeObject(eq(expectedPath), anyString());
+  }
+
+  @Test
+  void testCloseMethodWithBufferedData() throws Exception {
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+
+    // Record an operation to buffer data
+    auditService.recordOperation(AuditOperationState.START, System.currentTimeMillis());
+
+    // Close should not write additional data (data is written immediately after each operation)
+    auditService.close();
+
+    // Verify write was called only once for the START operation
+    verify(storageLockClient, times(1)).writeObject(anyString(), anyString());
+  }
+
+  @Test
+  void testConcurrentOperations() throws Exception {
+    when(storageLockClient.writeObject(anyString(), anyString())).thenReturn(true);
+    when(lockHeldSupplier.get()).thenReturn(true, false);
+
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    CountDownLatch startLatch = new CountDownLatch(1);
+    CountDownLatch completionLatch = new CountDownLatch(2);
+
+    long baseTime = System.currentTimeMillis();
+
+    // Concurrent RENEW and END operations
+    executor.submit(() -> {
+      try {
+        startLatch.await();
+        auditService.recordOperation(AuditOperationState.RENEW, baseTime + 1000);
+      } catch (Exception e) {
+        // Handle gracefully
+      } finally {
+        completionLatch.countDown();
+      }
+    });
+
+    executor.submit(() -> {
+      try {
+        startLatch.await();
+        auditService.recordOperation(AuditOperationState.END, baseTime + 1001);
+      } catch (Exception e) {
+        // Handle gracefully
+      } finally {
+        completionLatch.countDown();
+      }
+    });
+
+    startLatch.countDown();
+    assertTrue(completionLatch.await(5, TimeUnit.SECONDS));
+
+    // Verify both operations were recorded
+    ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
+    verify(storageLockClient, times(2)).writeObject(anyString(), contentCaptor.capture());
+
+    String finalContent = contentCaptor.getValue();
+    String[] lines = finalContent.trim().split("\\n");
+    assertEquals(2, lines.length);
+
+    // Verify both RENEW and END states are present
+    @SuppressWarnings("unchecked")
+    Map<String, Object> record1 = OBJECT_MAPPER.readValue(lines[0], Map.class);
+    @SuppressWarnings("unchecked")
+    Map<String, Object> record2 = OBJECT_MAPPER.readValue(lines[1], Map.class);
+
+    String state1 = (String) record1.get("state");
+    String state2 = (String) record2.get("state");
+    assertTrue((state1.equals("RENEW") && state2.equals("END"))
+        || (state1.equals("END") && state2.equals("RENEW")));
+
+    executor.shutdown();
+    assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS));
+  }
+}
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/StorageLockClientFileTest.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/StorageLockClientFileTest.java
index 33d3c9bf44ff..9a415850289a 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/StorageLockClientFileTest.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/StorageLockClientFileTest.java
@@ -18,6 +18,8 @@
 
 package org.apache.hudi.client.transaction.lock.models;
 
+import org.apache.hudi.client.transaction.lock.StorageLockClient;
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService;
 import org.apache.hudi.exception.HoodieIOException;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -162,4 +164,60 @@ public class StorageLockClientFileTest {
     StorageLockFile file = new StorageLockFile(data, VERSION_ID);
     assertEquals(VERSION_ID, file.getVersionId());
   }
+
+  @Test
+  void testGetLockFolderPathWithoutTrailingSlash() {
+    String basePath = "s3://bucket/table";
+    String expected = "s3://bucket/table/.hoodie/.locks";
+    String actual = StorageLockClient.getLockFolderPath(basePath);
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  void testGetLockFolderPathWithTrailingSlash() {
+    String basePath = "s3://bucket/table/";
+    String expected = "s3://bucket/table/.hoodie/.locks";
+    String actual = StorageLockClient.getLockFolderPath(basePath);
+    assertEquals(expected, actual, "Path with trailing slash should be normalized correctly");
+  }
+
+  @Test
+  void testGetLockFolderPathWithMultipleTrailingSlashes() {
+    String basePath = "s3://bucket/table///";
+    String expected = "s3://bucket/table/.hoodie/.locks";
+    String actual = StorageLockClient.getLockFolderPath(basePath);
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  void testGetLockFolderPathLocalFileSystem() {
+    String basePath = "/tmp/hudi/table";
+    String expected = "/tmp/hudi/table/.hoodie/.locks";
+    String actual = StorageLockClient.getLockFolderPath(basePath);
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  void testGetLockFolderPathLocalFileSystemWithTrailingSlash() {
+    String basePath = "/tmp/hudi/table/";
+    String expected = "/tmp/hudi/table/.hoodie/.locks";
+    String actual = StorageLockClient.getLockFolderPath(basePath);
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  void testGetAuditConfigPathWithTrailingSlash() {
+    String basePath = "s3://bucket/table/";
+    String expected = "s3://bucket/table/.hoodie/.locks/audit_enabled.json";
+    String actual = StorageLockProviderAuditService.getAuditConfigPath(basePath);
+    assertEquals(expected, actual, "Audit config path with trailing slash should be normalized correctly");
+  }
+
+  @Test
+  void testGetAuditFolderPathWithTrailingSlash() {
+    String basePath = "s3://bucket/table/";
+    String expected = "s3://bucket/table/.hoodie/.locks/audit";
+    String actual = StorageLockProviderAuditService.getAuditFolderPath(basePath);
+    assertEquals(expected, actual, "Audit folder path with trailing slash should be normalized correctly");
+  }
 }
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/TestLockProviderHeartbeatManager.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/TestLockProviderHeartbeatManager.java
index 234aebe70b76..94ce33f37e96 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/TestLockProviderHeartbeatManager.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/lock/models/TestLockProviderHeartbeatManager.java
@@ -94,7 +94,7 @@ public class TestLockProviderHeartbeatManager {
 
     assertTrue(manager.startHeartbeatForThread(Thread.currentThread()));
     assertFalse(manager.startHeartbeatForThread(Thread.currentThread()));
-    verify(mockLogger).warn("Owner {}: Heartbeat is already running.", LOGGER_ID);
+    verify(mockLogger).error("Owner {}: Heartbeat is already running.", LOGGER_ID);
   }
 
   @Test
@@ -114,7 +114,7 @@ public class TestLockProviderHeartbeatManager {
     manager = createDefaultManagerWithMocks(() -> true);
 
     assertFalse(manager.stopHeartbeat(true));
-    verify(mockLogger).warn("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
+    verify(mockLogger).info("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
   }
 
   @Test
@@ -132,7 +132,7 @@ public class TestLockProviderHeartbeatManager {
 
     // Call stop again
     assertFalse(manager.stopHeartbeat(true));
-    verify(mockLogger).warn("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
+    verify(mockLogger).info("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
   }
 
   @Test
@@ -212,7 +212,7 @@ public class TestLockProviderHeartbeatManager {
     // This call will wait for heartbeat task to stop itself, as the semaphore has already been acquired by the heartbeat task.
     assertFalse(manager.stopHeartbeat(true));
 
-    verify(mockLogger).warn("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
+    verify(mockLogger).info("Owner {}: No active heartbeat task to stop.", LOGGER_ID);
     verify(mockLogger).debug(
             "Owner {}: Heartbeat started with interval: {} ms",
             "test-owner",
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestStorageBasedLockConfig.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestStorageBasedLockConfig.java
index 4855b6505877..9a79e369b0a6 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestStorageBasedLockConfig.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/config/TestStorageBasedLockConfig.java
@@ -41,7 +41,7 @@ class TestStorageBasedLockConfig {
         .build();
 
     assertEquals(5 * 60, config.getValiditySeconds(), "Default lock validity should be 5 minutes");
-    assertEquals(30, config.getHeartbeatPollSeconds(), "Default heartbeat poll time should be 30 seconds");
+    assertEquals(30, config.getRenewIntervalSecs(), "Default heartbeat poll time should be 30 seconds");
   }
 
   @Test
@@ -49,7 +49,7 @@ class TestStorageBasedLockConfig {
     // Testing that custom values which differ from defaults can be read properly
     TypedProperties props = new TypedProperties();
     props.setProperty(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "120");
-    props.setProperty(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "10");
+    props.setProperty(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "10");
     props.setProperty(BASE_PATH.key(), "/hudi/table/basepath");
 
     StorageBasedLockConfig config = new StorageBasedLockConfig.Builder()
@@ -57,7 +57,7 @@ class TestStorageBasedLockConfig {
         .build();
 
     assertEquals(120, config.getValiditySeconds());
-    assertEquals(10, config.getHeartbeatPollSeconds());
+    assertEquals(10, config.getRenewIntervalSecs());
     assertEquals("/hudi/table/basepath", config.getHudiTableBasePath());
   }
 
@@ -81,7 +81,7 @@ class TestStorageBasedLockConfig {
     props.setProperty(BASE_PATH.key(), "/hudi/table/basepath");
     // Invalid config case: validity timeout is less than 10x of heartbeat poll period
     props.setProperty(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "5");
-    props.setProperty(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "3");
+    props.setProperty(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "3");
     StorageBasedLockConfig.Builder propsBuilder = new StorageBasedLockConfig.Builder();
 
     IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
@@ -89,13 +89,13 @@ class TestStorageBasedLockConfig {
     assertTrue(exception.getMessage().contains(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key()));
     // Invalid config case: validity timeout is less than 10 seconds
     props.setProperty(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "9");
-    props.setProperty(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "1");
+    props.setProperty(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "1");
     exception = assertThrows(IllegalArgumentException.class, () -> propsBuilder.fromProperties(props));
     assertTrue(exception.getMessage().contains(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key()));
     // Invalid config case: heartbeat poll period is less than 1 second
     props.setProperty(StorageBasedLockConfig.VALIDITY_TIMEOUT_SECONDS.key(), "10");
-    props.setProperty(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key(), "0");
+    props.setProperty(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key(), "0");
     exception = assertThrows(IllegalArgumentException.class, () -> propsBuilder.fromProperties(props));
-    assertTrue(exception.getMessage().contains(StorageBasedLockConfig.HEARTBEAT_POLL_SECONDS.key()));
+    assertTrue(exception.getMessage().contains(StorageBasedLockConfig.RENEW_INTERVAL_SECS.key()));
   }
 }
diff --git a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/datadog/TestDatadogHttpClient.java b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/datadog/TestDatadogHttpClient.java
index e96819052246..92ea5422f04c 100644
--- a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/datadog/TestDatadogHttpClient.java
+++ b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/metrics/datadog/TestDatadogHttpClient.java
@@ -145,7 +145,7 @@ public class TestDatadogHttpClient {
 
     verify(appender).append(logCaptor.capture());
     assertEquals("Failed to send to Datadog. Response was unauthorized", logCaptor.getValue().getMessage().getFormattedMessage());
-    assertEquals(Level.WARN, logCaptor.getValue().getLevel());
+    assertEquals(Level.ERROR, logCaptor.getValue().getLevel());
   }
 
   @Test
diff --git a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/clustering/plan/strategy/FlinkSizeBasedClusteringPlanStrategyRecently.java b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/clustering/plan/strategy/FlinkSizeBasedClusteringPlanStrategyRecently.java
index 7d2c5785527c..339a17153445 100644
--- a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/clustering/plan/strategy/FlinkSizeBasedClusteringPlanStrategyRecently.java
+++ b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/client/clustering/plan/strategy/FlinkSizeBasedClusteringPlanStrategyRecently.java
@@ -123,11 +123,11 @@ public class FlinkSizeBasedClusteringPlanStrategyRecently<T> extends FlinkSizeBa
         partitions.addAll(metadata.getWritePartitionPaths());
       } catch (IOException e) {
         // ignore Exception here
-        LOG.warn("Exception while get instant details from commit metadata.", e);
+        LOG.warn("Failed to get details from commit metadata for instant [{}].", instant, e);
       }
     });
 
-    LOG.info("Partitions related to active timeline: " + partitions);
+    LOG.info("Partitions related to active timeline: {}", partitions);
     return new ArrayList<>(partitions);
   }
 }
\ No newline at end of file
diff --git a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkAppendHandle.java b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkAppendHandle.java
index e1a030c97af5..3c232c3d8aae 100644
--- a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkAppendHandle.java
+++ b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkAppendHandle.java
@@ -118,7 +118,7 @@ public class FlinkAppendHandle<T, I, K, O>
     } catch (Throwable throwable) {
       // The intermediate log file can still append based on the incremental MERGE semantics,
       // there is no need to delete the file.
-      LOG.warn("Error while trying to dispose the APPEND handle", throwable);
+      LOG.warn("Failed to close the APPEND handle", throwable);
     }
   }
 
diff --git a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkCreateHandle.java b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkCreateHandle.java
index 1cd117d2f0b1..39d2867df1c2 100644
--- a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkCreateHandle.java
+++ b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkCreateHandle.java
@@ -118,7 +118,7 @@ public class FlinkCreateHandle<T, I, K, O>
       while (storage.exists(path)) {
         StoragePath existing = path;
         path = newFilePathWithRollover(rollNumber++);
-        LOG.warn("Duplicate write for INSERT bucket with path: " + existing + ", rolls over to new path: " + path);
+        LOG.warn("Duplicate write for INSERT bucket with path: {}. Will write to new path [{}] instead", existing, path);
       }
       return path;
     } catch (IOException e) {
@@ -157,13 +157,13 @@ public class FlinkCreateHandle<T, I, K, O>
     try {
       close();
     } catch (Throwable throwable) {
-      LOG.warn("Error while trying to dispose the CREATE handle", throwable);
+      LOG.error("Failed to close the CREATE handle", throwable);
       try {
         storage.deleteFile(path);
-        LOG.info("Deleting the intermediate CREATE data file: " + path + " success!");
+        LOG.info("Successfully deleted the intermediate CREATE data file: {}", path);
       } catch (IOException e) {
         // logging a warning and ignore the exception.
-        LOG.warn("Deleting the intermediate CREATE data file: " + path + " failed", e);
+        LOG.warn("Failed to delete the intermediate CREATE data file: {}", path, e);
       }
     }
   }
diff --git a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkMergeHandle.java b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkMergeHandle.java
index c9c53ab108c1..0f0a26d986b0 100644
--- a/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkMergeHandle.java
+++ b/hudi-client/hudi-flink-client/src/main/java/org/apache/hudi/io/FlinkMergeHandle.java
@@ -216,13 +216,13 @@ public class FlinkMergeHandle<T, I, K, O>
     try {
       close();
     } catch (Throwable throwable) {
-      LOG.warn("Error while trying to dispose the MERGE handle", throwable);
+      LOG.error("Failed to close the MERGE handle", throwable);
       try {
         storage.deleteFile(newFilePath);
         LOG.info("Deleting the intermediate MERGE data file: " + newFilePath + " success!");
       } catch (IOException e) {
         // logging a warning and ignore the exception.
-        LOG.warn("Deleting the intermediate MERGE data file: " + newFilePath + " failed", e);
+        LOG.info("Deleting the intermediate MERGE data file: " + newFilePath + " failed", e);
       }
     }
   }
diff --git a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkValidatorUtils.java b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkValidatorUtils.java
index c2e1c96b2cad..c2b573d05071 100644
--- a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkValidatorUtils.java
+++ b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkValidatorUtils.java
@@ -141,8 +141,7 @@ public class SparkValidatorUtils {
                 new TableSchemaResolver(table.getMetaClient()).getTableAvroSchema()));
       } catch (Exception e) {
         LOG.warn("Cannot get table schema from before state.", e);
-        LOG.warn("Use the schema from after state (current transaction) to create the empty Spark "
-            + "dataframe: " + newStructTypeSchema);
+        LOG.warn("Using the schema from after state (current transaction) to create the empty Spark dataframe: {}", newStructTypeSchema);
         return sqlContext.createDataFrame(
             sqlContext.emptyDataFrame().rdd(), newStructTypeSchema);
       }
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/bootstrap/index/hfile/HFileBootstrapIndexReader.java b/hudi-common/src/main/java/org/apache/hudi/common/bootstrap/index/hfile/HFileBootstrapIndexReader.java
index 5691d3cf3aca..b734d5e5f688 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/bootstrap/index/hfile/HFileBootstrapIndexReader.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/bootstrap/index/hfile/HFileBootstrapIndexReader.java
@@ -181,7 +181,7 @@ public class HFileBootstrapIndexReader extends BootstrapIndex.IndexReader {
             .map(e -> new BootstrapFileMapping(bootstrapBasePath, metadata.getBootstrapPartitionPath(),
                 partition, e.getValue(), e.getKey())).collect(Collectors.toList());
       } else {
-        LOG.warn("No value found for partition key (" + partition + ")");
+        LOG.warn("No value found for partition key ({})", partition);
         return new ArrayList<>();
       }
     } catch (IOException ioe) {
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/conflict/detection/DirectMarkerBasedDetectionStrategy.java b/hudi-common/src/main/java/org/apache/hudi/common/conflict/detection/DirectMarkerBasedDetectionStrategy.java
index 40503f4d7139..96cd954a4cc6 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/conflict/detection/DirectMarkerBasedDetectionStrategy.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/conflict/detection/DirectMarkerBasedDetectionStrategy.java
@@ -106,7 +106,7 @@ public abstract class DirectMarkerBasedDetectionStrategy implements EarlyConflic
     }).count();
 
     if (res != 0L) {
-      LOG.warn("Detected conflict marker files: " + partitionPath + "/" + fileId + " for " + instantTime);
+      LOG.warn("Detected conflict marker files: {}/{} for {}", partitionPath, fileId, instantTime);
       return true;
     }
     return false;
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/fs/FailSafeConsistencyGuard.java b/hudi-common/src/main/java/org/apache/hudi/common/fs/FailSafeConsistencyGuard.java
index f873d8d22a54..ed82343ee3be 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/fs/FailSafeConsistencyGuard.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/fs/FailSafeConsistencyGuard.java
@@ -197,7 +197,7 @@ public class FailSafeConsistencyGuard implements ConsistencyGuard {
           return candidateFiles.isEmpty();
       }
     } catch (IOException ioe) {
-      LOG.warn("Got IOException waiting for file event. Have tried " + retryNum + " time(s)", ioe);
+      LOG.warn("Got IOException waiting for file event. Have tried {} time(s)", retryNum, ioe);
     }
     return false;
   }
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/heartbeat/HoodieHeartbeatUtils.java b/hudi-common/src/main/java/org/apache/hudi/common/heartbeat/HoodieHeartbeatUtils.java
index 7e6ce0e21351..a51914554d71 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/heartbeat/HoodieHeartbeatUtils.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/heartbeat/HoodieHeartbeatUtils.java
@@ -72,7 +72,7 @@ public class HoodieHeartbeatUtils {
     Long currentTime = System.currentTimeMillis();
     Long lastHeartbeatTime = getLastHeartbeatTime(storage, basePath, instantTime);
     if (currentTime - lastHeartbeatTime > maxAllowableHeartbeatIntervalInMs) {
-      LOG.warn("Heartbeat expired, for instant: " + instantTime);
+      LOG.warn("Heartbeat expired, for instant: {}", instantTime);
       return true;
     }
     return false;
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/model/HoodiePartitionMetadata.java b/hudi-common/src/main/java/org/apache/hudi/common/model/HoodiePartitionMetadata.java
index 16fd7d2f4348..533a9e3962f7 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/model/HoodiePartitionMetadata.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/model/HoodiePartitionMetadata.java
@@ -149,7 +149,7 @@ public class HoodiePartitionMetadata {
           storage.deleteFile(tmpPath);
         }
       } catch (IOException ioe) {
-        LOG.warn("Error trying to clean up temporary files for " + partitionPath, ioe);
+        LOG.info("Error trying to clean up temporary files for {}", partitionPath, ioe);
       }
     }
   }
@@ -213,7 +213,7 @@ public class HoodiePartitionMetadata {
       }
       return Option.of(props.getProperty(COMMIT_TIME_KEY));
     } catch (IOException ioe) {
-      LOG.warn("Error fetch Hoodie partition metadata for " + partitionPath, ioe);
+      LOG.warn("Error fetch Hoodie partition metadata for {}", partitionPath, ioe);
       return Option.empty();
     }
   }
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/TimelineUtils.java b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/TimelineUtils.java
index cdf2c6bb1d29..ea3c71cf1592 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/TimelineUtils.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/table/timeline/TimelineUtils.java
@@ -447,9 +447,8 @@ public class TimelineUtils {
             "Found hollow commit: '%s'. Adjust config `%s` accordingly if to avoid throwing this exception.",
             hollowCommitTimestamp, INCREMENTAL_READ_HANDLE_HOLLOW_COMMIT.key()));
       case BLOCK:
-        LOG.warn(String.format(
-            "Found hollow commit '%s'. Config `%s` was set to `%s`: no data will be returned beyond '%s' until it's completed.",
-            hollowCommitTimestamp, INCREMENTAL_READ_HANDLE_HOLLOW_COMMIT.key(), handlingMode, hollowCommitTimestamp));
+        LOG.warn("Found hollow commit '{}'. Config `{}` was set to `{}`: no data will be returned beyond '{}' until it's completed.",
+            hollowCommitTimestamp, INCREMENTAL_READ_HANDLE_HOLLOW_COMMIT.key(), handlingMode, hollowCommitTimestamp);
         return completedCommitTimeline.findInstantsBefore(hollowCommitTimestamp);
       default:
         throw new HoodieException("Unexpected handling mode: " + handlingMode);
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/util/ConfigUtils.java b/hudi-common/src/main/java/org/apache/hudi/common/util/ConfigUtils.java
index 3426477d90d2..e0bd6d11508f 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/util/ConfigUtils.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/ConfigUtils.java
@@ -292,9 +292,7 @@ public class ConfigUtils {
     }
     for (String alternative : configProperty.getAlternatives()) {
       if (props.containsKey(alternative)) {
-        LOG.warn(String.format("The configuration key '%s' has been deprecated "
-                + "and may be removed in the future. Please use the new key '%s' instead.",
-            alternative, configProperty.key()));
+        deprecationWarning(alternative, configProperty);
         return Option.ofNullable(props.get(alternative));
       }
     }
@@ -379,15 +377,18 @@ public class ConfigUtils {
     for (String alternative : configProperty.getAlternatives()) {
       value = props.get(alternative);
       if (value != null) {
-        LOG.warn(String.format("The configuration key '%s' has been deprecated "
-                + "and may be removed in the future. Please use the new key '%s' instead.",
-            alternative, configProperty.key()));
+        deprecationWarning(alternative, configProperty);
         return value.toString();
       }
     }
     return configProperty.hasDefaultValue() ? configProperty.defaultValue() : null;
   }
 
+  private static void deprecationWarning(String alternative, ConfigProperty<?> configProperty) {
+    LOG.warn("The configuration key '{}' has been deprecated and may be removed in the future."
+        + " Please use the new key '{}' instead.", alternative, configProperty.key());
+  }
+
   /**
    * Gets String value from properties with alternative keys.
    *
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/util/MarkerUtils.java b/hudi-common/src/main/java/org/apache/hudi/common/util/MarkerUtils.java
index 3b1270069c34..56f2b953856d 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/util/MarkerUtils.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/MarkerUtils.java
@@ -236,11 +236,10 @@ public class MarkerUtils {
       inputStream = storage.open(markersFilePath);
       markers = new HashSet<>(FileIOUtils.readAsUTFStringLines(inputStream));
     } catch (IOException e) {
-      String errorMessage = "Failed to read MARKERS file " + markersFilePath;
       if (ignoreException) {
-        LOG.warn(errorMessage + ". Ignoring the exception and continue.", e);
+        LOG.warn("Failed to read MARKERS file {}", markersFilePath, e);
       } else {
-        throw new HoodieIOException(errorMessage, e);
+        throw new HoodieIOException("Failed to read MARKERS file " + markersFilePath, e);
       }
     } finally {
       closeQuietly(inputStream);
diff --git a/hudi-common/src/main/java/org/apache/hudi/common/util/collection/LazyFileIterable.java b/hudi-common/src/main/java/org/apache/hudi/common/util/collection/LazyFileIterable.java
index 799aa3d4d564..fe587d78fd6b 100644
--- a/hudi-common/src/main/java/org/apache/hudi/common/util/collection/LazyFileIterable.java
+++ b/hudi-common/src/main/java/org/apache/hudi/common/util/collection/LazyFileIterable.java
@@ -133,7 +133,7 @@ public class LazyFileIterable<T, R> implements Iterable<R> {
 
     private void addShutdownHook() {
       shutdownThread = new Thread(() -> {
-        LOG.warn("Failed to properly close LazyFileIterable in application.");
+        LOG.debug("Failed to properly close LazyFileIterable in application.");
         this.closeHandle();
       });
       Runtime.getRuntime().addShutdownHook(shutdownThread);
diff --git a/hudi-common/src/main/java/org/apache/hudi/metadata/BaseTableMetadata.java b/hudi-common/src/main/java/org/apache/hudi/metadata/BaseTableMetadata.java
index 254f421284f0..c9e543f56092 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metadata/BaseTableMetadata.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metadata/BaseTableMetadata.java
@@ -416,8 +416,9 @@ public abstract class BaseTableMetadata extends AbstractHoodieTableMetadata {
   private void checkForSpuriousDeletes(HoodieMetadataPayload metadataPayload, String partitionName) {
     if (!metadataPayload.getDeletions().isEmpty()) {
       if (metadataConfig.shouldIgnoreSpuriousDeletes()) {
-        LOG.warn("Metadata record for " + partitionName + " encountered some files to be deleted which was not added before. "
-            + "Ignoring the spurious deletes as the `" + HoodieMetadataConfig.IGNORE_SPURIOUS_DELETES.key() + "` config is set to true");
+        LOG.warn("Metadata record for {} encountered some files to be deleted which were not added before."
+                + " Ignoring the spurious deletes as the `{}` config is set to true",
+            partitionName, HoodieMetadataConfig.IGNORE_SPURIOUS_DELETES.key());
       } else {
         throw new HoodieMetadataException("Metadata record for " + partitionName + " is inconsistent: "
             + metadataPayload);
diff --git a/hudi-common/src/main/java/org/apache/hudi/metadata/HoodieTableMetadataUtil.java b/hudi-common/src/main/java/org/apache/hudi/metadata/HoodieTableMetadataUtil.java
index 4c96148aab11..71dabc2e5a76 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metadata/HoodieTableMetadataUtil.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metadata/HoodieTableMetadataUtil.java
@@ -690,6 +690,7 @@ public class HoodieTableMetadataUtil {
 
     if (columnsToIndex.isEmpty()) {
       // In case there are no columns to index, bail
+      LOG.info("No columns to index for column stats index.");
       return engineContext.emptyHoodieData();
     }
 
@@ -859,7 +860,7 @@ public class HoodieTableMetadataUtil {
       final String filename = partitionFileFlagTuple.f1;
       final boolean isDeleted = partitionFileFlagTuple.f2;
       if (!FSUtils.isBaseFile(new StoragePath(filename))) {
-        LOG.warn("Ignoring file {} as it is not a base file", filename);
+        LOG.info("Ignoring file {} as it is not a base file", filename);
         return Stream.<HoodieRecord>empty().iterator();
       }
 
@@ -1419,7 +1420,7 @@ public class HoodieTableMetadataUtil {
               timeline.readRollbackInfoAsBytes(new HoodieInstant(HoodieInstant.State.REQUESTED, HoodieTimeline.ROLLBACK_ACTION,
                   instant.getTimestamp())).get(), HoodieRollbackPlan.class);
           commitsToRollback = Collections.singletonList(rollbackPlan.getInstantToRollback().getCommitTime());
-          LOG.warn("Had to fetch rollback info from requested instant since completed file is empty {}", instant);
+          LOG.info("Fetching rollback info from requested instant as the completed file is empty {}", instant);
         }
         return commitsToRollback;
       }
diff --git a/hudi-common/src/main/java/org/apache/hudi/metrics/Metrics.java b/hudi-common/src/main/java/org/apache/hudi/metrics/Metrics.java
index 441c0b5b002f..761f1fc6e749 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metrics/Metrics.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metrics/Metrics.java
@@ -125,7 +125,7 @@ public class Metrics {
     if (!fromShutdownHook) {
       Runtime.getRuntime().removeShutdownHook(shutdownThread);
     } else {
-      LOG.warn("Shutting down the metrics reporter from shutdown hook.");
+      LOG.debug("Shutting down the metrics reporter from shutdown hook.");
     }
     if (initialized) {
       try {
diff --git a/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java b/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
index 0b86007fb616..d910e240605c 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
@@ -58,8 +58,8 @@ public class MetricsReporterFactory {
     MetricsReporterType type = metricsConfig.getMetricsReporterType();
     MetricsReporter reporter = null;
     if (type == null) {
-      LOG.warn(String.format("Metric creation failed. %s is not configured",
-          HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()));
+      LOG.warn("Metric creation failed. {} is not configured",
+          HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key());
       return Option.empty();
     }
 
diff --git a/hudi-common/src/main/java/org/apache/hudi/metrics/datadog/DatadogHttpClient.java b/hudi-common/src/main/java/org/apache/hudi/metrics/datadog/DatadogHttpClient.java
index f4cb157ce759..f53d2d8ebb26 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metrics/datadog/DatadogHttpClient.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metrics/datadog/DatadogHttpClient.java
@@ -103,7 +103,7 @@ public class DatadogHttpClient implements Closeable {
     try (CloseableHttpResponse response = client.execute(request)) {
       int statusCode = response.getStatusLine().getStatusCode();
       if (statusCode >= 300) {
-        LOG.warn(String.format("Failed to send to Datadog. Response was %s", response));
+        LOG.error("Failed to send to Datadog. Response was {}", response);
       } else {
         LOG.debug(String.format("Sent metrics data (size: %d) to %s", payload.length(), seriesUrl));
       }
diff --git a/hudi-common/src/main/java/org/apache/hudi/metrics/prometheus/PushGatewayReporter.java b/hudi-common/src/main/java/org/apache/hudi/metrics/prometheus/PushGatewayReporter.java
index 21ecb85a0b8a..7c672a2e7992 100644
--- a/hudi-common/src/main/java/org/apache/hudi/metrics/prometheus/PushGatewayReporter.java
+++ b/hudi-common/src/main/java/org/apache/hudi/metrics/prometheus/PushGatewayReporter.java
@@ -136,7 +136,7 @@ public class PushGatewayReporter extends ScheduledReporter {
         }
       }
     } catch (IOException e) {
-      LOG.warn("Failed to delete metrics from pushGateway with jobName {" + jobName + "}", e);
+      LOG.warn("Failed to delete metrics from pushGateway with jobName {{}}", jobName, e);
     }
   }
 
diff --git a/hudi-common/src/test/java/org/apache/hudi/common/util/TestHoodieTimer.java b/hudi-common/src/test/java/org/apache/hudi/common/util/TestHoodieTimer.java
new file mode 100644
index 000000000000..bc3c7d1446c6
--- /dev/null
+++ b/hudi-common/src/test/java/org/apache/hudi/common/util/TestHoodieTimer.java
@@ -0,0 +1,41 @@
+/*
+ * 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.hudi.common.util;
+
+import org.junit.jupiter.api.Test;
+
+import static java.lang.Thread.sleep;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestHoodieTimer {
+
+  @Test
+  public void testTryStop() throws InterruptedException {
+    HoodieTimer timer = new HoodieTimer();
+    assertFalse(timer.tryEndTimer().isPresent());
+
+    timer.startTimer();
+    sleep(250);
+    Option<Long> result = timer.tryEndTimer();
+    assertTrue(result.isPresent());
+    assertTrue(result.get() >= 250);
+  }
+}
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkClusteringMetrics.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkClusteringMetrics.java
index 081c8f79a73f..d1ff3b28360e 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkClusteringMetrics.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkClusteringMetrics.java
@@ -90,7 +90,7 @@ public class FlinkClusteringMetrics extends FlinkWriteMetrics {
         this.clusteringDelay = Duration.between(start, Instant.now()).getSeconds();
       }
     } catch (ParseException e) {
-      LOG.warn("Invalid input clustering instant" + firstPendingClusteringInstant);
+      LOG.warn("Invalid input clustering instant: {}", firstPendingClusteringInstant);
     }
   }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkCompactionMetrics.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkCompactionMetrics.java
index abf7ef05a3fb..7526fdc23805 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkCompactionMetrics.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkCompactionMetrics.java
@@ -91,7 +91,7 @@ public class FlinkCompactionMetrics extends FlinkWriteMetrics {
         this.compactionDelay = Duration.between(start, Instant.now()).getSeconds();
       }
     } catch (ParseException e) {
-      LOG.warn("Invalid input compaction instant" + firstPendingCompactionInstant);
+      LOG.warn("Invalid input compaction instant: {}", firstPendingCompactionInstant);
     }
   }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkStreamReadMetrics.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkStreamReadMetrics.java
index ce7a982b82c5..b661451d4d79 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkStreamReadMetrics.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkStreamReadMetrics.java
@@ -79,7 +79,7 @@ public class FlinkStreamReadMetrics extends HoodieFlinkMetrics {
       this.issuedInstant = instant.getEpochSecond();
       this.issuedInstantDelay = Duration.between(instant, Instant.now()).getSeconds();
     } catch (ParseException e) {
-      LOG.warn("Invalid input issued instant: " + issuedInstant);
+      LOG.warn("Invalid input issued instant: {}", issuedInstant);
     }
   }
 
@@ -89,7 +89,7 @@ public class FlinkStreamReadMetrics extends HoodieFlinkMetrics {
       this.splitLatestCommit = instant.getEpochSecond();
       this.splitLatestCommitDelay = Duration.between(instant, Instant.now()).getSeconds();
     } catch (ParseException e) {
-      LOG.warn("Invalid input latest commit" + splitLatestCommit);
+      LOG.warn("Invalid input latest commit: {}", splitLatestCommit);
     }
   }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkWriteMetrics.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkWriteMetrics.java
index b19f8ef32d90..4f2725023893 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkWriteMetrics.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/FlinkWriteMetrics.java
@@ -78,7 +78,7 @@ public class FlinkWriteMetrics extends HoodieFlinkMetrics {
     try {
       commitEpochTimeInMs = HoodieInstantTimeGenerator.parseDateFromInstantTime(instantTime).getTime();
     } catch (ParseException e) {
-      LOG.warn("Invalid input issued instant: " + instantTime);
+      LOG.warn("Invalid input issued instant: {}", instantTime);
       return;
     }
     updateCommitMetrics(commitEpochTimeInMs, System.currentTimeMillis() - commitEpochTimeInMs, metadata);
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/HoodieFlinkMetrics.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/HoodieFlinkMetrics.java
index ce58f35402a0..66f9ba1226f3 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/HoodieFlinkMetrics.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/metrics/HoodieFlinkMetrics.java
@@ -44,14 +44,14 @@ public abstract class HoodieFlinkMetrics {
 
   protected void startTimer(String name) {
     if (timers.containsKey(name)) {
-      LOG.warn("Restarting timer for name: {}, override the value", name);
+      LOG.info("Restarting timer for name: {}, overriding the existing value", name);
     }
     timers.put(name, System.currentTimeMillis());
   }
 
   protected long stopTimer(String name) {
     if (!timers.containsKey(name)) {
-      LOG.warn("Cannot found name {} in timer, potentially caused by inconsistent call", name);
+      LOG.warn("Cannot find name {} in timer, potentially caused by inconsistent call", name);
       return 0;
     }
     long costs = System.currentTimeMillis() - timers.get(name);
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
index 9494f56cffa9..5c66f8469e4e 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/CleanFunction.java
@@ -98,7 +98,7 @@ public class CleanFunction<T> extends AbstractRichFunction
         this.isCleaning = true;
       } catch (Throwable throwable) {
         // catch the exception to not affect the normal checkpointing
-        LOG.warn("Error while start async cleaning", throwable);
+        LOG.warn("Failed to start async cleaning", throwable);
       }
     }
   }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteOperatorCoordinator.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteOperatorCoordinator.java
index 13b9fd5247a5..95711abfd97e 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteOperatorCoordinator.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/StreamWriteOperatorCoordinator.java
@@ -572,8 +572,8 @@ public class StreamWriteOperatorCoordinator
     if (!hasErrors || this.conf.getBoolean(FlinkOptions.IGNORE_FAILED)) {
       HashMap<String, String> checkpointCommitMetadata = new HashMap<>();
       if (hasErrors) {
-        LOG.warn("Some records failed to merge but forcing commit since commitOnErrors set to true. Errors/Total="
-            + totalErrorRecords + "/" + totalRecords);
+        LOG.warn("Some records failed to merge but forcing commit since commitOnErrors set to true. Errors/Total={}/{}",
+            totalErrorRecords, totalRecords);
       }
 
       final Map<String, List<String>> partitionToReplacedFileIds = tableState.isOverwrite
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperator.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperator.java
index 9e261051e29a..2da04dfa5afc 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperator.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperator.java
@@ -171,7 +171,7 @@ public class BootstrapOperator<I, O extends HoodieRecord<?>>
 
         TimeUnit.SECONDS.sleep(5);
       } catch (Exception e) {
-        LOG.warn("Update global task bootstrap summary error", e);
+        LOG.error("Updating global task bootstrap summary failed", e);
       }
     }
   }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringPlanSourceFunction.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringPlanSourceFunction.java
index 292e3bba5cc7..ed86a28818b9 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringPlanSourceFunction.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/clustering/ClusteringPlanSourceFunction.java
@@ -83,7 +83,7 @@ public class ClusteringPlanSourceFunction extends AbstractRichFunction implement
         sourceContext.collect(new ClusteringPlanEvent(this.clusteringInstantTime, ClusteringGroupInfo.create(clusteringGroup), clusteringPlan.getStrategy().getStrategyParams()));
       }
     } else {
-      LOG.warn(clusteringInstantTime + " not found in pending clustering instants.");
+      LOG.warn("{} not found in pending clustering instants.", clusteringInstantTime);
     }
   }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionPlanSourceFunction.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionPlanSourceFunction.java
index 6e48c0b386ca..8be9c5edac5c 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionPlanSourceFunction.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/compact/CompactionPlanSourceFunction.java
@@ -75,13 +75,13 @@ public class CompactionPlanSourceFunction extends AbstractRichFunction implement
     HoodieTimeline pendingCompactionTimeline = StreamerUtil.createMetaClient(conf).getActiveTimeline().filterPendingCompactionTimeline();
     for (Pair<String, HoodieCompactionPlan> pair : compactionPlans) {
       if (!pendingCompactionTimeline.containsInstant(pair.getLeft())) {
-        LOG.warn(pair.getLeft() + " not found in pending compaction instants.");
+        LOG.warn("{} not found in pending compaction instants.", pair.getLeft());
         continue;
       }
       HoodieCompactionPlan compactionPlan = pair.getRight();
       List<CompactionOperation> operations = compactionPlan.getOperations().stream()
           .map(CompactionOperation::convertFromAvroRecordInstance).collect(Collectors.toList());
-      LOG.info("CompactionPlanFunction compacting " + operations + " files");
+      LOG.info("CompactionPlanFunction compacting {} files", operations);
       for (CompactionOperation operation : operations) {
         sourceContext.collect(new CompactionPlanEvent(pair.getLeft(), operation));
       }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionEvaluators.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionEvaluators.java
index 0feda05a4c77..41d0014dc1e5 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionEvaluators.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionEvaluators.java
@@ -413,7 +413,7 @@ public class ExpressionEvaluators {
 
       if (vals.length > IN_PREDICATE_LIMIT) {
         // skip evaluating the predicate if the number of values is too big
-        LOGGER.warn("Skip evaluating in predicate because the number of values is too big!");
+        LOGGER.warn("Skipping evaluation of `in` predicate because the number of values is too big!");
         return true;
       }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionPredicates.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionPredicates.java
index 58ee59a81766..1b162150dcc8 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionPredicates.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/ExpressionPredicates.java
@@ -216,7 +216,9 @@ public class ExpressionPredicates {
       if (literalObject instanceof Serializable) {
         this.literal = (Serializable) literalObject;
       } else {
-        LOG.warn("Encountered a non-serializable literal. " + "Cannot push predicate with value literal [{}] into FileInputFormat. " + "This is a bug and should be reported.", valueLiteral);
+        LOG.warn("Encountered a non-serializable literal. "
+            + "Cannot push predicate with value literal [{}] into FileInputFormat. "
+            + "This is a bug and should be reported.", valueLiteral);
         this.literal = null;
       }
       return this;
@@ -437,7 +439,9 @@ public class ExpressionPredicates {
         if (literalObject instanceof Serializable) {
           return (Serializable) literalObject;
         } else {
-          LOG.warn("Encountered a non-serializable literal. " + "Cannot push predicate with value literal [{}] into FileInputFormat. " + "This is a bug and should be reported.", valueLiteral);
+          LOG.warn("Encountered a non-serializable literal. "
+              + "Cannot push predicate with value literal [{}] into FileInputFormat. "
+              + "This is a bug and should be reported.", valueLiteral);
           return null;
         }
       }).collect(Collectors.toList());
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/FileIndex.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/FileIndex.java
index 649a552a7b47..cd353b5b0366 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/FileIndex.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/FileIndex.java
@@ -312,7 +312,7 @@ public class FileIndex {
       if (metadataConfig.isEnabled()) {
         return true;
       } else {
-        LOG.warn("Data skipping requires Metadata Table to be enabled! Disable the data skipping");
+        LOG.warn("Data skipping requires Metadata Table to be enabled! Disable the data skipping or enable the metadata table.");
       }
     }
     return false;
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/IncrementalInputSplits.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/IncrementalInputSplits.java
index e13224123f05..834c398de708 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/IncrementalInputSplits.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/IncrementalInputSplits.java
@@ -214,7 +214,7 @@ public class IncrementalInputSplits implements Serializable {
           path, (org.apache.hadoop.conf.Configuration) metaClient.getStorageConf().unwrap(),
           metadataList, metaClient.getTableType(), false);
       if (files == null) {
-        LOG.warn("Found deleted files in metadata, fall back to full table scan.");
+        LOG.warn("Found deleted files in metadata, falling back to full table scan.");
         // fallback to full table scan
         // reading from the earliest, scans the partitions and files directly.
         FileIndex fileIndex = getFileIndex();
@@ -297,13 +297,13 @@ public class IncrementalInputSplits implements Serializable {
 
       Set<String> readPartitions = new TreeSet<>(fileIndex.getOrBuildPartitionPaths());
       if (readPartitions.size() == 0) {
-        LOG.warn("No partitions found for reading under path: " + path);
+        LOG.warn("No partitions found for reading under path: {}", path);
         return Result.EMPTY;
       }
 
       List<StoragePathInfo> pathInfoList = fileIndex.getFilesInPartitions();
       if (pathInfoList.size() == 0) {
-        LOG.warn("No files found for reading under path: " + path);
+        LOG.warn("No files found for reading under path: {}", path);
         return Result.EMPTY;
       }
 
@@ -343,8 +343,8 @@ public class IncrementalInputSplits implements Serializable {
     if (archivedMetadataList.size() > 0) {
       LOG.warn("\n"
           + "--------------------------------------------------------------------------------\n"
-          + "---------- caution: the reader has fall behind too much from the writer,\n"
-          + "---------- tweak 'read.tasks' option to add parallelism of read tasks.\n"
+          + "---------- caution: the reader has fallen behind too much from the writer,\n"
+          + "---------- update 'read.tasks' option to add parallelism of read tasks.\n"
           + "--------------------------------------------------------------------------------");
     }
     // IMPORTANT: the merged metadata list must be in ascending order by instant time
@@ -352,14 +352,14 @@ public class IncrementalInputSplits implements Serializable {
 
     Set<String> readPartitions = getReadPartitions(metadataList);
     if (readPartitions.size() == 0) {
-      LOG.warn("No partitions found for reading under path: " + path);
+      LOG.warn("No partitions found for reading under path: {}", path);
       return Collections.emptyList();
     }
     List<StoragePathInfo> pathInfoList = WriteProfiles.getFilesFromMetadata(
         path, hadoopConf, metadataList, metaClient.getTableType());
 
     if (pathInfoList.size() == 0) {
-      LOG.warn("No files found for reading under path: " + path);
+      LOG.warn("No files found for reading under path: {}", path);
       return Collections.emptyList();
     }
 
@@ -469,7 +469,7 @@ public class IncrementalInputSplits implements Serializable {
     Map<HoodieFileGroupId, List<HoodieCDCFileSplit>> fileSplits = extractor.extractCDCFileSplits();
 
     if (fileSplits.isEmpty()) {
-      LOG.warn("No change logs found for reading in path: " + path);
+      LOG.warn("No change logs found for reading in path: {}", path);
       return Collections.emptyList();
     }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/StreamReadMonitoringFunction.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/StreamReadMonitoringFunction.java
index 6f0fd9253e25..8cb37118e4c9 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/StreamReadMonitoringFunction.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/source/StreamReadMonitoringFunction.java
@@ -215,6 +215,7 @@ public class StreamReadMonitoringFunction
         incrementalInputSplits.inputSplits(metaClient, this.issuedInstant, this.issuedOffset, this.cdcEnabled);
     if (result.isEmpty()) {
       // no new instants, returns early
+      LOG.info("No new instants to read for current run.");
       return;
     }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
index 6865906b3674..3802f9a7b640 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
@@ -127,7 +127,7 @@ public class HoodieTableFactory implements DynamicTableSourceFactory, DynamicTab
           }
           if (tableConfig.contains(HoodieTableConfig.TYPE) && conf.contains(FlinkOptions.TABLE_TYPE)) {
             if (!tableConfig.getString(HoodieTableConfig.TYPE).equals(conf.get(FlinkOptions.TABLE_TYPE))) {
-              LOG.warn(
+              LOG.error(
                   String.format("Table type conflict : %s in %s and %s in table options. Fix the table type as to be in line with the hoodie.properties.",
                       tableConfig.getString(HoodieTableConfig.TYPE), HoodieTableConfig.HOODIE_PROPERTIES_FILE,
                       conf.get(FlinkOptions.TABLE_TYPE)));
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSource.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSource.java
index 7a397bf45c64..3d2f0fccdeee 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSource.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableSource.java
@@ -415,7 +415,7 @@ public class HoodieTableSource implements
             final List<MergeOnReadInputSplit> inputSplits = buildInputSplits();
             if (inputSplits.size() == 0) {
               // When there is no input splits, just return an empty source.
-              LOG.warn("No input splits generate for MERGE_ON_READ input format, returns empty collection instead");
+              LOG.info("No input splits generate for MERGE_ON_READ input format. Returning empty collection");
               return InputFormats.EMPTY_INPUT_FORMAT;
             }
             return mergeOnReadInputFormat(rowType, requiredRowType, tableAvroSchema,
@@ -439,7 +439,7 @@ public class HoodieTableSource implements
         final IncrementalInputSplits.Result result = incrementalInputSplits.inputSplits(metaClient, cdcEnabled);
         if (result.isEmpty()) {
           // When there is no input splits, just return an empty source.
-          LOG.warn("No input splits generate for incremental read, returns empty collection instead");
+          LOG.info("No input splits generated for incremental read. Returning empty collection");
           return InputFormats.EMPTY_INPUT_FORMAT;
         } else if (cdcEnabled) {
           return cdcInputFormat(rowType, requiredRowType, tableAvroSchema, rowDataType, result.getInputSplits());
@@ -601,7 +601,7 @@ public class HoodieTableSource implements
       return schemaResolver.getTableAvroSchema();
     } catch (Throwable e) {
       // table exists but has no written data
-      LOG.warn("Get table avro schema error, use schema from the DDL instead", e);
+      LOG.warn("Unable to resolve schema from table, using schema from the DDL", e);
       return inferSchemaFromDdl();
     }
   }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieCatalog.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieCatalog.java
index 63941ea36fa4..e5656590c803 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieCatalog.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieCatalog.java
@@ -583,7 +583,7 @@ public class HoodieCatalog extends AbstractCatalog {
         HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(path, hadoopConf);
         return new TableSchemaResolver(metaClient).getTableAvroSchema(false); // change log mode is not supported now
       } catch (Throwable throwable) {
-        LOG.warn("Error while resolving the latest table schema.", throwable);
+        LOG.warn("Failed to resolve the latest table schema.", throwable);
         // ignored
       }
     }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieHiveCatalog.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieHiveCatalog.java
index 3199448a90c2..bbb056df0970 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieHiveCatalog.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/catalog/HoodieHiveCatalog.java
@@ -441,7 +441,7 @@ public class HoodieHiveCatalog extends AbstractCatalog {
       }
       schema = builder.build();
     } else {
-      LOG.warn("{} does not have any hoodie schema, and use hive table schema to infer the table schema", tablePath);
+      LOG.warn(" Table: {}, does not have a hoodie schema. Using hive table schema instead.", tablePath);
       schema = HiveSchemaUtils.convertTableSchema(hiveTable);
     }
     Map<String, String> options = supplementOptions(tablePath, parameters);
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
index 6f90e4822180..ac53bd25c45c 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/format/cow/CopyOnWriteInputFormat.java
@@ -210,10 +210,8 @@ public class CopyOnWriteInputFormat extends FileInputFormat<RowData> {
       if (this.minSplitSize <= blockSize) {
         minSplitSize = this.minSplitSize;
       } else {
-        if (LOG.isWarnEnabled()) {
-          LOG.warn("Minimal split size of " + this.minSplitSize + " is larger than the block size of "
-              + blockSize + ". Decreasing minimal split size to block size.");
-        }
+        LOG.warn("Minimal split size of {} is larger than the block size of {}."
+            + " Decreasing minimal split size to block size.", this.minSplitSize, blockSize);
         minSplitSize = blockSize;
       }
 
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ClientIds.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ClientIds.java
index affea2e5d435..1f8bf1e65f71 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ClientIds.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ClientIds.java
@@ -200,7 +200,7 @@ public class ClientIds implements AutoCloseable, Serializable {
         // 1. If there are any zombie client ids, reuse the smallest one
         for (Path path : zombieHeartbeatPaths) {
           fs.delete(path, true);
-          LOG.warn("Delete inactive ckp metadata path: " + path);
+          LOG.warn("Deleting inactive checkpoint metadata path: {}", path);
         }
         return getClientId(zombieHeartbeatPaths.get(0));
       }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ImplicitTypeConverter.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ImplicitTypeConverter.java
index 601b878655fc..b05d9e7ad7e2 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ImplicitTypeConverter.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/ImplicitTypeConverter.java
@@ -127,7 +127,7 @@ public class ImplicitTypeConverter {
           return literal;
       }
     } catch (RuntimeException e) {
-      LOG.warn("Failed to convert literal [{}] to type [{}]. Use its original type", literal, literalType);
+      LOG.warn("Failed to convert literal [{}] to type [{}]. Will use its original type", literal, literalType);
       return literal;
     }
   }
diff --git a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
index 128a7385bf0c..a51fb029c8cc 100644
--- a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
+++ b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
@@ -444,7 +444,7 @@ public class StreamerUtil {
       HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(path, hadoopConf);
       return getTableAvroSchema(metaClient, false);
     } catch (Exception e) {
-      LOG.warn("Error while resolving the latest table schema", e);
+      LOG.error("Failed to resolve the latest table schema", e);
     }
     return null;
   }
diff --git a/hudi-gcp/src/main/java/org/apache/hudi/gcp/transaction/lock/GCSStorageLockClient.java b/hudi-gcp/src/main/java/org/apache/hudi/gcp/transaction/lock/GCSStorageLockClient.java
index e9b5e8c82aab..b1f39e9f0ac7 100644
--- a/hudi-gcp/src/main/java/org/apache/hudi/gcp/transaction/lock/GCSStorageLockClient.java
+++ b/hudi-gcp/src/main/java/org/apache/hudi/gcp/transaction/lock/GCSStorageLockClient.java
@@ -25,11 +25,9 @@ import org.apache.hudi.client.transaction.lock.models.StorageLockData;
 import org.apache.hudi.client.transaction.lock.models.StorageLockFile;
 import org.apache.hudi.common.util.Functions;
 import org.apache.hudi.common.util.Option;
-import org.apache.hudi.common.util.StringUtils;
 import org.apache.hudi.common.util.VisibleForTesting;
 import org.apache.hudi.common.util.collection.Pair;
 import org.apache.hudi.exception.HoodieIOException;
-import org.apache.hudi.exception.HoodieLockException;
 
 import com.google.cloud.storage.Blob;
 import com.google.cloud.storage.BlobId;
@@ -46,11 +44,11 @@ import javax.annotation.concurrent.ThreadSafe;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UncheckedIOException;
-import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.channels.Channels;
 import java.util.Properties;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 /**
  * A GCS-based implementation of a distributed lock provider using conditional writes
  * with generationMatch, plus local concurrency safety, heartbeat/renew, and pruning old locks.
@@ -68,10 +66,12 @@ public class GCSStorageLockClient implements StorageLockClient {
   private final String lockFilePath;
   private final String ownerId;
 
-  /** Constructor that is used by reflection to instantiate a GCS-based locking service.
-   * @param ownerId The owner id.
+  /**
+   * Constructor that is used by reflection to instantiate a GCS-based locking service.
+   *
+   * @param ownerId     The owner id.
    * @param lockFileUri The path within the bucket where to write lock files.
-   * @param props The properties for the lock config, can be used to customize client.
+   * @param props       The properties for the lock config, can be used to customize client.
    */
   public GCSStorageLockClient(
       String ownerId,
@@ -87,25 +87,12 @@ public class GCSStorageLockClient implements StorageLockClient {
       Properties properties,
       Functions.Function1<Properties, Storage> gcsClientSupplier,
       Logger logger) {
-    try {
-      // This logic can likely be extended to other lock client implementations.
-      // Consider creating base class with utilities, incl error handling.
-      URI uri = new URI(lockFileUri);
-      this.bucketName = uri.getHost();
-      this.lockFilePath = uri.getPath().replaceFirst("/", "");
-      this.gcsClient = gcsClientSupplier.apply(properties);
-
-      if (StringUtils.isNullOrEmpty(this.bucketName)) {
-        throw new IllegalArgumentException("LockFileUri does not contain a valid bucket name.");
-      }
-      if (StringUtils.isNullOrEmpty(this.lockFilePath)) {
-        throw new IllegalArgumentException("LockFileUri does not contain a valid lock file path.");
-      }
-      this.ownerId = ownerId;
-      this.logger = logger;
-    } catch (URISyntaxException e) {
-      throw new HoodieLockException(e);
-    }
+    Pair<String, String> bucketAndPath = StorageLockClient.parseBucketAndPath(lockFileUri);
+    this.bucketName = bucketAndPath.getLeft();
+    this.lockFilePath = bucketAndPath.getRight();
+    this.gcsClient = gcsClientSupplier.apply(properties);
+    this.ownerId = ownerId;
+    this.logger = logger;
   }
 
   private static Functions.Function1<Properties, Storage> createDefaultGcsClient() {
@@ -119,7 +106,7 @@ public class GCSStorageLockClient implements StorageLockClient {
   /**
    * Attempts to create or update the lock file using the given lock data and generation number.
    *
-   * @param lockData the new lock data to use.
+   * @param lockData         the new lock data to use.
    * @param generationNumber the expected generation number (0 for creation).
    * @return the updated StorageLockFile instance.
    * @throws StorageException if the update fails.
@@ -149,13 +136,13 @@ public class GCSStorageLockClient implements StorageLockClient {
       return Pair.of(LockUpsertResult.SUCCESS, Option.of(updatedFile));
     } catch (StorageException e) {
       if (e.getCode() == PRECONDITION_FAILURE_ERROR_CODE) {
-        logger.info("OwnerId: {}, Unable to write new lock file. Another process has modified this lockfile {} already.", 
+        logger.info("OwnerId: {}, Unable to write new lock file. Another process has modified this lockfile {} already.",
             ownerId, lockFilePath);
         return Pair.of(LockUpsertResult.ACQUIRED_BY_OTHERS, Option.empty());
       } else if (e.getCode() == RATE_LIMIT_ERROR_CODE) {
         logger.warn("OwnerId: {}, Rate limit exceeded for lock file: {}", ownerId, lockFilePath);
       } else if (e.getCode() >= INTERNAL_SERVER_ERROR_CODE_MIN) {
-        logger.warn("OwnerId: {}, GCS returned internal server error code for lock file: {}", 
+        logger.warn("OwnerId: {}, GCS returned internal server error code for lock file: {}",
             ownerId, lockFilePath, e);
       } else {
         throw e;
@@ -166,7 +153,8 @@ public class GCSStorageLockClient implements StorageLockClient {
 
   /**
    * Handling storage exception for GET request
-   * @param e The error to handle.
+   *
+   * @param e         The error to handle.
    * @param ignore404 Whether to ignore 404 as a valid exception.
    *                  When we read from stream we might see this, and
    *                  it should not be counted as NOT_EXISTS.
@@ -225,6 +213,69 @@ public class GCSStorageLockClient implements StorageLockClient {
     }
   }
 
+  @Override
+  public Option<String> readObject(String filePath, boolean checkExistsFirst) {
+    try {
+      // Parse the file path to get bucket and object path
+      Pair<String, String> bucketAndPath = StorageLockClient.parseBucketAndPath(filePath);
+      String bucket = bucketAndPath.getLeft();
+      String objectPath = bucketAndPath.getRight();
+
+      BlobId blobId = BlobId.of(bucket, objectPath);
+
+      if (checkExistsFirst) {
+        // First check if the file exists (lightweight metadata check)
+        Blob blob = gcsClient.get(blobId);
+
+        if (blob == null || !blob.exists()) {
+          // File doesn't exist - this is the common case for optional configs
+          logger.debug("JSON config file not found: {}", filePath);
+          return Option.empty();
+        }
+
+        // File exists, read its content
+        byte[] content = blob.getContent();
+        return Option.of(new String(content, UTF_8));
+      } else {
+        // Direct read without existence check
+        byte[] content = gcsClient.readAllBytes(blobId);
+        return Option.of(new String(content, UTF_8));
+      }
+    } catch (StorageException e) {
+      if (e.getCode() == NOT_FOUND_ERROR_CODE) {
+        logger.debug("JSON config file not found: {}", filePath);
+      } else {
+        logger.warn("Error reading JSON config file: {}", filePath, e);
+      }
+      return Option.empty();
+    } catch (Exception e) {
+      logger.warn("Error reading JSON config file: {}", filePath, e);
+      return Option.empty();
+    }
+  }
+
+  @Override
+  public boolean writeObject(String filePath, String content) {
+    try {
+      // Parse the file path to get bucket and object path
+      Pair<String, String> bucketAndPath = StorageLockClient.parseBucketAndPath(filePath);
+      String bucket = bucketAndPath.getLeft();
+      String objectPath = bucketAndPath.getRight();
+
+      BlobId blobId = BlobId.of(bucket, objectPath);
+      BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+
+      // Write the content to GCS
+      gcsClient.create(blobInfo, content.getBytes(UTF_8));
+
+      logger.debug("Successfully wrote object to: {}", filePath);
+      return true;
+    } catch (Exception e) {
+      logger.error("Error writing object to: {}", filePath, e);
+      return false;
+    }
+  }
+
   @Override
   public void close() throws Exception {
     this.gcsClient.close();
diff --git a/hudi-gcp/src/test/java/org/apache/hudi/gcp/transaction/lock/TestGCSStorageLockClientAuditOperations.java b/hudi-gcp/src/test/java/org/apache/hudi/gcp/transaction/lock/TestGCSStorageLockClientAuditOperations.java
new file mode 100644
index 000000000000..e7948206211a
--- /dev/null
+++ b/hudi-gcp/src/test/java/org/apache/hudi/gcp/transaction/lock/TestGCSStorageLockClientAuditOperations.java
@@ -0,0 +1,273 @@
+/*
+ * 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.hudi.gcp.transaction.lock;
+
+import org.apache.hudi.common.util.Option;
+
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for GCSStorageLockClient audit operations (readObject and writeObject methods)
+ */
+public class TestGCSStorageLockClientAuditOperations {
+
+  private Storage mockGcsClient;
+  private Blob mockBlob;
+  private Logger mockLogger;
+  private GCSStorageLockClient lockClient;
+
+  @BeforeEach
+  void setUp() {
+    mockGcsClient = mock(Storage.class);
+    mockLogger = mock(Logger.class);
+    mockBlob = mock(Blob.class);
+    String lockFileUri = "gs://test-bucket/table/.hoodie/.locks/table_lock.json";
+    String ownerId = "test-owner";
+    lockClient = new GCSStorageLockClient(
+        ownerId,
+        lockFileUri,
+        new Properties(),
+        props -> mockGcsClient,
+        mockLogger);
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstFileNotFound() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // get() returns null for non-existent blob
+    when(mockGcsClient.get(any(BlobId.class)))
+        .thenReturn(null);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isEmpty());
+    // Should only call get() for existence check, not readAllBytes
+    verify(mockGcsClient, times(1)).get(any(BlobId.class));
+    verify(mockGcsClient, never()).readAllBytes(any(BlobId.class));
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstBlobExistsFalse() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // get() returns blob but exists() is false
+    when(mockGcsClient.get(any(BlobId.class)))
+        .thenReturn(mockBlob);
+    when(mockBlob.exists()).thenReturn(false);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isEmpty());
+    // Should only check existence, not read content
+    verify(mockGcsClient, times(1)).get(any(BlobId.class));
+    verify(mockBlob, times(1)).exists();
+    verify(mockBlob, never()).getContent();
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstFileExists() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+    String expectedContent = "{\"STORAGE_LP_AUDIT_SERVICE_ENABLED\": true}";
+
+    // get() returns existing blob
+    when(mockGcsClient.get(any(BlobId.class)))
+        .thenReturn(mockBlob);
+    when(mockBlob.exists()).thenReturn(true);
+    when(mockBlob.getContent())
+        .thenReturn(expectedContent.getBytes(StandardCharsets.UTF_8));
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isPresent());
+    assertEquals(expectedContent, result.get());
+    // Should call get() for existence check and getContent() for reading
+    verify(mockGcsClient, times(1)).get(any(BlobId.class));
+    verify(mockBlob, times(1)).exists();
+    verify(mockBlob, times(1)).getContent();
+  }
+
+  @Test
+  void testReadConfigWithoutCheckExistsFirstFileNotFound() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // Direct readAllBytes throws 404 exception
+    StorageException notFoundException = new StorageException(404, "Not Found");
+    when(mockGcsClient.readAllBytes(any(BlobId.class)))
+        .thenThrow(notFoundException);
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isEmpty());
+    // Should not call get(), only readAllBytes
+    verify(mockGcsClient, never()).get(any(BlobId.class));
+    verify(mockGcsClient, times(1)).readAllBytes(any(BlobId.class));
+  }
+
+  @Test
+  void testReadConfigWithoutCheckExistsFirstFileExists() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+    String expectedContent = "{\"STORAGE_LP_AUDIT_SERVICE_ENABLED\": false}";
+
+    // Direct readAllBytes returns content
+    when(mockGcsClient.readAllBytes(any(BlobId.class)))
+        .thenReturn(expectedContent.getBytes(StandardCharsets.UTF_8));
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isPresent());
+    assertEquals(expectedContent, result.get());
+    // Should not call get(), only readAllBytes
+    verify(mockGcsClient, never()).get(any(BlobId.class));
+    verify(mockGcsClient, times(1)).readAllBytes(any(BlobId.class));
+  }
+
+  @Test
+  void testReadConfigWithCheckExistsFirstOtherGcsError() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // get() throws non-404 error
+    StorageException serverError = new StorageException(500, "Internal Server Error");
+    when(mockGcsClient.get(any(BlobId.class)))
+        .thenThrow(serverError);
+
+    Option<String> result = lockClient.readObject(configPath, true);
+
+    assertTrue(result.isEmpty());
+    verify(mockGcsClient, times(1)).get(any(BlobId.class));
+    verify(mockGcsClient, never()).readAllBytes(any(BlobId.class));
+  }
+
+  @Test
+  void testReadConfigWithInvalidUri() {
+    String invalidPath = "not-a-valid-uri";
+
+    Option<String> result = lockClient.readObject(invalidPath, false);
+
+    assertTrue(result.isEmpty());
+    // Should not make any GCS calls due to URI parsing error
+    verify(mockGcsClient, never()).get(any(BlobId.class));
+    verify(mockGcsClient, never()).readAllBytes(any(BlobId.class));
+  }
+
+  @Test
+  void testReadConfigWithRateLimitError() {
+    String configPath = "gs://test-bucket/table/.hoodie/.locks/audit_enabled.json";
+
+    // readAllBytes returns rate limit error
+    StorageException rateLimitException = new StorageException(429, "Too Many Requests");
+    when(mockGcsClient.readAllBytes(any(BlobId.class)))
+        .thenThrow(rateLimitException);
+
+    Option<String> result = lockClient.readObject(configPath, false);
+
+    assertTrue(result.isEmpty());
+    verify(mockGcsClient, times(1)).readAllBytes(any(BlobId.class));
+  }
+
+  // ================================
+  // writeObject() tests
+  // ================================
+
+  @Test
+  void testWriteObject_success() {
+    String filePath = "gs://test-bucket/audit/test-audit.jsonl";
+    String content = "{\"test\": \"data\"}\n";
+    when(mockGcsClient.create(any(BlobInfo.class), any(byte[].class))).thenReturn(mockBlob);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertTrue(result);
+    verify(mockGcsClient).create(any(BlobInfo.class), eq(content.getBytes(UTF_8)));
+    verify(mockLogger).debug("Successfully wrote object to: {}", filePath);
+  }
+
+  @Test
+  void testWriteObject_storageException() {
+    String filePath = "gs://test-bucket/audit/test-audit.jsonl";
+    String content = "{\"test\": \"data\"}\n";
+    StorageException storageException = new StorageException(500, "Internal Server Error");
+    when(mockGcsClient.create(any(BlobInfo.class), any(byte[].class))).thenThrow(storageException);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertFalse(result);
+    verify(mockLogger).error(contains("Error writing object to"), eq(filePath), eq(storageException));
+  }
+
+  @Test
+  void testWriteObject_invalidPath() {
+    String invalidPath = "invalid-path";
+    String content = "{\"test\": \"data\"}\n";
+
+    boolean result = lockClient.writeObject(invalidPath, content);
+
+    assertFalse(result);
+    verify(mockLogger).error(contains("Error writing object to"), eq(invalidPath), any(Exception.class));
+  }
+
+  @Test
+  void testWriteObject_emptyContent() {
+    String filePath = "gs://test-bucket/audit/empty-content.jsonl";
+    String content = "";
+    when(mockGcsClient.create(any(BlobInfo.class), any(byte[].class))).thenReturn(mockBlob);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertTrue(result);
+    verify(mockGcsClient).create(any(BlobInfo.class), eq(content.getBytes(UTF_8)));
+    verify(mockLogger).debug("Successfully wrote object to: {}", filePath);
+  }
+
+  @Test
+  void testWriteObject_rateLimitExceeded() {
+    String filePath = "gs://test-bucket/audit/test-audit.jsonl";
+    String content = "{\"test\": \"data\"}\n";
+    StorageException rateLimitException = new StorageException(429, "Rate Limit Exceeded");
+    when(mockGcsClient.create(any(BlobInfo.class), any(byte[].class))).thenThrow(rateLimitException);
+
+    boolean result = lockClient.writeObject(filePath, content);
+
+    assertFalse(result);
+    verify(mockLogger).error(contains("Error writing object to"), eq(filePath), eq(rateLimitException));
+  }
+}
diff --git a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/table/log/HoodieLogFormatWriter.java b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/table/log/HoodieLogFormatWriter.java
index 8e8c19b3c7b2..67de3aac0e86 100644
--- a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/table/log/HoodieLogFormatWriter.java
+++ b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/table/log/HoodieLogFormatWriter.java
@@ -296,7 +296,7 @@ public class HoodieLogFormatWriter implements HoodieLogFormat.Writer {
     shutdownThread = new Thread() {
       public void run() {
         try {
-          LOG.warn("running logformatwriter hook");
+          LOG.info("running logformatwriter hook");
           if (output != null) {
             closeStream();
           }
diff --git a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/HadoopConfigUtils.java b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/HadoopConfigUtils.java
index 9f1347872e2c..617c5abab477 100644
--- a/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/HadoopConfigUtils.java
+++ b/hudi-hadoop-common/src/main/java/org/apache/hudi/common/util/HadoopConfigUtils.java
@@ -62,9 +62,9 @@ public class HadoopConfigUtils {
     for (String alternative : configProperty.getAlternatives()) {
       String altValue = conf.get(alternative);
       if (altValue != null) {
-        LOG.warn(String.format("The configuration key '%s' has been deprecated "
-                + "and may be removed in the future. Please use the new key '%s' instead.",
-            alternative, configProperty.key()));
+        LOG.warn("The configuration key '{}' has been deprecated "
+            + "and may be removed in the future. Please use the new key '{}' instead.",
+            alternative, configProperty.key());
         return Option.of(altValue);
       }
     }
diff --git a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/SchemaEvolutionContext.java b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/SchemaEvolutionContext.java
index ab83d738faa9..1353a75a6705 100644
--- a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/SchemaEvolutionContext.java
+++ b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/SchemaEvolutionContext.java
@@ -122,7 +122,7 @@ public class SchemaEvolutionContext {
       return HoodieTableMetaClient.builder().setBasePath(tablePath.get().toString())
           .setConf(HadoopFSUtils.getStorageConfWithCopy(job)).build();
     } catch (Exception e) {
-      LOG.warn(String.format("Not a valid hoodie table, table path: %s", ((FileSplit)split).getPath()), e);
+      LOG.error(String.format("Not a valid hoodie table, table path: %s", ((FileSplit)split).getPath()), e);
       return null;
     }
   }
diff --git a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/AbstractRealtimeRecordReader.java b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/AbstractRealtimeRecordReader.java
index f435e5e18e1e..d68df6013df5 100644
--- a/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/AbstractRealtimeRecordReader.java
+++ b/hudi-hadoop-mr/src/main/java/org/apache/hudi/hadoop/realtime/AbstractRealtimeRecordReader.java
@@ -136,7 +136,7 @@ public abstract class AbstractRealtimeRecordReader {
       this.serializer = new HiveAvroSerializer(new ArrayWritableObjectInspector(rowTypeInfo), columnNameList, columnTypeList);
     } catch (Exception e) {
       // fallback to origin logical
-      LOG.warn("fall to init HiveAvroSerializer to support payload merge", e);
+      LOG.warn("Failed to init HiveAvroSerializer to support payload merge. Fallback to origin logical", e);
       this.supportPayload = false;
     }
   }
diff --git a/hudi-io/src/main/java/org/apache/hudi/common/util/FileIOUtils.java b/hudi-io/src/main/java/org/apache/hudi/common/util/FileIOUtils.java
index 6e398e96953d..a784892398f2 100644
--- a/hudi-io/src/main/java/org/apache/hudi/common/util/FileIOUtils.java
+++ b/hudi-io/src/main/java/org/apache/hudi/common/util/FileIOUtils.java
@@ -160,7 +160,7 @@ public class FileIOUtils {
     try {
       closeable.close();
     } catch (IOException e) {
-      LOG.warn("IOException during close", e);
+      LOG.warn("Failed to close the closeable", e);
     }
   }
 
@@ -183,9 +183,10 @@ public class FileIOUtils {
         }
       }
     } catch (IOException e) {
-      LOG.warn("Failed to create file " + fullPath, e);
       if (!ignoreIOE) {
         throw new HoodieIOException("Failed to create file " + fullPath, e);
+      } else {
+        LOG.warn("Failed to create file {}", fullPath, e);
       }
     }
   }
@@ -243,9 +244,10 @@ public class FileIOUtils {
     try (InputStream is = storage.open(detailPath)) {
       return Option.of(FileIOUtils.readAsByteArray(is));
     } catch (IOException e) {
-      LOG.warn("Could not read commit details from " + detailPath, e);
       if (!ignoreIOE) {
         throw new HoodieIOException("Could not read commit details from " + detailPath, e);
+      } else {
+        LOG.warn("Could not read commit details from {}", detailPath, e);
       }
       return Option.empty();
     }
diff --git a/hudi-io/src/main/java/org/apache/hudi/common/util/HoodieTimer.java b/hudi-io/src/main/java/org/apache/hudi/common/util/HoodieTimer.java
index a35cd60933f8..42ad16065f42 100644
--- a/hudi-io/src/main/java/org/apache/hudi/common/util/HoodieTimer.java
+++ b/hudi-io/src/main/java/org/apache/hudi/common/util/HoodieTimer.java
@@ -83,6 +83,17 @@ public class HoodieTimer {
     return timeInfoDeque.pop().stop();
   }
 
+  /**
+   * tries to end the timer but will not cause a runtime failure like endTimer will if the queue is empty
+   * @return option of the timer duration, empty option if the queue is empty
+   */
+  public Option<Long> tryEndTimer() {
+    if (timeInfoDeque.isEmpty()) {
+      return Option.empty();
+    }
+    return Option.of(timeInfoDeque.pop().stop());
+  }
+
   /**
    * Creates an instance of {@link HoodieTimer} already started
    */
diff --git a/hudi-io/src/main/java/org/apache/hudi/common/util/RetryHelper.java b/hudi-io/src/main/java/org/apache/hudi/common/util/RetryHelper.java
index 26ef5b3bed7d..30b4cbdb4df8 100644
--- a/hudi-io/src/main/java/org/apache/hudi/common/util/RetryHelper.java
+++ b/hudi-io/src/main/java/org/apache/hudi/common/util/RetryHelper.java
@@ -93,7 +93,7 @@ public class RetryHelper<T, R extends Exception> implements Serializable {
           LOG.error(message, e);
           throw e;
         }
-        LOG.warn("Catch Exception for " + taskInfo + ", will retry after " + waitTime + " ms.", e);
+        LOG.warn("Task [{}] failed. current retry number {}, will retry after {} ms.", taskInfo, retries, waitTime, e);
         try {
           Thread.sleep(waitTime);
         } catch (InterruptedException ex) {
diff --git a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/kafka/KafkaConnectControlAgent.java b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/kafka/KafkaConnectControlAgent.java
index a40a6f73a2c1..66cccf854c3b 100644
--- a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/kafka/KafkaConnectControlAgent.java
+++ b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/kafka/KafkaConnectControlAgent.java
@@ -154,17 +154,17 @@ public class KafkaConnectControlAgent implements KafkaControlAgent {
                   partitionWorker.processControlEvent(message);
                 }
               } else {
-                LOG.warn(String.format("Failed to send message for unregistered participants for topic %s", senderTopic));
+                LOG.warn("Failed to send message for unregistered participants for topic {}", senderTopic);
               }
             } else if (message.getReceiverType().equals(ControlMessage.EntityType.COORDINATOR)) {
               if (topicCoordinators.containsKey(senderTopic)) {
                 topicCoordinators.get(senderTopic).processControlEvent(message);
               }
             } else {
-              LOG.warn(String.format("Sender type of Control Message unknown %s", message.getSenderType().name()));
+              LOG.warn("Sender type of Control Message unknown {}", message.getSenderType().name());
             }
           } catch (Exception e) {
-            LOG.error(String.format("Fatal error while consuming a kafka record for topic = %s partition = %s", record.topic(), record.partition()), e);
+            LOG.error("Fatal error while consuming a kafka record for topic = {} partition = {}", record.topic(), record.partition(), e);
           }
         }
         try {
diff --git a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionCoordinator.java b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionCoordinator.java
index 2a2cd06487a4..e7762b942f37 100644
--- a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionCoordinator.java
+++ b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionCoordinator.java
@@ -166,8 +166,7 @@ public class ConnectTransactionCoordinator implements TransactionCoordinator, Ru
     if (message.getType().equals(ControlMessage.EventType.WRITE_STATUS)) {
       type = CoordinatorEvent.CoordinatorEventType.WRITE_STATUS;
     } else {
-      LOG.warn(String.format("The Coordinator should not be receiving messages of type %s",
-          message.getType().name()));
+      LOG.warn("Illegal message type [{}] processee by coordinator", message.getType().name());
       return;
     }
 
@@ -266,7 +265,8 @@ public class ConnectTransactionCoordinator implements TransactionCoordinator, Ru
     try {
       kafkaControlClient.publishMessage(buildControlMessage(ControlMessage.EventType.END_COMMIT));
     } catch (Exception exception) {
-      LOG.warn(String.format("Could not send END_COMMIT message for partition %s and commitTime %s", partition, currentCommitTime), exception);
+      LOG.warn("Could not send END_COMMIT message for partition {} and commitTime {}",
+          partition, currentCommitTime, exception);
     }
     currentConsumedKafkaOffsets.clear();
     currentState = State.ENDED_COMMIT;
@@ -323,7 +323,8 @@ public class ConnectTransactionCoordinator implements TransactionCoordinator, Ru
 
         // Submit the next start commit, that will rollback the current commit.
         currentState = State.FAILED_COMMIT;
-        LOG.warn("Current commit " + currentCommitTime + " failed, so starting a new commit after recovery delay");
+        LOG.warn("Current commit {} failed. Starting a new commit after recovery delay of {} {}",
+            currentCommitTime, RESTART_COMMIT_DELAY_MS, TimeUnit.MILLISECONDS.name());
         submitEvent(new CoordinatorEvent(CoordinatorEvent.CoordinatorEventType.START_COMMIT,
                 partition.topic(),
                 StringUtils.EMPTY_STRING),
@@ -338,7 +339,8 @@ public class ConnectTransactionCoordinator implements TransactionCoordinator, Ru
     // If we are still stuck in ENDED_STATE
     if (currentState.equals(State.ENDED_COMMIT)) {
       currentState = State.WRITE_STATUS_TIMEDOUT;
-      LOG.warn("Current commit " + currentCommitTime + " failed after a write status timeout, so starting a new commit after recovery delay");
+      LOG.warn("Current commit {} failed after a write status timeout. Starting a new commit after recovery delay of {} {}",
+          currentCommitTime, RESTART_COMMIT_DELAY_MS, TimeUnit.MILLISECONDS.name());
       // Submit the next start commit
       submitEvent(new CoordinatorEvent(CoordinatorEvent.CoordinatorEventType.START_COMMIT,
               partition.topic(),
@@ -351,7 +353,7 @@ public class ConnectTransactionCoordinator implements TransactionCoordinator, Ru
     try {
       kafkaControlClient.publishMessage(buildControlMessage(ControlMessage.EventType.ACK_COMMIT));
     } catch (Exception exception) {
-      LOG.warn(String.format("Could not send ACK_COMMIT message for partition %s and commitTime %s", partition, currentCommitTime), exception);
+      LOG.warn("Could not send ACK_COMMIT message for partition {} and commitTime {}", partition, currentCommitTime, exception);
     }
     currentState = State.ACKED_COMMIT;
 
diff --git a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionParticipant.java b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionParticipant.java
index 628f14d503ee..dda176c2ba9a 100644
--- a/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionParticipant.java
+++ b/hudi-kafka-connect/src/main/java/org/apache/hudi/connect/transaction/ConnectTransactionParticipant.java
@@ -151,17 +151,17 @@ public class ConnectTransactionParticipant implements TransactionParticipant {
       ongoingTransactionInfo = new TransactionInfo<>(currentCommitTime, writerProvider.getWriter(currentCommitTime));
       ongoingTransactionInfo.setExpectedKafkaOffset(committedKafkaOffset);
     } catch (Exception exception) {
-      LOG.warn("Error received while starting a new transaction", exception);
+      LOG.warn("Failed to start a new transaction", exception);
     }
   }
 
   private void handleEndCommit(ControlMessage message) {
     if (ongoingTransactionInfo == null) {
-      LOG.warn(String.format("END_COMMIT %s is received while we were NOT in active transaction", message.getCommitTime()));
+      LOG.warn("END_COMMIT {} is received while we were NOT in active transaction", message.getCommitTime());
       return;
     } else if (!ongoingTransactionInfo.getCommitTime().equals(message.getCommitTime())) {
-      LOG.error(String.format("Fatal error received END_COMMIT with commit time %s while local transaction commit time %s",
-          message.getCommitTime(), ongoingTransactionInfo.getCommitTime()));
+      LOG.error("Fatal error received END_COMMIT with commit time {} while local transaction commit time {}",
+          message.getCommitTime(), ongoingTransactionInfo.getCommitTime());
       // Recovery: A new END_COMMIT from leader caused interruption to an existing transaction,
       // explicitly reset Kafka commit offset to ensure no data loss
       cleanupOngoingTransaction();
@@ -219,25 +219,17 @@ public class ConnectTransactionParticipant implements TransactionParticipant {
             ongoingTransactionInfo.getWriter().writeRecord(record);
             ongoingTransactionInfo.setExpectedKafkaOffset(record.kafkaOffset() + 1);
           } else if (record != null && record.kafkaOffset() > ongoingTransactionInfo.getExpectedKafkaOffset()) {
-            LOG.warn(String.format("Received a kafka record with offset %s above the next expected kafka offset %s for partition %s, "
-                    + "hence resetting the kafka offset to %s",
-                record.kafkaOffset(),
-                ongoingTransactionInfo.getExpectedKafkaOffset(),
-                partition,
-                ongoingTransactionInfo.getExpectedKafkaOffset()));
+            LOG.warn("Received a kafka record with offset {} above the next expected kafka offset {} for partition {}. "
+                + "Resetting the kafka offset to {}", record.kafkaOffset(), ongoingTransactionInfo.getExpectedKafkaOffset(), partition, ongoingTransactionInfo.getExpectedKafkaOffset());
             context.offset(partition, ongoingTransactionInfo.getExpectedKafkaOffset());
           } else if (record != null && record.kafkaOffset() < ongoingTransactionInfo.getExpectedKafkaOffset()) {
-            LOG.warn(String.format("Received a kafka record with offset %s below the next expected kafka offset %s for partition %s, "
-                    + "no action will be taken but this record will be ignored since its already written",
-                record.kafkaOffset(),
-                ongoingTransactionInfo.getExpectedKafkaOffset(),
-                partition));
+            LOG.info("Received a kafka record with offset {} below the next expected kafka offset {} for partition {}. "
+                + "No action will be taken but this record will be ignored since its already written", record.kafkaOffset(), ongoingTransactionInfo.getExpectedKafkaOffset(), partition);
           }
           buffer.poll();
         } catch (Exception exception) {
-          LOG.warn(String.format("Error received while writing records for transaction %s in partition %s",
-              ongoingTransactionInfo.getCommitTime(), partition.partition()),
-              exception);
+          LOG.warn("Failed to write records for transaction [{}] in partition [{}]",
+              ongoingTransactionInfo.getCommitTime(), partition.partition(), exception);
         }
       }
     }
@@ -249,7 +241,7 @@ public class ConnectTransactionParticipant implements TransactionParticipant {
         ongoingTransactionInfo.getWriter().close();
         ongoingTransactionInfo = null;
       } catch (HoodieIOException exception) {
-        LOG.warn("Error received while trying to cleanup existing transaction", exception);
+        LOG.warn("Failed to cleanup existing transaction", exception);
       }
     }
   }
@@ -262,20 +254,15 @@ public class ConnectTransactionParticipant implements TransactionParticipant {
       if (coordinatorCommittedKafkaOffset != null && coordinatorCommittedKafkaOffset >= 0) {
         // Debug only messages
         if (coordinatorCommittedKafkaOffset != committedKafkaOffset) {
-          LOG.warn(String.format("The coordinator offset for kafka partition %s is %d while the locally committed offset is %d, "
-                  + "hence resetting the local committed offset to the coordinator provided one to ensure consistency",
-              partition,
-              coordinatorCommittedKafkaOffset,
-              committedKafkaOffset));
+          LOG.warn("The coordinator offset for kafka partition {} is {} while the locally committed offset is {}. "
+              + "Resetting the local committed offset to the coordinator provided one to ensure consistency", partition, coordinatorCommittedKafkaOffset, committedKafkaOffset);
         }
         committedKafkaOffset = coordinatorCommittedKafkaOffset;
         return;
       }
     } else {
-      LOG.warn(String.format("The coordinator offset for kafka partition %s is not present while the locally committed offset is %d, "
-              + "hence resetting the local committed offset to 0 to avoid data loss",
-          partition,
-          committedKafkaOffset));
+      LOG.warn("The coordinator offset for kafka partition {} is not present while the locally committed offset is {}. "
+          + "Resetting the local committed offset to 0 to avoid data loss", partition, committedKafkaOffset);
     }
     // If the coordinator does not have a committed offset for this partition, reset to zero offset.
     committedKafkaOffset = 0;
diff --git a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/DataSourceUtils.java b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/DataSourceUtils.java
index 47f12218b1ea..938f3d6b209a 100644
--- a/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/DataSourceUtils.java
+++ b/hudi-spark-datasource/hudi-spark-common/src/main/java/org/apache/hudi/DataSourceUtils.java
@@ -81,7 +81,7 @@ public class DataSourceUtils {
           return tablePath.get().toString();
         }
       } catch (HoodieException he) {
-        LOG.warn("Error trying to get table path from " + path.toString(), he);
+        LOG.warn("Error trying to get table path from {}", path.toString(), he);
       }
     }
 
diff --git a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
index edd08fe5d6c0..26a42cf4fa22 100644
--- a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
+++ b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieFileIndex.scala
@@ -520,7 +520,7 @@ object HoodieFileIndex extends Logging {
           }
         } catch {
           case NonFatal(e) =>
-            logWarning("Fail to convert filters for TimestampBaseAvroKeyGenerator", e)
+            logWarning("Failed to convert filters for TimestampBaseAvroKeyGenerator", e)
             partitionFilters
         }
       }
diff --git a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieSparkSqlWriter.scala b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieSparkSqlWriter.scala
index 02ca533f45b1..6061a4f4b426 100644
--- a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieSparkSqlWriter.scala
+++ b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieSparkSqlWriter.scala
@@ -184,7 +184,7 @@ class HoodieSparkSqlWriterInternal {
       try {
         toReturn = writeInternal(sqlContext, mode, optParams, sourceDf, streamingWritesParamsOpt, hoodieWriteClient)
         if (counter > 0) {
-          log.warn(s"Succeeded with attempt no $counter")
+          log.info(s"Succeeded with attempt no $counter")
         }
         succeeded = true
       } catch {
@@ -192,7 +192,7 @@ class HoodieSparkSqlWriterInternal {
           val writeConcurrencyMode = optParams.getOrElse(HoodieWriteConfig.WRITE_CONCURRENCY_MODE.key(), HoodieWriteConfig.WRITE_CONCURRENCY_MODE.defaultValue())
           if (writeConcurrencyMode.equalsIgnoreCase(WriteConcurrencyMode.OPTIMISTIC_CONCURRENCY_CONTROL.name()) && counter < maxRetry) {
             counter += 1
-            log.warn(s"Conflict found. Retrying again for attempt no $counter")
+            log.info(s"Conflict found. Retrying again for attempt no $counter")
           } else {
             throw e
           }
@@ -261,7 +261,7 @@ class HoodieSparkSqlWriterInternal {
 
     val keyGenerator = HoodieSparkKeyGeneratorFactory.createKeyGenerator(new TypedProperties(hoodieConfig.getProps))
     if (mode == SaveMode.Ignore && tableExists) {
-      log.warn(s"hoodie table at $basePath already exists. Ignoring & not performing actual writes.")
+      log.info(s"hoodie table at $basePath already exists. Ignoring & not performing actual writes.")
       (false, common.util.Option.empty(), common.util.Option.empty(), common.util.Option.empty(), hoodieWriteClient.orNull, tableConfig)
     } else {
       // Handle various save modes
@@ -517,7 +517,6 @@ class HoodieSparkSqlWriterInternal {
     val asyncCompactionEnabled = isAsyncCompactionEnabled(writeClient, tableConfig, parameters, configuration)
     val asyncClusteringEnabled = isAsyncClusteringEnabled(writeClient, parameters)
     if (!asyncCompactionEnabled && !asyncClusteringEnabled) {
-      log.warn("Closing write client")
       writeClient.close()
     }
   }
@@ -813,7 +812,7 @@ class HoodieSparkSqlWriterInternal {
     }
 
     if (mode == SaveMode.Ignore && tableExists) {
-      log.warn(s"hoodie table at $basePath already exists. Ignoring & not performing actual writes.")
+      log.error(s"hoodie table at $basePath already exists. Ignoring & not performing actual writes.")
       if (!hoodieWriteClient.isEmpty) {
         hoodieWriteClient.get.close()
       }
diff --git a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieStreamingSink.scala b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieStreamingSink.scala
index 95d8714e05c0..40b270004ff0 100644
--- a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieStreamingSink.scala
+++ b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/HoodieStreamingSink.scala
@@ -68,7 +68,7 @@ class HoodieStreamingSink(sqlContext: SQLContext,
         .build())
     } catch {
       case _: TableNotFoundException =>
-        log.warn("Ignore TableNotFoundException as it is first microbatch.")
+        log.info("Ignore TableNotFoundException as it is first microbatch.")
         Option.empty
     }
   }
@@ -108,7 +108,7 @@ class HoodieStreamingSink(sqlContext: SQLContext,
     val queryId = sqlContext.sparkContext.getLocalProperty(StreamExecution.QUERY_ID_KEY)
     checkArgument(queryId != null, "queryId is null")
     if (metaClient.isDefined && canSkipBatch(batchId, options.getOrElse(OPERATION.key, UPSERT_OPERATION_OPT_VAL))) {
-      log.warn(s"Skipping already completed batch $batchId in query $queryId")
+      log.info(s"Skipping already completed batch $batchId in query $queryId")
       // scalastyle:off return
       return
       // scalastyle:on return
@@ -178,12 +178,12 @@ class HoodieStreamingSink(sqlContext: SQLContext,
                   case t: Exception => log.warn("Got excepting trying to unpersist rdd", t)
                 }
             }
-          log.error(s"Micro batch id=$batchId threw following exception: ", e)
           if (ignoreFailedBatch) {
-            log.warn(s"Ignore the exception and move on streaming as per " +
+            log.info(s"Ignore the exception and move on streaming as per " +
               s"${STREAMING_IGNORE_FAILED_BATCH.key} configuration")
             Success((true, None, None))
           } else {
+            log.error(s"Micro batch id=$batchId threw following exception: ", e)
             if (retryCnt > 1) log.info(s"Retrying the failed micro batch id=$batchId ...")
             Failure(e)
           }
diff --git a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCRDD.scala b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCRDD.scala
index 9794c10061a1..efc44bf47216 100644
--- a/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCRDD.scala
+++ b/hudi-spark-datasource/hudi-spark-common/src/main/scala/org/apache/hudi/cdc/HoodieCDCRDD.scala
@@ -356,7 +356,7 @@ class HoodieCDCRDD(
         val existingRecordOpt = beforeImageRecords.remove(key)
         if (existingRecordOpt.isEmpty) {
           // no real record is deleted, just ignore.
-          logWarning("can not get any record that have the same key with the deleting logRecord.")
+          logDebug(s"Cannot find any record corresponding to delete key [$key] in the log record.")
         } else {
           // there is a real record deleted.
           recordToLoad.update(0, CDCRelation.CDC_OPERATION_DELETE)
diff --git a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/BootstrapExecutorUtils.java b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/BootstrapExecutorUtils.java
index 5d8a0d7a30c4..dec350f43bc0 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/BootstrapExecutorUtils.java
+++ b/hudi-spark-datasource/hudi-spark/src/main/java/org/apache/hudi/cli/BootstrapExecutorUtils.java
@@ -187,7 +187,7 @@ public class BootstrapExecutorUtils implements Serializable {
     } catch (Exception e) {
       Path basePath = new Path(cfg.basePath);
       if (fs.exists(basePath)) {
-        LOG.warn("deleted target base path " + cfg.basePath);
+        LOG.info("deleted target base path {}", cfg.basePath);
         fs.delete(basePath, true);
       }
       throw new HoodieException("Failed to bootstrap table", e);
@@ -221,7 +221,7 @@ public class BootstrapExecutorUtils implements Serializable {
     Path basePath = new Path(cfg.basePath);
     if (fs.exists(basePath)) {
       if (cfg.bootstrapOverwrite) {
-        LOG.warn("Target base path already exists, overwrite it");
+        LOG.info("Target base path already exists, overwrite it");
         fs.delete(basePath, true);
       } else {
         throw new HoodieException("target base path already exists at " + cfg.basePath
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CleanupAuditLockProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CleanupAuditLockProcedure.scala
new file mode 100644
index 000000000000..ccf1fcf4cd9a
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CleanupAuditLockProcedure.scala
@@ -0,0 +1,191 @@
+/*
+ * 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.spark.sql.hudi.command.procedures
+
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService
+import org.apache.hudi.storage.StoragePath
+import org.apache.hudi.util.JFunction
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.types.{DataTypes, Metadata, StructField, StructType}
+
+import java.util.function.Supplier
+
+import scala.collection.JavaConverters._
+
+/**
+ * Spark SQL procedure for cleaning up old audit lock files for Hudi tables.
+ *
+ * This procedure removes audit lock files that are older than a specified number
+ * of days. It supports dry-run mode to preview which files would be deleted
+ * without actually removing them.
+ *
+ * Usage:
+ * {{{
+ * CALL cleanup_audit_lock(table => 'my_table')
+ * CALL cleanup_audit_lock(path => '/path/to/table', dry_run => true, age_days => 30)
+ * }}}
+ *
+ * The procedure operates on audit files in:
+ * `{table_path}/.hoodie/.locks/audit/`
+ *
+ * @author Apache Hudi
+ * @since 1.0.0
+ */
+class CleanupAuditLockProcedure extends BaseProcedure with ProcedureBuilder {
+  private val PARAMETERS = Array[ProcedureParameter](
+    ProcedureParameter.optional(0, "table", DataTypes.StringType),
+    ProcedureParameter.optional(1, "path", DataTypes.StringType),
+    ProcedureParameter.optional(2, "dry_run", DataTypes.BooleanType, false),
+    ProcedureParameter.optional(3, "age_days", DataTypes.IntegerType, 7)
+  )
+
+  private val OUTPUT_TYPE = new StructType(Array[StructField](
+    StructField("table", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("files_cleaned", DataTypes.IntegerType, nullable = false, Metadata.empty),
+    StructField("dry_run", DataTypes.BooleanType, nullable = false, Metadata.empty),
+    StructField("age_days", DataTypes.IntegerType, nullable = false, Metadata.empty),
+    StructField("message", DataTypes.StringType, nullable = false, Metadata.empty)
+  ))
+
+  /**
+   * Returns the procedure parameters definition.
+   *
+   * @return Array of parameters: table (optional String), path (optional String),
+   *         dry_run (optional Boolean, default false), and age_days (optional Integer, default 7)
+   */
+  def parameters: Array[ProcedureParameter] = PARAMETERS
+
+  /**
+   * Returns the output schema for the procedure result.
+   *
+   * @return StructType containing table, files_cleaned, dry_run, age_days, and message columns
+   */
+  def outputType: StructType = OUTPUT_TYPE
+
+  /**
+   * Executes the audit lock cleanup procedure.
+   *
+   * @param args Procedure arguments containing table name or path, dry_run flag, and age threshold
+   * @return Sequence containing a single Row with cleanup results
+   * @throws IllegalArgumentException if neither table nor path is provided, or both are provided
+   */
+  override def call(args: ProcedureArgs): Seq[Row] = {
+    super.checkArgs(PARAMETERS, args)
+
+    val tableName = getArgValueOrDefault(args, PARAMETERS(0))
+    val tablePath = getArgValueOrDefault(args, PARAMETERS(1))
+    val dryRun = getArgValueOrDefault(args, PARAMETERS(2)).getOrElse(false).asInstanceOf[Boolean]
+    val ageDays = getArgValueOrDefault(args, PARAMETERS(3)).getOrElse(7).asInstanceOf[Int]
+
+    // Validate age_days is positive
+    if (ageDays <= 0) {
+      throw new IllegalArgumentException("age_days must be a positive integer")
+    }
+
+    // Get the base path using BaseProcedure helper (handles table/path validation)
+    val basePath: String = getBasePath(tableName, tablePath)
+    val metaClient = createMetaClient(jsc, basePath)
+
+    // Use table name if provided, otherwise extract from path
+    val displayName = tableName.map(_.asInstanceOf[String]).getOrElse(tablePath.get.asInstanceOf[String])
+
+    try {
+      val auditFolderPath = new StoragePath(StorageLockProviderAuditService.getAuditFolderPath(basePath))
+      val storage = metaClient.getStorage
+
+      // Check if audit folder exists
+      if (!storage.exists(auditFolderPath)) {
+        val message = "No audit folder found - nothing to cleanup"
+        Seq(Row(displayName, 0, dryRun, ageDays, message))
+      } else {
+
+        // Calculate cutoff timestamp (ageDays ago)
+        val cutoffTime = System.currentTimeMillis() - (ageDays * 24 * 60 * 60 * 1000L)
+
+        // List all files in audit folder and filter by modification time
+        val allFiles = storage.listDirectEntries(auditFolderPath).asScala
+        val auditFiles = allFiles.filter(pathInfo => pathInfo.isFile && pathInfo.getPath.getName.endsWith(".jsonl"))
+        val oldFiles = auditFiles.filter(_.getModificationTime < cutoffTime)
+
+        if (oldFiles.isEmpty) {
+          val message = s"No audit files older than $ageDays days found"
+          Seq(Row(displayName, 0, dryRun, ageDays, message))
+        } else {
+
+          val fileCount = oldFiles.size
+
+          if (dryRun) {
+            val message = s"Dry run: Would delete $fileCount audit files older than $ageDays days"
+            Seq(Row(displayName, fileCount, dryRun, ageDays, message))
+          } else {
+            // Actually delete the files
+            var deletedCount = 0
+            var failedCount = 0
+
+            oldFiles.foreach { pathInfo =>
+              try {
+                storage.deleteFile(pathInfo.getPath)
+                deletedCount += 1
+              } catch {
+                case e: Exception =>
+                  failedCount += 1
+                  // Log the error but continue with other files
+              }
+            }
+
+            val message = if (failedCount == 0) {
+              s"Successfully deleted $deletedCount audit files older than $ageDays days"
+            } else {
+              s"Deleted $deletedCount audit files, failed to delete $failedCount files"
+            }
+
+            Seq(Row(displayName, deletedCount, dryRun, ageDays, message))
+          }
+        }
+      }
+    } catch {
+      case e: Exception =>
+        val errorMessage = s"Cleanup failed: ${e.getMessage}"
+        Seq(Row(displayName, 0, dryRun, ageDays, errorMessage))
+    }
+  }
+
+  /**
+   * Builds a new instance of the CleanupAuditLockProcedure.
+   *
+   * @return New CleanupAuditLockProcedure instance
+   */
+  override def build: Procedure = new CleanupAuditLockProcedure()
+}
+
+
+/**
+ * Companion object for CleanupAuditLockProcedure containing constants and factory methods.
+ */
+object CleanupAuditLockProcedure {
+  /** The name used to register and invoke this procedure */
+  val NAME = "cleanup_audit_lock"
+
+  /**
+   * Factory method to create procedure builder instances.
+   *
+   * @return Supplier that creates new CleanupAuditLockProcedure instances
+   */
+  def builder: Supplier[ProcedureBuilder] = JFunction.toJavaSupplier(() => new CleanupAuditLockProcedure())
+}
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CreateSavepointProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CreateSavepointProcedure.scala
index 0ae22f54af7f..5ae04311b036 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CreateSavepointProcedure.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/CreateSavepointProcedure.scala
@@ -74,7 +74,7 @@ class CreateSavepointProcedure extends BaseProcedure with ProcedureBuilder with
       result = true
     } catch {
       case _: HoodieSavepointException =>
-        logWarning(s"Failed: Could not create savepoint $commitTime.")
+        logWarning(s"Could not create savepoint $commitTime.")
     } finally {
       client.close()
     }
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteMarkerProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteMarkerProcedure.scala
index 8d73a753cf4c..f308c75e0444 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteMarkerProcedure.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteMarkerProcedure.scala
@@ -68,7 +68,7 @@ class DeleteMarkerProcedure extends BaseProcedure with ProcedureBuilder with Log
         logInfo(s"Marker $instantTime deleted.")
         true
       case Failure(e) =>
-        logWarning(s"Failed: Could not clean marker instantTime: $currentInstant.", e)
+        logWarning(s"Could not clean marker instantTime: $currentInstant.", e)
         false
     }
 
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteSavepointProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteSavepointProcedure.scala
index d9a6dc4197d2..c9227a351042 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteSavepointProcedure.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/DeleteSavepointProcedure.scala
@@ -75,7 +75,7 @@ class DeleteSavepointProcedure extends BaseProcedure with ProcedureBuilder with
         logInfo(s"The commit $instantTime has been deleted savepoint.")
       } catch {
         case _: HoodieSavepointException =>
-          logWarning(s"Failed: Could not delete savepoint $currentInstant.")
+          logWarning(s"Could not delete savepoint $currentInstant.")
           result = false
       } finally {
         client.close()
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedures.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedures.scala
index ad63ddbb29ee..1e746d7249f3 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedures.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/HoodieProcedures.scala
@@ -89,6 +89,10 @@ object HoodieProcedures {
       ,(ShowTablePropertiesProcedure.NAME, ShowTablePropertiesProcedure.builder)
       ,(HelpProcedure.NAME, HelpProcedure.builder)
       ,(ArchiveCommitsProcedure.NAME, ArchiveCommitsProcedure.builder)
+      ,(SetAuditLockProcedure.NAME, SetAuditLockProcedure.builder)
+      ,(ShowAuditLockStatusProcedure.NAME, ShowAuditLockStatusProcedure.builder)
+      ,(ValidateAuditLockProcedure.NAME, ValidateAuditLockProcedure.builder)
+      ,(CleanupAuditLockProcedure.NAME, CleanupAuditLockProcedure.builder)
     )
   }
 }
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RollbackToSavepointProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RollbackToSavepointProcedure.scala
index 80688838bd2b..e366618cfc93 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RollbackToSavepointProcedure.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/RollbackToSavepointProcedure.scala
@@ -73,11 +73,11 @@ class RollbackToSavepointProcedure extends BaseProcedure with ProcedureBuilder w
       if (tableName.isDefined) {
         spark.catalog.refreshTable(tableName.get.asInstanceOf[String])
       }
-      logInfo("The commit $instantTime rolled back.")
+      logInfo(s"The commit $instantTime rolled back.")
       result = true
     } catch {
       case _: HoodieSavepointException =>
-        logWarning(s"The commit $instantTime failed to roll back.")
+        logWarning(s"Failed to roll back commit at instant time: $instantTime")
     } finally {
       client.close()
     }
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/SetAuditLockProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/SetAuditLockProcedure.scala
new file mode 100644
index 000000000000..49c6133f0b37
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/SetAuditLockProcedure.scala
@@ -0,0 +1,188 @@
+/*
+ * 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.spark.sql.hudi.command.procedures
+
+import org.apache.hudi.client.transaction.lock.StorageLockClient
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService
+import org.apache.hudi.common.table.HoodieTableMetaClient
+import org.apache.hudi.storage.StoragePath
+import org.apache.hudi.util.JFunction
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.node.ObjectNode
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.types.{DataTypes, Metadata, StructField, StructType}
+
+import java.util.function.Supplier
+
+import scala.util.{Failure, Success, Try}
+
+
+/**
+ * Spark SQL procedure for enabling or disabling lock audit logging for Hudi tables.
+ *
+ * This procedure allows users to control audit logging for storage lock operations through
+ * Spark SQL commands. When enabled, lock operations will generate audit logs in JSONL format
+ * that track lock lifecycle events.
+ *
+ * Usage:
+ * {{{
+ * CALL set_audit_lock(table => 'my_table', state => 'enabled')
+ * CALL set_audit_lock(path => '/path/to/table', state => 'disabled')
+ * }}}
+ *
+ * The procedure creates or updates an audit configuration file at:
+ * `{table_path}/.hoodie/.locks/audit_enabled.json`
+ *
+ * @author Apache Hudi
+ * @since 1.0.0
+ */
+class SetAuditLockProcedure extends BaseProcedure with ProcedureBuilder {
+  private val PARAMETERS = Array[ProcedureParameter](
+    ProcedureParameter.optional(0, "table", DataTypes.StringType),
+    ProcedureParameter.optional(1, "path", DataTypes.StringType),
+    ProcedureParameter.required(2, "state", DataTypes.StringType)
+  )
+
+  private val OUTPUT_TYPE = new StructType(Array[StructField](
+    StructField("table", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("audit_state", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("message", DataTypes.StringType, nullable = false, Metadata.empty)
+  ))
+
+  private val OBJECT_MAPPER = new ObjectMapper()
+
+  /**
+   * Returns the procedure parameters definition.
+   *
+   * @return Array of parameters: table (optional String), path (optional String), and state (required String)
+   */
+  def parameters: Array[ProcedureParameter] = PARAMETERS
+
+  /**
+   * Returns the output schema for the procedure result.
+   *
+   * @return StructType containing table, audit_state, and message columns
+   */
+  def outputType: StructType = OUTPUT_TYPE
+
+  /**
+   * Executes the audit lock set procedure.
+   *
+   * @param args Procedure arguments containing table name or path and desired state
+   * @return Sequence containing a single Row with execution results
+   * @throws IllegalArgumentException if state parameter is not 'enabled' or 'disabled'
+   */
+  override def call(args: ProcedureArgs): Seq[Row] = {
+    super.checkArgs(PARAMETERS, args)
+
+    val tableName = getArgValueOrDefault(args, PARAMETERS(0))
+    val tablePath = getArgValueOrDefault(args, PARAMETERS(1))
+    val state = getArgValueOrDefault(args, PARAMETERS(2)).get.asInstanceOf[String].toLowerCase
+
+    // Validate state parameter
+    if (state != "enabled" && state != "disabled") {
+      throw new IllegalArgumentException("State parameter must be 'enabled' or 'disabled'")
+    }
+
+    // Get the base path using BaseProcedure helper (handles table/path validation)
+    val basePath: String = getBasePath(tableName, tablePath)
+    val metaClient = createMetaClient(jsc, basePath)
+
+    // Use table name if provided, otherwise extract from path
+    val displayName = tableName.map(_.asInstanceOf[String]).getOrElse(tablePath.get.asInstanceOf[String])
+
+    try {
+      val auditEnabled = state == "enabled"
+      setAuditState(metaClient, basePath, auditEnabled)
+
+      val resultState = if (auditEnabled) "enabled" else "disabled"
+      val message = s"Lock audit logging successfully $resultState"
+
+      Seq(Row(displayName, resultState, message))
+    } catch {
+      case e: Exception =>
+        val errorMessage = s"Failed to set audit state: ${e.getMessage}"
+        Seq(Row(displayName, "error", errorMessage))
+    }
+  }
+
+  /**
+   * Sets the audit state by creating or updating the audit configuration file.
+   *
+   * @param metaClient Hudi table meta client for storage operations
+   * @param basePath Base path of the Hudi table
+   * @param enabled Whether audit logging should be enabled
+   * @throws RuntimeException if unable to write the audit configuration
+   */
+  private def setAuditState(metaClient: HoodieTableMetaClient, basePath: String, enabled: Boolean): Unit = {
+    val storage = metaClient.getStorage
+    val lockFolderPath = StorageLockClient.getLockFolderPath(basePath)
+    val auditConfigPath = new StoragePath(StorageLockProviderAuditService.getAuditConfigPath(basePath))
+
+    // Ensure the locks folder exists
+    if (!storage.exists(new StoragePath(lockFolderPath))) {
+      storage.createDirectory(new StoragePath(lockFolderPath))
+    }
+
+    // Create or update the audit configuration file
+    val jsonContent = createAuditConfig(enabled)
+
+    Try {
+      val outputStream = storage.create(auditConfigPath, true) // overwrite if exists
+      try {
+        outputStream.write(jsonContent.getBytes("UTF-8"))
+      } finally {
+        outputStream.close()
+      }
+    } match {
+      case Success(_) =>
+        // Configuration written successfully
+      case Failure(exception) =>
+        throw new RuntimeException(s"Failed to write audit configuration to ${auditConfigPath.toString}", exception)
+    }
+  }
+
+  /**
+   * Creates the JSON configuration content for audit settings.
+   *
+   * @param enabled Whether audit logging should be enabled
+   * @return JSON string representation of the audit configuration
+   */
+  private def createAuditConfig(enabled: Boolean): String = {
+    val rootNode: ObjectNode = OBJECT_MAPPER.createObjectNode()
+    rootNode.put(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD, enabled)
+    OBJECT_MAPPER.writeValueAsString(rootNode)
+  }
+
+  override def build: Procedure = new SetAuditLockProcedure()
+}
+
+/**
+ * Companion object for SetAuditLockProcedure containing constants and factory methods.
+ */
+object SetAuditLockProcedure {
+  val NAME = "set_audit_lock"
+
+  /**
+   * Factory method to create procedure builder instances.
+   *
+   * @return Supplier that creates new SetAuditLockProcedure instances
+   */
+  def builder: Supplier[ProcedureBuilder] = JFunction.toJavaSupplier(() => new SetAuditLockProcedure())
+}
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ShowAuditLockStatusProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ShowAuditLockStatusProcedure.scala
new file mode 100644
index 000000000000..c196a44915f0
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ShowAuditLockStatusProcedure.scala
@@ -0,0 +1,169 @@
+/*
+ * 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.spark.sql.hudi.command.procedures
+
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService
+import org.apache.hudi.common.util.FileIOUtils
+import org.apache.hudi.storage.StoragePath
+import org.apache.hudi.util.JFunction
+
+import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.types.{DataTypes, Metadata, StructField, StructType}
+
+import java.util.function.Supplier
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Spark SQL procedure for showing the current audit logging status for Hudi tables.
+ *
+ * This procedure allows users to check whether audit logging is currently enabled
+ * or disabled for storage lock operations. It reads the audit configuration file
+ * and reports the current status along with relevant paths.
+ *
+ * Usage:
+ * {{{
+ * CALL show_audit_lock_status(table => 'my_table')
+ * CALL show_audit_lock_status(path => '/path/to/table')
+ * }}}
+ *
+ * The procedure reads the audit configuration file from:
+ * `{table_path}/.hoodie/.locks/audit_enabled.json`
+ *
+ * @author Apache Hudi
+ * @since 1.0.0
+ */
+class ShowAuditLockStatusProcedure extends BaseProcedure with ProcedureBuilder {
+  private val PARAMETERS = Array[ProcedureParameter](
+    ProcedureParameter.optional(0, "table", DataTypes.StringType),
+    ProcedureParameter.optional(1, "path", DataTypes.StringType)
+  )
+
+  private val OUTPUT_TYPE = new StructType(Array[StructField](
+    StructField("table", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("audit_enabled", DataTypes.BooleanType, nullable = false, Metadata.empty),
+    StructField("config_path", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("audit_folder_path", DataTypes.StringType, nullable = false, Metadata.empty)
+  ))
+
+  private val OBJECT_MAPPER = new ObjectMapper()
+
+  /**
+   * Returns the procedure parameters definition.
+   *
+   * @return Array of parameters: table (optional String) and path (optional String)
+   */
+  def parameters: Array[ProcedureParameter] = PARAMETERS
+
+  /**
+   * Returns the output schema for the procedure result.
+   *
+   * @return StructType containing table, audit_enabled, config_path, and audit_folder_path columns
+   */
+  def outputType: StructType = OUTPUT_TYPE
+
+  /**
+   * Executes the show audit lock status procedure.
+   *
+   * @param args Procedure arguments containing table name or path
+   * @return Sequence containing a single Row with status information
+   * @throws IllegalArgumentException if neither table nor path is provided, or both are provided
+   */
+  override def call(args: ProcedureArgs): Seq[Row] = {
+    super.checkArgs(PARAMETERS, args)
+
+    val tableName = getArgValueOrDefault(args, PARAMETERS(0))
+    val tablePath = getArgValueOrDefault(args, PARAMETERS(1))
+
+    // Get the base path using BaseProcedure helper (handles table/path validation)
+    val basePath: String = getBasePath(tableName, tablePath)
+    val metaClient = createMetaClient(jsc, basePath)
+
+    // Use table name if provided, otherwise extract from path
+    val displayName = tableName.map(_.asInstanceOf[String]).getOrElse(tablePath.get.asInstanceOf[String])
+
+    try {
+      val auditStatus = checkAuditStatus(metaClient, basePath)
+      val configPath = StorageLockProviderAuditService.getAuditConfigPath(basePath)
+      val auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(basePath)
+
+      Seq(Row(displayName, auditStatus, configPath, auditFolderPath))
+    } catch {
+      case e: Exception =>
+        // Return false for audit status if we can't read the config
+        val configPath = StorageLockProviderAuditService.getAuditConfigPath(basePath)
+        val auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(basePath)
+        Seq(Row(displayName, false, configPath, auditFolderPath))
+    }
+  }
+
+  /**
+   * Checks the current audit status by reading the audit configuration file.
+   *
+   * @param metaClient Hudi table meta client for storage operations
+   * @param basePath Base path of the Hudi table
+   * @return true if audit logging is enabled, false otherwise
+   */
+  private def checkAuditStatus(metaClient: org.apache.hudi.common.table.HoodieTableMetaClient, basePath: String): Boolean = {
+    val storage = metaClient.getStorage
+    val auditConfigPath = new StoragePath(StorageLockProviderAuditService.getAuditConfigPath(basePath))
+
+    if (!storage.exists(auditConfigPath)) {
+      false
+    } else {
+
+      Try {
+        val inputStream = storage.open(auditConfigPath)
+        try {
+          val configContent = new String(FileIOUtils.readAsByteArray(inputStream))
+          val rootNode: JsonNode = OBJECT_MAPPER.readTree(configContent)
+          val enabledNode = rootNode.get(StorageLockProviderAuditService.STORAGE_LOCK_AUDIT_SERVICE_ENABLED_FIELD)
+          Option(enabledNode).exists(_.asBoolean(false))
+        } finally {
+          inputStream.close()
+        }
+      } match {
+        case Success(enabled) => enabled
+        case Failure(_) => false
+      }
+    }
+  }
+
+  /**
+   * Builds a new instance of the ShowAuditLockStatusProcedure.
+   *
+   * @return New ShowAuditLockStatusProcedure instance
+   */
+  override def build: Procedure = new ShowAuditLockStatusProcedure()
+}
+
+/**
+ * Companion object for ShowAuditLockStatusProcedure containing constants and factory methods.
+ */
+object ShowAuditLockStatusProcedure {
+  /** The name used to register and invoke this procedure */
+  val NAME = "show_audit_lock_status"
+
+  /**
+   * Factory method to create procedure builder instances.
+   *
+   * @return Supplier that creates new ShowAuditLockStatusProcedure instances
+   */
+  def builder: Supplier[ProcedureBuilder] = JFunction.toJavaSupplier(() => new ShowAuditLockStatusProcedure())
+}
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/UpgradeOrDowngradeProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/UpgradeOrDowngradeProcedure.scala
index 63eadb740e1a..25e37499ec1b 100644
--- a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/UpgradeOrDowngradeProcedure.scala
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/UpgradeOrDowngradeProcedure.scala
@@ -74,7 +74,7 @@ class UpgradeOrDowngradeProcedure extends BaseProcedure with ProcedureBuilder wi
         logInfo(s"Table at $basePath upgraded / downgraded to version $toVersion.")
         true
       case Failure(e) =>
-        logWarning(s"Failed: Could not upgrade/downgrade table at $basePath to version $toVersion.", e)
+        logError(s"Failed: Could not upgrade/downgrade table at $basePath to version $toVersion.", e)
         false
     }
 
diff --git a/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ValidateAuditLockProcedure.scala b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ValidateAuditLockProcedure.scala
new file mode 100644
index 000000000000..e650e0824046
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/main/scala/org/apache/spark/sql/hudi/command/procedures/ValidateAuditLockProcedure.scala
@@ -0,0 +1,315 @@
+/*
+ * 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.spark.sql.hudi.command.procedures
+
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService
+import org.apache.hudi.storage.{StoragePath, StoragePathInfo}
+import org.apache.hudi.util.JFunction
+
+import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.types.{DataTypes, Metadata, StructField, StructType}
+
+import java.io.FileNotFoundException
+import java.util.function.Supplier
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Spark SQL procedure for validating audit lock files for Hudi tables.
+ *
+ * This procedure validates the integrity and consistency of audit lock files
+ * generated by the storage lock audit service. It checks for various issues
+ * including file format, lock state transitions, and orphaned locks.
+ *
+ * Usage:
+ * {{{
+ * CALL validate_audit_lock(table => 'my_table')
+ * CALL validate_audit_lock(path => '/path/to/table')
+ * }}}
+ *
+ * The procedure reads audit files from:
+ * `{table_path}/.hoodie/.locks/audit/`
+ *
+ * @author Apache Hudi
+ * @since 1.0.0
+ */
+class ValidateAuditLockProcedure extends BaseProcedure with ProcedureBuilder {
+  private val PARAMETERS = Array[ProcedureParameter](
+    ProcedureParameter.optional(0, "table", DataTypes.StringType),
+    ProcedureParameter.optional(1, "path", DataTypes.StringType)
+  )
+
+  private val OUTPUT_TYPE = new StructType(Array[StructField](
+    StructField("table", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("validation_result", DataTypes.StringType, nullable = false, Metadata.empty),
+    StructField("transactions_validated", DataTypes.IntegerType, nullable = false, Metadata.empty),
+    StructField("issues_found", DataTypes.IntegerType, nullable = false, Metadata.empty),
+    StructField("details", DataTypes.StringType, nullable = false, Metadata.empty)
+  ))
+
+  private val OBJECT_MAPPER = new ObjectMapper()
+
+  /**
+   * Represents a transaction window with start time, end time, and metadata.
+   */
+  case class TransactionWindow(
+    ownerId: String,
+    transactionCreationTime: Long,
+    lockAcquisitionTime: Long,
+    lockReleaseTime: Option[Long],
+    lockExpirationTime: Option[Long],
+    filename: String
+  ) {
+    def effectiveEndTime: Long = lockReleaseTime.orElse(lockExpirationTime).getOrElse(lockAcquisitionTime)
+  }
+
+  /**
+   * Returns the procedure parameters definition.
+   *
+   * @return Array of parameters: table (optional String) and path (optional String)
+   */
+  def parameters: Array[ProcedureParameter] = PARAMETERS
+
+  /**
+   * Returns the output schema for the procedure result.
+   *
+   * @return StructType containing table, validation_result, issues_found, and details columns
+   */
+  def outputType: StructType = OUTPUT_TYPE
+
+  /**
+   * Executes the audit lock validation procedure.
+   *
+   * @param args Procedure arguments containing table name or path
+   * @return Sequence containing a single Row with validation results
+   * @throws IllegalArgumentException if neither table nor path is provided, or both are provided
+   */
+  override def call(args: ProcedureArgs): Seq[Row] = {
+    super.checkArgs(PARAMETERS, args)
+
+    val tableName = getArgValueOrDefault(args, PARAMETERS(0))
+    val tablePath = getArgValueOrDefault(args, PARAMETERS(1))
+
+    // Get the base path using BaseProcedure helper (handles table/path validation)
+    val basePath: String = getBasePath(tableName, tablePath)
+    val metaClient = createMetaClient(jsc, basePath)
+
+    // Use table name if provided, otherwise extract from path
+    val displayName = tableName.map(_.asInstanceOf[String]).getOrElse(tablePath.get.asInstanceOf[String])
+
+    try {
+      val auditFolderPath = new StoragePath(StorageLockProviderAuditService.getAuditFolderPath(basePath))
+      val storage = metaClient.getStorage
+
+      // Get all audit files
+      val allFilesResult = try {
+        Some(storage.listDirectEntries(auditFolderPath).asScala)
+      } catch {
+        case _: FileNotFoundException =>
+          None
+      }
+
+      allFilesResult match {
+        case None =>
+          Seq(Row(displayName, "PASSED", 0, 0, "No audit folder found - nothing to validate"))
+        case Some(allFiles) =>
+          val auditFiles = allFiles.filter(pathInfo => pathInfo.isFile && pathInfo.getPath.getName.endsWith(".jsonl"))
+
+          if (auditFiles.isEmpty) {
+            Seq(Row(displayName, "PASSED", 0, 0, "No audit files found - nothing to validate"))
+          } else {
+
+            // Parse all audit files into transaction windows
+            val parseResults = auditFiles.map(pathInfo => (pathInfo, parseAuditFile(pathInfo, storage)))
+            val windows = parseResults.flatMap(_._2).toSeq
+            val parseErrors = parseResults.filter(_._2.isEmpty).map(p =>
+              s"[ERROR] Failed to parse audit file: ${p._1.getPath.getName}")
+
+            if (windows.isEmpty) {
+              Seq(Row(displayName, "FAILED", 0, auditFiles.size, "Failed to parse any audit files"))
+            } else {
+
+              // Validate transactions
+              val validationResults = validateTransactionWindows(windows)
+
+              // Add parse errors to validation results
+              val allErrors = validationResults.errors ++ parseErrors
+              val resultsWithParseErrors = validationResults.copy(errors = allErrors.toList)
+
+              // Generate result
+              val (result, issuesFound, details) = formatValidationResults(resultsWithParseErrors, auditFiles.size, windows.size, parseErrors.size)
+
+              Seq(Row(displayName, result, windows.size, issuesFound, details))
+            }
+          }
+      }
+    } catch {
+      case e: Exception =>
+        val errorMessage = s"Validation failed: ${e.getMessage}"
+        Seq(Row(displayName, "ERROR", 0, -1, errorMessage))
+    }
+  }
+
+  /**
+   * Parses an audit file and extracts transaction window information.
+   */
+  private def parseAuditFile(pathInfo: StoragePathInfo, storage: org.apache.hudi.storage.HoodieStorage): Option[TransactionWindow] = {
+    val filename = pathInfo.getPath.getName
+
+    Try {
+      // Read and parse JSONL content
+      val inputStream = storage.open(pathInfo.getPath)
+      val jsonNodes = try {
+        val content = scala.io.Source.fromInputStream(inputStream).mkString
+        val lines = content.split('\n').filter(_.trim.nonEmpty)
+        lines.map(line => OBJECT_MAPPER.readTree(line)).toSeq
+      } finally {
+        inputStream.close()
+      }
+
+      if (jsonNodes.isEmpty) {
+        None
+      } else {
+
+        // Extract transaction metadata from first entry
+        val firstNode = jsonNodes.head
+        val ownerId = firstNode.get("ownerId").asText()
+        val transactionCreationTime = firstNode.get("transactionStartTime").asLong()
+
+        // Find first START timestamp
+        val startNodes = jsonNodes.filter(node => node.get("state").asText() == "START")
+        val lockAcquisitionTime = if (startNodes.nonEmpty) {
+          startNodes.head.get("timestamp").asLong()
+        } else {
+          transactionCreationTime // fallback to transaction creation time
+        }
+
+        // Find last END timestamp
+        val endNodes = jsonNodes.filter(node => node.get("state").asText() == "END")
+        val lockReleaseTime = if (endNodes.nonEmpty) {
+          Some(endNodes.last.get("timestamp").asLong())
+        } else {
+          None
+        }
+
+        // Find last expiration time as fallback
+        val lockExpirationTime = if (jsonNodes.nonEmpty) {
+          Some(jsonNodes.last.get("lockExpiration").asLong())
+        } else {
+          None
+        }
+
+        Some(TransactionWindow(
+          ownerId = ownerId,
+          transactionCreationTime = transactionCreationTime,
+          lockAcquisitionTime = lockAcquisitionTime,
+          lockReleaseTime = lockReleaseTime,
+          lockExpirationTime = lockExpirationTime,
+          filename = filename
+        ))
+      }
+    } match {
+      case Success(window) => window
+      case Failure(_) => None // Skip corrupted files
+    }
+  }
+
+  /**
+   * Validates transaction windows for overlaps and proper closure.
+   */
+  private def validateTransactionWindows(windows: Seq[TransactionWindow]): ValidationResults = {
+    val errors = mutable.ListBuffer[String]()
+    val warnings = mutable.ListBuffer[String]()
+
+    // Check for transactions without proper END
+    windows.foreach { window =>
+      if (window.lockReleaseTime.isEmpty) {
+        warnings += s"[WARNING] ${window.filename} => transaction did not end gracefully. This could be due to driver OOM or non-graceful shutdown."
+      }
+    }
+
+    // Sort windows by start time for overlap detection
+    val sortedWindows = windows.sortBy(_.lockAcquisitionTime)
+
+    // Check for overlaps
+    for (i <- sortedWindows.indices) {
+      val currentWindow = sortedWindows(i)
+      val currentEnd = currentWindow.effectiveEndTime
+
+      // Check all subsequent windows for overlaps
+      for (j <- (i + 1) until sortedWindows.length) {
+        val otherWindow = sortedWindows(j)
+        val otherStart = otherWindow.lockAcquisitionTime
+
+        // Check if windows overlap
+        if (otherStart < currentEnd) {
+          errors += s"[ERROR] ${currentWindow.filename} => overlaps with ${otherWindow.filename}"
+        }
+      }
+    }
+
+    ValidationResults(errors.toList, warnings.toList)
+  }
+
+  /**
+   * Formats validation results into the output format.
+   */
+  private def formatValidationResults(results: ValidationResults, totalFiles: Int, parsedFiles: Int, failedFiles: Int): (String, Int, String) = {
+    val totalIssues = results.errors.size + results.warnings.size
+
+    if (totalIssues == 0) {
+      ("PASSED", 0, s"All audit lock transactions validated successfully. Audit Files: $totalFiles total, $parsedFiles parsed successfully, $failedFiles failed to parse")
+    } else {
+      val resultType = if (results.errors.nonEmpty) "FAILED" else "WARNING"
+      val fileInfo = s"Audit Files: $totalFiles total, $parsedFiles parsed successfully, $failedFiles failed to parse. "
+      val details = fileInfo + (results.errors ++ results.warnings).mkString(", ")
+      (resultType, totalIssues, details)
+    }
+  }
+
+  /**
+   * Case class to hold validation results.
+   */
+  private case class ValidationResults(errors: List[String], warnings: List[String])
+
+  /**
+   * Builds a new instance of the ValidateAuditLockProcedure.
+   *
+   * @return New ValidateAuditLockProcedure instance
+   */
+  override def build: Procedure = new ValidateAuditLockProcedure()
+}
+
+
+/**
+ * Companion object for ValidateAuditLockProcedure containing constants and factory methods.
+ */
+object ValidateAuditLockProcedure {
+  /** The name used to register and invoke this procedure */
+  val NAME = "validate_audit_lock"
+
+  /**
+   * Factory method to create procedure builder instances.
+   *
+   * @return Supplier that creates new ValidateAuditLockProcedure instances
+   */
+  def builder: Supplier[ProcedureBuilder] = JFunction.toJavaSupplier(() => new ValidateAuditLockProcedure())
+}
diff --git a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestCleanupAuditLockProcedure.scala b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestCleanupAuditLockProcedure.scala
new file mode 100644
index 000000000000..ca7717b0b44f
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestCleanupAuditLockProcedure.scala
@@ -0,0 +1,296 @@
+/*
+ * 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.spark.sql.hudi.procedure
+
+import org.apache.hudi.client.transaction.lock.audit.StorageLockProviderAuditService
+import org.apache.hudi.storage.StoragePath
+import org.apache.hudi.testutils.HoodieClientTestUtils
+
+import java.io.File
+import java.nio.file.{Files, Path, Paths}
+import java.util.function.Predicate
+
+/**
+ * Test suite for the CleanupAuditLockProcedure Spark SQL procedure.
+ *
+ * This class contains comprehensive tests to verify the functionality of
+ * the cleanup_audit_lock procedure, including dry run mode, age-based cleanup,
+ * parameter validation, and error handling scenarios.
+ *
+ * @author Apache Hudi
+ * @since 1.1.0
+ */
+class TestCleanupAuditLockProcedure extends HoodieSparkProcedureTestBase {
+
+  override def generateTableName: String = {
+    super.generateTableName.split("\\.").last
+  }
+
+  /**
+   * Helper method to create a test table and return its path.
+   */
+  private def createTestTable(tmp: File, tableName: String): String = {
+    spark.sql(
+      s"""
+         |create table $tableName (
+         |  id int,
+         |  name string,
+         |  price double,
+         |  ts long
+         |) using hudi
+         | location '${tmp.getCanonicalPath}/$tableName'
+         | tblproperties (
+         |  primaryKey = 'id',
+         |  orderingFields = 'ts'
+         | )
+       """.stripMargin)
+    // Insert data to initialize the Hudi metadata structure
+    spark.sql(s"insert into $tableName select 1, 'test', 10, 1000")
+    s"${tmp.getCanonicalPath}/$tableName"
+  }
+
+  /**
+   * Helper method to create test audit files with specified ages.
+   *
+   * @param tablePath The base path of the table
+   * @param filenames List of filenames to create
+   * @param ageDaysAgo How many days ago to set the modification time
+   */
+  private def createTestAuditFiles(tablePath: String, filenames: List[String], ageDaysAgo: Int): Unit = {
+    val auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(tablePath)
+    val auditDir = Paths.get(auditFolderPath)
+
+    // Create audit directory if it doesn't exist
+    if (!Files.exists(auditDir)) {
+      Files.createDirectories(auditDir)
+    }
+
+    // Create test audit files with specified modification time
+    val targetTime = System.currentTimeMillis() - (ageDaysAgo * 24L * 60L * 60L * 1000L)
+
+    filenames.foreach { filename =>
+      val filePath = auditDir.resolve(filename)
+      val content = s"""{"ownerId":"test","transactionStartTime":${System.currentTimeMillis()},"timestamp":${System.currentTimeMillis()},"state":"START","lockExpiration":${System.currentTimeMillis() + 60000},"lockHeld":true}"""
+      Files.write(filePath, content.getBytes())
+      // Set the modification time to simulate old files
+      Files.setLastModifiedTime(filePath, java.nio.file.attribute.FileTime.fromMillis(targetTime))
+    }
+  }
+
+  /**
+   * Helper method to count audit files in the audit folder.
+   */
+  private def countAuditFiles(tablePath: String): Int = {
+    val auditFolderPath = StorageLockProviderAuditService.getAuditFolderPath(tablePath)
+    val auditDir = Paths.get(auditFolderPath)
+
+    if (Files.exists(auditDir)) {
+      Files.list(auditDir)
+        .filter(new Predicate[Path]{
+          override def test(t: Path): Boolean = t.toString.endsWith(".jsonl")
+        })
+        .count()
+        .toInt
+    } else {
+      0
+    }
+  }
+
+  /**
+   * Test cleanup with table name parameter - dry run mode.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Dry Run with Table Name") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Create some old audit files (10 days old)
+      createTestAuditFiles(tablePath, List("old1.jsonl", "old2.jsonl"), ageDaysAgo = 10)
+      // Create some recent audit files (2 days old)
+      createTestAuditFiles(tablePath, List("recent1.jsonl"), ageDaysAgo = 2)
+
+      val result = spark.sql(s"""call cleanup_audit_lock(table => '$tableName', dry_run => true, age_days => 7)""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult(2)(result.head.get(1)) // Should find 2 old files
+      assertResult(true)(result.head.get(2)) // dry_run = true
+      assertResult(7)(result.head.get(3)) // age_days = 7
+      assert(result.head.get(4).toString.contains("Dry run"))
+
+      // Verify files still exist (dry run shouldn't delete)
+      assertResult(3)(countAuditFiles(tablePath))
+    }
+  }
+
+  /**
+   * Test cleanup with path parameter - actual deletion.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Actual Cleanup with Path") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Create some old audit files (10 days old)
+      createTestAuditFiles(tablePath, List("old1.jsonl", "old2.jsonl"), ageDaysAgo = 10)
+      // Create some recent audit files (2 days old)
+      createTestAuditFiles(tablePath, List("recent1.jsonl"), ageDaysAgo = 2)
+
+      val result = spark.sql(s"""call cleanup_audit_lock(path => '$tablePath', dry_run => false, age_days => 7)""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tablePath)(result.head.get(0))
+      assertResult(2)(result.head.get(1)) // Should delete 2 old files
+      assertResult(false)(result.head.get(2)) // dry_run = false
+      assertResult(7)(result.head.get(3)) // age_days = 7
+      assert(result.head.get(4).toString.contains("Successfully deleted"))
+
+      // Verify only recent file remains
+      assertResult(1)(countAuditFiles(tablePath))
+    }
+  }
+
+  /**
+   * Test cleanup with default parameters.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Default Parameters") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Create some old audit files (10 days old) - should be deleted with default 7 days
+      createTestAuditFiles(tablePath, List("old1.jsonl"), ageDaysAgo = 10)
+      // Create some recent audit files (3 days old) - should be kept
+      createTestAuditFiles(tablePath, List("recent1.jsonl"), ageDaysAgo = 3)
+
+      val result = spark.sql(s"""call cleanup_audit_lock(table => '$tableName')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult(1)(result.head.get(1)) // Should delete 1 old file
+      assertResult(false)(result.head.get(2)) // dry_run defaults to false
+      assertResult(7)(result.head.get(3)) // age_days defaults to 7
+
+      // Verify only recent file remains
+      assertResult(1)(countAuditFiles(tablePath))
+    }
+  }
+
+  /**
+   * Test cleanup when no audit folder exists.
+   */
+  test("Test Call cleanup_audit_lock Procedure - No Audit Folder") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+      // Don't create any audit files
+
+      val result = spark.sql(s"""call cleanup_audit_lock(table => '$tableName')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult(0)(result.head.get(1)) // No files to delete
+      assert(result.head.get(4).toString.contains("No audit folder found"))
+    }
+  }
+
+  /**
+   * Test cleanup when no old files exist.
+   */
+  test("Test Call cleanup_audit_lock Procedure - No Old Files") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Create only recent audit files (2 days old)
+      createTestAuditFiles(tablePath, List("recent1.jsonl", "recent2.jsonl"), ageDaysAgo = 2)
+
+      val result = spark.sql(s"""call cleanup_audit_lock(table => '$tableName', age_days => 7)""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult(0)(result.head.get(1)) // No old files to delete
+      assert(result.head.get(4).toString.contains("No audit files older than 7 days found"))
+
+      // Verify all files remain
+      assertResult(2)(countAuditFiles(tablePath))
+    }
+  }
+
+  /**
+   * Test parameter validation - negative age_days.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Invalid Age Days") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      createTestTable(tmp, tableName)
+
+      checkExceptionContain(s"""call cleanup_audit_lock(table => '$tableName', age_days => -1)""")(
+        "age_days must be a positive integer")
+
+      checkExceptionContain(s"""call cleanup_audit_lock(table => '$tableName', age_days => 0)""")(
+        "age_days must be a positive integer")
+    }
+  }
+
+  /**
+   * Test parameter validation - both table and path provided.
+   * Since both are provided, the procedure will use the table parameter (consistent with other procedures).
+   */
+  test("Test Call cleanup_audit_lock Procedure - Both Table and Path Parameters") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Should work fine - will use table parameter when both are provided
+      val result = spark.sql(s"""call cleanup_audit_lock(table => '$tableName', path => '$tablePath')""").collect()
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.getString(0))
+    }
+  }
+
+  /**
+   * Test parameter validation - missing required arguments.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Missing Required Arguments") {
+    checkExceptionContain(s"""call cleanup_audit_lock(dry_run => true)""")(
+      "Table name or table path must be given one")
+  }
+
+  /**
+   * Test cleanup with non-existent table.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Non-existent Table") {
+    val nonExistentTable = "non_existent_table"
+
+    intercept[Exception] {
+      spark.sql(s"""call cleanup_audit_lock(table => '$nonExistentTable')""")
+    }
+  }
+
+  /**
+   * Test cleanup with invalid path.
+   */
+  test("Test Call cleanup_audit_lock Procedure - Invalid Path") {
+    val invalidPath = "/non/existent/path"
+
+    intercept[Exception] {
+      spark.sql(s"""call cleanup_audit_lock(path => '$invalidPath')""")
+    }
+  }
+}
diff --git a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestSetAuditLockProcedure.scala b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestSetAuditLockProcedure.scala
new file mode 100644
index 000000000000..348780ad9b64
--- /dev/null
+++ b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/procedure/TestSetAuditLockProcedure.scala
@@ -0,0 +1,169 @@
+/*
+ * 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.spark.sql.hudi.procedure
+
+import java.io.File
+
+/**
+ * Test suite for the SetAuditLockProcedure Spark SQL procedure.
+ *
+ * This class contains comprehensive tests to verify the functionality of
+ * the set_audit_lock procedure, including enabling/disabling audit logging,
+ * parameter validation, and error handling scenarios.
+ *
+ * @author Apache Hudi
+ * @since 1.1.0
+ */
+class TestSetAuditLockProcedure extends HoodieSparkProcedureTestBase {
+
+  override def generateTableName: String = {
+    super.generateTableName.split("\\.").last
+  }
+
+  /**
+   * Helper method to create a test table and return its path.
+   */
+  private def createTestTable(tmp: File, tableName: String): String = {
+    spark.sql(
+      s"""
+         |create table $tableName (
+         |  id int,
+         |  name string,
+         |  price double,
+         |  ts long
+         |) using hudi
+         | location '${tmp.getCanonicalPath}/$tableName'
+         | tblproperties (
+         |  primaryKey = 'id',
+         |  orderingFields = 'ts'
+         | )
+       """.stripMargin)
+    // Insert data to initialize the Hudi metadata structure
+    spark.sql(s"insert into $tableName select 1, 'test', 10, 1000")
+    s"${tmp.getCanonicalPath}/$tableName"
+  }
+
+  /**
+   * Test enabling audit logging using table name parameter.
+   */
+  test("Test Call set_audit_lock Procedure - Enable Audit with Table Name") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      val result = spark.sql(s"""call set_audit_lock(table => '$tableName', state => 'enabled')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult("enabled")(result.head.get(1))
+      assert(result.head.get(2).toString.contains("successfully enabled"))
+    }
+  }
+
+  /**
+   * Test enabling audit logging using path parameter.
+   */
+  test("Test Call set_audit_lock Procedure - Enable Audit with Path") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      val result = spark.sql(s"""call set_audit_lock(path => '$tablePath', state => 'enabled')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tablePath)(result.head.get(0))
+      assertResult("enabled")(result.head.get(1))
+      assert(result.head.get(2).toString.contains("successfully enabled"))
+    }
+  }
+
+  /**
+   * Test disabling audit logging using table name parameter.
+   */
+  test("Test Call set_audit_lock Procedure - Disable Audit with Table Name") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      val result = spark.sql(s"""call set_audit_lock(table => '$tableName', state => 'disabled')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.get(0))
+      assertResult("disabled")(result.head.get(1))
+      assert(result.head.get(2).toString.contains("successfully disabled"))
+    }
+  }
+
+  /**
+   * Test disabling audit logging using path parameter.
+   */
+  test("Test Call set_audit_lock Procedure - Disable Audit with Path") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      val result = spark.sql(s"""call set_audit_lock(path => '$tablePath', state => 'disabled')""").collect()
+
+      assertResult(1)(result.length)
+      assertResult(tablePath)(result.head.get(0))
+      assertResult("disabled")(result.head.get(1))
+      assert(result.head.get(2).toString.contains("successfully disabled"))
+    }
+  }
+
+  /**
+   * Test parameter validation by providing an invalid state parameter.
+   * Verifies that the procedure rejects invalid state values and provides
+   * an appropriate error message.
+   */
+  test("Test Call set_audit_lock Procedure - Invalid State Parameter") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      createTestTable(tmp, tableName)
+
+      // Test invalid state parameter
+      checkExceptionContain(s"""call set_audit_lock(table => '$tableName', state => 'invalid')""")(
+        "State parameter must be 'enabled' or 'disabled'")
+    }
+  }
+
+  /**
+   * Test parameter validation by omitting required arguments.
+   * Verifies that the procedure properly validates required parameters
+   * and provides appropriate error messages for missing arguments.
+   */
+  test("Test Call set_audit_lock Procedure - Missing Required Arguments") {
+    withTempDir { tmp =>
+      val tableName = generateTableName
+      val tablePath = createTestTable(tmp, tableName)
+
+      // Test missing both table and path parameters
+      checkExceptionContain(s"""call set_audit_lock(state => 'enabled')""")(
+        "Table name or table path must be given one")
+
+      // Test missing state parameter
+      checkExceptionContain(s"""call set_audit_lock(table => '$tableName')""")(
+        "Argument: state is required")
+
+      // Test providing both table and path parameters - should work fine (uses table parameter)
+      val result = spark.sql(s"""call set_audit_lock(table => '$tableName', path => '$tablePath', state => 'enabled')""").collect()
+      assertResult(1)(result.length)
+      assertResult(tableName)(result.head.getString(0))
+    }
... 1170 lines suppressed ...