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

diqiu50 pushed a commit to branch trino-irc-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit 8e01e964063ed446c94b0586188bc3c8e5790f48
Author: diqiu50 <[email protected]>
AuthorDate: Fri Aug 21 14:11:08 2026 +0800

    [#12546] improvement(trino-connector): Report catalog registration status 
through system tables
    
    Record the registration state of every catalog and the health of the load
    loop, and expose both through the gravitino.system.catalog_status and
    gravitino.system.load_status system tables. Errors now report the root
    cause rather than the outer wrapper.
---
 docs/trino-connector/supported-catalog.md          |  78 +++++
 .../testsets/jdbc-mysql/00015_catalog_status.sql   |   3 +
 .../testsets/jdbc-mysql/00015_catalog_status.txt   |   3 +
 .../trino/connector/GravitinoConnectorFactory.java |  19 ++
 .../connector/catalog/CatalogConnectorManager.java | 347 ++++++++++++++++++---
 .../catalog/CatalogRegistrationState.java          | 247 +++++++++++++++
 .../connector/system/GravitinoSystemConnector.java |  31 +-
 .../AlterCatalogStoredProcedure.java               |   8 +-
 .../CreateCatalogStoredProcedure.java              |   7 +-
 .../system/table/GravitinoSystemTable.java         |  35 +++
 .../table/GravitinoSystemTableCatalogStatus.java   | 116 +++++++
 .../system/table/GravitinoSystemTableFactory.java  |   6 +
 .../table/GravitinoSystemTableLoadStatus.java      | 118 +++++++
 .../trino/connector/TestGravitinoConnector.java    |  36 ++-
 .../catalog/TestCatalogConnectorManager.java       | 250 +++++++++++++++
 .../table/TestGravitinoSystemStatusTables.java     | 135 ++++++++
 16 files changed, 1382 insertions(+), 57 deletions(-)

diff --git a/docs/trino-connector/supported-catalog.md 
b/docs/trino-connector/supported-catalog.md
index 80a0380f30..8762ce145b 100644
--- a/docs/trino-connector/supported-catalog.md
+++ b/docs/trino-connector/supported-catalog.md
@@ -80,6 +80,66 @@ The result is like:
  gt_hive      | hive     | 
{gravitino.bypass.hive.metastore.client.capability.check=false, 
metastore.uris=thrift://trino-ci-hive:9083}
 ```
 
+Check catalog registration status:
+
+`gravitino.system.catalog` lists the catalogs the Gravitino server knows 
about. A catalog listed
+there is not necessarily usable in Trino: registering it is a separate step 
that can fail. When a
+catalog does not show up in `SHOW CATALOGS`, `gravitino.system.catalog_status` 
says why.
+
+```sql
+select catalog_name, status, last_error from gravitino.system.catalog_status;
+```
+
+The result is like:
+
+```test
+ catalog_name | status     | last_error
+--------------+------------+-------------------------------------------------
+ gt_hive      | REGISTERED | NULL
+ gt_iceberg   | FAILED     | Access Denied: Cannot create catalog gt_iceberg
+ gt_files     | UNSUPPORTED| Only relational catalogs are supported, the 
catalog type is FILESET
+```
+
+| Column               | Description                                           
                                       |
+|----------------------|----------------------------------------------------------------------------------------------|
+| `metalake`           | The metalake the catalog belongs to.                  
                                         |
+| `catalog_name`       | The name of the catalog in Gravitino.                 
                                         |
+| `trino_catalog_name` | The name the catalog is registered under in Trino, as 
it appears in `SHOW CATALOGS`.           |
+| `provider`           | The catalog provider, for example `hive` or 
`lakehouse-iceberg`.                               |
+| `status`             | One of `REGISTERED`, `FAILED`, `UNSUPPORTED` or 
`SKIPPED`. See the table below.                |
+| `last_error`         | The reason the catalog is not registered, `NULL` when 
it is.                                   |
+| `last_attempt_time`  | When the catalog was last processed, as an ISO-8601 
UTC timestamp.                             |
+| `last_success_time`  | When the catalog was last registered successfully, 
`NULL` if it never was.                     |
+| `failure_count`      | The number of consecutive failed attempts, `0` when 
the last attempt succeeded.                |
+
+| Status        | Meaning                                                      
                                 |
+|---------------|-----------------------------------------------------------------------------------------------|
+| `REGISTERED`  | The catalog is registered in Trino and appears in `SHOW 
CATALOGS`.                              |
+| `FAILED`      | The last registration attempt failed, `last_error` carries 
the reason. Retried every refresh.   |
+| `UNSUPPORTED` | The catalog is not relational, or its provider is not 
supported by the connector.               |
+| `SKIPPED`     | The catalog matches `gravitino.trino.skip-catalog-patterns` 
and is deliberately not registered. |
+
+A failure that stops the connector before it can list catalogs at all, such as 
an unreachable
+Gravitino server, leaves no row to attach itself to. 
`gravitino.system.load_status` reports the
+health of the loop itself, and always has exactly one row.
+
+```sql
+select * from gravitino.system.load_status;
+```
+
+| Column                 | Description                                         
                                    |
+|------------------------|-------------------------------------------------------------------------------------------|
+| `trino_started`        | Whether the connector can reach the Trino server 
over JDBC. No catalog is registered until it can. |
+| `last_attempt_time`    | When the loop last ran, as an ISO-8601 UTC 
timestamp.                                       |
+| `last_success_time`    | When the loop last completed successfully, `NULL` 
if it never did.                          |
+| `consecutive_failures` | The number of consecutive failed runs, `0` when the 
last run succeeded.                     |
+| `last_error`           | The reason the last run failed, `NULL` when it 
succeeded.                                   |
+| `metalake_errors`      | A JSON map of metalake name to its last error, 
`NULL` when every metalake loaded.           |
+
+Both tables are served by the coordinator and reflect the last refresh, which 
runs every
+`gravitino.metadata.refresh-interval-second` seconds (10 by default). A 
catalog created moments ago
+may not have been processed yet.
+
 Example:
 Run the following SQL to create a catalog named `mysql` with `jdbc-mysql` 
provider.
 
@@ -181,3 +241,21 @@ Hive does not support `TIME` data type.
 | Struct                | ROW                      |
 
 For more about Trino data types, refer to [Trino data 
types](https://trino.io/docs/current/language/types.html) and Gravitino data 
types, refer to [Gravitino data 
types](../tables-and-views.md#table-column-type).
+
+## Troubleshooting
+
+Registration happens in the background, so a catalog that fails to register 
simply never appears in
+`SHOW CATALOGS`. Start from `gravitino.system.catalog_status` rather than the 
coordinator log.
+
+| Symptom                                                            | Likely 
cause                                                                           
                           |
+|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
+| A catalog is missing from `SHOW CATALOGS`                            | Query 
`gravitino.system.catalog_status` and read `status` and `last_error`, then 
follow the rows below              |
+| `status = FAILED`, `last_error` mentions `Access Denied`             | The 
`gravitino.trino.user` lacks a Trino system role permitted to run `CREATE 
CATALOG`                              |
+| `status = FAILED`, `last_error` mentions a configuration property    | A 
`trino.bypass.` property is not accepted by the underlying Trino connector      
                                  |
+| `status = UNSUPPORTED`                                               | The 
catalog is not relational, or its provider is outside the supported list. 
`last_error` names the supported providers |
+| `status = SKIPPED`                                                   | The 
catalog matches `gravitino.trino.skip-catalog-patterns`                         
                                |
+| The catalog has no row in `catalog_status` at all                    | The 
load loop never reached it. Check `gravitino.system.load_status`                
                                |
+| `load_status.trino_started = false`                                  | The 
connector cannot reach Trino over JDBC. Check `discovery.uri`, 
`gravitino.trino.user` and `gravitino.trino.password` |
+| `load_status.last_error` mentions connection refused                 | The 
Gravitino server is unreachable. Check `gravitino.uri`                          
                                |
+| `load_status.metalake_errors` names a metalake                       | That 
metalake could not be listed, the other metalakes are unaffected                
                               |
+| Querying `gravitino.system.catalog_status` itself fails              | The 
entry catalog did not initialise. The error is reported when the entry catalog 
is created, check the Trino server log at startup |
diff --git 
a/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.sql
 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.sql
new file mode 100644
index 0000000000..53e3f32237
--- /dev/null
+++ 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.sql
@@ -0,0 +1,3 @@
+select catalog_name, status, failure_count from 
gravitino.system.catalog_status where catalog_name = 'gt_mysql';
+
+select cast(trino_started as varchar), consecutive_failures from 
gravitino.system.load_status;
diff --git 
a/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.txt
 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.txt
new file mode 100644
index 0000000000..dce3462b9c
--- /dev/null
+++ 
b/trino-connector/integration-test/src/test/resources/trino-ci-testset/testsets/jdbc-mysql/00015_catalog_status.txt
@@ -0,0 +1,3 @@
+"gt_mysql","REGISTERED","0"
+
+"true","0"
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
index a748c0ab58..917d9f3928 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
@@ -25,6 +25,7 @@ import static 
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import io.trino.spi.HostAddress;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.Connector;
 import io.trino.spi.connector.ConnectorContext;
@@ -132,6 +133,13 @@ public class GravitinoConnectorFactory implements 
ConnectorFactory {
           catalogConnectorManager.config(config, client);
 
           gravitinoSystemTableFactory = new 
GravitinoSystemTableFactory(catalogConnectorManager);
+          if (isCoordinator(trinoConnectorContext)) {
+            // Pin the system table splits here: the registration state the 
system tables report
+            // is only recorded on the coordinator by the load loop started 
below. Starting the
+            // manager remains deferred until the static connector supplies 
its JDBC settings.
+            GravitinoSystemConnector.Split.setCoordinatorAddress(
+                getCurrentNodeAddress(trinoConnectorContext));
+          }
         }
 
         // The `trino.jdbc.*` settings that CatalogRegister needs to connect 
back to the
@@ -289,6 +297,17 @@ public class GravitinoConnectorFactory implements 
ConnectorFactory {
     return connectorContext.getNodeManager().getCurrentNode().isCoordinator();
   }
 
+  /**
+   * Retrieves the address of the Trino node this connector is running on.
+   *
+   * @param connectorContext the Trino connector context
+   * @return the host and port of the current node
+   */
+  @SuppressWarnings("deprecation")
+  protected HostAddress getCurrentNodeAddress(ConnectorContext 
connectorContext) {
+    return connectorContext.getNodeManager().getCurrentNode().getHostAndPort();
+  }
+
   private CatalogConnectorFactory 
createCatalogConnectorFactory(GravitinoConfig config) {
     // Create a CatalogConnectorFactory. If we specify a customized class name 
for the
     // CatalogConnectorFactory,
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
index 380c5214f8..f55752be30 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogConnectorManager.java
@@ -22,18 +22,20 @@ import com.google.common.base.Preconditions;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorContext;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.regex.Pattern;
-import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Catalog;
 import org.apache.gravitino.client.GravitinoAdminClient;
@@ -72,6 +74,20 @@ public class CatalogConnectorManager {
   private final ConcurrentHashMap<String, CatalogConnectorContext> 
catalogConnectors =
       new ConcurrentHashMap<>();
 
+  // The registration state of every catalog seen by the load loop, keyed by 
the Trino catalog
+  // name. Written only by the load loop thread, read by query threads through 
the system tables.
+  private final ConcurrentHashMap<String, CatalogRegistrationState> 
catalogStates =
+      new ConcurrentHashMap<>();
+
+  // The last error reported by each metalake, keyed by the metalake name.
+  private final ConcurrentHashMap<String, String> metalakeErrors = new 
ConcurrentHashMap<>();
+
+  private volatile boolean trinoStarted = false;
+  private volatile long lastLoadAttemptTimeMs = 0L;
+  private volatile long lastSuccessfulLoadTimeMs = 0L;
+  private volatile String lastLoadError = null;
+  private final AtomicLong consecutiveLoadFailures = new AtomicLong();
+
   private String targetMetalake;
   private final Map<String, GravitinoMetalake> metalakes = new 
ConcurrentHashMap<>();
   // Tracks which metalakes' Iceberg REST discovery is currently failing, so a 
failure is logged
@@ -179,11 +195,18 @@ public class CatalogConnectorManager {
   }
 
   private void loadMetalake() {
+    lastLoadAttemptTimeMs = System.currentTimeMillis();
     try {
       if (!catalogRegister.isTrinoStarted()) {
-        LOG.info("Waiting for the Trino started.");
+        String message = "Waiting for the Trino server to start";
+        if (!Objects.equals(lastLoadError, message)) {
+          LOG.info("{}.", message);
+        }
+        lastLoadError = message;
+        trinoStarted = false;
         return;
       }
+      trinoStarted = true;
 
       Set<String> usedMetalakes = new HashSet<>();
       if (config.singleMetalakeMode()) {
@@ -207,12 +230,61 @@ public class CatalogConnectorManager {
           }
           loadCatalogs(metalake);
         } catch (Exception e) {
-          LOG.error("Load Metalake {} failed.", usedMetalake, e);
+          recordMetalakeError(usedMetalake, e);
         }
       }
+
+      lastSuccessfulLoadTimeMs = System.currentTimeMillis();
+      recordLoadSuccess();
     } catch (Exception e) {
-      LOG.error("Error when loading metalake", e);
+      recordLoadFailure(toErrorMessage(e), e);
+    }
+  }
+
+  private void recordLoadSuccess() {
+    if (lastLoadError != null) {
+      LOG.info("The Gravitino catalog load loop recovered.");
+    }
+    lastLoadError = null;
+    consecutiveLoadFailures.set(0);
+  }
+
+  private void recordLoadFailure(String message, Exception cause) {
+    boolean changed = !Objects.equals(lastLoadError, message);
+    lastLoadError = message;
+    consecutiveLoadFailures.incrementAndGet();
+    if (changed) {
+      LOG.error("Failed to load catalogs from the Gravitino server: {}", 
message, cause);
+    } else {
+      LOG.debug("Failed to load catalogs from the Gravitino server: {}", 
message, cause);
+    }
+  }
+
+  private void recordMetalakeError(String metalakeName, Exception cause) {
+    String message = toErrorMessage(cause);
+    String previous = metalakeErrors.put(metalakeName, message);
+    if (!Objects.equals(previous, message)) {
+      LOG.error("Load metalake {} failed: {}", metalakeName, message, cause);
+    } else {
+      LOG.debug("Load metalake {} failed: {}", metalakeName, message, cause);
+    }
+  }
+
+  private static String toErrorMessage(Exception e) {
+    // Report the root cause: the actual reason a registration failed, such as 
"Access Denied:
+    // Cannot create catalog", is wrapped in several layers of TrinoException 
by the time it gets
+    // here, and the outer messages say nothing a user can act on. Do not use
+    // GravitinoErrorCode.toSimpleErrorMessage(), it throws a 
NullPointerException when the
+    // exception carries no message.
+    Throwable rootCause = e;
+    while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
+      rootCause = rootCause.getCause();
     }
+    String message = rootCause.getMessage();
+    if (StringUtils.isBlank(message)) {
+      message = e.getMessage();
+    }
+    return StringUtils.isBlank(message) ? e.getClass().getName() : message;
   }
 
   /**
@@ -262,30 +334,53 @@ public class CatalogConnectorManager {
   }
 
   private void loadCatalogs(GravitinoMetalake metalake) {
-    List<String> catalogNames;
+    String metalakeName = metalake.name();
+    String[] allCatalogNames;
     try {
-      catalogNames =
-          Arrays.stream(metalake.listCatalogs())
-              .filter(id -> !skipCatalog(getTrinoCatalogName(metalake.name(), 
id)))
-              .collect(Collectors.toList());
+      allCatalogNames = metalake.listCatalogs();
     } catch (Exception e) {
-      LOG.error("Failed to list catalogs in metalake {}.", metalake.name(), e);
+      // Keep the existing catalog states untouched, a transient listing 
failure must not turn
+      // healthy catalogs into failed ones. The load status system table 
reports the cause.
+      recordMetalakeError(metalakeName, e);
       return;
     }
+    metalakeErrors.remove(metalakeName);
+
+    // The Trino names of every catalog the Gravitino server currently 
reports, including the
+    // catalogs that are intentionally not registered.
+    Set<String> presentTrinoNames = new HashSet<>();
+    List<String> catalogNames = new ArrayList<>();
+    for (String catalogName : allCatalogNames) {
+      String trinoCatalogName = getTrinoCatalogName(metalakeName, catalogName);
+      presentTrinoNames.add(trinoCatalogName);
+      if (skipCatalog(trinoCatalogName)) {
+        recordCatalogState(
+            CatalogRegistrationState.notLoaded(
+                metalakeName,
+                catalogName,
+                trinoCatalogName,
+                null,
+                CatalogRegistrationState.Status.SKIPPED,
+                "Matched gravitino.trino.skip-catalog-patterns"),
+            null);
+        continue;
+      }
+      catalogNames.add(catalogName);
+    }
 
-    LOG.debug("Load metalake {}'s catalogs. catalogs: {}.", metalake.name(), 
catalogNames);
+    LOG.debug("Load metalake {}'s catalogs. catalogs: {}.", metalakeName, 
catalogNames);
 
     // Delete those catalogs that have been deleted in Gravitino server
-    Set<String> catalogNameStrings =
-        catalogNames.stream()
-            .map(id -> getTrinoCatalogName(metalake.name(), id))
-            .collect(Collectors.toSet());
+    Set<String> catalogNameStrings = new HashSet<>();
+    for (String catalogName : catalogNames) {
+      catalogNameStrings.add(getTrinoCatalogName(metalakeName, catalogName));
+    }
 
     for (Map.Entry<String, CatalogConnectorContext> entry : 
catalogConnectors.entrySet()) {
       if (!catalogNameStrings.contains(entry.getKey())
           &&
           // Skip the catalog doesn't belong to this metalake.
-          entry.getValue().getMetalake().name().equals(metalake.name())) {
+          entry.getValue().getMetalake().name().equals(metalakeName)) {
         try {
           unloadCatalog(entry.getValue().getCatalog());
         } catch (Exception e) {
@@ -294,35 +389,111 @@ public class CatalogConnectorManager {
       }
     }
 
+    // Drop the states of catalogs that no longer exist in the Gravitino 
server, including the
+    // states of catalogs that never had a connector.
+    catalogStates
+        .values()
+        .removeIf(
+            state ->
+                state.getMetalake().equals(metalakeName)
+                    && 
!presentTrinoNames.contains(state.getTrinoCatalogName()));
+
     // Load new catalogs belows to the metalake.
-    catalogNames.stream()
-        .forEach(
-            (String catalogName) -> {
-              try {
-                Catalog catalog = metalake.loadCatalog(catalogName);
-                GravitinoCatalog gravitinoCatalog = new 
GravitinoCatalog(metalake.name(), catalog);
-                if 
(catalogConnectors.containsKey(getTrinoCatalogName(gravitinoCatalog))) {
-                  // Reload catalogs that have been updated in Gravitino 
server.
-                  reloadCatalog(gravitinoCatalog);
-                } else {
-                  if (catalog.type() == Catalog.Type.RELATIONAL
-                      && catalogConnectorFactory
-                          .getSupportedCatalogProviders()
-                          .contains(gravitinoCatalog.getProvider())) {
-                    loadCatalog(gravitinoCatalog);
-                  }
-                }
-              } catch (UnsupportedOperationException e) {
-                LOG.warn(
-                    "Unsupported catalog type for catalog {} in metalake {}: 
{}",
-                    catalogName,
-                    metalake.name(),
-                    e.getMessage());
-              } catch (Exception e) {
-                LOG.error(
-                    "Failed to load metalake {}'s catalog {}.", 
metalake.name(), catalogName, e);
-              }
-            });
+    for (String catalogName : catalogNames) {
+      String trinoCatalogName = getTrinoCatalogName(metalakeName, catalogName);
+      // Tracked outside the try so that a failure can still report the 
provider it knows about.
+      String provider = null;
+      try {
+        Catalog catalog = metalake.loadCatalog(catalogName);
+        GravitinoCatalog gravitinoCatalog = new GravitinoCatalog(metalakeName, 
catalog);
+        provider = gravitinoCatalog.getProvider();
+        if (catalogConnectors.containsKey(trinoCatalogName)) {
+          // Reload catalogs that have been updated in Gravitino server.
+          reloadCatalog(gravitinoCatalog);
+          recordCatalogState(
+              CatalogRegistrationState.succeeded(gravitinoCatalog, 
trinoCatalogName), null);
+        } else if (catalog.type() != Catalog.Type.RELATIONAL) {
+          recordCatalogState(
+              CatalogRegistrationState.notLoaded(
+                  metalakeName,
+                  catalogName,
+                  trinoCatalogName,
+                  gravitinoCatalog.getProvider(),
+                  CatalogRegistrationState.Status.UNSUPPORTED,
+                  String.format(
+                      "Only relational catalogs are supported, the catalog 
type is %s",
+                      catalog.type())),
+              null);
+        } else if (!catalogConnectorFactory
+            .getSupportedCatalogProviders()
+            .contains(gravitinoCatalog.getProvider())) {
+          recordCatalogState(
+              CatalogRegistrationState.notLoaded(
+                  metalakeName,
+                  catalogName,
+                  trinoCatalogName,
+                  gravitinoCatalog.getProvider(),
+                  CatalogRegistrationState.Status.UNSUPPORTED,
+                  String.format(
+                      "The catalog provider %s is not supported, the supported 
providers are %s",
+                      gravitinoCatalog.getProvider(),
+                      catalogConnectorFactory.getSupportedCatalogProviders())),
+              null);
+        } else {
+          loadCatalog(gravitinoCatalog);
+          recordCatalogState(
+              CatalogRegistrationState.succeeded(gravitinoCatalog, 
trinoCatalogName), null);
+        }
+      } catch (UnsupportedOperationException e) {
+        recordCatalogState(
+            CatalogRegistrationState.notLoaded(
+                metalakeName,
+                catalogName,
+                trinoCatalogName,
+                provider,
+                CatalogRegistrationState.Status.UNSUPPORTED,
+                toErrorMessage(e)),
+            null);
+      } catch (Exception e) {
+        recordCatalogState(
+            CatalogRegistrationState.failed(
+                metalakeName,
+                catalogName,
+                trinoCatalogName,
+                provider,
+                toErrorMessage(e),
+                catalogStates.get(trinoCatalogName)),
+            e);
+      }
+    }
+  }
+
+  private void recordCatalogState(CatalogRegistrationState state, Exception 
cause) {
+    CatalogRegistrationState previous = 
catalogStates.put(state.getTrinoCatalogName(), state);
+    boolean changed =
+        previous == null
+            || previous.getStatus() != state.getStatus()
+            || !Objects.equals(previous.getLastError(), state.getLastError());
+    if (!changed) {
+      LOG.debug("Catalog {} registration state unchanged: {}", 
state.getTrinoCatalogName(), state);
+      return;
+    }
+
+    if (state.getStatus() == CatalogRegistrationState.Status.REGISTERED) {
+      LOG.info("Catalog {} is registered in Trino.", 
state.getTrinoCatalogName());
+    } else if (state.getStatus() == CatalogRegistrationState.Status.FAILED) {
+      LOG.error(
+          "Failed to register catalog {} in Trino: {}",
+          state.getTrinoCatalogName(),
+          state.getLastError(),
+          cause);
+    } else {
+      LOG.warn(
+          "Catalog {} is not registered in Trino ({}): {}",
+          state.getTrinoCatalogName(),
+          state.getStatus(),
+          state.getLastError());
+    }
   }
 
   private void reloadCatalog(GravitinoCatalog catalog) {
@@ -355,7 +526,7 @@ public class CatalogConnectorManager {
     } catch (Exception e) {
       String message =
           String.format("Failed to create internal catalog connector. The 
catalog is: %s", catalog);
-      LOG.error(message, e);
+      LOG.debug(message, e);
       throw new TrinoException(
           GravitinoErrorCode.GRAVITINO_CREATE_INTERNAL_CONNECTOR_ERROR, 
message, e);
     }
@@ -365,6 +536,7 @@ public class CatalogConnectorManager {
     String catalogFullName = getTrinoCatalogName(catalog);
     catalogRegister.unregisterCatalog(catalogFullName);
     catalogConnectors.remove(catalogFullName);
+    catalogStates.remove(catalogFullName);
     LOG.info(
         "Remove catalog '{}' in metalake {} successfully.",
         catalog.getName(),
@@ -437,6 +609,89 @@ public class CatalogConnectorManager {
     return getTrinoCatalogName(catalog.getMetalake(), catalog.getName());
   }
 
+  /**
+   * Retrieves a snapshot of the registration state of every Gravitino catalog 
seen by the load
+   * loop.
+   *
+   * @return the registration states
+   */
+  public List<CatalogRegistrationState> getCatalogRegistrationStates() {
+    return List.copyOf(catalogStates.values());
+  }
+
+  /**
+   * Checks whether the Trino server has become reachable over JDBC. No 
catalog can be registered
+   * before it does.
+   *
+   * @return true if the Trino server is started, false otherwise
+   */
+  public boolean isTrinoStarted() {
+    return trinoStarted;
+  }
+
+  /**
+   * Retrieves the time of the last catalog load attempt.
+   *
+   * @return the time in milliseconds since the epoch, 0 if the load loop 
never ran
+   */
+  public long getLastLoadAttemptTimeMs() {
+    return lastLoadAttemptTimeMs;
+  }
+
+  /**
+   * Retrieves the time of the last successful catalog load.
+   *
+   * @return the time in milliseconds since the epoch, 0 if the load loop 
never succeeded
+   */
+  public long getLastSuccessfulLoadTimeMs() {
+    return lastSuccessfulLoadTimeMs;
+  }
+
+  /**
+   * Retrieves the error that made the last catalog load fail.
+   *
+   * @return the error message, null if the last load succeeded
+   */
+  @Nullable
+  public String getLastLoadError() {
+    return lastLoadError;
+  }
+
+  /**
+   * Retrieves the number of consecutive failed catalog loads.
+   *
+   * @return the failure count, 0 if the last load succeeded
+   */
+  public long getConsecutiveLoadFailures() {
+    return consecutiveLoadFailures.get();
+  }
+
+  /**
+   * Retrieves the last error reported by each metalake, keyed by the metalake 
name.
+   *
+   * @return the metalake errors, empty if every metalake was loaded 
successfully
+   */
+  public Map<String, String> getMetalakeErrors() {
+    return Map.copyOf(metalakeErrors);
+  }
+
+  /**
+   * Describes why a catalog is not registered in Trino, for use in error 
messages.
+   *
+   * @param trinoCatalogName the name the catalog would be registered under in 
Trino
+   * @return a human readable explanation
+   */
+  public String describeRegistrationFailure(String trinoCatalogName) {
+    CatalogRegistrationState state = catalogStates.get(trinoCatalogName);
+    if (state != null && state.getLastError() != null) {
+      return String.format("%s: %s", state.getStatus(), state.getLastError());
+    }
+    if (lastLoadError != null) {
+      return lastLoadError;
+    }
+    return "The catalog has not been loaded yet, please retry later.";
+  }
+
   /**
    * Retrieves the set of metalakes that have been used.
    *
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogRegistrationState.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogRegistrationState.java
new file mode 100644
index 0000000000..3ebb5dcb02
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/catalog/CatalogRegistrationState.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.trino.connector.catalog;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
+
+/**
+ * The registration state of a single Apache Gravitino catalog in Trino.
+ *
+ * <p>Instances are immutable and are always replaced as a whole, so a reader 
never observes a
+ * partially updated state.
+ */
+public final class CatalogRegistrationState {
+
+  /** The registration status of a catalog. */
+  public enum Status {
+    /** The catalog was registered in Trino with a CREATE CATALOG statement. */
+    REGISTERED,
+    /** The last registration attempt failed. */
+    FAILED,
+    /** The catalog matches `gravitino.trino.skip-catalog-patterns` and is not 
registered. */
+    SKIPPED,
+    /** The catalog type or provider is not supported by the connector. */
+    UNSUPPORTED
+  }
+
+  private final String metalake;
+  private final String catalogName;
+  private final String trinoCatalogName;
+  private final String provider;
+  private final Status status;
+  private final String lastError;
+  private final long lastAttemptTimeMs;
+  private final long lastSuccessTimeMs;
+  private final long failureCount;
+
+  private CatalogRegistrationState(
+      String metalake,
+      String catalogName,
+      String trinoCatalogName,
+      String provider,
+      Status status,
+      String lastError,
+      long lastAttemptTimeMs,
+      long lastSuccessTimeMs,
+      long failureCount) {
+    this.metalake = metalake;
+    this.catalogName = catalogName;
+    this.trinoCatalogName = trinoCatalogName;
+    this.provider = provider;
+    this.status = status;
+    this.lastError = lastError;
+    this.lastAttemptTimeMs = lastAttemptTimeMs;
+    this.lastSuccessTimeMs = lastSuccessTimeMs;
+    this.failureCount = failureCount;
+  }
+
+  /**
+   * Creates a state for a catalog that was registered in Trino successfully.
+   *
+   * @param catalog the Gravitino catalog
+   * @param trinoCatalogName the name the catalog is registered under in Trino
+   * @return the registration state
+   */
+  public static CatalogRegistrationState succeeded(
+      GravitinoCatalog catalog, String trinoCatalogName) {
+    long now = System.currentTimeMillis();
+    return new CatalogRegistrationState(
+        catalog.getMetalake(),
+        catalog.getName(),
+        trinoCatalogName,
+        catalog.getProvider(),
+        Status.REGISTERED,
+        null,
+        now,
+        now,
+        0);
+  }
+
+  /**
+   * Creates a state for a catalog whose registration attempt failed.
+   *
+   * @param metalake the name of the metalake the catalog belongs to
+   * @param catalogName the name of the catalog in Gravitino
+   * @param trinoCatalogName the name the catalog would be registered under in 
Trino
+   * @param provider the catalog provider, null if it could not be determined
+   * @param error the error that prevented the registration
+   * @param previous the previous state of the catalog, null if the catalog 
was never seen before
+   * @return the registration state
+   */
+  public static CatalogRegistrationState failed(
+      String metalake,
+      String catalogName,
+      String trinoCatalogName,
+      @Nullable String provider,
+      String error,
+      @Nullable CatalogRegistrationState previous) {
+    return new CatalogRegistrationState(
+        metalake,
+        catalogName,
+        trinoCatalogName,
+        provider,
+        Status.FAILED,
+        error,
+        System.currentTimeMillis(),
+        previous == null ? 0 : previous.lastSuccessTimeMs,
+        previous == null ? 1 : previous.failureCount + 1);
+  }
+
+  /**
+   * Creates a state for a catalog that is intentionally not registered in 
Trino.
+   *
+   * @param metalake the name of the metalake the catalog belongs to
+   * @param catalogName the name of the catalog in Gravitino
+   * @param trinoCatalogName the name the catalog would be registered under in 
Trino
+   * @param provider the catalog provider, null if it could not be determined
+   * @param status the reason category, either {@link Status#SKIPPED} or 
{@link Status#UNSUPPORTED}
+   * @param reason a human readable explanation of why the catalog is not 
registered
+   * @return the registration state
+   */
+  public static CatalogRegistrationState notLoaded(
+      String metalake,
+      String catalogName,
+      String trinoCatalogName,
+      @Nullable String provider,
+      Status status,
+      String reason) {
+    return new CatalogRegistrationState(
+        metalake,
+        catalogName,
+        trinoCatalogName,
+        provider,
+        status,
+        reason,
+        System.currentTimeMillis(),
+        0,
+        0);
+  }
+
+  /**
+   * Retrieves the name of the metalake the catalog belongs to.
+   *
+   * @return the metalake name
+   */
+  public String getMetalake() {
+    return metalake;
+  }
+
+  /**
+   * Retrieves the name of the catalog in Gravitino.
+   *
+   * @return the catalog name
+   */
+  public String getCatalogName() {
+    return catalogName;
+  }
+
+  /**
+   * Retrieves the name the catalog is registered under in Trino.
+   *
+   * @return the Trino catalog name
+   */
+  public String getTrinoCatalogName() {
+    return trinoCatalogName;
+  }
+
+  /**
+   * Retrieves the catalog provider.
+   *
+   * @return the provider, null if it could not be determined
+   */
+  @Nullable
+  public String getProvider() {
+    return provider;
+  }
+
+  /**
+   * Retrieves the registration status of the catalog.
+   *
+   * @return the status
+   */
+  public Status getStatus() {
+    return status;
+  }
+
+  /**
+   * Retrieves the error or the reason why the catalog is not registered.
+   *
+   * @return the message, null if the catalog is registered
+   */
+  @Nullable
+  public String getLastError() {
+    return lastError;
+  }
+
+  /**
+   * Retrieves the time of the last registration attempt.
+   *
+   * @return the time in milliseconds since the epoch
+   */
+  public long getLastAttemptTimeMs() {
+    return lastAttemptTimeMs;
+  }
+
+  /**
+   * Retrieves the time of the last successful registration.
+   *
+   * @return the time in milliseconds since the epoch, 0 if the catalog was 
never registered
+   */
+  public long getLastSuccessTimeMs() {
+    return lastSuccessTimeMs;
+  }
+
+  /**
+   * Retrieves the number of consecutive failed registration attempts.
+   *
+   * @return the failure count, 0 if the last attempt succeeded
+   */
+  public long getFailureCount() {
+    return failureCount;
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "CatalogRegistrationState{metalake=%s, catalog=%s, trinoCatalog=%s, 
provider=%s,"
+            + " status=%s, lastError=%s, failureCount=%d}",
+        metalake, catalogName, trinoCatalogName, provider, status, lastError, 
failureCount);
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/GravitinoSystemConnector.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/GravitinoSystemConnector.java
index 57ee95c0ea..8f5052ec93 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/GravitinoSystemConnector.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/GravitinoSystemConnector.java
@@ -129,7 +129,16 @@ public class GravitinoSystemConnector implements Connector 
{
 
       SchemaTableName tableName =
           ((GravitinoSystemConnectorMetadata.SystemTableHandle) 
table).getName();
-      return 
createPageSource(GravitinoSystemTableFactory.loadPageData(tableName));
+      Page page = GravitinoSystemTableFactory.loadPageData(tableName);
+
+      // Project the page down to the requested columns. Trino only expects 
the columns it asked
+      // for, so handing it the whole row breaks any query that is not a 
SELECT *.
+      int[] channels = new int[columns.size()];
+      for (int i = 0; i < channels.length; i++) {
+        channels[i] =
+            ((GravitinoSystemConnectorMetadata.SystemColumnHandle) 
columns.get(i)).getIndex();
+      }
+      return createPageSource(page.getColumns(channels));
     }
 
     protected ConnectorPageSource createPageSource(Page page) {
@@ -183,14 +192,30 @@ public class GravitinoSystemConnector implements 
Connector {
       return tableName;
     }
 
+    // The system table data lives on the coordinator only: the catalog load 
loop runs there, and
+    // the registration state it records is never replicated to workers. 
Splits are built in
+    // SplitManager.getSplits(), which also runs on the coordinator, so this 
is always set by the
+    // time it is read. It stays null on worker JVMs, where the behaviour is 
unchanged.
+    private static volatile HostAddress coordinatorAddress;
+
+    /**
+     * Sets the coordinator address that system table splits are pinned to.
+     *
+     * @param address the host and port of the Trino coordinator
+     */
+    public static void setCoordinatorAddress(HostAddress address) {
+      coordinatorAddress = address;
+    }
+
     @Override
     public boolean isRemotelyAccessible() {
-      return true;
+      return coordinatorAddress == null;
     }
 
     @Override
     public List<HostAddress> getAddresses() {
-      return Collections.emptyList();
+      HostAddress address = coordinatorAddress;
+      return address == null ? Collections.emptyList() : List.of(address);
     }
   }
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
index 7126568911..caf469e06f 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/AlterCatalogStoredProcedure.java
@@ -137,15 +137,15 @@ public class AlterCatalogStoredProcedure extends 
GravitinoStoredProcedure {
           .alterCatalog(catalogName, changes.toArray(changes.toArray(new 
CatalogChange[0])));
 
       catalogConnectorManager.loadMetalakeSync();
-      catalogConnectorContext =
-          catalogConnectorManager.getCatalogConnector(
-              catalogConnectorManager.getTrinoCatalogName(metalake, 
catalogName));
+      String trinoCatalogName = 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName);
+      catalogConnectorContext = 
catalogConnectorManager.getCatalogConnector(trinoCatalogName);
       if (catalogConnectorContext == null
           || catalogConnectorContext.getCatalog().getLastModifiedTime()
               == oldCatalog.getLastModifiedTime()) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_OPERATION_FAILED,
-            "Update catalog failed due to the reloading process fails");
+            "Update catalog failed due to the reloading process fails. "
+                + 
catalogConnectorManager.describeRegistrationFailure(trinoCatalogName));
       }
       LOG.info("Alter catalog {} in metalake {} successfully.", catalogName, 
metalake);
 
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
index 0de2383d95..4fb8438a3a 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/storedprocedure/CreateCatalogStoredProcedure.java
@@ -115,11 +115,12 @@ public class CreateCatalogStoredProcedure extends 
GravitinoStoredProcedure {
               catalogName, Catalog.Type.RELATIONAL, provider, "Trino created", 
properties);
 
       catalogConnectorManager.loadMetalakeSync();
-      if (!catalogConnectorManager.catalogConnectorExist(
-          catalogConnectorManager.getTrinoCatalogName(metalake, catalogName))) 
{
+      String trinoCatalogName = 
catalogConnectorManager.getTrinoCatalogName(metalake, catalogName);
+      if (!catalogConnectorManager.catalogConnectorExist(trinoCatalogName)) {
         throw new TrinoException(
             GravitinoErrorCode.GRAVITINO_OPERATION_FAILED,
-            "Create catalog failed due to the loading process fails");
+            "Create catalog failed due to the loading process fails. "
+                + 
catalogConnectorManager.describeRegistrationFailure(trinoCatalogName));
       }
 
       LOG.info("Create catalog {} in metalake {} successfully.", catalogName, 
metalake);
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTable.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTable.java
index eb2328b0b5..136ec9290c 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTable.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTable.java
@@ -18,8 +18,13 @@
  */
 package org.apache.gravitino.trino.connector.system.table;
 
+import static io.trino.spi.type.VarcharType.VARCHAR;
+
 import io.trino.spi.Page;
+import io.trino.spi.block.BlockBuilder;
 import io.trino.spi.connector.ConnectorTableMetadata;
+import java.time.Instant;
+import javax.annotation.Nullable;
 
 /** Gravitino System table interfaces */
 public abstract class GravitinoSystemTable {
@@ -40,4 +45,34 @@ public abstract class GravitinoSystemTable {
    * @return a Page object containing all the table data
    */
   public abstract Page loadPageData();
+
+  /**
+   * Appends a string to a VARCHAR column, writing a null when the value is 
absent.
+   *
+   * @param builder the column builder to append to
+   * @param value the value to append, may be null
+   */
+  protected static void writeNullableString(BlockBuilder builder, @Nullable 
String value) {
+    if (value == null) {
+      builder.appendNull();
+    } else {
+      VARCHAR.writeString(builder, value);
+    }
+  }
+
+  /**
+   * Appends a timestamp to a VARCHAR column as an ISO-8601 UTC string, 
writing a null when the
+   * timestamp is unset. Timestamps are rendered as strings because the block 
encoding of
+   * TimestampType differs across the supported Trino versions.
+   *
+   * @param builder the column builder to append to
+   * @param timeMs the time in milliseconds since the epoch, 0 when unset
+   */
+  protected static void writeTime(BlockBuilder builder, long timeMs) {
+    if (timeMs == 0) {
+      builder.appendNull();
+    } else {
+      VARCHAR.writeString(builder, Instant.ofEpochMilli(timeMs).toString());
+    }
+  }
 }
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
new file mode 100644
index 0000000000..2d2722f506
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableCatalogStatus.java
@@ -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.
+ */
+package org.apache.gravitino.trino.connector.system.table;
+
+import static io.trino.spi.type.BigintType.BIGINT;
+import static io.trino.spi.type.VarcharType.VARCHAR;
+
+import io.trino.spi.Page;
+import io.trino.spi.block.BlockBuilder;
+import io.trino.spi.connector.ColumnMetadata;
+import io.trino.spi.connector.ConnectorTableMetadata;
+import io.trino.spi.connector.SchemaTableName;
+import java.util.List;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+import org.apache.gravitino.trino.connector.catalog.CatalogRegistrationState;
+
+/**
+ * An implementation of the catalog status system table.
+ *
+ * <p>It reports why every Apache Gravitino catalog is or is not registered in 
Trino, so that a
+ * catalog missing from SHOW CATALOGS can be diagnosed without reading the 
coordinator log.
+ */
+public class GravitinoSystemTableCatalogStatus extends GravitinoSystemTable {
+
+  /** The name of the catalog status system table. */
+  public static final SchemaTableName TABLE_NAME =
+      new SchemaTableName(SYSTEM_TABLE_SCHEMA_NAME, "catalog_status");
+
+  private static final ConnectorTableMetadata TABLE_METADATA =
+      new ConnectorTableMetadata(
+          TABLE_NAME,
+          List.of(
+              
ColumnMetadata.builder().setName("metalake").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("catalog_name").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("trino_catalog_name").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("provider").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("status").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("last_error").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("last_attempt_time").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("last_success_time").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("failure_count").setType(BIGINT).build()));
+
+  private final CatalogConnectorManager catalogConnectorManager;
+
+  /**
+   * Constructs a new GravitinoSystemTableCatalogStatus.
+   *
+   * @param catalogConnectorManager the manager for catalog connectors
+   */
+  public GravitinoSystemTableCatalogStatus(CatalogConnectorManager 
catalogConnectorManager) {
+    this.catalogConnectorManager = catalogConnectorManager;
+  }
+
+  @Override
+  public Page loadPageData() {
+    // Take a snapshot first, the load loop writes these states concurrently 
and the column
+    // builders must all end up with the same number of positions.
+    List<CatalogRegistrationState> states = 
catalogConnectorManager.getCatalogRegistrationStates();
+    int size = states.size();
+
+    BlockBuilder metalakeColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
+    BlockBuilder catalogNameColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
+    BlockBuilder trinoCatalogNameColumnBuilder = 
VARCHAR.createBlockBuilder(null, size);
+    BlockBuilder providerColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
+    BlockBuilder statusColumnBuilder = VARCHAR.createBlockBuilder(null, size);
+    BlockBuilder lastErrorColumnBuilder = VARCHAR.createBlockBuilder(null, 
size);
+    BlockBuilder lastAttemptTimeColumnBuilder = 
VARCHAR.createBlockBuilder(null, size);
+    BlockBuilder lastSuccessTimeColumnBuilder = 
VARCHAR.createBlockBuilder(null, size);
+    BlockBuilder failureCountColumnBuilder = BIGINT.createBlockBuilder(null, 
size);
+
+    for (CatalogRegistrationState state : states) {
+      VARCHAR.writeString(metalakeColumnBuilder, state.getMetalake());
+      VARCHAR.writeString(catalogNameColumnBuilder, state.getCatalogName());
+      VARCHAR.writeString(trinoCatalogNameColumnBuilder, 
state.getTrinoCatalogName());
+      writeNullableString(providerColumnBuilder, state.getProvider());
+      VARCHAR.writeString(statusColumnBuilder, state.getStatus().name());
+      writeNullableString(lastErrorColumnBuilder, state.getLastError());
+      writeTime(lastAttemptTimeColumnBuilder, state.getLastAttemptTimeMs());
+      writeTime(lastSuccessTimeColumnBuilder, state.getLastSuccessTimeMs());
+      BIGINT.writeLong(failureCountColumnBuilder, state.getFailureCount());
+    }
+
+    return new Page(
+        size,
+        metalakeColumnBuilder.build(),
+        catalogNameColumnBuilder.build(),
+        trinoCatalogNameColumnBuilder.build(),
+        providerColumnBuilder.build(),
+        statusColumnBuilder.build(),
+        lastErrorColumnBuilder.build(),
+        lastAttemptTimeColumnBuilder.build(),
+        lastSuccessTimeColumnBuilder.build(),
+        failureCountColumnBuilder.build());
+  }
+
+  @Override
+  public ConnectorTableMetadata getTableMetaData() {
+    return TABLE_METADATA;
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
index 4995e69821..832e89b78d 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableFactory.java
@@ -49,6 +49,12 @@ public class GravitinoSystemTableFactory {
     SYSTEM_TABLES.put(
         GravitinoSystemTableCatalog.TABLE_NAME,
         new GravitinoSystemTableCatalog(catalogConnectorManager));
+    SYSTEM_TABLES.put(
+        GravitinoSystemTableCatalogStatus.TABLE_NAME,
+        new GravitinoSystemTableCatalogStatus(catalogConnectorManager));
+    SYSTEM_TABLES.put(
+        GravitinoSystemTableLoadStatus.TABLE_NAME,
+        new GravitinoSystemTableLoadStatus(catalogConnectorManager));
   }
 
   /**
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
new file mode 100644
index 0000000000..dc4cbfe882
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/system/table/GravitinoSystemTableLoadStatus.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.trino.connector.system.table;
+
+import static io.trino.spi.type.BigintType.BIGINT;
+import static io.trino.spi.type.BooleanType.BOOLEAN;
+import static io.trino.spi.type.VarcharType.VARCHAR;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.trino.spi.Page;
+import io.trino.spi.TrinoException;
+import io.trino.spi.block.BlockBuilder;
+import io.trino.spi.connector.ColumnMetadata;
+import io.trino.spi.connector.ConnectorTableMetadata;
+import io.trino.spi.connector.SchemaTableName;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+
+/**
+ * An implementation of the load status system table.
+ *
+ * <p>It reports the health of the loop that registers Apache Gravitino 
catalogs into Trino. A
+ * failure that prevents the loop from listing catalogs at all, such as an 
unreachable Gravitino
+ * server, has no catalog to attach itself to and is only visible here.
+ */
+public class GravitinoSystemTableLoadStatus extends GravitinoSystemTable {
+
+  /** The name of the load status system table. */
+  public static final SchemaTableName TABLE_NAME =
+      new SchemaTableName(SYSTEM_TABLE_SCHEMA_NAME, "load_status");
+
+  private static final ConnectorTableMetadata TABLE_METADATA =
+      new ConnectorTableMetadata(
+          TABLE_NAME,
+          List.of(
+              
ColumnMetadata.builder().setName("trino_started").setType(BOOLEAN).build(),
+              
ColumnMetadata.builder().setName("last_attempt_time").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("last_success_time").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("consecutive_failures").setType(BIGINT).build(),
+              
ColumnMetadata.builder().setName("last_error").setType(VARCHAR).build(),
+              
ColumnMetadata.builder().setName("metalake_errors").setType(VARCHAR).build()));
+
+  private final CatalogConnectorManager catalogConnectorManager;
+
+  /**
+   * Constructs a new GravitinoSystemTableLoadStatus.
+   *
+   * @param catalogConnectorManager the manager for catalog connectors
+   */
+  public GravitinoSystemTableLoadStatus(CatalogConnectorManager 
catalogConnectorManager) {
+    this.catalogConnectorManager = catalogConnectorManager;
+  }
+
+  @Override
+  public Page loadPageData() {
+    BlockBuilder trinoStartedColumnBuilder = BOOLEAN.createBlockBuilder(null, 
1);
+    BlockBuilder lastAttemptTimeColumnBuilder = 
VARCHAR.createBlockBuilder(null, 1);
+    BlockBuilder lastSuccessTimeColumnBuilder = 
VARCHAR.createBlockBuilder(null, 1);
+    BlockBuilder consecutiveFailuresColumnBuilder = 
BIGINT.createBlockBuilder(null, 1);
+    BlockBuilder lastErrorColumnBuilder = VARCHAR.createBlockBuilder(null, 1);
+    BlockBuilder metalakeErrorsColumnBuilder = 
VARCHAR.createBlockBuilder(null, 1);
+
+    BOOLEAN.writeBoolean(trinoStartedColumnBuilder, 
catalogConnectorManager.isTrinoStarted());
+    writeTime(lastAttemptTimeColumnBuilder, 
catalogConnectorManager.getLastLoadAttemptTimeMs());
+    writeTime(lastSuccessTimeColumnBuilder, 
catalogConnectorManager.getLastSuccessfulLoadTimeMs());
+    BIGINT.writeLong(
+        consecutiveFailuresColumnBuilder, 
catalogConnectorManager.getConsecutiveLoadFailures());
+    writeNullableString(lastErrorColumnBuilder, 
catalogConnectorManager.getLastLoadError());
+
+    Map<String, String> metalakeErrors = 
catalogConnectorManager.getMetalakeErrors();
+    if (metalakeErrors.isEmpty()) {
+      metalakeErrorsColumnBuilder.appendNull();
+    } else {
+      try {
+        VARCHAR.writeString(
+            metalakeErrorsColumnBuilder,
+            new ObjectMapper().writeValueAsString(new 
TreeMap<>(metalakeErrors)));
+      } catch (JsonProcessingException e) {
+        throw new TrinoException(
+            GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT, "Invalid metalake 
error format", e);
+      }
+    }
+
+    return new Page(
+        1,
+        trinoStartedColumnBuilder.build(),
+        lastAttemptTimeColumnBuilder.build(),
+        lastSuccessTimeColumnBuilder.build(),
+        consecutiveFailuresColumnBuilder.build(),
+        lastErrorColumnBuilder.build(),
+        metalakeErrorsColumnBuilder.build());
+  }
+
+  @Override
+  public ConnectorTableMetadata getTableMetaData() {
+    return TABLE_METADATA;
+  }
+}
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnector.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnector.java
index fecc1fd9cc..2fbd12c4c1 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnector.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnector.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.trino.connector;
 import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 import com.google.common.base.Preconditions;
 import io.trino.testing.DistributedQueryRunner;
@@ -282,7 +283,10 @@ public abstract class TestGravitinoConnector extends 
AbstractGravitinoConnectorT
     assertQueryFails(
         "call gravitino.system.create_catalog("
             + "catalog=>'memory1', provider=>'memory', properties => 
Map(array['trino.bypass.unknown-direct-key'], array['10']))",
-        format("Create catalog failed. Create catalog failed due to the 
loading process fails"));
+        // The message must carry the reason the registration failed, not just 
the fact that it
+        // did. (?s) lets .* span the newlines of the underlying configuration 
error.
+        "(?s)Create catalog failed. Create catalog failed due to the loading 
process fails\\."
+            + ".*unknown-direct-key.*");
     assertThat(computeActual("show 
catalogs").getOnlyColumnAsSet()).doesNotContain("memory1");
 
     assertUpdate(
@@ -306,6 +310,36 @@ public abstract class TestGravitinoConnector extends 
AbstractGravitinoConnectorT
     assertEquals(row.getField(2), "{\"max_ttl\":\"10\"}");
   }
 
+  @Test
+  public void testCatalogStatusSystemTable() throws Exception {
+    MaterializedResult result =
+        computeActual(
+            "select metalake, catalog_name, trino_catalog_name, provider, 
status, last_error,"
+                + " failure_count from gravitino.system.catalog_status");
+    assertEquals(result.getRowCount(), 1);
+    MaterializedRow row = result.getMaterializedRows().get(0);
+    assertEquals(row.getField(1), "memory");
+    assertEquals(row.getField(2), "memory");
+    assertEquals(row.getField(3), "memory");
+    assertEquals(row.getField(4), "REGISTERED");
+    assertNull(row.getField(5));
+    assertEquals(row.getField(6), 0L);
+  }
+
+  @Test
+  public void testLoadStatusSystemTable() throws Exception {
+    MaterializedResult result =
+        computeActual(
+            "select trino_started, consecutive_failures, last_error, 
metalake_errors"
+                + " from gravitino.system.load_status");
+    assertEquals(result.getRowCount(), 1);
+    MaterializedRow row = result.getMaterializedRows().get(0);
+    assertEquals(row.getField(0), true);
+    assertEquals(row.getField(1), 0L);
+    assertNull(row.getField(2));
+    assertNull(row.getField(3));
+  }
+
   private TableName createTestTable(String fullTableName) throws Exception {
     TableName tableName = new TableName(fullTableName);
 
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
index 995aedc29d..bc6f42f7ea 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/catalog/TestCatalogConnectorManager.java
@@ -21,9 +21,12 @@ package org.apache.gravitino.trino.connector.catalog;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -31,12 +34,18 @@ import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import io.trino.spi.TrinoException;
 import io.trino.spi.connector.ConnectorContext;
 import java.util.Optional;
 import org.apache.gravitino.client.GravitinoAdminClient;
 import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.exceptions.RESTException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.Catalog;
 import org.apache.gravitino.trino.connector.GravitinoConfig;
 import org.apache.gravitino.trino.connector.GravitinoErrorCode;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
@@ -269,6 +278,185 @@ public class TestCatalogConnectorManager {
     assertEquals("http://irc-host:9001/iceberg";, 
config.getDiscoveredIcebergRestUri("test"));
   }
 
+  @Test
+  public void testSuccessfulRegistrationIsRecorded() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals(CatalogRegistrationState.Status.REGISTERED, 
state.getStatus());
+    assertEquals("test", state.getMetalake());
+    assertEquals("memory", state.getCatalogName());
+    assertEquals("memory", state.getTrinoCatalogName());
+    assertEquals("memory", state.getProvider());
+    assertNull(state.getLastError());
+    assertEquals(0, state.getFailureCount());
+    assertTrue(state.getLastSuccessTimeMs() > 0);
+
+    assertTrue(manager.isTrinoStarted());
+    assertNull(manager.getLastLoadError());
+    assertEquals(0, manager.getConsecutiveLoadFailures());
+    assertTrue(manager.getMetalakeErrors().isEmpty());
+  }
+
+  @Test
+  public void testRegistrationFailureIsRecorded() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+    doThrow(
+            new TrinoException(
+                GravitinoErrorCode.GRAVITINO_RUNTIME_ERROR,
+                "Access Denied: Cannot create catalog memory"))
+        .when(fixture.catalogRegister)
+        .registerCatalog(any(), any());
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals(CatalogRegistrationState.Status.FAILED, state.getStatus());
+    assertTrue(state.getLastError().contains("Access Denied"));
+    assertEquals(1, state.getFailureCount());
+    assertEquals(0, state.getLastSuccessTimeMs());
+
+    // A second failing round must accumulate rather than reset the failure 
count.
+    manager.loadMetalakeSync();
+    state = singleState(manager);
+    assertEquals(2, state.getFailureCount());
+    assertEquals(0, state.getLastSuccessTimeMs());
+  }
+
+  @Test
+  public void testRegistrationFailureIsReportedByStoredProcedureMessage() 
throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+    doThrow(
+            new TrinoException(
+                GravitinoErrorCode.GRAVITINO_RUNTIME_ERROR,
+                "Access Denied: Cannot create catalog memory"))
+        .when(fixture.catalogRegister)
+        .registerCatalog(any(), any());
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    String description = manager.describeRegistrationFailure("memory");
+    assertTrue(description.contains("FAILED"));
+    assertTrue(description.contains("Access Denied"));
+  }
+
+  @Test
+  public void testNonRelationalCatalogIsRecorded() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("files", "hadoop", Catalog.Type.FILESET));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals(CatalogRegistrationState.Status.UNSUPPORTED, 
state.getStatus());
+    assertTrue(state.getLastError().contains("FILESET"));
+  }
+
+  @Test
+  public void testUnsupportedProviderIsRecorded() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("other", "unknown", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals(CatalogRegistrationState.Status.UNSUPPORTED, 
state.getStatus());
+    assertTrue(state.getLastError().contains("unknown"));
+    assertEquals("unknown", state.getProvider());
+  }
+
+  @Test
+  public void testSkippedCatalogIsRecorded() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager =
+        
fixture.createManager(ImmutableMap.of("gravitino.trino.skip-catalog-patterns", 
"mem.*"));
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals(CatalogRegistrationState.Status.SKIPPED, state.getStatus());
+    assertTrue(state.getLastError().contains("skip-catalog-patterns"));
+  }
+
+  @Test
+  public void testStaleStateIsPruned() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    Catalog first = mockCatalog("a", "memory", Catalog.Type.RELATIONAL);
+    Catalog second = mockCatalog("b", "memory", Catalog.Type.RELATIONAL);
+    fixture.withCatalogs(first, second);
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+    assertEquals(2, manager.getCatalogRegistrationStates().size());
+
+    // The second catalog is dropped in the Gravitino server, its state must 
not linger.
+    fixture.withCatalogs(first);
+    manager.loadMetalakeSync();
+
+    CatalogRegistrationState state = singleState(manager);
+    assertEquals("a", state.getCatalogName());
+  }
+
+  @Test
+  public void testListCatalogsFailureIsRecordedPerMetalake() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    fixture.withCatalogs(mockCatalog("memory", "memory", 
Catalog.Type.RELATIONAL));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+    assertEquals(CatalogRegistrationState.Status.REGISTERED, 
singleState(manager).getStatus());
+
+    when(fixture.metalake.listCatalogs()).thenThrow(new 
RuntimeException("Connection refused"));
+    manager.loadMetalakeSync();
+
+    Map<String, String> metalakeErrors = manager.getMetalakeErrors();
+    assertEquals(1, metalakeErrors.size());
+    assertTrue(metalakeErrors.get("test").contains("Connection refused"));
+    // A transient listing failure must not turn a healthy catalog into a 
failed one.
+    assertEquals(CatalogRegistrationState.Status.REGISTERED, 
singleState(manager).getStatus());
+  }
+
+  @Test
+  public void testUnreachableServerIsRecordedInLoadStatus() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    when(fixture.client.loadMetalake(any())).thenThrow(new 
RuntimeException("Connection refused"));
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    assertNotNull(manager.getLastLoadError());
+    assertTrue(manager.getLastLoadError().contains("Connection refused"));
+    assertEquals(1, manager.getConsecutiveLoadFailures());
+    assertEquals(0, manager.getLastSuccessfulLoadTimeMs());
+    assertTrue(manager.getLastLoadAttemptTimeMs() > 0);
+    assertTrue(manager.getCatalogRegistrationStates().isEmpty());
+  }
+
+  @Test
+  public void testTrinoNotStartedIsRecordedInLoadStatus() throws Exception {
+    LoadFixture fixture = new LoadFixture();
+    when(fixture.catalogRegister.isTrinoStarted()).thenReturn(false);
+
+    CatalogConnectorManager manager = fixture.createManager(ImmutableMap.of());
+    manager.loadMetalakeSync();
+
+    assertFalse(manager.isTrinoStarted());
+    assertNotNull(manager.getLastLoadError());
+    assertTrue(manager.getLastLoadError().contains("Waiting for the Trino 
server"));
+    assertTrue(manager.getCatalogRegistrationStates().isEmpty());
+  }
+
   private CatalogConnectorManager createManager(ImmutableMap<String, String> 
configMap)
       throws Exception {
     return createManager(createCatalogConnectorFactory(), configMap);
@@ -323,4 +511,66 @@ public class TestCatalogConnectorManager {
   private static ConnectorContext mockContext() {
     return mock(ConnectorContext.class);
   }
+
+  private static CatalogRegistrationState singleState(CatalogConnectorManager 
manager) {
+    List<CatalogRegistrationState> states = 
manager.getCatalogRegistrationStates();
+    assertEquals(1, states.size());
+    return states.get(0);
+  }
+
+  private static Catalog mockCatalog(String name, String provider, 
Catalog.Type type) {
+    Catalog catalog = mock(Catalog.class);
+    when(catalog.name()).thenReturn(name);
+    when(catalog.provider()).thenReturn(provider);
+    when(catalog.type()).thenReturn(type);
+    when(catalog.properties()).thenReturn(ImmutableMap.of());
+    Audit audit = mock(Audit.class);
+    when(audit.createTime()).thenReturn(Instant.now());
+    when(audit.lastModifiedTime()).thenReturn(null);
+    when(catalog.auditInfo()).thenReturn(audit);
+    return catalog;
+  }
+
+  /** Wires up a manager whose load loop can be driven with {@code 
loadMetalakeSync()}. */
+  private static class LoadFixture {
+    private final CatalogRegister catalogRegister = 
mock(CatalogRegister.class);
+    private final GravitinoAdminClient client = 
mock(GravitinoAdminClient.class);
+    private final GravitinoMetalake metalake = mock(GravitinoMetalake.class);
+    private final CatalogConnectorFactory catalogFactory = 
mock(CatalogConnectorFactory.class);
+
+    LoadFixture() throws Exception {
+      when(catalogRegister.isTrinoStarted()).thenReturn(true);
+      when(metalake.name()).thenReturn("test");
+      when(client.loadMetalake(any())).thenReturn(metalake);
+      
when(catalogFactory.getSupportedCatalogProviders()).thenReturn(ImmutableSet.of("memory"));
+      CatalogConnectorContext.Builder builder = 
mock(CatalogConnectorContext.Builder.class);
+      
when(catalogFactory.createCatalogConnectorContextBuilder(any())).thenReturn(builder);
+      when(builder.withMetalake(any())).thenReturn(builder);
+      when(builder.withContext(any())).thenReturn(builder);
+      when(builder.build()).thenReturn(mock(CatalogConnectorContext.class));
+    }
+
+    void withCatalogs(Catalog... catalogs) {
+      String[] names = new String[catalogs.length];
+      for (int i = 0; i < catalogs.length; i++) {
+        names[i] = catalogs[i].name();
+        when(metalake.loadCatalog(names[i])).thenReturn(catalogs[i]);
+      }
+      when(metalake.listCatalogs()).thenReturn(names);
+    }
+
+    CatalogConnectorManager createManager(Map<String, String> extraConfig) {
+      ImmutableMap<String, String> configMap =
+          ImmutableMap.<String, String>builder()
+              .put("gravitino.uri", "http://127.0.0.1:8090";)
+              .put("gravitino.metalake", "test")
+              .put("gravitino.use-single-metalake", "true")
+              .putAll(extraConfig)
+              .build();
+      CatalogConnectorManager manager =
+          new CatalogConnectorManager(catalogRegister, catalogFactory, null);
+      manager.config(new GravitinoConfig(configMap), client);
+      return manager;
+    }
+  }
 }
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
new file mode 100644
index 0000000000..7139f1a163
--- /dev/null
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/system/table/TestGravitinoSystemStatusTables.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.trino.connector.system.table;
+
+import static io.trino.spi.type.BigintType.BIGINT;
+import static io.trino.spi.type.BooleanType.BOOLEAN;
+import static io.trino.spi.type.VarcharType.VARCHAR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import io.trino.spi.Page;
+import io.trino.spi.block.Block;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.trino.connector.catalog.CatalogConnectorManager;
+import org.apache.gravitino.trino.connector.catalog.CatalogRegistrationState;
+import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
+import org.junit.jupiter.api.Test;
+
+public class TestGravitinoSystemStatusTables {
+
+  @Test
+  public void testCatalogStatusTableRendersRegisteredCatalog() {
+    GravitinoCatalog catalog =
+        new GravitinoCatalog("test", "memory", "memory", ImmutableMap.of(), 
0L);
+    CatalogRegistrationState state = 
CatalogRegistrationState.succeeded(catalog, "memory");
+
+    Page page = loadCatalogStatusPage(List.of(state));
+
+    assertEquals(1, page.getPositionCount());
+    assertEquals(9, page.getChannelCount());
+    assertEquals("test", varchar(page, 0));
+    assertEquals("memory", varchar(page, 1));
+    assertEquals("memory", varchar(page, 2));
+    assertEquals("memory", varchar(page, 3));
+    assertEquals("REGISTERED", varchar(page, 4));
+    // last_error is null for a registered catalog, last_success_time is set.
+    assertTrue(page.getBlock(5).isNull(0));
+    assertFalse(page.getBlock(7).isNull(0));
+    assertEquals(0, BIGINT.getLong(page.getBlock(8), 0));
+  }
+
+  @Test
+  public void testCatalogStatusTableRendersFailedCatalog() {
+    CatalogRegistrationState state =
+        CatalogRegistrationState.failed(
+            "test", "memory", "memory", null, "Access Denied: Cannot create 
catalog memory", null);
+
+    Page page = loadCatalogStatusPage(List.of(state));
+
+    assertEquals("FAILED", varchar(page, 4));
+    assertTrue(varchar(page, 5).contains("Access Denied"));
+    // The provider is unknown on this path, and the catalog was never 
registered.
+    assertTrue(page.getBlock(3).isNull(0));
+    assertTrue(page.getBlock(7).isNull(0));
+    assertEquals(1, BIGINT.getLong(page.getBlock(8), 0));
+  }
+
+  @Test
+  public void testCatalogStatusTableIsEmptyWhenNoCatalogWasSeen() {
+    Page page = loadCatalogStatusPage(List.of());
+    assertEquals(0, page.getPositionCount());
+  }
+
+  @Test
+  public void testLoadStatusTableRendersHealthyLoop() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    when(manager.isTrinoStarted()).thenReturn(true);
+    when(manager.getLastLoadAttemptTimeMs()).thenReturn(1000L);
+    when(manager.getLastSuccessfulLoadTimeMs()).thenReturn(1000L);
+    when(manager.getConsecutiveLoadFailures()).thenReturn(0L);
+    when(manager.getLastLoadError()).thenReturn(null);
+    when(manager.getMetalakeErrors()).thenReturn(Map.of());
+
+    Page page = new GravitinoSystemTableLoadStatus(manager).loadPageData();
+
+    assertEquals(1, page.getPositionCount());
+    assertEquals(6, page.getChannelCount());
+    assertTrue(BOOLEAN.getBoolean(page.getBlock(0), 0));
+    assertEquals("1970-01-01T00:00:01Z", varchar(page, 1));
+    assertEquals(0, BIGINT.getLong(page.getBlock(3), 0));
+    assertTrue(page.getBlock(4).isNull(0));
+    assertTrue(page.getBlock(5).isNull(0));
+  }
+
+  @Test
+  public void testLoadStatusTableRendersUnreachableServer() {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    when(manager.isTrinoStarted()).thenReturn(true);
+    when(manager.getLastLoadAttemptTimeMs()).thenReturn(2000L);
+    when(manager.getLastSuccessfulLoadTimeMs()).thenReturn(0L);
+    when(manager.getConsecutiveLoadFailures()).thenReturn(3L);
+    when(manager.getLastLoadError()).thenReturn("Connection refused");
+    when(manager.getMetalakeErrors()).thenReturn(Map.of("test", "Connection 
refused"));
+
+    Page page = new GravitinoSystemTableLoadStatus(manager).loadPageData();
+
+    // last_success_time stays null while the server is unreachable.
+    assertTrue(page.getBlock(2).isNull(0));
+    assertEquals(3, BIGINT.getLong(page.getBlock(3), 0));
+    assertEquals("Connection refused", varchar(page, 4));
+    assertEquals("{\"test\":\"Connection refused\"}", varchar(page, 5));
+  }
+
+  private static Page loadCatalogStatusPage(List<CatalogRegistrationState> 
states) {
+    CatalogConnectorManager manager = mock(CatalogConnectorManager.class);
+    when(manager.getCatalogRegistrationStates()).thenReturn(states);
+    return new GravitinoSystemTableCatalogStatus(manager).loadPageData();
+  }
+
+  private static String varchar(Page page, int channel) {
+    Block block = page.getBlock(channel);
+    return VARCHAR.getSlice(block, 0).toStringUtf8();
+  }
+}

Reply via email to