// This file is part of OpenTSDB.
// Copyright (C) 2010-2014 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.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Atomics;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.opentsdb.tools.BuildData;
import net.opentsdb.core.Aggregators;
import net.opentsdb.core.TSDB;
import net.opentsdb.query.filter.TagVFilter;
import net.opentsdb.stats.StatsCollector;
import net.opentsdb.utils.Config;
import net.opentsdb.utils.JSON;
import net.opentsdb.utils.PluginLoader;
/**
* Manager for the lifecycle of <code>HttpRpc</code>s, <code>TelnetRpc</code>s,
* <code>RpcPlugin</code>s, and <code>HttpRpcPlugin</code>. This is a
* singleton. Its lifecycle must be managed by the "container". If you are
* launching via {@code TSDMain} then shutdown (and non-lazy initialization)
* is taken care of. Outside of the use of {@code TSDMain}, you are responsible
* for shutdown, at least.
*
* <p> Here's an example of how to correctly handle shutdown manually:
*
* <pre>
* // Startup our TSDB instance...
* TSDB tsdb_instance = ...;
*
* // ... later, during shtudown ..
*
* if (RpcManager.isInitialized()) {
* // Check that its actually been initialized. We don't want to
* // create a new instance only to shutdown!
* RpcManager.instance(tsdb_instance).shutdown().join();
* }
* </pre>
*
* @since 2.2
*/
public final class RpcManager {
private static final Logger LOG = LoggerFactory.getLogger(RpcManager.class);
/** This is base path where {@link HttpRpcPlugin}s are rooted. It's used
* to match incoming requests. */
@VisibleForTesting
protected static final String PLUGIN_BASE_WEBPATH = "plugin";
/** Splitter for web paths. Removes empty strings to handle trailing or
* leading slashes. For instance, all of <code>/plugin/mytest</code>,
* <code>plugin/mytest/</code>, and <code>plugin/mytest</code> will be
* split to <code>[plugin, mytest]</code>. */
private static final Splitter WEBPATH_SPLITTER = Splitter.on('/')
.trimResults()
.omitEmptyStrings();
/** Matches paths declared by {@link HttpRpcPlugin}s that are rooted in
* the system's plugins path. */
private static final Pattern HAS_PLUGIN_BASE_WEBPATH = Pattern.compile(
"^/?" + PLUGIN_BASE_WEBPATH + "/?.*",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
/** Reference to our singleton instance. Set in {@link #initialize}. */
private static final AtomicReference<RpcManager> INSTANCE = Atomics.newReference();
/** Commands we can serve on the simple, telnet-style RPC interface. */
private ImmutableMap<String, TelnetRpc> telnet_commands;
/** Commands we serve on the HTTP interface. */
private ImmutableMap<String, HttpRpc> http_commands;
/** HTTP commands from user plugins. */
private ImmutableMap<String, HttpRpcPlugin> http_plugin_commands;
/** List of activated RPC plugins */
private ImmutableList<RpcPlugin> rpc_plugins;
/** The TSDB that owns us. */
private TSDB tsdb;
/**
* Constructor used by singleton factory method.
* @param tsdb the owning TSDB instance.
*/
private RpcManager(final TSDB tsdb) {
this.tsdb = tsdb;
}
/**
* Get or create the singleton instance of the manager, loading all the
* plugins enabled in the given TSDB's {@link Config}.
* @return the shared instance of {@link RpcManager}. It's okay to
* hold this reference once obtained.
*/
public static synchronized RpcManager instance(final TSDB tsdb) {
final RpcManager existing = INSTANCE.get();
if (existing != null) {
return existing;
}
final RpcManager manager = new RpcManager(tsdb);
final String mode = Strings.nullToEmpty(tsdb.getConfig().getString("tsd.mode"));
// Load any plugins that are enabled via Config. Fail if any plugin cannot be loaded.
final ImmutableList.Builder<RpcPlugin> rpcBuilder = ImmutableList.builder();
if (tsdb.getConfig().hasProperty("tsd.rpc.plugins")) {
final String[] plugins = tsdb.getConfig().getString("tsd.rpc.plugins").split(",");
manager.initializeRpcPlugins(plugins, rpcBuilder);
}
manager.rpc_plugins = rpcBuilder.build();
final ImmutableMap.Builder<String, TelnetRpc> telnetBuilder = ImmutableMap.builder();
final ImmutableMap.Builder<String, HttpRpc> httpBuilder = ImmutableMap.builder();
manager.initializeBuiltinRpcs(mode, telnetBuilder, httpBuilder);
manager.telnet_commands = telnetBuilder.build();
manager.http_commands = httpBuilder.build();
final ImmutableMap.Builder<String, HttpRpcPlugin> httpPluginsBuilder = ImmutableMap.builder();
if (tsdb.getConfig().hasProperty("tsd.http.rpc.plugins")) {
final String[] plugins = tsdb.getConfig().getString("tsd.http.rpc.plugins").split(",");
manager.initializeHttpRpcPlugins(mode, plugins, httpPluginsBuilder);
}
manager.http_plugin_commands = httpPluginsBuilder.build();
INSTANCE.set(manager);
return manager;
}
/**
* @return {@code true} if the shared instance has been initialized;
* {@code false} otherwise.
*/
public static synchronized boolean isInitialized() {
return INSTANCE.get() != null;
}
/**
* @return list of loaded {@link RpcPlugin}s. Possibly empty but
* never {@code null}.
*/
@VisibleForTesting
protected ImmutableList<RpcPlugin> getRpcPlugins() {
return rpc_plugins;
}
/**
* Lookup a {@link TelnetRpc} based on given command name. Note that this
* lookup is case sensitive in that the {@code command} passed in must
* match a registered RPC command exactly.
* @param command a telnet API command name.
* @return the {@link TelnetRpc} for the given {@code command} or {@code null}
* if not found.
*/
TelnetRpc lookupTelnetRpc(final String command) {
return telnet_commands.get(command);
}
/**
* Lookup a built-in {@link HttpRpc} based on the given {@code queryBaseRoute}.
* The lookup is based on exact match of the input parameter and the registered
* {@link HttpRpc}s.
* @param queryBaseRoute the HTTP query's base route, with no trailing or
* leading slashes. For example: {@code api/query}
* @return the {@link HttpRpc} for the given {@code queryBaseRoute} or
* {@code null} if not found.
*/
HttpRpc lookupHttpRpc(final String queryBaseRoute) {
return http_commands.get(queryBaseRoute);
}
/**
* Lookup a user-supplied {@link HttpRpcPlugin} for the given
* {@code queryBaseRoute}. The lookup is based on exact match of the input
* parameter and the registered {@link HttpRpcPlugin}s.
* @param queryBaseRoute the value of {@link HttpRpcPlugin#getPath()} with no
* trailing or leading slashes.
* @return the {@link HttpRpcPlugin} for the given {@code queryBaseRoute} or
* {@code null} if not found.
*/
HttpRpcPlugin lookupHttpRpcPlugin(final String queryBaseRoute) {
return http_plugin_commands.get(queryBaseRoute);
}
/**
* @param uri HTTP request URI, with or without query parameters.
* @return {@code true} if the URI represents a request for a
* {@link HttpRpcPlugin}; {@code false} otherwise. Note that this
* method returning true <strong>says nothing</strong> about
* whether or not there is a {@link HttpRpcPlugin} registered
* at the given URI, only that it's a valid RPC plugin request.
*/
boolean isHttpRpcPluginPath(final String uri) {
if (Strings.isNullOrEmpty(uri) || uri.length() <= PLUGIN_BASE_WEBPATH.length()) {
return false;
} else {
// Don't consider the query portion, if any.
int qmark = uri.indexOf('?');
String path = uri;
if (qmark != -1) {
path = uri.substring(0, qmark);
}
final List<String> parts = WEBPATH_SPLITTER.splitToList(path);
return (parts.size() > 1 && parts.get(0).equals(PLUGIN_BASE_WEBPATH));
}
}
/**
* Load and init instances of {@link TelnetRpc}s and {@link HttpRpc}s.
* These are not generally configurable via TSDB config.
* @param mode is this TSD in read/write ("rw") or read-only ("ro")
* mode?
* @param telnet a map of telnet command names to {@link TelnetRpc}
* instances.
* @param http a map of API endpoints to {@link HttpRpc} instances.
*/
private void initializeBuiltinRpcs(final String mode,
final ImmutableMap.Builder<String, TelnetRpc> telnet,
final ImmutableMap.Builder<String, HttpRpc> http) {
final Boolean enableApi = tsdb.getConfig().getString("tsd.core.enable_api").equals("true");
final Boolean enableUi = tsdb.getConfig().getString("tsd.core.enable_ui").equals("true");
final Boolean enableDieDieDie = tsdb.getConfig().getString("tsd.no_diediedie").equals("false");
LOG.info("Mode: {}, HTTP UI Enabled: {}, HTTP API Enabled: {}", mode, enableUi, enableApi);
if (mode.equals("rw") || mode.equals("wo")) {
final PutDataPointRpc put = new PutDataPointRpc();
telnet.put("put", put);
if (enableApi) {
http.put("api/put", put);
}
}
if (mode.equals("rw") || mode.equals("ro")) {
final StaticFileRpc staticfile = new StaticFileRpc();
final StatsRpc stats = new StatsRpc();
final DropCachesRpc dropcaches = new DropCachesRpc();
final ListAggregators aggregators = new ListAggregators();
final SuggestRpc suggest_rpc = new SuggestRpc();
final AnnotationRpc annotation_rpc = new AnnotationRpc();
final Version version = new Version();
telnet.put("stats", stats);
telnet.put("dropcaches", dropcaches);
telnet.put("version", version);
telnet.put("exit", new Exit());
telnet.put("help", new Help());
if (enableUi) {
http.put("", new HomePage());
http.put("aggregators", aggregators);
http.put("dropcaches", dropcaches);
http.put("favicon.ico", staticfile);
http.put("logs", new LogsRpc());
http.put("q", new GraphHandler());
http.put("s", staticfile);
http.put("stats", stats);
http.put("suggest", suggest_rpc);
http.put("version", version);
}
if (enableApi) {
http.put("api/aggregators", aggregators);
http.put("api/annotation", annotation_rpc);
http.put("api/annotations", annotation_rpc);
http.put("api/config", new ShowConfig());
http.put("api/dropcaches", dropcaches);
http.put("api/query", new QueryRpc());
http.put("api/search", new SearchRpc());
http.put("api/serializers", new Serializers());
http.put("api/stats", stats);
http.put("api/suggest", suggest_rpc);
http.put("api/tree", new TreeRpc());
http.put("api/uid", new UniqueIdRpc());
http.put("api/version", version);
}
}
if (enableDieDieDie) {
final DieDieDie diediedie = new DieDieDie();
telnet.put("diediedie", diediedie);
if (enableUi) {
http.put("diediedie", diediedie);
}
}
}
/**
* Load and init the {@link HttpRpcPlugin}s provided as an array of
* {@code pluginClassNames}.
* @param mode is this TSD in read/write ("rw") or read-only ("ro")
* mode?
* @param pluginClassNames fully-qualified class names that are
* instances of {@link HttpRpcPlugin}s
* @param http a map of canonicalized paths
* (obtained via {@link #canonicalizePluginPath(String)})
* to {@link HttpRpcPlugin} instance.
*/
@VisibleForTesting
protected void initializeHttpRpcPlugins(final String mode,
final String[] pluginClassNames,
final ImmutableMap.Builder<String, HttpRpcPlugin> http) {
for (final String plugin : pluginClassNames) {
final HttpRpcPlugin rpc = createAndInitialize(plugin, HttpRpcPlugin.class);
validateHttpRpcPluginPath(rpc.getPath());
final String path = rpc.getPath().trim();
final String canonicalized_path = canonicalizePluginPath(path);
http.put(canonicalized_path, rpc);
LOG.info("Mounted HttpRpcPlugin [{}] at path \"{}\"", rpc.getClass().getName(), canonicalized_path);
}
}
/**
* Ensure that the given path for an {@link HttpRpcPlugin} is valid. This
* method simply returns for valid inputs; throws and exception otherwise.
* @param path a request path, no query parameters, etc.
* @throws IllegalArgumentException on invalid paths.
*/
@VisibleForTesting
protected void validateHttpRpcPluginPath(final String path) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(path),
"Invalid HttpRpcPlugin path. Path is null or empty.");
final String testPath = path.trim();
Preconditions.checkArgument(!HAS_PLUGIN_BASE_WEBPATH.matcher(path).matches(),
"Invalid HttpRpcPlugin path %s. Path contains system's plugin base path.",
testPath);
URI uri = URI.create(testPath);
Preconditions.checkArgument(!Strings.isNullOrEmpty(uri.getPath()),
"Invalid HttpRpcPlugin path %s. Parsed path is null or empty.", testPath);
Preconditions.checkArgument(!uri.getPath().equals("/"),
"Invalid HttpRpcPlugin path %s. Path is equal to root.", testPath);
Preconditions.checkArgument(Strings.isNullOrEmpty(uri.getQuery()),
"Invalid HttpRpcPlugin path %s. Path contains query parameters.", testPath);
}
/**
* @param origPath a request path, no query parameters, etc.
* @return a canonical representation of the input, with trailing and leading
* slashes removed.
* @throws IllegalArgumentException if the given path is a root.
*/
@VisibleForTesting
protected String canonicalizePluginPath(final String origPath) {
Preconditions.checkArgument(!(Strings.isNullOrEmpty(origPath) || origPath.equals("/")),
"Path %s is a root.", origPath);
String new_path = origPath;
if (new_path.startsWith("/")) {
new_path = new_path.substring(1);
}
if (new_path.endsWith("/")) {
new_path = new_path.substring(0, new_path.length()-1);
}
return new_path;
}
/**
* Load and init the {@link RpcPlugin}s provided as an array of
* {@code pluginClassNames}.
* @param pluginClassNames fully-qualified class names that are
* instances of {@link RpcPlugin}s
* @param rpcs a list of loaded and initialized plugins
*/
private void initializeRpcPlugins(final String[] pluginClassNames,
final ImmutableList.Builder<RpcPlugin> rpcs) {
for (final String plugin : pluginClassNames) {
final RpcPlugin rpc = createAndInitialize(plugin, RpcPlugin.class);
rpcs.add(rpc);
}
}
/**
* Helper method to load and initialize a given plugin class. This uses reflection
* because plugins share no common interfaces. (They could though!)
* @param pluginClassName the class name of the plugin to load
* @param pluginClass class of the plugin
* @return loaded an initialized instance of {@code pluginClass}
*/
@VisibleForTesting
protected <T> T createAndInitialize(final String pluginClassName, final Class<T> pluginClass) {
final T instance = PluginLoader.loadSpecificPlugin(pluginClassName, pluginClass);
Preconditions.checkState(instance != null,
"Unable to locate %s using name '%s", pluginClass, pluginClassName);
try {
final Method initMeth = instance.getClass().getMethod("initialize", TSDB.class);
initMeth.invoke(instance, tsdb);
final Method versionMeth = instance.getClass().getMethod("version");
String version = (String) versionMeth.invoke(instance);
LOG.info("Successfully initialized plugin [{}] version: {}",
instance.getClass().getCanonicalName(),
version);
return instance;
} catch (Exception e) {
throw new RuntimeException("Failed to initialize " + instance.getClass(), e);
}
}
/**
* Called to gracefully shutdown the plugin. Implementations should close
* any IO they have open
* @return A deferred object that indicates the completion of the request.
* The {@link Object} has not special meaning and can be {@code null}
* (think of it as {@code Deferred<Void>}).
*/
public Deferred<ArrayList<Object>> shutdown() {
// Clear shared instance.
INSTANCE.set(null);
final Collection<Deferred<Object>> deferreds = Lists.newArrayList();
if (http_plugin_commands != null) {
for (final Map.Entry<String, HttpRpcPlugin> entry : http_plugin_commands.entrySet()) {
deferreds.add(entry.getValue().shutdown());
}
}
if (rpc_plugins != null) {
for (final RpcPlugin rpc : rpc_plugins) {
deferreds.add(rpc.shutdown());
}
}
return Deferred.groupInOrder(deferreds);
}
/**
* Collect stats on the shared instance of {@link RpcManager}.
*/
static void collectStats(final StatsCollector collector) {
final RpcManager manager = INSTANCE.get();
if (manager != null) {
if (manager.rpc_plugins != null) {
try {
collector.addExtraTag("plugin", "rpc");
for (final RpcPlugin rpc : manager.rpc_plugins) {
rpc.collectStats(collector);
}
} finally {
collector.clearExtraTag("plugin");
}
}
if (manager.http_plugin_commands != null) {
try {
collector.addExtraTag("plugin", "httprpc");
for (final Map.Entry<String, HttpRpcPlugin> entry
: manager.http_plugin_commands.entrySet()) {
entry.getValue().collectStats(collector);
}
} finally {
collector.clearExtraTag("plugin");
}
}
}
}
// ---------------------------- //
// Individual command handlers. //
// ---------------------------- //
/** The "diediedie" command and "/diediedie" endpoint. */
private final class DieDieDie implements TelnetRpc, HttpRpc {
public Deferred<Object> execute(final TSDB tsdb, final Channel chan,
final String[] cmd) {
LOG.warn("{} {}", chan, "shutdown requested");
chan.write("Cleaning up and exiting now.\n");
return doShutdown(tsdb, chan);
}
public void execute(final TSDB tsdb, final HttpQuery query) {
LOG.warn("{} {}", query, "shutdown requested");
query.sendReply(HttpQuery.makePage("TSD Exiting", "You killed me",
"Cleaning up and exiting now."));
doShutdown(tsdb, query.channel());
}
private Deferred<Object> doShutdown(final TSDB tsdb, final Channel chan) {
((GraphHandler) http_commands.get("q")).shutdown();
ConnectionManager.closeAllConnections();
// Netty gets stuck in an infinite loop if we shut it down from within a
// NIO thread. So do this from a newly created thread.
final class ShutdownNetty extends Thread {
ShutdownNetty() {
super("ShutdownNetty");
}
public void run() {
chan.getFactory().releaseExternalResources();
}
}
new ShutdownNetty().start(); // Stop accepting new connections.
// Log any error that might occur during shutdown.
final class ShutdownTSDB implements Callback<Exception, Exception> {
public Exception call(final Exception arg) {
LOG.error("Unexpected exception while shutting down", arg);
return arg;
}
public String toString() {
return "shutdown callback";
}
}
return tsdb.shutdown().addErrback(new ShutdownTSDB());
}
}
/** The "exit" command. */
private static final class Exit implements TelnetRpc {
public Deferred<Object> execute(final TSDB tsdb, final Channel chan,
final String[] cmd) {
chan.disconnect();
return Deferred.fromResult(null);
}
}
/** The "help" command. */
private final class Help implements TelnetRpc {
public Deferred<Object> execute(final TSDB tsdb, final Channel chan,
final String[] cmd) {
final StringBuilder buf = new StringBuilder();
buf.append("available commands: ");
// TODO(tsuna): Maybe sort them?
for (final String command : telnet_commands.keySet()) {
buf.append(command).append(' ');
}
buf.append('\n');
chan.write(buf.toString());
return Deferred.fromResult(null);
}
}
/** The home page ("GET /"). */
private static final class HomePage implements HttpRpc {
public void execute(final TSDB tsdb, final HttpQuery query)
throws IOException {
final StringBuilder buf = new StringBuilder(2048);
buf.append("<div id=queryuimain></div>"
+ "<noscript>You must have JavaScript enabled.</noscript>"
+ "<iframe src=javascript:'' id=__gwt_historyFrame tabIndex=-1"
+ " style=position:absolute;width:0;height:0;border:0>"
+ "</iframe>");
query.sendReply(HttpQuery.makePage(
"<script type=text/javascript language=javascript"
+ " src=s/queryui.nocache.js></script>",
"OpenTSDB", "", buf.toString()));
}
}
/** The "/aggregators" endpoint. */
private static final class ListAggregators implements HttpRpc {
public void execute(final TSDB tsdb, final HttpQuery query)
throws IOException {
// only accept GET / POST
RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName());
if (query.apiVersion() > 0) {
query.sendReply(
query.serializer().formatAggregatorsV1(Aggregators.set()));
} else {
query.sendReply(JSON.serializeToBytes(Aggregators.set()));
}
}
}
/** The "version" command. */
private static final class Version implements TelnetRpc, HttpRpc {
public Deferred<Object> execute(final TSDB tsdb, final Channel chan,
final String[] cmd) {
if (chan.isConnected()) {
chan.write(BuildData.revisionString() + '\n'
+ BuildData.buildString() + '\n');
}
return Deferred.fromResult(null);
}
public void execute(final TSDB tsdb, final HttpQuery query) throws
IOException {
// only accept GET / POST
RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName());
final HashMap<String, String> version = new HashMap<String, String>();
version.put("version", BuildData.version);
version.put("short_revision", BuildData.short_revision);
version.put("full_revision", BuildData.full_revision);
version.put("timestamp", Long.toString(BuildData.timestamp));
version.put("repo_status", BuildData.repo_status.toString());
version.put("user", BuildData.user);
version.put("host", BuildData.host);
version.put("repo", BuildData.repo);
version.put("branch", BuildData.branch);
if (query.apiVersion() > 0) {
query.sendReply(query.serializer().formatVersionV1(version));
} else {
final boolean json = query.request().getUri().endsWith("json");
if (json) {
query.sendReply(JSON.serializeToBytes(version));
} else {
final String revision = BuildData.revisionString();
final String build = BuildData.buildString();
StringBuilder buf;
buf = new StringBuilder(2 // For the \n's
+ revision.length() + build.length());
buf.append(revision).append('\n').append(build).append('\n');
query.sendReply(buf);
}
}
}
}
/** The /api/formatters endpoint
* @since 2.0 */
private static final class Serializers implements HttpRpc {
public void execute(final TSDB tsdb, final HttpQuery query)
throws IOException {
// only accept GET / POST
RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName());
switch (query.apiVersion()) {
case 0:
case 1:
query.sendReply(query.serializer().formatSerializersV1());
break;
default:
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
}
}
private static final class ShowConfig implements HttpRpc {
@Override
public void execute(TSDB tsdb, HttpQuery query) throws IOException {
// only accept GET/POST
RpcUtil.allowedMethods(query.method(), HttpMethod.GET.getName(), HttpMethod.POST.getName());
final String[] uri = query.explodeAPIPath();
final String endpoint = uri.length > 1 ? uri[1].toLowerCase() : "";
if (endpoint.equals("filters")) {
switch (query.apiVersion()) {
case 0:
case 1:
query.sendReply(query.serializer().formatFilterConfigV1(
TagVFilter.loadedFilters()));
break;
default:
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
} else {
switch (query.apiVersion()) {
case 0:
case 1:
query.sendReply(query.serializer().formatConfigV1(tsdb.getConfig()));
break;
default:
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Requested API version not implemented", "Version " +
query.apiVersion() + " is not implemented");
}
}
}
}
}