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

lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 71e415cf feat(metrics): expose scan and prefetch metrics (#211)
71e415cf is described below

commit 71e415cf9c281da641a7f0452d820b836c0c27ae
Author: gripleaf <[email protected]>
AuthorDate: Fri Aug 28 16:47:00 2026 +0800

    feat(metrics): expose scan and prefetch metrics (#211)
---
 docs/source/user_guide.rst                         |   1 +
 docs/source/user_guide/metrics.rst                 | 116 ++++++++
 include/paimon/defs.h                              |   4 +
 include/paimon/reader/prefetch_file_batch_reader.h |  41 +++
 include/paimon/table/source/scan_metrics.h         |  54 ++++
 include/paimon/table/source/table_scan.h           |   7 +
 src/paimon/common/defs.cpp                         |   1 +
 .../apply_bitmap_index_batch_reader_test.cpp       |   3 +-
 .../common/reader/delegating_prefetch_reader.h     |   2 +-
 .../late_materializing_file_batch_reader_test.cpp  |   6 +-
 .../reader/prefetch_file_batch_reader_impl.cpp     | 293 +++++++++++++++++++--
 .../reader/prefetch_file_batch_reader_impl.h       |   7 +-
 .../prefetch_file_batch_reader_impl_test.cpp       | 235 ++++++++++++++---
 src/paimon/core/core_options.cpp                   |   7 +
 src/paimon/core/core_options.h                     |   2 +
 src/paimon/core/core_options_test.cpp              |   3 +
 .../apply_deletion_vector_batch_reader_test.cpp    |   3 +-
 src/paimon/core/operation/abstract_split_read.cpp  |   2 +-
 .../operation/append_only_file_store_scan_test.cpp |  43 ++-
 src/paimon/core/operation/file_store_scan.cpp      |  50 +++-
 src/paimon/core/operation/file_store_scan.h        |   7 +-
 .../operation/key_value_file_store_scan_test.cpp   |   2 +-
 src/paimon/core/operation/metrics/scan_metrics.h   |  35 ---
 src/paimon/core/table/source/abstract_table_scan.h |   4 +
 src/paimon/core/table/source/realtime_table_scan.h |   4 +
 .../core/table/source/snapshot/snapshot_reader.h   |   4 +
 src/paimon/core/table/source/table_scan.cpp        |   5 +
 src/paimon/core/table/source/table_scan_test.cpp   |  46 ++++
 src/paimon/core/table/system/system_table_test.cpp |  13 +
 29 files changed, 895 insertions(+), 105 deletions(-)

diff --git a/docs/source/user_guide.rst b/docs/source/user_guide.rst
index 38ef0fc6..fdee89d9 100644
--- a/docs/source/user_guide.rst
+++ b/docs/source/user_guide.rst
@@ -38,6 +38,7 @@ User Guide
    user_guide/commit
    user_guide/compaction
    user_guide/read
+   user_guide/metrics
    user_guide/clean
    user_guide/prefetch
    user_guide/arrow
diff --git a/docs/source/user_guide/metrics.rst 
b/docs/source/user_guide/metrics.rst
new file mode 100644
index 00000000..72ee2a17
--- /dev/null
+++ b/docs/source/user_guide/metrics.rst
@@ -0,0 +1,116 @@
+.. 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.
+
+Metrics reference
+=================
+
+``Metrics`` contains counters, gauges, and histogram snapshots. A counter is a 
non-negative
+integer, a gauge represents current state, and a histogram records a 
distribution of observed
+values. The scan and prefetch-reader metrics described below are returned as 
point-in-time
+snapshots. Modifying a returned snapshot does not modify the component that 
produced it.
+
+Scan planning
+-------------
+
+The names below are declared by ``ScanMetrics``. ``last*`` counters are 
replaced after each
+successful ``CreatePlan()`` call. Histograms and the cache hit/miss counters 
accumulate for the
+lifetime of the scan. They are unrelated to ``SetReadSchema()`` and 
``ReadAheadCache::Reset()``.
+The first six names match Java ``ScanMetrics``; the remaining names are 
C++-only.
+
+.. csv-table::
+   :header: "Name", "Type", "Unit", "Meaning"
+   :widths: 34, 12, 12, 52
+
+   "lastScanDuration", "counter", "milliseconds", "Duration of the last 
successful plan"
+   "scanDuration", "histogram", "milliseconds", "Distribution of successful 
plan durations"
+   "lastScannedSnapshotId", "counter", "snapshot ID", "Snapshot used by the 
last plan, or 0"
+   "lastScannedManifests", "counter", "files", "Manifest files selected by the 
last plan"
+   "lastScanSkippedTableFiles", "counter", "files", "Table files skipped by 
the last plan"
+   "lastScanResultedTableFiles", "counter", "files", "Table files returned by 
the last plan"
+   "lastManifestReadDuration", "counter", "milliseconds", "Manifest-list and 
entry read time for the last plan"
+   "manifestReadDuration", "histogram", "milliseconds", "Distribution of 
manifest read times"
+   "lastSnapshotCacheEnabled", "counter", "boolean", "Whether snapshot 
manifest-entry cache was eligible"
+   "lastSnapshotCacheHit", "counter", "boolean", "Whether the last eligible 
lookup hit"
+   "snapshotCacheHits", "counter", "lookups", "Cumulative exact-snapshot cache 
hits"
+   "snapshotCacheMisses", "counter", "lookups", "Cumulative eligible cache 
misses"
+   "lastSnapshotCacheLoadDuration", "counter", "milliseconds", "Cache load 
time for the last plan"
+   "snapshotCacheLoadDuration", "histogram", "milliseconds", "Distribution of 
cache load times"
+   "lastSnapshotCacheStoreDuration", "counter", "milliseconds", "Cache store 
time for the last plan; 0 when not stored"
+   "snapshotCacheStoreDuration", "histogram", "milliseconds", "Distribution of 
cache store times"
+   "lastLazyDecodeScannedRows", "counter", "manifest rows", "Candidate 
manifest rows inspected by the last plan"
+   "lastLazyDecodeMaterializedRows", "counter", "manifest rows", "Manifest 
rows retained after lazy filtering"
+
+Prefetch reader
+---------------
+
+The names below are declared by ``PrefetchMetrics``. Counters and histograms 
accumulate for the
+lifetime of the prefetch reader, including across ``SetReadSchema()``. 
``enabled`` and
+``parallelism`` describe the most recently initialized schema. ``queue-depth`` 
is reset by
+``SetReadSchema()`` and ``Close()``; ``queue-depth.max`` remains the lifetime 
maximum.
+These metrics are C++-only and have no counterparts in Java Paimon.
+
+.. csv-table::
+   :header: "Name", "Type", "Unit", "Meaning"
+   :widths: 38, 12, 12, 48
+
+   "prefetch.enabled", "gauge", "boolean", "Whether the most recently 
initialized schema selected prefetch"
+   "prefetch.parallelism", "gauge", "readers", "Effective reader parallelism"
+   "prefetch.read-ranges.total", "counter", "ranges", "Generated ranges before 
bitmap filtering"
+   "prefetch.read-ranges.after-bitmap", "counter", "ranges", "Ranges retained 
after bitmap filtering"
+   "prefetch.seek.count", "counter", "operations", "Underlying reader seek 
operations"
+   "prefetch.produced-batches", "counter", "batches", "Data batches placed 
into prefetch queues"
+   "prefetch.consumed-batches", "counter", "batches", "Data batches returned 
to the consumer"
+   "prefetch.discarded-batches", "counter", "batches", "Data batches released 
without consumption, plus EOF entries released during cleanup"
+   "prefetch.errors", "counter", "errors", "Errors recorded by the background 
prefetch loop"
+   "prefetch.adaptive-disabled-count", "counter", "decisions", "Times adaptive 
strategy disabled prefetch"
+   "prefetch.queue-full-count", "counter", "events", "Times production found a 
full queue"
+   "prefetch.queue-depth", "gauge", "queue entries", "Current queued entries, 
including retained EOF markers"
+   "prefetch.queue-depth.max", "gauge", "queue entries", "Maximum queued 
entries in the reader lifetime"
+   "prefetch.reader-read-latency-us", "histogram", "microseconds", "Underlying 
reader batch latency"
+   "prefetch.consumer-wait-latency-us", "histogram", "microseconds", "Consumer 
wait latency per returned batch or EOF"
+
+Prefetch I/O
+------------
+
+``PrefetchIoMetrics`` describes only I/O that passes through the prefetch 
reader's instrumented
+input streams. It is not a whole-query or whole-table I/O total. All counters 
accumulate for the
+reader lifetime and are retained across ``SetReadSchema()`` and cache reset. 
Latency uses relaxed
+atomic count and sum counters instead of per-I/O histograms to reduce hot-path 
cost. Collection is
+disabled by default; set ``prefetch.io-metrics.enabled`` to ``true`` in the 
read options to enable
+it. When disabled, these per-I/O metrics are absent and the input streams have 
no metrics
+instrumentation.
+``io.async.pending`` is current state and returns to zero when all callbacks 
complete.
+These metrics are C++-only and have no counterparts in Java Paimon.
+
+.. csv-table::
+   :header: "Name", "Type", "Unit", "Meaning"
+   :widths: 34, 12, 12, 52
+
+   "io.read.requests", "counter", "requests", "Synchronous read requests"
+   "io.read.requested-bytes", "counter", "bytes", "Bytes requested by 
synchronous reads"
+   "io.read.physical-bytes", "counter", "bytes", "Bytes returned by successful 
synchronous reads"
+   "io.read.failed", "counter", "requests", "Failed synchronous reads"
+   "io.read.latency.count", "counter", "requests", "Completed synchronous read 
latency samples"
+   "io.read.latency.sum-us", "counter", "microseconds", "Sum of synchronous 
read latency"
+   "io.async.requests", "counter", "requests", "Asynchronous read requests"
+   "io.async.requested-bytes", "counter", "bytes", "Bytes requested by 
asynchronous reads"
+   "io.async.physical-bytes", "counter", "bytes", "Bytes attributed to 
successful asynchronous reads"
+   "io.async.completed", "counter", "requests", "Successful asynchronous reads"
+   "io.async.failed", "counter", "requests", "Failed asynchronous reads"
+   "io.async.pending", "gauge", "requests", "Asynchronous callbacks not yet 
completed"
+   "io.async.latency.count", "counter", "requests", "Completed asynchronous 
callback latency samples"
+   "io.async.latency.sum-us", "counter", "microseconds", "Sum of asynchronous 
callback latency"
diff --git a/include/paimon/defs.h b/include/paimon/defs.h
index c96cf5d4..0ca67e3a 100644
--- a/include/paimon/defs.h
+++ b/include/paimon/defs.h
@@ -208,6 +208,10 @@ struct PAIMON_EXPORT Options {
     /// for the target bucket when rebuilding the cache. Default value is true.
     static const char SCAN_MANIFEST_ENTRY_LAZY_DECODE_ENABLED[];
 
+    /// "prefetch.io-metrics.enabled" - Whether to collect per-I/O metrics for 
prefetch reads.
+    /// Default value is false.
+    static const char PREFETCH_IO_METRICS_ENABLED[];
+
     /// "read.batch-size" - Read batch size for any file format if it supports.
     /// The default value is 1024.
     static const char READ_BATCH_SIZE[];
diff --git a/include/paimon/reader/prefetch_file_batch_reader.h 
b/include/paimon/reader/prefetch_file_batch_reader.h
index 5e6313e8..84227995 100644
--- a/include/paimon/reader/prefetch_file_batch_reader.h
+++ b/include/paimon/reader/prefetch_file_batch_reader.h
@@ -27,6 +27,47 @@
 
 namespace paimon {
 
+/// C++-only prefetch reader metrics. Java Paimon has no corresponding metrics.
+class PAIMON_EXPORT PrefetchMetrics {
+ public:
+    static constexpr char ENABLED[] = "prefetch.enabled";
+    static constexpr char PARALLELISM[] = "prefetch.parallelism";
+    static constexpr char READ_RANGES_TOTAL[] = "prefetch.read-ranges.total";
+    static constexpr char READ_RANGES_AFTER_BITMAP[] = 
"prefetch.read-ranges.after-bitmap";
+    static constexpr char SEEK_COUNT[] = "prefetch.seek.count";
+    static constexpr char PRODUCED_BATCHES[] = "prefetch.produced-batches";
+    static constexpr char CONSUMED_BATCHES[] = "prefetch.consumed-batches";
+    static constexpr char DISCARDED_BATCHES[] = "prefetch.discarded-batches";
+    static constexpr char ERRORS[] = "prefetch.errors";
+    static constexpr char ADAPTIVE_DISABLED_COUNT[] = 
"prefetch.adaptive-disabled-count";
+    static constexpr char QUEUE_FULL_COUNT[] = "prefetch.queue-full-count";
+    static constexpr char QUEUE_DEPTH[] = "prefetch.queue-depth";
+    static constexpr char QUEUE_DEPTH_MAX[] = "prefetch.queue-depth.max";
+    static constexpr char READER_READ_LATENCY_US[] = 
"prefetch.reader-read-latency-us";
+    static constexpr char CONSUMER_WAIT_LATENCY_US[] = 
"prefetch.consumer-wait-latency-us";
+};
+
+/// C++-only metric names for I/O observed by the prefetch reader's 
instrumented input streams.
+/// Java Paimon has no corresponding metrics.
+/// These metrics do not represent whole-query or whole-table I/O.
+class PAIMON_EXPORT PrefetchIoMetrics {
+ public:
+    static constexpr char READ_REQUESTS[] = "io.read.requests";
+    static constexpr char READ_REQUESTED_BYTES[] = "io.read.requested-bytes";
+    static constexpr char READ_PHYSICAL_BYTES[] = "io.read.physical-bytes";
+    static constexpr char READ_FAILED[] = "io.read.failed";
+    static constexpr char READ_LATENCY_COUNT[] = "io.read.latency.count";
+    static constexpr char READ_LATENCY_SUM_US[] = "io.read.latency.sum-us";
+    static constexpr char ASYNC_REQUESTS[] = "io.async.requests";
+    static constexpr char ASYNC_REQUESTED_BYTES[] = "io.async.requested-bytes";
+    static constexpr char ASYNC_PHYSICAL_BYTES[] = "io.async.physical-bytes";
+    static constexpr char ASYNC_COMPLETED[] = "io.async.completed";
+    static constexpr char ASYNC_FAILED[] = "io.async.failed";
+    static constexpr char ASYNC_PENDING[] = "io.async.pending";
+    static constexpr char ASYNC_LATENCY_COUNT[] = "io.async.latency.count";
+    static constexpr char ASYNC_LATENCY_SUM_US[] = "io.async.latency.sum-us";
+};
+
 /// The prefetch file batch reader extends the basic FileBatchReader interface 
for prefetch read,
 /// if a format implementation inherits from this class, it will automatically 
support the C++
 /// Paimon prefetch capability and integrate with the Paimon prefetch 
framework.
diff --git a/include/paimon/table/source/scan_metrics.h 
b/include/paimon/table/source/scan_metrics.h
new file mode 100644
index 00000000..15bc41ff
--- /dev/null
+++ b/include/paimon/table/source/scan_metrics.h
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "paimon/visibility.h"
+
+namespace paimon {
+
+/// Metric names for scan planning operations.
+class PAIMON_EXPORT ScanMetrics {
+ public:
+    static constexpr char LAST_SCAN_DURATION[] = "lastScanDuration";
+    // Histogram metric for scan plan duration (milliseconds).
+    static constexpr char SCAN_DURATION[] = "scanDuration";
+    static constexpr char LAST_SCANNED_SNAPSHOT_ID[] = "lastScannedSnapshotId";
+    static constexpr char LAST_SCANNED_MANIFESTS[] = "lastScannedManifests";
+    static constexpr char LAST_SCAN_SKIPPED_TABLE_FILES[] = 
"lastScanSkippedTableFiles";
+    static constexpr char LAST_SCAN_RESULTED_TABLE_FILES[] = 
"lastScanResultedTableFiles";
+
+    // The metrics below are C++-only and do not have counterparts in Java 
ScanMetrics.
+    static constexpr char LAST_MANIFEST_READ_DURATION[] = 
"lastManifestReadDuration";
+    // Histogram metric for manifest-list and manifest-entry read duration 
(milliseconds).
+    static constexpr char MANIFEST_READ_DURATION[] = "manifestReadDuration";
+    static constexpr char LAST_SNAPSHOT_CACHE_ENABLED[] = 
"lastSnapshotCacheEnabled";
+    static constexpr char LAST_SNAPSHOT_CACHE_HIT[] = "lastSnapshotCacheHit";
+    static constexpr char SNAPSHOT_CACHE_HITS[] = "snapshotCacheHits";
+    static constexpr char SNAPSHOT_CACHE_MISSES[] = "snapshotCacheMisses";
+    static constexpr char LAST_SNAPSHOT_CACHE_LOAD_DURATION[] = 
"lastSnapshotCacheLoadDuration";
+    static constexpr char SNAPSHOT_CACHE_LOAD_DURATION[] = 
"snapshotCacheLoadDuration";
+    static constexpr char LAST_SNAPSHOT_CACHE_STORE_DURATION[] = 
"lastSnapshotCacheStoreDuration";
+    static constexpr char SNAPSHOT_CACHE_STORE_DURATION[] = 
"snapshotCacheStoreDuration";
+    // Candidate manifest-entry rows inspected by lazy scan filtering.
+    static constexpr char LAST_LAZY_DECODE_SCANNED_ROWS[] = 
"lastLazyDecodeScannedRows";
+    // Full manifest entries retained after lazy scan filtering.
+    static constexpr char LAST_LAZY_DECODE_MATERIALIZED_ROWS[] = 
"lastLazyDecodeMaterializedRows";
+};
+
+}  // namespace paimon
diff --git a/include/paimon/table/source/table_scan.h 
b/include/paimon/table/source/table_scan.h
index c9b42915..14c44756 100644
--- a/include/paimon/table/source/table_scan.h
+++ b/include/paimon/table/source/table_scan.h
@@ -23,6 +23,7 @@
 
 #include "paimon/result.h"
 #include "paimon/table/source/plan.h"
+#include "paimon/table/source/scan_metrics.h"
 #include "paimon/type_fwd.h"
 #include "paimon/visibility.h"
 
@@ -44,5 +45,11 @@ class PAIMON_EXPORT TableScan {
     ///
     /// @return A Result containing a shared pointer to the created `Plan` or 
an error status.
     virtual Result<std::shared_ptr<Plan>> CreatePlan() = 0;
+
+    /// Retrieve metrics related to scan planning operations.
+    ///
+    /// @return A point-in-time snapshot of scan metrics. Mutating the 
returned object does not
+    /// affect metrics collected by this scan.
+    virtual std::shared_ptr<Metrics> GetMetrics() const;
 };
 }  // namespace paimon
diff --git a/src/paimon/common/defs.cpp b/src/paimon/common/defs.cpp
index 45496146..39e6d494 100644
--- a/src/paimon/common/defs.cpp
+++ b/src/paimon/common/defs.cpp
@@ -62,6 +62,7 @@ const char Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS[] 
=
     "scan.manifest-entry-cache.max-snapshots";
 const char Options::SCAN_MANIFEST_ENTRY_LAZY_DECODE_ENABLED[] =
     "scan.manifest-entry.lazy-decode.enabled";
+const char Options::PREFETCH_IO_METRICS_ENABLED[] = 
"prefetch.io-metrics.enabled";
 const char Options::READ_BATCH_SIZE[] = "read.batch-size";
 const char Options::WRITE_BATCH_SIZE[] = "write.batch-size";
 const char Options::WRITE_BUFFER_SIZE[] = "write-buffer-size";
diff --git 
a/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader_test.cpp 
b/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader_test.cpp
index 1082e669..ea45c979 100644
--- 
a/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader_test.cpp
+++ 
b/src/paimon/common/file_index/bitmap/apply_bitmap_index_batch_reader_test.cpp
@@ -97,7 +97,8 @@ class ApplyBitmapIndexBatchReaderTest : public 
::testing::Test,
                         prefetch_batch_count, batch_size, prefetch_batch_count 
* 2,
                         /*enable_adaptive_prefetch_strategy=*/false, executor_,
                         /*initialize_read_ranges=*/true,
-                        /*read_ahead_cache_enabled=*/true, CacheConfig(), 
pool_));
+                        /*read_ahead_cache_enabled=*/true, CacheConfig(),
+                        /*enable_io_metrics=*/false, pool_));
             } else {
                 file_batch_reader =
                     std::make_unique<MockFileBatchReader>(data, target_type_, 
batch_size);
diff --git a/src/paimon/common/reader/delegating_prefetch_reader.h 
b/src/paimon/common/reader/delegating_prefetch_reader.h
index 3cbcb08b..78396e20 100644
--- a/src/paimon/common/reader/delegating_prefetch_reader.h
+++ b/src/paimon/common/reader/delegating_prefetch_reader.h
@@ -45,7 +45,7 @@ class DelegatingPrefetchReader : public FileBatchReader {
     }
 
     std::shared_ptr<Metrics> GetReaderMetrics() const override {
-        return GetReader()->GetReaderMetrics();
+        return prefetch_reader_->GetReaderMetrics();
     }
 
     Result<std::unique_ptr<::ArrowSchema>> GetFileSchema() const override {
diff --git 
a/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp 
b/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
index ff571a28..075fbe10 100644
--- a/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
+++ b/src/paimon/common/reader/late_materializing_file_batch_reader_test.cpp
@@ -532,7 +532,7 @@ TEST_F(LateMaterializingFileBatchReaderTest, 
WorksAsInnerOfPrefetchReader) {
             /*prefetch_max_parallel_num=*/1, /*batch_size=*/3, 
/*prefetch_batch_count=*/2,
             /*enable_adaptive_prefetch_strategy=*/false, executor,
             /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/false, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     auto predicate =
         PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "k", 
FieldType::BIGINT, Literal(4l));
     ::ArrowSchema c_schema;
@@ -568,7 +568,7 @@ TEST_F(LateMaterializingFileBatchReaderTest, 
PrefetchInnerReentrantSetReadSchema
             /*prefetch_max_parallel_num=*/1, /*batch_size=*/3, 
/*prefetch_batch_count=*/2,
             /*enable_adaptive_prefetch_strategy=*/false, executor,
             /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/false, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
 
     auto full_schema = arrow::schema(full_fields_);
     auto predicate1 =
@@ -631,7 +631,7 @@ TEST_F(LateMaterializingFileBatchReaderTest, 
PrefetchInnerParallelReadersWithSee
             /*prefetch_max_parallel_num=*/3, /*batch_size=*/3, 
/*prefetch_batch_count=*/6,
             /*enable_adaptive_prefetch_strategy=*/false, executor,
             /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/false, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     auto predicate =
         PredicateBuilder::GreaterOrEqual(/*field_index=*/0, "k", 
FieldType::BIGINT, Literal(5l));
     ::ArrowSchema c_schema;
diff --git a/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp 
b/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
index 4b6411b7..5285a2d8 100644
--- a/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
+++ b/src/paimon/common/reader/prefetch_file_batch_reader_impl.cpp
@@ -21,6 +21,7 @@
 #include <algorithm>
 #include <chrono>
 #include <future>
+#include <limits>
 #include <thread>
 
 #include "arrow/array/array_base.h"
@@ -44,8 +45,149 @@ class Schema;
 
 namespace paimon {
 
+struct PrefetchMetricsState {
+    std::atomic<uint64_t> read_ranges_total{0};
+    std::atomic<uint64_t> read_ranges_after_bitmap{0};
+    std::atomic<uint64_t> seek_count{0};
+    std::atomic<uint64_t> produced_batches{0};
+    std::atomic<uint64_t> consumed_batches{0};
+    std::atomic<uint64_t> discarded_batches{0};
+    std::atomic<uint64_t> errors{0};
+    std::atomic<uint64_t> adaptive_disabled_count{0};
+    std::atomic<uint64_t> queue_full_count{0};
+    std::atomic<uint64_t> queue_depth{0};
+    std::atomic<uint64_t> queue_depth_max{0};
+    std::atomic<bool> enabled{false};
+    std::shared_ptr<MetricsImpl> histograms = std::make_shared<MetricsImpl>();
+};
+
+struct PrefetchIoMetricsState {
+    std::atomic<uint64_t> read_requests{0};
+    std::atomic<uint64_t> read_requested_bytes{0};
+    std::atomic<uint64_t> read_physical_bytes{0};
+    std::atomic<uint64_t> read_failed{0};
+    std::atomic<uint64_t> read_latency_count{0};
+    std::atomic<uint64_t> read_latency_sum_us{0};
+    std::atomic<uint64_t> async_requests{0};
+    std::atomic<uint64_t> async_requested_bytes{0};
+    std::atomic<uint64_t> async_physical_bytes{0};
+    std::atomic<uint64_t> async_completed{0};
+    std::atomic<uint64_t> async_failed{0};
+    std::atomic<uint64_t> async_pending{0};
+    std::atomic<uint64_t> async_latency_count{0};
+    std::atomic<uint64_t> async_latency_sum_us{0};
+};
+
 namespace {
 
+// Metrics do not synchronize reader state. A concurrently collected snapshot 
may be approximate.
+constexpr std::memory_order kMetricsMemoryOrder = std::memory_order_relaxed;
+
+uint64_t ElapsedMicros(const std::chrono::steady_clock::time_point& start) {
+    return 
static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
+                                     std::chrono::steady_clock::now() - start)
+                                     .count());
+}
+
+void UpdateMax(std::atomic<uint64_t>* target, uint64_t value) {
+    uint64_t current = target->load(kMetricsMemoryOrder);
+    while (current < value && !target->compare_exchange_weak(current, value, 
kMetricsMemoryOrder,
+                                                             
kMetricsMemoryOrder)) {
+    }
+}
+
+void RecordLatency(uint64_t latency_us, std::atomic<uint64_t>* count,
+                   std::atomic<uint64_t>* sum_us) {
+    count->fetch_add(1, kMetricsMemoryOrder);
+    sum_us->fetch_add(latency_us, kMetricsMemoryOrder);
+}
+
+class MetricsInputStream : public InputStream {
+ public:
+    MetricsInputStream(const std::shared_ptr<InputStream>& stream,
+                       const std::shared_ptr<PrefetchIoMetricsState>& metrics)
+        : stream_(stream), metrics_(metrics) {}
+
+    MetricsInputStream(std::unique_ptr<InputStream>&& stream,
+                       const std::shared_ptr<PrefetchIoMetricsState>& metrics)
+        : stream_(std::move(stream)), metrics_(metrics) {}
+
+    Status Seek(int64_t offset, SeekOrigin origin) override {
+        return stream_->Seek(offset, origin);
+    }
+
+    Result<int64_t> GetPos() const override {
+        return stream_->GetPos();
+    }
+
+    Result<int64_t> Read(char* buffer, int64_t size) override {
+        return RecordRead([&]() { return stream_->Read(buffer, size); }, size);
+    }
+
+    Result<int64_t> Read(char* buffer, int64_t size, int64_t offset) override {
+        return RecordRead([&]() { return stream_->Read(buffer, size, offset); 
}, size);
+    }
+
+    void ReadAsync(char* buffer, int64_t size, int64_t offset,
+                   std::function<void(Status)>&& callback) override {
+        metrics_->async_requests.fetch_add(1, kMetricsMemoryOrder);
+        
metrics_->async_requested_bytes.fetch_add(static_cast<uint64_t>(std::max<int64_t>(0,
 size)),
+                                                  kMetricsMemoryOrder);
+        metrics_->async_pending.fetch_add(1, kMetricsMemoryOrder);
+        std::shared_ptr<PrefetchIoMetricsState> metrics = metrics_;
+        const auto start = std::chrono::steady_clock::now();
+        stream_->ReadAsync(
+            buffer, size, offset,
+            [metrics, size, start, callback = std::move(callback)](Status 
status) mutable {
+                metrics->async_pending.fetch_sub(1, kMetricsMemoryOrder);
+                if (status.ok()) {
+                    metrics->async_completed.fetch_add(1, kMetricsMemoryOrder);
+                    metrics->async_physical_bytes.fetch_add(
+                        static_cast<uint64_t>(std::max<int64_t>(0, size)), 
kMetricsMemoryOrder);
+                } else {
+                    metrics->async_failed.fetch_add(1, kMetricsMemoryOrder);
+                }
+                RecordLatency(ElapsedMicros(start), 
&metrics->async_latency_count,
+                              &metrics->async_latency_sum_us);
+                callback(status);
+            });
+    }
+
+    Status Close() override {
+        return stream_->Close();
+    }
+
+    Result<std::string> GetUri() const override {
+        return stream_->GetUri();
+    }
+
+    Result<int64_t> Length() const override {
+        return stream_->Length();
+    }
+
+ private:
+    template <typename ReadFunction>
+    Result<int64_t> RecordRead(ReadFunction&& read, int64_t size) {
+        metrics_->read_requests.fetch_add(1, kMetricsMemoryOrder);
+        
metrics_->read_requested_bytes.fetch_add(static_cast<uint64_t>(std::max<int64_t>(0,
 size)),
+                                                 kMetricsMemoryOrder);
+        const auto start = std::chrono::steady_clock::now();
+        Result<int64_t> result = read();
+        if (result.ok()) {
+            metrics_->read_physical_bytes.fetch_add(
+                static_cast<uint64_t>(std::max<int64_t>(0, result.value())), 
kMetricsMemoryOrder);
+        } else {
+            metrics_->read_failed.fetch_add(1, kMetricsMemoryOrder);
+        }
+        RecordLatency(ElapsedMicros(start), &metrics_->read_latency_count,
+                      &metrics_->read_latency_sum_us);
+        return result;
+    }
+
+    std::shared_ptr<InputStream> stream_;
+    std::shared_ptr<PrefetchIoMetricsState> metrics_;
+};
+
 std::pair<int64_t, int64_t> ComputeBatchSliceByReadRange(
     const std::vector<uint64_t>& global_row_ids, const std::pair<uint64_t, 
uint64_t>& read_range) {
     auto begin_it =
@@ -62,7 +204,7 @@ Result<std::unique_ptr<PrefetchFileBatchReaderImpl>> 
PrefetchFileBatchReaderImpl
     const std::shared_ptr<FileSystem>& fs, uint32_t prefetch_max_parallel_num, 
int32_t batch_size,
     uint32_t prefetch_batch_count, bool enable_adaptive_prefetch_strategy,
     const std::shared_ptr<Executor>& executor, bool initialize_read_ranges,
-    bool read_ahead_cache_enabled, const CacheConfig& cache_config,
+    bool read_ahead_cache_enabled, const CacheConfig& cache_config, bool 
enable_io_metrics,
     const std::shared_ptr<MemoryPool>& pool) {
     if (prefetch_max_parallel_num == 0) {
         return Status::Invalid("prefetch max parallel num should be greater 
than 0.");
@@ -83,24 +225,35 @@ Result<std::unique_ptr<PrefetchFileBatchReaderImpl>> 
PrefetchFileBatchReaderImpl
         return Status::Invalid("executor should not be nullptr.");
     }
 
+    std::shared_ptr<PrefetchIoMetricsState> io_metrics;
+    if (enable_io_metrics) {
+        io_metrics = std::make_shared<PrefetchIoMetricsState>();
+    }
     std::shared_ptr<ReadAheadCache> cache;
     if (read_ahead_cache_enabled) {
         PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> input_stream,
                                fs->Open(FileStatus(data_file_path, 
data_file_size)));
+        if (io_metrics) {
+            input_stream = std::make_shared<MetricsInputStream>(input_stream, 
io_metrics);
+        }
         cache = std::make_shared<ReadAheadCache>(input_stream, cache_config, 
pool);
     }
     std::vector<std::future<Result<std::unique_ptr<FileBatchReader>>>> futures;
     for (uint32_t i = 0; i < prefetch_max_parallel_num; i++) {
-        futures.push_back(
-            Via(executor.get(),
-                [&fs, &data_file_path, data_file_size, &reader_builder,
-                 &cache]() -> Result<std::unique_ptr<FileBatchReader>> {
-                    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> 
input_stream,
-                                           fs->Open(FileStatus(data_file_path, 
data_file_size)));
-                    auto cache_input_stream =
-                        
std::make_shared<CacheInputStream>(std::move(input_stream), cache);
-                    return reader_builder->Build(cache_input_stream);
-                }));
+        futures.push_back(Via(
+            executor.get(),
+            [&fs, &data_file_path, data_file_size, &reader_builder, &cache,
+             io_metrics]() -> Result<std::unique_ptr<FileBatchReader>> {
+                PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> 
input_stream,
+                                       fs->Open(FileStatus(data_file_path, 
data_file_size)));
+                if (io_metrics) {
+                    input_stream =
+                        
std::make_unique<MetricsInputStream>(std::move(input_stream), io_metrics);
+                }
+                auto cache_input_stream =
+                    
std::make_shared<CacheInputStream>(std::move(input_stream), cache);
+                return reader_builder->Build(cache_input_stream);
+            }));
     }
     std::vector<std::shared_ptr<PrefetchFileBatchReader>> readers;
     for (auto& file_batch_reader : CollectAll(futures)) {
@@ -121,9 +274,9 @@ Result<std::unique_ptr<PrefetchFileBatchReaderImpl>> 
PrefetchFileBatchReaderImpl
     }
     uint32_t prefetch_queue_capacity = prefetch_batch_count / readers.size();
 
-    auto reader = std::unique_ptr<PrefetchFileBatchReaderImpl>(
-        new PrefetchFileBatchReaderImpl(readers, batch_size, 
prefetch_queue_capacity,
-                                        enable_adaptive_prefetch_strategy, 
executor, cache, pool));
+    auto reader = std::unique_ptr<PrefetchFileBatchReaderImpl>(new 
PrefetchFileBatchReaderImpl(
+        readers, batch_size, prefetch_queue_capacity, 
enable_adaptive_prefetch_strategy, executor,
+        cache, io_metrics, pool));
     if (initialize_read_ranges) {
         // normally initialize read ranges should be false, as set read schema 
will refresh read
         // ranges, and set read schema will always be called before read.
@@ -136,6 +289,7 @@ PrefetchFileBatchReaderImpl::PrefetchFileBatchReaderImpl(
     const std::vector<std::shared_ptr<PrefetchFileBatchReader>>& readers, 
int32_t batch_size,
     uint32_t prefetch_queue_capacity, bool enable_adaptive_prefetch_strategy,
     const std::shared_ptr<Executor>& executor, const 
std::shared_ptr<ReadAheadCache>& cache,
+    const std::shared_ptr<PrefetchIoMetricsState>& io_metrics,
     const std::shared_ptr<MemoryPool>& pool)
     : readers_(std::move(readers)),
       batch_size_(batch_size),
@@ -143,7 +297,9 @@ PrefetchFileBatchReaderImpl::PrefetchFileBatchReaderImpl(
       cache_(cache),
       arrow_pool_(GetArrowPool(pool)),
       prefetch_queue_capacity_(prefetch_queue_capacity),
-      enable_adaptive_prefetch_strategy_(enable_adaptive_prefetch_strategy) {
+      enable_adaptive_prefetch_strategy_(enable_adaptive_prefetch_strategy),
+      prefetch_metrics_(std::make_shared<PrefetchMetricsState>()),
+      io_metrics_(io_metrics) {
     for (size_t i = 0; i < readers_.size(); i++) {
         
prefetch_queues_.emplace_back(std::make_unique<ThreadsafeQueue<PrefetchBatch>>());
         readers_pos_.emplace_back(std::make_unique<std::atomic<uint64_t>>(0));
@@ -186,6 +342,7 @@ Status PrefetchFileBatchReaderImpl::RefreshReadRanges() {
 Status PrefetchFileBatchReaderImpl::RefreshReadRangesAfterCleanUp() {
     bool need_prefetch;
     PAIMON_ASSIGN_OR_RAISE(auto read_ranges, 
readers_[0]->GenReadRanges(&need_prefetch));
+    const bool format_requested_prefetch = need_prefetch;
 
     if (!enable_adaptive_prefetch_strategy_) {
         need_prefetch = true;
@@ -197,8 +354,17 @@ Status 
PrefetchFileBatchReaderImpl::RefreshReadRangesAfterCleanUp() {
         }
     }
 
+    if (format_requested_prefetch && !need_prefetch) {
+        prefetch_metrics_->adaptive_disabled_count.fetch_add(1, 
kMetricsMemoryOrder);
+    }
     need_prefetch_ = need_prefetch;
-    PAIMON_RETURN_NOT_OK(SetReadRanges(FilterReadRanges(read_ranges, 
selection_bitmap_)));
+    prefetch_metrics_->enabled.store(need_prefetch_, kMetricsMemoryOrder);
+    prefetch_metrics_->read_ranges_total.fetch_add(read_ranges.size(), 
kMetricsMemoryOrder);
+    std::vector<std::pair<uint64_t, uint64_t>> filtered_ranges =
+        FilterReadRanges(read_ranges, selection_bitmap_);
+    
prefetch_metrics_->read_ranges_after_bitmap.fetch_add(filtered_ranges.size(),
+                                                          kMetricsMemoryOrder);
+    PAIMON_RETURN_NOT_OK(SetReadRanges(filtered_ranges));
     return Status::OK();
 }
 
@@ -272,6 +438,7 @@ Status PrefetchFileBatchReaderImpl::CleanUp() {
                 if (batch == std::nullopt) {
                     break;
                 }
+                prefetch_metrics_->discarded_batches.fetch_add(1, 
kMetricsMemoryOrder);
                 
ReaderUtils::ReleaseReadBatch(std::move(batch.value().batch.first));
             }
         }
@@ -297,6 +464,7 @@ Status PrefetchFileBatchReaderImpl::CleanUp() {
     current_batch_global_row_ids_.clear();
     read_ranges_freshed_ = false;
     clean_prefetch_queue();
+    prefetch_metrics_->queue_depth.store(0, kMetricsMemoryOrder);
     for (size_t i = 0; i < readers_pos_.size(); i++) {
         readers_pos_[i]->store(0);
         reader_is_working_[i] = false;
@@ -357,6 +525,7 @@ void PrefetchFileBatchReaderImpl::Workloop() {
                 }
                 if (prefetch_queues_[reader_idx]->size() >= 
prefetch_queue_capacity_) {
                     // queue is full, skip
+                    prefetch_metrics_->queue_full_count.fetch_add(1, 
kMetricsMemoryOrder);
                     continue;
                 }
                 if (readers_pos_[reader_idx]->load() != 
std::numeric_limits<uint64_t>::max()) {
@@ -417,6 +586,7 @@ Status PrefetchFileBatchReaderImpl::EnsureReaderPosition(
     uint64_t pos = std::max(readers_pos_[reader_idx]->load(), 
current_read_range.first);
     PAIMON_ASSIGN_OR_RAISE(uint64_t next_row_to_read, 
readers_[reader_idx]->GetNextRowToRead());
     if (next_row_to_read != pos) {
+        prefetch_metrics_->seek_count.fetch_add(1, kMetricsMemoryOrder);
         return readers_[reader_idx]->SeekToRow(pos);
     }
     return Status::OK();
@@ -447,6 +617,7 @@ Status PrefetchFileBatchReaderImpl::HandleReadResult(
             global_row_ids.push_back(global_row_id);
         }
         if (global_row_ids.empty()) {
+            prefetch_metrics_->discarded_batches.fetch_add(1, 
kMetricsMemoryOrder);
             ReaderUtils::ReleaseReadBatch(std::move(read_batch));
             return Status::OK();
         }
@@ -464,6 +635,7 @@ Status PrefetchFileBatchReaderImpl::HandleReadResult(
                 FindReadRangeContaining(reader_idx, global_row_ids[0]);
             if (owner_range == std::nullopt) {
                 readers_pos_[reader_idx]->store(global_row_ids[0]);
+                prefetch_metrics_->discarded_batches.fetch_add(1, 
kMetricsMemoryOrder);
                 ReaderUtils::ReleaseReadBatch(std::move(read_batch));
                 return Status::OK();
             }
@@ -493,15 +665,23 @@ Status PrefetchFileBatchReaderImpl::HandleReadResult(
             readers_pos_[reader_idx]->store(next_row_to_read);
         }
         if (bitmap.IsEmpty()) {
+            prefetch_metrics_->discarded_batches.fetch_add(1, 
kMetricsMemoryOrder);
             ReaderUtils::ReleaseReadBatch(std::move(read_batch));
             return Status::OK();
         }
         prefetch_queue->push(
             {read_range, std::move(read_batch_with_bitmap), 
std::move(global_row_ids)});
+        prefetch_metrics_->produced_batches.fetch_add(1, kMetricsMemoryOrder);
+        const uint64_t queue_depth =
+            prefetch_metrics_->queue_depth.fetch_add(1, kMetricsMemoryOrder) + 
1;
+        UpdateMax(&prefetch_metrics_->queue_depth_max, queue_depth);
     } else {
         std::pair<uint64_t, uint64_t> eof_range;
         PAIMON_ASSIGN_OR_RAISE(eof_range, EofRange());
         prefetch_queue->push({eof_range, std::move(read_batch_with_bitmap), 
{}});
+        const uint64_t queue_depth =
+            prefetch_metrics_->queue_depth.fetch_add(1, kMetricsMemoryOrder) + 
1;
+        UpdateMax(&prefetch_metrics_->queue_depth_max, queue_depth);
         readers_pos_[reader_idx]->store(std::numeric_limits<uint64_t>::max());
     }
     return Status::OK();
@@ -532,8 +712,11 @@ Status PrefetchFileBatchReaderImpl::DoReadBatch(size_t 
reader_idx) {
     FileBatchReader* reader = readers_[reader_idx].get();
     PAIMON_RETURN_NOT_OK(EnsureReaderPosition(reader_idx, read_range));
 
-    PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap read_batch_with_bitmap,
-                           reader->NextBatchWithBitmap());
+    const auto read_start = std::chrono::steady_clock::now();
+    Result<ReadBatchWithBitmap> read_result = reader->NextBatchWithBitmap();
+    
prefetch_metrics_->histograms->ObserveHistogram(PrefetchMetrics::READER_READ_LATENCY_US,
+                                                    ElapsedMicros(read_start));
+    PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap read_batch_with_bitmap, 
std::move(read_result));
 
     return HandleReadResult(reader_idx, read_range, 
std::move(read_batch_with_bitmap));
 }
@@ -547,6 +730,7 @@ Result<BatchReader::ReadBatchWithBitmap> 
PrefetchFileBatchReaderImpl::NextBatchW
             
std::make_unique<std::thread>(&PrefetchFileBatchReaderImpl::Workloop, this);
     }
 
+    const auto wait_start = std::chrono::steady_clock::now();
     while (true) {
         PAIMON_RETURN_NOT_OK(GetReadStatus());
         if (is_shutdown_) {
@@ -585,6 +769,10 @@ Result<BatchReader::ReadBatchWithBitmap> 
PrefetchFileBatchReaderImpl::NextBatchW
                     cv_.notify_one();
                 }
                 current_batch_global_row_ids_ = 
std::move(prefetch_batch.value().global_row_ids);
+                prefetch_metrics_->consumed_batches.fetch_add(1, 
kMetricsMemoryOrder);
+                prefetch_metrics_->queue_depth.fetch_sub(1, 
kMetricsMemoryOrder);
+                prefetch_metrics_->histograms->ObserveHistogram(
+                    PrefetchMetrics::CONSUMER_WAIT_LATENCY_US, 
ElapsedMicros(wait_start));
                 return std::move(prefetch_batch).value().batch;
             }
         }
@@ -595,6 +783,8 @@ Result<BatchReader::ReadBatchWithBitmap> 
PrefetchFileBatchReaderImpl::NextBatchW
                 return Status::Invalid("peek batch not suppose to be nullptr");
             }
             current_batch_global_row_ids_.clear();
+            prefetch_metrics_->histograms->ObserveHistogram(
+                PrefetchMetrics::CONSUMER_WAIT_LATENCY_US, 
ElapsedMicros(wait_start));
             return BatchReader::MakeEofBatchWithBitmap();
         }
         if (value_count == prefetch_queues_.size()) {
@@ -620,15 +810,69 @@ Status PrefetchFileBatchReaderImpl::SeekToRow(uint64_t 
row_number) {
 }
 
 std::shared_ptr<Metrics> PrefetchFileBatchReaderImpl::GetReaderMetrics() const 
{
-    auto res_metrics = MetricsImpl::CollectReadMetrics(readers_);
+    auto result = std::make_shared<MetricsImpl>();
+    if (need_prefetch_) {
+        result->Merge(MetricsImpl::CollectReadMetrics(readers_));
+    } else if (!readers_.empty()) {
+        result->Merge(readers_[0]->GetReaderMetrics());
+    }
+
+    auto set_prefetch_counter = [&result](const char* name, const 
std::atomic<uint64_t>& value) {
+        result->SetCounter(name, value.load(kMetricsMemoryOrder));
+    };
+    set_prefetch_counter(PrefetchMetrics::READ_RANGES_TOTAL, 
prefetch_metrics_->read_ranges_total);
+    set_prefetch_counter(PrefetchMetrics::READ_RANGES_AFTER_BITMAP,
+                         prefetch_metrics_->read_ranges_after_bitmap);
+    set_prefetch_counter(PrefetchMetrics::SEEK_COUNT, 
prefetch_metrics_->seek_count);
+    set_prefetch_counter(PrefetchMetrics::PRODUCED_BATCHES, 
prefetch_metrics_->produced_batches);
+    set_prefetch_counter(PrefetchMetrics::CONSUMED_BATCHES, 
prefetch_metrics_->consumed_batches);
+    set_prefetch_counter(PrefetchMetrics::DISCARDED_BATCHES, 
prefetch_metrics_->discarded_batches);
+    set_prefetch_counter(PrefetchMetrics::ERRORS, prefetch_metrics_->errors);
+    set_prefetch_counter(PrefetchMetrics::ADAPTIVE_DISABLED_COUNT,
+                         prefetch_metrics_->adaptive_disabled_count);
+    set_prefetch_counter(PrefetchMetrics::QUEUE_FULL_COUNT, 
prefetch_metrics_->queue_full_count);
+    result->SetGauge(PrefetchMetrics::ENABLED,
+                     prefetch_metrics_->enabled.load(kMetricsMemoryOrder) ? 
1.0 : 0.0);
+    result->SetGauge(PrefetchMetrics::PARALLELISM,
+                     prefetch_metrics_->enabled.load(kMetricsMemoryOrder)
+                         ? static_cast<double>(parallel_num_)
+                         : 1.0);
+    result->SetGauge(PrefetchMetrics::QUEUE_DEPTH,
+                     
static_cast<double>(prefetch_metrics_->queue_depth.load(kMetricsMemoryOrder)));
+    result->SetGauge(
+        PrefetchMetrics::QUEUE_DEPTH_MAX,
+        
static_cast<double>(prefetch_metrics_->queue_depth_max.load(kMetricsMemoryOrder)));
+    result->Merge(prefetch_metrics_->histograms);
     if (cache_) {
-        // The shared read-ahead cache serves reads of all sub-readers, so its
-        // hit/miss counters are file-level and merge into the reader metrics.
+        // PR #209 owns the read-ahead cache metrics. Keep collecting its 
file-level
+        // hit/miss counters without defining another C++ metrics surface here.
         std::shared_ptr<Metrics> cache_metrics = 
std::make_shared<MetricsImpl>();
         cache_->CollectMetrics(&cache_metrics);
-        res_metrics->Merge(cache_metrics);
+        result->Merge(cache_metrics);
     }
-    return res_metrics;
+
+    if (!io_metrics_) {
+        return result;
+    }
+    auto set_io_counter = [&result](const char* name, const 
std::atomic<uint64_t>& value) {
+        result->SetCounter(name, value.load(kMetricsMemoryOrder));
+    };
+    set_io_counter(PrefetchIoMetrics::READ_REQUESTS, 
io_metrics_->read_requests);
+    set_io_counter(PrefetchIoMetrics::READ_REQUESTED_BYTES, 
io_metrics_->read_requested_bytes);
+    set_io_counter(PrefetchIoMetrics::READ_PHYSICAL_BYTES, 
io_metrics_->read_physical_bytes);
+    set_io_counter(PrefetchIoMetrics::READ_FAILED, io_metrics_->read_failed);
+    set_io_counter(PrefetchIoMetrics::READ_LATENCY_COUNT, 
io_metrics_->read_latency_count);
+    set_io_counter(PrefetchIoMetrics::READ_LATENCY_SUM_US, 
io_metrics_->read_latency_sum_us);
+    set_io_counter(PrefetchIoMetrics::ASYNC_REQUESTS, 
io_metrics_->async_requests);
+    set_io_counter(PrefetchIoMetrics::ASYNC_REQUESTED_BYTES, 
io_metrics_->async_requested_bytes);
+    set_io_counter(PrefetchIoMetrics::ASYNC_PHYSICAL_BYTES, 
io_metrics_->async_physical_bytes);
+    set_io_counter(PrefetchIoMetrics::ASYNC_COMPLETED, 
io_metrics_->async_completed);
+    set_io_counter(PrefetchIoMetrics::ASYNC_FAILED, io_metrics_->async_failed);
+    set_io_counter(PrefetchIoMetrics::ASYNC_LATENCY_COUNT, 
io_metrics_->async_latency_count);
+    set_io_counter(PrefetchIoMetrics::ASYNC_LATENCY_SUM_US, 
io_metrics_->async_latency_sum_us);
+    result->SetGauge(PrefetchIoMetrics::ASYNC_PENDING,
+                     
static_cast<double>(io_metrics_->async_pending.load(kMetricsMemoryOrder)));
+    return result;
 }
 
 Result<std::unique_ptr<::ArrowSchema>> 
PrefetchFileBatchReaderImpl::GetFileSchema() const {
@@ -662,6 +906,9 @@ Result<uint64_t> 
PrefetchFileBatchReaderImpl::GetNextRowToRead() const {
 }
 
 void PrefetchFileBatchReaderImpl::SetReadStatus(const Status& status) {
+    if (!status.ok()) {
+        prefetch_metrics_->errors.fetch_add(1, kMetricsMemoryOrder);
+    }
     std::unique_lock<std::shared_mutex> lock(rw_mutex_);
     read_status_ = status;
 }
diff --git a/src/paimon/common/reader/prefetch_file_batch_reader_impl.h 
b/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
index 08d3bd37..c9932369 100644
--- a/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
+++ b/src/paimon/common/reader/prefetch_file_batch_reader_impl.h
@@ -57,6 +57,8 @@ class FileSystem;
 class Executor;
 class Predicate;
 class Metrics;
+struct PrefetchMetricsState;
+struct PrefetchIoMetricsState;
 
 class PrefetchFileBatchReaderImpl : public PrefetchFileBatchReader {
  public:
@@ -66,7 +68,7 @@ class PrefetchFileBatchReaderImpl : public 
PrefetchFileBatchReader {
         uint32_t prefetch_max_parallel_num, int32_t batch_size, uint32_t 
prefetch_batch_count,
         bool enable_adaptive_prefetch_strategy, const 
std::shared_ptr<Executor>& executor,
         bool initialize_read_ranges, bool read_ahead_cache_enabled, const 
CacheConfig& cache_config,
-        const std::shared_ptr<MemoryPool>& pool);
+        bool enable_io_metrics, const std::shared_ptr<MemoryPool>& pool);
 
     ~PrefetchFileBatchReaderImpl() override;
 
@@ -119,6 +121,7 @@ class PrefetchFileBatchReaderImpl : public 
PrefetchFileBatchReader {
         const std::vector<std::shared_ptr<PrefetchFileBatchReader>>& readers, 
int32_t batch_size,
         uint32_t prefetch_queue_capacity, bool 
enable_adaptive_prefetch_strategy,
         const std::shared_ptr<Executor>& executor, const 
std::shared_ptr<ReadAheadCache>& cache,
+        const std::shared_ptr<PrefetchIoMetricsState>& io_metrics,
         const std::shared_ptr<MemoryPool>& pool);
 
     Status CleanUp();
@@ -178,5 +181,7 @@ class PrefetchFileBatchReaderImpl : public 
PrefetchFileBatchReader {
     const uint32_t prefetch_queue_capacity_;
     const bool enable_adaptive_prefetch_strategy_;
     int32_t parallel_num_;
+    std::shared_ptr<PrefetchMetricsState> prefetch_metrics_;
+    std::shared_ptr<PrefetchIoMetricsState> io_metrics_;
 };
 }  // namespace paimon
diff --git a/src/paimon/common/reader/prefetch_file_batch_reader_impl_test.cpp 
b/src/paimon/common/reader/prefetch_file_batch_reader_impl_test.cpp
index 192028ac..514e7e49 100644
--- a/src/paimon/common/reader/prefetch_file_batch_reader_impl_test.cpp
+++ b/src/paimon/common/reader/prefetch_file_batch_reader_impl_test.cpp
@@ -34,6 +34,7 @@
 #include "paimon/format/format_writer.h"
 #include "paimon/fs/file_system_factory.h"
 #include "paimon/fs/local/local_file_system.h"
+#include "paimon/metrics.h"
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/testing/mock/mock_file_batch_reader.h"
 #include "paimon/testing/mock/mock_file_system.h"
@@ -113,6 +114,62 @@ class ControlledMockFormatReaderBuilder : public 
ReaderBuilder {
     mutable std::atomic<size_t> build_count_{0};
 };
 
+class FailingInputStream : public MockInputStream {
+ public:
+    Result<int64_t> Read(char* buffer, int64_t size, int64_t offset) override {
+        return Status::IOError("injected synchronous read failure");
+    }
+};
+
+class FailingFileSystem : public MockFileSystem {
+ public:
+    Result<std::unique_ptr<InputStream>> Open(const std::string& path) const 
override {
+        return std::make_unique<FailingInputStream>();
+    }
+};
+
+class IoReadingMockFileBatchReader : public MockFileBatchReader {
+ public:
+    IoReadingMockFileBatchReader(const std::shared_ptr<arrow::Array>& data,
+                                 const std::shared_ptr<arrow::DataType>& 
schema,
+                                 int32_t read_batch_size,
+                                 const std::shared_ptr<InputStream>& 
input_stream)
+        : MockFileBatchReader(data, schema, read_batch_size), 
input_stream_(input_stream) {}
+
+    Result<ReadBatchWithBitmap> NextBatchWithBitmap() override {
+        char value = 0;
+        PAIMON_ASSIGN_OR_RAISE(int64_t read_size, input_stream_->Read(&value, 
1, 0));
+        (void)read_size;
+        return MockFileBatchReader::NextBatchWithBitmap();
+    }
+
+ private:
+    std::shared_ptr<InputStream> input_stream_;
+};
+
+class IoReadingMockFormatReaderBuilder : public ReaderBuilder {
+ public:
+    IoReadingMockFormatReaderBuilder(const std::shared_ptr<arrow::Array>& data,
+                                     const std::shared_ptr<arrow::DataType>& 
schema,
+                                     int32_t read_batch_size)
+        : data_(data), schema_(schema), read_batch_size_(read_batch_size) {}
+
+    ReaderBuilder* WithMemoryPool(const std::shared_ptr<MemoryPool>& pool) 
override {
+        return this;
+    }
+
+    Result<std::unique_ptr<FileBatchReader>> Build(
+        const std::shared_ptr<InputStream>& input_stream) const override {
+        return std::make_unique<IoReadingMockFileBatchReader>(data_, schema_, 
read_batch_size_,
+                                                              input_stream);
+    }
+
+ private:
+    std::shared_ptr<arrow::Array> data_;
+    std::shared_ptr<arrow::DataType> schema_;
+    int32_t read_batch_size_ = 0;
+};
+
 struct TestParam {
     std::string file_format;
     bool read_ahead_cache_enabled;
@@ -210,7 +267,7 @@ class PrefetchFileBatchReaderImplTest : public 
::testing::Test,
                 prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                 /*enable_adaptive_prefetch_strategy=*/false, executor,
                 /*initialize_read_ranges=*/false, read_ahead_cache_enabled, 
CacheConfig(),
-                GetDefaultPool()));
+                /*enable_io_metrics=*/true, GetDefaultPool()));
         std::unique_ptr<ArrowSchema> c_schema = 
std::make_unique<ArrowSchema>();
         auto arrow_status = arrow::ExportSchema(*read_schema, c_schema.get());
         EXPECT_TRUE(arrow_status.ok());
@@ -299,7 +356,11 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestSimple) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
+        if (prefetch_max_parallel_num == 1) {
+            ASSERT_NOK(
+                
reader->GetReaderMetrics()->GetCounter(PrefetchIoMetrics::READ_LATENCY_COUNT));
+        }
         ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
         ASSERT_OK_AND_ASSIGN(auto array_and_row_ids, 
CollectResultAndRowIds(reader.get()));
 
@@ -322,7 +383,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestReadWithLimits) 
{
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/true, 
GetDefaultPool()));
     // simulate read limits, only read 8 batches
     for (int32_t i = 0; i < 8; i++) {
         ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatchWithBitmap 
batch_with_bitmap,
@@ -337,6 +398,30 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestReadWithLimits) {
     // test metrics
     auto read_metrics = reader->GetReaderMetrics();
     ASSERT_TRUE(read_metrics);
+    ASSERT_OK_AND_ASSIGN(double prefetch_enabled, 
read_metrics->GetGauge(PrefetchMetrics::ENABLED));
+    ASSERT_OK_AND_ASSIGN(uint64_t produced_batches,
+                         
read_metrics->GetCounter(PrefetchMetrics::PRODUCED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t consumed_batches,
+                         
read_metrics->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t read_ranges,
+                         
read_metrics->GetCounter(PrefetchMetrics::READ_RANGES_TOTAL));
+    ASSERT_EQ(prefetch_enabled, 1.0);
+    ASSERT_GT(produced_batches, 0);
+    ASSERT_EQ(consumed_batches, 8);
+    ASSERT_GT(read_ranges, 0);
+    ASSERT_OK(read_metrics->GetGauge(PrefetchMetrics::QUEUE_DEPTH));
+    ASSERT_OK_AND_ASSIGN(uint64_t async_completed,
+                         
read_metrics->GetCounter(PrefetchIoMetrics::ASYNC_COMPLETED));
+    ASSERT_OK_AND_ASSIGN(uint64_t async_failed,
+                         
read_metrics->GetCounter(PrefetchIoMetrics::ASYNC_FAILED));
+    ASSERT_OK_AND_ASSIGN(uint64_t async_latency_count,
+                         
read_metrics->GetCounter(PrefetchIoMetrics::ASYNC_LATENCY_COUNT));
+    ASSERT_EQ(async_latency_count, async_completed + async_failed);
+
+    std::shared_ptr<Metrics> second_metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t second_consumed_batches,
+                         
second_metrics->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_EQ(second_consumed_batches, consumed_batches);
 }
 
 TEST_F(PrefetchFileBatchReaderImplTest, TestReadWithoutInitializeReadRanges) {
@@ -351,13 +436,43 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestReadWithoutInitializeReadRanges) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     // simulate read limits, only read 8 batches
     ASSERT_NOK_WITH_MSG(reader->NextBatchWithBitmap(),
                         "prefetch reader read ranges are not initialized");
     reader->Close();
 }
 
+TEST_F(PrefetchFileBatchReaderImplTest, TestFailedIoMetrics) {
+    auto data_array = PrepareArray(10);
+    IoReadingMockFormatReaderBuilder reader_builder(data_array, data_type_, 
/*read_batch_size=*/10);
+    auto failing_fs = std::make_shared<FailingFileSystem>();
+    ASSERT_OK_AND_ASSIGN(
+        auto reader,
+        PrefetchFileBatchReaderImpl::Create(
+            /*data_file_path=*/"", /*data_file_size=*/0, &reader_builder, 
failing_fs,
+            /*prefetch_max_parallel_num=*/1, /*batch_size=*/10,
+            /*prefetch_batch_count=*/2, 
/*enable_adaptive_prefetch_strategy=*/false, executor_,
+            /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/false, CacheConfig(),
+            /*enable_io_metrics=*/true, GetDefaultPool()));
+
+    ASSERT_NOK_WITH_MSG(reader->NextBatchWithBitmap(), "injected synchronous 
read failure");
+    std::shared_ptr<Metrics> metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t read_requests,
+                         
metrics->GetCounter(PrefetchIoMetrics::READ_REQUESTS));
+    ASSERT_OK_AND_ASSIGN(uint64_t failed_requests,
+                         metrics->GetCounter(PrefetchIoMetrics::READ_FAILED));
+    ASSERT_OK_AND_ASSIGN(uint64_t physical_bytes,
+                         
metrics->GetCounter(PrefetchIoMetrics::READ_PHYSICAL_BYTES));
+    ASSERT_EQ(read_requests, 1);
+    ASSERT_EQ(failed_requests, 1);
+    ASSERT_EQ(physical_bytes, 0);
+    ASSERT_OK_AND_ASSIGN(uint64_t latency_count,
+                         
metrics->GetCounter(PrefetchIoMetrics::READ_LATENCY_COUNT));
+    ASSERT_OK(metrics->GetCounter(PrefetchIoMetrics::READ_LATENCY_SUM_US));
+    ASSERT_EQ(latency_count, read_requests);
+}
+
 TEST_F(PrefetchFileBatchReaderImplTest, FilterReadRangesWithoutBitmap) {
     std::vector<std::pair<uint64_t, uint64_t>> read_ranges = {
         {0, 1000},    {1000, 2000}, {2000, 3000}, {3000, 4000}, {4000, 5000},
@@ -427,7 +542,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, RefreshReadRanges) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     ASSERT_OK(prefetch_reader->RefreshReadRanges());
     std::vector<std::pair<uint64_t, uint64_t>> read_ranges_0 = {{0, 30}, {90, 
101}};
@@ -457,9 +572,17 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
RefreshReadRangesDisablePrefetchByAdapti
                              /*prefetch_batch_count=*/2,
                              /*enable_adaptive_prefetch_strategy=*/true, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 
     ASSERT_FALSE(reader->NeedPrefetch());
+    std::shared_ptr<Metrics> metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(double enabled, 
metrics->GetGauge(PrefetchMetrics::ENABLED));
+    ASSERT_OK_AND_ASSIGN(double parallelism, 
metrics->GetGauge(PrefetchMetrics::PARALLELISM));
+    ASSERT_OK_AND_ASSIGN(uint64_t adaptive_disabled_count,
+                         
metrics->GetCounter(PrefetchMetrics::ADAPTIVE_DISABLED_COUNT));
+    ASSERT_EQ(enabled, 0.0);
+    ASSERT_EQ(parallelism, 1.0);
+    ASSERT_EQ(adaptive_disabled_count, 1);
 }
 
 TEST_F(PrefetchFileBatchReaderImplTest, SetReadRanges) {
@@ -473,7 +596,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, SetReadRanges) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     ASSERT_FALSE(prefetch_reader->need_prefetch_);
     prefetch_reader->need_prefetch_ = true;
@@ -516,7 +639,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
SetReadRangesReturnErrorWhenPushDownFail
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     prefetch_reader->need_prefetch_ = true;
@@ -542,7 +665,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
WorkloopSetReadStatusWhenCacheInitFailed
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             invalid_cache_config, GetDefaultPool()));
+                             invalid_cache_config, 
/*enable_io_metrics=*/false, GetDefaultPool()));
 
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     prefetch_reader->Workloop();
@@ -562,7 +685,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
DoReadBatchReturnOkWhenShutdown) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     prefetch_reader->is_shutdown_ = true;
@@ -580,7 +703,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
DoReadBatchReturnOkWhenNoCurrentReadRang
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/false, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     prefetch_reader->read_ranges_in_group_ = {{}};
@@ -598,7 +721,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestReadWithLargeBatchSize) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
     ASSERT_OK_AND_ASSIGN(auto array_and_row_ids, 
CollectResultAndRowIds(reader.get()));
     auto row_ids = array_and_row_ids.second;
@@ -618,7 +741,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestPartialReaderSuccessRead) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     for (int32_t i = 0; i < prefetch_max_parallel_num; i++) {
         dynamic_cast<MockFileBatchReader*>(prefetch_reader->readers_[i].get())
@@ -663,7 +786,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestAllReaderFailedWithIOError) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 
     auto prefetch_reader = 
dynamic_cast<PrefetchFileBatchReaderImpl*>(reader.get());
     for (int32_t i = 0; i < prefetch_max_parallel_num; i++) {
@@ -679,6 +802,9 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestAllReaderFailedWithIOError) {
     ASSERT_FALSE(prefetch_reader->is_shutdown_);
     ASSERT_NOK(prefetch_reader->GetReadStatus());
     ASSERT_FALSE(HasValue(prefetch_reader->prefetch_queues_));
+    std::shared_ptr<Metrics> metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t prefetch_errors, 
metrics->GetCounter(PrefetchMetrics::ERRORS));
+    ASSERT_GT(prefetch_errors, 0);
 
     // call NextBatch again, will still return error status
     auto batch_result2 = reader->NextBatchWithBitmap();
@@ -698,7 +824,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestPrefetchWithEmptyData) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
     ASSERT_OK_AND_ASSIGN(auto array_and_row_ids, 
CollectResultAndRowIds(reader.get()));
     auto row_ids = array_and_row_ids.second;
@@ -717,7 +843,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestCallNextBatchAfterReadingEof) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
     ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
     ASSERT_OK_AND_ASSIGN(auto array_and_row_ids, 
CollectResultAndRowIds(reader.get()));
     auto row_ids = array_and_row_ids.second;
@@ -725,10 +851,28 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestCallNextBatchAfterReadingEof) {
     auto expected_array = std::make_shared<arrow::ChunkedArray>(data_array);
     ASSERT_TRUE(array_and_row_ids.first->Equals(expected_array));
 
+    std::shared_ptr<Metrics> eof_metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t produced_batches,
+                         
eof_metrics->GetCounter(PrefetchMetrics::PRODUCED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t consumed_batches,
+                         
eof_metrics->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t discarded_batches,
+                         
eof_metrics->GetCounter(PrefetchMetrics::DISCARDED_BATCHES));
+
     // continue to call NextBatch() after reading eof
     ASSERT_OK_AND_ASSIGN(auto batch_with_bitmap, 
reader->NextBatchWithBitmap());
     ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
     ASSERT_TRUE(BatchReader::IsEofBatch(batch_with_bitmap));
+    std::shared_ptr<Metrics> repeated_eof_metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t produced_batches_after,
+                         
repeated_eof_metrics->GetCounter(PrefetchMetrics::PRODUCED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t consumed_batches_after,
+                         
repeated_eof_metrics->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t discarded_batches_after,
+                         
repeated_eof_metrics->GetCounter(PrefetchMetrics::DISCARDED_BATCHES));
+    ASSERT_EQ(produced_batches_after, produced_batches);
+    ASSERT_EQ(consumed_batches_after, consumed_batches);
+    ASSERT_EQ(discarded_batches_after, discarded_batches);
 }
 
 TEST_F(PrefetchFileBatchReaderImplTest, TestCreateReaderWithoutNextBatch) {
@@ -742,7 +886,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestCreateReaderWithoutNextBatch) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
 }
 
 TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
@@ -757,7 +901,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
             /*prefetch_max_parallel_num=*/0, batch_size, 2,
             /*enable_adaptive_prefetch_strategy=*/false, executor_,
             /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     }
     {
         ASSERT_NOK(PrefetchFileBatchReaderImpl::Create(
@@ -765,7 +909,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
             prefetch_max_parallel_num, /*batch_size=*/-1, 
prefetch_max_parallel_num * 2,
             /*enable_adaptive_prefetch_strategy=*/false, executor_,
             /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     }
     {
         ASSERT_NOK(PrefetchFileBatchReaderImpl::Create(
@@ -773,7 +917,8 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
             prefetch_max_parallel_num, batch_size, prefetch_max_parallel_num * 
2,
             /*enable_adaptive_prefetch_strategy=*/false,
             /*executor=*/nullptr, /*initialize_read_ranges=*/true,
-            /*read_ahead_cache_enabled=*/true, CacheConfig(), 
GetDefaultPool()));
+            /*read_ahead_cache_enabled=*/true, CacheConfig(), 
/*enable_io_metrics=*/false,
+            GetDefaultPool()));
     }
     {
         ASSERT_NOK(PrefetchFileBatchReaderImpl::Create(
@@ -781,7 +926,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
             prefetch_max_parallel_num, batch_size, prefetch_max_parallel_num * 
2,
             /*enable_adaptive_prefetch_strategy=*/false, executor_,
             /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     }
     {
         ASSERT_NOK(PrefetchFileBatchReaderImpl::Create(
@@ -789,7 +934,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
             /*fs=*/nullptr, prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
             /*enable_adaptive_prefetch_strategy=*/false, executor_,
             /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true, CacheConfig(),
-            GetDefaultPool()));
+            /*enable_io_metrics=*/false, GetDefaultPool()));
     }
     {
         ASSERT_OK_AND_ASSIGN(
@@ -798,7 +943,7 @@ TEST_F(PrefetchFileBatchReaderImplTest, TestInvalidCase) {
                              prefetch_max_parallel_num, batch_size, 
prefetch_max_parallel_num * 2,
                              /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
                              /*initialize_read_ranges=*/true, 
/*read_ahead_cache_enabled=*/true,
-                             CacheConfig(), GetDefaultPool()));
+                             CacheConfig(), /*enable_io_metrics=*/false, 
GetDefaultPool()));
         ASSERT_NOK_WITH_MSG(reader->SeekToRow(/*row_number=*/101),
                             "not support seek to row for prefetch reader");
     }
@@ -834,6 +979,18 @@ TEST_P(PrefetchFileBatchReaderImplTest, 
TestPrefetchWithPredicatePushdownWithCom
     auto expected_array = 
std::make_shared<arrow::ChunkedArray>(expected_array_vector);
     ASSERT_TRUE(expected_array->Equals(array_and_row_ids.first));
     ASSERT_EQ(expected_row_ids, array_and_row_ids.second);
+
+    std::shared_ptr<Metrics> metrics = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t sync_requests,
+                         
metrics->GetCounter(PrefetchIoMetrics::READ_REQUESTS));
+    ASSERT_OK_AND_ASSIGN(uint64_t async_requests,
+                         
metrics->GetCounter(PrefetchIoMetrics::ASYNC_REQUESTS));
+    ASSERT_OK_AND_ASSIGN(uint64_t sync_physical_bytes,
+                         
metrics->GetCounter(PrefetchIoMetrics::READ_PHYSICAL_BYTES));
+    ASSERT_OK_AND_ASSIGN(uint64_t async_physical_bytes,
+                         
metrics->GetCounter(PrefetchIoMetrics::ASYNC_PHYSICAL_BYTES));
+    ASSERT_GT(sync_requests + async_requests, 0);
+    ASSERT_GT(sync_physical_bytes + async_physical_bytes, 0);
 }
 
 /// There are three stripes: [0,30), [30,60), [60,90). Each stripe has 3 row 
groups.
@@ -883,14 +1040,14 @@ TEST_F(PrefetchFileBatchReaderImplTest, 
TestPrefetchWithBitmap) {
     MockFormatReaderBuilder reader_builder(data_array, data_type_, bitmap,
                                            /*read_batch_size=*/100);
     int32_t prefetch_max_parallel_num = 3;
-    ASSERT_OK_AND_ASSIGN(auto reader,
-                         PrefetchFileBatchReaderImpl::Create(
-                             /*data_file_path=*/"", /*data_file_size=*/0, 
&reader_builder, mock_fs_,
-                             prefetch_max_parallel_num,
-                             /*batch_size=*/100, prefetch_max_parallel_num * 2,
-                             /*enable_adaptive_prefetch_strategy=*/false, 
executor_,
-                             /*initialize_read_ranges=*/true,
-                             /*read_ahead_cache_enabled=*/true, CacheConfig(), 
GetDefaultPool()));
+    ASSERT_OK_AND_ASSIGN(auto reader, PrefetchFileBatchReaderImpl::Create(
+                                          /*data_file_path=*/"", 
/*data_file_size=*/0,
+                                          &reader_builder, mock_fs_, 
prefetch_max_parallel_num,
+                                          /*batch_size=*/100, 
prefetch_max_parallel_num * 2,
+                                          
/*enable_adaptive_prefetch_strategy=*/false, executor_,
+                                          /*initialize_read_ranges=*/true,
+                                          /*read_ahead_cache_enabled=*/true, 
CacheConfig(),
+                                          /*enable_io_metrics=*/false, 
GetDefaultPool()));
     ASSERT_OK_AND_ASSIGN(auto result_chunk_array, 
ReadResultCollector::CollectResult(reader.get()));
 
     ASSERT_OK_AND_ASSIGN(auto data_batch, 
ReadResultCollector::GetReadBatch(data_array));
@@ -934,12 +1091,28 @@ TEST_P(PrefetchFileBatchReaderImplTest, TestRowMapping) {
         ASSERT_EQ(reader->GetPreviousBatchFileRowId(i).value(), 70 + i);
     }
 
+    std::shared_ptr<Metrics> metrics_before_schema = 
reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t consumed_before_schema,
+                         
metrics_before_schema->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t ranges_before_schema,
+                         
metrics_before_schema->GetCounter(PrefetchMetrics::READ_RANGES_TOTAL));
+
     // Set read schema again
     std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
     ASSERT_TRUE(arrow::ExportSchema(*schema, c_schema.get()).ok());
     predicate = PredicateBuilder::Between(/*field_index=*/1, 
/*field_name=*/"f1", FieldType::BIGINT,
                                           Literal(30l), Literal(49l));
     ASSERT_OK(reader->SetReadSchema(c_schema.get(), predicate, std::nullopt));
+    std::shared_ptr<Metrics> metrics_after_schema = reader->GetReaderMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t consumed_after_schema,
+                         
metrics_after_schema->GetCounter(PrefetchMetrics::CONSUMED_BATCHES));
+    ASSERT_OK_AND_ASSIGN(uint64_t ranges_after_schema,
+                         
metrics_after_schema->GetCounter(PrefetchMetrics::READ_RANGES_TOTAL));
+    ASSERT_OK_AND_ASSIGN(double queue_depth_after_schema,
+                         
metrics_after_schema->GetGauge(PrefetchMetrics::QUEUE_DEPTH));
+    ASSERT_EQ(consumed_after_schema, consumed_before_schema);
+    ASSERT_GT(ranges_after_schema, ranges_before_schema);
+    ASSERT_EQ(queue_depth_after_schema, 0.0);
 
     ASSERT_NOK(reader->GetPreviousBatchFileRowId(0));
     ASSERT_OK_AND_ASSIGN(batch,
diff --git a/src/paimon/core/core_options.cpp b/src/paimon/core/core_options.cpp
index a5b32b09..dfa0aa7f 100644
--- a/src/paimon/core/core_options.cpp
+++ b/src/paimon/core/core_options.cpp
@@ -421,6 +421,7 @@ struct CoreOptions::Impl {
     bool key_value_sequence_number_enabled = false;
     bool file_index_read_enabled = true;
     bool enable_adaptive_prefetch_strategy = true;
+    bool prefetch_io_metrics_enabled = false;
     bool index_file_in_data_file_dir = false;
     bool row_tracking_enabled = false;
     bool row_tracking_partition_group_on_commit = true;
@@ -779,6 +780,8 @@ struct CoreOptions::Impl {
         }
         
PAIMON_RETURN_NOT_OK(parser.Parse<bool>(Options::SCAN_MANIFEST_ENTRY_LAZY_DECODE_ENABLED,
                                                 
&scan_manifest_entry_lazy_decode_enabled));
+        PAIMON_RETURN_NOT_OK(
+            parser.Parse<bool>(Options::PREFETCH_IO_METRICS_ENABLED, 
&prefetch_io_metrics_enabled));
         // Parse scan.fallback-branch - fallback branch when partition not 
found
         PAIMON_RETURN_NOT_OK(parser.Parse(Options::SCAN_FALLBACK_BRANCH, 
&scan_fallback_branch));
         // Parse branch - branch name, default "main"
@@ -1376,6 +1379,10 @@ bool CoreOptions::EnableAdaptivePrefetchStrategy() const 
{
     return impl_->enable_adaptive_prefetch_strategy;
 }
 
+bool CoreOptions::PrefetchIoMetricsEnabled() const {
+    return impl_->prefetch_io_metrics_enabled;
+}
+
 Result<std::optional<std::string>> CoreOptions::GetFieldAggFunc(
     const std::string& field_name) const {
     ConfigParser parser(impl_->raw_options);
diff --git a/src/paimon/core/core_options.h b/src/paimon/core/core_options.h
index 85a4a7fd..f4e7964f 100644
--- a/src/paimon/core/core_options.h
+++ b/src/paimon/core/core_options.h
@@ -230,6 +230,8 @@ class PAIMON_EXPORT CoreOptions {
 
     std::string DataFilePrefix() const;
 
+    bool PrefetchIoMetricsEnabled() const;
+
     bool IndexFileInDataFileDir() const;
 
     bool RowTrackingEnabled() const;
diff --git a/src/paimon/core/core_options_test.cpp 
b/src/paimon/core/core_options_test.cpp
index c110623d..b0a3274b 100644
--- a/src/paimon/core/core_options_test.cpp
+++ b/src/paimon/core/core_options_test.cpp
@@ -142,6 +142,7 @@ TEST(CoreOptionsTest, TestDefaultValue) {
     ASSERT_EQ(std::nullopt, core_options.GetDataFileExternalPaths());
     ASSERT_EQ(ExternalPathStrategy::NONE, 
core_options.GetExternalPathStrategy());
     ASSERT_TRUE(core_options.EnableAdaptivePrefetchStrategy());
+    ASSERT_FALSE(core_options.PrefetchIoMetricsEnabled());
     ASSERT_EQ(core_options.DataFilePrefix(), "data-");
     ASSERT_FALSE(core_options.IndexFileInDataFileDir());
     ASSERT_FALSE(core_options.RowTrackingEnabled());
@@ -223,6 +224,7 @@ TEST(CoreOptionsTest, TestFromMap) {
         {Options::SCAN_MODE, "from-snapshot-full"},
         {Options::SCAN_MANIFEST_ENTRY_CACHE_MAX_SNAPSHOTS, "7"},
         {Options::SCAN_MANIFEST_ENTRY_LAZY_DECODE_ENABLED, "false"},
+        {Options::PREFETCH_IO_METRICS_ENABLED, "true"},
         {Options::SNAPSHOT_NUM_RETAINED_MIN, "15"},
         {Options::SNAPSHOT_NUM_RETAINED_MAX, "30"},
         {Options::SNAPSHOT_EXPIRE_LIMIT, "20"},
@@ -362,6 +364,7 @@ TEST(CoreOptionsTest, TestFromMap) {
     ASSERT_EQ(5, core_options.GetScanSnapshotId().value_or(-1));
     ASSERT_EQ(7, core_options.GetScanManifestEntryCacheMaxSnapshots());
     ASSERT_FALSE(core_options.ScanManifestEntryLazyDecodeEnabled());
+    ASSERT_TRUE(core_options.PrefetchIoMetricsEnabled());
     ExpireConfig expire_config = core_options.GetExpireConfig();
     ASSERT_EQ(15, expire_config.GetSnapshotRetainMin());
     ASSERT_EQ(30, expire_config.GetSnapshotRetainMax());
diff --git 
a/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp 
b/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
index 5451c9d2..6c0d23be 100644
--- 
a/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
+++ 
b/src/paimon/core/deletionvectors/apply_deletion_vector_batch_reader_test.cpp
@@ -87,7 +87,8 @@ class ApplyDeletionVectorBatchReaderTest : public 
::testing::Test,
                         prefetch_batch_count, batch_size, prefetch_batch_count 
* 2,
                         /*enable_adaptive_prefetch_strategy=*/false, executor_,
                         /*initialize_read_ranges=*/true,
-                        /*read_ahead_cache_enabled=*/true, CacheConfig(), 
pool_));
+                        /*read_ahead_cache_enabled=*/true, CacheConfig(),
+                        /*enable_io_metrics=*/false, pool_));
             } else {
                 file_batch_reader =
                     std::make_unique<MockFileBatchReader>(data, target_type_, 
batch_size);
diff --git a/src/paimon/core/operation/abstract_split_read.cpp 
b/src/paimon/core/operation/abstract_split_read.cpp
index 91157f71..5beee34b 100644
--- a/src/paimon/core/operation/abstract_split_read.cpp
+++ b/src/paimon/core/operation/abstract_split_read.cpp
@@ -168,7 +168,7 @@ Result<std::unique_ptr<FileBatchReader>> 
AbstractSplitRead::CreateFileBatchReade
                 context_->GetPrefetchBatchCount(), 
options_.EnableAdaptivePrefetchStrategy(),
                 executor_,
                 /*initialize_read_ranges=*/false, 
context_->ReadAheadCacheEnabled(),
-                context_->GetCacheConfig(), pool_));
+                context_->GetCacheConfig(), 
options_.PrefetchIoMetricsEnabled(), pool_));
         return 
std::make_unique<DelegatingPrefetchReader>(std::move(prefetch_reader));
     } else {
         PAIMON_ASSIGN_OR_RAISE(
diff --git a/src/paimon/core/operation/append_only_file_store_scan_test.cpp 
b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
index e1fb5a43..d72dcaf0 100644
--- a/src/paimon/core/operation/append_only_file_store_scan_test.cpp
+++ b/src/paimon/core/operation/append_only_file_store_scan_test.cpp
@@ -31,7 +31,6 @@
 #include "paimon/common/io/cache/lru_cache.h"
 #include "paimon/core/manifest/manifest_entry.h"
 #include "paimon/core/manifest/partition_entry.h"
-#include "paimon/core/operation/metrics/scan_metrics.h"
 #include "paimon/core/schema/schema_manager.h"
 #include "paimon/core/schema/table_schema.h"
 #include "paimon/core/stats/simple_stats_evolution.h"
@@ -45,6 +44,7 @@
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/scan_context.h"
 #include "paimon/status.h"
+#include "paimon/table/source/scan_metrics.h"
 #include "paimon/table/source/table_scan.h"
 #include "paimon/testing/utils/testharness.h"
 #include "paimon/testing/utils/timezone_guard.h"
@@ -173,7 +173,20 @@ TEST(AppendOnlyFileStoreScanTest, TestScanDurationMetric) {
                          metrics->GetCounter(ScanMetrics::LAST_SCAN_DURATION));
     ASSERT_OK_AND_ASSIGN(HistogramStats stats,
                          
metrics->GetHistogramStats(ScanMetrics::SCAN_DURATION));
+    ASSERT_OK_AND_ASSIGN(uint64_t manifest_read_duration,
+                         
metrics->GetCounter(ScanMetrics::LAST_MANIFEST_READ_DURATION));
+    ASSERT_OK_AND_ASSIGN(HistogramStats manifest_stats,
+                         
metrics->GetHistogramStats(ScanMetrics::MANIFEST_READ_DURATION));
+    ASSERT_OK_AND_ASSIGN(uint64_t scanned_rows,
+                         
metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_SCANNED_ROWS));
+    ASSERT_OK_AND_ASSIGN(uint64_t materialized_rows,
+                         
metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS));
     ASSERT_EQ(stats.count, kPlanCount);
+    ASSERT_EQ(manifest_stats.count, kPlanCount);
+    ASSERT_LE(manifest_stats.min, static_cast<double>(manifest_read_duration));
+    ASSERT_LE(static_cast<double>(manifest_read_duration), manifest_stats.max);
+    ASSERT_GT(scanned_rows, 0);
+    ASSERT_GE(scanned_rows, materialized_rows);
     ASSERT_LE(stats.min, stats.max);
     ASSERT_LE(stats.min, static_cast<double>(last_scan_duration));
     ASSERT_LE(static_cast<double>(last_scan_duration), stats.max);
@@ -268,12 +281,40 @@ TEST(AppendOnlyFileStoreScanTest, 
TestSnapshotLiveManifestCachePath) {
     scan_first->WithSnapshot(snapshot_5);
     ASSERT_OK_AND_ASSIGN(auto plan_first, scan_first->CreatePlan());
     std::vector<std::string> first_file_names = 
SortedFileNames(plan_first->Files());
+    std::shared_ptr<Metrics> first_metrics = scan_first->GetScanMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t first_cache_enabled,
+                         
first_metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_ENABLED));
+    ASSERT_OK_AND_ASSIGN(uint64_t first_cache_hit,
+                         
first_metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_HIT));
+    ASSERT_OK_AND_ASSIGN(uint64_t first_cache_misses,
+                         
first_metrics->GetCounter(ScanMetrics::SNAPSHOT_CACHE_MISSES));
+    
ASSERT_OK(first_metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_LOAD_DURATION));
+    
ASSERT_OK(first_metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_STORE_DURATION));
+    
ASSERT_OK(first_metrics->GetHistogramStats(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION));
+    
ASSERT_OK(first_metrics->GetHistogramStats(ScanMetrics::SNAPSHOT_CACHE_STORE_DURATION));
+    ASSERT_EQ(first_cache_enabled, 1);
+    ASSERT_EQ(first_cache_hit, 0);
+    ASSERT_EQ(first_cache_misses, 1);
 
     // Second scan on the same snapshot should read the same bucket live 
entries from cache.
     auto scan_second = BuildScan(table_path, cache, /*bucket=*/0);
     scan_second->WithSnapshot(snapshot_5);
     ASSERT_OK_AND_ASSIGN(auto plan_second, scan_second->CreatePlan());
     ASSERT_EQ(first_file_names, SortedFileNames(plan_second->Files()));
+    std::shared_ptr<Metrics> second_metrics = scan_second->GetScanMetrics();
+    ASSERT_OK_AND_ASSIGN(uint64_t second_cache_hit,
+                         
second_metrics->GetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_HIT));
+    ASSERT_OK_AND_ASSIGN(uint64_t second_cache_hits,
+                         
second_metrics->GetCounter(ScanMetrics::SNAPSHOT_CACHE_HITS));
+    ASSERT_OK_AND_ASSIGN(uint64_t scanned_rows,
+                         
second_metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_SCANNED_ROWS));
+    ASSERT_OK_AND_ASSIGN(
+        uint64_t materialized_rows,
+        
second_metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS));
+    ASSERT_EQ(second_cache_hit, 1);
+    ASSERT_EQ(second_cache_hits, 1);
+    ASSERT_GE(scanned_rows, materialized_rows);
+    
ASSERT_OK(second_metrics->GetHistogramStats(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION));
 }
 
 TEST(AppendOnlyFileStoreScanTest, TestSnapshotLiveManifestCacheRebuildOnMiss) {
diff --git a/src/paimon/core/operation/file_store_scan.cpp 
b/src/paimon/core/operation/file_store_scan.cpp
index f21b0bb7..467ba6b2 100644
--- a/src/paimon/core/operation/file_store_scan.cpp
+++ b/src/paimon/core/operation/file_store_scan.cpp
@@ -43,7 +43,6 @@
 #include "paimon/core/manifest/manifest_file_meta.h"
 #include "paimon/core/manifest/manifest_list.h"
 #include "paimon/core/manifest/snapshot_live_manifest_entries.h"
-#include "paimon/core/operation/metrics/scan_metrics.h"
 #include "paimon/core/partition/partition_info.h"
 #include "paimon/core/stats/simple_stats.h"
 #include "paimon/core/stats/simple_stats_evolution.h"
@@ -58,6 +57,7 @@
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/predicate/predicate_utils.h"
 #include "paimon/scan_context.h"
+#include "paimon/table/source/scan_metrics.h"
 
 namespace paimon {
 enum class FieldType;
@@ -136,6 +136,9 @@ Result<std::vector<PartitionEntry>> 
FileStoreScan::ReadPartitionEntries() const
 
 Result<std::shared_ptr<FileStoreScan::RawPlan>> FileStoreScan::CreatePlan() 
const {
     Duration duration;
+    Duration manifest_read_duration;
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_LOAD_DURATION, 0);
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_STORE_DURATION, 0);
     std::optional<Snapshot> snapshot;
     std::vector<ManifestFileMeta> all_manifest_file_metas;
     std::vector<ManifestFileMeta> filtered_manifest_file_metas;
@@ -148,9 +151,13 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
         core_options_.GetScanManifestEntryCacheMaxSnapshots() > 0 &&
         core_options_.GetCache() != nullptr && !table_path_.empty() &&
         !row_range_index_.has_value() && bucket_filter_.has_value();
+    uint64_t lazy_decode_scanned_rows = 0;
+    bool snapshot_cache_hit = false;
     if (use_snapshot_live_manifest_cache) {
-        PAIMON_RETURN_NOT_OK(ReadManifestEntriesWithCache(
-            snapshot.value(), all_manifest_file_metas, bucket_filter_.value(), 
&manifest_entries));
+        PAIMON_RETURN_NOT_OK(ReadManifestEntriesWithCache(snapshot.value(), 
all_manifest_file_metas,
+                                                          
bucket_filter_.value(), &manifest_entries,
+                                                          
&snapshot_cache_hit));
+        lazy_decode_scanned_rows = manifest_entries.size();
         std::vector<ManifestEntry> filtered_entries;
         filtered_entries.reserve(manifest_entries.size());
         for (auto& entry : manifest_entries) {
@@ -161,8 +168,15 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
         }
         manifest_entries = std::move(filtered_entries);
     } else {
+        lazy_decode_scanned_rows = std::accumulate(
+            filtered_manifest_file_metas.begin(), 
filtered_manifest_file_metas.end(), uint64_t{0},
+            [](uint64_t sum, const ManifestFileMeta& meta) {
+                return sum + static_cast<uint64_t>(meta.NumAddedFiles() + 
meta.NumDeletedFiles());
+            });
         PAIMON_RETURN_NOT_OK(ReadManifestEntries(filtered_manifest_file_metas, 
&manifest_entries));
     }
+    const uint64_t lazy_decode_materialized_rows = manifest_entries.size();
+    const uint64_t manifest_read_duration_ms = manifest_read_duration.Get();
     PAIMON_ASSIGN_OR_RAISE(manifest_entries,
                            
PostFilterManifestEntries(std::move(manifest_entries)));
 
@@ -208,6 +222,22 @@ Result<std::shared_ptr<FileStoreScan::RawPlan>> 
FileStoreScan::CreatePlan() cons
         ScanMetrics::LAST_SCAN_SKIPPED_TABLE_FILES,
         std::max(int64_t{0}, all_data_files - 
static_cast<int64_t>(manifest_entries.size())));
     metrics_->SetCounter(ScanMetrics::LAST_SCAN_RESULTED_TABLE_FILES, 
manifest_entries.size());
+    metrics_->SetCounter(ScanMetrics::LAST_MANIFEST_READ_DURATION, 
manifest_read_duration_ms);
+    metrics_->ObserveHistogram(ScanMetrics::MANIFEST_READ_DURATION,
+                               static_cast<double>(manifest_read_duration_ms));
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_ENABLED,
+                         use_snapshot_live_manifest_cache ? 1 : 0);
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_HIT, 
snapshot_cache_hit ? 1 : 0);
+    Result<uint64_t> cache_hits = 
metrics_->GetCounter(ScanMetrics::SNAPSHOT_CACHE_HITS);
+    Result<uint64_t> cache_misses = 
metrics_->GetCounter(ScanMetrics::SNAPSHOT_CACHE_MISSES);
+    metrics_->SetCounter(ScanMetrics::SNAPSHOT_CACHE_HITS,
+                         (cache_hits.ok() ? cache_hits.value() : 0) + 
(snapshot_cache_hit ? 1 : 0));
+    metrics_->SetCounter(ScanMetrics::SNAPSHOT_CACHE_MISSES,
+                         (cache_misses.ok() ? cache_misses.value() : 0) +
+                             (use_snapshot_live_manifest_cache && 
!snapshot_cache_hit ? 1 : 0));
+    metrics_->SetCounter(ScanMetrics::LAST_LAZY_DECODE_SCANNED_ROWS, 
lazy_decode_scanned_rows);
+    metrics_->SetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS,
+                         lazy_decode_materialized_rows);
     return std::make_shared<FileStoreScan::RawPlan>(scan_mode_, snapshot,
                                                     
std::move(manifest_entries));
 }
@@ -298,15 +328,22 @@ Status FileStoreScan::ReadManifestEntries(const 
std::vector<ManifestFileMeta>& m
 // snapshot's data manifests.
 Status FileStoreScan::ReadManifestEntriesWithCache(
     const Snapshot& snapshot, const std::vector<ManifestFileMeta>& 
all_manifest_metas,
-    int32_t bucket, std::vector<ManifestEntry>* manifest_entries) const {
+    int32_t bucket, std::vector<ManifestEntry>* manifest_entries, bool* 
cache_hit) const {
+    Duration cache_load_duration;
     PAIMON_ASSIGN_OR_RAISE(SnapshotLiveManifestEntries cached_entries,
                            LoadSnapshotLiveManifestEntries(bucket));
+    const uint64_t cache_load_duration_ms = cache_load_duration.Get();
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_LOAD_DURATION, 
cache_load_duration_ms);
+    metrics_->ObserveHistogram(ScanMetrics::SNAPSHOT_CACHE_LOAD_DURATION,
+                               static_cast<double>(cache_load_duration_ms));
     std::optional<SnapshotLiveManifestEntries::Entry> cached =
         cached_entries.LatestBeforeOrEqual(snapshot.Id());
     if (cached && cached->snapshot_id == snapshot.Id()) {
+        *cache_hit = true;
         *manifest_entries = *cached->entries;
         return Status::OK();
     }
+    *cache_hit = false;
 
     // Rebuild the target snapshot bucket from all manifests and write the 
live entries back to the
     // cache.
@@ -320,7 +357,12 @@ Status FileStoreScan::ReadManifestEntriesWithCache(
         ReadAndMergeBucketFileEntries(bucket_manifest_metas, bucket, 
manifest_entries));
     std::vector<ManifestEntry> cache_entries = *manifest_entries;
     cached_entries.Put(snapshot.Id(), std::move(cache_entries));
+    Duration cache_store_duration;
     PAIMON_RETURN_NOT_OK(StoreSnapshotLiveManifestEntries(bucket, 
cached_entries));
+    const uint64_t cache_store_duration_ms = cache_store_duration.Get();
+    metrics_->SetCounter(ScanMetrics::LAST_SNAPSHOT_CACHE_STORE_DURATION, 
cache_store_duration_ms);
+    metrics_->ObserveHistogram(ScanMetrics::SNAPSHOT_CACHE_STORE_DURATION,
+                               static_cast<double>(cache_store_duration_ms));
     return Status::OK();
 }
 
diff --git a/src/paimon/core/operation/file_store_scan.h 
b/src/paimon/core/operation/file_store_scan.h
index 155f36e7..ff53e0c9 100644
--- a/src/paimon/core/operation/file_store_scan.h
+++ b/src/paimon/core/operation/file_store_scan.h
@@ -161,7 +161,9 @@ class FileStoreScan {
     }
 
     std::shared_ptr<Metrics> GetScanMetrics() const {
-        return metrics_;
+        auto snapshot = std::make_shared<MetricsImpl>();
+        snapshot->Overwrite(metrics_);
+        return snapshot;
     }
 
     static Result<std::shared_ptr<PredicateFilter>> CreatePartitionPredicate(
@@ -260,7 +262,8 @@ class FileStoreScan {
     Status ReadManifestEntriesWithCache(const Snapshot& snapshot,
                                         const std::vector<ManifestFileMeta>& 
bucket_manifest_metas,
                                         int32_t bucket,
-                                        std::vector<ManifestEntry>* 
manifest_entries) const;
+                                        std::vector<ManifestEntry>* 
manifest_entries,
+                                        bool* cache_hit) const;
     std::shared_ptr<CacheKey> SnapshotLiveManifestEntriesCacheKey(int32_t 
bucket) const;
     Result<SnapshotLiveManifestEntries> 
LoadSnapshotLiveManifestEntries(int32_t bucket) const;
     Status StoreSnapshotLiveManifestEntries(int32_t bucket,
diff --git a/src/paimon/core/operation/key_value_file_store_scan_test.cpp 
b/src/paimon/core/operation/key_value_file_store_scan_test.cpp
index 703d7e49..f9fd2ea2 100644
--- a/src/paimon/core/operation/key_value_file_store_scan_test.cpp
+++ b/src/paimon/core/operation/key_value_file_store_scan_test.cpp
@@ -33,7 +33,6 @@
 #include "paimon/core/manifest/file_source.h"
 #include "paimon/core/manifest/manifest_file.h"
 #include "paimon/core/manifest/manifest_list.h"
-#include "paimon/core/operation/metrics/scan_metrics.h"
 #include "paimon/core/schema/schema_manager.h"
 #include "paimon/core/schema/table_schema.h"
 #include "paimon/core/snapshot.h"
@@ -50,6 +49,7 @@
 #include "paimon/predicate/literal.h"
 #include "paimon/predicate/predicate_builder.h"
 #include "paimon/scan_context.h"
+#include "paimon/table/source/scan_metrics.h"
 #include "paimon/testing/utils/binary_row_generator.h"
 #include "paimon/testing/utils/testharness.h"
 
diff --git a/src/paimon/core/operation/metrics/scan_metrics.h 
b/src/paimon/core/operation/metrics/scan_metrics.h
deleted file mode 100644
index 483ab7af..00000000
--- a/src/paimon/core/operation/metrics/scan_metrics.h
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.
- */
-
-#pragma once
-
-namespace paimon {
-
-/// Metrics to measure scan operation.
-class ScanMetrics {
- public:
-    static constexpr char LAST_SCAN_DURATION[] = "lastScanDuration";
-    // Histogram metric for scan plan duration (milliseconds).
-    static constexpr char SCAN_DURATION[] = "scanDuration";
-    static constexpr char LAST_SCANNED_SNAPSHOT_ID[] = "lastScannedSnapshotId";
-    static constexpr char LAST_SCANNED_MANIFESTS[] = "lastScannedManifests";
-    static constexpr char LAST_SCAN_SKIPPED_TABLE_FILES[] = 
"lastScanSkippedTableFiles";
-    static constexpr char LAST_SCAN_RESULTED_TABLE_FILES[] = 
"lastScanResultedTableFiles";
-};
-
-}  // namespace paimon
diff --git a/src/paimon/core/table/source/abstract_table_scan.h 
b/src/paimon/core/table/source/abstract_table_scan.h
index 363fe0dc..f8dd15aa 100644
--- a/src/paimon/core/table/source/abstract_table_scan.h
+++ b/src/paimon/core/table/source/abstract_table_scan.h
@@ -39,6 +39,10 @@ class AbstractTableScan : public TableScan {
                       const std::shared_ptr<SnapshotReader>& snapshot_reader)
         : core_options_(core_options), snapshot_reader_(snapshot_reader) {}
 
+    std::shared_ptr<Metrics> GetMetrics() const override {
+        return snapshot_reader_->GetMetrics();
+    }
+
  protected:
     Result<std::shared_ptr<StartingScanner>> CreateStartingScanner(bool 
is_streaming) const {
         const auto& snapshot_manager = snapshot_reader_->GetSnapshotManager();
diff --git a/src/paimon/core/table/source/realtime_table_scan.h 
b/src/paimon/core/table/source/realtime_table_scan.h
index 7d036d42..2b635faf 100644
--- a/src/paimon/core/table/source/realtime_table_scan.h
+++ b/src/paimon/core/table/source/realtime_table_scan.h
@@ -47,6 +47,10 @@ class RealtimeTableScan : public TableScan {
 
     Result<std::shared_ptr<Plan>> CreatePlan() override;
 
+    std::shared_ptr<Metrics> GetMetrics() const override {
+        return disk_scan_->GetMetrics();
+    }
+
  private:
     using MemoryViewMap = std::map<RealtimePartitionBucket, 
RealtimePartitionBucketView>;
 
diff --git a/src/paimon/core/table/source/snapshot/snapshot_reader.h 
b/src/paimon/core/table/source/snapshot/snapshot_reader.h
index c425fd86..35134d22 100644
--- a/src/paimon/core/table/source/snapshot/snapshot_reader.h
+++ b/src/paimon/core/table/source/snapshot/snapshot_reader.h
@@ -108,6 +108,10 @@ class SnapshotReader {
         return scan_->GetPartitionPredicate();
     }
 
+    std::shared_ptr<Metrics> GetMetrics() const {
+        return scan_->GetScanMetrics();
+    }
+
     /// Get splits from `FileKind::ADD` files.
     Result<std::shared_ptr<Plan>> Read() const;
 
diff --git a/src/paimon/core/table/source/table_scan.cpp 
b/src/paimon/core/table/source/table_scan.cpp
index 2dda955a..8111a94e 100644
--- a/src/paimon/core/table/source/table_scan.cpp
+++ b/src/paimon/core/table/source/table_scan.cpp
@@ -27,6 +27,7 @@
 #include <vector>
 
 #include "fmt/format.h"
+#include "paimon/common/metrics/metrics_impl.h"
 #include "paimon/common/predicate/predicate_validator.h"
 #include "paimon/common/types/data_field.h"
 #include "paimon/common/utils/fields_comparator.h"
@@ -187,6 +188,10 @@ Result<std::unique_ptr<TableScan>> NewDataTableScan(const 
std::shared_ptr<ScanCo
 
 }  // namespace
 
+std::shared_ptr<Metrics> TableScan::GetMetrics() const {
+    return std::make_shared<MetricsImpl>();
+}
+
 Result<std::unique_ptr<TableScan>> 
TableScan::Create(std::unique_ptr<ScanContext> context) {
     if (context == nullptr) {
         return Status::Invalid("scan context is null pointer");
diff --git a/src/paimon/core/table/source/table_scan_test.cpp 
b/src/paimon/core/table/source/table_scan_test.cpp
index 358dfd46..dbc5b992 100644
--- a/src/paimon/core/table/source/table_scan_test.cpp
+++ b/src/paimon/core/table/source/table_scan_test.cpp
@@ -26,11 +26,36 @@
 
 #include "gtest/gtest.h"
 #include "paimon/defs.h"
+#include "paimon/metrics.h"
 #include "paimon/scan_context.h"
 #include "paimon/status.h"
 #include "paimon/testing/utils/testharness.h"
 
 namespace paimon::test {
+namespace {
+
+class DefaultMetricsTableScan : public TableScan {
+ public:
+    Result<std::shared_ptr<Plan>> CreatePlan() override {
+        return Status::NotImplemented("not implemented");
+    }
+};
+
+}  // namespace
+
+TEST(TableScanTest, TestDefaultMetricsSnapshot) {
+    DefaultMetricsTableScan table_scan;
+    std::shared_ptr<Metrics> metrics = table_scan.GetMetrics();
+    ASSERT_TRUE(metrics);
+    metrics->SetCounter("external", 1);
+
+    std::shared_ptr<Metrics> second_metrics = table_scan.GetMetrics();
+    ASSERT_TRUE(second_metrics);
+    Result<uint64_t> external_counter = second_metrics->GetCounter("external");
+    ASSERT_FALSE(external_counter.ok());
+    ASSERT_EQ(external_counter.status().code(), StatusCode::KeyError);
+}
+
 TEST(TableScanTest, TestNoSnapshot) {
     std::string path = paimon::test::GetDataDir() +
                        
"/orc/append_table_with_nested_type.db/append_table_with_nested_type/";
@@ -61,6 +86,27 @@ TEST(TableScanTest, TestPkSchemaEvolutionScan) {
     ASSERT_OK_AND_ASSIGN(auto plan, table_scan->CreatePlan());
     ASSERT_TRUE(plan->SnapshotId());
     ASSERT_FALSE(plan->Splits().empty());
+
+    std::shared_ptr<Metrics> metrics = table_scan->GetMetrics();
+    ASSERT_TRUE(metrics);
+    ASSERT_OK_AND_ASSIGN(uint64_t scanned_snapshot_id,
+                         
metrics->GetCounter(ScanMetrics::LAST_SCANNED_SNAPSHOT_ID));
+    ASSERT_EQ(scanned_snapshot_id, 
static_cast<uint64_t>(plan->SnapshotId().value()));
+    ASSERT_OK_AND_ASSIGN(uint64_t resulted_table_files,
+                         
metrics->GetCounter(ScanMetrics::LAST_SCAN_RESULTED_TABLE_FILES));
+    ASSERT_GT(resulted_table_files, 0);
+    ASSERT_OK(metrics->GetCounter(ScanMetrics::LAST_MANIFEST_READ_DURATION));
+    ASSERT_OK(metrics->GetHistogramStats(ScanMetrics::MANIFEST_READ_DURATION));
+    ASSERT_OK_AND_ASSIGN(uint64_t lazy_decode_scanned_rows,
+                         
metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_SCANNED_ROWS));
+    ASSERT_OK_AND_ASSIGN(uint64_t lazy_decode_materialized_rows,
+                         
metrics->GetCounter(ScanMetrics::LAST_LAZY_DECODE_MATERIALIZED_ROWS));
+    ASSERT_GE(lazy_decode_scanned_rows, lazy_decode_materialized_rows);
+
+    metrics->SetCounter(ScanMetrics::LAST_SCANNED_SNAPSHOT_ID, 0);
+    ASSERT_OK_AND_ASSIGN(uint64_t internal_snapshot_id, 
table_scan->GetMetrics()->GetCounter(
+                                                            
ScanMetrics::LAST_SCANNED_SNAPSHOT_ID));
+    ASSERT_EQ(internal_snapshot_id, 
static_cast<uint64_t>(plan->SnapshotId().value()));
 }
 
 TEST(TableScanTest, TestReadOptimizedPrimaryKeyStreamingScanUnsupported) {
diff --git a/src/paimon/core/table/system/system_table_test.cpp 
b/src/paimon/core/table/system/system_table_test.cpp
index 0c67ae3a..7e5847db 100644
--- a/src/paimon/core/table/system/system_table_test.cpp
+++ b/src/paimon/core/table/system/system_table_test.cpp
@@ -33,10 +33,12 @@
 #include "paimon/core/table/system/audit_log_system_table.h"
 #include "paimon/core/table/system/binlog_system_table.h"
 #include "paimon/core/table/system/read_optimized_system_table.h"
+#include "paimon/core/table/system/system_table_scan.h"
 #include "paimon/defs.h"
 #include "paimon/fs/file_system.h"
 #include "paimon/fs/file_system_factory.h"
 #include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
 #include "paimon/reader/batch_reader.h"
 #include "paimon/result.h"
 #include "paimon/status.h"
@@ -199,4 +201,15 @@ TEST(SystemTableTest, 
TestGlobalSystemTableWithoutCatalogReturnsNotImplemented)
                         "global system table requires catalog context: 
tables");
 }
 
+TEST(SystemTableTest, TestScanMetricsAreSnapshots) {
+    SystemTableScan scan("/tmp/table");
+    std::shared_ptr<Metrics> metrics = scan.GetMetrics();
+    ASSERT_TRUE(metrics);
+    metrics->SetCounter("external", 1);
+
+    std::shared_ptr<Metrics> second_metrics = scan.GetMetrics();
+    ASSERT_TRUE(second_metrics);
+    ASSERT_NOK_WITH_MSG(second_metrics->GetCounter("external"), "metric 
'external' not found");
+}
+
 }  // namespace paimon::test

Reply via email to