You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by ns...@apache.org on 2011/10/11 19:43:42 UTC

svn commit: r1181948 - in /hbase/branches/0.89/src: main/java/org/apache/hadoop/hbase/ main/java/org/apache/hadoop/hbase/client/ main/java/org/apache/hadoop/hbase/ipc/ test/java/org/apache/hadoop/hbase/client/

Author: nspiegelberg
Date: Tue Oct 11 17:43:41 2011
New Revision: 1181948

URL: http://svn.apache.org/viewvc?rev=1181948&view=rev
Log:
Enhanced, Parseable, and Fingerprinted Slow Query Log

Summary:
Starting with the general concept from Kannan's slow query logging, developed
an interface for fingerprinting and building JSON objects to report crucial
information about slow queries without flooding the log. Implemented this for
Puts, Gets, Scans, Deletes, and MultiPuts. Also changed the location of the
logging from HBaseServer to HBaseRPC for ease of getting region information for
these queries.

Sample output:
2011-08-22 15:58:40,516 WARN org.apache.hadoop.ipc.HBaseServer:
(operationTooSlow):
{"processingtimemillis":1,"class":"HRegionServer","responsesize":0,"starttimemillis":1314053920513,"method":"put","totalColumns":2,"table":".META.","queuetimemillis":0,"families":{"info":[{"timestamp":1314053920513,"qualifier":"server","vlen":31},{"timestamp":1314053920513,"qualifier":"serverstartcode","vlen":8}]},"row":"peeps,,1313609181577.d7b7001adabe030d66dd59a1746b49c3."}

Test Plan: Built and ran with a 1ms warnResponseTime and ran all types of
logged queries to see the output.

Reviewers: nspiegelberg, kannan

Reviewed By: kannan

CC: kannan, hbase@lists, nspiegelberg, riley

Differential Revision: 288291

Task ID: 630871

Added:
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Operation.java
    hbase/branches/0.89/src/test/java/org/apache/hadoop/hbase/client/TestOperation.java
Modified:
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/KeyValue.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Delete.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Get.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/MultiPut.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Put.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Scan.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseRPC.java
    hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/KeyValue.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/KeyValue.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/KeyValue.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/KeyValue.java Tue Oct 11 17:43:41 2011
@@ -642,6 +642,7 @@ public class KeyValue implements Writabl
     stringMap.put("family", Bytes.toStringBinary(getFamily()));
     stringMap.put("qualifier", Bytes.toStringBinary(getQualifier()));
     stringMap.put("timestamp", getTimestamp());
+    stringMap.put("vlen", getValueLength());
     return stringMap;
   }
 

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Delete.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Delete.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Delete.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Delete.java Tue Oct 11 17:43:41 2011
@@ -29,6 +29,7 @@ import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -65,7 +66,8 @@ import java.util.TreeMap;
  * deleteFamily -- then you need to use the method overrides that take a
  * timestamp.  The constructor timestamp is not referenced.
  */
-public class Delete implements Writable, Row, Comparable<Row> {
+public class Delete extends Operation
+  implements Writable, Row, Comparable<Row> {
   private static final byte DELETE_VERSION = (byte)1;
 
   private byte [] row = null;
@@ -275,39 +277,66 @@ public class Delete implements Writable,
   }
 
   /**
-   * @return string
+   * Compile the column family (i.e. schema) information
+   * into a Map. Useful for parsing and aggregation by debugging,
+   * logging, and administration tools.
+   * @return Map
    */
   @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append("row=");
-    sb.append(Bytes.toString(this.row));
-    sb.append(", ts=");
-    sb.append(this.ts);
-    sb.append(", families={");
-    boolean moreThanOne = false;
-    for(Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
-      if(moreThanOne) {
-        sb.append(", ");
-      } else {
-        moreThanOne = true;
+  public Map<String, Object> getFingerprint() {
+    Map<String, Object> map = new HashMap<String, Object>();
+    List<String> families = new ArrayList<String>();
+    // ideally, we would also include table information, but that information
+    // is not stored in each Operation instance.
+    map.put("families", families);
+    for (Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
+      families.add(Bytes.toStringBinary(entry.getKey()));
+    }
+    return map;
+  }
+
+  /**
+   * Compile the details beyond the scope of getFingerprint (row, columns,
+   * timestamps, etc.) into a Map along with the fingerprinted information.
+   * Useful for debugging, logging, and administration tools.
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return Map
+   */
+  @Override
+  public Map<String, Object> toMap(int maxCols) {
+    // we start with the fingerprint map and build on top of it.
+    Map<String, Object> map = getFingerprint();
+    // replace the fingerprint's simple list of families with a
+    // map from column families to lists of qualifiers and kv details
+    Map<String, List<Map<String, Object>>> columns =
+      new HashMap<String, List<Map<String, Object>>>();
+    map.put("families", columns);
+    map.put("row", Bytes.toStringBinary(this.row));
+    int colCount = 0;
+    // iterate through all column families affected by this Delete
+    for (Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
+      // map from this family to details for each kv affected within the family
+      List<Map<String, Object>> qualifierDetails =
+        new ArrayList<Map<String, Object>>();
+      columns.put(Bytes.toStringBinary(entry.getKey()), qualifierDetails);
+      colCount += entry.getValue().size();
+      if (maxCols <= 0) {
+        continue;
       }
-      sb.append("(family=");
-      sb.append(Bytes.toString(entry.getKey()));
-      sb.append(", keyvalues=(");
-      boolean moreThanOneB = false;
-      for(KeyValue kv : entry.getValue()) {
-        if(moreThanOneB) {
-          sb.append(", ");
-        } else {
-          moreThanOneB = true;
+      // add details for each kv
+      for (KeyValue kv : entry.getValue()) {
+        if (--maxCols <= 0 ) {
+          continue;
         }
-        sb.append(kv.toString());
+        Map<String, Object> kvMap = kv.toStringMap();
+        // row and family information are already available in the bigger map
+        kvMap.remove("row");
+        kvMap.remove("family");
+        qualifierDetails.add(kvMap);
       }
-      sb.append(")");
     }
-    sb.append("}");
-    return sb.toString();
+    map.put("totalColumns", colCount);
+    return map;
   }
 
   //Writable

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Get.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Get.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Get.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Get.java Tue Oct 11 17:43:41 2011
@@ -30,6 +30,9 @@ import org.apache.hadoop.io.WritableFact
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.NavigableSet;
 import java.util.Set;
@@ -63,7 +66,7 @@ import java.util.TreeSet;
  * <p>
  * To add a filter, execute {@link #setFilter(Filter) setFilter}.
  */
-public class Get implements Writable {
+public class Get extends Operation implements Writable {
   private static final byte GET_VERSION = (byte)2;
 
   private byte [] row = null;
@@ -299,55 +302,71 @@ public class Get implements Writable {
   }
 
   /**
-   * @return String
+   * Compile the table and column family (i.e. schema) information
+   * into a String. Useful for parsing and aggregation by debugging,
+   * logging, and administration tools.
+   * @return Map
    */
   @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append("row=");
-    sb.append(Bytes.toString(this.row));
-    sb.append(", maxVersions=");
-    sb.append("").append(this.maxVersions);
-    sb.append(", storeLimit=");
-    sb.append("").append(this.storeLimit);
-    sb.append(", timeRange=");
-    sb.append("[").append(this.tr.getMin()).append(",");
-    sb.append(this.tr.getMax()).append(")");
-    sb.append(", families=");
-    if(this.familyMap.size() == 0) {
-      sb.append("ALL");
-      return sb.toString();
+  public Map<String, Object> getFingerprint() {
+    Map<String, Object> map = new HashMap<String, Object>();
+    List<String> families = new ArrayList<String>();
+    map.put("families", families);
+    for (Map.Entry<byte [], NavigableSet<byte[]>> entry :
+      this.familyMap.entrySet()) {
+      families.add(Bytes.toStringBinary(entry.getKey()));
     }
-    boolean moreThanOne = false;
-    for(Map.Entry<byte [], NavigableSet<byte[]>> entry :
+    return map;
+  }
+
+  /**
+   * Compile the details beyond the scope of getFingerprint (row, columns,
+   * timestamps, etc.) into a Map along with the fingerprinted information.
+   * Useful for debugging, logging, and administration tools.
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return Map
+   */
+  @Override
+  public Map<String, Object> toMap(int maxCols) {
+    // we start with the fingerprint map and build on top of it.
+    Map<String, Object> map = getFingerprint();
+    // replace the fingerprint's simple list of families with a
+    // map from column families to lists of qualifiers and kv details
+    Map<String, List<String>> columns = new HashMap<String, List<String>>();
+    map.put("families", columns);
+    // add scalar information first
+    map.put("row", Bytes.toStringBinary(this.row));
+    map.put("maxVersions", this.maxVersions);
+    map.put("storeLimit", this.storeLimit);
+    List<Long> timeRange = new ArrayList<Long>();
+    timeRange.add(this.tr.getMin());
+    timeRange.add(this.tr.getMax());
+    map.put("timeRange", timeRange);
+    int colCount = 0;
+    // iterate through affected families and add details
+    for (Map.Entry<byte [], NavigableSet<byte[]>> entry :
       this.familyMap.entrySet()) {
-      if(moreThanOne) {
-        sb.append("), ");
-      } else {
-        moreThanOne = true;
-        sb.append("{");
-      }
-      sb.append("(family=");
-      sb.append(Bytes.toString(entry.getKey()));
-      sb.append(", columns=");
+      List<String> familyList = new ArrayList<String>();
+      columns.put(Bytes.toStringBinary(entry.getKey()), familyList);
       if(entry.getValue() == null) {
-        sb.append("ALL");
+        colCount++;
+        --maxCols;
+        familyList.add("ALL");
       } else {
-        sb.append("{");
-        boolean moreThanOneB = false;
-        for(byte [] column : entry.getValue()) {
-          if(moreThanOneB) {
-            sb.append(", ");
-          } else {
-            moreThanOneB = true;
+        colCount += entry.getValue().size();
+        if (maxCols <= 0) {
+          continue;
+        }
+        for (byte [] column : entry.getValue()) {
+          if (--maxCols <= 0) {
+            continue;
           }
-          sb.append(Bytes.toString(column));
+          familyList.add(Bytes.toStringBinary(column));
         }
-        sb.append("}");
       }
     }
-    sb.append("}");
-    return sb.toString();
+    map.put("totalColumns", colCount);
+    return map;
   }
 
   //Writable

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/MultiPut.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/MultiPut.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/MultiPut.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/MultiPut.java Tue Oct 11 17:43:41 2011
@@ -20,6 +20,7 @@
 
 package org.apache.hadoop.hbase.client;
 
+import org.apache.hadoop.hbase.HRegionInfo;
 import org.apache.hadoop.hbase.HServerAddress;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.apache.hadoop.io.Writable;
@@ -29,16 +30,24 @@ import java.io.DataOutput;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 /**
  * Data type class for putting multiple regions worth of puts in one RPC.
  */
-public class MultiPut implements Writable {
+public class MultiPut extends Operation implements Writable {
   public HServerAddress address; // client code ONLY
 
+  // for limiting string/map output
+  // TODO make this configurable
+  public static final int DEFAULT_MAX_PUT_OUTPUT = 10;
+
   // map of regions to lists of puts for that region.
   public Map<byte[], List<Put> > puts = new TreeMap<byte[], List<Put>>(Bytes.BYTES_COMPARATOR);
 
@@ -80,24 +89,115 @@ public class MultiPut implements Writabl
     return res;
   }
 
+  /**
+   * Compile the table and column family (i.e. schema) information
+   * into a String. Useful for parsing and aggregation by debugging,
+   * logging, and administration tools.
+   * @return Map
+   */
   @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-
-    sb.append("#regions:" + puts.size() + " {");
-    for (List<Put> items : puts.values()) {
-      sb.append("#puts:" + items.size() + " <");
-      for (Put p : items) {
-        sb.append(p.toStringMax(512));
-        if (sb.length() > 512 * 2) {
-          sb.append("....<output truncated>...");
-          return sb.toString();
+  public Map<String, Object> getFingerprint() {
+    Map<String, Object> map = new HashMap<String, Object>();
+    // for extensibility, we have a map of table information that we will
+    // populate with only family information for each table
+    Map<String, Map> tableInfo =
+      new HashMap<String, Map>();
+    map.put("tables", tableInfo);
+    for (Map.Entry<byte[], List<Put>> entry : puts.entrySet()) {
+      // our fingerprint only concerns itself with which families are touched,
+      // not how many Puts touch them, so we use this Set to do just that.
+      Set<String> familySet;
+      try {
+        // since the puts are stored by region, we may have already
+        // recorded families for this region. if that is the case,
+        // we want to add to the existing Set. if not, we make a new Set.
+        String tableName = Bytes.toStringBinary(
+            HRegionInfo.parseRegionName(entry.getKey())[0]);
+        if (tableInfo.get(tableName) == null) {
+          Map<String, Object> table = new HashMap<String, Object>();
+          familySet = new TreeSet<String>();
+          table.put("families", familySet);
+          tableInfo.put(tableName, table);
+        } else {
+          familySet = (Set<String>) tableInfo.get(tableName).get("families");
         }
+      } catch (IOException ioe) {
+        // in the case of parse error, default to labeling by region
+        Map<String, Object> table = new HashMap<String, Object>();
+        familySet = new TreeSet<String>();
+        table.put("families", familySet);
+        tableInfo.put(Bytes.toStringBinary(entry.getKey()), table);
+      }
+      // we now iterate through each Put and keep track of which families
+      // are affected in this table.
+      for (Put p : entry.getValue()) {
+        for (byte[] fam : p.getFamilyMap().keySet()) {
+          familySet.add(Bytes.toStringBinary(fam));
+        }
+      }
+    }
+    return map;
+  }
+
+  /**
+   * Compile the details beyond the scope of getFingerprint (mostly
+   * toMap from the Puts) into a Map along with the fingerprinted
+   * information. Useful for debugging, logging, and administration tools.
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return Map
+   */
+  @Override
+  public Map<String, Object> toMap(int maxCols) {
+    Map<String, Object> map = getFingerprint();
+    Map<String, Object> tableInfo = (Map<String, Object>) map.get("tables");
+    int putCount = 0;
+    for (Map.Entry<byte[], List<Put>> entry : puts.entrySet()) {
+      // If the limit has been hit for put output, just adjust our counter
+      if (putCount >= DEFAULT_MAX_PUT_OUTPUT) {
+        putCount += entry.getValue().size();
+        continue;
+      }
+      List<Put> regionPuts = entry.getValue();
+      List<Map<String, Object>> putSummaries =
+        new ArrayList<Map<String, Object>>();
+      // find out how many of this region's puts we can add without busting
+      // the maximum
+      int regionPutsToAdd = regionPuts.size();
+      putCount += regionPutsToAdd;
+      if (putCount > DEFAULT_MAX_PUT_OUTPUT) {
+        regionPutsToAdd -= putCount - DEFAULT_MAX_PUT_OUTPUT;
+      }
+      for (Iterator<Put> iter = regionPuts.iterator(); regionPutsToAdd-- > 0;) {
+        putSummaries.add(iter.next().toMap(maxCols));
+      }
+      // attempt to extract the table name from the region name
+      String tableName = "";
+      try {
+        tableName = Bytes.toStringBinary(
+            HRegionInfo.parseRegionName(entry.getKey())[0]);
+      } catch (IOException ioe) {
+        // in the case of parse error, default to labeling by region
+        tableName = Bytes.toStringBinary(entry.getKey());
+      }
+      // since the puts are stored by region, we may have already
+      // recorded puts for this table. if that is the case,
+      // we want to add to the existing List. if not, we place a new list
+      // in the map
+      Map<String, Object> table =
+        (Map<String, Object>) tableInfo.get(tableName);
+      if (table == null) {
+        // in case the Put has changed since getFingerprint's map was built
+        table = new HashMap<String, Object>();
+        tableInfo.put(tableName, table);
+        table.put("puts", putSummaries);
+      } else if (table.get("puts") == null) {
+        table.put("puts", putSummaries);
+      } else {
+        ((List<Map<String, Object>>) table.get("puts")).addAll(putSummaries);
       }
-      sb.append(">");
     }
-    sb.append("}");
-    return sb.toString();
+    map.put("totalPuts", putCount);
+    return map;
   }
 
   @Override

Added: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Operation.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Operation.java?rev=1181948&view=auto
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Operation.java (added)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Operation.java Tue Oct 11 17:43:41 2011
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011 The Apache Software Foundation
+ *
+ * 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.hbase.client;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.codehaus.jackson.map.ObjectMapper;
+
+/**
+ * Superclass for any type that maps to a potentially application-level query.
+ * (e.g. Put, Get, Delete, Scan, Next, etc.)
+ * Contains methods for exposure to logging and debugging tools.
+ */
+public abstract class Operation {
+  // TODO make this configurable
+  private static final int DEFAULT_MAX_COLS = 5;
+
+  /**
+   * Produces a Map containing a fingerprint which identifies the type and
+   * the static schema components of a query (i.e. column families)
+   * @return a map containing fingerprint information (i.e. column families)
+   */
+  public abstract Map<String, Object> getFingerprint();
+
+  /**
+   * Produces a Map containing a summary of the details of a query
+   * beyond the scope of the fingerprint (i.e. columns, rows...)
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return a map containing parameters of a query (i.e. rows, columns...)
+   */
+  public abstract Map<String, Object> toMap(int maxCols);
+
+  /**
+   * Produces a Map containing a full summary of a query.
+   * @return a map containing parameters of a query (i.e. rows, columns...)
+   */
+  public Map<String, Object> toMap() {
+    return toMap(DEFAULT_MAX_COLS);
+  }
+
+  /**
+   * Produces a JSON object for fingerprint and details exposure in a
+   * parseable format.
+   * @param maxCols a limit on the number of columns to include in the JSON
+   * @return a JSONObject containing this Operation's information, as a string
+   */
+  public String toJSON(int maxCols) throws IOException {
+    ObjectMapper mapper = new ObjectMapper();
+    return mapper.writeValueAsString(toMap(maxCols));
+  }
+
+  /**
+   * Produces a JSON object sufficient for description of a query
+   * in a debugging or logging context.
+   * @return the produced JSON object, as a string
+   */
+  public String toJSON() throws IOException {
+    return toJSON(DEFAULT_MAX_COLS);
+  }
+
+  /**
+   * Produces a string representation of this Operation. It defaults to a JSON
+   * representation, but falls back to a string representation of the
+   * fingerprint and details in the case of a JSON encoding failure.
+   * @param maxCols a limit on the number of columns output in the summary
+   * prior to truncation
+   * @return a JSON-parseable String
+   */
+  public String toString(int maxCols) {
+    /* for now this is merely a wrapper from producing a JSON string, but
+     * toJSON is kept separate in case this is changed to be a less parsable
+     * pretty printed representation.
+     */
+    try {
+      return toJSON(maxCols);
+    } catch (IOException ioe) {
+      return toMap(maxCols).toString();
+    }
+  }
+
+  /**
+   * Produces a string representation of this Operation. It defaults to a JSON
+   * representation, but falls back to a string representation of the
+   * fingerprint and details in the case of a JSON encoding failure.
+   * @return String
+   */
+  @Override
+  public String toString() {
+    return toString(DEFAULT_MAX_COLS);
+  }
+}

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Put.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Put.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Put.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Put.java Tue Oct 11 17:43:41 2011
@@ -32,6 +32,7 @@ import java.io.DataOutput;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -44,7 +45,8 @@ import java.util.TreeMap;
  * for each column to be inserted, execute {@link #add(byte[], byte[], byte[]) add} or
  * {@link #add(byte[], byte[], long, byte[]) add} if setting the timestamp.
  */
-public class Put implements HeapSize, Writable, Row, Comparable<Row> {
+public class Put extends Operation
+  implements HeapSize, Writable, Row, Comparable<Row> {
   private static final byte PUT_VERSION = (byte)1;
 
   private byte [] row = null;
@@ -408,49 +410,66 @@ public class Put implements HeapSize, Wr
   }
 
   /**
-   * Truncate output buffer at suggested max size.
-   * @return String
+   * Compile the column family (i.e. schema) information
+   * into a Map. Useful for parsing and aggregation by debugging,
+   * logging, and administration tools.
+   * @return Map
    */
-  public String toStringMax(int suggestedMaxSize) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("row=");
-    sb.append(Bytes.toString(this.row));
-    sb.append(", families={");
-    boolean moreThanOne = false;
-    for(Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
-      if(moreThanOne) {
-        sb.append(", ");
-      } else {
-        moreThanOne = true;
-      }
-      sb.append("(family=");
-      sb.append(Bytes.toString(entry.getKey()));
-      sb.append(", keyvalues=(");
-      boolean moreThanOneB = false;
-      for(KeyValue kv : entry.getValue()) {
-        if(moreThanOneB) {
-          sb.append(", ");
-        } else {
-          moreThanOneB = true;
-        }
-        sb.append(kv.toString());
-        if (sb.length() > suggestedMaxSize) {
-          sb.append("...<output truncated>...");
-          return sb.toString();
-        }
-      }
-      sb.append(")");
-    }
-    sb.append("}");
-    return sb.toString();
+  @Override
+  public Map<String, Object> getFingerprint() {
+    Map<String, Object> map = new HashMap<String, Object>();
+    List<String> families = new ArrayList<String>();
+    // ideally, we would also include table information, but that information
+    // is not stored in each Operation instance.
+    map.put("families", families);
+    for (Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
+      families.add(Bytes.toStringBinary(entry.getKey()));
+    }
+    return map;
   }
 
   /**
-   * @return String
+   * Compile the details beyond the scope of getFingerprint (row, columns,
+   * timestamps, etc.) into a Map along with the fingerprinted information.
+   * Useful for debugging, logging, and administration tools.
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return Map
    */
   @Override
-  public String toString() {
-    return toStringMax(Integer.MAX_VALUE);
+  public Map<String, Object> toMap(int maxCols) {
+    // we start with the fingerprint map and build on top of it.
+    Map<String, Object> map = getFingerprint();
+    // replace the fingerprint's simple list of families with a
+    // map from column families to lists of qualifiers and kv details
+    Map<String, List<Map<String, Object>>> columns =
+      new HashMap<String, List<Map<String, Object>>>();
+    map.put("families", columns);
+    map.put("row", Bytes.toStringBinary(this.row));
+    int colCount = 0;
+    // iterate through all column families affected by this Put
+    for (Map.Entry<byte [], List<KeyValue>> entry : this.familyMap.entrySet()) {
+      // map from this family to details for each kv affected within the family
+      List<Map<String, Object>> qualifierDetails =
+        new ArrayList<Map<String, Object>>();
+      columns.put(Bytes.toStringBinary(entry.getKey()), qualifierDetails);
+      colCount += entry.getValue().size();
+      if (maxCols <= 0) {
+        continue;
+      }
+      // add details for each kv
+      for (KeyValue kv : entry.getValue()) {
+        if (--maxCols <= 0 ) {
+          continue;
+        }
+        Map<String, Object> kvMap = kv.toStringMap();
+        // row and family information are already available in the bigger map
+        kvMap.remove("row");
+        kvMap.remove("family");
+        qualifierDetails.add(kvMap);
+      }
+    }
+    map.put("totalColumns", colCount);
+    return map;
   }
 
   public int compareTo(Row p) {

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Scan.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Scan.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Scan.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/client/Scan.java Tue Oct 11 17:43:41 2011
@@ -33,6 +33,9 @@ import org.apache.hadoop.io.WritableFact
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.NavigableSet;
 import java.util.TreeMap;
@@ -79,8 +82,9 @@ import java.util.TreeSet;
  * Expert: To explicitly disable server-side block caching for this scan,
  * execute {@link #setCacheBlocks(boolean)}.
  */
-public class Scan implements Writable {
+public class Scan extends Operation implements Writable {
   private static final byte SCAN_VERSION = (byte)2;
+
   private byte [] startRow = HConstants.EMPTY_START_ROW;
   private byte [] stopRow  = HConstants.EMPTY_END_ROW;
   private int maxVersions = 1;
@@ -455,62 +459,79 @@ public class Scan implements Writable {
   }
 
   /**
-   * @return String
+   * Compile the table and column family (i.e. schema) information
+   * into a String. Useful for parsing and aggregation by debugging,
+   * logging, and administration tools.
+   * @return Map
    */
   @Override
-  public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append("startRow=");
-    sb.append(Bytes.toString(this.startRow));
-    sb.append(", stopRow=");
-    sb.append(Bytes.toString(this.stopRow));
-    sb.append(", maxVersions=");
-    sb.append(this.maxVersions);
-    sb.append(", batch=");
-    sb.append(this.batch);
-    sb.append(", storeLimit=");
-    sb.append(this.storeLimit);
-    sb.append(", caching=");
-    sb.append(this.caching);
-    sb.append(", cacheBlocks=");
-    sb.append(this.cacheBlocks);
-    sb.append(", timeRange=");
-    sb.append("[").append(this.tr.getMin()).append(",");
-    sb.append(this.tr.getMax()).append(")");
-    sb.append(", families=");
+  public Map<String, Object> getFingerprint() {
+    Map<String, Object> map = new HashMap<String, Object>();
+    List<String> families = new ArrayList<String>();
     if(this.familyMap.size() == 0) {
-      sb.append("ALL");
-      return sb.toString();
+      map.put("families", "ALL");
+      return map;
+    } else {
+      map.put("families", families);
     }
-    boolean moreThanOne = false;
-    for(Map.Entry<byte [], NavigableSet<byte[]>> entry : this.familyMap.entrySet()) {
-      if(moreThanOne) {
-        sb.append("), ");
-      } else {
-        moreThanOne = true;
-        sb.append("{");
-      }
-      sb.append("(family=");
-      sb.append(Bytes.toString(entry.getKey()));
-      sb.append(", columns=");
+    for (Map.Entry<byte [], NavigableSet<byte[]>> entry :
+      this.familyMap.entrySet()) {
+      families.add(Bytes.toStringBinary(entry.getKey()));
+    }
+    return map;
+  }
+
+  /**
+   * Compile the details beyond the scope of getFingerprint (row, columns,
+   * timestamps, etc.) into a Map along with the fingerprinted information.
+   * Useful for debugging, logging, and administration tools.
+   * @param maxCols a limit on the number of columns output prior to truncation
+   * @return Map
+   */
+  @Override
+  public Map<String, Object> toMap(int maxCols) {
+    // start with the fingerpring map and build on top of it
+    Map<String, Object> map = getFingerprint();
+    // map from families to column list replaces fingerprint's list of families
+    Map<String, List<String>> familyColumns = new HashMap<String, List<String>>();
+    map.put("families", familyColumns);
+    // add scalar information first
+    map.put("startRow", Bytes.toStringBinary(this.startRow));
+    map.put("stopRow", Bytes.toStringBinary(this.stopRow));
+    map.put("maxVersions", this.maxVersions);
+    map.put("batch", this.batch);
+    map.put("caching", this.caching);
+    map.put("cacheBlocks", this.cacheBlocks);
+    map.put("storeLimit", this.storeLimit);
+    List<Long> timeRange = new ArrayList<Long>();
+    timeRange.add(this.tr.getMin());
+    timeRange.add(this.tr.getMax());
+    map.put("timeRange", timeRange);
+    int colCount = 0;
+    // iterate through affected families and list out up to maxCols columns
+    for (Map.Entry<byte [], NavigableSet<byte[]>> entry :
+      this.familyMap.entrySet()) {
+      List<String> columns = new ArrayList<String>();
+      familyColumns.put(Bytes.toStringBinary(entry.getKey()), columns);
       if(entry.getValue() == null) {
-        sb.append("ALL");
+        colCount++;
+        --maxCols;
+        columns.add("ALL");
       } else {
-        sb.append("{");
-        boolean moreThanOneB = false;
-        for(byte [] column : entry.getValue()) {
-          if(moreThanOneB) {
-            sb.append(", ");
-          } else {
-            moreThanOneB = true;
+        colCount += entry.getValue().size();
+        if (maxCols <= 0) {
+          continue;
+        }
+        for (byte [] column : entry.getValue()) {
+          if (--maxCols <= 0) {
+            continue;
           }
-          sb.append(Bytes.toString(column));
+          columns.add(Bytes.toStringBinary(column));
         }
-        sb.append("}");
       }
     }
-    sb.append("}");
-    return sb.toString();
+    map.put("totalColumns", colCount);
+    return map;
   }
 
   @SuppressWarnings("unchecked")

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseRPC.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseRPC.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseRPC.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseRPC.java Tue Oct 11 17:43:41 2011
@@ -25,11 +25,18 @@ import org.apache.commons.logging.LogFac
 import org.apache.hadoop.conf.Configurable;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.client.RetriesExhaustedException;
+import org.apache.hadoop.hbase.client.Operation;
 import org.apache.hadoop.hbase.io.HbaseObjectWritable;
+import org.apache.hadoop.hbase.regionserver.HRegionServer;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.HRegionInfo;
 import org.apache.hadoop.io.Writable;
 import org.apache.hadoop.ipc.VersionedProtocol;
 import org.apache.hadoop.net.NetUtils;
 import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.util.StringUtils;
+
+import org.codehaus.jackson.map.ObjectMapper;
 
 import javax.net.SocketFactory;
 import java.io.DataInput;
@@ -530,6 +537,18 @@ public class HBaseRPC {
     private Class<?> implementation;
     private boolean verbose;
 
+    private static final String WARN_RESPONSE_TIME =
+      "hbase.ipc.warn.response.time";
+    private static final String WARN_RESPONSE_SIZE =
+      "hbase.ipc.warn.response.size";
+
+    /** Default value for above params */
+    private static final int DEFAULT_WARN_RESPONSE_TIME = 10000; // milliseconds
+    private static final int DEFAULT_WARN_RESPONSE_SIZE = 100 * 1024 * 1024;
+
+    private final int warnResponseTime;
+    private final int warnResponseSize;
+
     /**
      * Construct an RPC server.
      * @param instance the instance whose methods will be called
@@ -566,6 +585,10 @@ public class HBaseRPC {
       this.instance = instance;
       this.implementation = instance.getClass();
       this.verbose = verbose;
+      this.warnResponseTime = conf.getInt(WARN_RESPONSE_TIME,
+          DEFAULT_WARN_RESPONSE_TIME);
+    this.warnResponseSize = conf.getInt(WARN_RESPONSE_SIZE,
+        DEFAULT_WARN_RESPONSE_SIZE);
     }
 
     @Override
@@ -593,14 +616,36 @@ public class HBaseRPC {
         rpcMetrics.rpcQueueTime.inc(qTime);
         rpcMetrics.rpcProcessingTime.inc(processingTime);
         rpcMetrics.inc(call.getMethodName(), processingTime);
+        if (verbose) log("Return: "+value);
+
+        HbaseObjectWritable retVal =
+          new HbaseObjectWritable(method.getReturnType(), value);
+        long responseSize = retVal.getWritableSize();
+        // log any RPC responses that are slower than the configured warn
+        // response time or larger than configured warning size
+        boolean tooSlow = (processingTime > warnResponseTime
+            && warnResponseTime > -1);
+        boolean tooLarge = (responseSize > warnResponseSize
+            && warnResponseSize > -1);
+        if (tooSlow || tooLarge) {
+          // when tagging, we let TooLarge trump TooSmall to keep output simple
+          // note that large responses will often also be slow.
+          logResponse(call, (tooLarge ? "TooLarge" : "TooSlow"),
+              startTime, processingTime, qTime, responseSize);
+          if (tooSlow) {
+            // increment global slow RPC response counter
+            rpcMetrics.inc("slowResponse.", processingTime);
+          }
+        }
         if (processingTime > 1000) {
+          // we use a hard-coded one second period so that we can clearly
+          // indicate the time period we're warning about in the name of the
+          // metric itself
           rpcMetrics.inc(call.getMethodName() + ".aboveOneSec.",
-                         processingTime);
+              processingTime);
         }
-        if (verbose) log("Return: "+value);
-
-        return new HbaseObjectWritable(method.getReturnType(), value);
 
+        return retVal;
       } catch (InvocationTargetException e) {
         Throwable target = e.getTargetException();
         if (target instanceof IOException) {
@@ -615,6 +660,60 @@ public class HBaseRPC {
         throw ioe;
       }
     }
+
+    /**
+     * Logs an RPC response to the LOG file, producing valid JSON objects for
+     * client Operations.
+     * @param call The call to log.
+     * @param tag  The tag that will be used to indicate this event in the log.
+     * @param startTime       The time that the call was initiated, in ms.
+     * @param processingTime  The duration that the call took to run, in ms.
+     * @param qTime           The duration that the call spent on the queue
+     *                        prior to being initiated, in ms.
+     * @param responseSize    The size in bytes of the response buffer.
+     */
+    private void logResponse(Invocation call, String tag,
+        long startTime, int processingTime, int qTime, long responseSize)
+      throws IOException {
+      Object params[] = call.getParameters();
+      // for JSON encoding
+      ObjectMapper mapper = new ObjectMapper();
+      // base information that is reported regardless of type of call
+      Map<String, Object> responseInfo = new HashMap<String, Object>();
+      responseInfo.put("starttimems", startTime);
+      responseInfo.put("processingtimems", processingTime);
+      responseInfo.put("queuetimems", qTime);
+      responseInfo.put("responsesize", responseSize);
+      responseInfo.put("class", instance.getClass().getSimpleName());
+      responseInfo.put("method", call.getMethodName());
+      if (params.length == 2 && instance instanceof HRegionServer &&
+          params[0] instanceof byte[] &&
+          params[1] instanceof Operation) {
+        // if the slow process is a query, we want to log its table as well
+        // as its own fingerprint
+        byte [] tableName =
+          HRegionInfo.parseRegionName((byte[]) params[0])[0];
+        responseInfo.put("table", Bytes.toStringBinary(tableName));
+        // annotate the response map with operation details
+        responseInfo.putAll(((Operation) params[1]).toMap());
+        // report to the log file
+        LOG.warn("(operation" + tag + "): " +
+            mapper.writeValueAsString(responseInfo));
+      } else if (params.length == 1 && instance instanceof HRegionServer &&
+          params[0] instanceof Operation) {
+        // annotate the response map with operation details
+        responseInfo.putAll(((Operation) params[1]).toMap());
+        // report to the log file
+        LOG.warn("(operation" + tag + "): " +
+            mapper.writeValueAsString(responseInfo));
+      } else {
+        // can't get JSON details, so just report call.toString() along with
+        // a more generic tag.
+        responseInfo.put("call", call.toString());
+        LOG.warn("(response" + tag + "): " +
+            mapper.writeValueAsString(responseInfo));
+      }
+    }
   }
 
   protected static void log(String value) {

Modified: hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java?rev=1181948&r1=1181947&r2=1181948&view=diff
==============================================================================
--- hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java (original)
+++ hbase/branches/0.89/src/main/java/org/apache/hadoop/hbase/ipc/HBaseServer.java Tue Oct 11 17:43:41 2011
@@ -87,19 +87,6 @@ public abstract class HBaseServer {
    */
   private static final int MAX_QUEUE_SIZE_PER_HANDLER = 100;
 
-  private static final String WARN_RESPONSE_SIZE =
-    "hbase.ipc.warn.response.size";
-
-  private static final String WARN_RESPONSE_TIME =
-    "hbase.ipc.warn.response.time";
-
-  /** Default value for above params */
-  private static final int DEFAULT_WARN_RESPONSE_SIZE = 100 * 1024 * 1024;
-  private static final int DEFAULT_WARN_RESPONSE_TIME = 1000; // milliseconds
-
-  private final int warnResponseSize;
-  private final int warnResponseTime;
-
   public static final Log LOG =
     LogFactory.getLog("org.apache.hadoop.ipc.HBaseServer");
 
@@ -925,7 +912,6 @@ public abstract class HBaseServer {
           String errorClass = null;
           String error = null;
           Writable value = null;
-          long now = System.currentTimeMillis();
           CurCall.set(call);
           UserGroupInformation previous = UserGroupInformation.getCurrentUGI();
           UserGroupInformation.setCurrentUser(call.connection.ticket);
@@ -968,16 +954,6 @@ public abstract class HBaseServer {
             WritableUtils.writeString(out, errorClass);
             WritableUtils.writeString(out, error);
           }
-          long took = System.currentTimeMillis() - now;
-          if ((buf.size() > warnResponseSize) ||
-              (took > warnResponseTime)) {
-            LOG.warn(getName() + ": "
-                + ((buf.size() > warnResponseSize) ? "(responseTooLarge) " : "")
-                + ((took > warnResponseTime) ? "(responseTooSlow) " : "")
-                + call +
-                ": Size: " + StringUtils.humanReadableInt(buf.size()) +
-                ": Time (ms): " + took);
-          }
 
           call.setResponse(buf.getByteBuffer());
           responder.doRespond(call);
@@ -1042,12 +1018,6 @@ public abstract class HBaseServer {
     this.tcpNoDelay = conf.getBoolean("ipc.server.tcpnodelay", false);
     this.tcpKeepAlive = conf.getBoolean("ipc.server.tcpkeepalive", true);
 
-    this.warnResponseSize = conf.getInt(WARN_RESPONSE_SIZE,
-        DEFAULT_WARN_RESPONSE_SIZE);
-
-    this.warnResponseTime = conf.getInt(WARN_RESPONSE_TIME,
-        DEFAULT_WARN_RESPONSE_TIME);
-
     // Create the responder here
     responder = new Responder();
   }

Added: hbase/branches/0.89/src/test/java/org/apache/hadoop/hbase/client/TestOperation.java
URL: http://svn.apache.org/viewvc/hbase/branches/0.89/src/test/java/org/apache/hadoop/hbase/client/TestOperation.java?rev=1181948&view=auto
==============================================================================
--- hbase/branches/0.89/src/test/java/org/apache/hadoop/hbase/client/TestOperation.java (added)
+++ hbase/branches/0.89/src/test/java/org/apache/hadoop/hbase/client/TestOperation.java Tue Oct 11 17:43:41 2011
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2009 The Apache Software Foundation
+ *
+ * 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.hbase.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.hbase.util.Bytes;
+
+import org.codehaus.jackson.map.ObjectMapper;
+
+/**
+ * Run tests that use the funtionality of the Operation superclass for
+ * Puts, Gets, Deletes, Scans, and MultiPuts.
+ */
+public class TestOperation {
+  private static byte [] ROW = Bytes.toBytes("testRow");
+  private static byte [] FAMILY = Bytes.toBytes("testFamily");
+  private static byte [] QUALIFIER = Bytes.toBytes("testQualifier");
+  private static byte [] VALUE = Bytes.toBytes("testValue");
+
+  private static ObjectMapper mapper = new ObjectMapper();
+
+  /**
+   * Test the client Operations' JSON encoding to ensure that produced JSON is
+   * parseable and that the details are present and not corrupted.
+   * @throws IOException
+   */
+  @Test
+  public void testOperationJSON()
+      throws IOException {
+    // produce a Scan Operation
+    Scan scan = new Scan(ROW);
+    scan.addColumn(FAMILY, QUALIFIER);
+    // get its JSON representation, and parse it
+    String json = scan.toJSON();
+    Map<String, Object> parsedJSON = mapper.readValue(json, HashMap.class);
+    // check for the row
+    assertEquals("startRow incorrect in Scan.toJSON()",
+        Bytes.toStringBinary(ROW), parsedJSON.get("startRow"));
+    // check for the family and the qualifier.
+    List familyInfo = (List) ((Map) parsedJSON.get("families")).get(
+        Bytes.toStringBinary(FAMILY));
+    assertNotNull("Family absent in Scan.toJSON()", familyInfo);
+    assertEquals("Qualifier absent in Scan.toJSON()", 1, familyInfo.size());
+    assertEquals("Qualifier incorrect in Scan.toJSON()",
+        Bytes.toStringBinary(QUALIFIER),
+        familyInfo.get(0));
+
+    // produce a Get Operation
+    Get get = new Get(ROW);
+    get.addColumn(FAMILY, QUALIFIER);
+    // get its JSON representation, and parse it
+    json = get.toJSON();
+    parsedJSON = mapper.readValue(json, HashMap.class);
+    // check for the row
+    assertEquals("row incorrect in Get.toJSON()",
+        Bytes.toStringBinary(ROW), parsedJSON.get("row"));
+    // check for the family and the qualifier.
+    familyInfo = (List) ((Map) parsedJSON.get("families")).get(
+        Bytes.toStringBinary(FAMILY));
+    assertNotNull("Family absent in Get.toJSON()", familyInfo);
+    assertEquals("Qualifier absent in Get.toJSON()", 1, familyInfo.size());
+    assertEquals("Qualifier incorrect in Get.toJSON()",
+        Bytes.toStringBinary(QUALIFIER),
+        familyInfo.get(0));
+
+    // produce a Put operation
+    Put put = new Put(ROW);
+    put.add(FAMILY, QUALIFIER, VALUE);
+    // get its JSON representation, and parse it
+    json = put.toJSON();
+    parsedJSON = mapper.readValue(json, HashMap.class);
+    // check for the row
+    assertEquals("row absent in Put.toJSON()",
+        Bytes.toStringBinary(ROW), parsedJSON.get("row"));
+    // check for the family and the qualifier.
+    familyInfo = (List) ((Map) parsedJSON.get("families")).get(
+        Bytes.toStringBinary(FAMILY));
+    assertNotNull("Family absent in Put.toJSON()", familyInfo);
+    assertEquals("KeyValue absent in Put.toJSON()", 1, familyInfo.size());
+    Map kvMap = (Map) familyInfo.get(0);
+    assertEquals("Qualifier incorrect in Put.toJSON()",
+        Bytes.toStringBinary(QUALIFIER),
+        kvMap.get("qualifier"));
+    assertEquals("Value length incorrect in Put.toJSON()",
+        VALUE.length, kvMap.get("vlen"));
+
+    // produce a Delete operation
+    Delete delete = new Delete(ROW);
+    delete.deleteColumn(FAMILY, QUALIFIER);
+    // get its JSON representation, and parse it
+    json = delete.toJSON();
+    parsedJSON = mapper.readValue(json, HashMap.class);
+    // check for the row
+    assertEquals("row absent in Delete.toJSON()",
+        Bytes.toStringBinary(ROW), parsedJSON.get("row"));
+    // check for the family and the qualifier.
+    familyInfo = (List) ((Map) parsedJSON.get("families")).get(
+        Bytes.toStringBinary(FAMILY));
+    assertNotNull("Family absent in Delete.toJSON()", familyInfo);
+    assertEquals("KeyValue absent in Delete.toJSON()", 1, familyInfo.size());
+    kvMap = (Map) familyInfo.get(0);
+    assertEquals("Qualifier incorrect in Delete.toJSON()",
+        Bytes.toStringBinary(QUALIFIER), kvMap.get("qualifier"));
+  }
+}