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);