/*
* Copyright © 2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.metrics.store.upgrade;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.DatasetManagementException;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Scanner;
import co.cask.cdap.api.metrics.MetricStore;
import co.cask.cdap.api.metrics.MetricType;
import co.cask.cdap.api.metrics.MetricValues;
import co.cask.cdap.common.ServiceUnavailableException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.data2.datafabric.DefaultDatasetNamespace;
import co.cask.cdap.data2.datafabric.dataset.DatasetsUtil;
import co.cask.cdap.data2.dataset2.DatasetFramework;
import co.cask.cdap.data2.dataset2.DatasetNamespace;
import co.cask.cdap.data2.dataset2.lib.table.MetricsTable;
import co.cask.cdap.data2.dataset2.lib.table.hbase.MetricHBaseTableUtil;
import co.cask.cdap.data2.dataset2.lib.table.hbase.MetricHBaseTableUtil.Version;
import co.cask.cdap.data2.dataset2.lib.timeseries.EntityTable;
import co.cask.cdap.data2.util.hbase.HBaseTableUtil;
import co.cask.cdap.metrics.store.DefaultMetricStore;
import co.cask.cdap.metrics.store.MetricDatasetFactory;
import co.cask.cdap.proto.Id;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Migration for metrics data from 2.6 to 2.8
*/
public class MetricsDataMigrator {
private static final Logger LOG = LoggerFactory.getLogger(MetricsDataMigrator.class);
private static final String TYPE = "type";
private static final List<String> scopes = ImmutableList.of("system", "user");
private static final Map<String, List<String>> typeToTagNameMapping =
ImmutableMap.<String, List<String>>builder()
.put("f", ImmutableList.of(Constants.Metrics.Tag.APP,
TYPE,
Constants.Metrics.Tag.FLOW,
Constants.Metrics.Tag.FLOWLET,
Constants.Metrics.Tag.INSTANCE_ID))
.put("b", ImmutableList.of(Constants.Metrics.Tag.APP,
TYPE,
Constants.Metrics.Tag.MAPREDUCE,
Constants.Metrics.Tag.MR_TASK_TYPE,
Constants.Metrics.Tag.INSTANCE_ID))
.put("s", ImmutableList.of(Constants.Metrics.Tag.APP,
TYPE,
Constants.Metrics.Tag.SPARK,
Constants.Metrics.Tag.INSTANCE_ID))
.put("u", ImmutableList.of(Constants.Metrics.Tag.APP,
TYPE,
Constants.Metrics.Tag.SERVICE,
Constants.Metrics.Tag.HANDLER,
Constants.Metrics.Tag.INSTANCE_ID))
.put("w", ImmutableList.of(Constants.Metrics.Tag.APP,
TYPE,
Constants.Metrics.Tag.WORKFLOW,
Constants.Metrics.Tag.INSTANCE_ID))
.build();
private static final Map<String, String> metricNameToTagNameMapping =
ImmutableMap.<String, String>builder()
.put("store.reads", Constants.Metrics.Tag.DATASET)
.put("store.writes", Constants.Metrics.Tag.DATASET)
.put("store.ops", Constants.Metrics.Tag.DATASET)
.put("store.bytes", Constants.Metrics.Tag.DATASET)
.put("dataset.store.reads", Constants.Metrics.Tag.DATASET)
.put("dataset.store.writes", Constants.Metrics.Tag.DATASET)
.put("dataset.store.ops", Constants.Metrics.Tag.DATASET)
.put("dataset.store.bytes", Constants.Metrics.Tag.DATASET)
.put("dataset.size.mb", Constants.Metrics.Tag.DATASET)
.put("collect.events", Constants.Metrics.Tag.STREAM)
.put("collect.bytes", Constants.Metrics.Tag.STREAM)
.put("process.tuples.read", Constants.Metrics.Tag.FLOWLET_QUEUE)
.put("process.events.in", Constants.Metrics.Tag.FLOWLET_QUEUE)
.put("process.events.out", Constants.Metrics.Tag.FLOWLET_QUEUE)
.put("process.events.processed", Constants.Metrics.Tag.FLOWLET_QUEUE)
.build();
private static final Map<String, Map<String, String>> mapOldSystemContextToNew =
ImmutableMap.<String, Map<String, String>>of(
"transactions", ImmutableMap.of(Constants.Metrics.Tag.NAMESPACE, Id.Namespace.SYSTEM.getId(),
Constants.Metrics.Tag.COMPONENT, "transactions"),
"-", ImmutableMap.of(Constants.Metrics.Tag.NAMESPACE, Id.Namespace.SYSTEM.getId()),
"gateway", ImmutableMap.of(Constants.Metrics.Tag.NAMESPACE, Id.Namespace.SYSTEM.getId(),
Constants.Metrics.Tag.COMPONENT, Constants.Gateway.METRICS_CONTEXT,
Constants.Metrics.Tag.HANDLER, Constants.Gateway.STREAM_HANDLER_NAME)
);
private final DatasetFramework dsFramework;
private final MetricStore aggMetricStore;
private final String entityTableName;
private final String metricsTableNamePrefix;
private final String metricsTableName;
private final CConfiguration cConf;
private final Configuration hConf;
public MetricsDataMigrator(final CConfiguration cConf, final Configuration hConf, final DatasetFramework dsFramework,
MetricDatasetFactory factory) {
this.dsFramework = dsFramework;
this.entityTableName = cConf.get(Constants.Metrics.ENTITY_TABLE_NAME,
UpgradeMetricsConstants.DEFAULT_ENTITY_TABLE_NAME);
this.metricsTableNamePrefix = cConf.get(Constants.Metrics.METRICS_TABLE_PREFIX,
UpgradeMetricsConstants.DEFAULT_METRICS_TABLE_PREFIX);
this.metricsTableName = metricsTableNamePrefix + ".agg";
aggMetricStore = new DefaultMetricStore(factory, new int[]{Integer.MAX_VALUE});
this.cConf = cConf;
this.hConf = hConf;
}
public void migrateMetricsTables(HBaseTableUtil hBaseTableUtil,
boolean keepOldData) throws DataMigrationException {
Version cdapVersion = findMetricsTableVersion(new MetricHBaseTableUtil(hBaseTableUtil));
if (cdapVersion == Version.VERSION_2_6_OR_LOWER) {
migrateMetricsTableFromVersion26(cdapVersion);
} else if (cdapVersion == Version.VERSION_2_7) {
migrateMetricsTableFromVersion27(cdapVersion);
} else {
System.out.println("Unsupported version" + cdapVersion);
return;
}
if (!keepOldData) {
System.out.println("Performing cleanup of old metrics tables");
cleanUpOldTables(cdapVersion);
}
}
@Nullable
private Version findMetricsTableVersion(MetricHBaseTableUtil metricHBaseTableUtil) {
// Figure out what is the latest working version of CDAP
// 1) if latest is 2.8.x - nothing to do, if "pre-2.8", proceed to next step.
// 2) find a most recent metrics table: start by looking for 2.7, then 2.6
// 3) if we find 2.7 - we will migrate data from 2.7 table, if not - migrate data from 2.6 metrics table
// todo - use UpgradeTool to figure out if version is 2.8.x, return if it is 2.8.x
String tableName27 = cConf.get(Constants.Metrics.METRICS_TABLE_PREFIX,
UpgradeMetricsConstants.DEFAULT_METRICS_TABLE_PREFIX) + ".agg";
// versions older than 2.7, has two metrics table, identified by system and user prefix
String tableName26 = "system." + tableName27;
DefaultDatasetNamespace defaultDatasetNamespace = new DefaultDatasetNamespace(cConf);
String metricsEntityTable26 = defaultDatasetNamespace.namespace(Id.Namespace.SYSTEM, tableName26);
String metricsEntityTable27 = defaultDatasetNamespace.namespace(Id.Namespace.SYSTEM, tableName27);
Version version = null;
try {
HBaseAdmin hAdmin = new HBaseAdmin(hConf);
for (HTableDescriptor desc : hAdmin.listTables()) {
if (desc.getNameAsString().equals(metricsEntityTable27)) {
System.out.println("Matched HBase Table Name For Migration " + desc.getNameAsString());
version = metricHBaseTableUtil.getVersion(desc);
version = verifyVersion(Version.VERSION_2_7, version);
if (version == null) {
return null;
}
break;
}
if (desc.getNameAsString().equals(metricsEntityTable26)) {
System.out.println("Matched HBase Table Name For Migration " + desc.getNameAsString());
version = metricHBaseTableUtil.getVersion(desc);
version = verifyVersion(Version.VERSION_2_6_OR_LOWER, version);
if (version == null) {
return null;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return version;
}
private Version verifyVersion(Version expected, Version actual) {
if (expected != actual) {
System.out.println("Version detected based on table name does not match table configuration");
return null;
}
return actual;
}
private void migrateMetricsTableFromVersion26(Version version) throws DataMigrationException {
for (String scope : scopes) {
EntityTable entityTable = new EntityTable(getOrCreateMetricsTable(String.format("%s.%s", scope, entityTableName),
DatasetProperties.EMPTY));
String scopedMetricsTableName = String.format("%s.%s", scope, metricsTableName);
MetricsTable metricsTable = getOrCreateMetricsTable(scopedMetricsTableName, DatasetProperties.EMPTY);
System.out.println("Migrating Metrics Data from table : " + scopedMetricsTableName);
migrateMetricsData(entityTable, metricsTable, scope, version);
}
}
public void cleanUpOldTables(Version version) throws DataMigrationException {
Set<String> tablesToDelete = Sets.newHashSet();
if (version == Version.VERSION_2_6_OR_LOWER) {
List<String> scopes = ImmutableList.of("system", "user");
// add user and system - entity tables , aggregates table and time series table to the list
for (String scope : scopes) {
addTableNamesToDelete(tablesToDelete, cConf, scope, ImmutableList.of(1));
}
}
if (version == Version.VERSION_2_7) {
addTableNamesToDelete(tablesToDelete, cConf, null, ImmutableList.of(1, 60, 3600));
}
System.out.println("Deleting Tables : " + tablesToDelete);
deleteTables(hConf, tablesToDelete);
}
private void deleteTables(Configuration hConf, Set<String> tablesToDelete) throws DataMigrationException {
HBaseAdmin hAdmin = null;
try {
hAdmin = new HBaseAdmin(hConf);
for (HTableDescriptor desc : hAdmin.listTables()) {
if (tablesToDelete.contains(desc.getNameAsString())) {
// disable the table
hAdmin.disableTable(desc.getName());
// delete the table
hAdmin.deleteTable(desc.getName());
}
}
} catch (Exception e) {
LOG.error("Exception while trying to delete old metrics tables", e);
throw new DataMigrationException("Failed deleting old metrics tables");
}
}
private String addNamespace(DatasetNamespace dsNamespace, String tableName) {
return addNamespace(dsNamespace, null , tableName);
}
private String addNamespace(DatasetNamespace dsNamespace, String scope, String tableName) {
tableName = scope == null ? tableName : scope + "." + tableName;
return dsNamespace.namespace(Id.Namespace.SYSTEM, tableName);
}
private void addTableNamesToDelete(Set<String> tablesToDelete, CConfiguration cConf,
String scope, List<Integer> resolutions) {
DefaultDatasetNamespace defaultDatasetNamespace = new DefaultDatasetNamespace(cConf);
tablesToDelete.add(addNamespace(defaultDatasetNamespace, scope, entityTableName));
// add aggregates table
tablesToDelete.add(addNamespace(defaultDatasetNamespace, scope, metricsTableName));
// add timeseries tables
for (int resolution : resolutions) {
tablesToDelete.add(addNamespace(defaultDatasetNamespace, scope, metricsTableNamePrefix + ".ts." + resolution));
}
}
private void migrateMetricsTableFromVersion27(Version version) throws DataMigrationException {
EntityTable entityTable = new EntityTable(getOrCreateMetricsTable(entityTableName, DatasetProperties.EMPTY));
MetricsTable metricsTable = getOrCreateMetricsTable(metricsTableName, DatasetProperties.EMPTY);
System.out.println("Migrating Metrics Data from table : " + metricsTableName);
migrateMetricsData(entityTable, metricsTable, null, version);
}
private void migrateMetricsData(EntityTable entityTable, MetricsTable metricsTable, String scope,
Version version) {
MetricsEntityCodec codec = getEntityCodec(entityTable);
int idSize = getIdSize(version);
Row row;
long rowCount = 0;
try {
Scanner scanner = metricsTable.scan(null, null, null);
while ((row = scanner.next()) != null) {
byte[] rowKey = row.getRow();
int offset = 0;
String context = codec.decode(MetricsEntityType.CONTEXT, rowKey, offset, idSize);
context = getContextBasedOnVersion(context, version);
offset += codec.getEncodedSize(MetricsEntityType.CONTEXT, idSize);
String metricName = codec.decode(MetricsEntityType.METRIC, rowKey, offset, idSize);
offset += codec.getEncodedSize(MetricsEntityType.METRIC, idSize);
scope = getScopeBasedOnVersion(scope, metricName, version);
metricName = getMetricNameBasedOnVersion(metricName, version);
String runId = codec.decode(MetricsEntityType.RUN, rowKey, offset, idSize);
parseAndAddNewMetricValue(scope, context, metricName, runId, row.getColumns());
rowCount++;
printStatus(rowCount);
}
System.out.println("Migrated " + rowCount + " records");
} catch (Exception e) {
LOG.warn("Exception during data-transfer in aggregates table", e);
//no-op
}
}
private void printStatus(long rowCount) {
if (rowCount % 10000 == 0) {
System.out.println("Migrated " + rowCount + " records.");
}
}
private MetricsEntityCodec getEntityCodec(EntityTable table) {
return new MetricsEntityCodec(table, UpgradeMetricsConstants.DEFAULT_CONTEXT_DEPTH,
UpgradeMetricsConstants.DEFAULT_METRIC_DEPTH,
UpgradeMetricsConstants.DEFAULT_TAG_DEPTH);
}
// todo: batch writing metrics to a store, see MetricStore.add(Collection<MetricValue>)
private void addMetrics(Map<byte[], byte[]> columns, String metricName,
Map<String, String> tagMap, String metricTagType) throws Exception {
for (Map.Entry<byte[], byte[]> entry : columns.entrySet()) {
String tagValue = Bytes.toString(entry.getKey());
if (metricTagType != null) {
if (tagValue.equals(UpgradeMetricsConstants.EMPTY_TAG)) {
continue;
} else {
long value = Bytes.toLong(entry.getValue());
Map<String, String> tagValues = Maps.newHashMap(tagMap);
tagValues.put(Constants.Metrics.Tag.NAMESPACE, Id.Namespace.DEFAULT.getId());
tagValues.put(metricTagType, tagValue);
addMetricValueToMetricStore(tagValues, metricName, 0, value, MetricType.COUNTER);
}
} else {
addMetricValueToMetricStore(tagMap, metricName, 0, Bytes.toLong(entry.getValue()),
MetricType.COUNTER);
}
}
}
private void parseAndAddNewMetricValue(String scope, String context, String metricName, String runId,
Map<byte[], byte[]> columns) throws Exception {
List<String> contextParts = Lists.newArrayList(Splitter.on(".").split(context));
Map<String, String> tagMap = Maps.newHashMap();
tagMap.put(Constants.Metrics.Tag.SCOPE, scope);
if (runId != null) {
tagMap.put(Constants.Metrics.Tag.RUN_ID, runId);
}
Map<String, String> systemMap = null;
if (contextParts.size() > 0) {
systemMap = mapOldSystemContextToNew.get(contextParts.get(0));
}
String tagKey = metricNameToTagNameMapping.get(metricName);
if (systemMap != null) {
tagMap.putAll(systemMap);
} else if (contextParts.size() > 1) {
// application metrics
List<String> targetTagList = typeToTagNameMapping.get(contextParts.get(1));
if (targetTagList != null) {
populateApplicationTags(tagMap, contextParts, targetTagList, context);
} else {
// service metrics , we are skipping them as they were not exposed for querying before
LOG.trace("Skipping context : {}", context);
return;
}
} else {
System.out.println(String.format("Unexpected metric context %s.", context));
}
LOG.trace("Adding metrics - tagMap : {} - context : {} - metricName : {} and tagKey : {}", tagMap, context,
metricName, tagKey);
addMetrics(columns, metricName, tagMap, tagKey);
}
private void populateApplicationTags(Map<String, String> tagMap, List<String> contextParts,
List<String> targetTagList, String context) {
tagMap.put(Constants.Metrics.Tag.NAMESPACE, Id.Namespace.DEFAULT.getId());
for (int i = 0; i < contextParts.size(); i++) {
if (i == targetTagList.size()) {
LOG.trace(" Context longer than targetTagList" + context);
break;
}
if (targetTagList.get(i).equals(TYPE)) {
continue;
}
tagMap.put(targetTagList.get(i), contextParts.get(i));
}
}
// constructs MetricValue based on parameters passed and adds the MetricValue to MetricStore.
private void addMetricValueToMetricStore(Map<String, String> tags, String metricName,
int timeStamp, long value, MetricType counter) throws Exception {
aggMetricStore.add(new MetricValues(tags, metricName, timeStamp, value, counter));
}
private MetricsTable getOrCreateMetricsTable(String tableName, DatasetProperties empty)
throws DataMigrationException {
MetricsTable table = null;
// for default namespace, we have to provide the complete table name.
tableName = "system." + tableName;
// metrics tables are in the system namespace
Id.DatasetInstance metricsDatasetInstanceId = Id.DatasetInstance.from(Id.Namespace.DEFAULT, tableName);
try {
table = DatasetsUtil.getOrCreateDataset(dsFramework, metricsDatasetInstanceId,
MetricsTable.class.getName(), empty, null, null);
} catch (DatasetManagementException | ServiceUnavailableException e) {
String msg = String.format("Cannot access or create table %s.", tableName) + " " + e.getMessage();
LOG.warn(msg);
throw new DataMigrationException(msg);
} catch (IOException e) {
String msg = String.format("Exception while creating table %s", tableName);
LOG.error(msg, e);
throw new DataMigrationException(msg);
}
return table;
}
public void cleanupDestinationTables() throws DataMigrationException {
System.out.println("Cleaning up destination tables");
String rootPrefix = cConf.get(Constants.Dataset.TABLE_PREFIX) + "_";
String destEntityTableName = cConf.get(Constants.Metrics.ENTITY_TABLE_NAME,
Constants.Metrics.DEFAULT_ENTITY_TABLE_NAME);
destEntityTableName = getTableName(rootPrefix, Id.DatasetInstance.from(Id.Namespace.SYSTEM, destEntityTableName));
String destMetricsTablePrefix = cConf.get(Constants.Metrics.METRICS_TABLE_PREFIX,
Constants.Metrics.DEFAULT_METRIC_TABLE_PREFIX);
destMetricsTablePrefix = getTableName(rootPrefix, Id.DatasetInstance.from(
Id.Namespace.SYSTEM, destMetricsTablePrefix));
try {
HBaseAdmin hAdmin = new HBaseAdmin(hConf);
for (HTableDescriptor desc : hAdmin.listTables()) {
if (desc.getNameAsString().equals(destEntityTableName) ||
desc.getNameAsString().startsWith(destMetricsTablePrefix)) {
System.out.println(String.format("Deleting table %s before upgrade", desc.getNameAsString()));
//disable the table
hAdmin.disableTable(desc.getName());
//delete the table
hAdmin.deleteTable(desc.getName());
}
}
} catch (Exception e) {
LOG.error("Exception during cleanup of destination tables " + e);
throw new DataMigrationException("Failed Cleaning up destination tables");
}
}
private String getTableName(String rootPrefix, Id.DatasetInstance instance) {
return rootPrefix + instance.getNamespaceId() + ":" + instance.getId();
}
private String getMetricNameBasedOnVersion(String metricName, Version version) {
if (version == Version.VERSION_2_6_OR_LOWER) {
return metricName;
} else {
// metric name has scope prefix, lets remove the scope prefix and return
return metricName.substring(metricName.indexOf(".") + 1);
}
}
private int getIdSize(Version version) {
if (version == Version.VERSION_2_6_OR_LOWER) {
// we use 2 bytes in 2.6
return 2;
} else {
// we increased the size to 3 bytes from 2.7
return 3;
}
}
private String getContextBasedOnVersion(String context, Version version) {
if (version == Version.VERSION_2_6_OR_LOWER) {
return context;
} else {
// skip namespace - some metrics are emitted in system namespace though they have app-name, dataset name, etc
// so lets skip and figure out manually
return context.substring(context.indexOf(".") + 1);
}
}
private String getScopeBasedOnVersion(String scope, String metricName, Version version) {
if (version == Version.VERSION_2_6_OR_LOWER) {
return scope;
} else {
// metric name has scope prefix, lets split that
return metricName.substring(0, metricName.indexOf("."));
}
}
}