From 98b30abe02083ebfa90c35a0a951db5a853c154f Mon Sep 17 00:00:00 2001 From: yatanokarasu Date: Tue, 23 Jan 2018 23:54:16 +0900 Subject: [PATCH] For beta release Signed-off-by: yatanokarasu --- src/main/java/io/sniffer4j/LogBroker.java | 33 ++- src/main/java/io/sniffer4j/Options.java | 218 +++++++++++++++--- src/main/java/io/sniffer4j/Premain.java | 66 ++---- .../io/sniffer4j/Sniffer4jTransformer.java | 14 +- 4 files changed, 248 insertions(+), 83 deletions(-) diff --git a/src/main/java/io/sniffer4j/LogBroker.java b/src/main/java/io/sniffer4j/LogBroker.java index b1b5771..fa7fae8 100644 --- a/src/main/java/io/sniffer4j/LogBroker.java +++ b/src/main/java/io/sniffer4j/LogBroker.java @@ -32,7 +32,6 @@ import java.io.PrintWriter; import java.io.UncheckedIOException; import java.nio.file.Files; -import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; @@ -70,13 +69,17 @@ public static LogBroker instance() { /** * @param aThread An instance of {@link Thread} that called this method + * @param className Name of class which called this method + * @param methodName Name of method which called this method * @param begin An instant at the beginning of method execution * @param end An instant at the end of method execution */ - public void submit(final Thread aThread, final Instant begin, final Instant end) { + public void submit(final Thread aThread, final String className, final String methodName, final Instant begin, final Instant end) { final Record aRecord = new Record(); aRecord.threadName = aThread.getName(); aRecord.threadId = aThread.getId(); + aRecord.className = className; + aRecord.methodName = methodName; aRecord.begin = begin; aRecord.end = end; @@ -147,7 +150,7 @@ public void run() { private void startConsumerThread() { this.consumer.execute(() -> { - try (final BufferedWriter buffer = Files.newBufferedWriter(Paths.get("d:", "sniffer4j.log"), CREATE, TRUNCATE_EXISTING); + try (final BufferedWriter buffer = Files.newBufferedWriter(Options.LOGFILE.value(), CREATE, TRUNCATE_EXISTING); final PrintWriter writer = new PrintWriter(buffer)) { writer.println(csvFileHeader()); @@ -172,16 +175,20 @@ private static final class Record { private Instant end; + private String className; + + private String methodName; + private long threadId; private String threadName; - - + + private String begin() { return toLocalDateTime(this.begin); } - - + + private String end() { return toLocalDateTime(this.end); } @@ -192,6 +199,16 @@ private String duration() { } + private String className() { + return this.className; + } + + + private String methodName() { + return this.methodName; + } + + private String threadId() { return String.valueOf(this.threadId); } @@ -215,6 +232,8 @@ public String toString() { return new StringJoiner(",") .add(threadName()) .add(threadId()) + .add(className()) + .add(methodName()) .add(begin()) .add(end()) .add(duration()) diff --git a/src/main/java/io/sniffer4j/Options.java b/src/main/java/io/sniffer4j/Options.java index ceb6ff9..05bbe1b 100644 --- a/src/main/java/io/sniffer4j/Options.java +++ b/src/main/java/io/sniffer4j/Options.java @@ -24,61 +24,227 @@ package io.sniffer4j; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.regex.Pattern; /** + * Defines options used by Sniffer4j. * + *

When instanciate this class, the following arguments is mandatory: + *

+ *
defaultValue
+ *
Default value of the option
+ * + *
converter
+ *
{@link Function} to convert from String to Parameterized type
+ *
+ * + *

Also, you can specify the following arguments as optional: + *

+ *
prehook
+ *
{@link UnaryOperator} for converting new value to desired format like + * "*" -> ".*" in Regex
+ *
composer
+ *
{@link BiFunction} for composing or replacing default value and new value
+ *
*/ class Options { - static final Options> PATTERNS = new Options<>( - Pattern.compile("^(javax?|jdk|(com\\.|)(sun|oracle))").asPredicate().negate(), - (defaultHandler, newValue) -> defaultHandler.and(Pattern.compile(newValue).asPredicate()), - s -> s.replace(";", "|") - ); + static final Options LOGFILE = Options. builder() + .defaultValue(Paths.get(".", "sniffer4j.log")) + .converter(v -> { + // @formatter:off + final Path logPath = Paths.get(v); + final Path parentDir = logPath.getParent(); + + try { + Files.createDirectories(parentDir); + Files.createFile(logPath); + } catch (final IOException cause) { + throw new UncheckedIOException(cause); + } + + return logPath; + })// @formatter:on + .build(); + + static final Options> PACKAGES = Options.> builder() + .defaultValue(Pattern.compile("^(javax?|jdk|(com/|)(sun|oracle)|io/sniffer4j).*").asPredicate()) + .converter(v -> Pattern.compile(v).asPredicate()) + .withOptional() + .prehook(v -> v.replace(";", "|").replace(".", "/").replaceAll("*", Pattern.quote("*"))) + .composer((o, n) -> o.or(n.negate())) + .build(); + + @SuppressWarnings("boxing") + static final Options INTERVAL = new IntValueOptions(Integer.MIN_VALUE); + + @SuppressWarnings("boxing") + static final Options THRESHOLD = new IntValueOptions(Integer.MIN_VALUE); - private final L defaultHandler; + static final Options NULL = new NullOptions(); - private final BiFunction updateHandler; - - private Function hook; + private final BiFunction composer; - private L handler; + private final Function converter; + private final Function prehook; - private Options(final L defaultHandler, final BiFunction updateHandler) { - this(defaultHandler, updateHandler, Function.identity()); + private L value; + + + private Options(final L defaultValue, final Function converter) { + this(defaultValue, converter, UnaryOperator.identity(), (o, n) -> n); } - - - private Options(final L defaultHandler, final BiFunction updateHandler, final Function hook) { - this.defaultHandler = defaultHandler; - this.updateHandler = updateHandler; - this.hook = hook; + + + private Options(final L defaultValue, final Function converter, final UnaryOperator prehook, final BiFunction composer) { + this.value = defaultValue; + this.converter = converter; + this.prehook = prehook; + this.composer = composer; } - static Options valueOf(final String name) { + private static MandatoryValueBuilder builder() { + return new MandatoryValueBuilder<>(); + } + + + static Options of(final String name) { switch (name.toUpperCase()) { - case "PATTERNS": - return PATTERNS; + // @formatter:off + case "PACKAGES": return PACKAGES; + case "THRESHOLD": return THRESHOLD; + case "LOGFILE": return LOGFILE; + // @formatter:on default: - throw new IllegalArgumentException("No constant " + name + "."); + System.err.println("No option: " + name + "."); + return NULL; + } + } + + + void value(final String newValue) { + try { + L converted = this.converter.apply(this.prehook.apply(newValue)); + this.value = this.composer.apply(this.value, converted); + } catch (final Throwable cause) { + throw new IllegalArgumentException(cause); + } + } + + + L value() { + return this.value; + } + + + private static final class IntValueOptions extends Options { + + private IntValueOptions(final Integer defaultValue) { + super(defaultValue, Integer::valueOf); + } + + } + + + private static final class MandatoryValueBuilder { + + private Function converter; + + private L defaultValue; + + + private Options build() { + validate(); + + return new Options<>(this.defaultValue, this.converter); } + + + private MandatoryValueBuilder converter(final Function converter) { + this.converter = converter; + return this; + } + + + private MandatoryValueBuilder defaultValue(final L defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + + private void validate() { + if (Objects.isNull(this.defaultValue) || Objects.isNull(this.converter)) { + throw new IllegalStateException(); + } + } + + + private OptionalValueBuilder withOptional() { + validate(); + + return new OptionalValueBuilder<>(this.defaultValue, this.converter); + } + } - L handler() { - return this.handler; + private static final class NullOptions extends Options { + + private NullOptions() { + super(null, null); + } + } - void update(final String value) { - this.handler = this.updateHandler.apply(this.defaultHandler, this.hook.apply(value)); + private static final class OptionalValueBuilder { + + private final L defaultValue; + + private final Function converter; + + private UnaryOperator prehook; + + private BiFunction composer; + + + private OptionalValueBuilder(final L defaultValue, final Function converter) { + this.defaultValue = defaultValue; + this.converter = converter; + this.prehook = UnaryOperator.identity(); + this.composer = (o, n) -> n; + } + + + private Options build() { + return new Options<>(this.defaultValue, this.converter, this.prehook, this.composer); + } + + + private OptionalValueBuilder composer(final BiFunction composer) { + this.composer = composer; + return this; + } + + + private OptionalValueBuilder prehook(final UnaryOperator prehook) { + this.prehook = prehook; + return this; + } + } } diff --git a/src/main/java/io/sniffer4j/Premain.java b/src/main/java/io/sniffer4j/Premain.java index 6b63046..940445f 100644 --- a/src/main/java/io/sniffer4j/Premain.java +++ b/src/main/java/io/sniffer4j/Premain.java @@ -25,20 +25,24 @@ import java.lang.instrument.Instrumentation; -import java.util.AbstractMap; import java.util.Arrays; -import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; +import java.util.stream.Stream; /** + * Injects instrumentation code into byte-codes of methods included specified packages at the time + * of JVM startup, see details for {@link java.lang.instrument.Instrumentation}. * + *

By default, Sniffer4j does NOT inject instrumentation code under the following packages: + * {@link java}.*, {@link jdk}.*, {@link com.sun}.*, {@link sun}.*, {@link oracle}.* and {@link io.sniffer4j}.*. */ public final class Premain { /** + * + * * @param agentArguments Java Agent arguments * @param instrumentation An instrumentation instance for pre-main */ @@ -52,57 +56,31 @@ public static void premain(final String agentArguments, final Instrumentation in private static void parseArguments(final String agentArguments) { - Arrays.asList(agentArguments.split(",", -1)).stream() - .map(toKeyValuePair()) - .filter(isValidOption()) - .map(keyToOptions()) - .forEach(setOptionValue()); - } + if (Objects.isNull(agentArguments)) { + return; + } + separateByComma(agentArguments).forEach(setOptionValue()); + } - private static Function> toKeyValuePair() { - return v -> { - final String[] pair = v.split("=", -1); - return new AbstractMap.SimpleImmutableEntry<>(pair[0], pair[1]); - }; + private static Stream separateByComma(final String agentArguments) { + return Arrays.asList(agentArguments.split(",", -1)).stream(); } - private static Predicate> isValidOption() { - return p -> { - final String name = p.getKey(); + private static Consumer setOptionValue() { + return arg -> { + final String[] pair = arg.split("=", -1); + final String name = pair[0]; + final String value = pair[1]; - // validate option name try { - Options.valueOf(name.toUpperCase()); + Options.of(name).value(value); } catch (@SuppressWarnings("unused") final IllegalArgumentException ignored) { - System.err.println("[WARNING] " + name + ": unknown option."); - - return false; - } - - // validate option value - if (p.getValue().isEmpty()) { - System.err.println("[WARNING] " + name + ": invalid option."); - - return false; + System.err.printf("Unexpected value '%s' of %s, use default value.", value, name); } - - return true; }; } - - @SuppressWarnings("rawtypes") - private static Function, Map.Entry> keyToOptions() { - return v -> new AbstractMap.SimpleImmutableEntry<>(Options.valueOf(v.getKey()), v.getValue()); - } - - - @SuppressWarnings("rawtypes") - private static Consumer> setOptionValue() { - return e -> e.getKey().update(e.getValue()); - } - } diff --git a/src/main/java/io/sniffer4j/Sniffer4jTransformer.java b/src/main/java/io/sniffer4j/Sniffer4jTransformer.java index e452a7c..8e8a4df 100644 --- a/src/main/java/io/sniffer4j/Sniffer4jTransformer.java +++ b/src/main/java/io/sniffer4j/Sniffer4jTransformer.java @@ -74,7 +74,7 @@ public byte[] transform( private boolean isNotSubjectToBeInjectedSniffer(final String fullyClassname) { - return !Options.PATTERNS.handler().test(fullyClassname); + return Options.PACKAGES.value().test(fullyClassname); } @@ -102,11 +102,13 @@ private void injectPerMethod(final String className, final CtMethod aMethod) thr aMethod.insertBefore(beginVariableName + " = java.time.Instant.now();"); aMethod.insertAfter(endVariableName + " = java.time.Instant.now();"); - - aMethod.insertAfter("System.out.println(\"" + className + "#" + methodName - + "()[Thread=\" + Thread.currentThread().getName() + \":\" + Thread.currentThread().getId() + \"]: \" + java.time.Duration.between(" - //+ beginVariableName + ", java.time.Instant.now()).toMillis());"); - + beginVariableName + ", " + endVariableName + ").toMillis());"); + + aMethod.insertAfter("io.sniffer4j.LogBroker.instance().submit(" + + "Thread.currentThread()," + + "\"" + className + "\"," + + "\"" + methodName + "\"," + + beginVariableName + "," + + endVariableName + ");"); } }