diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java
index 2d1da32849..0e8e8b30f7 100644
--- a/src/tsd/GraphHandler.java
+++ b/src/tsd/GraphHandler.java
@@ -584,9 +584,9 @@ static void setPlotParams(final HttpQuery query, final Plot plot) {
* graph from the file it produces, or if we have been interrupted.
* @throws GnuplotException if Gnuplot returns non-zero.
*/
- private static int runGnuplot(final HttpQuery query,
- final String basepath,
- final Plot plot) throws IOException {
+ static int runGnuplot(final HttpQuery query,
+ final String basepath,
+ final Plot plot) throws IOException {
final int nplotted = plot.dumpToFiles(basepath);
final long start_time = System.nanoTime();
final Process gnuplot = new ProcessBuilder(
diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java
index 3aa1cbfdd3..0186453b2d 100644
--- a/src/tsd/HttpQuery.java
+++ b/src/tsd/HttpQuery.java
@@ -16,6 +16,7 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -41,6 +42,7 @@
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.jboss.netty.util.CharsetUtil;
+import net.opentsdb.graph.Plot;
import net.opentsdb.stats.Histogram;
import net.opentsdb.stats.StatsCollector;
@@ -202,6 +204,8 @@ public void internalError(final Exception cause) {
HttpQuery.escapeJson(pretty_exc, buf);
buf.append("\"}");
sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf);
+ } else if (hasQueryStringParam("png")) {
+ sendAsPNG(HttpResponseStatus.INTERNAL_SERVER_ERROR, pretty_exc, 30);
} else {
sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR,
makePage("Internal Server Error", "Houston, we have a problem",
@@ -227,6 +231,8 @@ public void badRequest(final String explain) {
HttpQuery.escapeJson(explain, buf);
buf.append("\"}");
sendReply(HttpResponseStatus.BAD_REQUEST, buf);
+ } else if (hasQueryStringParam("png")) {
+ sendAsPNG(HttpResponseStatus.BAD_REQUEST, explain, 3600);
} else {
sendReply(HttpResponseStatus.BAD_REQUEST,
makePage("Bad Request", "Looks like it's your fault this time",
@@ -247,6 +253,8 @@ public void notFound() {
if (hasQueryStringParam("json")) {
sendReply(HttpResponseStatus.NOT_FOUND,
new StringBuilder("{\"err\":\"Page Not Found\"}"));
+ } else if (hasQueryStringParam("png")) {
+ sendAsPNG(HttpResponseStatus.NOT_FOUND, "Page Not Found", 3600);
} else {
sendReply(HttpResponseStatus.NOT_FOUND, PAGE_NOT_FOUND);
}
@@ -399,17 +407,75 @@ public void sendReply(final HttpResponseStatus status,
}
/**
- * Send a file (with zero-copy) to the client.
+ * Sends the given message as a PNG image.
+ * This method will block while image is being generated.
+ * It's only recommended for cases where we want to report an error back to
+ * the user and the user's browser expects a PNG image. Don't abuse it.
+ * @param status The status of the request (e.g. 200 OK or 404 Not Found).
+ * @param msg The message to send as an image.
+ * @param max_age The expiration time of this entity, in seconds. This is
+ * not a timestamp, it's how old the resource is allowed to be in the client
+ * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable
+ * caching.
+ */
+ public void sendAsPNG(final HttpResponseStatus status,
+ final String msg,
+ final int max_age) {
+ try {
+ final long now = System.currentTimeMillis() / 1000;
+ Plot plot = new Plot(now - 1, now);
+ HashMap params = new HashMap(1);
+ StringBuilder buf = new StringBuilder(1 + msg.length() + 18);
+
+ buf.append('"');
+ escapeJson(msg, buf);
+ buf.append("\" at graph 0.02,0.97");
+ params.put("label", buf.toString());
+ buf = null;
+ plot.setParams(params);
+ params = null;
+ final String basepath =
+ RpcHandler.getDirectoryFromSystemProp("tsd.http.cachedir")
+ + Integer.toHexString(msg.hashCode());
+ GraphHandler.runGnuplot(this, basepath, plot);
+ plot = null;
+ sendFile(status, basepath + ".png", max_age);
+ } catch (Exception e) {
+ getQueryString().remove("png"); // Avoid recursion.
+ internalError(new RuntimeException("Failed to generate a PNG with the"
+ + " following message: " + msg, e));
+ }
+ }
+
+ /**
+ * Send a file (with zero-copy) to the client with a 200 OK status.
* This method doesn't provide any security guarantee. The caller is
* responsible for the argument they pass in.
* @param path The path to the file to send to the client.
* @param max_age The expiration time of this entity, in seconds. This is
* not a timestamp, it's how old the resource is allowed to be in the client
- * cache. See rfc2616 section 14.9 for more information. Use 0 to disable
+ * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable
* caching.
*/
public void sendFile(final String path,
final int max_age) throws IOException {
+ sendFile(HttpResponseStatus.OK, path, max_age);
+ }
+
+ /**
+ * Send a file (with zero-copy) to the client.
+ * This method doesn't provide any security guarantee. The caller is
+ * responsible for the argument they pass in.
+ * @param status The status of the request (e.g. 200 OK or 404 Not Found).
+ * @param path The path to the file to send to the client.
+ * @param max_age The expiration time of this entity, in seconds. This is
+ * not a timestamp, it's how old the resource is allowed to be in the client
+ * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable
+ * caching.
+ */
+ public void sendFile(final HttpResponseStatus status,
+ final String path,
+ final int max_age) throws IOException {
if (max_age < 0) {
throw new IllegalArgumentException("Negative max_age=" + max_age
+ " for path=" + path);
@@ -419,13 +485,16 @@ public void sendFile(final String path,
file = new RandomAccessFile(path, "r");
} catch (FileNotFoundException e) {
logWarn("File not found: " + e.getMessage());
+ if (querystring != null) {
+ querystring.remove("png"); // Avoid potential recursion.
+ }
notFound();
return;
}
final long length = file.length();
{
final DefaultHttpResponse response =
- new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+ new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
final String mimetype = guessMimeTypeFromUri(path);
response.setHeader(HttpHeaders.Names.CONTENT_TYPE,
mimetype == null ? "text/plain" : mimetype);