Repository: apache/ozone
Pull request #8264: HDDS-12697. Ozone debug CLI command to display details of a single container
Author: login: sreejasahithi
Labels: name: tools
description: Tools that helps with debugging
Created: 2025-04-11T04:35:36Z | Merged: 2025-05-06T15:23:20Z

## What changes were proposed in this pull request?
This update introduces a new Ozone debug CLI command that displays detailed information about a specific container across all datanodes where it is present.
it displays the following information for the particular container:

> - Container id 
> - Datanode id 
> - State 
> - BCSID
> - Timestamp
> - error message if any
> - Analysis (Duplicate open states , Unhealthy , Mismatched and more)
                

Command is as follows:
`ozone debug log container --db=<path to db>  info <container id>`

sample output 1 for the command:
```
Timestamp                 | Container ID    | Datanode ID   | Container State  | BCSID  | Message                        | Index Value
--------------------------------------------------------------------------------------------------------------------------------------
2024-06-04 15:07:55,390   | 700             | 100           | QUASI_CLOSED     | 353807 | No error                       | 0
2024-06-04 14:50:18,177   | 700             | 200           | QUASI_CLOSED     | 353807 | No error                       | 0
2024-04-04 10:32:29,026   | 700             | 300           | OPEN             | 0      | No error                       | 0
2024-06-04 14:44:28,126   | 700             | 300           | CLOSING          | 353807 | No error                       | 0
2024-06-04 14:47:59,893   | 700             | 300           | QUASI_CLOSED     | 353807 | Ratis group removed            | 0
2024-06-04 14:50:17,038   | 700             | 300           | QUASI_CLOSED     | 353807 | No error                       | 0
2024-06-04 14:50:18,184   | 700             | 300           | QUASI_CLOSED     | 353807 | No error                       | 0
2024-04-04 10:32:29,026   | 700             | 350           | OPEN             | 0      | No error                       | 0

Container 700 is QUASI_CLOSED_STUCK.
```

sample output 2 for the command:
```
Timestamp                 | Container ID    | Datanode ID   | Container State  | BCSID  | Message                        | Index Value
--------------------------------------------------------------------------------------------------------------------------------------
2024-11-08 12:23:33,580   | 44998           | 120           | CLOSED           | 744081 | No error                       | 0
2025-01-15 00:02:16,463   | 44998           | 120           | CLOSED           | 744081 | Volume failure                 | 0
2024-10-08 14:32:37,024   | 44998           | 150           | OPEN             | 0      | No error                       | 0
2024-11-08 11:39:47,473   | 44998           | 150           | CLOSING          | 744081 | No error                       | 0
2024-11-08 11:39:47,474   | 44998           | 150           | QUASI_CLOSED     | 744081 | Ratis pipeline does not exist  | 0
2024-11-08 11:44:48,523   | 44998           | 150           | CLOSED           | 744081 | No error                       | 0
2024-10-08 14:32:37,035   | 44998           | 250           | OPEN             | 0      | No error                       | 0
2024-10-31 16:30:44,105   | 44998           | 250           | OPEN             | 744081 | Volume failure                 | 0
2024-10-08 14:32:37,037   | 44998           | 310           | OPEN             | 0      | No error                       | 0
2024-11-08 11:39:43,668   | 44998           | 310           | CLOSING          | 744081 | No error                       | 0
2024-11-08 11:39:44,945   | 44998           | 310           | QUASI_CLOSED     | 744081 | Ratis group removed            | 0
2024-11-08 11:44:45,979   | 44998           | 310           | CLOSED           | 744081 | No error                       | 0
2024-11-08 12:23:26,345   | 44998           | 310           | CLOSED           | 744081 | No error                       | 0

Container 44998 might have duplicate OPEN state.
```

## What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-12697

## How was this patch tested?
Below is the workflow link that passed successfully:
https://github.com/sreejasahithi/ozone/actions/runs/14394759787



Diff:
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
deleted file mode 100644
index 9dcf3fb71872..000000000000
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
+++ /dev/null
@@ -1,291 +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.
- */
-
-package org.apache.hadoop.ozone.containerlog.parser;
-
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.nio.charset.StandardCharsets;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.List;
-import org.sqlite.SQLiteConfig;
-
-/**
- * Handles creation and interaction with the database.
- * Provides methods for table creation, log data insertion, and index setup.
- */
-public class ContainerDatanodeDatabase {
-  
-  private static String databasePath;
-  
-  public static void setDatabasePath(String dbPath) {
-    if (databasePath == null) {
-      databasePath = dbPath;
-    }
-  }
-
-  private static Connection getConnection() throws Exception {
-    if (databasePath == null) {
-      throw new IllegalStateException("Database path not set");
-    }
-    
-    Class.forName(DBConsts.DRIVER);
-
-    SQLiteConfig config = new SQLiteConfig();
-
-    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
-    config.setCacheSize(DBConsts.CACHE_SIZE);
-    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
-    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
-    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
-
-    return DriverManager.getConnection(DBConsts.CONNECTION_PREFIX + databasePath, config.toProperties());
-  }
-
-  public void createDatanodeContainerLogTable() throws SQLException {
-    String createTableSQL = DBConsts.CREATE_DATANODE_CONTAINER_LOG_TABLE;
-    try (Connection connection = getConnection();
-         Statement dropStmt = connection.createStatement();
-         Statement createStmt = connection.createStatement()) {
-      dropTable(DBConsts.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
-      createStmt.execute(createTableSQL);
-      createDatanodeContainerIndex(createStmt);
-    } catch (SQLException e) {
-      System.err.println("Error while creating the table: " + e.getMessage());
-      throw e;
-    } catch (Exception e) {
-      System.err.println("Unexpected error: " + e.getMessage());
-      throw new RuntimeException(e);
-    }
-  }
-
-  private void createContainerLogTable() throws SQLException {
-    String createTableSQL = DBConsts.CREATE_CONTAINER_LOG_TABLE;
-    try (Connection connection = getConnection();
-         Statement dropStmt = connection.createStatement();
-         Statement createStmt = connection.createStatement()) {
-      dropTable(DBConsts.CONTAINER_LOG_TABLE_NAME, dropStmt);
-      createStmt.execute(createTableSQL);
-    } catch (SQLException e) {
-      System.err.println("Error while creating the table: " + e.getMessage());
-      throw e;
-    } catch (Exception e) {
-      System.err.println("Unexpected error: " + e.getMessage());
-      throw new RuntimeException(e);
-    }
-  }
-
-  /**
-   * Inserts a list of container log entries into the DatanodeContainerLogTable.
-   *
-   * @param transitionList List of container log entries to insert into the table.
-   */
-  
-  public synchronized void insertContainerDatanodeData(List<DatanodeContainerInfo> transitionList) throws SQLException {
-
-    String insertSQL = DBConsts.INSERT_DATANODE_CONTAINER_LOG;
-
-    long containerId = 0;
-    String datanodeId = null;
-    
-    try (Connection connection = getConnection();
-         PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {
-
-      int count = 0;
-
-      for (DatanodeContainerInfo info : transitionList) {
-        datanodeId = info.getDatanodeId();
-        containerId = info.getContainerId();
-
-        preparedStatement.setString(1, datanodeId);
-        preparedStatement.setLong(2, containerId);
-        preparedStatement.setString(3, info.getTimestamp());
-        preparedStatement.setString(4, info.getState());
-        preparedStatement.setLong(5, info.getBcsid());
-        preparedStatement.setString(6, info.getErrorMessage());
-        preparedStatement.setString(7, info.getLogLevel());
-        preparedStatement.setInt(8, info.getIndexValue());
-        preparedStatement.addBatch();
-
-        count++;
-
-        if (count % DBConsts.BATCH_SIZE == 0) {
-          preparedStatement.executeBatch();
-          count = 0;
-        }
-      }
-
-      if (count != 0) {
-        preparedStatement.executeBatch();
-      }
-    } catch (SQLException e) {
-      System.err.println("Failed to insert container log for container " + containerId + " on datanode " + datanodeId);
-      throw e;
-    } catch (Exception e) {
-      System.err.println("Unexpected error: " + e.getMessage());
-      throw new RuntimeException(e);
-    }
-  }
-
-  private void createDatanodeContainerIndex(Statement stmt) throws SQLException {
-    String createIndexSQL = DBConsts.CREATE_DATANODE_CONTAINER_INDEX;
-    stmt.execute(createIndexSQL);
-  }
-
-  /**
-   * Extracts the latest container log data from the DatanodeContainerLogTable
-   * and inserts it into ContainerLogTable.
-   */
-
-  public void insertLatestContainerLogData() throws SQLException {
-    createContainerLogTable();
-    String selectSQL = DBConsts.SELECT_LATEST_CONTAINER_LOG;
-    String insertSQL = DBConsts.INSERT_CONTAINER_LOG;
-
-    try (Connection connection = getConnection();
-         PreparedStatement selectStmt = connection.prepareStatement(selectSQL);
-         ResultSet resultSet = selectStmt.executeQuery();
-         PreparedStatement insertStmt = connection.prepareStatement(insertSQL)) {
-      
-      int count = 0;
-      
-      while (resultSet.next()) {
-        String datanodeId = resultSet.getString("datanode_id");
-        long containerId = resultSet.getLong("container_id");
-        String containerState = resultSet.getString("container_state");
-        long bcsid = resultSet.getLong("bcsid");
-        try {
-          insertStmt.setString(1, datanodeId);
-          insertStmt.setLong(2, containerId);
-          insertStmt.setString(3, containerState);
-          insertStmt.setLong(4, bcsid);
-          insertStmt.addBatch();
-
-          count++;
-
-          if (count % DBConsts.BATCH_SIZE == 0) {
-            insertStmt.executeBatch();
-            count = 0;
-          }
-        } catch (SQLException e) {
-          System.err.println("Failed to insert container log entry for container " + containerId + " on datanode "
-              + datanodeId);
-          throw e;
-        }
-      }
-
-      if (count != 0) {
-        insertStmt.executeBatch();
-      }
-    } catch (SQLException e) {
-      System.err.println("Failed to insert container log entry: " + e.getMessage());
-      throw e;
-    } catch (Exception e) {
-      System.err.println("Unexpected error: " + e.getMessage());
-      throw new RuntimeException(e);
-    }
-  }
-
-  private void dropTable(String tableName, Statement stmt) throws SQLException {
-    String dropTableSQL = DBConsts.DROP_TABLE.replace("{table_name}", tableName);
-    stmt.executeUpdate(dropTableSQL);
-  }
-
-  private void createContainerLogIndex(Statement stmt) throws SQLException {
-    String createIndexSQL = DBConsts.CREATE_INDEX_LATEST_STATE;
-    stmt.execute(createIndexSQL);
-  }
-
-  /**
-   * Lists containers filtered by the specified state and writes their details to stdout 
-   * unless redirected to a file explicitly.
-   * The output includes timestamp, datanode ID, container ID, BCSID, error message, and index value,
-   * written in a human-readable table format to a file or console.
-   * Behavior based on the {@code limit} parameter:
-   * If {@code limit} is provided, only up to the specified number of rows are printed.
-   * If the number of matching containers exceeds the {@code limit},
-   * a note is printed indicating more containers exist.
-   *
-   * @param state the container state to filter by (e.g., "OPEN", "CLOSED", "QUASI_CLOSED")
-   * @param limit the maximum number of rows to display; use {@link Integer#MAX_VALUE} to fetch all rows
-   */
-
-  public void listContainersByState(String state, Integer limit) throws SQLException {
-    int count = 0;
-
-    boolean limitProvided = limit != Integer.MAX_VALUE;
-
-    String baseQuery = DBConsts.SELECT_LATEST_CONTAINER_LOGS_BY_STATE;
-    String finalQuery = limitProvided ? baseQuery + " LIMIT ?" : baseQuery;
-
-    try (Connection connection = getConnection();
-         Statement stmt = connection.createStatement()) {
-
-      createContainerLogIndex(stmt);
-
-      try (PreparedStatement pstmt = connection.prepareStatement(finalQuery)) {
-        pstmt.setString(1, state);
-        if (limitProvided) {
-          pstmt.setInt(2, limit + 1);
-        }
-
-        try (ResultSet rs = pstmt.executeQuery();
-             PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out,
-                 StandardCharsets.UTF_8), true)) {
-
-          writer.printf("%-25s | %-35s | %-15s | %-15s | %-40s | %-12s%n",
-              "Timestamp", "Datanode ID", "Container ID", "BCSID", "Message", "Index Value");
-          writer.println("-------------------------------------------------------------------------------------" +
-              "---------------------------------------------------------------------------------------");
-
-          while (rs.next()) {
-            if (limitProvided && count >= limit) {
-              writer.println("Note: There might be more containers. Use -all option to list all entries");
-              break;
-            }
-            String timestamp = rs.getString("timestamp");
-            String datanodeId = rs.getString("datanode_id");
-            long containerId = rs.getLong("container_id");
-            long latestBcsid = rs.getLong("latest_bcsid");
-            String errorMessage = rs.getString("error_message");
-            int indexValue = rs.getInt("index_value");
-            count++;
-
-            writer.printf("%-25s | %-35s | %-15d | %-15d | %-40s | %-12d%n",
-                timestamp, datanodeId, containerId, latestBcsid, errorMessage, indexValue);
-          }
-          
-          if (count == 0) {
-            writer.printf("No containers found for state: %s%n", state);
-          } else {
-            writer.printf("Number of containers listed: %d%n", count);
-          }
-        }
-      }
-    } catch (SQLException e) {
-      throw new SQLException("Error while retrieving containers with state " + state);
-    } catch (Exception e) {
-      throw new RuntimeException(e);
-    }
-  }
-}
-
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/LogParser.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/LogParser.java
index 20847dde18b8..fe44955bf04f 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/LogParser.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/LogParser.java
@@ -31,8 +31,8 @@
     subcommands = {
         ContainerLogController.class
     },
-    description = "This serves as a common entry point for all commands that parse and analyze logs," +
-        "regardless of their source or type and require logs to be extracted first."
+    description = "Command to parse and analyze logs, " +
+        "regardless of their source or type. It require logs to be extracted first."
 )
 @MetaInfServices(DebugSubcommand.class)
 public class LogParser implements DebugSubcommand {
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
new file mode 100644
index 000000000000..7c5f8b32dc0d
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
@@ -0,0 +1,63 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container;
+
+import java.nio.file.Path;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "info",
+    description = "Provides complete state transition history of each replica for a single container along with " +
+        "analysis over the container"
+)
+public class ContainerInfoCommand extends AbstractSubcommand implements Callable<Void> {
+
+  @CommandLine.Parameters(index = "0", description = "Container ID")
+  private Long containerId;
+
+  @CommandLine.ParentCommand
+  private ContainerLogController parent;
+
+  @Override
+  public Void call() throws Exception {
+
+    if (containerId < 0) {
+      err().println("Invalid container ID: " + containerId);
+      return null;
+    }
+    
+    Path dbPath = parent.resolveDbPath();
+    if (dbPath == null) {
+      return null;
+    }
+
+    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase(dbPath.toString());
+ 
+    cdd.showContainerDetails(containerId);
+    
+    return null;
+  }
+
+}
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogController.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogController.java
index 4ba220c9d457..1d4972b9c0a1 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogController.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogController.java
@@ -17,6 +17,11 @@
 
 package org.apache.hadoop.ozone.debug.logs.container;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
 import picocli.CommandLine;
 
 /**
@@ -26,6 +31,7 @@
 @CommandLine.Command(
     name = "container",
     subcommands = {
+        ContainerInfoCommand.class,    
         ContainerLogParser.class,
         ListContainers.class
     },
@@ -33,7 +39,7 @@
             " Supports querying state transitions of container replicas using various subcommands."
 )
 
-public class ContainerLogController {
+public class ContainerLogController extends AbstractSubcommand {
   @CommandLine.Option(names = {"--db"},
       scope = CommandLine.ScopeType.INHERIT,
       description = "Path to the SQLite database file where the parsed information from logs is stored.")
@@ -46,4 +52,30 @@ public String getDbPath() {
   public void setDbPath(String dbPath) {
     this.dbPath = dbPath;
   }
+
+  public Path resolveDbPath() {
+    Path resolvedPath;
+
+    if (dbPath == null) {
+      resolvedPath = Paths.get(System.getProperty("user.dir"), SQLDBConstants.DEFAULT_DB_FILENAME);
+
+      if (Files.exists(resolvedPath) && Files.isRegularFile(resolvedPath)) {
+        out().println("Using default database file found in current directory: " + resolvedPath);
+      } else {
+        err().println("No database path provided and default file '" + SQLDBConstants.DEFAULT_DB_FILENAME + "' not " +
+            "found in current directory. Please provide a valid database path");
+        return null;
+      }
+    } else {
+      resolvedPath = Paths.get(dbPath);
+      Path parentDir = resolvedPath.getParent();
+
+      if (parentDir != null && !Files.exists(parentDir)) {
+        err().println("The parent directory of the provided database path does not exist: " + parentDir);
+        return null;
+      }
+    }
+
+    return resolvedPath;
+  }
 }
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogParser.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogParser.java
index df1288543bf1..e2d472e2e95f 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogParser.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogParser.java
@@ -21,9 +21,10 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.concurrent.Callable;
-import org.apache.hadoop.ozone.containerlog.parser.ContainerDatanodeDatabase;
-import org.apache.hadoop.ozone.containerlog.parser.ContainerLogFileParser;
-import org.apache.hadoop.ozone.containerlog.parser.DBConsts;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerLogFileParser;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
 import picocli.CommandLine;
 
 /**
@@ -35,7 +36,7 @@
     description = "Parse container logs and extract key details such as datanode ID, container ID, state, " +
             "BCSID, timestamp, log level, index value, and messages (if any)."
 )
-public class ContainerLogParser implements Callable<Void> {
+public class ContainerLogParser extends AbstractSubcommand implements Callable<Void> {
   private static final int DEFAULT_THREAD_COUNT = 10;
   
   @CommandLine.Option(names = {"--path"},
@@ -54,46 +55,42 @@ public class ContainerLogParser implements Callable<Void> {
   @Override
   public Void call() throws Exception {
     if (threadCount <= 0) {
-      System.out.println("Invalid threadCount value provided (" + threadCount + "). Using default value: "
+      out().println("Invalid threadCount value provided (" + threadCount + "). Using default value: "
           + DEFAULT_THREAD_COUNT);
       threadCount = DEFAULT_THREAD_COUNT;
     }
     
     Path logPath = Paths.get(path);
     if (!Files.exists(logPath) || !Files.isDirectory(logPath)) {
-      System.err.println("Invalid path provided: " + path);
+      err().println("Invalid path provided: " + path);
       return null;
     }
     
     Path providedDbPath;
     if (parent.getDbPath() == null) {
-      providedDbPath = Paths.get(System.getProperty("user.dir"), DBConsts.DEFAULT_DB_FILENAME);
+      providedDbPath = Paths.get(System.getProperty("user.dir"), SQLDBConstants.DEFAULT_DB_FILENAME);
 
-      System.out.println("No database path provided. Creating new database at: " + providedDbPath);
+      out().println("No database path provided. Creating new database at: " + providedDbPath);
     } else {
       providedDbPath = Paths.get(parent.getDbPath());
       Path parentDir = providedDbPath.getParent();
 
       if (parentDir != null && !Files.exists(parentDir)) {
-        System.err.println("The parent directory of the provided database path does not exist: " + parentDir);
+        err().println("The parent directory of the provided database path does not exist: " + parentDir);
         return null;
       }
     }
     
-    ContainerDatanodeDatabase.setDatabasePath(providedDbPath.toString());
-    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase();
+    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase(providedDbPath.toString());
     ContainerLogFileParser parser = new ContainerLogFileParser();
+    
+    cdd.createDatanodeContainerLogTable();
 
-    try {
-      cdd.createDatanodeContainerLogTable();
+    parser.processLogEntries(path, cdd, threadCount);
 
-      parser.processLogEntries(path, cdd, threadCount);
+    cdd.insertLatestContainerLogData();
+    out().println("Successfully parsed the log files and updated the respective tables");
 
-      cdd.insertLatestContainerLogData();
-      System.out.println("Successfully parsed the log files and updated the respective tables");
-    } catch (Exception e) {
-      throw e;
-    }
     return null;
   }
 
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ListContainers.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ListContainers.java
index 7bfa66a742d3..6bc2e4b13641 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ListContainers.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ListContainers.java
@@ -17,13 +17,11 @@
 
 package org.apache.hadoop.ozone.debug.logs.container;
 
-import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
-import org.apache.hadoop.ozone.containerlog.parser.ContainerDatanodeDatabase;
-import org.apache.hadoop.ozone.containerlog.parser.DBConsts;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
 import org.apache.hadoop.ozone.shell.ListOptions;
 import picocli.CommandLine;
 
@@ -36,7 +34,7 @@
     name = "list",
     description = "Finds containers from the database based on the option provided."
 )
-public class ListContainers implements Callable<Void> {
+public class ListContainers extends AbstractSubcommand implements Callable<Void> {
   
   @CommandLine.Option(names = {"--state"},
       description = "Life cycle state of the container.",
@@ -51,35 +49,15 @@ public class ListContainers implements Callable<Void> {
 
   @Override
   public Void call() throws Exception {
-    Path providedDbPath;
-    if (parent.getDbPath() == null) {
-      providedDbPath = Paths.get(System.getProperty("user.dir"), DBConsts.DEFAULT_DB_FILENAME);
-
-      if (Files.exists(providedDbPath) && Files.isRegularFile(providedDbPath)) {
-        System.out.println("Using default database file found in current directory: " + providedDbPath);
-      } else {
-        System.err.println("No database path provided and default file '" + DBConsts.DEFAULT_DB_FILENAME + "' not " +
-            "found in current directory. Please provide a valid database path");
-        return null;
-      }
-    } else {
-      providedDbPath = Paths.get(parent.getDbPath());
-      Path parentDir = providedDbPath.getParent();
-
-      if (parentDir != null && !Files.exists(parentDir)) {
-        System.err.println("The parent directory of the provided database path does not exist: " + parentDir);
-        return null;
-      }
-    }
-
-    ContainerDatanodeDatabase.setDatabasePath(providedDbPath.toString());
     
-    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase();
-    try {
-      cdd.listContainersByState(state.name(), listOptions.getLimit());
-    } catch (Exception e) {
-      throw e;
+    Path dbPath = parent.resolveDbPath();
+    if (dbPath == null) {
+      return null;
     }
+
+    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase(dbPath.toString());
+
+    cdd.listContainersByState(state.name(), listOptions.getLimit());
     
     return null;
   }
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
new file mode 100644
index 000000000000..e05f70e7bb31
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
@@ -0,0 +1,545 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container.utils;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.sqlite.SQLiteConfig;
+
+/**
+ * Handles creation and interaction with the database.
+ * Provides methods for table creation, log data insertion, and index setup.
+ */
+public class ContainerDatanodeDatabase {
+  
+  private final String databasePath;
+  private static final int DEFAULT_REPLICATION_FACTOR;
+
+  private final PrintWriter out;
+
+  public ContainerDatanodeDatabase(String dbPath) {
+    this.databasePath = dbPath;
+    this.out = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true);
+  }
+
+  public ContainerDatanodeDatabase(String dbPath, PrintWriter out) {
+    this.databasePath = dbPath;
+    this.out = out;
+  }
+  
+  static {
+    OzoneConfiguration configuration = new OzoneConfiguration();
+    final String replication = configuration.getTrimmed(
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY,
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_DEFAULT);
+
+    DEFAULT_REPLICATION_FACTOR = Integer.parseInt(replication.toUpperCase());
+  }
+  
+  private Connection getConnection() throws Exception {
+    if (databasePath == null) {
+      throw new IllegalStateException("Database path not set");
+    }
+    
+    Class.forName(SQLDBConstants.DRIVER);
+
+    SQLiteConfig config = new SQLiteConfig();
+
+    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
+    config.setCacheSize(SQLDBConstants.CACHE_SIZE);
+    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
+    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
+    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
+
+    return DriverManager.getConnection(SQLDBConstants.CONNECTION_PREFIX + databasePath, config.toProperties());
+  }
+
+  public void createDatanodeContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+      createDatanodeContainerIndex(createStmt);
+    } catch (SQLException e) {
+      throw new SQLException("Error while creating the table: " + e.getMessage());
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  private void createContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+    } catch (SQLException e) {
+      throw new SQLException("Error while creating the table: " + e.getMessage());
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  /**
+   * Inserts a list of container log entries into the DatanodeContainerLogTable.
+   *
+   * @param transitionList List of container log entries to insert into the table.
+   */
+  
+  public synchronized void insertContainerDatanodeData(List<DatanodeContainerInfo> transitionList) throws SQLException {
+
+    String insertSQL = SQLDBConstants.INSERT_DATANODE_CONTAINER_LOG;
+
+    long containerId = 0;
+    String datanodeId = null;
+    
+    try (Connection connection = getConnection();
+         PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {
+
+      int count = 0;
+
+      for (DatanodeContainerInfo info : transitionList) {
+        datanodeId = info.getDatanodeId();
+        containerId = info.getContainerId();
+
+        preparedStatement.setString(1, datanodeId);
+        preparedStatement.setLong(2, containerId);
+        preparedStatement.setString(3, info.getTimestamp());
+        preparedStatement.setString(4, info.getState());
+        preparedStatement.setLong(5, info.getBcsid());
+        preparedStatement.setString(6, info.getErrorMessage());
+        preparedStatement.setString(7, info.getLogLevel());
+        preparedStatement.setInt(8, info.getIndexValue());
+        preparedStatement.addBatch();
+
+        count++;
+
+        if (count % SQLDBConstants.BATCH_SIZE == 0) {
+          preparedStatement.executeBatch();
+          count = 0;
+        }
+      }
+
+      if (count != 0) {
+        preparedStatement.executeBatch();
+      }
+    } catch (SQLException e) {
+      throw new SQLException("Failed to insert container log for container " + containerId + 
+              " on datanode " + datanodeId);
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  private void createDatanodeContainerIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_INDEX;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Extracts the latest container log data from the DatanodeContainerLogTable
+   * and inserts it into ContainerLogTable.
+   */
+
+  public void insertLatestContainerLogData() throws SQLException {
+    createContainerLogTable();
+    String selectSQL = SQLDBConstants.SELECT_LATEST_CONTAINER_LOG;
+    String insertSQL = SQLDBConstants.INSERT_CONTAINER_LOG;
+
+    try (Connection connection = getConnection();
+         PreparedStatement selectStmt = connection.prepareStatement(selectSQL);
+         ResultSet resultSet = selectStmt.executeQuery();
+         PreparedStatement insertStmt = connection.prepareStatement(insertSQL)) {
+      
+      int count = 0;
+      
+      while (resultSet.next()) {
+        String datanodeId = resultSet.getString("datanode_id");
+        long containerId = resultSet.getLong("container_id");
+        String containerState = resultSet.getString("container_state");
+        long bcsid = resultSet.getLong("bcsid");
+        try {
+          insertStmt.setString(1, datanodeId);
+          insertStmt.setLong(2, containerId);
+          insertStmt.setString(3, containerState);
+          insertStmt.setLong(4, bcsid);
+          insertStmt.addBatch();
+
+          count++;
+
+          if (count % SQLDBConstants.BATCH_SIZE == 0) {
+            insertStmt.executeBatch();
+            count = 0;
+          }
+        } catch (SQLException e) {
+          throw new SQLException("Failed to insert container log entry for container " + containerId + " on datanode "
+                  + datanodeId);
+        }
+      }
+
+      if (count != 0) {
+        insertStmt.executeBatch();
+      }
+    } catch (SQLException e) {
+      throw new SQLException("Failed to insert container log entry: " + e.getMessage());
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  private void dropTable(String tableName, Statement stmt) throws SQLException {
+    String dropTableSQL = SQLDBConstants.DROP_TABLE.replace("{table_name}", tableName);
+    stmt.executeUpdate(dropTableSQL);
+  }
+
+  private void createContainerLogIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_INDEX_LATEST_STATE;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Lists containers filtered by the specified state and writes their details to stdout 
+   * unless redirected to a file explicitly.
+   * The output includes timestamp, datanode ID, container ID, BCSID, error message, and index value,
+   * written in a human-readable table format to a file or console.
+   * Behavior based on the {@code limit} parameter:
+   * If {@code limit} is provided, only up to the specified number of rows are printed.
+   * If the number of matching containers exceeds the {@code limit},
+   * a note is printed indicating more containers exist.
+   *
+   * @param state the container state to filter by (e.g., "OPEN", "CLOSED", "QUASI_CLOSED")
+   * @param limit the maximum number of rows to display; use {@link Integer#MAX_VALUE} to fetch all rows
+   */
+
+  public void listContainersByState(String state, Integer limit) throws SQLException {
+    int count = 0;
+
+    boolean limitProvided = limit != Integer.MAX_VALUE;
+
+    String baseQuery = SQLDBConstants.SELECT_LATEST_CONTAINER_LOGS_BY_STATE;
+    String finalQuery = limitProvided ? baseQuery + " LIMIT ?" : baseQuery;
+
+    try (Connection connection = getConnection();
+         Statement stmt = connection.createStatement()) {
+
+      createContainerLogIndex(stmt);
+
+      try (PreparedStatement pstmt = connection.prepareStatement(finalQuery)) {
+        pstmt.setString(1, state);
+        if (limitProvided) {
+          pstmt.setInt(2, limit + 1);
+        }
+
+        try (ResultSet rs = pstmt.executeQuery()) {
+
+          out.printf("%-25s | %-35s | %-15s | %-15s | %-40s | %-12s%n",
+              "Timestamp", "Datanode ID", "Container ID", "BCSID", "Message", "Index Value");
+          out.println("-------------------------------------------------------------------------------------" +
+              "---------------------------------------------------------------------------------------");
+
+          while (rs.next()) {
+            if (limitProvided && count >= limit) {
+              out.println("Note: There might be more containers. Use -all option to list all entries");
+              break;
+            }
+            String timestamp = rs.getString("timestamp");
+            String datanodeId = rs.getString("datanode_id");
+            long containerId = rs.getLong("container_id");
+            long latestBcsid = rs.getLong("latest_bcsid");
+            String errorMessage = rs.getString("error_message");
+            int indexValue = rs.getInt("index_value");
+            count++;
+
+            out.printf("%-25s | %-35s | %-15d | %-15d | %-40s | %-12d%n",
+                timestamp, datanodeId, containerId, latestBcsid, errorMessage, indexValue);
+          }
+          
+          if (count == 0) {
+            out.printf("No containers found for state: %s%n", state);
+          } else {
+            out.printf("Number of containers listed: %d%n", count);
+          }
+        }
+      }
+    } catch (SQLException e) {
+      throw new SQLException("Error while retrieving containers with state " + state);
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  private void createIdxDclContainerStateTime(Connection conn) throws SQLException {
+    String sql = SQLDBConstants.CREATE_DCL_CONTAINER_STATE_TIME_INDEX;
+    try (Statement stmt = conn.createStatement()) {
+      stmt.execute(sql);
+    }
+  }
+
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      createIdxDclContainerStateTime(connection);
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        out.println("Missing container with ID: " + containerID);
+        return;
+      }
+
+      out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      logEntries.sort(Comparator.comparing(DatanodeContainerInfo::getTimestamp));
+
+      if (checkForMultipleOpenStates(logEntries)) {
+        out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      analyzeContainerHealth(containerID, latestPerDatanode);
+
+    } catch (SQLException e) {
+      throw new SQLException("Error while retrieving container with ID " + containerID);
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected error: "  + e);
+    }
+  }
+
+  private void analyzeContainerHealth(Long containerID, 
+      Map<String, DatanodeContainerInfo> latestPerDatanode) {
+
+    Set<String> lifeCycleStates = new HashSet<>();
+    for (HddsProtos.LifeCycleState state : HddsProtos.LifeCycleState.values()) {
+      lifeCycleStates.add(state.name());
+    }
+
+    Set<String> healthStates = new HashSet<>();
+    for (ReplicationManagerReport.HealthState state : ReplicationManagerReport.HealthState.values()) {
+      healthStates.add(state.name());
+    }
+
+    Set<String> unhealthyReplicas = new HashSet<>();
+    Set<String> closedReplicas = new HashSet<>();
+    Set<String> openReplicas = new HashSet<>();
+    Set<String> quasiclosedReplicas = new HashSet<>();
+    Set<String> deletedReplicas = new HashSet<>();
+    Set<Long> bcsids = new HashSet<>();
+    Set<String> datanodeIds = new HashSet<>();
+    List<String> closedTimestamps = new ArrayList<>();
+    List<String> otherTimestamps = new ArrayList<>();
+
+    for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+      String datanodeId = entry.getDatanodeId();
+      String state = entry.getState();
+      long bcsid = entry.getBcsid();
+      String stateTimestamp = entry.getTimestamp();
+
+      datanodeIds.add(datanodeId);
+      
+      if (healthStates.contains(state.toUpperCase())) {
+
+        ReplicationManagerReport.HealthState healthState =
+            ReplicationManagerReport.HealthState.valueOf(state.toUpperCase());
+
+        if (healthState == ReplicationManagerReport.HealthState.UNHEALTHY) {
+          unhealthyReplicas.add(datanodeId);
+        }
+
+      } else if (lifeCycleStates.contains(state.toUpperCase())) {
+
+        HddsProtos.LifeCycleState lifeCycleState = HddsProtos.LifeCycleState.valueOf(state.toUpperCase());
+
+        switch (lifeCycleState) {
+        case OPEN:
+          openReplicas.add(datanodeId);
+          otherTimestamps.add(stateTimestamp);
+          break;
+        case CLOSING:
+          otherTimestamps.add(stateTimestamp);
+          break;
+        case CLOSED:
+          closedReplicas.add(datanodeId);
+          bcsids.add(bcsid);
+          closedTimestamps.add(stateTimestamp);
+          break;
+        case QUASI_CLOSED:
+          quasiclosedReplicas.add(datanodeId);
+          otherTimestamps.add(stateTimestamp);
+          break;
+        case DELETED:
+          deletedReplicas.add(datanodeId);
+          break;
+        default:
+          break;
+        }
+
+      }
+    }
+
+    int closedCount = closedReplicas.size();
+    
+    boolean allClosedNewer = closedCount > 0 && closedTimestamps.stream()
+        .allMatch(ct -> otherTimestamps.stream().allMatch(ot -> ct.compareTo(ot) > 0));
+    
+    if (bcsids.size() > 1) {
+      out.println("Container " + containerID + " has MISMATCHED REPLICATION as there are multiple" +
+          " CLOSED containers with varying BCSIDs.");
+    } else if (closedCount == DEFAULT_REPLICATION_FACTOR && allClosedNewer) {
+      out.println("Container " + containerID + " has enough replicas.");
+    } else if (closedCount > DEFAULT_REPLICATION_FACTOR && allClosedNewer) {
+      out.println("Container " + containerID + " is OVER-REPLICATED.");
+    } else if (closedCount < DEFAULT_REPLICATION_FACTOR && closedCount != 0 && allClosedNewer) {
+      out.println("Container " + containerID + " is UNDER-REPLICATED.");
+    } else {
+      int replicaCount = datanodeIds.size();
+
+      if (!quasiclosedReplicas.isEmpty()
+              && closedReplicas.isEmpty()
+              && quasiclosedReplicas.size() >= DEFAULT_REPLICATION_FACTOR) {
+        out.println("Container " + containerID + " might be QUASI_CLOSED_STUCK.");
+      } else if (!unhealthyReplicas.isEmpty()) {
+        out.println("Container " + containerID + " has UNHEALTHY replicas.");
+      } else if (!openReplicas.isEmpty() &&
+              (replicaCount - openReplicas.size()) > 0) {
+        out.println("Container " + containerID + " might be OPEN_UNHEALTHY.");
+      } else if (replicaCount - deletedReplicas.size() < DEFAULT_REPLICATION_FACTOR) {
+        out.println("Container " + containerID + " is UNDER-REPLICATED.");
+      } else if (replicaCount - deletedReplicas.size() > DEFAULT_REPLICATION_FACTOR) {
+        out.println("Container " + containerID + " is OVER-REPLICATED.");
+      } else {
+        out.println("Container " + containerID + " has enough replicas.");
+      } 
+    }
+  }
+
+  /**
+   * Checks whether the specified container has multiple "OPEN" states.
+   * If multiple "OPEN" states are found, it returns {@code true}.
+   *
+   * @param entries The list of {@link DatanodeContainerInfo} entries to check for multiple "OPEN" states.
+   * @return {@code true} if multiple "OPEN" states are found, {@code false} otherwise.
+   */
+
+  private boolean checkForMultipleOpenStates(List<DatanodeContainerInfo> entries) {
+    List<String> firstOpenTimestamps = new ArrayList<>();
+    Set<String> firstOpenDatanodes = new HashSet<>();
+    boolean issueFound = false;
+
+    for (DatanodeContainerInfo entry : entries) {
+      if ("OPEN".equalsIgnoreCase(entry.getState())) {
+        if (firstOpenTimestamps.size() < DEFAULT_REPLICATION_FACTOR &&
+            !firstOpenDatanodes.contains(entry.getDatanodeId())) {
+          firstOpenTimestamps.add(entry.getTimestamp());
+          firstOpenDatanodes.add(entry.getDatanodeId());
+        } else if (isTimestampAfterFirstOpens(entry.getTimestamp(), firstOpenTimestamps)) {
+          issueFound = true;
+        }
+      }
+    }
+    return issueFound;
+  }
+
+  private boolean isTimestampAfterFirstOpens(String timestamp, List<String> firstOpenTimestamps) {
+    for (String firstTimestamp : firstOpenTimestamps) {
+      if (timestamp.compareTo(firstTimestamp) > 0) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private List<DatanodeContainerInfo> getContainerLogData(Long containerID, Connection connection)
+      throws SQLException {
+    String query = SQLDBConstants.CONTAINER_DETAILS_QUERY;
+    List<DatanodeContainerInfo> logEntries = new ArrayList<>();
+
+    try (PreparedStatement preparedStatement = connection.prepareStatement(query)) {
+      preparedStatement.setLong(1, containerID);
+      try (ResultSet rs = preparedStatement.executeQuery()) {
+        while (rs.next()) {
+          DatanodeContainerInfo entry = new DatanodeContainerInfo.Builder()
+              .setTimestamp(rs.getString("timestamp"))
+              .setContainerId(rs.getLong("container_id"))
+              .setDatanodeId(rs.getString("datanode_id"))
+              .setState(rs.getString("container_state"))
+              .setBcsid(rs.getLong("bcsid"))
+              .setErrorMessage(rs.getString("error_message"))
+              .setIndexValue(rs.getInt("index_value"))
+              .build();
+
+          logEntries.add(entry);
+        }
+      }
+    }
+
+    return logEntries;
+  }
+}
+
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerLogFileParser.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerLogFileParser.java
similarity index 99%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerLogFileParser.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerLogFileParser.java
index c41beb328f48..f4d5c53ad21d 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerLogFileParser.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerLogFileParser.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.ozone.containerlog.parser;
+package org.apache.hadoop.ozone.debug.logs.container.utils;
 
 import java.io.BufferedReader;
 import java.io.IOException;
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DatanodeContainerInfo.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/DatanodeContainerInfo.java
similarity index 98%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DatanodeContainerInfo.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/DatanodeContainerInfo.java
index a4134c0b3a8d..1928bb28378b 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DatanodeContainerInfo.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/DatanodeContainerInfo.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.ozone.containerlog.parser;
+package org.apache.hadoop.ozone.debug.logs.container.utils;
 
 /**
  *Holds information about a container.
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DBConsts.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/SQLDBConstants.java
similarity index 86%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DBConsts.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/SQLDBConstants.java
index 5cef4a335d94..c58b605a2f64 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/DBConsts.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/SQLDBConstants.java
@@ -15,12 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.hadoop.ozone.containerlog.parser;
+package org.apache.hadoop.ozone.debug.logs.container.utils;
 
 /**
  * Constants used for ContainerDatanodeDatabase.
  */
-public final class DBConsts {
+public final class SQLDBConstants {
   
   public static final String DEFAULT_DB_FILENAME = "container_datanode.db";
   public static final String DRIVER = "org.sqlite.JDBC";
@@ -63,8 +63,13 @@ public final class DBConsts {
           "WHERE sub_dcl.datanode_id = cl.datanode_id AND" +
           " sub_dcl.container_id = cl.container_id AND sub_dcl.bcsid = cl.latest_bcsid" +
           " AND sub_dcl.container_state = cl.latest_state)";
+  public static final String CONTAINER_DETAILS_QUERY = "SELECT d.timestamp, d.container_id, d.datanode_id, " +
+      "d.container_state, d.bcsid, d.error_message, d.index_value FROM DatanodeContainerLogTable d " +
+      "WHERE d.container_id = ? ORDER BY d.datanode_id ASC, d.timestamp ASC;";
+  public static final String CREATE_DCL_CONTAINER_STATE_TIME_INDEX = "CREATE INDEX IF NOT EXISTS " +
+      "idx_dcl_container_state_time ON DatanodeContainerLogTable(container_id, container_state, timestamp);";
   
-  private DBConsts() {
+  private SQLDBConstants() {
     //Never constructed
   }
 }
diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/package-info.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/package-info.java
similarity index 93%
rename from hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/package-info.java
rename to hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/package-info.java
index 9a22345a0627..5c33e2b09bc4 100644
--- a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/package-info.java
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/package-info.java
@@ -19,4 +19,4 @@
  * Classes used for Ozone Container Log parser tool.
  */
 
-package org.apache.hadoop.ozone.containerlog.parser;
+package org.apache.hadoop.ozone.debug.logs.container.utils;


Reviews:
user: login: adoroszlai
body: Thanks @sreejasahithi for the patch.  This will conflict with #8282, let's wait until that one is merged.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:08:30Z
user: login: aryangupta1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:00:33Z
user: login: aryangupta1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:02:02Z
user: login: ashishkumar50
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T10:17:34Z
user: login: sreejasahithi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T12:39:42Z
user: login: Tejaskriya
body: Thanks for working on this @sreejasahithi , left a few comments below.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T06:58:31Z
user: login: Tejaskriya
body: Thanks for making the changes, just one comment below
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T11:08:04Z
user: login: ashishkumar50
body: Thanks @sreejasahithi for updating patch, LGTM.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:18:48Z
user: login: Tejaskriya
body: Thanks for updating the patch, LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T05:03:52Z
user: login: adoroszlai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T05:09:54Z
user: login: aryangupta1998
body: Thanks for the patch @sreejasahithi, LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:17:44Z

Review comments:
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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.hadoop.ozone.debug.container;
+
+import java.sql.SQLException;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.ozone.containerlog.parser.ContainerDatanodeDatabase;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "container-info",
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/container/ContainerInfoCommand.java
user: login: adoroszlai
body: ```suggestion
    name = "info",
```
created_at: 2025-04-16T14:56:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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.hadoop.ozone.debug.container;
+
+import java.sql.SQLException;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.ozone.containerlog.parser.ContainerDatanodeDatabase;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "container-info",
+    description = "provides details of a single container"
+)
+public class ContainerInfoCommand implements Callable<Void> {
+
+  @CommandLine.Option(names = {"--containerId"},
+      description = "container Id", 
+          required = true)
+  private Long containerId;
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/container/ContainerInfoCommand.java
user: login: adoroszlai
body: Should this be a parameter and accept multiple values?  Like:

https://github.com/apache/ozone/blob/ef8e0da2c1180d2de6e1ce53d4379e1527ecd615/hadoop-hdds/tools/src/main/java/org/apache/hadoop/hdds/scm/cli/container/InfoSubcommand.java#L61-L62

Or simply be a parameter, similar to:

https://github.com/apache/ozone/blob/ef8e0da2c1180d2de6e1ce53d4379e1527ecd615/hadoop-hdds/tools/src/main/java/org/apache/hadoop/hdds/scm/cli/container/CloseSubcommand.java#L37-L38

If you prefer it as an option, then please at least rename to `--id`.
created_at: 2025-04-16T14:58:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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.hadoop.ozone.debug.container;
+
+import java.sql.SQLException;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.ozone.containerlog.parser.ContainerDatanodeDatabase;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "container-info",
+    description = "provides details of a single container"
+)
+public class ContainerInfoCommand implements Callable<Void> {
+
+  @CommandLine.Option(names = {"--containerId"},
+      description = "container Id", 
+          required = true)
+  private Long containerId;
+
+  @CommandLine.ParentCommand
+  private ContainerLogController parent;
+  
+  @Override
+  public Void call() throws Exception {
+    if (containerId != null) {
+
+      ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase();
+      try {
+        cdd.showContainerDetails(containerId);
+      } catch (SQLException e) {
+        System.err.println("SQL error while fetching container details: " + e.getMessage());
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/container/ContainerInfoCommand.java
user: login: adoroszlai
body: Same comment as in #8282:

```suggestion
```
created_at: 2025-04-16T15:00:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +236,189 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+    String query = queries.get("CONTAINER_SELECT_QUERY");
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      try (PreparedStatement statement = connection.prepareStatement(query)) {
+        statement.setLong(1, containerID);
+
+        try (ResultSet resultSet = statement.executeQuery()) {
+
+          boolean foundit = false;
+          Set<String> datanodeIds = new HashSet<>();
+          Set<String> unhealthyReplicas = new HashSet<>();
+          Set<String> closedReplicas = new HashSet<>();
+          Set<String> openReplicas = new HashSet<>();
+          Set<String> quasiclosedReplicas = new HashSet<>();
+          Set<Long> bcsids = new HashSet<>();
+          Set<String> deletedReplicas = new HashSet<>();
+
+          while (resultSet.next()) {
+            foundit = true;
+
+            String datanodeId = resultSet.getString("datanode_id");
+            String latestState = resultSet.getString("latest_state");
+            long latestBcsid = resultSet.getLong("latest_bcsid");
+            String timestamp = resultSet.getString("timestamp");
+            String errorMessage = resultSet.getString("error_message");
+            int indexValue = resultSet.getInt("index_value");
+
+            datanodeIds.add(datanodeId);
+
+            if ("UNHEALTHY".equalsIgnoreCase(latestState)) {
+              unhealthyReplicas.add(datanodeId);
+            } else if ("CLOSED".equalsIgnoreCase(latestState)) {
+              closedReplicas.add(datanodeId);
+              bcsids.add(latestBcsid);
+            } else if ("OPEN".equalsIgnoreCase(latestState)) {
+              openReplicas.add(datanodeId);
+            } else if ("QUASI_CLOSED".equalsIgnoreCase(latestState)) {
+              quasiclosedReplicas.add(datanodeId);
+            } else if ("DELETED".equalsIgnoreCase(latestState)) {
+              deletedReplicas.add(datanodeId);
+              bcsids.add(latestBcsid);
+            }
+          }
+
+          if (!foundit) {
+            System.out.println("Missing container with ID: " + containerID);
+            return;
+          }
+
+          if (!deletedReplicas.isEmpty() && !closedReplicas.isEmpty() && bcsids.size() > 1) {
+            System.out.println("Container " + containerID + " has MISMATCHED REPLICATION.");
+            return;
+          }
+
+          int unhealthyCount = unhealthyReplicas.size();
+          int replicaCount = datanodeIds.size();
+
+          if (unhealthyCount == replicaCount) {
+            System.out.println("Container " + containerID + " is UNHEALTHY across all datanodes.");
+          } else if (unhealthyCount >= 2 && closedReplicas.size() == replicaCount - unhealthyCount) {
+            System.out.println("Container " + containerID + " is both UNHEALTHY and UNDER-REPLICATED.");
+          } else if (unhealthyCount == 1 && closedReplicas.size() == replicaCount - unhealthyCount) {
+            System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+          } else if ((!openReplicas.isEmpty() && openReplicas.size() < 3) &&
+              (closedReplicas.size() == replicaCount - openReplicas.size() ||
+                  unhealthyCount == replicaCount - openReplicas.size())) {
+            System.out.println("Container " + containerID + " is OPEN_UNHEALTHY.");
+          } else if (quasiclosedReplicas.size() >= 3) {
+            System.out.println("Container " + containerID + " is OUASI_CLOSED_STUCK.");
+          } else {
+            int expectedReplicationFactor = 3;
+            if (replicaCount - deletedReplicas.size() < expectedReplicationFactor) {
+              System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+            } else if (replicaCount - deletedReplicas.size() > expectedReplicationFactor) {
+              System.out.println("Container " + containerID + " is OVER-REPLICATED.");
+            } else {
+              System.out.println("Container " + containerID + " has enough replicas.");
+            }
+          }
+        }
+      }
+    } catch (SQLException e) {
+      LOG.error("Failed to get details of container: {}", containerID, e);
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: adoroszlai
body: Error/warning messages should go to stderr (`System.err`).
created_at: 2025-04-16T15:01:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +237,173 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      Set<String> unhealthyReplicas = new HashSet<>();
+      Set<String> closedReplicas = new HashSet<>();
+      Set<String> openReplicas = new HashSet<>();
+      Set<String> quasiclosedReplicas = new HashSet<>();
+      Set<String> deletedReplicas = new HashSet<>();
+      Set<Long> bcsids = new HashSet<>();
+      Set<String> datanodeIds = new HashSet<>();
+
+      for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+        String datanodeId = entry.getDatanodeId();
+        String state = entry.getState();
+        long bcsid = entry.getBcsid();
+
+        datanodeIds.add(datanodeId);
+
+        switch (state.toUpperCase()) {
+        case "UNHEALTHY": unhealthyReplicas.add(datanodeId); break;
+        case "CLOSED": closedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+        case "OPEN": openReplicas.add(datanodeId); break;
+        case "QUASI_CLOSED": quasiclosedReplicas.add(datanodeId); break;
+        case "DELETED": deletedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+        default:
+          break;
+        }
+      }
+      
+      int unhealthyCount = unhealthyReplicas.size();
+      int replicaCount = datanodeIds.size();
+      int expectedReplicationFactor = 3;
+
+      if (!deletedReplicas.isEmpty() && !closedReplicas.isEmpty() && bcsids.size() > 1) {
+        System.out.println("Container " + containerID + " has MISMATCHED REPLICATION.");
+      } else if (unhealthyCount == replicaCount) {
+        System.out.println("Container " + containerID + " is UNHEALTHY across all datanodes.");
+      } else if (unhealthyCount >= 2 && closedReplicas.size() == replicaCount - unhealthyCount) {
+        System.out.println("Container " + containerID + " is both UNHEALTHY and UNDER-REPLICATED.");
+      } else if (unhealthyCount == 1 && closedReplicas.size() == replicaCount - unhealthyCount) {
+        System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+      } else if ((!openReplicas.isEmpty() && openReplicas.size() < 3) &&
+          (closedReplicas.size() == replicaCount - openReplicas.size() ||
+              unhealthyCount == replicaCount - openReplicas.size())) {
+        System.out.println("Container " + containerID + " is OPEN_UNHEALTHY.");
+      } else if (quasiclosedReplicas.size() >= 3) {
+        System.out.println("Container " + containerID + " is QUASI_CLOSED_STUCK.");
+      } else if (replicaCount - deletedReplicas.size() < expectedReplicationFactor) {
+        System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+      } else if (replicaCount - deletedReplicas.size() > expectedReplicationFactor) {
+        System.out.println("Container " + containerID + " is OVER-REPLICATED.");
+      } else {
+        System.out.println("Container " + containerID + " has enough replicas.");
+      }
+    } catch (SQLException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Checks whether the specified container has multiple "OPEN" states.
+   * If multiple "OPEN" states are found, it returns {@code true}.
+   *
+   * @param entries The list of {@link DatanodeContainerInfo} entries to check for multiple "OPEN" states.
+   * @return {@code true} if multiple "OPEN" states are found, {@code false} otherwise.
+   */
+  
+  private boolean checkForMultipleOpenStates(List<DatanodeContainerInfo> entries) {
+    List<String> firstOpenTimestamps = new ArrayList<>();
+    Set<String> firstOpenDatanodes = new HashSet<>();
+    boolean issueFound = false;
+
+    for (DatanodeContainerInfo entry : entries) {
+      if ("OPEN".equalsIgnoreCase(entry.getState())) {
+        if (firstOpenTimestamps.size() < 3 && !firstOpenDatanodes.contains(entry.getDatanodeId())) {
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: aryangupta1998
body: I'm assuming you're using 3 here, as the default replication factor is 3. I'm not sure if we can fetch the replication factor dynamically, but let's use a variable instead of hard-coding.
created_at: 2025-04-17T16:00:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +237,173 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      Set<String> unhealthyReplicas = new HashSet<>();
+      Set<String> closedReplicas = new HashSet<>();
+      Set<String> openReplicas = new HashSet<>();
+      Set<String> quasiclosedReplicas = new HashSet<>();
+      Set<String> deletedReplicas = new HashSet<>();
+      Set<Long> bcsids = new HashSet<>();
+      Set<String> datanodeIds = new HashSet<>();
+
+      for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+        String datanodeId = entry.getDatanodeId();
+        String state = entry.getState();
+        long bcsid = entry.getBcsid();
+
+        datanodeIds.add(datanodeId);
+
+        switch (state.toUpperCase()) {
+        case "UNHEALTHY": unhealthyReplicas.add(datanodeId); break;
+        case "CLOSED": closedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+        case "OPEN": openReplicas.add(datanodeId); break;
+        case "QUASI_CLOSED": quasiclosedReplicas.add(datanodeId); break;
+        case "DELETED": deletedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+        default:
+          break;
+        }
+      }
+      
+      int unhealthyCount = unhealthyReplicas.size();
+      int replicaCount = datanodeIds.size();
+      int expectedReplicationFactor = 3;
+
+      if (!deletedReplicas.isEmpty() && !closedReplicas.isEmpty() && bcsids.size() > 1) {
+        System.out.println("Container " + containerID + " has MISMATCHED REPLICATION.");
+      } else if (unhealthyCount == replicaCount) {
+        System.out.println("Container " + containerID + " is UNHEALTHY across all datanodes.");
+      } else if (unhealthyCount >= 2 && closedReplicas.size() == replicaCount - unhealthyCount) {
+        System.out.println("Container " + containerID + " is both UNHEALTHY and UNDER-REPLICATED.");
+      } else if (unhealthyCount == 1 && closedReplicas.size() == replicaCount - unhealthyCount) {
+        System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+      } else if ((!openReplicas.isEmpty() && openReplicas.size() < 3) &&
+          (closedReplicas.size() == replicaCount - openReplicas.size() ||
+              unhealthyCount == replicaCount - openReplicas.size())) {
+        System.out.println("Container " + containerID + " is OPEN_UNHEALTHY.");
+      } else if (quasiclosedReplicas.size() >= 3) {
+        System.out.println("Container " + containerID + " is QUASI_CLOSED_STUCK.");
+      } else if (replicaCount - deletedReplicas.size() < expectedReplicationFactor) {
+        System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
+      } else if (replicaCount - deletedReplicas.size() > expectedReplicationFactor) {
+        System.out.println("Container " + containerID + " is OVER-REPLICATED.");
+      } else {
+        System.out.println("Container " + containerID + " has enough replicas.");
+      }
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: aryangupta1998
body: Can we shift this if..else in a separate function?
created_at: 2025-04-17T16:02:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +47,7 @@
 public class ContainerDatanodeDatabase {
 
   private static Map<String, String> queries;
+  private static final int DEFAULT_REPLICATION_FACTOR = 3;
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: ashishkumar50
body: Instead of hardcoding use conf to get replication factor
created_at: 2025-04-22T09:31:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +239,184 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      logEntries.sort(Comparator.comparing(DatanodeContainerInfo::getTimestamp));
+      
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      analyzeContainerHealth(containerID, latestPerDatanode);
+      
+    } catch (SQLException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void analyzeContainerHealth(Long containerID,
+                                      Map<String, DatanodeContainerInfo> latestPerDatanode) {
+
+    Set<String> unhealthyReplicas = new HashSet<>();
+    Set<String> closedReplicas = new HashSet<>();
+    Set<String> openReplicas = new HashSet<>();
+    Set<String> quasiclosedReplicas = new HashSet<>();
+    Set<String> deletedReplicas = new HashSet<>();
+    Set<Long> bcsids = new HashSet<>();
+    Set<String> datanodeIds = new HashSet<>();
+
+    for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+      String datanodeId = entry.getDatanodeId();
+      String state = entry.getState();
+      long bcsid = entry.getBcsid();
+
+      datanodeIds.add(datanodeId);
+
+      switch (state.toUpperCase()) {
+      case "UNHEALTHY": unhealthyReplicas.add(datanodeId); break;
+      case "CLOSED": closedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+      case "OPEN": openReplicas.add(datanodeId); break;
+      case "QUASI_CLOSED": quasiclosedReplicas.add(datanodeId); break;
+      case "DELETED": deletedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+      default:
+        break;
+      }
+    }
+
+    int unhealthyCount = unhealthyReplicas.size();
+    int replicaCount = datanodeIds.size();
+    int openReplicasCount = openReplicas.size();
+    int closedReplicasCount = closedReplicas.size();
+    
+    if (bcsids.size() > 1) {
+      System.out.println("Container " + containerID + " has MISMATCHED REPLICATION.");
+    } else if (unhealthyCount == replicaCount && replicaCount >= DEFAULT_REPLICATION_FACTOR) {
+      System.out.println("Container " + containerID + " is UNHEALTHY across all datanodes.");
+    } else if (unhealthyCount >= 2 && closedReplicasCount == replicaCount - unhealthyCount) {
+      System.out.println("Container " + containerID + " is both UNHEALTHY and UNDER-REPLICATED.");
+    } else if (unhealthyCount == 1 && closedReplicasCount == replicaCount - unhealthyCount) {
+      System.out.println("Container " + containerID + " is UNDER-REPLICATED.");
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: ashishkumar50
body: If there is CLOSED replica but number of CLOSED replicas are less than REPLICATION_FACTOR, we can just mark container as UNDER-REPLICATED.
If there are any UNHEALTHY container, but number of CLOSED replica is equal to REPLICATION_FACTOR, and those CLOSED replica time is after UNHEALTHY container time, we can assume container as normal.
created_at: 2025-04-22T10:11:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +239,184 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      logEntries.sort(Comparator.comparing(DatanodeContainerInfo::getTimestamp));
+      
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      analyzeContainerHealth(containerID, latestPerDatanode);
+      
+    } catch (SQLException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void analyzeContainerHealth(Long containerID,
+                                      Map<String, DatanodeContainerInfo> latestPerDatanode) {
+
+    Set<String> unhealthyReplicas = new HashSet<>();
+    Set<String> closedReplicas = new HashSet<>();
+    Set<String> openReplicas = new HashSet<>();
+    Set<String> quasiclosedReplicas = new HashSet<>();
+    Set<String> deletedReplicas = new HashSet<>();
+    Set<Long> bcsids = new HashSet<>();
+    Set<String> datanodeIds = new HashSet<>();
+
+    for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+      String datanodeId = entry.getDatanodeId();
+      String state = entry.getState();
+      long bcsid = entry.getBcsid();
+
+      datanodeIds.add(datanodeId);
+
+      switch (state.toUpperCase()) {
+      case "UNHEALTHY": unhealthyReplicas.add(datanodeId); break;
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: ashishkumar50
body: Use container health/state enums instead of hardcoding.
created_at: 2025-04-22T10:15:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -233,5 +239,184 @@ private void dropTable(String tableName, Statement stmt) throws SQLException {
     stmt.executeUpdate(dropTableSQL);
   }
 
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+  
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+      
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      logEntries.sort(Comparator.comparing(DatanodeContainerInfo::getTimestamp));
+      
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      analyzeContainerHealth(containerID, latestPerDatanode);
+      
+    } catch (SQLException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void analyzeContainerHealth(Long containerID,
+                                      Map<String, DatanodeContainerInfo> latestPerDatanode) {
+
+    Set<String> unhealthyReplicas = new HashSet<>();
+    Set<String> closedReplicas = new HashSet<>();
+    Set<String> openReplicas = new HashSet<>();
+    Set<String> quasiclosedReplicas = new HashSet<>();
+    Set<String> deletedReplicas = new HashSet<>();
+    Set<Long> bcsids = new HashSet<>();
+    Set<String> datanodeIds = new HashSet<>();
+
+    for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+      String datanodeId = entry.getDatanodeId();
+      String state = entry.getState();
+      long bcsid = entry.getBcsid();
+
+      datanodeIds.add(datanodeId);
+
+      switch (state.toUpperCase()) {
+      case "UNHEALTHY": unhealthyReplicas.add(datanodeId); break;
+      case "CLOSED": closedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+      case "OPEN": openReplicas.add(datanodeId); break;
+      case "QUASI_CLOSED": quasiclosedReplicas.add(datanodeId); break;
+      case "DELETED": deletedReplicas.add(datanodeId); bcsids.add(bcsid); break;
+      default:
+        break;
+      }
+    }
+
+    int unhealthyCount = unhealthyReplicas.size();
+    int replicaCount = datanodeIds.size();
+    int openReplicasCount = openReplicas.size();
+    int closedReplicasCount = closedReplicas.size();
+    
+    if (bcsids.size() > 1) {
+      System.out.println("Container " + containerID + " has MISMATCHED REPLICATION.");
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/containerlog/parser/ContainerDatanodeDatabase.java
user: login: ashishkumar50
body: Give more details in message, as There are multiple CLOSED containers with different BCSID.
created_at: 2025-04-22T10:17:13Z
author_association: CONTRIBUTOR
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: sreejasahithi
body: ContainerDatanodeDatabase.java has been moved from containerlog.parser package to utils package.
created_at: 2025-04-29T12:39:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "info",
+    description = "provides complete state transition history of each replica for a single container along with " +
+        "analysis over the container"
+)
+public class ContainerInfoCommand extends AbstractSubcommand implements Callable<Void> {
+
+  @CommandLine.Parameters(index = "0", description = "Container ID")
+  private Long containerId;
+
+  @CommandLine.ParentCommand
+  private ContainerLogController parent;
+
+  @Override
+  public Void call() throws Exception {
+
+    if (containerId < 0) {
+      err().println("Invalid container ID: " + containerId);
+      return null;
+    }
+
+    Path providedDbPath;
+    if (parent.getDbPath() == null) {
+      providedDbPath = Paths.get(System.getProperty("user.dir"), SQLDBConstants.DEFAULT_DB_FILENAME);
+
+      if (Files.exists(providedDbPath) && Files.isRegularFile(providedDbPath)) {
+        out().println("Using default database file found in current directory: " + providedDbPath);
+      } else {
+        err().println("No database path provided and default file '" + SQLDBConstants.DEFAULT_DB_FILENAME + "' not " +
+            "found in current directory. Please provide a valid database path");
+        return null;
+      }
+    } else {
+      providedDbPath = Paths.get(parent.getDbPath());
+      Path parentDir = providedDbPath.getParent();
+
+      if (parentDir != null && !Files.exists(parentDir)) {
+        err().println("The parent directory of the provided database path does not exist: " + parentDir);
+        return null;
+      }
+    }
+
+    ContainerDatanodeDatabase.setDatabasePath(providedDbPath.toString());
+
+    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase();
+    try {
+      cdd.showContainerDetails(containerId);
+    } catch (Exception e) {
+      throw e;
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
user: login: Tejaskriya
body: If we are simply throwing the same exception, I don't think we need to have the try-catch block.
created_at: 2025-05-02T06:20:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,87 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "info",
+    description = "provides complete state transition history of each replica for a single container along with " +
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
user: login: Tejaskriya
body: nit: Capitalize the first letter as this will be the help message seen by users.
created_at: 2025-05-02T06:21:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,7 +27,8 @@
     name = "container",
     subcommands = {
         ContainerLogParser.class,
-        ListContainers.class
+        ListContainers.class,
+        ContainerInfoCommand.class
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogController.java
user: login: Tejaskriya
body: nit: alphabetical order of adding the subcommands is better
created_at: 2025-05-02T06:23:03Z
author_association: CONTRIBUTOR
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerLogParser.java
user: login: Tejaskriya
body: Thanks for making the changes as suggested in the other PR!
created_at: 2025-05-02T06:24:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,555 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container.utils;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.sqlite.SQLiteConfig;
+
+/**
+ * Handles creation and interaction with the database.
+ * Provides methods for table creation, log data insertion, and index setup.
+ */
+public class ContainerDatanodeDatabase {
+  
+  private static String databasePath;
+  private static final int DEFAULT_REPLICATION_FACTOR;
+  
+  static {
+    OzoneConfiguration configuration = new OzoneConfiguration();
+    final String replication = configuration.getTrimmed(
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY,
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_DEFAULT);
+
+    DEFAULT_REPLICATION_FACTOR = Integer.parseInt(replication.toUpperCase());
+  }
+  
+  public static void setDatabasePath(String dbPath) {
+    if (databasePath == null) {
+      databasePath = dbPath;
+    }
+  }
+
+  private static Connection getConnection() throws Exception {
+    if (databasePath == null) {
+      throw new IllegalStateException("Database path not set");
+    }
+    
+    Class.forName(SQLDBConstants.DRIVER);
+
+    SQLiteConfig config = new SQLiteConfig();
+
+    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
+    config.setCacheSize(SQLDBConstants.CACHE_SIZE);
+    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
+    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
+    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
+
+    return DriverManager.getConnection(SQLDBConstants.CONNECTION_PREFIX + databasePath, config.toProperties());
+  }
+
+  public void createDatanodeContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+      createDatanodeContainerIndex(createStmt);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: Tejaskriya
body: It would be better to wrap the exception with a custom message, and throw it (instead of printing the error and then throwing the exception again). 
```suggestion
      throw new SQLException("Error while creating the table: " + e.getMessage());
```
As this is now a part of the utils package, any error message printing being handled in the command would be cleaner.

created_at: 2025-05-02T06:43:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,555 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container.utils;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.sqlite.SQLiteConfig;
+
+/**
+ * Handles creation and interaction with the database.
+ * Provides methods for table creation, log data insertion, and index setup.
+ */
+public class ContainerDatanodeDatabase {
+  
+  private static String databasePath;
+  private static final int DEFAULT_REPLICATION_FACTOR;
+  
+  static {
+    OzoneConfiguration configuration = new OzoneConfiguration();
+    final String replication = configuration.getTrimmed(
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY,
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_DEFAULT);
+
+    DEFAULT_REPLICATION_FACTOR = Integer.parseInt(replication.toUpperCase());
+  }
+  
+  public static void setDatabasePath(String dbPath) {
+    if (databasePath == null) {
+      databasePath = dbPath;
+    }
+  }
+
+  private static Connection getConnection() throws Exception {
+    if (databasePath == null) {
+      throw new IllegalStateException("Database path not set");
+    }
+    
+    Class.forName(SQLDBConstants.DRIVER);
+
+    SQLiteConfig config = new SQLiteConfig();
+
+    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
+    config.setCacheSize(SQLDBConstants.CACHE_SIZE);
+    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
+    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
+    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
+
+    return DriverManager.getConnection(SQLDBConstants.CONNECTION_PREFIX + databasePath, config.toProperties());
+  }
+
+  public void createDatanodeContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+      createDatanodeContainerIndex(createStmt);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: Tejaskriya
body: Same as above,
```suggestion
      throw new RuntimeException(""Unexpected error: "  + e);
```
We can do the same for the other occurrences in the file.
created_at: 2025-05-02T06:43:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,555 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container.utils;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.sqlite.SQLiteConfig;
+
+/**
+ * Handles creation and interaction with the database.
+ * Provides methods for table creation, log data insertion, and index setup.
+ */
+public class ContainerDatanodeDatabase {
+  
+  private static String databasePath;
+  private static final int DEFAULT_REPLICATION_FACTOR;
+  
+  static {
+    OzoneConfiguration configuration = new OzoneConfiguration();
+    final String replication = configuration.getTrimmed(
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY,
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_DEFAULT);
+
+    DEFAULT_REPLICATION_FACTOR = Integer.parseInt(replication.toUpperCase());
+  }
+  
+  public static void setDatabasePath(String dbPath) {
+    if (databasePath == null) {
+      databasePath = dbPath;
+    }
+  }
+
+  private static Connection getConnection() throws Exception {
+    if (databasePath == null) {
+      throw new IllegalStateException("Database path not set");
+    }
+    
+    Class.forName(SQLDBConstants.DRIVER);
+
+    SQLiteConfig config = new SQLiteConfig();
+
+    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
+    config.setCacheSize(SQLDBConstants.CACHE_SIZE);
+    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
+    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
+    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
+
+    return DriverManager.getConnection(SQLDBConstants.CONNECTION_PREFIX + databasePath, config.toProperties());
+  }
+
+  public void createDatanodeContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+      createDatanodeContainerIndex(createStmt);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Inserts a list of container log entries into the DatanodeContainerLogTable.
+   *
+   * @param transitionList List of container log entries to insert into the table.
+   */
+  
+  public synchronized void insertContainerDatanodeData(List<DatanodeContainerInfo> transitionList) throws SQLException {
+
+    String insertSQL = SQLDBConstants.INSERT_DATANODE_CONTAINER_LOG;
+
+    long containerId = 0;
+    String datanodeId = null;
+    
+    try (Connection connection = getConnection();
+         PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {
+
+      int count = 0;
+
+      for (DatanodeContainerInfo info : transitionList) {
+        datanodeId = info.getDatanodeId();
+        containerId = info.getContainerId();
+
+        preparedStatement.setString(1, datanodeId);
+        preparedStatement.setLong(2, containerId);
+        preparedStatement.setString(3, info.getTimestamp());
+        preparedStatement.setString(4, info.getState());
+        preparedStatement.setLong(5, info.getBcsid());
+        preparedStatement.setString(6, info.getErrorMessage());
+        preparedStatement.setString(7, info.getLogLevel());
+        preparedStatement.setInt(8, info.getIndexValue());
+        preparedStatement.addBatch();
+
+        count++;
+
+        if (count % SQLDBConstants.BATCH_SIZE == 0) {
+          preparedStatement.executeBatch();
+          count = 0;
+        }
+      }
+
+      if (count != 0) {
+        preparedStatement.executeBatch();
+      }
+    } catch (SQLException e) {
+      System.err.println("Failed to insert container log for container " + containerId + " on datanode " + datanodeId);
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createDatanodeContainerIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_INDEX;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Extracts the latest container log data from the DatanodeContainerLogTable
+   * and inserts it into ContainerLogTable.
+   */
+
+  public void insertLatestContainerLogData() throws SQLException {
+    createContainerLogTable();
+    String selectSQL = SQLDBConstants.SELECT_LATEST_CONTAINER_LOG;
+    String insertSQL = SQLDBConstants.INSERT_CONTAINER_LOG;
+
+    try (Connection connection = getConnection();
+         PreparedStatement selectStmt = connection.prepareStatement(selectSQL);
+         ResultSet resultSet = selectStmt.executeQuery();
+         PreparedStatement insertStmt = connection.prepareStatement(insertSQL)) {
+      
+      int count = 0;
+      
+      while (resultSet.next()) {
+        String datanodeId = resultSet.getString("datanode_id");
+        long containerId = resultSet.getLong("container_id");
+        String containerState = resultSet.getString("container_state");
+        long bcsid = resultSet.getLong("bcsid");
+        try {
+          insertStmt.setString(1, datanodeId);
+          insertStmt.setLong(2, containerId);
+          insertStmt.setString(3, containerState);
+          insertStmt.setLong(4, bcsid);
+          insertStmt.addBatch();
+
+          count++;
+
+          if (count % SQLDBConstants.BATCH_SIZE == 0) {
+            insertStmt.executeBatch();
+            count = 0;
+          }
+        } catch (SQLException e) {
+          System.err.println("Failed to insert container log entry for container " + containerId + " on datanode "
+              + datanodeId);
+          throw e;
+        }
+      }
+
+      if (count != 0) {
+        insertStmt.executeBatch();
+      }
+    } catch (SQLException e) {
+      System.err.println("Failed to insert container log entry: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void dropTable(String tableName, Statement stmt) throws SQLException {
+    String dropTableSQL = SQLDBConstants.DROP_TABLE.replace("{table_name}", tableName);
+    stmt.executeUpdate(dropTableSQL);
+  }
+
+  private void createContainerLogIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_INDEX_LATEST_STATE;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Lists containers filtered by the specified state and writes their details to stdout 
+   * unless redirected to a file explicitly.
+   * The output includes timestamp, datanode ID, container ID, BCSID, error message, and index value,
+   * written in a human-readable table format to a file or console.
+   * Behavior based on the {@code limit} parameter:
+   * If {@code limit} is provided, only up to the specified number of rows are printed.
+   * If the number of matching containers exceeds the {@code limit},
+   * a note is printed indicating more containers exist.
+   *
+   * @param state the container state to filter by (e.g., "OPEN", "CLOSED", "QUASI_CLOSED")
+   * @param limit the maximum number of rows to display; use {@link Integer#MAX_VALUE} to fetch all rows
+   */
+
+  public void listContainersByState(String state, Integer limit) throws SQLException {
+    int count = 0;
+
+    boolean limitProvided = limit != Integer.MAX_VALUE;
+
+    String baseQuery = SQLDBConstants.SELECT_LATEST_CONTAINER_LOGS_BY_STATE;
+    String finalQuery = limitProvided ? baseQuery + " LIMIT ?" : baseQuery;
+
+    try (Connection connection = getConnection();
+         Statement stmt = connection.createStatement()) {
+
+      createContainerLogIndex(stmt);
+
+      try (PreparedStatement pstmt = connection.prepareStatement(finalQuery)) {
+        pstmt.setString(1, state);
+        if (limitProvided) {
+          pstmt.setInt(2, limit + 1);
+        }
+
+        try (ResultSet rs = pstmt.executeQuery();
+             PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out,
+                 StandardCharsets.UTF_8), true)) {
+
+          writer.printf("%-25s | %-35s | %-15s | %-15s | %-40s | %-12s%n",
+              "Timestamp", "Datanode ID", "Container ID", "BCSID", "Message", "Index Value");
+          writer.println("-------------------------------------------------------------------------------------" +
+              "---------------------------------------------------------------------------------------");
+
+          while (rs.next()) {
+            if (limitProvided && count >= limit) {
+              writer.println("Note: There might be more containers. Use -all option to list all entries");
+              break;
+            }
+            String timestamp = rs.getString("timestamp");
+            String datanodeId = rs.getString("datanode_id");
+            long containerId = rs.getLong("container_id");
+            long latestBcsid = rs.getLong("latest_bcsid");
+            String errorMessage = rs.getString("error_message");
+            int indexValue = rs.getInt("index_value");
+            count++;
+
+            writer.printf("%-25s | %-35s | %-15d | %-15d | %-40s | %-12d%n",
+                timestamp, datanodeId, containerId, latestBcsid, errorMessage, indexValue);
+          }
+          
+          if (count == 0) {
+            writer.printf("No containers found for state: %s%n", state);
+          } else {
+            writer.printf("Number of containers listed: %d%n", count);
+          }
+        }
+      }
+    } catch (SQLException e) {
+      throw new SQLException("Error while retrieving containers with state " + state);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createIdxDclContainerStateTime(Connection conn) throws SQLException {
+    String sql = SQLDBConstants.CREATE_DCL_CONTAINER_STATE_TIME_INDEX;
+    try (Statement stmt = conn.createStatement()) {
+      stmt.execute(sql);
+    }
+  }
+
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      createIdxDclContainerStateTime(connection);
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
+          "Timestamp", "Container ID", "Datanode ID", "Container State", "BCSID", "Message", "Index Value");
+      System.out.println("-----------------------------------------------------------------------------------" +
+          "-------------------------------------------------------------------------------------------------");
+
+      for (DatanodeContainerInfo entry : logEntries) {
+        System.out.printf("%-25s | %-15d | %-35s | %-20s | %-10d | %-30s | %-12d%n",
+            entry.getTimestamp(),
+            entry.getContainerId(),
+            entry.getDatanodeId(),
+            entry.getState(),
+            entry.getBcsid(),
+            entry.getErrorMessage(),
+            entry.getIndexValue());
+      }
+
+      logEntries.sort(Comparator.comparing(DatanodeContainerInfo::getTimestamp));
+
+      if (checkForMultipleOpenStates(logEntries)) {
+        System.out.println("Container " + containerID + " might have duplicate OPEN state.");
+        return;
+      }
+
+      Map<String, DatanodeContainerInfo> latestPerDatanode = new HashMap<>();
+      for (DatanodeContainerInfo entry : logEntries) {
+        String datanodeId = entry.getDatanodeId();
+        DatanodeContainerInfo existing = latestPerDatanode.get(datanodeId);
+        if (existing == null || entry.getTimestamp().compareTo(existing.getTimestamp()) > 0) {
+          latestPerDatanode.put(datanodeId, entry);
+        }
+      }
+
+      analyzeContainerHealth(containerID, latestPerDatanode);
+
+    } catch (SQLException e) {
+      throw new SQLException("Error while retrieving container with ID " + containerID);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void analyzeContainerHealth(Long containerID,
+                                      Map<String, DatanodeContainerInfo> latestPerDatanode) {
+
+    Set<String> lifeCycleStates = new HashSet<>();
+    for (HddsProtos.LifeCycleState state : HddsProtos.LifeCycleState.values()) {
+      lifeCycleStates.add(state.name());
+    }
+
+    Set<String> healthStates = new HashSet<>();
+    for (ReplicationManagerReport.HealthState state : ReplicationManagerReport.HealthState.values()) {
+      healthStates.add(state.name());
+    }
+
+    Set<String> unhealthyReplicas = new HashSet<>();
+    Set<String> closedReplicas = new HashSet<>();
+    Set<String> openReplicas = new HashSet<>();
+    Set<String> quasiclosedReplicas = new HashSet<>();
+    Set<String> deletedReplicas = new HashSet<>();
+    Set<Long> bcsids = new HashSet<>();
+    Set<String> datanodeIds = new HashSet<>();
+    List<String> unhealthyTimestamps = new ArrayList<>();
+    List<String> closedTimestamps = new ArrayList<>();
+
+    for (DatanodeContainerInfo entry : latestPerDatanode.values()) {
+      String datanodeId = entry.getDatanodeId();
+      String state = entry.getState();
+      long bcsid = entry.getBcsid();
+      String stateTimestamp = entry.getTimestamp();
+
+      datanodeIds.add(datanodeId);
+
+
+
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: Tejaskriya
body: nit: unnecessary extra lines, can be removed
created_at: 2025-05-02T06:47:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,555 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container.utils;
+
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ReplicationManagerReport;
+import org.apache.hadoop.ozone.om.OMConfigKeys;
+import org.sqlite.SQLiteConfig;
+
+/**
+ * Handles creation and interaction with the database.
+ * Provides methods for table creation, log data insertion, and index setup.
+ */
+public class ContainerDatanodeDatabase {
+  
+  private static String databasePath;
+  private static final int DEFAULT_REPLICATION_FACTOR;
+  
+  static {
+    OzoneConfiguration configuration = new OzoneConfiguration();
+    final String replication = configuration.getTrimmed(
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_KEY,
+        OMConfigKeys.OZONE_SERVER_DEFAULT_REPLICATION_DEFAULT);
+
+    DEFAULT_REPLICATION_FACTOR = Integer.parseInt(replication.toUpperCase());
+  }
+  
+  public static void setDatabasePath(String dbPath) {
+    if (databasePath == null) {
+      databasePath = dbPath;
+    }
+  }
+
+  private static Connection getConnection() throws Exception {
+    if (databasePath == null) {
+      throw new IllegalStateException("Database path not set");
+    }
+    
+    Class.forName(SQLDBConstants.DRIVER);
+
+    SQLiteConfig config = new SQLiteConfig();
+
+    config.setJournalMode(SQLiteConfig.JournalMode.OFF);
+    config.setCacheSize(SQLDBConstants.CACHE_SIZE);
+    config.setLockingMode(SQLiteConfig.LockingMode.EXCLUSIVE);
+    config.setSynchronous(SQLiteConfig.SynchronousMode.OFF);
+    config.setTempStore(SQLiteConfig.TempStore.MEMORY);
+
+    return DriverManager.getConnection(SQLDBConstants.CONNECTION_PREFIX + databasePath, config.toProperties());
+  }
+
+  public void createDatanodeContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.DATANODE_CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+      createDatanodeContainerIndex(createStmt);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createContainerLogTable() throws SQLException {
+    String createTableSQL = SQLDBConstants.CREATE_CONTAINER_LOG_TABLE;
+    try (Connection connection = getConnection();
+         Statement dropStmt = connection.createStatement();
+         Statement createStmt = connection.createStatement()) {
+      dropTable(SQLDBConstants.CONTAINER_LOG_TABLE_NAME, dropStmt);
+      createStmt.execute(createTableSQL);
+    } catch (SQLException e) {
+      System.err.println("Error while creating the table: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Inserts a list of container log entries into the DatanodeContainerLogTable.
+   *
+   * @param transitionList List of container log entries to insert into the table.
+   */
+  
+  public synchronized void insertContainerDatanodeData(List<DatanodeContainerInfo> transitionList) throws SQLException {
+
+    String insertSQL = SQLDBConstants.INSERT_DATANODE_CONTAINER_LOG;
+
+    long containerId = 0;
+    String datanodeId = null;
+    
+    try (Connection connection = getConnection();
+         PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) {
+
+      int count = 0;
+
+      for (DatanodeContainerInfo info : transitionList) {
+        datanodeId = info.getDatanodeId();
+        containerId = info.getContainerId();
+
+        preparedStatement.setString(1, datanodeId);
+        preparedStatement.setLong(2, containerId);
+        preparedStatement.setString(3, info.getTimestamp());
+        preparedStatement.setString(4, info.getState());
+        preparedStatement.setLong(5, info.getBcsid());
+        preparedStatement.setString(6, info.getErrorMessage());
+        preparedStatement.setString(7, info.getLogLevel());
+        preparedStatement.setInt(8, info.getIndexValue());
+        preparedStatement.addBatch();
+
+        count++;
+
+        if (count % SQLDBConstants.BATCH_SIZE == 0) {
+          preparedStatement.executeBatch();
+          count = 0;
+        }
+      }
+
+      if (count != 0) {
+        preparedStatement.executeBatch();
+      }
+    } catch (SQLException e) {
+      System.err.println("Failed to insert container log for container " + containerId + " on datanode " + datanodeId);
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createDatanodeContainerIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_DATANODE_CONTAINER_INDEX;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Extracts the latest container log data from the DatanodeContainerLogTable
+   * and inserts it into ContainerLogTable.
+   */
+
+  public void insertLatestContainerLogData() throws SQLException {
+    createContainerLogTable();
+    String selectSQL = SQLDBConstants.SELECT_LATEST_CONTAINER_LOG;
+    String insertSQL = SQLDBConstants.INSERT_CONTAINER_LOG;
+
+    try (Connection connection = getConnection();
+         PreparedStatement selectStmt = connection.prepareStatement(selectSQL);
+         ResultSet resultSet = selectStmt.executeQuery();
+         PreparedStatement insertStmt = connection.prepareStatement(insertSQL)) {
+      
+      int count = 0;
+      
+      while (resultSet.next()) {
+        String datanodeId = resultSet.getString("datanode_id");
+        long containerId = resultSet.getLong("container_id");
+        String containerState = resultSet.getString("container_state");
+        long bcsid = resultSet.getLong("bcsid");
+        try {
+          insertStmt.setString(1, datanodeId);
+          insertStmt.setLong(2, containerId);
+          insertStmt.setString(3, containerState);
+          insertStmt.setLong(4, bcsid);
+          insertStmt.addBatch();
+
+          count++;
+
+          if (count % SQLDBConstants.BATCH_SIZE == 0) {
+            insertStmt.executeBatch();
+            count = 0;
+          }
+        } catch (SQLException e) {
+          System.err.println("Failed to insert container log entry for container " + containerId + " on datanode "
+              + datanodeId);
+          throw e;
+        }
+      }
+
+      if (count != 0) {
+        insertStmt.executeBatch();
+      }
+    } catch (SQLException e) {
+      System.err.println("Failed to insert container log entry: " + e.getMessage());
+      throw e;
+    } catch (Exception e) {
+      System.err.println("Unexpected error: " + e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void dropTable(String tableName, Statement stmt) throws SQLException {
+    String dropTableSQL = SQLDBConstants.DROP_TABLE.replace("{table_name}", tableName);
+    stmt.executeUpdate(dropTableSQL);
+  }
+
+  private void createContainerLogIndex(Statement stmt) throws SQLException {
+    String createIndexSQL = SQLDBConstants.CREATE_INDEX_LATEST_STATE;
+    stmt.execute(createIndexSQL);
+  }
+
+  /**
+   * Lists containers filtered by the specified state and writes their details to stdout 
+   * unless redirected to a file explicitly.
+   * The output includes timestamp, datanode ID, container ID, BCSID, error message, and index value,
+   * written in a human-readable table format to a file or console.
+   * Behavior based on the {@code limit} parameter:
+   * If {@code limit} is provided, only up to the specified number of rows are printed.
+   * If the number of matching containers exceeds the {@code limit},
+   * a note is printed indicating more containers exist.
+   *
+   * @param state the container state to filter by (e.g., "OPEN", "CLOSED", "QUASI_CLOSED")
+   * @param limit the maximum number of rows to display; use {@link Integer#MAX_VALUE} to fetch all rows
+   */
+
+  public void listContainersByState(String state, Integer limit) throws SQLException {
+    int count = 0;
+
+    boolean limitProvided = limit != Integer.MAX_VALUE;
+
+    String baseQuery = SQLDBConstants.SELECT_LATEST_CONTAINER_LOGS_BY_STATE;
+    String finalQuery = limitProvided ? baseQuery + " LIMIT ?" : baseQuery;
+
+    try (Connection connection = getConnection();
+         Statement stmt = connection.createStatement()) {
+
+      createContainerLogIndex(stmt);
+
+      try (PreparedStatement pstmt = connection.prepareStatement(finalQuery)) {
+        pstmt.setString(1, state);
+        if (limitProvided) {
+          pstmt.setInt(2, limit + 1);
+        }
+
+        try (ResultSet rs = pstmt.executeQuery();
+             PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out,
+                 StandardCharsets.UTF_8), true)) {
+
+          writer.printf("%-25s | %-35s | %-15s | %-15s | %-40s | %-12s%n",
+              "Timestamp", "Datanode ID", "Container ID", "BCSID", "Message", "Index Value");
+          writer.println("-------------------------------------------------------------------------------------" +
+              "---------------------------------------------------------------------------------------");
+
+          while (rs.next()) {
+            if (limitProvided && count >= limit) {
+              writer.println("Note: There might be more containers. Use -all option to list all entries");
+              break;
+            }
+            String timestamp = rs.getString("timestamp");
+            String datanodeId = rs.getString("datanode_id");
+            long containerId = rs.getLong("container_id");
+            long latestBcsid = rs.getLong("latest_bcsid");
+            String errorMessage = rs.getString("error_message");
+            int indexValue = rs.getInt("index_value");
+            count++;
+
+            writer.printf("%-25s | %-35s | %-15d | %-15d | %-40s | %-12d%n",
+                timestamp, datanodeId, containerId, latestBcsid, errorMessage, indexValue);
+          }
+          
+          if (count == 0) {
+            writer.printf("No containers found for state: %s%n", state);
+          } else {
+            writer.printf("Number of containers listed: %d%n", count);
+          }
+        }
+      }
+    } catch (SQLException e) {
+      throw new SQLException("Error while retrieving containers with state " + state);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createIdxDclContainerStateTime(Connection conn) throws SQLException {
+    String sql = SQLDBConstants.CREATE_DCL_CONTAINER_STATE_TIME_INDEX;
+    try (Statement stmt = conn.createStatement()) {
+      stmt.execute(sql);
+    }
+  }
+
+  /**
+   * Displays detailed information about a container based on its ID, including its state, BCSID, 
+   * timestamp, message, and index value. It also checks for issues such as UNHEALTHY 
+   * replicas, under-replication, over-replication, OPEN_UNHEALTHY, OUASI_CLOSED_STUCK, mismatched replication
+   * and duplicate open.
+   *
+   * @param containerID The ID of the container to display details for.
+   */
+
+  public void showContainerDetails(Long containerID) throws SQLException {
+
+    try (Connection connection = getConnection()) {
+      createIdxDclContainerStateTime(connection);
+      List<DatanodeContainerInfo> logEntries = getContainerLogData(containerID, connection);
+
+      if (logEntries.isEmpty()) {
+        System.out.println("Missing container with ID: " + containerID);
+        return;
+      }
+
+      System.out.printf("%-25s | %-15s | %-35s | %-20s | %-10s | %-30s | %-12s%n",
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: Tejaskriya
body: A suggestion w.r.t. using System.out for printing the results. Can we pass a PrintStream or a PrintWriter to this class/method, and use that to print everything? Maybe add it in the constructor itself.
Even for `listContainersByState()`, while creating the PrintWriter the same can be used. As a fallback, we can use System.out as the default.

That would give some flexibility. 
created_at: 2025-05-02T06:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,6 +47,16 @@ public class ContainerDatanodeDatabase {
   
   private static String databasePath;
   private static final int DEFAULT_REPLICATION_FACTOR;
+
+  private final PrintWriter out;
+
+  public ContainerDatanodeDatabase() {
+    this.out = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8), true);
+  }
+
+  public ContainerDatanodeDatabase(PrintWriter out, PrintWriter err) {
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/utils/ContainerDatanodeDatabase.java
user: login: Tejaskriya
body: Looks like after changing the exceptions, we aren't using err anywhere, we can remove it from the constructor
created_at: 2025-05-02T11:07:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "info",
+    description = "Provides complete state transition history of each replica for a single container along with " +
+        "analysis over the container"
+)
+public class ContainerInfoCommand extends AbstractSubcommand implements Callable<Void> {
+
+  @CommandLine.Parameters(index = "0", description = "Container ID")
+  private Long containerId;
+
+  @CommandLine.ParentCommand
+  private ContainerLogController parent;
+
+  @Override
+  public Void call() throws Exception {
+
+    if (containerId < 0) {
+      err().println("Invalid container ID: " + containerId);
+      return null;
+    }
+
+    Path providedDbPath;
+    if (parent.getDbPath() == null) {
+      providedDbPath = Paths.get(System.getProperty("user.dir"), SQLDBConstants.DEFAULT_DB_FILENAME);
+
+      if (Files.exists(providedDbPath) && Files.isRegularFile(providedDbPath)) {
+        out().println("Using default database file found in current directory: " + providedDbPath);
+      } else {
+        err().println("No database path provided and default file '" + SQLDBConstants.DEFAULT_DB_FILENAME + "' not " +
+            "found in current directory. Please provide a valid database path");
+        return null;
+      }
+    } else {
+      providedDbPath = Paths.get(parent.getDbPath());
+      Path parentDir = providedDbPath.getParent();
+
+      if (parentDir != null && !Files.exists(parentDir)) {
+        err().println("The parent directory of the provided database path does not exist: " + parentDir);
+        return null;
+      }
+    }
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
user: login: adoroszlai
body: `--db` is inherited from the parent command, but validation is duplicated among subcommands.  It should be done in `ContainerLogController`.
created_at: 2025-05-06T04:56:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+/*
+ * 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.hadoop.ozone.debug.logs.container;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.AbstractSubcommand;
+import org.apache.hadoop.ozone.debug.logs.container.utils.ContainerDatanodeDatabase;
+import org.apache.hadoop.ozone.debug.logs.container.utils.SQLDBConstants;
+import picocli.CommandLine;
+
+/**
+ * Command to display detailed information of a single container by ID.
+ */
+
+@CommandLine.Command(
+    name = "info",
+    description = "Provides complete state transition history of each replica for a single container along with " +
+        "analysis over the container"
+)
+public class ContainerInfoCommand extends AbstractSubcommand implements Callable<Void> {
+
+  @CommandLine.Parameters(index = "0", description = "Container ID")
+  private Long containerId;
+
+  @CommandLine.ParentCommand
+  private ContainerLogController parent;
+
+  @Override
+  public Void call() throws Exception {
+
+    if (containerId < 0) {
+      err().println("Invalid container ID: " + containerId);
+      return null;
+    }
+
+    Path providedDbPath;
+    if (parent.getDbPath() == null) {
+      providedDbPath = Paths.get(System.getProperty("user.dir"), SQLDBConstants.DEFAULT_DB_FILENAME);
+
+      if (Files.exists(providedDbPath) && Files.isRegularFile(providedDbPath)) {
+        out().println("Using default database file found in current directory: " + providedDbPath);
+      } else {
+        err().println("No database path provided and default file '" + SQLDBConstants.DEFAULT_DB_FILENAME + "' not " +
+            "found in current directory. Please provide a valid database path");
+        return null;
+      }
+    } else {
+      providedDbPath = Paths.get(parent.getDbPath());
+      Path parentDir = providedDbPath.getParent();
+
+      if (parentDir != null && !Files.exists(parentDir)) {
+        err().println("The parent directory of the provided database path does not exist: " + parentDir);
+        return null;
+      }
+    }
+
+    ContainerDatanodeDatabase.setDatabasePath(providedDbPath.toString());
+
+    ContainerDatanodeDatabase cdd = new ContainerDatanodeDatabase();
path: hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/logs/container/ContainerInfoCommand.java
user: login: adoroszlai
body: DB path should be passed to the `ContainerDatanodeDatabase` instance, where `databasePath` should not be static.
created_at: 2025-05-06T05:00:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: ashishkumar50
created_at: 2025-05-06T04:49:28Z
body: @adoroszlai @aryangupta1998 @Tejaskriya, would you like to take another look?
author_association: CONTRIBUTOR
user: login: adoroszlai
created_at: 2025-05-06T15:24:09Z
body: Thanks @sreejasahithi for the patch, @aryangupta1998, @ashishkumar50, @Tejaskriya for the review.
author_association: CONTRIBUTOR