diff --git a/scylla-apiclient/pom.xml b/scylla-apiclient/pom.xml index 184d215..272692b 100644 --- a/scylla-apiclient/pom.xml +++ b/scylla-apiclient/pom.xml @@ -60,6 +60,21 @@ activation 1.1 + + com.fasterxml.jackson.core + jackson-annotations + 2.9.9 + + + com.fasterxml.jackson.core + jackson-databind + 2.9.9 + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + 2.9.9 + diff --git a/scylla-apiclient/src/main/java/com/scylladb/jmx/api/APIClient.java b/scylla-apiclient/src/main/java/com/scylladb/jmx/api/APIClient.java index 86ae4f4..dc6903e 100644 --- a/scylla-apiclient/src/main/java/com/scylladb/jmx/api/APIClient.java +++ b/scylla-apiclient/src/main/java/com/scylladb/jmx/api/APIClient.java @@ -38,6 +38,7 @@ import javax.ws.rs.core.Response; import org.glassfish.jersey.client.ClientConfig; +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.scylladb.jmx.utils.SnapshotDetailsTabularData; public class APIClient { @@ -78,9 +79,12 @@ public class APIClient { private static final Logger logger = Logger.getLogger(APIClient.class.getName()); private final APIConfig config; + private final ClientConfig clientConfig; public APIClient(APIConfig config) { this.config = config; + this.clientConfig = new ClientConfig(); + clientConfig.register(new JacksonJaxbJsonProvider()); } private String getBaseUrl() { @@ -88,7 +92,7 @@ public class APIClient { } public Invocation.Builder get(String path, MultivaluedMap queryParams) { - Client client = ClientBuilder.newClient(new ClientConfig()); + Client client = ClientBuilder.newClient(clientConfig); WebTarget webTarget = client.target(getBaseUrl()).path(path); if (queryParams != null) { for (Entry> qp : queryParams.entrySet()) { diff --git a/src/main/java/com/scylladb/jmx/utils/DateXmlAdapter.java b/src/main/java/com/scylladb/jmx/utils/DateXmlAdapter.java new file mode 100644 index 0000000..572cec9 --- /dev/null +++ b/src/main/java/com/scylladb/jmx/utils/DateXmlAdapter.java @@ -0,0 +1,19 @@ +package com.scylladb.jmx.utils; + +import java.time.Instant; +import java.util.Date; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class DateXmlAdapter extends XmlAdapter { + @Override + public String marshal(Date v) throws Exception { + return Instant.ofEpochMilli(v.getTime()).toString(); + } + + @Override + public Date unmarshal(String v) throws Exception { + return new Date(Instant.parse(v).toEpochMilli()); + } + +} \ No newline at end of file diff --git a/src/main/java/org/apache/cassandra/service/PerTableSSTableInfo.java b/src/main/java/org/apache/cassandra/service/PerTableSSTableInfo.java new file mode 100644 index 0000000..bfcd4fd --- /dev/null +++ b/src/main/java/org/apache/cassandra/service/PerTableSSTableInfo.java @@ -0,0 +1,66 @@ +package org.apache.cassandra.service; + +import static com.sun.jmx.mbeanserver.MXBeanMappingFactory.DEFAULT; + +import java.io.InvalidObjectException; +import java.util.List; + +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.OpenDataException; +import javax.xml.bind.annotation.XmlRootElement; + +import com.sun.jmx.mbeanserver.MXBeanMapping; + +@SuppressWarnings("restriction") +@XmlRootElement +public class PerTableSSTableInfo { + private static final MXBeanMapping mxBeanMapping; + + static { + try { + mxBeanMapping = DEFAULT.mappingForType(PerTableSSTableInfo.class, DEFAULT); + } catch (OpenDataException e) { + throw new RuntimeException(e); + } + } + + private String keyspace; + private List sstables; + private String table; + + public String getTable() { + return table; + } + + public void setTable(String table) { + this.table = table; + } + + public String getKeyspace() { + return keyspace; + } + + public void setKeyspace(String keyspace) { + this.keyspace = keyspace; + } + + public List getSSTables() { + return sstables; + } + + public void setSSTableInfos(List sstableInfos) { + this.sstables = sstableInfos; + } + + public CompositeData toCompositeData() { + try { + return (CompositeData) mxBeanMapping.toOpenValue(this); + } catch (OpenDataException e) { + throw new Error(e); // should not reach. + } + } + + public static PerTableSSTableInfo from(CompositeData data) throws InvalidObjectException { + return (PerTableSSTableInfo) mxBeanMapping.fromOpenValue(data); + } +} diff --git a/src/main/java/org/apache/cassandra/service/SSTableInfo.java b/src/main/java/org/apache/cassandra/service/SSTableInfo.java new file mode 100644 index 0000000..17287c8 --- /dev/null +++ b/src/main/java/org/apache/cassandra/service/SSTableInfo.java @@ -0,0 +1,143 @@ +package org.apache.cassandra.service; + +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.scylladb.jmx.utils.DateXmlAdapter; + +public class SSTableInfo { + private long size; + + @JsonProperty("data_size") + private long dataSize; + + @JsonProperty("index_size") + private long indexSize; + + @JsonProperty("filter_size") + private long filterSize; + + @XmlJavaTypeAdapter(type = Date.class, value = DateXmlAdapter.class) + private Date timestamp; + + private long generation; + + private long level; + + private String version; + + private Map properties; + + public void setProperties(Map properties) { + this.properties = properties; + } + + @JsonIgnore + private Map> extendedProperties; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public long getDataSize() { + return dataSize; + } + + public void setDataSize(long dataSize) { + this.dataSize = dataSize; + } + + public long getIndexSize() { + return indexSize; + } + + public void setIndexSize(long indexSize) { + this.indexSize = indexSize; + } + + public long getFilterSize() { + return filterSize; + } + + public void setFilterSize(long filterSize) { + this.filterSize = filterSize; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public long getGeneration() { + return generation; + } + + public void setGeneration(long generation) { + this.generation = generation; + } + + public long getLevel() { + return level; + } + + public void setLevel(long level) { + this.level = level; + } + + public Map getProperties() { + return properties; + } + + public Map> getExtendedProperties() { + return extendedProperties; + } + + public void setExtendedProperties(Map> extendedProperties) { + this.extendedProperties = extendedProperties; + } + + @JsonProperty("extended_properties") + private void unpackNested(List> properties) { + Map> result = new HashMap>(); + + for (Map map : properties) { + Object name = map.get("group"); + if (name != null) { + Map dst = new HashMap<>(); + List value = (List) map.get("attributes"); + for (Object v : value) { + Map subMap = (Map) v; + dst.put(String.valueOf(subMap.get("key")), String.valueOf(subMap.get("value"))); + } + result.put(String.valueOf(name), dst); + } else { + for (Map.Entry e : map.entrySet()) { + result.put(e.getKey(), Collections.singletonMap(String.valueOf(e.getValue()), "")); + } + } + } + extendedProperties = result; + } +} diff --git a/src/main/java/org/apache/cassandra/service/StorageService.java b/src/main/java/org/apache/cassandra/service/StorageService.java index bda4ad9..0dd0c27 100644 --- a/src/main/java/org/apache/cassandra/service/StorageService.java +++ b/src/main/java/org/apache/cassandra/service/StorageService.java @@ -42,6 +42,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.json.JsonArray; import javax.json.JsonObject; @@ -52,14 +53,14 @@ import javax.management.NotificationBroadcaster; import javax.management.NotificationBroadcasterSupport; import javax.management.NotificationFilter; import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; import javax.management.openmbean.TabularData; +import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import org.apache.cassandra.metrics.StorageMetrics; import org.apache.cassandra.repair.RepairParallelism; -import org.glassfish.jersey.client.ClientConfig; -import org.glassfish.jersey.client.ClientProperties; import com.google.common.base.Joiner; import com.scylladb.jmx.api.APIClient; @@ -492,12 +493,12 @@ public class StorageService extends MetricsMBean implements StorageServiceMBean, @Override public void takeSnapshot(String tag, Map options, String... keyspaceNames) throws IOException { log(" takeSnapshot(String tag, String... keyspaceNames) throws IOException"); - MultivaluedMap queryParams = new MultivaluedHashMap(); + MultivaluedMap queryParams = new MultivaluedHashMap(); APIClient.set_query_param(queryParams, "tag", tag); - + if (keyspaceNames.length == 1 && keyspaceNames[0].indexOf('.') != -1) { String[] parts = keyspaceNames[0].split("\\."); - keyspaceNames = new String[] { parts[0] }; + keyspaceNames = new String[] { parts[0] }; APIClient.set_query_param(queryParams, "cf", parts[1]); } APIClient.set_query_param(queryParams, "kn", APIClient.join(keyspaceNames)); @@ -603,8 +604,9 @@ public class StorageService extends MetricsMBean implements StorageServiceMBean, client.post("/storage_service/keyspace_compaction/" + keyspaceName, queryParams); } - @Override - public void forceKeyspaceCompactionForTokenRange(String keyspaceName, String startToken, String endToken, String... tableNames) throws IOException, ExecutionException, InterruptedException { + @Override + public void forceKeyspaceCompactionForTokenRange(String keyspaceName, String startToken, String endToken, + String... tableNames) throws IOException, ExecutionException, InterruptedException { // TODO: actually handle token ranges. forceKeyspaceCompaction(keyspaceName, tableNames); } @@ -869,7 +871,7 @@ public class StorageService extends MetricsMBean implements StorageServiceMBean, @Deprecated public int forceRepairAsync(String keyspace, boolean isSequential, Collection dataCenters, Collection hosts, boolean primaryRange, boolean repairedAt, String... columnFamilies) - throws IOException { + throws IOException { log(" forceRepairAsync(String keyspace, boolean isSequential, Collection dataCenters, Collection hosts, boolean primaryRange, boolean repairedAt, String... columnFamilies) throws IOException"); return repairRangeAsync(null, null, keyspace, isSequential, dataCenters, hosts, primaryRange, repairedAt, columnFamilies); @@ -1298,12 +1300,19 @@ public class StorageService extends MetricsMBean implements StorageServiceMBean, } /** - * Same as {@link #rebuild(String)}, but only for specified keyspace and ranges. + * Same as {@link #rebuild(String)}, but only for specified keyspace and + * ranges. * - * @param sourceDc Name of DC from which to select sources for streaming or null to pick any node - * @param keyspace Name of the keyspace which to rebuild or null to rebuild all keyspaces. - * @param tokens Range of tokens to rebuild or null to rebuild all token ranges. In the format of: - * "(start_token_1,end_token_1],(start_token_2,end_token_2],...(start_token_n,end_token_n]" + * @param sourceDc + * Name of DC from which to select sources for streaming or null + * to pick any node + * @param keyspace + * Name of the keyspace which to rebuild or null to rebuild all + * keyspaces. + * @param tokens + * Range of tokens to rebuild or null to rebuild all token + * ranges. In the format of: + * "(start_token_1,end_token_1],(start_token_2,end_token_2],...(start_token_n,end_token_n]" */ @Override public void rebuild(String sourceDc, String keyspace, String tokens, String specificSources) { @@ -1704,4 +1713,29 @@ public class StorageService extends MetricsMBean implements StorageServiceMBean, log(" resumeBootstrap"); return false; } + + @Override + public List getSSTableInfo(String keyspace, String table) { + if (keyspace == null && table != null) { + throw new IllegalArgumentException("Missing keyspace name"); + } + MultivaluedMap queryParams = null; + + if (keyspace != null) { + queryParams = new MultivaluedHashMap(); + queryParams.add("keyspace", keyspace); + } + if (table != null) { + queryParams.add("cf", table); + } + + return client.get("/storage_service/sstable_info", queryParams) + .get(new GenericType>() { + }).stream().map((i) -> i.toCompositeData()).collect(Collectors.toList()); + } + + @Override + public List getSSTableInfo() { + return getSSTableInfo(null, null); + } } diff --git a/src/main/java/org/apache/cassandra/service/StorageServiceMBean.java b/src/main/java/org/apache/cassandra/service/StorageServiceMBean.java index 05f1c30..f775364 100644 --- a/src/main/java/org/apache/cassandra/service/StorageServiceMBean.java +++ b/src/main/java/org/apache/cassandra/service/StorageServiceMBean.java @@ -35,6 +35,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import javax.management.NotificationEmitter; +import javax.management.openmbean.CompositeData; import javax.management.openmbean.TabularData; public interface StorageServiceMBean extends NotificationEmitter { @@ -872,4 +873,8 @@ public interface StorageServiceMBean extends NotificationEmitter { * Sets the hinted handoff throttle in kb per second, per delivery thread. */ public boolean resumeBootstrap(); + + public List getSSTableInfo(String keyspace, String table); + + public List getSSTableInfo(); }