Add created column to Fate Print command (#1980)

* Add timestamp of created time based on transaction Id's of FaTE operations
* Create a helper function to format the returned long value of the timestamp
* Formatted created time within the `fate print` shell command
diff --git a/core/src/main/java/org/apache/accumulo/core/logging/FateLogger.java b/core/src/main/java/org/apache/accumulo/core/logging/FateLogger.java
index c856275..6121bce 100644
--- a/core/src/main/java/org/apache/accumulo/core/logging/FateLogger.java
+++ b/core/src/main/java/org/apache/accumulo/core/logging/FateLogger.java
@@ -86,6 +86,11 @@
       }
 
       @Override
+      public long timeCreated(long tid) {
+        return store.timeCreated(tid);
+      }
+
+      @Override
       public long create() {
         long tid = store.create();
         if (storeLog.isTraceEnabled())
diff --git a/core/src/main/java/org/apache/accumulo/fate/AdminUtil.java b/core/src/main/java/org/apache/accumulo/fate/AdminUtil.java
index 3016d01..d4e45a8 100644
--- a/core/src/main/java/org/apache/accumulo/fate/AdminUtil.java
+++ b/core/src/main/java/org/apache/accumulo/fate/AdminUtil.java
@@ -20,8 +20,11 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Date;
 import java.util.EnumSet;
 import java.util.Formatter;
 import java.util.HashMap;
@@ -80,9 +83,10 @@
     private final List<String> hlocks;
     private final List<String> wlocks;
     private final String top;
+    private final long timeCreated;
 
     private TransactionStatus(Long tid, TStatus status, String debug, List<String> hlocks,
-        List<String> wlocks, String top) {
+        List<String> wlocks, String top, Long timeCreated) {
 
       this.txid = tid;
       this.status = status;
@@ -90,6 +94,7 @@
       this.hlocks = Collections.unmodifiableList(hlocks);
       this.wlocks = Collections.unmodifiableList(wlocks);
       this.top = top;
+      this.timeCreated = timeCreated;
 
     }
 
@@ -132,6 +137,21 @@
     public String getTop() {
       return top;
     }
+
+    /**
+     * @return The timestamp of when the operation was created in ISO format wiht UTC timezone.
+     */
+    public String getTimeCreatedFormatted() {
+      return timeCreated > 0 ? new Date(timeCreated).toInstant().atZone(ZoneOffset.UTC)
+          .format(DateTimeFormatter.ISO_DATE_TIME) : "ERROR";
+    }
+
+    /**
+     * @return The unformatted form of the timestamp.
+     */
+    public long getTimeCreated() {
+      return timeCreated;
+    }
   }
 
   public static class FateStatus {
@@ -373,13 +393,15 @@
 
       TStatus status = zs.getStatus(tid);
 
+      long timeCreated = zs.timeCreated(tid);
+
       zs.unreserve(tid, 0);
 
       if ((filterTxid != null && !filterTxid.contains(tid))
           || (filterStatus != null && !filterStatus.contains(status)))
         continue;
 
-      statuses.add(new TransactionStatus(tid, status, debug, hlocks, wlocks, top));
+      statuses.add(new TransactionStatus(tid, status, debug, hlocks, wlocks, top, timeCreated));
     }
 
     return new FateStatus(statuses, heldLocks, waitingLocks);
@@ -398,9 +420,10 @@
     FateStatus fateStatus = getStatus(zs, zk, lockPath, filterTxid, filterStatus);
 
     for (TransactionStatus txStatus : fateStatus.getTransactions()) {
-      fmt.format("txid: %s  status: %-18s  op: %-15s  locked: %-15s locking: %-15s top: %s%n",
+      fmt.format(
+          "txid: %s  status: %-18s  op: %-15s  locked: %-15s locking: %-15s top: %-15s created: %s%n",
           txStatus.getTxid(), txStatus.getStatus(), txStatus.getDebug(), txStatus.getHeldLocks(),
-          txStatus.getWaitingLocks(), txStatus.getTop());
+          txStatus.getWaitingLocks(), txStatus.getTop(), txStatus.getTimeCreatedFormatted());
     }
     fmt.format(" %s transactions", fateStatus.getTransactions().size());
 
diff --git a/core/src/main/java/org/apache/accumulo/fate/AgeOffStore.java b/core/src/main/java/org/apache/accumulo/fate/AgeOffStore.java
index ccc3aca..0c41d31 100644
--- a/core/src/main/java/org/apache/accumulo/fate/AgeOffStore.java
+++ b/core/src/main/java/org/apache/accumulo/fate/AgeOffStore.java
@@ -231,6 +231,11 @@
   }
 
   @Override
+  public long timeCreated(long tid) {
+    return store.timeCreated(tid);
+  }
+
+  @Override
   public List<ReadOnlyRepo<T>> getStack(long tid) {
     return store.getStack(tid);
   }
diff --git a/core/src/main/java/org/apache/accumulo/fate/ReadOnlyStore.java b/core/src/main/java/org/apache/accumulo/fate/ReadOnlyStore.java
index c5361be..c53a73b 100644
--- a/core/src/main/java/org/apache/accumulo/fate/ReadOnlyStore.java
+++ b/core/src/main/java/org/apache/accumulo/fate/ReadOnlyStore.java
@@ -116,4 +116,9 @@
   public List<ReadOnlyRepo<T>> getStack(long tid) {
     return store.getStack(tid);
   }
+
+  @Override
+  public long timeCreated(long tid) {
+    return store.timeCreated(tid);
+  }
 }
diff --git a/core/src/main/java/org/apache/accumulo/fate/ReadOnlyTStore.java b/core/src/main/java/org/apache/accumulo/fate/ReadOnlyTStore.java
index a302bc4..1d0d93b 100644
--- a/core/src/main/java/org/apache/accumulo/fate/ReadOnlyTStore.java
+++ b/core/src/main/java/org/apache/accumulo/fate/ReadOnlyTStore.java
@@ -139,4 +139,12 @@
    */
   List<Long> list();
 
+  /**
+   * Retrieve the creation time of a FaTE transaction.
+   *
+   * @param tid
+   *          Transaction id, previously reserved.
+   * @return creation time of transaction.
+   */
+  long timeCreated(long tid);
 }
diff --git a/core/src/main/java/org/apache/accumulo/fate/ZooStore.java b/core/src/main/java/org/apache/accumulo/fate/ZooStore.java
index ee48b10..d84da1d 100644
--- a/core/src/main/java/org/apache/accumulo/fate/ZooStore.java
+++ b/core/src/main/java/org/apache/accumulo/fate/ZooStore.java
@@ -44,6 +44,7 @@
 import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.KeeperException.NoNodeException;
 import org.apache.zookeeper.KeeperException.NodeExistsException;
+import org.apache.zookeeper.data.Stat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -479,6 +480,18 @@
   }
 
   @Override
+  public long timeCreated(long tid) {
+    verifyReserved(tid);
+
+    try {
+      Stat stat = zk.getZooKeeper().exists(getTXPath(tid), false);
+      return stat.getCtime();
+    } catch (Exception e) {
+      return 0;
+    }
+  }
+
+  @Override
   public List<ReadOnlyRepo<T>> getStack(long tid) {
     String txpath = getTXPath(tid);
 
diff --git a/core/src/test/java/org/apache/accumulo/fate/SimpleStore.java b/core/src/test/java/org/apache/accumulo/fate/SimpleStore.java
index 3d5bc34..0bdd017 100644
--- a/core/src/test/java/org/apache/accumulo/fate/SimpleStore.java
+++ b/core/src/test/java/org/apache/accumulo/fate/SimpleStore.java
@@ -126,6 +126,11 @@
   }
 
   @Override
+  public long timeCreated(long tid) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public List<ReadOnlyRepo<T>> getStack(long tid) {
     throw new UnsupportedOperationException();
   }