// This file is part of OpenTSDB.
// Copyright (C) 2013 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import com.stumbleupon.async.Deferred;
import net.opentsdb.core.DataPoints;
import net.opentsdb.core.IncomingDataPoint;
import net.opentsdb.core.TSDB;
import net.opentsdb.core.TSQuery;
import net.opentsdb.meta.Annotation;
import net.opentsdb.meta.TSMeta;
import net.opentsdb.meta.UIDMeta;
import net.opentsdb.search.SearchQuery;
import net.opentsdb.tree.Branch;
import net.opentsdb.tree.Tree;
import net.opentsdb.tree.TreeRule;
import net.opentsdb.utils.Config;
/**
* Abstract base class for Serializers; plugins that handle converting requests
* and responses between OpenTSDB's internal data and various popular formats
* such as JSON, XML, OData, etc. They can also be used to accept inputs from
* existing collection systems such as CollectD.
* <p>
* The serializer workflow is as follows:
* <ul><li>Request comes in via the HTTP API</li>
* <li>The proper serializer is instantiated via:
* <ul><li>Query string parameter "serializer=<shortName>"</li>
* <li>If no query string parameter is found, the Content-Type is parsed</li>
* <li>Otherwise the default serializer is used</li></ul></li>
* <li>The request is routed to an RPC handler</li>
* <li>If the handler needs details for a complex request, it calls on the
* proper serializer's "parseX" method to get a query object</li>
* <li>The RPC handler fetches and organizes the data</li>
* <li>The handler passes the data to the proper serializer's "formatX"
* method</li>
* <li>The serializer formats the data and sends it back as a byte array</li>
* </ul>
* <b>Warning:</b> Every HTTP request will instantiate a new serializer object
* (except for a few that don't require it) so please avoid creating heavy
* objects in the constructor, parse or format methods. Instead, use the
* {@link #initialize} method to instantiate thread-safe, static objects that
* you need for de/serializtion. It will be called once on TSD startup.
* <p>
* <b>Note:</b> If a method needs to throw an exception due to user error, such
* as missing data or a bad request, throw a {@link BadRequestException} with
* a status code, error message and optional details.
* <p>
* Runtime exceptions, anything that goes wrong internally with your serializer,
* will be returned with a 500 Internal Server Error status.
* <p>
* <b>Note:</b> You can change the HTTP status code before returning from a
* "formatX" method by accessing "this.query.response().setStatus()" and
* providing an {@link HttpResponseStatus} object.
* <p>
* <b>Note:</b> You can also set response headers via
* "this.query.response().headers().set()". The "Content-Type" header will be set
* automatically with the "response_content_type" field value that can be
* overridden by the plugin. HttpQuery will also set some other headers before
* returning
* @since 2.0
*/
public abstract class HttpSerializer {
/** Content type to use for matching a serializer to incoming requests */
protected String request_content_type = "application/json";
/** Content type to return with data from this serializer */
protected String response_content_type = "application/json; charset=UTF-8";
/** The query used for accessing the DefaultHttpResponse object and other
* information */
protected final HttpQuery query;
/**
* Empty constructor required for plugin operation
*/
public HttpSerializer() {
this(null);
}
/**
* Constructor that serializers must implement. This is how each plugin will
* get the request content and have the option to set headers or a custom
* status code in the response.
* <p>
* <b>Note:</b> A new serializer is instantiated for every HTTP connection, so
* don't do any heavy object creation here. Instead, use the
* {@link #initialize} method to setup static, thread-safe objects if you
* need stuff like that
* @param query
*/
public HttpSerializer(final HttpQuery query) {
this.query = query;
}
/**
* Initializer called one time when the TSD starts up and loads serializer
* plugins. You should use this method to setup static, thread-safe objects
* required for parsing or formatting data.
* @param tsdb The TSD this plugin belongs to. Use it to fetch config data
* if require.
*/
public abstract void initialize(final TSDB tsdb);
/**
* Called when the TSD is shutting down so implementations can gracefully
* close their objects or connections if necessary
* @return An object, usually a Boolean, used to wait on during shutdown
*/
public abstract Deferred<Object> shutdown();
/**
* The version of this serializer plugin in the format "MAJOR.MINOR.MAINT"
* The MAJOR version should match the major version of OpenTSDB, e.g. if the
* plugin is associated with 2.0.1, your version should be 2.x.x.
* @return the version as a String
*/
public abstract String version();
/**
* The simple name for this serializer referenced by users.
* The name should be lower case, all one word without any odd characters
* so it can be used in a query string. E.g. "json" or "xml" or "odata"
* @return the name of the serializer
*/
public abstract String shortName();
/** @return the incoming content type */
public String requestContentType() {
return this.request_content_type;
}
/** @return the outgoing content type */
public String responseContentType() {
return this.response_content_type;
}
/**
* Parses one or more data points for storage
* @return an array of data points to process for storage
* @throws BadRequestException if the plugin has not implemented this method
*/
public List<IncomingDataPoint> parsePutV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parsePutV1");
}
/**
* Parses a suggestion query
* @return a hash map of key/value pairs
* @throws BadRequestException if the plugin has not implemented this method
*/
public HashMap<String, String> parseSuggestV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseSuggestV1");
}
/**
* Parses a list of metrics, tagk and/or tagvs to assign UIDs to
* @return as hash map of lists for the different types
* @throws BadRequestException if the plugin has not implemented this method
*/
public HashMap<String, List<String>> parseUidAssignV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseUidAssignV1");
}
/**
* Parses a SearchQuery request
* @return The parsed search query
* @throws BadRequestException if the plugin has not implemented this method
*/
public SearchQuery parseSearchQueryV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseSearchQueryV1");
}
/**
* Parses a timeseries data query
* @return A TSQuery with data ready to validate
* @throws BadRequestException if the plugin has not implemented this method
*/
public TSQuery parseQueryV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseQueryV1");
}
/**
* Parses a single UIDMeta object
* @return the parsed meta data object
* @throws BadRequestException if the plugin has not implemented this method
*/
public UIDMeta parseUidMetaV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseUidMetaV1");
}
/**
* Parses a single TSMeta object
* @return the parsed meta data object
* @throws BadRequestException if the plugin has not implemented this method
*/
public TSMeta parseTSMetaV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseTSMetaV1");
}
/**
* Parses a single Tree object
* @return the parsed tree object
* @throws BadRequestException if the plugin has not implemented this method
*/
public Tree parseTreeV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseTreeV1");
}
/**
* Parses a single TreeRule object
* @return the parsed rule object
* @throws BadRequestException if the plugin has not implemented this method
*/
public TreeRule parseTreeRuleV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseTreeRuleV1");
}
/**
* Parses one or more tree rules
* @return A list of one or more rules
* @throws BadRequestException if the plugin has not implemented this method
*/
public List<TreeRule> parseTreeRulesV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseTreeRulesV1");
}
/**
* Parses a tree ID and optional list of TSUIDs to search for collisions or
* not matched TSUIDs.
* @return A map with "treeId" as an integer and optionally "tsuids" as a
* List<String>
* @throws BadRequestException if the plugin has not implemented this method
*/
public Map<String, Object> parseTreeTSUIDsListV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseTreeCollisionNotMatchedV1");
}
/**
* Parses an annotation object
* @return An annotation object
* @throws BadRequestException if the plugin has not implemented this method
*/
public Annotation parseAnnotationV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented parseAnnotationV1");
}
/**
* Formats the results of an HTTP data point storage request
* @param results A map of results. The map will consist of:
* <ul><li>success - (long) the number of successfully parsed datapoints</li>
* <li>failed - (long) the number of datapoint parsing failures</li>
* <li>errors - (ArrayList<HashMap<String, Object>>) an optional list of
* datapoints that had errors. The nested map has these fields:
* <ul><li>error - (String) the error that occurred</li>
* <li>datapoint - (IncomingDatapoint) the datapoint that generated the error
* </li></ul></li></ul>
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatPutV1(final Map<String, Object> results) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatPutV1");
}
/**
* Formats a suggestion response
* @param suggestions List of suggestions for the given type
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatSuggestV1(final List<String> suggestions) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatSuggestV1");
}
/**
* Format the serializers status map
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatSerializersV1() {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatSerializersV1");
}
/**
* Format the list of implemented aggregators
* @param aggregators The list of aggregation functions
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatAggregatorsV1(final Set<String> aggregators) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatAggregatorsV1");
}
/**
* Format a hash map of information about the OpenTSDB version
* @param version A hash map with version information
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatVersionV1(final Map<String, String> version) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatVersionV1");
}
/**
* Format a response from the DropCaches call
* @param response A hash map with a response
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatDropCachesV1(final Map<String, String> response) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatDropCachesV1");
}
/**
* Format a response from the Uid Assignment RPC
* @param response A map of lists of pairs representing the results of the
* assignment
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatUidAssignV1(final
Map<String, TreeMap<String, String>> response) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatUidAssignV1");
}
/**
* Format the results from a timeseries data query
* @param query The TSQuery object used to fetch the results
* @param results The data fetched from storage
* @param globals An optional list of global annotation objects
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatQueryV1(final TSQuery query,
final List<DataPoints[]> results, final List<Annotation> globals) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatQueryV1");
}
/**
* Format a single UIDMeta object
* @param meta The UIDMeta object to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatUidMetaV1(final UIDMeta meta) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatUidMetaV1");
}
/**
* Format a single TSMeta object
* @param meta The TSMeta object to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTSMetaV1(final TSMeta meta) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTSMetaV1");
}
/**
* Format a single Branch object
* @param branch The branch to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatBranchV1(final Branch branch) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatBranchV1");
}
/**
* Format a single tree object
* @param tree tree to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTreeV1(final Tree tree) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTreeV1");
}
/**
* Format a list of tree objects. Note that the list may be empty if no trees
* were present.
* @param trees A list of one or more trees to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTreesV1(final List<Tree> trees) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTreesV1");
}
/**
* Format a single TreeRule object
* @param rule The rule to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTreeRuleV1(final TreeRule rule) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTreeRuleV1");
}
/**
* Format a map of one or more TSUIDs that collided or were not matched
* @param results The list of results. Collisions: key = tsuid, value =
* collided TSUID. Not Matched: key = tsuid, value = message about non matched
* rules.
* @param is_collisions Whether or the map is a collision result set (true) or
* a not matched set (false).
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTreeCollisionNotMatchedV1(
final Map<String, String> results, final boolean is_collisions) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTreeCollisionNotMatched");
}
/**
* Format the results of testing one or more TSUIDs through a tree's ruleset
* @param results The list of results. Main map key is the tsuid. Child map:
* "branch" : Parsed branch result, may be null
* "meta" : TSMeta object, may be null
* "messages" : An ArrayList<String> of one or more messages
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatTreeTestV1(final
HashMap<String, HashMap<String, Object>> results) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatTreeTestV1");
}
/**
* Format an annotation object
* @param note The annotation object to format
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatAnnotationV1(final Annotation note) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatAnnotationV1");
}
/**
* Format a list of statistics
* @param stats The statistics list to format
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatStatsV1(final List<IncomingDataPoint> stats) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatStatsV1");
}
/**
* Format the response from a search query
* @param results The query (hopefully filled with results) to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatSearchResultsV1(final SearchQuery results) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatSearchResultsV1");
}
/**
* Format the running configuration
* @param config The running config to serialize
* @return A ChannelBuffer object to pass on to the caller
* @throws BadRequestException if the plugin has not implemented this method
*/
public ChannelBuffer formatConfigV1(final Config config) {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"The requested API endpoint has not been implemented",
this.getClass().getCanonicalName() +
" has not implemented formatConfigV1");
}
/**
* Formats a 404 error when an endpoint or file wasn't found
* <p>
* <b>WARNING:</b> If overriding, make sure this method catches all errors and
* returns a byte array with a simple string error at the minimum
* @return A standard JSON error
*/
public ChannelBuffer formatNotFoundV1() {
StringBuilder output =
new StringBuilder(1024);
if (query.hasQueryStringParam("jsonp")) {
output.append(query.getQueryStringParam("jsonp") + "(");
}
output.append("{\"error\":{\"code\":");
output.append(404);
output.append(",\"message\":\"");
if (query.apiVersion() > 0) {
output.append("Endpoint not found");
} else {
output.append("Page not found");
}
output.append("\"}}");
if (query.hasQueryStringParam("jsonp")) {
output.append(")");
}
return ChannelBuffers.copiedBuffer(
output.toString().getBytes(this.query.getCharset()));
}
/**
* Format a bad request exception, indicating an invalid request from the
* user
* <p>
* <b>WARNING:</b> If overriding, make sure this method catches all errors and
* returns a byte array with a simple string error at the minimum
* @param exception The exception to format
* @return A standard JSON error
*/
public ChannelBuffer formatErrorV1(final BadRequestException exception) {
StringBuilder output =
new StringBuilder(exception.getMessage().length() * 2);
final String jsonp = query.getQueryStringParam("jsonp");
if (jsonp != null && !jsonp.isEmpty()) {
output.append(query.getQueryStringParam("jsonp") + "(");
}
output.append("{\"error\":{\"code\":");
output.append(exception.getStatus().getCode());
final StringBuilder msg = new StringBuilder(exception.getMessage().length());
HttpQuery.escapeJson(exception.getMessage(), msg);
output.append(",\"message\":\"").append(msg.toString()).append("\"");
if (!exception.getDetails().isEmpty()) {
final StringBuilder details = new StringBuilder(
exception.getDetails().length());
HttpQuery.escapeJson(exception.getDetails(), details);
output.append(",\"details\":\"").append(details.toString()).append("\"");
}
if (query.showStackTrace()) {
ThrowableProxy tp = new ThrowableProxy(exception);
tp.calculatePackagingData();
final String pretty_exc = ThrowableProxyUtil.asString(tp);
final StringBuilder trace = new StringBuilder(pretty_exc.length());
HttpQuery.escapeJson(pretty_exc, trace);
output.append(",\"trace\":\"").append(trace.toString()).append("\"");
}
output.append("}}");
if (jsonp != null && !jsonp.isEmpty()) {
output.append(")");
}
return ChannelBuffers.copiedBuffer(
output.toString().getBytes(this.query.getCharset()));
}
/**
* Format an internal error exception that was caused by the system
* Should return a 500 error
* <p>
* <b>WARNING:</b> If overriding, make sure this method catches all errors and
* returns a byte array with a simple string error at the minimum
* @param exception The system exception to format
* @return A standard JSON error
*/
public ChannelBuffer formatErrorV1(final Exception exception) {
String message = exception.getMessage();
// NPEs have a null for the message string (why?!?!?!)
if (exception.getClass() == NullPointerException.class) {
message = "An internal null pointer exception was thrown";
} else if (message == null) {
message = "An unknown exception occurred";
}
StringBuilder output =
new StringBuilder(message.length() * 2);
final String jsonp = query.getQueryStringParam("jsonp");
if (jsonp != null && !jsonp.isEmpty()) {
output.append(query.getQueryStringParam("jsonp") + "(");
}
output.append("{\"error\":{\"code\":");
output.append(500);
final StringBuilder msg = new StringBuilder(message.length());
HttpQuery.escapeJson(message, msg);
output.append(",\"message\":\"").append(msg.toString()).append("\"");
if (query.showStackTrace()) {
ThrowableProxy tp = new ThrowableProxy(exception);
tp.calculatePackagingData();
final String pretty_exc = ThrowableProxyUtil.asString(tp);
final StringBuilder trace = new StringBuilder(pretty_exc.length());
HttpQuery.escapeJson(pretty_exc, trace);
output.append(",\"trace\":\"").append(trace.toString()).append("\"");
}
output.append("}}");
if (jsonp != null && !jsonp.isEmpty()) {
output.append(")");
}
return ChannelBuffers.copiedBuffer(
output.toString().getBytes(this.query.getCharset()));
}
}