blob: e9171794df58fa6baf879d149b5d1e7481113fe3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cassandra.tools;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanFeatureInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.airlift.airline.Arguments;
import io.airlift.airline.Cli;
import io.airlift.airline.Command;
import io.airlift.airline.Help;
import io.airlift.airline.HelpOption;
import io.airlift.airline.Option;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
public class JMXTool
{
private static final List<String> METRIC_PACKAGES = Arrays.asList("org.apache.cassandra.metrics",
"org.apache.cassandra.db",
"org.apache.cassandra.hints",
"org.apache.cassandra.internal",
"org.apache.cassandra.net",
"org.apache.cassandra.request",
"org.apache.cassandra.service");
private static final Comparator<MBeanOperationInfo> OPERATOR_COMPARATOR = (a, b) -> {
int rc = a.getName().compareTo(b.getName());
if (rc != 0)
return rc;
String[] aSig = Stream.of(a.getSignature()).map(MBeanParameterInfo::getName).toArray(String[]::new);
String[] bSig = Stream.of(b.getSignature()).map(MBeanParameterInfo::getName).toArray(String[]::new);
rc = Integer.compare(aSig.length, bSig.length);
if (rc != 0)
return rc;
for (int i = 0; i < aSig.length; i++)
{
rc = aSig[i].compareTo(bSig[i]);
if (rc != 0)
return rc;
}
return rc;
};
@Command(name = "dump", description = "Dump the Apache Cassandra JMX objects and metadata.")
public static final class Dump implements Callable<Void>
{
@Inject
private HelpOption helpOption;
@Option(title = "url", name = { "-u", "--url" }, description = "JMX url to target")
private String targetUrl = "service:jmx:rmi:///jndi/rmi://localhost:7199/jmxrmi";
@Option(title = "format", name = { "-f", "--format" }, description = "What format to dump content as; supported values are console (default), json, and yaml")
private Format format = Format.console;
public Void call() throws Exception
{
Map<String, Info> map = load(new JMXServiceURL(targetUrl));
format.dump(System.out, map);
return null;
}
public enum Format
{
console
{
void dump(OutputStream output, Map<String, Info> map)
{
@SuppressWarnings("resource")
// output should be released by caller
PrintStream out = toPrintStream(output);
for (Map.Entry<String, Info> e : map.entrySet())
{
String name = e.getKey();
Info info = e.getValue();
out.println(name);
out.println("\tAttributes");
Stream.of(info.attributes).forEach(a -> printRow(out, a.name, a.type, a.access));
out.println("\tOperations");
Stream.of(info.operations).forEach(o -> {
String args = Stream.of(o.parameters)
.map(i -> i.name + ": " + i.type)
.collect(Collectors.joining(",", "(", ")"));
printRow(out, o.name, o.returnType, args);
});
}
}
},
json
{
void dump(OutputStream output, Map<String, Info> map) throws IOException
{
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(output, map);
}
},
yaml
{
void dump(OutputStream output, Map<String, Info> map) throws IOException
{
Representer representer = new Representer();
representer.addClassTag(Info.class, Tag.MAP); // avoid the auto added tag
Yaml yaml = new Yaml(representer);
yaml.dump(map, new OutputStreamWriter(output));
}
};
private static PrintStream toPrintStream(OutputStream output)
{
try
{
return output instanceof PrintStream ? (PrintStream) output : new PrintStream(output, true, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
throw new AssertionError(e); // utf-8 is a required charset for the JVM
}
}
abstract void dump(OutputStream output, Map<String, Info> map) throws IOException;
}
}
@Command(name = "diff", description = "Diff two jmx dump files and report their differences")
public static final class Diff implements Callable<Void>
{
@Inject
private HelpOption helpOption;
@Arguments(title = "files", usage = "<left> <right>", description = "Files to diff")
private List<File> files;
@Option(title = "format", name = { "-f", "--format" }, description = "What format the files are in; only support json and yaml as format")
private Format format = Format.yaml;
@Option(title = "ignore left", name = { "--ignore-missing-on-left" }, description = "Ignore results missing on the left")
private boolean ignoreMissingLeft;
@Option(title = "ignore right", name = { "--ignore-missing-on-right" }, description = "Ignore results missing on the right")
private boolean ignoreMissingRight;
@Option(title = "exclude objects", name = "--exclude-object", description
= "Ignores processing specific objects. " +
"Each usage should take a single object, " +
"but can use this flag multiple times.")
private List<CliPattern> excludeObjects = new ArrayList<>();
@Option(title = "exclude attributes", name = "--exclude-attribute", description
= "Ignores processing specific attributes. " +
"Each usage should take a single attribute, " +
"but can use this flag multiple times.")
private List<CliPattern> excludeAttributes = new ArrayList<>();
@Option(title = "exclude operations", name = "--exclude-operation", description
= "Ignores processing specific operations. " +
"Each usage should take a single operation, " +
"but can use this flag multiple times.")
private List<CliPattern> excludeOperations = new ArrayList<>();
public Void call() throws Exception
{
Preconditions.checkArgument(files.size() == 2, "files requires 2 arguments but given %s", files);
Map<String, Info> left;
Map<String, Info> right;
try (FileInputStream leftStream = new FileInputStream(files.get(0));
FileInputStream rightStream = new FileInputStream(files.get(1)))
{
left = format.load(leftStream);
right = format.load(rightStream);
}
diff(left, right);
return null;
}
private void diff(Map<String, Info> left, Map<String, Info> right)
{
DiffResult<String> objectNames = diff(left.keySet(), right.keySet(), name -> {
for (CliPattern p : excludeObjects)
{
if (p.pattern.matcher(name).matches())
return false;
}
return true;
});
if (!ignoreMissingRight && !objectNames.notInRight.isEmpty())
{
System.out.println("Objects not in right:");
printSet(0, objectNames.notInRight);
}
if (!ignoreMissingLeft && !objectNames.notInLeft.isEmpty())
{
System.out.println("Objects not in left: ");
printSet(0, objectNames.notInLeft);
}
Runnable printHeader = new Runnable()
{
boolean printedHeader = false;
public void run()
{
if (!printedHeader)
{
System.out.println("Difference found in attribute or operation");
printedHeader = true;
}
}
};
for (String key : objectNames.shared)
{
Info leftInfo = left.get(key);
Info rightInfo = right.get(key);
DiffResult<Attribute> attributes = diff(leftInfo.attributeSet(), rightInfo.attributeSet(), attribute -> {
for (CliPattern p : excludeAttributes)
{
if (p.pattern.matcher(attribute.name).matches())
return false;
}
return true;
});
if (!ignoreMissingRight && !attributes.notInRight.isEmpty())
{
printHeader.run();
System.out.println(key + "\tattribute not in right:");
printSet(1, attributes.notInRight);
}
if (!ignoreMissingLeft && !attributes.notInLeft.isEmpty())
{
printHeader.run();
System.out.println(key + "\tattribute not in left:");
printSet(1, attributes.notInLeft);
}
DiffResult<Operation> operations = diff(leftInfo.operationSet(), rightInfo.operationSet(), operation -> {
for (CliPattern p : excludeOperations)
{
if (p.pattern.matcher(operation.name).matches())
return false;
}
return true;
});
if (!ignoreMissingRight && !operations.notInRight.isEmpty())
{
printHeader.run();
System.out.println(key + "\toperation not in right:");
printSet(1, operations.notInRight, (sb, o) ->
rightInfo.getOperation(o.name).ifPresent(match ->
sb.append("\t").append("similar in right: ").append(match)));
}
if (!ignoreMissingLeft && !operations.notInLeft.isEmpty())
{
printHeader.run();
System.out.println(key + "\toperation not in left:");
printSet(1, operations.notInLeft, (sb, o) ->
leftInfo.getOperation(o.name).ifPresent(match ->
sb.append("\t").append("similar in left: ").append(match)));
}
}
}
private static <T extends Comparable<T>> void printSet(int indent, Set<T> set)
{
printSet(indent, set, (i1, i2) -> {});
}
private static <T extends Comparable<T>> void printSet(int indent, Set<T> set, BiConsumer<StringBuilder, T> fn)
{
StringBuilder sb = new StringBuilder();
for (T t : new TreeSet<>(set))
{
sb.setLength(0);
for (int i = 0; i < indent; i++)
sb.append('\t');
sb.append(t);
fn.accept(sb, t);
System.out.println(sb);
}
}
private static <T> DiffResult<T> diff(Set<T> left, Set<T> right, Predicate<T> fn)
{
left = Sets.filter(left, fn);
right = Sets.filter(right, fn);
return new DiffResult<>(Sets.difference(left, right), Sets.difference(right, left), Sets.intersection(left, right));
}
private static final class DiffResult<T>
{
private final SetView<T> notInRight;
private final SetView<T> notInLeft;
private final SetView<T> shared;
private DiffResult(SetView<T> notInRight, SetView<T> notInLeft, SetView<T> shared)
{
this.notInRight = notInRight;
this.notInLeft = notInLeft;
this.shared = shared;
}
}
public enum Format
{
json
{
Map<String, Info> load(InputStream input) throws IOException
{
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(input, new TypeReference<Map<String, Info>>() {});
}
},
yaml
{
Map<String, Info> load(InputStream input) throws IOException
{
Yaml yaml = new Yaml(new CustomConstructor());
return (Map<String, Info>) yaml.load(input);
}
};
abstract Map<String, Info> load(InputStream input) throws IOException;
}
private static final class CustomConstructor extends Constructor
{
private static final String ROOT = "__root__";
private static final TypeDescription INFO_TYPE = new TypeDescription(Info.class);
public CustomConstructor()
{
this.rootTag = new Tag(ROOT);
this.addTypeDescription(INFO_TYPE);
}
protected Object constructObject(Node node)
{
if (ROOT.equals(node.getTag().getValue()) && node instanceof MappingNode)
{
MappingNode mn = (MappingNode) node;
return mn.getValue().stream()
.collect(Collectors.toMap(t -> super.constructObject(t.getKeyNode()),
t -> {
Node child = t.getValueNode();
child.setType(INFO_TYPE.getType());
return super.constructObject(child);
}));
}
else
{
return super.constructObject(node);
}
}
}
}
private static Map<String, Info> load(JMXServiceURL url) throws IOException, MalformedObjectNameException, IntrospectionException, InstanceNotFoundException, ReflectionException
{
try (JMXConnector jmxc = JMXConnectorFactory.connect(url, null))
{
MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
Map<String, Info> map = new TreeMap<>();
for (String pkg : new TreeSet<>(METRIC_PACKAGES))
{
Set<ObjectName> metricNames = new TreeSet<>(mbsc.queryNames(new ObjectName(pkg + ":*"), null));
for (ObjectName name : metricNames)
{
if (mbsc.isRegistered(name))
{
MBeanInfo info = mbsc.getMBeanInfo(name);
map.put(name.toString(), Info.from(info));
}
}
}
return map;
}
}
private static String getAccess(MBeanAttributeInfo a)
{
String access;
if (a.isReadable())
{
if (a.isWritable())
access = "read/write";
else
access = "read-only";
}
else if (a.isWritable())
access = "write-only";
else
access = "no-access";
return access;
}
private static String normalizeType(String type)
{
switch (type)
{
case "[Z":
return "boolean[]";
case "[B":
return "byte[]";
case "[S":
return "short[]";
case "[I":
return "int[]";
case "[J":
return "long[]";
case "[F":
return "float[]";
case "[D":
return "double[]";
case "[C":
return "char[]";
}
if (type.startsWith("[L"))
return type.substring(2, type.length() - 1) + "[]"; // -1 will remove the ; at the end
return type;
}
private static final StringBuilder ROW_BUFFER = new StringBuilder();
private static void printRow(PrintStream out, String... args)
{
ROW_BUFFER.setLength(0);
ROW_BUFFER.append("\t\t");
for (String a : args)
ROW_BUFFER.append(a).append("\t");
out.println(ROW_BUFFER);
}
public static final class Info
{
private Attribute[] attributes;
private Operation[] operations;
public Info()
{
}
public Info(Attribute[] attributes, Operation[] operations)
{
this.attributes = attributes;
this.operations = operations;
}
private static Info from(MBeanInfo info)
{
Attribute[] attributes = Stream.of(info.getAttributes())
.sorted(Comparator.comparing(MBeanFeatureInfo::getName))
.map(Attribute::from)
.toArray(Attribute[]::new);
Operation[] operations = Stream.of(info.getOperations())
.sorted(OPERATOR_COMPARATOR)
.map(Operation::from)
.toArray(Operation[]::new);
return new Info(attributes, operations);
}
public Attribute[] getAttributes()
{
return attributes;
}
public void setAttributes(Attribute[] attributes)
{
this.attributes = attributes;
}
public Set<String> attributeNames()
{
return Stream.of(attributes).map(a -> a.name).collect(Collectors.toSet());
}
public Set<Attribute> attributeSet()
{
return new HashSet<>(Arrays.asList(attributes));
}
public Operation[] getOperations()
{
return operations;
}
public void setOperations(Operation[] operations)
{
this.operations = operations;
}
public Set<String> operationNames()
{
return Stream.of(operations).map(o -> o.name).collect(Collectors.toSet());
}
public Set<Operation> operationSet()
{
return new HashSet<>(Arrays.asList(operations));
}
public Optional<Attribute> getAttribute(String name)
{
return Stream.of(attributes).filter(a -> a.name.equals(name)).findFirst();
}
public Attribute getAttributePresent(String name)
{
return getAttribute(name).orElseThrow(AssertionError::new);
}
public Optional<Operation> getOperation(String name)
{
return Stream.of(operations).filter(o -> o.name.equals(name)).findFirst();
}
public Operation getOperationPresent(String name)
{
return getOperation(name).orElseThrow(AssertionError::new);
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Info info = (Info) o;
return Arrays.equals(attributes, info.attributes) &&
Arrays.equals(operations, info.operations);
}
@Override
public int hashCode()
{
int result = Arrays.hashCode(attributes);
result = 31 * result + Arrays.hashCode(operations);
return result;
}
}
public static final class Attribute implements Comparable<Attribute>
{
private String name;
private String type;
private String access;
public Attribute()
{
}
public Attribute(String name, String type, String access)
{
this.name = name;
this.type = type;
this.access = access;
}
private static Attribute from(MBeanAttributeInfo info)
{
return new Attribute(info.getName(), normalizeType(info.getType()), JMXTool.getAccess(info));
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
public String getAccess()
{
return access;
}
public void setAccess(String access)
{
this.access = access;
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Attribute attribute = (Attribute) o;
return Objects.equals(name, attribute.name) &&
Objects.equals(type, attribute.type);
}
public int hashCode()
{
return Objects.hash(name, type);
}
public String toString()
{
return name + ": " + type;
}
public int compareTo(Attribute o)
{
int rc = name.compareTo(o.name);
if (rc != 0)
return rc;
return type.compareTo(o.type);
}
}
public static final class Operation implements Comparable<Operation>
{
private String name;
private Parameter[] parameters;
private String returnType;
public Operation()
{
}
public Operation(String name, Parameter[] parameters, String returnType)
{
this.name = name;
this.parameters = parameters;
this.returnType = returnType;
}
private static Operation from(MBeanOperationInfo info)
{
Parameter[] params = Stream.of(info.getSignature()).map(Parameter::from).toArray(Parameter[]::new);
return new Operation(info.getName(), params, normalizeType(info.getReturnType()));
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public Parameter[] getParameters()
{
return parameters;
}
public void setParameters(Parameter[] parameters)
{
this.parameters = parameters;
}
public List<String> parameterTypes()
{
return Stream.of(parameters).map(p -> p.type).collect(Collectors.toList());
}
public String getReturnType()
{
return returnType;
}
public void setReturnType(String returnType)
{
this.returnType = returnType;
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Operation operation = (Operation) o;
return Objects.equals(name, operation.name) &&
Arrays.equals(parameters, operation.parameters) &&
Objects.equals(returnType, operation.returnType);
}
public int hashCode()
{
int result = Objects.hash(name, returnType);
result = 31 * result + Arrays.hashCode(parameters);
return result;
}
public String toString()
{
return name + Stream.of(parameters).map(Parameter::toString).collect(Collectors.joining(", ", "(", ")")) + ": " + returnType;
}
public int compareTo(Operation o)
{
int rc = name.compareTo(o.name);
if (rc != 0)
return rc;
rc = Integer.compare(parameters.length, o.parameters.length);
if (rc != 0)
return rc;
for (int i = 0; i < parameters.length; i++)
{
rc = parameters[i].type.compareTo(o.parameters[i].type);
if (rc != 0)
return rc;
}
return returnType.compareTo(o.returnType);
}
}
public static final class Parameter
{
private String name;
private String type;
public Parameter()
{
}
public Parameter(String name, String type)
{
this.name = name;
this.type = type;
}
private static Parameter from(MBeanParameterInfo info)
{
return new Parameter(info.getName(), normalizeType(info.getType()));
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Parameter parameter = (Parameter) o;
return Objects.equals(type, parameter.type);
}
public int hashCode()
{
return Objects.hash(type);
}
public String toString()
{
return name + ": " + type;
}
}
public static final class CliPattern
{
private final Pattern pattern;
public CliPattern(String pattern)
{
this.pattern = Pattern.compile(pattern);
}
}
public static void main(String[] args) throws Exception
{
Cli.CliBuilder<Callable<Void>> builder = Cli.builder("jmxtool");
builder.withDefaultCommand(Help.class);
builder.withCommands(Help.class, Dump.class, Diff.class);
Cli<Callable<Void>> parser = builder.build();
Callable<Void> command = parser.parse(args);
command.call();
}
}