HTRACE-303. Add client-side htraceDropped log file to track dropped spans (cmccabe)
diff --git a/htrace-htraced/go/src/org/apache/htrace/common/rpc.go b/htrace-htraced/go/src/org/apache/htrace/common/rpc.go
index 6375688..8028cc6 100644
--- a/htrace-htraced/go/src/org/apache/htrace/common/rpc.go
+++ b/htrace-htraced/go/src/org/apache/htrace/common/rpc.go
@@ -41,7 +41,6 @@
 	Addr          string `json:",omitempty"` // This gets filled in by the RPC layer.
 	DefaultTrid   string `json:",omitempty"`
 	Spans         []*Span
-	ClientDropped uint64 `json:",omitempty"`
 }
 
 // Info returned by /server/version
@@ -97,20 +96,6 @@
 
 	// The total number of spans dropped by the server.
 	ServerDropped uint64
-
-	// The total number of spans dropped by the client.
-	//
-	// This number is just an estimate and may be incorrect for many reasons.
-	// If the client can't contact the server at all, then obviously the server
-	// will never increment ClientDropped... even though spans are being
-	// dropped.  The client may also tell the server about some new spans it
-	// has dropped, but then for some reason fail to get the acknowledgement
-	// from the server.  In that case, the client would re-send its client
-	// dropped estimate and it would be double-counted by the server
-	//
-	// The intention here is to provide a rough estimate of how overloaded
-	// htraced clients are, not to provide strongly consistent numbers.
-	ClientDroppedEstimate uint64
 }
 
 // A map from network address strings to SpanMetrics structures.
@@ -145,10 +130,6 @@
 	// The total number of spans dropped by the server since the server started.
 	ServerDroppedSpans uint64
 
-	// An estimate of the total number of spans dropped by the server since the server started.
-	// See SpanMetrics#ClientDroppedEstimate
-	ClientDroppedEstimate uint64
-
 	// The maximum latency of a writeSpans request, in milliseconds.
 	MaxWriteSpansLatencyMs uint32
 
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/datastore.go b/htrace-htraced/go/src/org/apache/htrace/htraced/datastore.go
index 9310e6e..1dab5c8 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/datastore.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/datastore.go
@@ -792,7 +792,7 @@
 	}
 }
 
-func (ing *SpanIngestor) Close(clientDropped int, startTime time.Time) {
+func (ing *SpanIngestor) Close(startTime time.Time) {
 	for shardIdx := range(ing.batches) {
 		batch := ing.batches[shardIdx]
 		if len(batch.incoming) > 0 {
@@ -809,7 +809,7 @@
 
 	endTime := time.Now()
 	ing.store.msink.UpdateIngested(ing.addr, ing.totalIngested,
-		ing.serverDropped, clientDropped, endTime.Sub(startTime))
+		ing.serverDropped, endTime.Sub(startTime))
 }
 
 func (store *dataStore) WriteSpans(shardIdx int, ispans []*IncomingSpan) {
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/datastore_test.go b/htrace-htraced/go/src/org/apache/htrace/htraced/datastore_test.go
index e6d1df7..d38c1b0 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/datastore_test.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/datastore_test.go
@@ -77,7 +77,7 @@
 	for idx := range spans {
 		ing.IngestSpan(&spans[idx])
 	}
-	ing.Close(0, time.Now())
+	ing.Close(time.Now())
 	store.WrittenSpans.Waits(int64(len(spans)))
 }
 
@@ -364,7 +364,7 @@
 	for n := 0; n < b.N; n++ {
 		ing.IngestSpan(allSpans[n])
 	}
-	ing.Close(0, time.Now())
+	ing.Close(time.Now())
 	// Wait for all the spans to be written.
 	ht.Store.WrittenSpans.Waits(int64(b.N))
 	assertNumWrittenEquals(b, ht.Store.msink, b.N)
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/hrpc.go b/htrace-htraced/go/src/org/apache/htrace/htraced/hrpc.go
index a649420..0d569a0 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/hrpc.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/hrpc.go
@@ -270,7 +270,7 @@
 	for spanIdx := range req.Spans {
 		ing.IngestSpan(req.Spans[spanIdx])
 	}
-	ing.Close(int(req.ClientDropped), startTime)
+	ing.Close(startTime)
 	return nil
 }
 
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/metrics.go b/htrace-htraced/go/src/org/apache/htrace/htraced/metrics.go
index 5ce3339..7bf42fd 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/metrics.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/metrics.go
@@ -55,9 +55,6 @@
 	// The total number of spans dropped by the server.
 	ServerDropped uint64
 
-	// The total number of spans dropped by the client (self-reported).
-	ClientDroppedEstimate uint64
-
 	// Per-host Span Metrics
 	HostSpanMetrics common.SpanMetricsMap
 
@@ -80,13 +77,12 @@
 // Update the total number of spans which were ingested, as well as other
 // metrics that get updated during span ingest. 
 func (msink *MetricsSink) UpdateIngested(addr string, totalIngested int,
-		serverDropped int, clientDroppedEstimate int, wsLatency time.Duration) {
+		serverDropped int, wsLatency time.Duration) {
 	msink.lock.Lock()
 	defer msink.lock.Unlock()
 	msink.IngestedSpans += uint64(totalIngested)
 	msink.ServerDropped += uint64(serverDropped)
-	msink.ClientDroppedEstimate += uint64(clientDroppedEstimate)
-	msink.updateSpanMetrics(addr, 0, serverDropped, clientDroppedEstimate)
+	msink.updateSpanMetrics(addr, 0, serverDropped)
 	wsLatencyMs := wsLatency.Nanoseconds() / 1000000
 	var wsLatency32 uint32
 	if wsLatencyMs > math.MaxUint32 {
@@ -99,7 +95,7 @@
 
 // Update the per-host span metrics.  Must be called with the lock held.
 func (msink *MetricsSink) updateSpanMetrics(addr string, numWritten int,
-		serverDropped int, clientDroppedEstimate int) {
+		serverDropped int) {
 	mtx, found := msink.HostSpanMetrics[addr]
 	if !found {
 		// Ensure that the per-host span metrics map doesn't grow too large.
@@ -117,7 +113,6 @@
 	}
 	mtx.Written += uint64(numWritten)
 	mtx.ServerDropped += uint64(serverDropped)
-	mtx.ClientDroppedEstimate += uint64(clientDroppedEstimate)
 }
 
 // Update the total number of spans which were persisted to disk.
@@ -127,7 +122,7 @@
 	defer msink.lock.Unlock()
 	msink.WrittenSpans += uint64(totalWritten)
 	msink.ServerDropped += uint64(serverDropped)
-	msink.updateSpanMetrics(addr, totalWritten, serverDropped, 0)
+	msink.updateSpanMetrics(addr, totalWritten, serverDropped)
 }
 
 // Read the server stats.
@@ -137,7 +132,6 @@
 	stats.IngestedSpans = msink.IngestedSpans
 	stats.WrittenSpans = msink.WrittenSpans
 	stats.ServerDroppedSpans = msink.ServerDropped
-	stats.ClientDroppedEstimate = msink.ClientDroppedEstimate
 	stats.MaxWriteSpansLatencyMs = msink.wsLatencyCircBuf.Max()
 	stats.AverageWriteSpansLatencyMs = msink.wsLatencyCircBuf.Average()
 	stats.HostSpanMetrics = make(common.SpanMetricsMap)
@@ -145,7 +139,6 @@
 		stats.HostSpanMetrics[k] = &common.SpanMetrics {
 			Written: v.Written,
 			ServerDropped: v.ServerDropped,
-			ClientDroppedEstimate: v.ClientDroppedEstimate,
 		}
 	}
 }
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/reaper_test.go b/htrace-htraced/go/src/org/apache/htrace/htraced/reaper_test.go
index 0140dbb..b354a2c 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/reaper_test.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/reaper_test.go
@@ -56,7 +56,7 @@
 	for spanIdx := range testSpans {
 		ing.IngestSpan(testSpans[spanIdx])
 	}
-	ing.Close(0, time.Now())
+	ing.Close(time.Now())
 	// Wait the spans to be created
 	ht.Store.WrittenSpans.Waits(NUM_TEST_SPANS)
 	// Set a reaper date that will remove all the spans except final one.
diff --git a/htrace-htraced/go/src/org/apache/htrace/htraced/rest.go b/htrace-htraced/go/src/org/apache/htrace/htraced/rest.go
index 1b90bd4..c327cdd 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htraced/rest.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htraced/rest.go
@@ -257,7 +257,7 @@
 	for spanIdx := range msg.Spans {
 		ing.IngestSpan(msg.Spans[spanIdx])
 	}
-	ing.Close(int(msg.ClientDropped), startTime)
+	ing.Close(startTime)
 }
 
 type queryHandler struct {
diff --git a/htrace-htraced/go/src/org/apache/htrace/htracedTool/cmd.go b/htrace-htraced/go/src/org/apache/htrace/htracedTool/cmd.go
index c81bbb7..9837e94 100644
--- a/htrace-htraced/go/src/org/apache/htrace/htracedTool/cmd.go
+++ b/htrace-htraced/go/src/org/apache/htrace/htracedTool/cmd.go
@@ -218,8 +218,6 @@
 	fmt.Fprintf(w, "Spans ingested\t%d\n", stats.IngestedSpans)
 	fmt.Fprintf(w, "Spans written\t%d\n", stats.WrittenSpans)
 	fmt.Fprintf(w, "Spans dropped by server\t%d\n", stats.ServerDroppedSpans)
-	fmt.Fprintf(w, "Estimated spans dropped by clients\t%d\n",
-		stats.ClientDroppedEstimate)
 	dur := time.Millisecond * time.Duration(stats.AverageWriteSpansLatencyMs)
 	fmt.Fprintf(w, "Average WriteSpan Latency\t%s\n", dur.String())
 	dur = time.Millisecond * time.Duration(stats.MaxWriteSpansLatencyMs)
@@ -247,8 +245,8 @@
 	sort.Sort(keys)
 	for k := range keys {
 		mtx := mtxMap[keys[k]]
-		fmt.Fprintf(w, "%s\twritten: %d\tserver dropped: %d\tclient dropped estimate: %d\n",
-			keys[k], mtx.Written, mtx.ServerDropped, mtx.ClientDroppedEstimate)
+		fmt.Fprintf(w, "%s\twritten: %d\tserver dropped: %d\n",
+			keys[k], mtx.Written, mtx.ServerDropped)
 	}
 	w.Flush()
 	return EXIT_SUCCESS
diff --git a/htrace-htraced/src/main/java/org/apache/htrace/impl/Conf.java b/htrace-htraced/src/main/java/org/apache/htrace/impl/Conf.java
index cdd176f..3206dd6 100644
--- a/htrace-htraced/src/main/java/org/apache/htrace/impl/Conf.java
+++ b/htrace-htraced/src/main/java/org/apache/htrace/impl/Conf.java
@@ -18,6 +18,7 @@
 package org.apache.htrace.impl;
 
 import java.io.IOException;
+import java.io.File;
 import java.net.InetSocketAddress;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
@@ -144,6 +145,18 @@
       "htraced.error.log.period.ms";
   final static long ERROR_LOG_PERIOD_MS_DEFAULT = 30000L;
 
+  final static String DROPPED_SPANS_LOG_PATH_KEY =
+      "htraced.dropped.spans.log.path";
+
+  final static String DROPPED_SPANS_LOG_PATH_DEFAULT =
+      new File(System.getProperty("java.io.tmpdir", "/tmp"), "htraceDropped").
+        getAbsolutePath();
+
+  final static String DROPPED_SPANS_LOG_MAX_SIZE_KEY =
+      "htraced.dropped.spans.log.max.size";
+
+  final static long DROPPED_SPANS_LOG_MAX_SIZE_DEFAULT = 1024L * 1024L;
+
   @JsonProperty("ioTimeoutMs")
   final int ioTimeoutMs;
 
@@ -180,6 +193,12 @@
   @JsonProperty("endpoint")
   final InetSocketAddress endpoint;
 
+  @JsonProperty("droppedSpansLogPath")
+  final String droppedSpansLogPath;
+
+  @JsonProperty("droppedSpansLogMaxSize")
+  final long droppedSpansLogMaxSize;
+
   private static int getBoundedInt(final HTraceConfiguration conf,
         String key, int defaultValue, int minValue, int maxValue) {
     int val = conf.getInt(key, defaultValue);
@@ -341,6 +360,11 @@
       throw new IOException("Error reading " + ADDRESS_KEY + ": " +
           e.getMessage());
     }
+    this.droppedSpansLogPath = conf.get(
+        DROPPED_SPANS_LOG_PATH_KEY, DROPPED_SPANS_LOG_PATH_DEFAULT);
+    this.droppedSpansLogMaxSize = getBoundedLong(conf,
+        DROPPED_SPANS_LOG_MAX_SIZE_KEY, DROPPED_SPANS_LOG_MAX_SIZE_DEFAULT,
+        0, Long.MAX_VALUE);
   }
 
   @Override
diff --git a/htrace-htraced/src/main/java/org/apache/htrace/impl/HTracedSpanReceiver.java b/htrace-htraced/src/main/java/org/apache/htrace/impl/HTracedSpanReceiver.java
index f5f493c..22b64f6 100644
--- a/htrace-htraced/src/main/java/org/apache/htrace/impl/HTracedSpanReceiver.java
+++ b/htrace-htraced/src/main/java/org/apache/htrace/impl/HTracedSpanReceiver.java
@@ -17,7 +17,20 @@
  */
 package org.apache.htrace.impl;
 
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.WRITE;
+
 import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+//import java.nio.file.attribute.FileAttribute;
+//import java.util.EnumSet;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.ReentrantLock;
@@ -75,6 +88,8 @@
 
   private long lastBufferClearedTimeMs = 0;
 
+  private long unbufferableSpans = 0;
+
   static class FaultInjector {
     static FaultInjector NO_OP = new FaultInjector();
     public void handleContentLengthTrigger(int len) { }
@@ -140,10 +155,12 @@
         }
         long deltaMs = TimeUtil.deltaMs(startTimeMs, TimeUtil.nowMs());
         if (deltaMs > conf.spanDropTimeoutMs) {
+          StringBuilder bld = new StringBuilder();
           spanDropLog.error("Dropping a span after unsuccessfully " +
               "attempting to add it for " + deltaMs + " ms.  There is not " +
               "enough buffer space. Please increase " + Conf.BUFFER_SIZE_KEY +
               " or decrease the rate of spans being generated.");
+          unbufferableSpans++;
           return;
         } else if (LOG.isDebugEnabled()) {
           LOG.debug("Unable to write span to buffer #" + activeBuf +
@@ -296,6 +313,16 @@
         return;
       }
       int flushTries = 0;
+      if (unbufferableSpans > 0) {
+        try {
+          appendToDroppedSpansLog("Dropped " + unbufferableSpans +
+              " spans because of lack of local buffer space.\n");
+        } catch (IOException e) {
+          // Ignore.  We already logged a message about the dropped spans
+          // earlier.
+        }
+        unbufferableSpans = 0;
+      }
       while (true) {
         Throwable exc;
         try {
@@ -311,17 +338,22 @@
           return;
         }
         int numSpans = flushBufManager.getNumberOfSpans();
-        String excMessage = "Failed to flush " + numSpans  + " htrace " +
-            "spans to " + conf.endpointStr + " on try " + (flushTries + 1);
+        flushErrorLog.error("Failed to flush " + numSpans  + " htrace " +
+            "spans to " + conf.endpointStr + " on try " + (flushTries + 1),
+            exc);
         if (flushTries >= conf.flushRetryDelays.length) {
-          excMessage += ".  Discarding all spans.";
-        }
-        if (LOG.isDebugEnabled()) {
-          LOG.error(excMessage, exc);
-        } else {
-          flushErrorLog.error(excMessage, exc);
-        }
-        if (flushTries >= conf.flushRetryDelays.length) {
+          StringBuilder bld = new StringBuilder();
+          bld.append("Failed to flush ").append(numSpans).
+            append(" spans to htraced at").append(conf.endpointStr).
+            append(" after ").append(flushTries).append(" tries: ").
+            append(exc.getMessage());
+          try {
+            appendToDroppedSpansLog(bld.toString());
+          } catch (IOException e) {
+            bld.append(".  Failed to write to dropped spans log: ").
+              append(e.getMessage());
+          }
+          spanDropLog.error(bld.toString());
           return;
         }
         int delayMs = conf.flushRetryDelays[flushTries];
@@ -330,4 +362,43 @@
       }
     }
   }
+
+  void appendToDroppedSpansLog(String text) throws IOException {
+    // Is the dropped spans log is disabled?
+    if (conf.droppedSpansLogPath.isEmpty() ||
+        (conf.droppedSpansLogMaxSize == 0)) {
+      return;
+    }
+    FileLock lock = null;
+    ByteBuffer bb = ByteBuffer.wrap(
+        text.getBytes(StandardCharsets.UTF_8));
+    // FileChannel locking corresponds to advisory locking on UNIX.  It will
+    // protect multiple processes from attempting to write to the same dropped
+    // spans log at once.  However, within a single process, we need this
+    // synchronized block to ensure that multiple HTracedSpanReceiver objects
+    // don't try to write to the same log at once.  (It is unusal to configure
+    // multiple HTracedSpanReceiver objects, but possible.)
+    synchronized(HTracedSpanReceiver.class) {
+      FileChannel channel = FileChannel.open(
+          Paths.get(conf.droppedSpansLogPath), APPEND, CREATE, WRITE);
+      try {
+        lock = channel.lock();
+        long size = channel.size();
+        if (size > conf.droppedSpansLogMaxSize) {
+          throw new IOException("Dropped spans log " +
+              conf.droppedSpansLogPath + " is already " + size +
+              " bytes; will not add to it.");
+        }
+        channel.write(bb);
+      } finally {
+        try {
+          if (lock != null) {
+            lock.release();
+          }
+        } finally {
+          channel.close();
+        }
+      }
+    }
+  }
 }
diff --git a/htrace-htraced/src/test/java/org/apache/htrace/impl/TestDroppedSpans.java b/htrace-htraced/src/test/java/org/apache/htrace/impl/TestDroppedSpans.java
new file mode 100644
index 0000000..8947365
--- /dev/null
+++ b/htrace-htraced/src/test/java/org/apache/htrace/impl/TestDroppedSpans.java
@@ -0,0 +1,173 @@
+/**
+ * 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.htrace.impl;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.htrace.core.HTraceConfiguration;
+import org.apache.htrace.core.MilliSpan;
+import org.apache.htrace.core.TracerId;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestDroppedSpans {
+  private static final Log LOG = LogFactory.getLog(TestDroppedSpans.class);
+
+  private static Path tempDir;
+
+  @BeforeClass
+  public static void beforeClass() throws IOException {
+    // Allow setting really small buffer sizes for testing purposes.
+    // We do not allow setting such small sizes in production.
+    Conf.BUFFER_SIZE_MIN = 0;
+
+    // Create a temporary directory to hold the dropped spans logs.
+    String tmp = System.getProperty("java.io.tmpdir", "/tmp");
+    File dir = new File(tmp,
+        "TestDroppedSpans." + UUID.randomUUID().toString());
+    Files.createDirectory(dir.toPath());
+    tempDir = dir.toPath();
+  }
+
+  @BeforeClass
+  public static void afterClass() throws IOException {
+    if (tempDir != null) {
+      try (DirectoryStream<Path> stream = Files.newDirectoryStream(tempDir)) {
+        for (final Iterator<Path> it = stream.iterator(); it.hasNext();) {
+          Files.delete(it.next());
+        }
+      }
+      Files.delete(tempDir);
+      tempDir = null;
+    }
+  }
+
+  /**
+   * Test that we can disable the dropped spans log.
+   */
+  @Test(timeout = 60000)
+  public void testDisableDroppedSpansLog() throws Exception {
+    HTraceConfiguration conf = HTraceConfiguration.fromMap(
+        new HashMap<String, String>() {{
+          put(Conf.ADDRESS_KEY, "127.0.0.1:8080");
+          put(TracerId.TRACER_ID_KEY, "testAppendToDroppedSpansLog");
+          put(Conf.DROPPED_SPANS_LOG_PATH_KEY, "/");
+          put(Conf.DROPPED_SPANS_LOG_MAX_SIZE_KEY, "0");
+        }});
+    HTracedSpanReceiver rcvr = new HTracedSpanReceiver(conf);
+    try {
+      rcvr.appendToDroppedSpansLog("this won't get written");
+    } finally {
+      rcvr.close();
+    }
+  }
+
+  /**
+   * Test that we can write to the dropped spans log.
+   */
+  @Test(timeout = 60000)
+  public void testWriteToDroppedSpansLog() throws Exception {
+    final String logPath = new File(
+        tempDir.toFile(), "testWriteToDroppedSpansLog").getAbsolutePath();
+    HTraceConfiguration conf = HTraceConfiguration.fromMap(
+        new HashMap<String, String>() {{
+          put(Conf.ADDRESS_KEY, "127.0.0.1:8080");
+          put(TracerId.TRACER_ID_KEY, "testWriteToDroppedSpansLog");
+          put(Conf.DROPPED_SPANS_LOG_PATH_KEY, logPath);
+          put(Conf.DROPPED_SPANS_LOG_MAX_SIZE_KEY, "78");
+        }});
+    HTracedSpanReceiver rcvr = new HTracedSpanReceiver(conf);
+    try {
+      final String LINE1 = "This is a test of the dropped spans log.";
+      rcvr.appendToDroppedSpansLog(LINE1 + "\n");
+      final String LINE2 = "These lines should appear in the log.";
+      rcvr.appendToDroppedSpansLog(LINE2 + "\n");
+      try {
+        rcvr.appendToDroppedSpansLog("This line won't be written because we're " +
+            "out of space.");
+        Assert.fail("expected append to fail because of lack of space");
+      } catch (IOException e) {
+        // ignore
+      }
+      List<String> lines =
+          Files.readAllLines(Paths.get(logPath), StandardCharsets.UTF_8);
+      Assert.assertEquals(2, lines.size());
+      Assert.assertEquals(LINE1, lines.get(0));
+      Assert.assertEquals(LINE2, lines.get(1));
+    } finally {
+      rcvr.close();
+    }
+  }
+
+  /**
+   * Test that we write to the dropped spans log when htraced is unreachable.
+   */
+  @Test(timeout = 60000)
+  public void testSpansDroppedBecauseOfUnreachableHTraced() throws Exception {
+    final String logPath = new File(tempDir.toFile(),
+        "testSpansDroppedBecauseOfUnreachableHTraced").getAbsolutePath();
+    // Open a local socket.  We know that nobody is listening on this socket, so
+    // all attempts to send to it will fail.
+    final ServerSocket serverSocket = new ServerSocket(0);
+    HTracedSpanReceiver rcvr = null;
+    try {
+      HTraceConfiguration conf = HTraceConfiguration.fromMap(
+          new HashMap<String, String>() {{
+            put(Conf.ADDRESS_KEY, "127.0.0.1:" + serverSocket.getLocalPort());
+            put(TracerId.TRACER_ID_KEY,
+                "testSpansDroppedBecauseOfUnreachableHTraced");
+            put(Conf.DROPPED_SPANS_LOG_PATH_KEY, logPath);
+            put(Conf.DROPPED_SPANS_LOG_MAX_SIZE_KEY, "78");
+            put(Conf.CONNECT_TIMEOUT_MS_KEY, "1");
+            put(Conf.IO_TIMEOUT_MS_KEY, "1");
+            put(Conf.FLUSH_RETRY_DELAYS_KEY, "1,1");
+          }});
+      rcvr = new HTracedSpanReceiver(conf);
+      rcvr.receiveSpan(new MilliSpan.Builder().
+          begin(123).end(456).description("FooBar").build());
+      HTracedSpanReceiver tmpRcvr = rcvr;
+      rcvr = null;
+      tmpRcvr.close();
+      List<String> lines =
+          Files.readAllLines(Paths.get(logPath), StandardCharsets.UTF_8);
+      Assert.assertTrue(lines.size() >= 1);
+      Assert.assertTrue(lines.get(0).contains("Failed to flush "));
+    } finally {
+      serverSocket.close();
+      if (rcvr != null) {
+        rcvr.close();
+      }
+    }
+  }
+}
diff --git a/htrace-webapp/src/main/webapp/app/server_info_view.js b/htrace-webapp/src/main/webapp/app/server_info_view.js
index efb7545..43533d4 100644
--- a/htrace-webapp/src/main/webapp/app/server_info_view.js
+++ b/htrace-webapp/src/main/webapp/app/server_info_view.js
@@ -50,7 +50,6 @@
             '<th>Remote</th>' +
             '<th>Written</th>' +
             '<th>ServerDropped</th>' +
-            '<th>ClientDroppedEstimate</th>' +
           '</tr>' +
         '</thead>';
     var remotes = [];
@@ -69,7 +68,6 @@
         "<td>" + remote + "</td>" +
         "<td>" + smtx.Written + "</td>" +
         "<td>" + smtx.ServerDropped + "</td>" +
-        "<td>" + smtx.ClientDroppedEstimate + "</td>" +
         "</tr>";
     }
     out = out + '</table>';
diff --git a/htrace-webapp/src/main/webapp/app/server_stats.js b/htrace-webapp/src/main/webapp/app/server_stats.js
index 4cfea92..65d8c60 100644
--- a/htrace-webapp/src/main/webapp/app/server_stats.js
+++ b/htrace-webapp/src/main/webapp/app/server_stats.js
@@ -26,7 +26,6 @@
     "IngestedSpans": "(unknown)",
     "WrittenSpans": "(unknown)",
     "ServerDroppedSpans": "(unknown)",
-    "ClientDroppedSpans": "(unknown)",
     "MaxWriteSpansLatencyMs": "(unknown)",
     "AverageWriteSpansLatencyMs": "(unknown)"
   },
diff --git a/htrace-webapp/src/main/webapp/index.html b/htrace-webapp/src/main/webapp/index.html
index 1e20ec0..edbfd26 100644
--- a/htrace-webapp/src/main/webapp/index.html
+++ b/htrace-webapp/src/main/webapp/index.html
@@ -92,10 +92,6 @@
               <td><%= model.stats.get("ServerDroppedSpans") %></td>
             </tr>
             <tr>
-              <td>Estimated Client Dropped Spans</td>
-              <td><%= model.stats.get("ClientDroppedEstimate") %></td>
-            </tr>
-            <tr>
               <td>Maximum WriteSpans Latency (ms)</td>
               <td><%= model.stats.get("MaxWriteSpansLatencyMs") %></td>
             </tr>