// 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.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.hbase.async.Bytes;
import org.hbase.async.Bytes.ByteMap;
import org.hbase.async.PutRequest;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
import net.opentsdb.core.TSDB;
import net.opentsdb.core.Tags;
import net.opentsdb.meta.TSMeta;
import net.opentsdb.meta.TSUIDQuery;
import net.opentsdb.meta.UIDMeta;
import net.opentsdb.uid.NoSuchUniqueId;
import net.opentsdb.uid.NoSuchUniqueName;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.uid.UniqueId.UniqueIdType;
/**
* Handles calls for UID processing including getting UID status, assigning UIDs
* and other functions.
* @since 2.0
*/
final class UniqueIdRpc implements HttpRpc {
@Override
public void execute(TSDB tsdb, HttpQuery query) throws IOException {
// the uri will be /api/vX/uid/? or /api/uid/?
final String[] uri = query.explodeAPIPath();
final String endpoint = uri.length > 1 ? uri[1] : "";
if (endpoint.toLowerCase().equals("assign")) {
this.handleAssign(tsdb, query);
return;
} else if (endpoint.toLowerCase().equals("uidmeta")) {
this.handleUIDMeta(tsdb, query);
return;
} else if (endpoint.toLowerCase().equals("tsmeta")) {
this.handleTSMeta(tsdb, query);
return;
} else if (endpoint.toLowerCase().equals("rename")) {
this.handleRename(tsdb, query);
return;
} else {
throw new BadRequestException(HttpResponseStatus.NOT_IMPLEMENTED,
"Other UID endpoints have not been implemented yet");
}
}
/**
* Assigns UIDs to the given metric, tagk or tagv names if applicable
* <p>
* This handler supports GET and POST whereby the GET command can
* parse query strings with the {@code type} as their parameter and a comma
* separated list of values to assign UIDs to.
* <p>
* Multiple types and names can be provided in one call. Each name will be
* processed independently and if there's an error (such as an invalid name or
* it is already assigned) the error will be stored in a separate error map
* and other UIDs will be processed.
* @param tsdb The TSDB from the RPC router
* @param query The query for this request
*/
private void handleAssign(final TSDB tsdb, final HttpQuery query) {
// only accept GET And POST
if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) {
throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
"Method not allowed", "The HTTP method [" + query.method().getName() +
"] is not permitted for this endpoint");
}
final HashMap<String, List<String>> source;
if (query.method() == HttpMethod.POST) {
source = query.serializer().parseUidAssignV1();
} else {
source = new HashMap<String, List<String>>(3);
// cut down on some repetitive code, split the query string values by
// comma and add them to the source hash
String[] types = {"metric", "tagk", "tagv"};
for (int i = 0; i < types.length; i++) {
final String values = query.getQueryStringParam(types[i]);
if (values != null && !values.isEmpty()) {
final String[] metrics = values.split(",");
if (metrics != null && metrics.length > 0) {
source.put(types[i], Arrays.asList(metrics));
}
}
}
}
if (source.size() < 1) {
throw new BadRequestException("Missing values to assign UIDs");
}
final Map<String, TreeMap<String, String>> response =
new HashMap<String, TreeMap<String, String>>();
int error_count = 0;
for (Map.Entry<String, List<String>> entry : source.entrySet()) {
final TreeMap<String, String> results =
new TreeMap<String, String>();
final TreeMap<String, String> errors =
new TreeMap<String, String>();
for (String name : entry.getValue()) {
try {
final byte[] uid = tsdb.assignUid(entry.getKey(), name);
results.put(name,
UniqueId.uidToString(uid));
} catch (IllegalArgumentException e) {
errors.put(name, e.getMessage());
error_count++;
}
}
response.put(entry.getKey(), results);
if (errors.size() > 0) {
response.put(entry.getKey() + "_errors", errors);
}
}
if (error_count < 1) {
query.sendReply(query.serializer().formatUidAssignV1(response));
} else {
query.sendReply(HttpResponseStatus.BAD_REQUEST,
query.serializer().formatUidAssignV1(response));
}
}
/**
* Handles CRUD calls to individual UIDMeta data entries
* @param tsdb The TSDB from the RPC router
* @param query The query for this request
*/
private void handleUIDMeta(final TSDB tsdb, final HttpQuery query) {
final HttpMethod method = query.getAPIMethod();
// GET
if (method == HttpMethod.GET) {
final String uid = query.getRequiredQueryStringParam("uid");
final UniqueIdType type = UniqueId.stringToUniqueIdType(
query.getRequiredQueryStringParam("type"));
try {
final UIDMeta meta = UIDMeta.getUIDMeta(tsdb, type, uid)
.joinUninterruptibly();
query.sendReply(query.serializer().formatUidMetaV1(meta));
} catch (NoSuchUniqueId e) {
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Could not find the requested UID", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
// POST
} else if (method == HttpMethod.POST || method == HttpMethod.PUT) {
final UIDMeta meta;
if (query.hasContent()) {
meta = query.serializer().parseUidMetaV1();
} else {
meta = this.parseUIDMetaQS(query);
}
/**
* Storage callback used to determine if the storage call was successful
* or not. Also returns the updated object from storage.
*/
class SyncCB implements Callback<Deferred<UIDMeta>, Boolean> {
@Override
public Deferred<UIDMeta> call(Boolean success) throws Exception {
if (!success) {
throw new BadRequestException(
HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Failed to save the UIDMeta to storage",
"This may be caused by another process modifying storage data");
}
return UIDMeta.getUIDMeta(tsdb, meta.getType(), meta.getUID());
}
}
try {
final Deferred<UIDMeta> process_meta = meta.syncToStorage(tsdb,
method == HttpMethod.PUT).addCallbackDeferring(new SyncCB());
final UIDMeta updated_meta = process_meta.joinUninterruptibly();
tsdb.indexUIDMeta(updated_meta);
query.sendReply(query.serializer().formatUidMetaV1(updated_meta));
} catch (IllegalStateException e) {
query.sendStatusOnly(HttpResponseStatus.NOT_MODIFIED);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e);
} catch (NoSuchUniqueId e) {
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Could not find the requested UID", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
// DELETE
} else if (method == HttpMethod.DELETE) {
final UIDMeta meta;
if (query.hasContent()) {
meta = query.serializer().parseUidMetaV1();
} else {
meta = this.parseUIDMetaQS(query);
}
try {
meta.delete(tsdb).joinUninterruptibly();
tsdb.deleteUIDMeta(meta);
} catch (IllegalArgumentException e) {
throw new BadRequestException("Unable to delete UIDMeta information", e);
} catch (NoSuchUniqueId e) {
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Could not find the requested UID", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
query.sendStatusOnly(HttpResponseStatus.NO_CONTENT);
} else {
throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
"Method not allowed", "The HTTP method [" + method.getName() +
"] is not permitted for this endpoint");
}
}
/**
* Handles CRUD calls to individual TSMeta data entries
* @param tsdb The TSDB from the RPC router
* @param query The query for this request
*/
private void handleTSMeta(final TSDB tsdb, final HttpQuery query) {
final HttpMethod method = query.getAPIMethod();
// GET
if (method == HttpMethod.GET) {
String tsuid = null;
if (query.hasQueryStringParam("tsuid")) {
tsuid = query.getQueryStringParam("tsuid");
try {
final TSMeta meta = TSMeta.getTSMeta(tsdb, tsuid).joinUninterruptibly();
if (meta != null) {
query.sendReply(query.serializer().formatTSMetaV1(meta));
} else {
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Could not find Timeseries meta data");
}
} catch (NoSuchUniqueName e) {
// this would only happen if someone deleted a UID but left the
// the timeseries meta data
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Unable to find one of the UIDs", e);
} catch (BadRequestException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
String mquery = query.getRequiredQueryStringParam("m");
// m is of the following forms:
// metric[{tag=value,...}]
// where the parts in square brackets `[' .. `]' are optional.
final HashMap<String, String> tags = new HashMap<String, String>();
String metric = null;
try {
metric = Tags.parseWithMetric(mquery, tags);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e);
}
final TSUIDQuery tsuid_query = new TSUIDQuery(tsdb, metric, tags);
try {
final List<TSMeta> tsmetas = tsuid_query.getTSMetas()
.joinUninterruptibly();
query.sendReply(query.serializer().formatTSMetaListV1(tsmetas));
} catch (NoSuchUniqueName e) {
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Unable to find one of the UIDs", e);
} catch (BadRequestException e) {
throw e;
} catch (RuntimeException e) {
throw new BadRequestException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// POST / PUT
} else if (method == HttpMethod.POST || method == HttpMethod.PUT) {
final TSMeta meta;
if (query.hasContent()) {
meta = query.serializer().parseTSMetaV1();
} else {
meta = this.parseTSMetaQS(query);
}
/**
* Storage callback used to determine if the storage call was successful
* or not. Also returns the updated object from storage.
*/
class SyncCB implements Callback<Deferred<TSMeta>, Boolean> {
@Override
public Deferred<TSMeta> call(Boolean success) throws Exception {
if (!success) {
throw new BadRequestException(
HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Failed to save the TSMeta to storage",
"This may be caused by another process modifying storage data");
}
return TSMeta.getTSMeta(tsdb, meta.getTSUID());
}
}
if (meta.getTSUID() == null || meta.getTSUID().isEmpty()) {
// we got a JSON object without TSUID. Try to find a timeseries spec of
// the form "m": "metric{tagk=tagv,...}"
final String metric = query.getRequiredQueryStringParam("m");
final boolean create = query.getQueryStringParam("create") != null
&& query.getQueryStringParam("create").equals("true");
final String tsuid = getTSUIDForMetric(metric, tsdb);
class WriteCounterIfNotPresentCB implements Callback<Boolean, Boolean> {
@Override
public Boolean call(Boolean exists) throws Exception {
if (!exists && create) {
final PutRequest put = new PutRequest(tsdb.metaTable(),
UniqueId.stringToUid(tsuid), TSMeta.FAMILY(),
TSMeta.COUNTER_QUALIFIER(), Bytes.fromLong(0));
tsdb.getClient().put(put);
}
return exists;
}
}
try {
// Check whether we have a TSMeta stored already
final boolean exists = TSMeta
.metaExistsInStorage(tsdb, tsuid)
.joinUninterruptibly();
// set TSUID
meta.setTSUID(tsuid);
if (!exists && create) {
// Write 0 to counter column if not present
TSMeta.counterExistsInStorage(tsdb, UniqueId.stringToUid(tsuid))
.addCallback(new WriteCounterIfNotPresentCB())
.joinUninterruptibly();
// set TSUID
final Deferred<TSMeta> process_meta = meta.storeNew(tsdb)
.addCallbackDeferring(new SyncCB());
final TSMeta updated_meta = process_meta.joinUninterruptibly();
tsdb.indexTSMeta(updated_meta);
tsdb.processTSMetaThroughTrees(updated_meta);
query.sendReply(query.serializer().formatTSMetaV1(updated_meta));
} else if (exists) {
final Deferred<TSMeta> process_meta = meta.syncToStorage(tsdb,
method == HttpMethod.PUT).addCallbackDeferring(new SyncCB());
final TSMeta updated_meta = process_meta.joinUninterruptibly();
tsdb.indexTSMeta(updated_meta);
query.sendReply(query.serializer().formatTSMetaV1(updated_meta));
} else {
throw new BadRequestException(
"Could not find TSMeta, specify \"create=true\" to create a new one.");
}
} catch (IllegalStateException e) {
query.sendStatusOnly(HttpResponseStatus.NOT_MODIFIED);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e);
} catch (BadRequestException e) {
throw e;
} catch (NoSuchUniqueName e) {
// this would only happen if someone deleted a UID but left the
// the timeseries meta data
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Unable to find one or more UIDs", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
try {
final Deferred<TSMeta> process_meta = meta.syncToStorage(tsdb,
method == HttpMethod.PUT).addCallbackDeferring(new SyncCB());
final TSMeta updated_meta = process_meta.joinUninterruptibly();
tsdb.indexTSMeta(updated_meta);
query.sendReply(query.serializer().formatTSMetaV1(updated_meta));
} catch (IllegalStateException e) {
query.sendStatusOnly(HttpResponseStatus.NOT_MODIFIED);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e);
} catch (NoSuchUniqueName e) {
// this would only happen if someone deleted a UID but left the
// the timeseries meta data
throw new BadRequestException(HttpResponseStatus.NOT_FOUND,
"Unable to find one or more UIDs", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// DELETE
} else if (method == HttpMethod.DELETE) {
final TSMeta meta;
if (query.hasContent()) {
meta = query.serializer().parseTSMetaV1();
} else {
meta = this.parseTSMetaQS(query);
}
try{
meta.delete(tsdb);
tsdb.deleteTSMeta(meta.getTSUID());
} catch (IllegalArgumentException e) {
throw new BadRequestException("Unable to delete TSMeta information", e);
}
query.sendStatusOnly(HttpResponseStatus.NO_CONTENT);
} else {
throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
"Method not allowed", "The HTTP method [" + method.getName() +
"] is not permitted for this endpoint");
}
}
/**
* Used with verb overrides to parse out values from a query string
* @param query The query to parse
* @return An UIDMeta object with configured values
* @throws BadRequestException if a required value was missing or could not
* be parsed
*/
private UIDMeta parseUIDMetaQS(final HttpQuery query) {
final String uid = query.getRequiredQueryStringParam("uid");
final String type = query.getRequiredQueryStringParam("type");
final UIDMeta meta = new UIDMeta(UniqueId.stringToUniqueIdType(type), uid);
final String display_name = query.getQueryStringParam("display_name");
if (display_name != null) {
meta.setDisplayName(display_name);
}
final String description = query.getQueryStringParam("description");
if (description != null) {
meta.setDescription(description);
}
final String notes = query.getQueryStringParam("notes");
if (notes != null) {
meta.setNotes(notes);
}
return meta;
}
/**
* Rename UID to a new name of the given metric, tagk or tagv names
* <p>
* This handler supports GET and POST whereby the GET command can parse query
* strings with the {@code type} and {@code name} as their parameters.
* <p>
* @param tsdb The TSDB from the RPC router
* @param query The query for this request
*/
private void handleRename(final TSDB tsdb, final HttpQuery query) {
// only accept GET and POST
if (query.method() != HttpMethod.GET && query.method() != HttpMethod.POST) {
throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED,
"Method not allowed", "The HTTP method[" + query.method().getName() +
"] is not permitted for this endpoint");
}
final HashMap<String, String> source;
if (query.method() == HttpMethod.POST) {
source = query.serializer().parseUidRenameV1();
} else {
source = new HashMap<String, String>(3);
final String[] types = {"metric", "tagk", "tagv", "name"};
for (int i = 0; i < types.length; i++) {
final String value = query.getQueryStringParam(types[i]);
if (value!= null && !value.isEmpty()) {
source.put(types[i], value);
}
}
}
String type = null;
String oldname = null;
String newname = null;
for (Map.Entry<String, String> entry : source.entrySet()) {
if (entry.getKey().equals("name")) {
newname = entry.getValue();
} else {
type = entry.getKey();
oldname = entry.getValue();
}
}
// we need a type/value and new name
if (type == null || oldname == null || newname == null) {
throw new BadRequestException("Missing necessary values to rename UID");
}
HashMap<String, String> response = new HashMap<String, String>(2);
try {
tsdb.renameUid(type, oldname, newname);
response.put("result", "true");
} catch (IllegalArgumentException e) {
response.put("result", "false");
response.put("error", e.getMessage());
}
if (!response.containsKey("error")) {
query.sendReply(query.serializer().formatUidRenameV1(response));
} else {
query.sendReply(HttpResponseStatus.BAD_REQUEST,
query.serializer().formatUidRenameV1(response));
}
}
/**
* Used with verb overrides to parse out values from a query string
* @param query The query to parse
* @return An TSMeta object with configured values
* @throws BadRequestException if a required value was missing or could not
* be parsed
*/
private TSMeta parseTSMetaQS(final HttpQuery query) {
final String tsuid = query.getQueryStringParam("tsuid");
final TSMeta meta;
if (tsuid != null && !tsuid.isEmpty()) {
meta = new TSMeta(tsuid);
} else {
meta = new TSMeta();
}
final String display_name = query.getQueryStringParam("display_name");
if (display_name != null) {
meta.setDisplayName(display_name);
}
final String description = query.getQueryStringParam("description");
if (description != null) {
meta.setDescription(description);
}
final String notes = query.getQueryStringParam("notes");
if (notes != null) {
meta.setNotes(notes);
}
final String units = query.getQueryStringParam("units");
if (units != null) {
meta.setUnits(units);
}
final String data_type = query.getQueryStringParam("data_type");
if (data_type != null) {
meta.setDataType(data_type);
}
final String retention = query.getQueryStringParam("retention");
if (retention != null && !retention.isEmpty()) {
try {
meta.setRetention(Integer.parseInt(retention));
} catch (NumberFormatException nfe) {
throw new BadRequestException("Unable to parse 'retention' value");
}
}
final String max = query.getQueryStringParam("max");
if (max != null && !max.isEmpty()) {
try {
meta.setMax(Float.parseFloat(max));
} catch (NumberFormatException nfe) {
throw new BadRequestException("Unable to parse 'max' value");
}
}
final String min = query.getQueryStringParam("min");
if (min != null && !min.isEmpty()) {
try {
meta.setMin(Float.parseFloat(min));
} catch (NumberFormatException nfe) {
throw new BadRequestException("Unable to parse 'min' value");
}
}
return meta;
}
/**
* Parses a query string "m=metric{tagk1=tagv1,...}" type query and returns
* a tsuid.
* @param data_query The query we're building
* @throws BadRequestException if we are unable to parse the query or it is
* missing components
* @todo - make this asynchronous
*/
private String getTSUIDForMetric(final String query_string, TSDB tsdb) {
if (query_string == null || query_string.isEmpty()) {
throw new BadRequestException("The query string was empty");
}
// m is of the following forms:
// metric[{tag=value,...}]
// where the parts in square brackets `[' .. `]' are optional.
final HashMap<String, String> tags = new HashMap<String, String>();
String metric = null;
try {
metric = Tags.parseWithMetric(query_string, tags);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e);
}
// sort the UIDs on tagk values
final ByteMap<byte[]> tag_uids = new ByteMap<byte[]>();
for (final Entry<String, String> pair : tags.entrySet()) {
tag_uids.put(tsdb.getUID(UniqueIdType.TAGK, pair.getKey()),
tsdb.getUID(UniqueIdType.TAGV, pair.getValue()));
}
// Byte Buffer to generate TSUID, pre allocated to the size of the TSUID
final ByteArrayOutputStream buf = new ByteArrayOutputStream(
TSDB.metrics_width() + tag_uids.size() *
(TSDB.tagk_width() + TSDB.tagv_width()));
try {
buf.write(tsdb.getUID(UniqueIdType.METRIC, metric));
for (final Entry<byte[], byte[]> uids: tag_uids.entrySet()) {
buf.write(uids.getKey());
buf.write(uids.getValue());
}
} catch (IOException e) {
throw new BadRequestException(e);
}
final String tsuid = UniqueId.uidToString(buf.toByteArray());
return tsuid;
}
}