├── metrics-agent-core ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── MANIFEST.MF │ │ │ └── logging.properties │ │ └── java │ │ │ └── com │ │ │ └── fleury │ │ │ └── metrics │ │ │ └── agent │ │ │ ├── reporter │ │ │ ├── MetricSystemProvider.java │ │ │ ├── MetricSystem.java │ │ │ ├── MetricSystemProviderFactory.java │ │ │ └── Reporter.java │ │ │ ├── transformer │ │ │ ├── visitors │ │ │ │ ├── injectors │ │ │ │ │ ├── Injector.java │ │ │ │ │ ├── CounterInjector.java │ │ │ │ │ ├── ExceptionCounterInjector.java │ │ │ │ │ ├── InjectorFactory.java │ │ │ │ │ ├── GaugeInjector.java │ │ │ │ │ ├── TimerInjector.java │ │ │ │ │ ├── TimedExceptionCountedInjector.java │ │ │ │ │ └── AbstractInjector.java │ │ │ │ ├── AnnotationClassVisitor.java │ │ │ │ ├── MetricClassVisitor.java │ │ │ │ ├── AnnotationMethodVisitor.java │ │ │ │ ├── RestrictedClassVisitor.java │ │ │ │ ├── MetricAnnotationAttributeVisitor.java │ │ │ │ └── MetricAdapter.java │ │ │ ├── util │ │ │ │ ├── CollectionUtil.java │ │ │ │ ├── OpCodeUtil.java │ │ │ │ └── AnnotationUtil.java │ │ │ ├── AnnotatedMetricClassTransformer.java │ │ │ └── ASMClassWriter.java │ │ │ ├── model │ │ │ ├── MetricType.java │ │ │ ├── LabelValidator.java │ │ │ ├── LabelUtil.java │ │ │ └── Metric.java │ │ │ ├── config │ │ │ ├── LoggerUtil.java │ │ │ ├── ArgParser.java │ │ │ └── Configuration.java │ │ │ ├── Agent.java │ │ │ └── introspector │ │ │ └── GenericClassIntrospector.java │ └── test │ │ ├── resources │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── com.fleury.metrics.agent.reporter.MetricSystemProvider │ │ └── config │ │ │ └── sample.yaml │ │ └── java │ │ └── com │ │ └── fleury │ │ └── metrics │ │ └── agent │ │ ├── reporter │ │ ├── TestMetricSystemProvider.java │ │ └── TestMetricSystem.java │ │ ├── config │ │ ├── ArgParserTest.java │ │ └── ConfigurationTest.java │ │ └── transformer │ │ └── visitors │ │ └── injectors │ │ ├── WhiteBlackListTest.java │ │ ├── SyntheticMethodsTest.java │ │ ├── TimedExceptionCountedInjectorTest.java │ │ ├── BaseMetricTest.java │ │ ├── ExceptionCounterInjectorTest.java │ │ ├── TimerInjectorTest.java │ │ ├── CounterInjectorTest.java │ │ ├── MixedInjectorTest.java │ │ ├── GaugeInjectorTest.java │ │ └── LabelsTest.java └── pom.xml ├── metrics-agent-dropwizard ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── com.fleury.metrics.agent.reporter.MetricSystemProvider │ │ └── java │ │ └── com │ │ └── fleury │ │ └── metrics │ │ └── agent │ │ └── reporter │ │ ├── DropwizardMetricSystemProvider.java │ │ └── DropwizardMetricSystem.java └── pom.xml ├── metrics-agent-prometheus ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── com.fleury.metrics.agent.reporter.MetricSystemProvider │ │ └── java │ │ └── com │ │ └── fleury │ │ └── metrics │ │ └── agent │ │ └── reporter │ │ ├── PrometheusMetricSystemProvider.java │ │ └── PrometheusMetricSystem.java └── pom.xml ├── .gitignore ├── metrics-agent-annotation ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── fleury │ └── metrics │ └── agent │ └── annotation │ ├── Timed.java │ ├── Counted.java │ ├── ExceptionCounted.java │ └── Gauged.java ├── example-configurations ├── jersey.yaml ├── dropwizard.yaml ├── tomcat.yaml └── hibernate.yaml ├── pom.xml ├── metrics-agent-dist └── pom.xml ├── LICENSE.md └── README.md /metrics-agent-core/src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Premain-Class: com.fleury.metrics.agent.Agent -------------------------------------------------------------------------------- /metrics-agent-core/src/test/resources/META-INF/services/com.fleury.metrics.agent.reporter.MetricSystemProvider: -------------------------------------------------------------------------------- 1 | com.fleury.metrics.agent.reporter.TestMetricSystemProvider -------------------------------------------------------------------------------- /metrics-agent-dropwizard/src/main/resources/META-INF/services/com.fleury.metrics.agent.reporter.MetricSystemProvider: -------------------------------------------------------------------------------- 1 | com.fleury.metrics.agent.reporter.DropwizardMetricSystemProvider -------------------------------------------------------------------------------- /metrics-agent-prometheus/src/main/resources/META-INF/services/com.fleury.metrics.agent.reporter.MetricSystemProvider: -------------------------------------------------------------------------------- 1 | com.fleury.metrics.agent.reporter.PrometheusMetricSystemProvider -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings/ 4 | .DS_Store 5 | nbactions.xml 6 | nb-configuration.xml 7 | target/ 8 | dependency-reduced-pom.xml 9 | .idea/ 10 | *.iml 11 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/MetricSystemProvider.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * 7 | * @author Will Fleury 8 | */ 9 | public interface MetricSystemProvider { 10 | 11 | MetricSystem createMetricSystem(Map configuration); 12 | } 13 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | 3 | .level=INFO 4 | 5 | java.util.logging.ConsoleHandler.level=INFO 6 | java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 7 | java.util.logging.SimpleFormatter.format = [%4$s] %1$tFT%1$tT %3$s%6$s %5$s%n 8 | 9 | #com.fleury.classX.level=INFO -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/Injector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | /** 4 | * 5 | * @author Will Fleury 6 | */ 7 | public interface Injector { 8 | 9 | public void injectAtMethodEnter(); 10 | 11 | public void injectAtVisitMaxs(int maxStack, int maxLocals); 12 | 13 | public void injectAtMethodExit(int opcode); 14 | } 15 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/reporter/TestMetricSystemProvider.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * 7 | * @author Will Fleury 8 | */ 9 | public class TestMetricSystemProvider implements MetricSystemProvider { 10 | 11 | @Override 12 | public MetricSystem createMetricSystem(Map configuration) { 13 | return new TestMetricSystem(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /metrics-agent-dropwizard/src/main/java/com/fleury/metrics/agent/reporter/DropwizardMetricSystemProvider.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * 7 | * @author Will Fleury 8 | */ 9 | public class DropwizardMetricSystemProvider implements MetricSystemProvider { 10 | 11 | @Override 12 | public MetricSystem createMetricSystem(Map configuration) { 13 | return new DropwizardMetricSystem(configuration); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /metrics-agent-prometheus/src/main/java/com/fleury/metrics/agent/reporter/PrometheusMetricSystemProvider.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * 7 | * @author Will Fleury 8 | */ 9 | public class PrometheusMetricSystemProvider implements MetricSystemProvider { 10 | 11 | @Override 12 | public MetricSystem createMetricSystem(Map configuration) { 13 | return new PrometheusMetricSystem(configuration); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/util/CollectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.util; 2 | 3 | import java.util.Collection; 4 | 5 | public class CollectionUtil { 6 | 7 | public static boolean isEmpty(Collection collection) { 8 | if (collection == null || collection.isEmpty()) { 9 | return true; 10 | } 11 | 12 | return false; 13 | } 14 | 15 | public static boolean isNotEmpty(Collection collection) { 16 | return !isEmpty(collection); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /metrics-agent-annotation/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | metrics-agent 7 | 0.0.6-SNAPSHOT 8 | 9 | metrics-agent-annotation 10 | metrics-agent-annotation 11 | jar 12 | -------------------------------------------------------------------------------- /metrics-agent-annotation/src/main/java/com/fleury/metrics/agent/annotation/Timed.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | @Documented 14 | @Retention(value = RetentionPolicy.RUNTIME) 15 | @Target(value = {ElementType.METHOD, ElementType.CONSTRUCTOR}) 16 | public @interface Timed { 17 | 18 | String name(); 19 | 20 | String[] labels() default {}; 21 | 22 | String doc() default ""; 23 | } 24 | -------------------------------------------------------------------------------- /metrics-agent-annotation/src/main/java/com/fleury/metrics/agent/annotation/Counted.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | @Documented 14 | @Retention(value = RetentionPolicy.RUNTIME) 15 | @Target(value = {ElementType.METHOD, ElementType.CONSTRUCTOR}) 16 | public @interface Counted { 17 | 18 | String name(); 19 | 20 | String[] labels() default {}; 21 | 22 | String doc() default ""; 23 | } 24 | -------------------------------------------------------------------------------- /metrics-agent-annotation/src/main/java/com/fleury/metrics/agent/annotation/ExceptionCounted.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | @Documented 14 | @Retention(value = RetentionPolicy.RUNTIME) 15 | @Target(value = {ElementType.METHOD, ElementType.CONSTRUCTOR}) 16 | public @interface ExceptionCounted { 17 | 18 | String name(); 19 | 20 | String[] labels() default {}; 21 | 22 | String doc() default ""; 23 | 24 | String[] include() default {}; 25 | } 26 | -------------------------------------------------------------------------------- /metrics-agent-annotation/src/main/java/com/fleury/metrics/agent/annotation/Gauged.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | @Documented 14 | @Retention(value = RetentionPolicy.RUNTIME) 15 | @Target(value = {ElementType.CONSTRUCTOR, ElementType.METHOD}) 16 | public @interface Gauged { 17 | 18 | enum mode { 19 | in_flight 20 | } 21 | 22 | String name(); 23 | 24 | mode mode() default mode.in_flight; 25 | 26 | String[] labels() default {}; 27 | 28 | String doc() default ""; 29 | } 30 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/AnnotationClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import com.fleury.metrics.agent.config.Configuration; 4 | import org.objectweb.asm.ClassVisitor; 5 | import org.objectweb.asm.MethodVisitor; 6 | 7 | 8 | /** 9 | * 10 | * @author Will Fleury 11 | */ 12 | public class AnnotationClassVisitor extends RestrictedClassVisitor { 13 | 14 | public AnnotationClassVisitor(ClassVisitor cv, Configuration config) { 15 | super(cv, config); 16 | } 17 | 18 | @Override 19 | public MethodVisitor visitAllowedMethod(MethodVisitor mv, int access, String name, String desc, String signature, String[] exceptions) { 20 | return new AnnotationMethodVisitor(mv, config, className, name, desc); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/MetricSystem.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | /** 4 | * 5 | * @author Will Fleury 6 | */ 7 | public interface MetricSystem { 8 | 9 | void registerGauge(String name, String[] labelNames, String doc); 10 | 11 | void registerCounter(String name, String[] labelNames, String doc); 12 | 13 | void registerTimer(String name, String[] labelNames, String doc); 14 | 15 | void recordCount(String name, String[] labelValues); 16 | 17 | void recordCount(String name, String[] labelValues, long n); 18 | 19 | void recordGaugeInc(String name, String[] labelValues); 20 | 21 | void recordGaugeDec(String name, String[] labelValues); 22 | 23 | void recordTime(String name, String[] labelValues, long duration); 24 | 25 | void startDefaultEndpoint(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/model/MetricType.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.model; 2 | 3 | import com.fleury.metrics.agent.annotation.Counted; 4 | import com.fleury.metrics.agent.annotation.ExceptionCounted; 5 | import com.fleury.metrics.agent.annotation.Gauged; 6 | import com.fleury.metrics.agent.annotation.Timed; 7 | import org.objectweb.asm.Type; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | public enum MetricType { 14 | 15 | Counted(Counted.class), 16 | Gauged(Gauged.class), 17 | Timed(Timed.class), 18 | ExceptionCounted(ExceptionCounted.class); 19 | 20 | private final Class annotation; 21 | private final String desc; 22 | 23 | MetricType(Class annotation) { 24 | this.annotation = annotation; 25 | this.desc = Type.getDescriptor(annotation); 26 | } 27 | 28 | public Class getAnnotation() { 29 | return annotation; 30 | } 31 | 32 | public String getDesc() { 33 | return desc; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/resources/config/sample.yaml: -------------------------------------------------------------------------------- 1 | 2 | imports: 3 | - com/fleury/sample/Engine 4 | - java/lang/Object 5 | - java/lang/String 6 | 7 | # key is class name, method name and method signature 8 | # {class}:{method}{signature} 9 | 10 | # labels are of the format 11 | # {name}:{value} 12 | # {value} can be a constant or it can be templated based on the method stack of 13 | # variables/fields accessible via objects on the stack 14 | 15 | metrics: 16 | # the import above for com/fleury/sample/Engine ensures I can use className alone here 17 | Engine.sampleMethod(I)J: 18 | - type: Counted 19 | name: count 20 | doc: trying to count 21 | labels: ['name1:value1', 'name2:value2'] 22 | - type: Timed 23 | name: timer 24 | doc: trying to time 25 | labels: ['name1:value1', 'name2:value2'] 26 | 27 | com/test/Special.sampleMethod(LString;)J: 28 | - type: Counted 29 | name: count 30 | doc: trying to count 31 | 32 | system: 33 | jvm: 34 | - gc 35 | - memory -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/config/LoggerUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | 4 | import com.fleury.metrics.agent.Agent; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.logging.LogManager; 8 | 9 | public class LoggerUtil { 10 | 11 | public static void initializeLogging(String resource) { 12 | InputStream in = null; 13 | try { 14 | in = Agent.class.getResourceAsStream(resource); 15 | 16 | if (in == null) { 17 | throw new NullPointerException("Logger configuration " + resource + " not found"); 18 | } 19 | 20 | LogManager.getLogManager().readConfiguration(in); 21 | } catch (Exception e) { 22 | throw new RuntimeException("Unable to initialize agent logging with config: " + resource, e); 23 | } finally { 24 | if (in != null) { 25 | try { 26 | in.close(); 27 | } catch (IOException ignored) { } 28 | } 29 | } 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/MetricClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import com.fleury.metrics.agent.config.Configuration; 4 | import com.fleury.metrics.agent.model.Metric; 5 | import java.util.List; 6 | import org.objectweb.asm.ClassVisitor; 7 | import org.objectweb.asm.MethodVisitor; 8 | import org.objectweb.asm.commons.JSRInlinerAdapter; 9 | 10 | /** 11 | * 12 | * @author Will Fleury 13 | */ 14 | public class MetricClassVisitor extends RestrictedClassVisitor { 15 | 16 | public MetricClassVisitor(ClassVisitor cv, Configuration config) { 17 | super(cv, config); 18 | } 19 | 20 | @Override 21 | public MethodVisitor visitAllowedMethod(MethodVisitor mv, int access, String name, String desc, String signature, String[] exceptions) { 22 | List metadata = config.findMetrics(className, name, desc); 23 | 24 | mv = new MetricAdapter(mv, className, access, name, desc, metadata); 25 | return new JSRInlinerAdapter(mv, access, name, desc, signature, exceptions); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/util/OpCodeUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.util; 2 | 3 | import static org.objectweb.asm.Opcodes.ICONST_0; 4 | import static org.objectweb.asm.Opcodes.ICONST_1; 5 | import static org.objectweb.asm.Opcodes.ICONST_2; 6 | import static org.objectweb.asm.Opcodes.ICONST_3; 7 | import static org.objectweb.asm.Opcodes.ICONST_4; 8 | import static org.objectweb.asm.Opcodes.ICONST_5; 9 | 10 | /** 11 | * 12 | * @author Will Fleury 13 | */ 14 | public class OpCodeUtil { 15 | 16 | public static int getIConstOpcodeForInteger(int val) { 17 | switch (val) { 18 | case 0: 19 | return ICONST_0; 20 | case 1: 21 | return ICONST_1; 22 | case 2: 23 | return ICONST_2; 24 | case 3: 25 | return ICONST_3; 26 | case 4: 27 | return ICONST_4; 28 | case 5: 29 | return ICONST_5; 30 | 31 | default: 32 | throw new RuntimeException("No ICONST_ for int " + val); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/Agent.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent; 2 | 3 | import static com.fleury.metrics.agent.config.LoggerUtil.initializeLogging; 4 | 5 | import com.fleury.metrics.agent.config.ArgParser; 6 | import com.fleury.metrics.agent.config.Configuration; 7 | import com.fleury.metrics.agent.reporter.MetricSystemProviderFactory; 8 | import com.fleury.metrics.agent.transformer.AnnotatedMetricClassTransformer; 9 | import java.lang.instrument.Instrumentation; 10 | 11 | /** 12 | * 13 | * @author Will Fleury 14 | */ 15 | public class Agent { 16 | 17 | public static void premain(String args, Instrumentation instrumentation) { 18 | 19 | ArgParser argParser = new ArgParser(args); 20 | 21 | initializeLogging(argParser.getLogConfigFilename()); 22 | 23 | Configuration config = Configuration.createConfig(argParser.getConfigFilename()); 24 | MetricSystemProviderFactory.INSTANCE.init(config.getSystem()); 25 | 26 | instrumentation.addTransformer( 27 | new AnnotatedMetricClassTransformer(config), 28 | instrumentation.isRetransformClassesSupported()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/config/ArgParser.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | /** 4 | * 5 | * @author Will Fleury 6 | */ 7 | public class ArgParser { 8 | 9 | private final String[] agentArgs; 10 | 11 | public ArgParser(String args) { 12 | this.agentArgs = args == null ? new String[] {} : args.split(","); 13 | } 14 | 15 | public String getConfigFilename() { 16 | if (agentArgs.length == 0) { 17 | return System.getProperty("agent-config"); 18 | } 19 | 20 | return getArg("agent-config"); 21 | } 22 | 23 | public String getLogConfigFilename() { 24 | if (agentArgs.length == 0) { 25 | return System.getProperty("log-config"); 26 | } 27 | 28 | String resource = getArg("log-config"); 29 | 30 | return resource == null ? "/logging.properties" : resource; 31 | } 32 | 33 | public String getArg(String key) { 34 | for (String arg : agentArgs) { 35 | if (arg.startsWith(key)) { 36 | return arg.replace(key + ":", ""); 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/config/ArgParserTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.core.Is.is; 5 | import static org.hamcrest.core.IsNull.nullValue; 6 | 7 | import org.junit.Test; 8 | 9 | public class ArgParserTest { 10 | 11 | @Test 12 | public void testAllConfigPresent() throws Exception { 13 | String args = "agent-config:agent.yaml,log-config:log.properties"; 14 | ArgParser parser = new ArgParser(args); 15 | 16 | assertThat(parser.getConfigFilename(), is("agent.yaml")); 17 | assertThat(parser.getLogConfigFilename(), is("log.properties")); 18 | } 19 | 20 | @Test 21 | public void testNoConfigPresent() throws Exception { 22 | ArgParser parser = new ArgParser(""); 23 | 24 | assertThat(parser.getConfigFilename(), is(nullValue())); 25 | assertThat(parser.getLogConfigFilename(), is("/logging.properties")); 26 | } 27 | 28 | @Test 29 | public void testOneConfigPresent() throws Exception { 30 | ArgParser parser = new ArgParser("agent-config:agent.yaml"); 31 | 32 | assertThat(parser.getConfigFilename(), is("agent.yaml")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/util/AnnotationUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.util; 2 | 3 | import static com.fleury.metrics.agent.model.MetricType.Counted; 4 | import static com.fleury.metrics.agent.model.MetricType.ExceptionCounted; 5 | import static com.fleury.metrics.agent.model.MetricType.Gauged; 6 | import static com.fleury.metrics.agent.model.MetricType.Timed; 7 | 8 | import com.fleury.metrics.agent.model.MetricType; 9 | 10 | /** 11 | * 12 | * @author Will Fleury 13 | */ 14 | public class AnnotationUtil { 15 | 16 | public static MetricType checkSignature(String desc) { 17 | if (isAnnotationPresent(desc, Counted)) { 18 | return Counted; 19 | } 20 | 21 | if (isAnnotationPresent(desc, Gauged)) { 22 | return Gauged; 23 | } 24 | 25 | if (isAnnotationPresent(desc, Timed)) { 26 | return Timed; 27 | } 28 | 29 | if (isAnnotationPresent(desc, ExceptionCounted)) { 30 | return ExceptionCounted; 31 | } 32 | 33 | return null; 34 | } 35 | 36 | public static boolean isAnnotationPresent(String desc, MetricType annotation) { 37 | return annotation.getDesc().equals(desc); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /example-configurations/jersey.yaml: -------------------------------------------------------------------------------- 1 | # Compatible & tested with Jersey 2.3 2 | # Should be compatible with 2.x range 3 | 4 | imports: 5 | - com/fleury/resources/HelloWorldResource 6 | - org/glassfish/jersey/server/ContainerRequest 7 | - org/glassfish/jersey/server/ContainerResponse 8 | - org/glassfish/jersey/message/internal/OutboundJaxrsResponse 9 | - org/glassfish/jersey/internal/util/collection/Value 10 | - org/glassfish/jersey/servlet/ServletContainer 11 | - javax/servlet/http/HttpServletRequest 12 | - javax/servlet/http/HttpServletResponse 13 | - java/util/Optional 14 | - java/lang/Object 15 | - java/lang/String 16 | - java/net/URI 17 | 18 | 19 | 20 | metrics: 21 | 22 | ServletContainer.service(LURI;LURI;LHttpServletRequest;LHttpServletResponse;)LValue;: 23 | - type: Timed 24 | name: resource_latency 25 | doc: Measuring http resource latencies 26 | labels: ['path:$1.path', 'method:$2.method'] 27 | 28 | - type: Gauged 29 | name: in_flight_requests 30 | mode: in_flight 31 | doc: Measuring in flight requests 32 | labels: ['path:$1.path', 'method:$2.method'] 33 | 34 | ContainerResponse.(LContainerRequest;LOutboundJaxrsResponse;)V: 35 | - type: Counted 36 | name: http_call_count 37 | doc: Http methods call counts 38 | labels: ['path:$0.requestUri.path', 'method:$0.method', 'status:$1.status'] 39 | 40 | 41 | system: 42 | jvm: 43 | - gc 44 | - memory 45 | - threads 46 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/CounterInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import org.objectweb.asm.Type; 5 | import org.objectweb.asm.commons.AdviceAdapter; 6 | 7 | /** 8 | * Transforms from 9 | * 10 | *
11 |  * public void someMethod() {
12 |  *     //original method code
13 |  * }
14 |  * 
15 | * 16 | * To 17 | * 18 | *
19 |  * public void someMethod() {
20 |  *     PrometheusMetricSystem.recordCount(COUNTER, labels);
21 |  *
22 |  *     //original method code
23 |  * }
24 |  * 
25 | * 26 | * @author Will Fleury 27 | */ 28 | public class CounterInjector extends AbstractInjector { 29 | 30 | private static final String METHOD = "recordCount"; 31 | private static final String SIGNATURE = Type.getMethodDescriptor( 32 | Type.VOID_TYPE, 33 | Type.getType(String.class), Type.getType(String[].class)); 34 | 35 | private final Metric metric; 36 | 37 | public CounterInjector(Metric metric, AdviceAdapter aa, Type[] argTypes, int access) { 38 | super(aa, argTypes, access); 39 | this.metric = metric; 40 | } 41 | 42 | @Override 43 | public void injectAtMethodEnter() { 44 | injectNameAndLabelToStack(metric); 45 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /metrics-agent-dropwizard/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | metrics-agent 7 | 0.0.6-SNAPSHOT 8 | 9 | metrics-agent-dropwizard 10 | metrics-agent-dropwizard 11 | jar 12 | 13 | 14 | 15 | ${project.groupId} 16 | metrics-agent-core 17 | ${project.version} 18 | 19 | 20 | 21 | io.dropwizard.metrics 22 | metrics-core 23 | ${dropwizard.metrics} 24 | 25 | 26 | 27 | io.dropwizard.metrics 28 | metrics-jvm 29 | ${dropwizard.metrics} 30 | 31 | 32 | 33 | 34 | 35 | dev-experts 36 | https://dl.bintray.com/devexperts/Maven/ 37 | 38 | 39 | -------------------------------------------------------------------------------- /example-configurations/dropwizard.yaml: -------------------------------------------------------------------------------- 1 | # As Dropwizard resources are Jersey Resources running on Jetty, we simply need to instrument the same as Jersey. 2 | # Tested on Dropwizard 1.x but should work with any Jersey 2.x release 3 | 4 | imports: 5 | - com/fleury/resources/HelloWorldResource 6 | - org/glassfish/jersey/server/ContainerRequest 7 | - org/glassfish/jersey/server/ContainerResponse 8 | - org/glassfish/jersey/message/internal/OutboundJaxrsResponse 9 | - org/glassfish/jersey/internal/util/collection/Value 10 | - org/glassfish/jersey/servlet/ServletContainer 11 | - javax/servlet/http/HttpServletRequest 12 | - javax/servlet/http/HttpServletResponse 13 | - java/util/Optional 14 | - java/lang/Object 15 | - java/lang/String 16 | - java/net/URI 17 | 18 | 19 | 20 | metrics: 21 | 22 | ServletContainer.service(LURI;LURI;LHttpServletRequest;LHttpServletResponse;)LValue;: 23 | - type: Timed 24 | name: resource_latency 25 | doc: Measuring http resource latencies 26 | labels: ['path:$1.path', 'method:$2.method'] 27 | 28 | - type: Gauged 29 | name: in_flight_requests 30 | mode: in_flight 31 | doc: Measuring in flight requests 32 | labels: ['path:$1.path', 'method:$2.method'] 33 | 34 | ContainerResponse.(LContainerRequest;LOutboundJaxrsResponse;)V: 35 | - type: Counted 36 | name: http_call_count 37 | doc: Http methods call counts 38 | labels: ['path:$0.requestUri.path', 'method:$0.method', 'status:$1.status'] 39 | 40 | 41 | system: 42 | jvm: 43 | - gc 44 | - memory 45 | - threads 46 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/AnnotationMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import static com.fleury.metrics.agent.transformer.util.AnnotationUtil.checkSignature; 4 | import static org.objectweb.asm.Opcodes.ASM5; 5 | 6 | import com.fleury.metrics.agent.config.Configuration; 7 | import com.fleury.metrics.agent.model.MetricType; 8 | import org.objectweb.asm.AnnotationVisitor; 9 | import org.objectweb.asm.MethodVisitor; 10 | 11 | 12 | /** 13 | * 14 | * @author Will Fleury 15 | */ 16 | public class AnnotationMethodVisitor extends MethodVisitor { 17 | 18 | private final Configuration config; 19 | private final String className; 20 | private final String methodName; 21 | private final String methodDesc; 22 | 23 | public AnnotationMethodVisitor(MethodVisitor mv, Configuration config, String className, String name, String desc) { 24 | super(ASM5, mv); 25 | 26 | this.config = config; 27 | this.className = className; 28 | this.methodName = name; 29 | this.methodDesc = desc; 30 | } 31 | 32 | @Override 33 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) { 34 | MetricType metricType = checkSignature(desc); 35 | 36 | if (metricType != null) { 37 | Configuration.Key key = new Configuration.Key(className, methodName, methodDesc); 38 | 39 | return new MetricAnnotationAttributeVisitor(super.visitAnnotation(desc, visible), metricType, config, key); 40 | } 41 | 42 | return super.visitAnnotation(desc, visible); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /metrics-agent-prometheus/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | metrics-agent 7 | 0.0.6-SNAPSHOT 8 | 9 | metrics-agent-prometheus 10 | jar 11 | 12 | 13 | 14 | ${project.groupId} 15 | metrics-agent-core 16 | ${project.version} 17 | 18 | 19 | 20 | io.prometheus 21 | simpleclient 22 | ${prometheus.version} 23 | 24 | 25 | 26 | io.prometheus 27 | simpleclient_hotspot 28 | ${prometheus.version} 29 | 30 | 31 | 32 | io.prometheus 33 | simpleclient_httpserver 34 | ${prometheus.version} 35 | 36 | 37 | 38 | org.ow2.asm 39 | asm-util 40 | ${asm.version} 41 | test 42 | 43 | 44 | 45 | org.ow2.asm 46 | asm-analysis 47 | ${asm.version} 48 | test 49 | 50 | 51 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/introspector/GenericClassIntrospector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.introspector; 2 | 3 | import static java.util.logging.Level.FINE; 4 | 5 | import java.beans.IntrospectionException; 6 | import java.beans.PropertyDescriptor; 7 | import java.lang.reflect.Method; 8 | import java.util.logging.Logger; 9 | import org.apache.commons.beanutils.BeanIntrospector; 10 | import org.apache.commons.beanutils.IntrospectionContext; 11 | 12 | public class GenericClassIntrospector implements BeanIntrospector { 13 | 14 | private static final Logger LOGGER = Logger.getLogger(GenericClassIntrospector.class.getName()); 15 | 16 | @Override 17 | public void introspect(IntrospectionContext icontext) { 18 | 19 | for (final Method m : icontext.getTargetClass().getMethods()) { 20 | 21 | if (isValidValueMethod(m)) { 22 | try { 23 | icontext.addPropertyDescriptor(new PropertyDescriptor(m.getName(), m, null)); 24 | } catch (final IntrospectionException e) { 25 | LOGGER.info("Error when creating PropertyDescriptor for " + m + "! Ignoring this property."); 26 | LOGGER.log(FINE, "Exception is:", e); 27 | } 28 | } 29 | } 30 | } 31 | 32 | private boolean isValidValueMethod(Method m) { 33 | //e.g. name() 34 | return m.getParameterTypes().length == 0 && 35 | !m.getReturnType().equals(Void.TYPE) && 36 | !m.getName().startsWith("get") && //already obtained via DefaultBeanIntrospector 37 | !m.getName().startsWith("wait") && 38 | !m.getName().equals("hashCode") && 39 | !m.getName().equals("toString"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/model/LabelValidator.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.model; 2 | 3 | 4 | import org.objectweb.asm.Type; 5 | 6 | /** 7 | * 8 | * @author Will Fleury 9 | */ 10 | public class LabelValidator { 11 | 12 | private final String method; 13 | private final Type[] argTypes; 14 | 15 | public LabelValidator(String method, Type[] argTypes) { 16 | this.method = method; 17 | this.argTypes = argTypes; 18 | } 19 | 20 | public void validate(String value) { 21 | if (value.startsWith("$this") && method.equals("")) { 22 | throwLabelInvalidException(value, "Cannot use $this in Constructor"); 23 | } 24 | 25 | if (value.startsWith("$")) { 26 | if (!value.matches("\\$([0-9]+|this)([a-zA-Z.]+)*")) { 27 | throwLabelInvalidException(value, "Must match pattern \\\\$([0-9]+|this)([a-zA-Z.]+)* or start with $this"); 28 | } 29 | 30 | if (!value.startsWith("$this")) { 31 | 32 | int index = LabelUtil.getLabelVarIndex(value); 33 | 34 | if (index >= argTypes.length) { 35 | throwLabelInvalidException(value, "It only has " + argTypes.length + " params"); 36 | } 37 | 38 | Type argType = argTypes[index]; 39 | if (argType.getSort() == Type.ARRAY) { 40 | throwLabelInvalidException(value, "ARRAY type is not allowed"); 41 | } 42 | } 43 | } 44 | } 45 | 46 | 47 | private void throwLabelInvalidException(String value, String reason) { 48 | throw new IllegalArgumentException( 49 | "Label value " + value + " for method " + method + " is invalid: " + reason); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.fleury 5 | metrics-agent 6 | 0.0.6-SNAPSHOT 7 | pom 8 | 9 | UTF-8 10 | 11 | 1.6 12 | 1.6 13 | 14 | metrics-agent 15 | 16 | 3.2.5 17 | 5.1 18 | 0.0.26 19 | 2.4.0 20 | 1.9.3 21 | 22 | 4.11 23 | 2.5 24 | 25 | 26 | 27 | 28 | metrics-agent-core 29 | metrics-agent-annotation 30 | metrics-agent-dropwizard 31 | metrics-agent-prometheus 32 | metrics-agent-dist 33 | 34 | 35 | 36 | 37 | junit 38 | junit 39 | ${junit.version} 40 | test 41 | 42 | 43 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/WhiteBlackListTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static com.fleury.metrics.agent.config.Configuration.dotToSlash; 4 | import static java.util.Arrays.asList; 5 | import static org.junit.Assert.assertEquals; 6 | 7 | import com.fleury.metrics.agent.annotation.Counted; 8 | import com.fleury.metrics.agent.config.Configuration; 9 | import java.util.List; 10 | import org.junit.Test; 11 | 12 | public class WhiteBlackListTest extends BaseMetricTest { 13 | 14 | @Test 15 | public void allowWhiteListed() throws Exception { 16 | List whiteList = asList(dotToSlash(CountedMethodClass.class.getName())); 17 | Configuration config = new Configuration(null, null, null, whiteList, null); 18 | 19 | Class clazz = execute(CountedMethodClass.class, config); 20 | 21 | Object obj = clazz.newInstance(); 22 | 23 | obj.getClass().getMethod("counted").invoke(obj); 24 | 25 | assertEquals(1, metrics.getCount("counted")); 26 | } 27 | 28 | @Test 29 | public void ignoreBlackListed() throws Exception { 30 | List blackList = asList(dotToSlash(CountedMethodClass.class.getName())); 31 | Configuration config = new Configuration(null, null, null, null, blackList); 32 | 33 | Class clazz = execute(CountedMethodClass.class, config); 34 | 35 | Object obj = clazz.newInstance(); 36 | 37 | obj.getClass().getMethod("counted").invoke(obj); 38 | 39 | assertEquals(0, metrics.getCount("counted")); 40 | } 41 | 42 | public static class CountedMethodClass { 43 | 44 | @Counted(name = "counted") 45 | public void counted() { 46 | BaseMetricTest.performBasicTask(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/RestrictedClassVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import static org.objectweb.asm.Opcodes.ACC_INTERFACE; 4 | import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; 5 | import static org.objectweb.asm.Opcodes.ASM5; 6 | 7 | import com.fleury.metrics.agent.config.Configuration; 8 | import org.objectweb.asm.ClassVisitor; 9 | import org.objectweb.asm.MethodVisitor; 10 | 11 | public abstract class RestrictedClassVisitor extends ClassVisitor { 12 | 13 | protected boolean isInterface; 14 | protected String className; 15 | protected int classVersion; 16 | protected Configuration config; 17 | 18 | public RestrictedClassVisitor(ClassVisitor cv, Configuration config) { 19 | super(ASM5, cv); 20 | this.config = config; 21 | } 22 | 23 | @Override 24 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 25 | super.visit(version, access, name, signature, superName, interfaces); 26 | this.classVersion = version; 27 | this.className = name; 28 | this.isInterface = (access & ACC_INTERFACE) != 0; 29 | } 30 | 31 | @Override 32 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 33 | MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 34 | 35 | boolean isSyntheticMethod = (access & ACC_SYNTHETIC) != 0; 36 | 37 | if (!isInterface && !isSyntheticMethod && mv != null) { 38 | mv = visitAllowedMethod(mv, access, name, desc, signature, exceptions); 39 | } 40 | 41 | return mv; 42 | } 43 | 44 | public abstract MethodVisitor visitAllowedMethod(MethodVisitor mv, int access, String name, String desc, String signature, String[] exceptions); 45 | } -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/MetricSystemProviderFactory.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.ServiceLoader; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | public class MetricSystemProviderFactory { 14 | 15 | public static final MetricSystemProviderFactory INSTANCE = new MetricSystemProviderFactory(); 16 | 17 | private final MetricSystemProvider provider; 18 | 19 | private Map configuration; 20 | 21 | private MetricSystemProviderFactory() { 22 | this.provider = initialiseMetricSystem(); 23 | } 24 | 25 | public void init(Map configuration) { 26 | this.configuration = configuration; 27 | } 28 | 29 | public MetricSystemProvider getProvider() { 30 | return provider; 31 | } 32 | 33 | public MetricSystem createMetricSystem() { 34 | return provider.createMetricSystem(configuration); 35 | } 36 | 37 | private MetricSystemProvider initialiseMetricSystem() { 38 | final ServiceLoader loader = ServiceLoader.load(MetricSystemProvider.class); 39 | List integrations = new ArrayList(); 40 | 41 | Iterator iterator = loader.iterator(); 42 | while (iterator.hasNext()) { 43 | integrations.add(iterator.next()); 44 | } 45 | 46 | if (integrations.isEmpty()) { 47 | throw new IllegalStateException("You must attach at least one reporting system to classpath"); 48 | } 49 | 50 | if (integrations.size() > 1) { 51 | throw new IllegalStateException("More than one reporting system found on classpath. There can only be one."); 52 | } 53 | 54 | MetricSystemProvider first = integrations.get(0); 55 | 56 | return first; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/ExceptionCounterInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import org.objectweb.asm.Label; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.commons.AdviceAdapter; 7 | 8 | /** 9 | * Transforms from 10 | * 11 | *
12 |  * public void someMethod() {
13 |  *     //original method code
14 |  * }
15 |  * 
16 | * 17 | * To 18 | * 19 | *
20 |  * public void someMethod() {
21 |  *     try {
22 |  *
23 |  *         //original method code
24 |  *
25 |  *     } catch (Throwable t) {
26 |  *         PrometheusMetricSystem.recordCount(COUNTER, labels);
27 |  *         throw t;
28 |  *     }
29 |  * }
30 |  * 
31 | * 32 | * @author Will Fleury 33 | */ 34 | public class ExceptionCounterInjector extends AbstractInjector { 35 | 36 | private static final String METHOD = "recordCount"; 37 | private static final String SIGNATURE = Type.getMethodDescriptor( 38 | Type.VOID_TYPE, 39 | Type.getType(String.class), Type.getType(String[].class)); 40 | 41 | private final Metric metric; 42 | 43 | private Label startFinally; 44 | 45 | public ExceptionCounterInjector(Metric metric, AdviceAdapter aa, Type[] argTypes, int access) { 46 | super(aa, argTypes, access); 47 | this.metric = metric; 48 | } 49 | 50 | @Override 51 | public void injectAtMethodEnter() { 52 | startFinally = new Label(); 53 | aa.visitLabel(startFinally); 54 | } 55 | 56 | @Override 57 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 58 | Label endFinally = new Label(); 59 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 60 | aa.visitLabel(endFinally); 61 | 62 | injectNameAndLabelToStack(metric); 63 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 64 | 65 | aa.visitInsn(ATHROW); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/config/ConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | import static com.fleury.metrics.agent.model.MetricType.Counted; 4 | import static com.fleury.metrics.agent.model.MetricType.Timed; 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertFalse; 7 | 8 | import com.fleury.metrics.agent.model.Metric; 9 | import com.fleury.metrics.agent.model.MetricType; 10 | import java.io.InputStream; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import org.junit.Test; 14 | 15 | /** 16 | * 17 | * @author Will Fleury 18 | */ 19 | public class ConfigurationTest { 20 | 21 | @Test 22 | public void testParseAndExpandMetricImports() { 23 | InputStream is = this.getClass().getResourceAsStream("/config/sample.yaml"); 24 | Configuration config = Configuration.createConfig(is); 25 | 26 | assertFalse(config.findMetrics("com/fleury/sample/Engine").isEmpty()); 27 | 28 | List metrics = config.findMetrics("com/fleury/sample/Engine", "sampleMethod", "(I)J"); 29 | assertEquals(2, metrics.size()); 30 | 31 | assertMetricDetails(metrics.get(0), Counted, "count", "trying to count", Arrays.asList("name1:value1", "name2:value2")); 32 | assertMetricDetails(metrics.get(1), Timed, "timer", "trying to time", Arrays.asList("name1:value1", "name2:value2")); 33 | 34 | metrics = config.findMetrics("com/test/Special", "sampleMethod", "(Ljava/lang/String;)J"); 35 | assertEquals(1, metrics.size()); 36 | 37 | assertMetricDetails(metrics.get(0), Counted, "count", "trying to count", null); 38 | } 39 | 40 | private void assertMetricDetails(Metric metric, MetricType type, String name, String doc, List labels) { 41 | assertEquals(type, metric.getType()); 42 | assertEquals(name, metric.getName()); 43 | assertEquals(doc, metric.getDoc()); 44 | assertEquals(labels, metric.getLabels()); 45 | } 46 | 47 | @Test 48 | public void testParseMetricSystemConfig() { 49 | InputStream is = this.getClass().getResourceAsStream("/config/sample.yaml"); 50 | Configuration config = Configuration.createConfig(is); 51 | 52 | assertFalse(config.getSystem().isEmpty()); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/SyntheticMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import com.fleury.metrics.agent.annotation.Counted; 6 | import org.junit.Test; 7 | 8 | /** 9 | * 10 | * @author Will Fleury 11 | */ 12 | public class SyntheticMethodsTest extends BaseMetricTest { 13 | 14 | /** 15 | * 16 | * The following generics results in two methods with the same annotation in the CountedMethodClass bytecode.. This 17 | * confused the annotation scanning as annotations are placed on both the real and synthetic method.. Therefore need 18 | * to check the method access code to ensure its not synthetic. 19 | * 20 | * public counted(Lcom/fleury/metrics/agent/transformer/asm/injectors/OverrideMethodAnnotationTest$B;)V 21 | * @Lcom/fleury/metrics/agent/annotation/Counted;(name="counted") 22 | * ... 23 | * 24 | * 25 | * public synthetic bridge counted(Lcom/fleury/metrics/agent/transformer/asm/injectors/OverrideMethodAnnotationTest$A;)V 26 | * @Lcom/fleury/metrics/agent/annotation/Counted;(name="counted") 27 | * ... 28 | * 29 | * See https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html 30 | */ 31 | 32 | 33 | @Test 34 | public void shouldCountMethodInvocation() throws Exception { 35 | Class clazz = execute(CountedMethodClass.class); 36 | 37 | Object obj = clazz.newInstance(); 38 | 39 | obj.getClass().getMethod("counted", B.class).invoke(obj, new Object[] {new B()}); 40 | 41 | assertEquals(1, metrics.getCount("counted")); 42 | 43 | //if bridge methods being instrumented also we would have two invocations here.. validate only happens once. 44 | assertEquals(1, metrics.getCounterRegistrations()); 45 | } 46 | 47 | public static class A { } 48 | 49 | public static class B extends A { } 50 | 51 | public static class BaseClass { 52 | public void counted(T value) { } 53 | } 54 | 55 | public static class CountedMethodClass extends BaseClass { 56 | 57 | @Override 58 | @Counted(name = "counted") 59 | public void counted(B value) { 60 | BaseMetricTest.performBasicTask(); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/model/LabelUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import org.objectweb.asm.Type; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | public class LabelUtil { 14 | 15 | public static Map splitLabelNameAndValue(List labels) { 16 | Map names = new LinkedHashMap(); 17 | 18 | if (labels == null) { 19 | return names; 20 | } 21 | 22 | for (String label : labels) { 23 | String[] tokens = label.split(":"); 24 | names.put(tokens[0].trim(), tokens[1].trim()); 25 | } 26 | return names; 27 | } 28 | 29 | public static int getLabelVarIndex(String value) { 30 | if (isLabelVarNested(value)) { 31 | return Integer.valueOf(value.substring(1, value.indexOf('.'))); 32 | } 33 | 34 | return Integer.valueOf(value.substring(1, value.length())); 35 | } 36 | 37 | public static String getNestedLabelVar(String value) { 38 | return value.substring(value.indexOf('.') + 1, value.length()); 39 | } 40 | 41 | public static boolean isLabelVarNested(String value) { 42 | return value.contains("."); 43 | } 44 | 45 | public static boolean isThis(String value) { 46 | return value.startsWith("$this"); 47 | } 48 | 49 | public static boolean isTemplatedLabelValue(String value) { 50 | return value.startsWith("$"); 51 | } 52 | 53 | public static List getLabelNames(List labels) { 54 | return new ArrayList(splitLabelNameAndValue(labels).keySet()); 55 | } 56 | 57 | public static String[] getLabelNamesAsArray(List labels) { 58 | return getLabelNames(labels).toArray(new String[0]); 59 | } 60 | 61 | public static List getLabelValues(List labels) { 62 | return new ArrayList(splitLabelNameAndValue(labels).values()); 63 | } 64 | 65 | public static void validateLabelValues(String method, List labels, Type[] argTypes) { 66 | List values = getLabelValues(labels); 67 | 68 | for (String value : values) { 69 | new LabelValidator(method, argTypes).validate(value); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/InjectorFactory.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import com.fleury.metrics.agent.model.MetricType; 5 | 6 | import static com.fleury.metrics.agent.model.MetricType.ExceptionCounted; 7 | import static com.fleury.metrics.agent.model.MetricType.Timed; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Map; 11 | import org.objectweb.asm.Type; 12 | import org.objectweb.asm.commons.AdviceAdapter; 13 | 14 | /** 15 | * 16 | * @author Will Fleury 17 | */ 18 | public class InjectorFactory { 19 | 20 | public static List createInjectors(Map metrics, AdviceAdapter adviceAdapter, Type[] argTypes, int access) { 21 | List injectors = new ArrayList(); 22 | 23 | //handle special case for both exception counter and timer (try catch finally) 24 | if (metrics.containsKey(ExceptionCounted) && metrics.containsKey(Timed)) { 25 | injectors.add(new TimedExceptionCountedInjector( 26 | metrics.get(Timed), 27 | metrics.get(ExceptionCounted), 28 | adviceAdapter, argTypes, access)); 29 | 30 | metrics.remove(Timed); 31 | metrics.remove(ExceptionCounted); 32 | } 33 | 34 | for (Metric metric : metrics.values()) { 35 | injectors.add(createInjector(metric, adviceAdapter, argTypes, access)); 36 | } 37 | 38 | return injectors; 39 | } 40 | 41 | public static Injector createInjector(Metric metric, AdviceAdapter adviceAdapter, Type[] argTypes, int access) { 42 | switch (metric.getType()) { 43 | case Counted: 44 | return new CounterInjector(metric, adviceAdapter, argTypes, access); 45 | 46 | case Gauged: 47 | return new GaugeInjector(metric, adviceAdapter, argTypes, access); 48 | 49 | case ExceptionCounted: 50 | return new ExceptionCounterInjector(metric, adviceAdapter, argTypes, access); 51 | 52 | case Timed: 53 | return new TimerInjector(metric, adviceAdapter, argTypes, access); 54 | 55 | default: 56 | throw new IllegalStateException("unknown metric type: " + metric.getType()); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/GaugeInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import org.objectweb.asm.Label; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.commons.AdviceAdapter; 7 | 8 | /** 9 | * Only currently supports IN_FLIGHT mode which means it tracks the number of method calls in flight. 10 | * 11 | * Transforms from 12 | * 13 | *
14 |  * public void someMethod() {
15 |  *     //original method code
16 |  * }
17 |  * 
18 | * 19 | * To 20 | * 21 | *
22 |  * public void someMethod() {
23 |  *     PrometheusMetricSystem.recordGaugeInc(GAUGE, labels);
24 |  *     try {
25 |  *
26 |  *         //original method code
27 |  *
28 |  *     } finally {
29 |  *         PrometheusMetricSystem.recordGaugeDec(GAUGE, labels);
30 |  *     }
31 |  * }
32 |  * 
33 | * 34 | * @author Will Fleury 35 | */ 36 | public class GaugeInjector extends AbstractInjector { 37 | 38 | private static final String INC_METHOD = "recordGaugeInc"; 39 | private static final String DEC_METHOD = "recordGaugeDec"; 40 | private static final String SIGNATURE = Type.getMethodDescriptor( 41 | Type.VOID_TYPE, 42 | Type.getType(String.class), Type.getType(String[].class)); 43 | 44 | private final Metric metric; 45 | 46 | private Label startFinally; 47 | 48 | public GaugeInjector(Metric metric, AdviceAdapter aa, Type[] argTypes, int access) { 49 | super(aa, argTypes, access); 50 | this.metric = metric; 51 | } 52 | 53 | @Override 54 | public void injectAtMethodEnter() { 55 | startFinally = new Label(); 56 | aa.visitLabel(startFinally); 57 | 58 | injectNameAndLabelToStack(metric); 59 | 60 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, INC_METHOD, SIGNATURE, false); 61 | } 62 | 63 | @Override 64 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 65 | Label endFinally = new Label(); 66 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 67 | aa.visitLabel(endFinally); 68 | 69 | onFinally(ATHROW); 70 | aa.visitInsn(ATHROW); 71 | } 72 | 73 | @Override 74 | public void injectAtMethodExit(int opcode) { 75 | if (opcode != ATHROW) { 76 | onFinally(opcode); 77 | } 78 | } 79 | 80 | private void onFinally(int opcode) { 81 | injectNameAndLabelToStack(metric); 82 | 83 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, DEC_METHOD, SIGNATURE, false); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /example-configurations/tomcat.yaml: -------------------------------------------------------------------------------- 1 | # Example tomcat request instrumentation. We demo instrumenting for 2 | # 3 | # - Basic Servlets 4 | # - JSP 5 | # - Jersey REST Resources (the same as in jersey.yaml) 6 | 7 | imports: 8 | - com/fleury/resources/HelloWorldResource 9 | - org/glassfish/jersey/server/ContainerRequest 10 | - org/glassfish/jersey/server/ContainerResponse 11 | - org/glassfish/jersey/message/internal/OutboundJaxrsResponse 12 | - org/glassfish/jersey/internal/util/collection/Value 13 | - org/glassfish/jersey/servlet/ServletContainer 14 | - org/apache/jasper/servlet/JspServlet 15 | - javax/servlet/http/HttpServletRequest 16 | - javax/servlet/http/HttpServletResponse 17 | - javax/servlet/http/HttpServlet 18 | - java/util/Optional 19 | - java/lang/Object 20 | - java/lang/String 21 | - java/net/URI 22 | 23 | 24 | metrics: 25 | # Basic Servlet 26 | HttpServlet.service(LHttpServletRequest;LHttpServletResponse;)V: 27 | - type: Timed 28 | name: servlet_resource_latency 29 | doc: Measuring http resource latencies 30 | labels: ['path:$0.servletPath', 'method:$0.method'] 31 | 32 | - type: Gauged 33 | name: servlet_in_flight_requests 34 | mode: in_flight 35 | doc: Measuring in flight requests 36 | labels: ['path:$0.servletPath', 'method:$0.method'] 37 | 38 | # JSP Servlet 39 | JspServlet.service(LHttpServletRequest;LHttpServletResponse;)V: 40 | - type: Timed 41 | name: jsp_resource_latency 42 | doc: Measuring http resource latencies 43 | labels: ['path:$0.servletPath', 'method:$0.method'] 44 | 45 | - type: Gauged 46 | name: jsp_in_flight_requests 47 | mode: in_flight 48 | doc: Measuring in flight requests 49 | labels: ['path:$0.servletPath', 'method:$0.method'] 50 | 51 | # Jersey Resources 52 | ServletContainer.service(LURI;LURI;LHttpServletRequest;LHttpServletResponse;)LValue;: 53 | - type: Timed 54 | name: jersey_resource_latency 55 | doc: Measuring http resource latencies 56 | labels: ['path:$1.path', 'method:$2.method'] 57 | 58 | - type: Gauged 59 | name: jersey_in_flight_requests 60 | mode: in_flight 61 | doc: Measuring in flight requests 62 | labels: ['path:$1.path', 'method:$2.method'] 63 | 64 | # Jersey Resources - tracks call count by response code (status) 65 | ContainerResponse.(LContainerRequest;LOutboundJaxrsResponse;)V: 66 | - type: Counted 67 | name: jersey_http_call_count 68 | doc: Http methods call counts 69 | labels: ['path:$0.requestUri.path', 'method:$0.method', 'status:$1.status'] 70 | 71 | system: 72 | jvm: 73 | - gc 74 | - memory 75 | - threads 76 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/MetricAnnotationAttributeVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import com.fleury.metrics.agent.config.Configuration; 4 | import com.fleury.metrics.agent.model.Metric; 5 | import com.fleury.metrics.agent.model.MetricType; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import org.objectweb.asm.AnnotationVisitor; 9 | import static org.objectweb.asm.Opcodes.ASM5; 10 | 11 | /** 12 | * @author Will Fleury 13 | */ 14 | public class MetricAnnotationAttributeVisitor extends AnnotationVisitor { 15 | 16 | private final Configuration config; 17 | private final Configuration.Key metricKey; 18 | private final Metric.MetricBuilder metricBuilder; 19 | 20 | public MetricAnnotationAttributeVisitor(AnnotationVisitor av, MetricType metricType, Configuration config, Configuration.Key metricKey) { 21 | super(ASM5, av); 22 | 23 | this.config = config; 24 | this.metricKey = metricKey; 25 | 26 | this.metricBuilder = Metric.builder().withType(metricType); 27 | } 28 | 29 | @Override 30 | public void visit(String name, Object value) { 31 | super.visit(name, value); 32 | 33 | if ("name".equals(name)) { 34 | metricBuilder.withName(value.toString()); 35 | } else if ("doc".equals(name)) { 36 | metricBuilder.withDoc(value.toString()); 37 | } 38 | } 39 | 40 | @Override 41 | public void visitEnum(String name, String desc, String value) { 42 | super.visit(name, value); 43 | 44 | if ("mode".equals(name)) { 45 | metricBuilder.withMode(value); 46 | } 47 | } 48 | 49 | @Override 50 | public AnnotationVisitor visitArray(String name) { 51 | if ("labels".equals(name)) { 52 | 53 | final List labels = new ArrayList(); 54 | metricBuilder.withLabels(labels); 55 | 56 | return new AnnotationVisitor(ASM5, av) { 57 | 58 | @Override 59 | public void visit(String name, Object value) { 60 | String label = value.toString(); 61 | 62 | if (!label.contains(":")) { 63 | throw new IllegalArgumentException("Label: " + label + " is not format {name}:{value}"); 64 | } 65 | 66 | labels.add(label); 67 | } 68 | }; 69 | } 70 | 71 | return super.visitArray(name); 72 | } 73 | 74 | @Override 75 | public void visitEnd() { 76 | super.visitEnd(); 77 | 78 | config.addMetric(metricKey, metricBuilder.createMetric()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /metrics-agent-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | metrics-agent 7 | 0.0.6-SNAPSHOT 8 | 9 | metrics-agent-core 10 | metrics-agent-core 11 | jar 12 | 13 | 14 | 15 | ${project.groupId} 16 | metrics-agent-annotation 17 | ${project.version} 18 | 19 | 20 | 21 | org.ow2.asm 22 | asm 23 | ${asm.version} 24 | 25 | 26 | 27 | org.ow2.asm 28 | asm-commons 29 | ${asm.version} 30 | 31 | 32 | 33 | com.fasterxml.jackson.core 34 | jackson-databind 35 | ${jackson.version} 36 | 37 | 38 | 39 | com.fasterxml.jackson.dataformat 40 | jackson-dataformat-yaml 41 | ${jackson.version} 42 | 43 | 44 | 45 | commons-beanutils 46 | commons-beanutils 47 | ${commons.beanutils.version} 48 | 49 | 50 | 51 | org.ow2.asm 52 | asm-util 53 | ${asm.version} 54 | test 55 | 56 | 57 | 58 | org.ow2.asm 59 | asm-analysis 60 | ${asm.version} 61 | test 62 | 63 | 64 | 65 | commons-io 66 | commons-io 67 | ${commons.io.version} 68 | test 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/TimerInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import org.objectweb.asm.Label; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.commons.AdviceAdapter; 7 | 8 | /** 9 | * Transforms from 10 | * 11 | *
12 |  * public void someMethod() {
13 |  *     //original method code
14 |  * }
15 |  * 
16 | * 17 | * To 18 | * 19 | *
20 |  * public void someMethod() {
21 |  *     long startTimer = System.nanoTime();
22 |  *     try {
23 |  *
24 |  *         //original method code
25 |  *
26 |  *     } finally {
27 |  *         PrometheusMetricSystem.recordTime(TIMER, labels);
28 |  *     }
29 |  * }
30 |  * 
31 | * 32 | * @author Will Fleury 33 | */ 34 | public class TimerInjector extends AbstractInjector { 35 | 36 | private static final String METHOD = "recordTime"; 37 | private static final String SIGNATURE = Type.getMethodDescriptor( 38 | Type.VOID_TYPE, 39 | Type.getType(String.class), Type.getType(String[].class), Type.LONG_TYPE); 40 | 41 | private final Metric metric; 42 | 43 | private int startTimeVar; 44 | private Label startFinally; 45 | 46 | public TimerInjector(Metric metric, AdviceAdapter aa, Type[] argTypes, int access) { 47 | super(aa, argTypes, access); 48 | this.metric = metric; 49 | } 50 | 51 | @Override 52 | public void injectAtMethodEnter() { 53 | startFinally = new Label(); 54 | startTimeVar = aa.newLocal(Type.LONG_TYPE); 55 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 56 | aa.visitVarInsn(LSTORE, startTimeVar); 57 | aa.visitLabel(startFinally); 58 | } 59 | 60 | @Override 61 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 62 | Label endFinally = new Label(); 63 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 64 | aa.visitLabel(endFinally); 65 | 66 | onFinally(ATHROW); 67 | aa.visitInsn(ATHROW); 68 | } 69 | 70 | @Override 71 | public void injectAtMethodExit(int opcode) { 72 | if (opcode != ATHROW) { 73 | onFinally(opcode); 74 | } 75 | } 76 | 77 | private void onFinally(int opcode) { 78 | injectNameAndLabelToStack(metric); 79 | 80 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 81 | aa.visitVarInsn(LLOAD, startTimeVar); 82 | aa.visitInsn(LSUB); 83 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/MetricAdapter.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import static com.fleury.metrics.agent.model.Metric.mapByType; 4 | import static java.util.logging.Level.FINE; 5 | 6 | import com.fleury.metrics.agent.model.LabelUtil; 7 | import com.fleury.metrics.agent.model.Metric; 8 | import com.fleury.metrics.agent.model.MetricType; 9 | import com.fleury.metrics.agent.reporter.Reporter; 10 | import com.fleury.metrics.agent.transformer.visitors.injectors.Injector; 11 | import com.fleury.metrics.agent.transformer.visitors.injectors.InjectorFactory; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.logging.Logger; 16 | import org.objectweb.asm.MethodVisitor; 17 | import org.objectweb.asm.Type; 18 | import org.objectweb.asm.commons.AdviceAdapter; 19 | 20 | /** 21 | * 22 | * @author Will Fleury 23 | */ 24 | public class MetricAdapter extends AdviceAdapter { 25 | 26 | private static final Logger LOGGER = Logger.getLogger(AdviceAdapter.class.getName()); 27 | 28 | private final Map metrics; 29 | private final Type[] argTypes; 30 | private final String className; 31 | private final String methodName; 32 | private final int access; 33 | 34 | private List injectors; 35 | 36 | public MetricAdapter(MethodVisitor mv, String className, int access, String name, String desc, List metadata) { 37 | super(ASM5, mv, access, name, desc); 38 | 39 | this.className = className; 40 | this.methodName = name; 41 | this.argTypes = Type.getArgumentTypes(desc); 42 | this.access = access; 43 | this.metrics = mapByType(metadata); 44 | } 45 | 46 | @Override 47 | protected void onMethodEnter() { 48 | if (metrics.isEmpty()) { 49 | injectors = Collections.emptyList(); 50 | return; 51 | } 52 | 53 | LOGGER.log(FINE, "Metrics found on : {0}.{1}", new Object[] {className, methodName}); 54 | 55 | injectors = InjectorFactory.createInjectors(metrics, this, argTypes, access); 56 | validateLabels(); 57 | Reporter.registerMetrics(metrics.values()); 58 | 59 | for (Injector injector : injectors) { 60 | injector.injectAtMethodEnter(); 61 | } 62 | } 63 | 64 | @Override 65 | public void visitMaxs(int maxStack, int maxLocals) { 66 | for (Injector injector : injectors) { 67 | injector.injectAtVisitMaxs(maxStack, maxLocals); 68 | } 69 | 70 | mv.visitMaxs(maxStack, maxLocals); 71 | } 72 | 73 | @Override 74 | protected void onMethodExit(int opcode) { 75 | for (Injector injector : injectors) { 76 | injector.injectAtMethodExit(opcode); 77 | } 78 | } 79 | 80 | private void validateLabels() { 81 | for (Metric metric : metrics.values()) { 82 | LabelUtil.validateLabelValues(methodName, metric.getLabels(), argTypes); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/TimedExceptionCountedInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import com.fleury.metrics.agent.annotation.ExceptionCounted; 7 | import com.fleury.metrics.agent.annotation.Timed; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.util.concurrent.TimeUnit; 10 | import org.junit.Test; 11 | 12 | /** 13 | * 14 | * @author Will Fleury 15 | */ 16 | public class TimedExceptionCountedInjectorTest extends BaseMetricTest { 17 | 18 | @Test 19 | public void shouldRecordConstructorInvocationStatistics() throws Exception { 20 | Class clazz = execute(TimedExceptionCountedConstructorClass.class); 21 | 22 | Object obj = clazz.newInstance(); 23 | 24 | long[] values = metrics.getTimes("constructor", new String[]{"timed"}); 25 | assertEquals(1, values.length); 26 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 27 | 28 | assertEquals(0, metrics.getCount("constructor", new String[]{"exception"})); 29 | } 30 | 31 | @Test 32 | public void shouldRecordMethodInvocationWhenExceptionThrownStatistics() throws Exception { 33 | Class clazz = execute(TimedExceptionCountedMethodClassWithException.class); 34 | 35 | Object obj = clazz.newInstance(); 36 | 37 | boolean exceptionOccured = false; 38 | try { 39 | obj.getClass().getMethod("timed").invoke(obj); 40 | } 41 | catch (InvocationTargetException e) { 42 | exceptionOccured = true; 43 | } 44 | 45 | assertTrue(exceptionOccured); 46 | 47 | long[] values = metrics.getTimes("timed", new String[]{"timed"}); 48 | assertEquals(1, values.length); 49 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 50 | 51 | assertEquals(1, metrics.getCount("timed", new String[]{"exception"})); 52 | } 53 | 54 | public static class TimedExceptionCountedConstructorClass { 55 | 56 | @Timed(name = "constructor", labels = {"type:timed"}) 57 | @ExceptionCounted(name = "constructor", labels = {"type:exception"}) 58 | public TimedExceptionCountedConstructorClass() { 59 | try { 60 | Thread.sleep(10L); 61 | } 62 | catch (InterruptedException e) { 63 | } 64 | } 65 | } 66 | 67 | public static class TimedExceptionCountedMethodClassWithException { 68 | 69 | @Timed(name = "timed", labels = {"type:timed"}) 70 | @ExceptionCounted(name = "timed", labels = {"type:exception"}) 71 | public void timed() { 72 | try { 73 | Thread.sleep(10L); 74 | callService(); 75 | } 76 | catch (InterruptedException e) { 77 | } 78 | } 79 | 80 | public final void callService() { 81 | BaseMetricTest.performBasicTask(); 82 | throw new RuntimeException(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/model/Metric.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | public class Metric { 14 | 15 | public static Map mapByType(List configMetrics) { 16 | Map metrics = new HashMap(); 17 | 18 | for (Metric metric : configMetrics) { 19 | metrics.put(metric.getType(), metric); 20 | } 21 | 22 | return metrics; 23 | } 24 | 25 | private final MetricType type; 26 | private final String name; 27 | private final String doc; 28 | private final List labels; 29 | private final String mode; 30 | 31 | 32 | @JsonCreator 33 | Metric(@JsonProperty("type") MetricType type, 34 | @JsonProperty("name") String name, 35 | @JsonProperty("doc") String doc, 36 | @JsonProperty("labels") List labels, 37 | @JsonProperty("mode") String mode) { 38 | this.type = type; 39 | this.name = name; 40 | this.doc = doc; 41 | this.labels = labels; 42 | this.mode = mode; 43 | } 44 | 45 | public MetricType getType() { 46 | return type; 47 | } 48 | 49 | public String getName() { 50 | return name; 51 | } 52 | 53 | public List getLabels() { 54 | return labels; 55 | } 56 | 57 | public String getDoc() { 58 | return doc; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "Metric{" + 64 | "type=" + type + 65 | ", name='" + name + '\'' + 66 | ", doc='" + doc + '\'' + 67 | ", labels=" + labels + 68 | ", mode=" + mode + 69 | '}'; 70 | } 71 | 72 | public static MetricBuilder builder() { 73 | return new MetricBuilder(); 74 | } 75 | 76 | public static class MetricBuilder { 77 | private MetricType type; 78 | private String name; 79 | private String doc; 80 | private List labels; 81 | private String mode; 82 | 83 | public MetricBuilder withType(MetricType type) { 84 | this.type = type; 85 | return this; 86 | } 87 | 88 | public MetricBuilder withName(String name) { 89 | this.name = name; 90 | return this; 91 | } 92 | 93 | public MetricBuilder withDoc(String doc) { 94 | this.doc = doc; 95 | return this; 96 | } 97 | 98 | public MetricBuilder withLabels(List labels) { 99 | this.labels = labels; 100 | return this; 101 | } 102 | 103 | public MetricBuilder withMode(String mode) { 104 | this.mode = mode; 105 | return this; 106 | } 107 | 108 | public Metric createMetric() { 109 | return new Metric(type, name, doc, labels, mode); 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/AnnotatedMetricClassTransformer.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer; 2 | 3 | import static java.util.logging.Level.WARNING; 4 | import static org.objectweb.asm.ClassReader.EXPAND_FRAMES; 5 | import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES; 6 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS; 7 | 8 | import com.fleury.metrics.agent.config.Configuration; 9 | import com.fleury.metrics.agent.transformer.visitors.AnnotationClassVisitor; 10 | import com.fleury.metrics.agent.transformer.visitors.MetricClassVisitor; 11 | import java.lang.instrument.ClassFileTransformer; 12 | import java.lang.instrument.IllegalClassFormatException; 13 | import java.security.ProtectionDomain; 14 | import java.util.logging.Logger; 15 | import org.objectweb.asm.ClassReader; 16 | import org.objectweb.asm.ClassVisitor; 17 | import org.objectweb.asm.ClassWriter; 18 | 19 | /** 20 | * 21 | * @author Will Fleury 22 | */ 23 | public class AnnotatedMetricClassTransformer implements ClassFileTransformer { 24 | 25 | private static final Logger LOGGER = Logger.getLogger(AnnotatedMetricClassTransformer.class.getName()); 26 | 27 | private final Configuration config; 28 | private final boolean propagateExceptions; 29 | 30 | public AnnotatedMetricClassTransformer(Configuration config) { 31 | this(config, false); 32 | } 33 | 34 | public AnnotatedMetricClassTransformer(Configuration config, boolean propagateExceptions) { 35 | this.config = config; 36 | this.propagateExceptions = propagateExceptions; 37 | } 38 | 39 | @Override 40 | public byte[] transform(ClassLoader loader, String className, 41 | Class classBeingRedefined, ProtectionDomain protectionDomain, 42 | byte[] classfileBuffer) throws IllegalClassFormatException { 43 | 44 | try { 45 | ClassReader cr = new ClassReader(classfileBuffer); 46 | 47 | // Scan for annotations in a pre-pass phase so we have all the metric information we need when performing 48 | // the actual instrumentation. This allows us to e.g. add Class Fields if desired for metrics which cannot 49 | // be done otherwise (as visitMethod happens after visitField in ClassVisitor). 50 | scanMetricAnnotations(loader, cr); 51 | 52 | // rewrite only if metric found & class allowed 53 | if (config.isMetric(className) && config.isWhiteListed(className) && !config.isBlackListed(className)) { 54 | ClassWriter cw = new ASMClassWriter(COMPUTE_FRAMES | COMPUTE_MAXS, loader); 55 | ClassVisitor cv = new MetricClassVisitor(cw, config); 56 | cr.accept(cv, EXPAND_FRAMES); 57 | 58 | return cw.toByteArray(); 59 | } 60 | 61 | } catch (RuntimeException e) { 62 | if (propagateExceptions) { 63 | throw e; //useful for testing & fail fast setups 64 | } 65 | else { 66 | LOGGER.log(WARNING, "Failed to transform " + className, e); 67 | } 68 | } 69 | 70 | return classfileBuffer; 71 | } 72 | 73 | private void scanMetricAnnotations(ClassLoader loader, ClassReader cr) { 74 | cr.accept(new AnnotationClassVisitor(new ASMClassWriter(0, loader), config), 0); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/Reporter.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import static java.util.logging.Level.FINER; 4 | 5 | import com.fleury.metrics.agent.model.LabelUtil; 6 | import com.fleury.metrics.agent.model.Metric; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * 13 | * @author Will Fleury 14 | */ 15 | public class Reporter { 16 | 17 | private static final Logger LOGGER = Logger.getLogger(Reporter.class.getName()); 18 | 19 | public static final MetricSystem METRIC_SYSTEM = MetricSystemProviderFactory.INSTANCE.createMetricSystem(); 20 | 21 | public static void startDefaultMetricEndpoint() { 22 | METRIC_SYSTEM.startDefaultEndpoint(); 23 | } 24 | 25 | public static void registerMetrics(Collection metrics) { 26 | for (Metric metric : metrics) { 27 | switch (metric.getType()) { 28 | case Counted: 29 | case ExceptionCounted: 30 | registerCounter(metric.getName(), getLabelNames(metric.getLabels()), metric.getDoc()); 31 | break; 32 | 33 | case Gauged: 34 | registerGauge(metric.getName(), getLabelNames(metric.getLabels()), metric.getDoc()); 35 | break; 36 | 37 | case Timed: 38 | registerTimer(metric.getName(), getLabelNames(metric.getLabels()), metric.getDoc()); 39 | break; 40 | 41 | default: 42 | throw new RuntimeException("Unhandled metric registration for type: " + metric.getType()); 43 | } 44 | } 45 | } 46 | 47 | public static void registerCounter(String name, String[] labelNames, String doc) { 48 | LOGGER.log(FINER, "registering metric name: {0} doc: {1}", new Object[] {name, doc}); 49 | METRIC_SYSTEM.registerCounter(name, labelNames, doc); 50 | } 51 | 52 | public static void registerGauge(String name, String[] labelNames, String doc) { 53 | LOGGER.log(FINER, "registering metric name: {0} doc: {1}", new Object[] {name, doc}); 54 | METRIC_SYSTEM.registerGauge(name, labelNames, doc); 55 | } 56 | 57 | public static void registerTimer(String name, String[] labelNames, String doc) { 58 | LOGGER.log(FINER, "registering metric name: {0} doc: {1}", new Object[] {name, doc}); 59 | METRIC_SYSTEM.registerTimer(name, labelNames, doc); 60 | } 61 | 62 | public static void recordCount(String name, String[] labelValues) { 63 | METRIC_SYSTEM.recordCount(name, labelValues); 64 | } 65 | 66 | public static void recordCount(String name, String[] labelValues, long n) { 67 | METRIC_SYSTEM.recordCount(name, labelValues, n); 68 | } 69 | 70 | public static void recordGaugeInc(String name, String[] labelValues) { 71 | METRIC_SYSTEM.recordGaugeInc(name, labelValues); 72 | } 73 | 74 | public static void recordGaugeDec(String name, String[] labelValues) { 75 | METRIC_SYSTEM.recordGaugeDec(name, labelValues); 76 | } 77 | 78 | public static void recordTime(String name, String[] labelValues, long duration) { 79 | METRIC_SYSTEM.recordTime(name, labelValues, duration); 80 | } 81 | 82 | private static String[] getLabelNames(List list) { 83 | return list == null ? null : LabelUtil.getLabelNamesAsArray(list); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/BaseMetricTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static com.fleury.metrics.agent.config.Configuration.dotToSlash; 4 | import static com.fleury.metrics.agent.config.Configuration.emptyConfiguration; 5 | 6 | import com.fleury.metrics.agent.config.Configuration; 7 | import com.fleury.metrics.agent.reporter.Reporter; 8 | import com.fleury.metrics.agent.reporter.TestMetricSystem; 9 | import com.fleury.metrics.agent.transformer.AnnotatedMetricClassTransformer; 10 | import java.io.PrintWriter; 11 | import java.lang.instrument.ClassFileTransformer; 12 | import java.util.logging.Logger; 13 | import org.apache.commons.io.IOUtils; 14 | import org.junit.Before; 15 | import org.objectweb.asm.ClassReader; 16 | import org.objectweb.asm.ClassWriter; 17 | import org.objectweb.asm.util.CheckClassAdapter; 18 | import org.objectweb.asm.util.TraceClassVisitor; 19 | 20 | /** 21 | * 22 | * @author Will Fleury 23 | */ 24 | public abstract class BaseMetricTest { 25 | 26 | private static final Logger LOGGER = Logger.getLogger(BaseMetricTest.class.getName()); 27 | 28 | protected TestMetricSystem metrics; 29 | 30 | @Before 31 | public void setup() { 32 | metrics = (TestMetricSystem) Reporter.METRIC_SYSTEM; 33 | metrics.reset(); 34 | } 35 | 36 | private final ByteCodeClassLoader loader = new ByteCodeClassLoader(); 37 | 38 | @SuppressWarnings("unchecked") 39 | public Class getClassFromBytes(Class clazz, byte[] bytes) { 40 | return loader.defineClass(clazz.getName(), bytes); 41 | } 42 | 43 | protected Class execute(Class clazz) throws Exception { 44 | return execute(clazz, emptyConfiguration()); 45 | } 46 | 47 | protected Class execute(Class clazz, Configuration config) throws Exception { 48 | String className = dotToSlash(clazz.getName()); 49 | String classAsPath = className + ".class"; 50 | 51 | ClassFileTransformer cft = new AnnotatedMetricClassTransformer(config, true); 52 | byte[] classfileBuffer = cft.transform( 53 | clazz.getClassLoader(), 54 | className, 55 | null, 56 | null, 57 | IOUtils.toByteArray(clazz.getClassLoader().getResourceAsStream(classAsPath))); 58 | 59 | traceBytecode(classfileBuffer); 60 | verifyBytecode(classfileBuffer); 61 | 62 | return getClassFromBytes(clazz, classfileBuffer); 63 | } 64 | 65 | private void traceBytecode(byte[] bytecode) { 66 | ClassReader cr = new ClassReader(bytecode); 67 | ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 68 | cr.accept(new TraceClassVisitor(cw, new PrintWriter(System.out)), 0); 69 | } 70 | 71 | private void verifyBytecode(byte[] bytecode) { 72 | ClassReader cr = new ClassReader(bytecode); 73 | ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); 74 | cr.accept(new CheckClassAdapter(cw), 0); 75 | } 76 | 77 | public static class ByteCodeClassLoader extends ClassLoader { 78 | 79 | public Class defineClass(String name, byte[] bytes) { 80 | return defineClass(name, bytes, 0, bytes.length); 81 | } 82 | } 83 | 84 | public static void performBasicTask() { 85 | LOGGER.fine("Debugging to ensure basic op perfomred by calling code"); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/TimedExceptionCountedInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import com.fleury.metrics.agent.model.Metric; 4 | import org.objectweb.asm.Label; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.commons.AdviceAdapter; 7 | 8 | /** 9 | * Transforms from 10 | * 11 | *
 12 |  * public void someMethod() {
 13 |  *     //original method code
 14 |  * }
 15 |  * 
16 | * 17 | * To 18 | * 19 | *
 20 |  * public void someMethod() {
 21 |  *     long startTimer = System.nanoTime();
 22 |  *     try {
 23 |  *
 24 |  *         //original method code
 25 |  *
 26 |  *     } catch (Throwable t) {
 27 |  *         PrometheusMetricSystem.recordCount(COUNTER, labels);
 28 |  *         throw t;
 29 |  *     } finally {
 30 |  *         PrometheusMetricSystem.recordTime(TIMER, labels);
 31 |  *     }
 32 |  * }
 33 |  * 
34 | * 35 | * @author Will Fleury 36 | */ 37 | public class TimedExceptionCountedInjector extends AbstractInjector { 38 | 39 | private static final String EXCEPTION_COUNT_METHOD = "recordCount"; 40 | private static final String EXCEPTION_COUNT_SIGNATURE = Type.getMethodDescriptor( 41 | Type.VOID_TYPE, 42 | Type.getType(String.class), Type.getType(String[].class)); 43 | 44 | private static final String TIMER_METHOD = "recordTime"; 45 | private static final String TIMER_SIGNATURE = Type.getMethodDescriptor( 46 | Type.VOID_TYPE, 47 | Type.getType(String.class), Type.getType(String[].class), Type.LONG_TYPE); 48 | 49 | private final Metric timerMetric; 50 | private final Metric exceptionMetric; 51 | 52 | private int startTimeVar; 53 | private Label startFinally; 54 | 55 | public TimedExceptionCountedInjector(Metric timerMetric, Metric exceptionMetric, AdviceAdapter aa, 56 | Type[] argTypes, int access) { 57 | super(aa, argTypes, access); 58 | this.timerMetric = timerMetric; 59 | this.exceptionMetric = exceptionMetric; 60 | } 61 | 62 | @Override 63 | public void injectAtMethodEnter() { 64 | startFinally = new Label(); 65 | startTimeVar = aa.newLocal(Type.LONG_TYPE); 66 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 67 | aa.visitVarInsn(LSTORE, startTimeVar); 68 | aa.visitLabel(startFinally); 69 | } 70 | 71 | @Override 72 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 73 | Label endFinally = new Label(); 74 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 75 | aa.visitLabel(endFinally); 76 | 77 | injectNameAndLabelToStack(exceptionMetric); 78 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, EXCEPTION_COUNT_METHOD, 79 | EXCEPTION_COUNT_SIGNATURE, false); 80 | 81 | onFinally(ATHROW); 82 | aa.visitInsn(ATHROW); 83 | } 84 | 85 | @Override 86 | public void injectAtMethodExit(int opcode) { 87 | if (opcode != ATHROW) { 88 | onFinally(opcode); 89 | } 90 | } 91 | 92 | private void onFinally(int opcode) { 93 | injectNameAndLabelToStack(timerMetric); 94 | 95 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 96 | aa.visitVarInsn(LLOAD, startTimeVar); 97 | aa.visitInsn(LSUB); 98 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, TIMER_METHOD, TIMER_SIGNATURE, false); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/ExceptionCounterInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import com.fleury.metrics.agent.annotation.ExceptionCounted; 8 | import java.lang.reflect.InvocationTargetException; 9 | import org.junit.Test; 10 | 11 | /** 12 | * 13 | * @author Will Fleury 14 | */ 15 | public class ExceptionCounterInjectorTest extends BaseMetricTest { 16 | 17 | private final static String EXCEPTION_TEXT = "I've been thrown!"; 18 | 19 | @Test 20 | public void shouldCountWhenExceptionThrownInConstructor() throws Exception { 21 | Class clazz = execute(ExceptionCountedConstructorClass.class); 22 | 23 | Exception thrown = null; 24 | try { 25 | Object obj = clazz.newInstance(); 26 | } 27 | catch (Exception e) { 28 | thrown = e; 29 | } 30 | 31 | assertNotNull(thrown); 32 | assertTrue(thrown instanceof RuntimeException); 33 | assertEquals(EXCEPTION_TEXT, thrown.getMessage()); 34 | 35 | assertEquals(1, metrics.getCount("constructor", new String[]{"type1"})); 36 | } 37 | 38 | @Test 39 | public void shouldCountAndRethrowWhenExceptionThrownInMethod() throws Exception { 40 | Class clazz = execute(ExceptionCountedMethodClass.class); 41 | 42 | Object obj = clazz.newInstance(); 43 | 44 | Exception thrown = null; 45 | try { 46 | obj.getClass().getMethod("exceptionCounted").invoke(obj); 47 | } 48 | catch (Exception e) { 49 | thrown = e; 50 | } 51 | 52 | assertNotNull(thrown); 53 | assertTrue(thrown instanceof InvocationTargetException); 54 | assertTrue(thrown.getCause() instanceof RuntimeException); 55 | assertEquals(EXCEPTION_TEXT, thrown.getCause().getMessage()); 56 | 57 | assertEquals(1, metrics.getCount("exceptionCounted")); 58 | } 59 | 60 | @Test 61 | public void shouldNotCountWhenNoExceptionThrown() throws Exception { 62 | Class clazz = execute(ExceptionCountedMethodClassWithNoException.class); 63 | 64 | Object obj = clazz.newInstance(); 65 | 66 | obj.getClass().getMethod("exceptionCounted").invoke(obj); 67 | 68 | assertEquals(0, metrics.getCount("exceptionCounted")); 69 | } 70 | 71 | public static class ExceptionCountedConstructorClass { 72 | 73 | @ExceptionCounted(name = "constructor", labels = "exception:type1") 74 | public ExceptionCountedConstructorClass() { 75 | callService(); 76 | } 77 | 78 | public final void callService() { 79 | BaseMetricTest.performBasicTask(); 80 | throw new RuntimeException(EXCEPTION_TEXT); 81 | } 82 | } 83 | 84 | public static class ExceptionCountedMethodClass { 85 | 86 | @ExceptionCounted(name = "exceptionCounted") 87 | public void exceptionCounted() { 88 | callService(); 89 | } 90 | 91 | public final void callService() { 92 | BaseMetricTest.performBasicTask(); 93 | throw new RuntimeException(EXCEPTION_TEXT); 94 | } 95 | } 96 | 97 | public static class ExceptionCountedMethodClassWithNoException { 98 | 99 | @ExceptionCounted(name = "exceptionCounted") 100 | public void exceptionCounted() { 101 | BaseMetricTest.performBasicTask(); 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/TimerInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import com.fleury.metrics.agent.annotation.Timed; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.util.concurrent.TimeUnit; 9 | import org.junit.Test; 10 | 11 | /** 12 | * 13 | * @author Will Fleury 14 | */ 15 | public class TimerInjectorTest extends BaseMetricTest { 16 | 17 | @Test 18 | public void shouldTimeConstructorInvocation() throws Exception { 19 | Class clazz = execute(TimedConstructorClass.class); 20 | 21 | Object obj = clazz.newInstance(); 22 | 23 | long[] values = metrics.getTimes("constructor"); 24 | assertEquals(1, values.length); 25 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 26 | } 27 | 28 | @Test 29 | public void shouldTimeMethodInvocation() throws Exception { 30 | Class clazz = execute(TimedMethodClass.class); 31 | 32 | Object obj = clazz.newInstance(); 33 | 34 | obj.getClass().getMethod("timed").invoke(obj); 35 | 36 | long[] values = metrics.getTimes("timed"); 37 | assertEquals(1, values.length); 38 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 39 | } 40 | 41 | @Test 42 | public void shouldTimeMethodWithLabelsInvocation() throws Exception { 43 | Class clazz = execute(TimedMethodWithLabelsClass.class); 44 | 45 | Object obj = clazz.newInstance(); 46 | 47 | obj.getClass().getMethod("timed").invoke(obj); 48 | 49 | long[] values = metrics.getTimes("timed", new String[]{"value1"}); 50 | assertEquals(1, values.length); 51 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 52 | } 53 | 54 | @Test 55 | public void shouldTimeMethodInvocationWhenExceptionThrown() throws Exception { 56 | Class clazz = execute(TimedMethodClassWithException.class); 57 | 58 | Object obj = clazz.newInstance(); 59 | 60 | boolean exceptionOccured = false; 61 | try { 62 | obj.getClass().getMethod("timed").invoke(obj); 63 | } 64 | catch (InvocationTargetException e) { 65 | exceptionOccured = true; 66 | } 67 | 68 | assertTrue(exceptionOccured); 69 | 70 | long[] values = metrics.getTimes("timed"); 71 | assertEquals(1, values.length); 72 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 73 | } 74 | 75 | public static class TimedConstructorClass { 76 | 77 | @Timed(name = "constructor") 78 | public TimedConstructorClass() { 79 | try { 80 | Thread.sleep(10L); 81 | } 82 | catch (InterruptedException e) { 83 | } 84 | } 85 | } 86 | 87 | public static class TimedMethodClass { 88 | 89 | @Timed(name = "timed") 90 | public void timed() { 91 | try { 92 | Thread.sleep(10L); 93 | } 94 | catch (InterruptedException e) { 95 | } 96 | } 97 | } 98 | 99 | public static class TimedMethodWithLabelsClass { 100 | 101 | @Timed(name = "timed", labels = {"name1:value1"}) 102 | public void timed() { 103 | try { 104 | Thread.sleep(10L); 105 | } 106 | catch (InterruptedException e) { 107 | } 108 | } 109 | } 110 | 111 | public static class TimedMethodClassWithException { 112 | 113 | @Timed(name = "timed") 114 | public void timed() { 115 | try { 116 | Thread.sleep(10L); 117 | callService(); 118 | } 119 | catch (InterruptedException e) { 120 | } 121 | } 122 | 123 | private void callService() { 124 | BaseMetricTest.performBasicTask(); 125 | throw new RuntimeException(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/CounterInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static com.fleury.metrics.agent.config.Configuration.emptyConfiguration; 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import com.fleury.metrics.agent.annotation.Counted; 8 | import com.fleury.metrics.agent.config.Configuration; 9 | import com.fleury.metrics.agent.model.Metric; 10 | import com.fleury.metrics.agent.model.MetricType; 11 | import java.util.Arrays; 12 | import org.junit.Test; 13 | import org.objectweb.asm.Type; 14 | 15 | /** 16 | * 17 | * @author Will Fleury 18 | */ 19 | public class CounterInjectorTest extends BaseMetricTest { 20 | 21 | @Test 22 | public void shouldCountConstructorInvocation() throws Exception { 23 | Class clazz = execute(CountedConstructorClass.class); 24 | 25 | Object obj = clazz.newInstance(); 26 | 27 | assertEquals(1, metrics.getCount("constructor")); 28 | } 29 | 30 | @Test 31 | public void shouldCountConstructorInvocationWithConfiguration() throws Exception { 32 | Metric meta = Metric.builder() 33 | .withType(MetricType.Counted) 34 | .withName("constructor") 35 | .withLabels(Arrays.asList("label1:value1")) 36 | .createMetric(); 37 | 38 | Configuration.Key key = new Configuration.Key( 39 | Type.getInternalName(ConfigurationCountedConstructorClass.class), 40 | "", "()V"); 41 | 42 | Configuration config = emptyConfiguration(); 43 | config.addMetric(key, meta); 44 | 45 | Class clazz = execute(ConfigurationCountedConstructorClass.class, config); 46 | 47 | Object obj = clazz.newInstance(); 48 | 49 | assertEquals(1, metrics.getCount("constructor", new String[]{"value1"})); 50 | } 51 | 52 | @Test 53 | public void shouldCountMethodInvocation() throws Exception { 54 | Class clazz = execute(CountedMethodClass.class); 55 | 56 | Object obj = clazz.newInstance(); 57 | 58 | obj.getClass().getMethod("counted").invoke(obj); 59 | 60 | assertEquals(1, metrics.getCount("counted")); 61 | } 62 | 63 | @Test 64 | public void shouldCountMethodWithLabelsInvocation() throws Exception { 65 | Class clazz = execute(CountedMethodWithLabelsClass.class); 66 | 67 | Object obj = clazz.newInstance(); 68 | 69 | obj.getClass().getMethod("counted").invoke(obj); 70 | 71 | assertEquals(1, metrics.getCount("counted", new String[]{"value1"})); 72 | } 73 | 74 | @Test 75 | public void shouldCountMethodWithParametersAndReturnInvocation() throws Exception { 76 | Class clazz = execute(CountedMethodWithParametersAndReturnClass.class); 77 | 78 | Object obj = clazz.newInstance(); 79 | 80 | Long count = (Long) obj.getClass().getMethod("counted", int.class).invoke(obj, 5); 81 | 82 | assertEquals(1, metrics.getCount("counted")); 83 | assertTrue(count >= 5); 84 | } 85 | 86 | public static class CountedConstructorClass { 87 | 88 | @Counted(name = "constructor") 89 | public CountedConstructorClass() { 90 | BaseMetricTest.performBasicTask(); 91 | } 92 | } 93 | 94 | public static class CountedMethodClass { 95 | 96 | @Counted(name = "counted") 97 | public void counted() { 98 | BaseMetricTest.performBasicTask(); 99 | } 100 | } 101 | 102 | public static class CountedMethodWithLabelsClass { 103 | 104 | @Counted(name = "counted", labels = "label1:value1") 105 | public void counted() { 106 | BaseMetricTest.performBasicTask(); 107 | } 108 | } 109 | 110 | public static class CountedMethodWithParametersAndReturnClass { 111 | 112 | @Counted(name = "counted") 113 | public long counted(int var) { 114 | long x = (long) (Math.random() * 10); 115 | BaseMetricTest.performBasicTask(); 116 | return x + var; 117 | } 118 | } 119 | 120 | public static class ConfigurationCountedConstructorClass { 121 | 122 | public ConfigurationCountedConstructorClass() { 123 | BaseMetricTest.performBasicTask(); 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/MixedInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import com.fleury.metrics.agent.annotation.Counted; 7 | import com.fleury.metrics.agent.annotation.ExceptionCounted; 8 | import com.fleury.metrics.agent.annotation.Timed; 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.util.concurrent.TimeUnit; 11 | import org.junit.Test; 12 | 13 | /** 14 | * 15 | * @author Will Fleury 16 | */ 17 | public class MixedInjectorTest extends BaseMetricTest { 18 | 19 | @Test 20 | public void shouldRecordConstructorInvocationStatistics() throws Exception { 21 | Class clazz = execute(MixedMetricConstructorClass.class); 22 | 23 | Object obj = clazz.newInstance(); 24 | 25 | long[] values = metrics.getTimes("constructor", new String[]{"timed"}); 26 | assertEquals(1, values.length); 27 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 28 | 29 | assertEquals(1, metrics.getCount("constructor", new String[]{"counted"})); 30 | assertEquals(0, metrics.getCount("constructor", new String[]{"exception"})); 31 | } 32 | 33 | @Test 34 | public void shouldRecordMethodInvocationStatistics() throws Exception { 35 | Class clazz = execute(MixedMetricMethodClass.class); 36 | 37 | Object obj = clazz.newInstance(); 38 | 39 | obj.getClass().getMethod("timed").invoke(obj); 40 | 41 | long[] values = metrics.getTimes("timed", new String[]{"timed"}); 42 | assertEquals(1, values.length); 43 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 44 | 45 | assertEquals(1, metrics.getCount("timed", new String[]{"counted"})); 46 | assertEquals(0, metrics.getCount("timed", new String[]{"exception"})); 47 | } 48 | 49 | @Test 50 | public void shouldRecordMethodInvocationWhenExceptionThrownStatistics() throws Exception { 51 | Class clazz = execute(MixedMetricMethodClassWithException.class); 52 | 53 | Object obj = clazz.newInstance(); 54 | 55 | boolean exceptionOccured = false; 56 | try { 57 | obj.getClass().getMethod("timed").invoke(obj); 58 | } 59 | catch (InvocationTargetException e) { 60 | exceptionOccured = true; 61 | } 62 | 63 | assertTrue(exceptionOccured); 64 | 65 | long[] values = metrics.getTimes("timed", new String[]{"timed"}); 66 | assertEquals(1, values.length); 67 | assertTrue(values[0] >= TimeUnit.NANOSECONDS.toMillis(10L)); 68 | 69 | assertEquals(1, metrics.getCount("timed", new String[]{"exception"})); 70 | assertEquals(1, metrics.getCount("timed", new String[]{"counted"})); 71 | } 72 | 73 | public static class MixedMetricConstructorClass { 74 | 75 | @Timed(name = "constructor", labels = {"type:timed"}) 76 | @ExceptionCounted(name = "constructor", labels = {"type:exception"}) 77 | @Counted(name = "constructor", labels = {"type:counted"}) 78 | public MixedMetricConstructorClass() { 79 | try { 80 | Thread.sleep(10L); 81 | } 82 | catch (InterruptedException e) { 83 | } 84 | } 85 | } 86 | 87 | public static class MixedMetricMethodClass { 88 | 89 | @Timed(name = "timed", labels = {"type:timed"}) 90 | @ExceptionCounted(name = "timed", labels = {"type:exception"}) 91 | @Counted(name = "timed", labels = {"type:counted"}) 92 | public void timed() { 93 | try { 94 | Thread.sleep(10L); 95 | } 96 | catch (InterruptedException e) { 97 | } 98 | } 99 | } 100 | 101 | public static class MixedMetricMethodClassWithException { 102 | 103 | @Timed(name = "timed", labels = {"type:timed"}) 104 | @ExceptionCounted(name = "timed", labels = {"type:exception"}) 105 | @Counted(name = "timed", labels = {"type:counted"}) 106 | public void timed() { 107 | try { 108 | Thread.sleep(10L); 109 | callService(); 110 | } 111 | catch (InterruptedException e) { 112 | } 113 | } 114 | 115 | public final void callService() { 116 | BaseMetricTest.performBasicTask(); 117 | throw new RuntimeException(); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /metrics-agent-dropwizard/src/main/java/com/fleury/metrics/agent/reporter/DropwizardMetricSystem.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import com.codahale.metrics.JmxReporter; 4 | import com.codahale.metrics.MetricRegistry; 5 | import com.codahale.metrics.SharedMetricRegistries; 6 | import com.codahale.metrics.jvm.CachedThreadStatesGaugeSet; 7 | import com.codahale.metrics.jvm.ClassLoadingGaugeSet; 8 | import com.codahale.metrics.jvm.GarbageCollectorMetricSet; 9 | import com.codahale.metrics.jvm.MemoryUsageGaugeSet; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.logging.Logger; 16 | 17 | /** 18 | * 19 | * @author Will Fleury 20 | */ 21 | public class DropwizardMetricSystem implements MetricSystem { 22 | 23 | private static final Logger LOGGER = Logger.getLogger(DropwizardMetricSystem.class.getName()); 24 | 25 | private final MetricRegistry registry = SharedMetricRegistries.getOrCreate("agent-metrics"); 26 | private final Map configuration; 27 | 28 | public DropwizardMetricSystem(Map configuration) { 29 | this.configuration = configuration; 30 | addJVMMetrics(configuration); 31 | 32 | startDefaultEndpoint(); 33 | } 34 | 35 | @Override 36 | public void registerCounter(String name, String labelNames[], String doc) { 37 | } 38 | 39 | @Override 40 | public void registerGauge(String name, String[] labelNames, String doc) { 41 | } 42 | 43 | @Override 44 | public void registerTimer(String name, String labelNames[], String doc) { 45 | } 46 | 47 | @Override 48 | public void recordCount(String name, String[] labelValues) { 49 | registry.counter(getMetricName(name, createSingleLabelValue(labelValues))).inc(); 50 | } 51 | 52 | @Override 53 | public void recordCount(String name, String[] labelValues, long n) { 54 | registry.counter(getMetricName(name, createSingleLabelValue(labelValues))).inc(n); 55 | } 56 | 57 | @Override 58 | public void recordGaugeInc(String name, String[] labelValues) { 59 | registry.counter(getMetricName(name, createSingleLabelValue(labelValues))).inc(); 60 | } 61 | 62 | @Override 63 | public void recordGaugeDec(String name, String[] labelValues) { 64 | registry.counter(getMetricName(name, createSingleLabelValue(labelValues))).dec(); 65 | } 66 | 67 | @Override 68 | public void recordTime(String name, String[] labelValues, long duration) { 69 | registry.timer(getMetricName(name, createSingleLabelValue(labelValues))).update(duration, TimeUnit.NANOSECONDS); 70 | } 71 | 72 | @Override 73 | public void startDefaultEndpoint() { 74 | String domain = (String)configuration.get("domain"); 75 | if (domain == null) { 76 | domain = "metrics"; 77 | } 78 | 79 | LOGGER.fine("Starting JMX reporter at domain: " + domain); 80 | 81 | JmxReporter 82 | .forRegistry(registry) 83 | .inDomain(domain) 84 | .convertDurationsTo(TimeUnit.MILLISECONDS) 85 | .convertRatesTo(TimeUnit.MINUTES) 86 | .build() 87 | .start(); 88 | } 89 | 90 | public static String getMetricName(String name, String label) { 91 | return label == null ? name : name + "." + label; 92 | } 93 | 94 | private String createSingleLabelValue(String[] labelValues) { 95 | if (labelValues == null || labelValues.length == 0) { 96 | return null; 97 | } 98 | 99 | StringBuilder builder = new StringBuilder(); 100 | for (int i = 0; i < labelValues.length; i++) { 101 | builder.append(labelValues[i]); 102 | 103 | if (i < labelValues.length - 1) { 104 | builder.append("."); 105 | } 106 | } 107 | 108 | return builder.toString(); 109 | } 110 | 111 | private void addJVMMetrics(Map configuration) { 112 | if (!configuration.containsKey("jvm")) { 113 | return; 114 | } 115 | Set jvmMetrics = new HashSet((List)configuration.get("jvm")); 116 | if (jvmMetrics.contains("gc")) { 117 | registry.register("gc", new GarbageCollectorMetricSet()); 118 | } 119 | 120 | if (jvmMetrics.contains("threads")) { 121 | registry.register("threads", new CachedThreadStatesGaugeSet(10, TimeUnit.SECONDS)); 122 | } 123 | 124 | if (jvmMetrics.contains("memory")) { 125 | registry.register("memory", new MemoryUsageGaugeSet()); 126 | } 127 | 128 | if (jvmMetrics.contains("classloader")) { 129 | registry.register("memory", new ClassLoadingGaugeSet()); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /example-configurations/hibernate.yaml: -------------------------------------------------------------------------------- 1 | # Example Hibernate Statistics Configuration. Tested with Hibernate 5.x 2 | # 3 | # Instrument the callback methods in the StatisticsImplementor implementation (ConcurrentStatisticsImpl). 4 | # We can get access to all of hibernates statistics in a simple and detailed manner. 5 | # 6 | # In this configuration we instrument the following statistics Methods 7 | # 8 | # Session Metrics 9 | # - openSession 10 | # - closeSession 11 | # - flush 12 | # - connect 13 | # 14 | # Statement Metrics 15 | # - prepareStatement 16 | # - closeStatement 17 | # - endTransaction 18 | # 19 | # Entity Metrics 20 | # - loadEntity 21 | # - fetchEntity 22 | # - updateEntity 23 | # - insertEntity 24 | # - deleteEntity 25 | # 26 | # Cache Metrics 27 | # - queryCachePut 28 | # - queryCacheHit 29 | # - queryCacheMiss 30 | # 31 | # - secondLevelCachePut 32 | # - secondLevelCacheHit 33 | # - secondLevelCacheMiss 34 | 35 | 36 | 37 | imports: 38 | - org/hibernate/stat/internal/ConcurrentStatisticsImpl 39 | - java/lang/String 40 | 41 | 42 | metrics: 43 | # Session Metrics 44 | ConcurrentStatisticsImpl.openSession()V: 45 | - type: Counted 46 | name: hibernate_session_opened_total 47 | doc: Global number of sessions opened (getSessionOpenCount) 48 | 49 | ConcurrentStatisticsImpl.closeSession()V: 50 | - type: Counted 51 | name: hibernate_session_closed_total 52 | doc: Global number of sessions opened (getSessionClosedCount) 53 | 54 | ConcurrentStatisticsImpl.flush()V: 55 | - type: Counted 56 | name: hibernate_flushed_total 57 | doc: The global number of flushes executed by sessions (getFlushCount) 58 | 59 | ConcurrentStatisticsImpl.connect()V: 60 | - type: Counted 61 | name: hibernate_connect_total 62 | doc: The global number of connections requested by the sessions (getConnectCount) 63 | 64 | 65 | # Statement Metrics 66 | ConcurrentStatisticsImpl.prepareStatement()V: 67 | - type: Counted 68 | name: hibernate_statement_prepared_total 69 | doc: The number of prepared statements that were acquired (getPrepareStatementCount) 70 | 71 | ConcurrentStatisticsImpl.closeStatement()V: 72 | - type: Counted 73 | name: hibernate_statement_closed_total 74 | doc: The number of prepared statements that were released (getCloseStatementCount) 75 | 76 | ConcurrentStatisticsImpl.endTransaction()V: 77 | - type: Counted 78 | name: hibernate_transaction_total 79 | doc: The number of transactions we know to have completed (getTransactionCount) 80 | 81 | 82 | 83 | # Entity Metrics 84 | ConcurrentStatisticsImpl.loadEntity(LString;)V: 85 | - type: Counted 86 | name: hibernate_entity_load_total 87 | doc: Global number of entity loads (loadEntity) 88 | labels: ['entity:$0'] 89 | 90 | ConcurrentStatisticsImpl.fetchEntity(LString;)V: 91 | - type: Counted 92 | name: hibernate_entity_fetch_total 93 | doc: Global number of entity fetches (fetchEntity) 94 | labels: ['entity:$0'] 95 | 96 | ConcurrentStatisticsImpl.updateEntity(LString;)V: 97 | - type: Counted 98 | name: hibernate_entity_update_total 99 | doc: Global number of entity updates (updateEntity) 100 | labels: ['entity:$0'] 101 | 102 | ConcurrentStatisticsImpl.insertEntity(LString;)V: 103 | - type: Counted 104 | name: hibernate_entity_insert_total 105 | doc: Global number of entity inserts (insertEntity) 106 | labels: ['entity:$0'] 107 | 108 | ConcurrentStatisticsImpl.deleteEntity(LString;)V: 109 | - type: Counted 110 | name: hibernate_entity_delete_total 111 | doc: Global number of entity deletes (deleteEntity) 112 | labels: ['entity:$0'] 113 | 114 | 115 | # Query Cache Metrics 116 | ConcurrentStatisticsImpl.queryCachePut(LString;LString;)V: 117 | - type: Counted 118 | name: hibernate_query_cache_put_total 119 | doc: The global number of cacheable queries put in cache (getQueryCachePutCount) 120 | 121 | ConcurrentStatisticsImpl.queryCacheHit(LString;LString;)V: 122 | - type: Counted 123 | name: hibernate_query_cache_hit_total 124 | doc: The global number of cached queries successfully retrieved from cache (getQueryCacheHitCount) 125 | 126 | ConcurrentStatisticsImpl.queryCacheMiss(LString;LString;)V: 127 | - type: Counted 128 | name: hibernate_query_cache_miss_total 129 | doc: The global number of cached queries not found in cache (getQueryCacheMissCount) 130 | 131 | # Query Cache Metrics (second level) 132 | ConcurrentStatisticsImpl.secondLevelCachePut(LString;)V: 133 | - type: Counted 134 | name: hibernate_second_level_cache_put_total 135 | doc: Global number of cacheable entities/collections put in the cache (getSecondLevelCachePutCount) 136 | 137 | ConcurrentStatisticsImpl.secondLevelCacheHit(LString;)V: 138 | - type: Counted 139 | name: hibernate_second_level_cache_hit_total 140 | doc: Global number of cacheable entities/collections successfully retrieved from the cache (getSecondLevelCacheHitCount) 141 | 142 | ConcurrentStatisticsImpl.secondLevelCacheMiss(LString;)V: 143 | - type: Counted 144 | name: hibernate_second_level_cache_miss_total 145 | doc: Global number of cacheable entities/collections not found in the cache and loaded from the database (getSecondLevelCacheMissCount) 146 | 147 | system: 148 | jvm: 149 | - gc 150 | - memory 151 | - threads -------------------------------------------------------------------------------- /metrics-agent-dist/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | metrics-agent 7 | 0.0.6-SNAPSHOT 8 | 9 | metrics-agent-dist 10 | jar 11 | 12 | 13 | 14 | ${project.groupId} 15 | metrics-agent-core 16 | ${project.version} 17 | 18 | 19 | 20 | 21 | 22 | prometheus 23 | 24 | 25 | ${project.groupId} 26 | metrics-agent-prometheus 27 | ${project.version} 28 | 29 | 30 | 31 | 32 | 33 | dropwizard 34 | 35 | 36 | ${project.groupId} 37 | metrics-agent-dropwizard 38 | ${project.version} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ${agent.artifact.name} 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-shade-plugin 51 | 2.3 52 | 53 | 54 | package 55 | 56 | shade 57 | 58 | 59 | 60 | 61 | *:* 62 | 63 | 64 | 65 | 66 | *:* 67 | 68 | META-INF/*.SF 69 | META-INF/*.DSA 70 | META-INF/*.RSA 71 | META-INF/*.RSA 72 | 73 | 74 | 75 | 76 | 77 | io.prometheus 78 | com.fleury.shaded.io.prometheus 79 | 80 | 81 | org 82 | com.fleury.shaded.org 83 | 84 | 85 | com.fasterxml.jackson 86 | com.fleury.shaded.com.fasterxml.jackson 87 | 88 | 89 | com.codahale 90 | com.fleury.shaded.com.codahale 91 | 92 | 93 | io.dropwizard.metrics 94 | com.fleury.shaded.io.dropwizard.metrics 95 | 96 | 97 | 98 | 99 | 100 | 101 | com.fleury.metrics.agent.Agent 102 | true 103 | 104 | NotSuitableAsMain 105 | Metrics Agent 106 | 1.0 107 | Will Fleury 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/reporter/TestMetricSystem.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * 11 | * @author Will Fleury 12 | */ 13 | public class TestMetricSystem implements MetricSystem { 14 | 15 | private final Map counters = new ConcurrentHashMap(); 16 | 17 | private final Map> timers = new ConcurrentHashMap>(); 18 | 19 | private int counterRegistrations = 0; 20 | private int gaugeRegistrations = 0; 21 | private int timerRegistrations = 0; 22 | 23 | @Override 24 | public void registerCounter(String name, String[] labelNames, String doc) { 25 | counterRegistrations++; 26 | } 27 | 28 | @Override 29 | public void registerGauge(String name, String[] labelNames, String doc) { 30 | gaugeRegistrations++; 31 | } 32 | 33 | @Override 34 | public void registerTimer(String name, String[] labelNames, String doc) { 35 | timerRegistrations++; 36 | } 37 | 38 | @Override 39 | public void recordCount(String name, String[] labelValues) { 40 | getOrAddCounter(counters, getName(name, createSingleLabelValue(labelValues))).incrementAndGet(); 41 | } 42 | 43 | @Override 44 | public void recordCount(String name, String[] labelValues, long n) { 45 | getOrAddCounter(counters, getName(name, createSingleLabelValue(labelValues))).addAndGet(n); 46 | } 47 | 48 | @Override 49 | public void recordGaugeInc(String name, String[] labelValues) { 50 | AtomicLong value = getOrAddCounter(counters, getName(name, createSingleLabelValue(labelValues))); 51 | value.incrementAndGet(); 52 | } 53 | 54 | @Override 55 | public void recordGaugeDec(String name, String[] labelValues) { 56 | AtomicLong value = getOrAddCounter(counters, getName(name, createSingleLabelValue(labelValues))); 57 | value.decrementAndGet(); 58 | } 59 | 60 | @Override 61 | public void recordTime(String name, String[] labelValues, long duration) { 62 | getOrAddTimer(timers, getName(name, createSingleLabelValue(labelValues))).add(duration); 63 | } 64 | 65 | @Override 66 | public void startDefaultEndpoint() { } 67 | 68 | public long getCount(String name) { 69 | return counters.containsKey(name) ? counters.get(name).get() : 0; 70 | } 71 | 72 | public long getCount(String name, String[] labelValues) { 73 | String key = getName(name, createSingleLabelValue(labelValues)); 74 | return counters.containsKey(key) ? counters.get(key).get() : 0; 75 | } 76 | 77 | public long[] getTimes(String name) { 78 | return convertBoxedToPrimitive(timers.get(name)); 79 | } 80 | 81 | public long[] getTimes(String name, String[] labelValues) { 82 | return convertBoxedToPrimitive(timers.get(getName(name, createSingleLabelValue(labelValues)))); 83 | } 84 | 85 | public int getCounterRegistrations() { 86 | return counterRegistrations; 87 | } 88 | 89 | public int getGaugeRegistrations() { 90 | return gaugeRegistrations; 91 | } 92 | 93 | public int getTimerRegistrations() { 94 | return timerRegistrations; 95 | } 96 | 97 | private String getName(String name, String labelValue) { 98 | return labelValue == null ? name : name + "." + labelValue; 99 | } 100 | 101 | private String createSingleLabelValue(String[] labelValues) { 102 | if (labelValues == null || labelValues.length == 0) { 103 | return null; 104 | } 105 | 106 | StringBuilder builder = new StringBuilder(); 107 | for (int i = 0; i < labelValues.length; i++) { 108 | builder.append(labelValues[i]); 109 | 110 | if (i < labelValues.length - 1) { 111 | builder.append("."); 112 | } 113 | } 114 | 115 | return builder.toString(); 116 | } 117 | 118 | public List getOrAddTimer(Map> registry, String name) { 119 | List value = registry.get(name); 120 | if (value == null) { 121 | value = new ArrayList(); 122 | 123 | List existing = registry.get(name); 124 | if (existing == null) { 125 | existing = registry.put(name, value); 126 | } 127 | 128 | if (existing != null) { 129 | throw new IllegalArgumentException("A metric named " + name + " already exists"); 130 | } 131 | } 132 | 133 | return value; 134 | } 135 | 136 | public AtomicLong getOrAddCounter(Map registry, String name) { 137 | AtomicLong value = registry.get(name); 138 | if (value == null) { 139 | value = new AtomicLong(); 140 | 141 | AtomicLong existing = registry.get(name); 142 | if (existing == null) { 143 | existing = registry.put(name, value); 144 | } 145 | 146 | if (existing != null) { 147 | throw new IllegalArgumentException("A metric named " + name + " already exists"); 148 | } 149 | } 150 | 151 | return value; 152 | } 153 | 154 | private long[] convertBoxedToPrimitive(List values) { 155 | long[] primitive = new long[values.size()]; 156 | 157 | for (int i = 0; i < values.size(); i++) { 158 | primitive[i] = values.get(i); 159 | } 160 | 161 | return primitive; 162 | } 163 | 164 | public void reset() { 165 | counters.clear(); 166 | timers.clear(); 167 | counterRegistrations = 0; 168 | gaugeRegistrations = 0; 169 | timerRegistrations = 0; 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/injectors/AbstractInjector.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static com.fleury.metrics.agent.model.LabelUtil.getLabelVarIndex; 4 | import static com.fleury.metrics.agent.model.LabelUtil.getNestedLabelVar; 5 | import static com.fleury.metrics.agent.model.LabelUtil.isLabelVarNested; 6 | import static com.fleury.metrics.agent.model.LabelUtil.isTemplatedLabelValue; 7 | import static com.fleury.metrics.agent.model.LabelUtil.isThis; 8 | import static com.fleury.metrics.agent.transformer.util.CollectionUtil.isNotEmpty; 9 | 10 | import com.fleury.metrics.agent.introspector.GenericClassIntrospector; 11 | import com.fleury.metrics.agent.model.LabelUtil; 12 | import com.fleury.metrics.agent.model.Metric; 13 | import com.fleury.metrics.agent.reporter.Reporter; 14 | import com.fleury.metrics.agent.transformer.util.OpCodeUtil; 15 | import java.util.List; 16 | import org.apache.commons.beanutils.PropertyUtils; 17 | import org.objectweb.asm.Opcodes; 18 | import org.objectweb.asm.Type; 19 | import org.objectweb.asm.commons.AdviceAdapter; 20 | 21 | /** 22 | * 23 | * @author Will Fleury 24 | */ 25 | public abstract class AbstractInjector implements Injector, Opcodes { 26 | 27 | public static final String METRIC_REPORTER_CLASSNAME = Type.getInternalName(Reporter.class); 28 | 29 | static { 30 | PropertyUtils.addBeanIntrospector(new GenericClassIntrospector()); 31 | } 32 | 33 | protected final AdviceAdapter aa; 34 | protected final Type[] argTypes; 35 | protected final int access; 36 | 37 | public AbstractInjector(AdviceAdapter aa, Type[] argTypes, int access) { 38 | this.aa = aa; 39 | this.argTypes = argTypes; 40 | this.access = access; 41 | } 42 | 43 | @Override 44 | public void injectAtMethodEnter() { 45 | } 46 | 47 | @Override 48 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 49 | } 50 | 51 | @Override 52 | public void injectAtMethodExit(int opcode) { 53 | } 54 | 55 | protected void injectNameAndLabelToStack(Metric metric) { 56 | int nameVar = aa.newLocal(Type.getType(String.class)); 57 | aa.visitLdcInsn(metric.getName()); 58 | aa.visitVarInsn(ASTORE, nameVar); 59 | 60 | List labelValues = LabelUtil.getLabelValues(metric.getLabels()); 61 | 62 | if (isNotEmpty(labelValues)) { 63 | int labelVar = injectLabelValuesArrayToStack(metric, labelValues); 64 | 65 | aa.visitVarInsn(ALOAD, nameVar); 66 | aa.visitVarInsn(ALOAD, labelVar); 67 | } else { 68 | aa.visitVarInsn(ALOAD, nameVar); 69 | aa.visitInsn(ACONST_NULL); 70 | } 71 | } 72 | 73 | protected int injectLabelValuesArrayToStack(Metric metric, List labelValues) { 74 | if (labelValues.size() > 5) { 75 | throw new IllegalStateException("Maximum labels per metric is 5. " 76 | + metric.getName() + " has " + labelValues.size()); 77 | } 78 | int labelVar = aa.newLocal(Type.getType(String[].class)); 79 | 80 | aa.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(labelValues.size())); 81 | aa.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); 82 | 83 | for (int i = 0; i < labelValues.size(); i++) { 84 | aa.visitInsn(DUP); 85 | aa.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(i)); 86 | injectLabelValueToStack(labelValues.get(i)); 87 | } 88 | 89 | aa.visitVarInsn(ASTORE, labelVar); 90 | 91 | return labelVar; 92 | } 93 | 94 | private void injectLabelValueToStack(String labelValue) { 95 | if (!isTemplatedLabelValue(labelValue)) { 96 | aa.visitLdcInsn(labelValue); 97 | } 98 | else { 99 | if (isThis(labelValue)) { 100 | aa.visitVarInsn(ALOAD, 0); //aa.loadThis(); 101 | } 102 | 103 | else { 104 | int argIndex = getLabelVarIndex(labelValue); 105 | 106 | boxParameterAndLoad(argIndex); 107 | } 108 | 109 | if (isLabelVarNested(labelValue)) { 110 | aa.visitLdcInsn(getNestedLabelVar(labelValue)); 111 | 112 | aa.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PropertyUtils.class), 113 | "getNestedProperty", 114 | Type.getMethodDescriptor( 115 | Type.getType(Object.class), 116 | Type.getType(Object.class), Type.getType(String.class)), 117 | false); 118 | } 119 | 120 | aa.visitMethodInsn(INVOKESTATIC, Type.getInternalName(String.class), 121 | "valueOf", 122 | Type.getMethodDescriptor( 123 | Type.getType(String.class), 124 | Type.getType(Object.class)), 125 | false); 126 | } 127 | 128 | aa.visitInsn(AASTORE); 129 | } 130 | 131 | private void boxParameterAndLoad(int argIndex) { 132 | Type type = argTypes[argIndex]; 133 | int stackIndex = getStackIndex(argIndex); 134 | 135 | switch (type.getSort()) { 136 | case Type.OBJECT: //no need to box Object 137 | aa.visitVarInsn(ALOAD, stackIndex); 138 | break; 139 | 140 | default: 141 | // aa.loadArg(argIndex); //doesn't work... 142 | aa.visitVarInsn(type.getOpcode(Opcodes.ILOAD), stackIndex); 143 | aa.valueOf(type); 144 | break; 145 | } 146 | } 147 | 148 | private int getStackIndex(int arg) { 149 | int index = (access & Opcodes.ACC_STATIC) == 0 ? 1 : 0; 150 | for (int i = 0; i < arg; i++) { 151 | index += argTypes[i].getSize(); 152 | } 153 | return index; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /metrics-agent-prometheus/src/main/java/com/fleury/metrics/agent/reporter/PrometheusMetricSystem.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import static java.util.logging.Level.WARNING; 4 | 5 | import io.prometheus.client.CollectorRegistry; 6 | import io.prometheus.client.Counter; 7 | import io.prometheus.client.Gauge; 8 | import io.prometheus.client.Histogram; 9 | import io.prometheus.client.exporter.HTTPServer; 10 | import io.prometheus.client.hotspot.ClassLoadingExports; 11 | import io.prometheus.client.hotspot.GarbageCollectorExports; 12 | import io.prometheus.client.hotspot.MemoryPoolsExports; 13 | import io.prometheus.client.hotspot.StandardExports; 14 | import io.prometheus.client.hotspot.ThreadExports; 15 | import java.util.HashSet; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Set; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.logging.Logger; 21 | 22 | /** 23 | * 24 | * @author Will Fleury 25 | */ 26 | public class PrometheusMetricSystem implements MetricSystem { 27 | 28 | private static final Logger LOGGER = Logger.getLogger(PrometheusMetricSystem.class.getName()); 29 | 30 | private static final Map COUNTERS = new ConcurrentHashMap(); 31 | private static final Map GAUGES = new ConcurrentHashMap(); 32 | private static final Map HISTOGRAMS = new ConcurrentHashMap(); 33 | 34 | private static final int DEFAULT_HTTP_PORT = 9899; 35 | 36 | private final Map configuration; 37 | 38 | protected PrometheusMetricSystem(Map configuration) { 39 | this.configuration = configuration; 40 | 41 | new StandardExports().register(); 42 | 43 | addJVMMetrics(configuration); 44 | 45 | startDefaultEndpoint(); 46 | } 47 | 48 | @Override 49 | public void registerCounter(String name, String[] labels, String doc) { 50 | Counter.Builder builder = Counter.build().name(name).help(doc); 51 | if (labels != null) { 52 | builder.labelNames(labels); 53 | } 54 | 55 | COUNTERS.put(name, builder.register()); 56 | } 57 | 58 | @Override 59 | public void registerGauge(String name, String[] labels, String doc) { 60 | Gauge.Builder builder = Gauge.build().name(name).help(doc); 61 | if (labels != null) { 62 | builder.labelNames(labels); 63 | } 64 | 65 | GAUGES.put(name, builder.register()); 66 | } 67 | 68 | @Override 69 | public void registerTimer(String name, String[] labels, String doc) { 70 | Histogram.Builder builder = Histogram.build().name(name).help(doc); 71 | if (labels != null) { 72 | builder.labelNames(labels); 73 | } 74 | 75 | HISTOGRAMS.put(name, builder.register()); 76 | } 77 | 78 | @Override 79 | public void recordCount(String name, String[] labels) { 80 | Counter counter = COUNTERS.get(name); 81 | if (labels != null) { 82 | counter.labels(labels).inc(); 83 | } else { 84 | counter.inc(); 85 | } 86 | } 87 | 88 | @Override 89 | public void recordGaugeInc(String name, String[] labelValues) { 90 | Gauge gauge = GAUGES.get(name); 91 | if (labelValues != null) { 92 | gauge.labels(labelValues).inc(); 93 | } else { 94 | gauge.inc(); 95 | } 96 | } 97 | 98 | @Override 99 | public void recordGaugeDec(String name, String[] labelValues) { 100 | Gauge gauge = GAUGES.get(name); 101 | if (labelValues != null) { 102 | gauge.labels(labelValues).dec(); 103 | } else { 104 | gauge.dec(); 105 | } 106 | } 107 | 108 | @Override 109 | public void recordCount(String name, String[] labels, long n) { 110 | Counter counter = COUNTERS.get(name); 111 | if (labels != null) { 112 | counter.labels(labels).inc(n); 113 | } else { 114 | counter.inc(n); 115 | } 116 | } 117 | 118 | @Override 119 | public void recordTime(String name, String[] labels, long duration) { 120 | Histogram summary = HISTOGRAMS.get(name); 121 | if (labels != null) { 122 | summary.labels(labels).observe(duration); 123 | } else { 124 | summary.observe(duration); 125 | } 126 | } 127 | 128 | @Override 129 | public void startDefaultEndpoint() { 130 | Thread thread = new Thread(new Runnable() { 131 | @Override 132 | public void run() { 133 | int port = DEFAULT_HTTP_PORT; 134 | 135 | if (configuration.containsKey("httpPort")) { 136 | port = Integer.parseInt((String)configuration.get("httpPort")); 137 | } 138 | 139 | try { 140 | LOGGER.fine("Starting Prometheus HttpServer on port " + port); 141 | 142 | new HTTPServer(port); 143 | 144 | } catch (Exception e) { //widen scope in case of ClassNotFoundException on non oracle/sun JVM 145 | LOGGER.log(WARNING, "Unable to register Prometheus HttpServer on port " + port, e); 146 | } 147 | } 148 | }); 149 | thread.setDaemon(true); 150 | thread.start(); 151 | } 152 | 153 | private void addJVMMetrics(Map configuration) { 154 | if (!configuration.containsKey("jvm")) { 155 | return; 156 | } 157 | Set jvmMetrics = new HashSet((List)configuration.get("jvm")); 158 | if (jvmMetrics.contains("gc")) { 159 | new GarbageCollectorExports().register(); 160 | } 161 | 162 | if (jvmMetrics.contains("threads")) { 163 | new ThreadExports().register(); 164 | } 165 | 166 | if (jvmMetrics.contains("memory")) { 167 | new MemoryPoolsExports().register(); 168 | } 169 | 170 | if (jvmMetrics.contains("classloader")) { 171 | new ClassLoadingExports().register(); 172 | } 173 | } 174 | 175 | void reset() { 176 | COUNTERS.clear(); 177 | GAUGES.clear(); 178 | HISTOGRAMS.clear(); 179 | CollectorRegistry.defaultRegistry.clear(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/GaugeInjectorTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import com.fleury.metrics.agent.annotation.Gauged; 7 | import com.fleury.metrics.agent.annotation.Gauged.mode; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | import org.junit.Test; 11 | 12 | /** 13 | * 14 | * @author Will Fleury 15 | */ 16 | public class GaugeInjectorTest extends BaseMetricTest { 17 | 18 | @Test 19 | public void shouldMeasureConstructorInFlight() throws Exception { 20 | final Class clazz = execute(GaugedConstructorClass.class); 21 | 22 | final CountDownLatch inProgressLatch = new CountDownLatch(1); 23 | final CountDownLatch callFinishedLatch = new CountDownLatch(1); 24 | 25 | startInNewThread(new Runnable() { 26 | @Override 27 | public void run() { 28 | try { 29 | clazz.getConstructor(CountDownLatch.class).newInstance(inProgressLatch); 30 | } catch (Exception e) { 31 | e.printStackTrace(); 32 | } finally { 33 | callFinishedLatch.countDown(); 34 | } 35 | } 36 | }); 37 | 38 | Thread.sleep(500); //wait to init thread creating new instance above 39 | 40 | assertEquals(1, metrics.getCount("constructor_initializing")); 41 | 42 | //allow request to finish 43 | inProgressLatch.countDown(); 44 | assertEquals(0, inProgressLatch.getCount()); 45 | 46 | callFinishedLatch.await(); 47 | 48 | //constructor has finished - so no in flight 49 | assertEquals(0, metrics.getCount("constructor_initializing")); 50 | } 51 | 52 | @Test 53 | public void shouldMeasureConstructorInFlightWithException() throws Exception { 54 | final Class clazz = execute(GaugedConstructorExceptionClass.class); 55 | 56 | final CountDownLatch inProgressLatch = new CountDownLatch(1); 57 | final CountDownLatch callFinishedLatch = new CountDownLatch(1); 58 | final AtomicBoolean exceptionOccurred = new AtomicBoolean(false); 59 | 60 | startInNewThread(new Runnable() { 61 | @Override 62 | public void run() { 63 | try { 64 | try { 65 | clazz.getConstructor(CountDownLatch.class).newInstance(inProgressLatch); 66 | } catch (Exception e) { 67 | if (e.getCause().getMessage().equals("Something bad..")) { 68 | exceptionOccurred.set(true); 69 | } 70 | } 71 | } catch (Exception e) { 72 | e.printStackTrace(); 73 | } finally { 74 | callFinishedLatch.countDown(); 75 | } 76 | } 77 | }); 78 | 79 | Thread.sleep(500); //wait to init thread creating new instance above 80 | 81 | assertEquals(1, metrics.getCount("constructor_initializing")); 82 | 83 | //allow request to finish 84 | inProgressLatch.countDown(); 85 | assertEquals(0, inProgressLatch.getCount()); 86 | 87 | callFinishedLatch.await(); 88 | 89 | //constructor has finished - so not in flight 90 | assertEquals(0, metrics.getCount("constructor_initializing")); 91 | assertTrue(exceptionOccurred.get()); 92 | } 93 | 94 | @Test 95 | public void shouldMeasureMethodInFlight() throws Exception { 96 | final Class clazz = execute(GaugedMethodClass.class); 97 | 98 | final CountDownLatch inProgressLatch = new CountDownLatch(1); 99 | final CountDownLatch callFinishedLatch = new CountDownLatch(1); 100 | 101 | startInNewThread(new Runnable() { 102 | @Override 103 | public void run() { 104 | try { 105 | Object obj = clazz.newInstance(); 106 | obj.getClass().getMethod("handleRequest", CountDownLatch.class).invoke(obj, inProgressLatch); 107 | } catch (Exception e) { 108 | e.printStackTrace(); 109 | } finally { 110 | callFinishedLatch.countDown(); 111 | } 112 | } 113 | }); 114 | 115 | Thread.sleep(500); //wait for init above and request method call 116 | 117 | assertEquals(1, metrics.getCount("request_handler")); 118 | 119 | //allow request to finish 120 | inProgressLatch.countDown(); 121 | assertEquals(0, inProgressLatch.getCount()); 122 | 123 | callFinishedLatch.await(); 124 | 125 | //request has finished - so no in flight 126 | assertEquals(0, metrics.getCount("constructor_initializing")); 127 | } 128 | 129 | public static class GaugedConstructorClass { 130 | 131 | @Gauged(name = "constructor_initializing", mode = mode.in_flight) 132 | public GaugedConstructorClass(CountDownLatch latch) { 133 | try { 134 | latch.await(); 135 | } catch (InterruptedException e) {} 136 | } 137 | } 138 | 139 | public static class GaugedConstructorExceptionClass { 140 | 141 | @Gauged(name = "constructor_initializing", mode = mode.in_flight) 142 | public GaugedConstructorExceptionClass(CountDownLatch latch) { 143 | try { 144 | latch.await(); 145 | } catch (InterruptedException e) {} 146 | 147 | callService(); 148 | } 149 | 150 | public final void callService() { 151 | BaseMetricTest.performBasicTask(); 152 | throw new RuntimeException("Something bad.."); 153 | } 154 | } 155 | 156 | public static class GaugedMethodClass { 157 | 158 | @Gauged(name = "request_handler", mode = mode.in_flight) 159 | public void handleRequest(CountDownLatch latch) { 160 | try { 161 | latch.await(); 162 | } catch (InterruptedException e) {} 163 | } 164 | } 165 | 166 | private void startInNewThread(Runnable r) { 167 | new Thread(r).start(); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /metrics-agent-core/src/test/java/com/fleury/metrics/agent/transformer/visitors/injectors/LabelsTest.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors.injectors; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import com.fleury.metrics.agent.annotation.Counted; 6 | import org.junit.Test; 7 | 8 | /** 9 | * 10 | * @author Will Fleury 11 | */ 12 | public class LabelsTest extends BaseMetricTest { 13 | 14 | @Test 15 | public void shouldCountConstructorInvocationWithLabels() throws Exception { 16 | testInvocation(CountedConstructorWithLabelsClass.class, new String[]{"value1", "value2"}); 17 | } 18 | 19 | @Test 20 | public void shouldCountConstructorInvocationWithoutLabels() throws Exception { 21 | testInvocation(CountedConstructorWithoutLabelsClass.class, null); 22 | } 23 | 24 | @Test 25 | public void shouldCountConstructorInvocationWithEmptyLabels() throws Exception { 26 | testInvocation(CountedConstructorWithEmptyLabelsClass.class, null); 27 | } 28 | 29 | @Test 30 | public void shouldCountConstructorInvocationWithDynamicStringLabelValue() throws Exception { 31 | testInvocationWithArgs(CountedConstructorWithDynamicStringLabelValueClass.class, 32 | new Object[]{"hello"}, new String[]{"hello"}); 33 | } 34 | 35 | @Test 36 | public void shouldCountMethodInvocationWithDynamicValueThis() throws Exception { 37 | testMethodInvocation(CountedMethodWithDynamicLabelValueThisClass.class, new String[]{"hello"}); 38 | } 39 | 40 | @Test(expected = IllegalArgumentException.class) 41 | public void shouldThrowExceptionWithInvalueDynamicLabelValueThisInConstructor() throws Exception { 42 | testInvocation(CountedConstructorWithDynamicLabelValueThisClass.class, new String[]{"hello"}); 43 | } 44 | 45 | @Test 46 | public void shouldCountConstructorInvocationWithDynamicLongValue() throws Exception { 47 | testInvocationWithArgs(CountedConstructorWithDynamicLongLabelValueClass.class, 48 | new Object[]{0, 5}, new String[]{"5"}); 49 | } 50 | 51 | @Test 52 | public void shouldCountConstructorInvocationWithDynamicNestedValue() throws Exception { 53 | testInvocationWithArgs(CountedConstructorWithDynamicNestedLabelValueClass.class, 54 | new Object[]{new CountedConstructorWithDynamicNestedLabelValueClass.Nester()}, new String[]{"hello"}); 55 | } 56 | 57 | @Test 58 | public void shouldCountConstructorInvocationWithDynamicNestedNonJavaBeanValue() throws Exception { 59 | testInvocationWithArgs(CountedConstructorWithDynamicNestedNonJavaBeanLabelValueClass.class, 60 | new Object[]{new CountedConstructorWithDynamicNestedNonJavaBeanLabelValueClass.Nester()}, new String[]{"hello"}); 61 | } 62 | 63 | @Test(expected = IllegalArgumentException.class) 64 | public void shouldThrowExceptionWhenInvalidParamIndexLabelValue() throws Exception { 65 | testInvocationWithArgs(CountedConstructorWithInvalidParamIndexLabelValueClass.class, 66 | new Object[]{5}, new String[]{"5"}); 67 | } 68 | 69 | @Test(expected = IllegalArgumentException.class) 70 | public void shouldThrowExceptionWhenInvalidDynamicLabelValue() throws Exception { 71 | testInvocationWithArgs(CountedConstructorWithInvalidDynamicLabelValueClass.class, 72 | new Object[]{5}, new String[]{"5"}); 73 | } 74 | 75 | private void testInvocation(Class instrumentClazz, String[] labelValues) throws Exception { 76 | Class clazz = execute(instrumentClazz); 77 | 78 | clazz.newInstance(); 79 | 80 | assertEquals(1, metrics.getCount("constructor", labelValues)); 81 | } 82 | 83 | private void testInvocationWithArgs(Class instrumentClazz, Object[] args, String[] labelValues) throws Exception { 84 | Class clazz = execute(instrumentClazz); 85 | 86 | clazz.getConstructors()[0].newInstance(args); 87 | 88 | assertEquals(1, metrics.getCount("constructor", labelValues)); 89 | } 90 | 91 | private void testMethodInvocation(Class instrumentClazz, String[] labelValues) throws Exception { 92 | Class clazz = execute(instrumentClazz); 93 | Object obj = clazz.newInstance(); 94 | 95 | obj.getClass().getMethod("method").invoke(obj); 96 | 97 | assertEquals(1, metrics.getCount("method", labelValues)); 98 | } 99 | 100 | public static class CountedConstructorWithLabelsClass { 101 | 102 | @Counted(name = "constructor", labels = {"name1:value1", "name2:value2"}) 103 | public CountedConstructorWithLabelsClass() { 104 | BaseMetricTest.performBasicTask(); 105 | } 106 | } 107 | 108 | public static class CountedConstructorWithoutLabelsClass { 109 | 110 | @Counted(name = "constructor") 111 | public CountedConstructorWithoutLabelsClass() { 112 | BaseMetricTest.performBasicTask(); 113 | } 114 | } 115 | 116 | public static class CountedConstructorWithEmptyLabelsClass { 117 | 118 | @Counted(name = "constructor", labels = {}) 119 | public CountedConstructorWithEmptyLabelsClass() { 120 | BaseMetricTest.performBasicTask(); 121 | } 122 | } 123 | 124 | public static class CountedConstructorWithDynamicLabelValueThisClass { 125 | @Counted(name = "constructor", labels = {"name1:$this"}) 126 | public CountedConstructorWithDynamicLabelValueThisClass() { 127 | BaseMetricTest.performBasicTask(); 128 | } 129 | } 130 | 131 | public static class CountedMethodWithDynamicLabelValueThisClass { 132 | 133 | public CountedMethodWithDynamicLabelValueThisClass() { 134 | } 135 | 136 | @Counted(name = "method", labels = {"name1:$this"}) 137 | public void method() { 138 | BaseMetricTest.performBasicTask(); 139 | } 140 | 141 | @Override 142 | public String toString() { 143 | return "hello"; 144 | } 145 | } 146 | 147 | public static class CountedConstructorWithDynamicStringLabelValueClass { 148 | 149 | @Counted(name = "constructor", labels = {"name1:$0"}) 150 | public CountedConstructorWithDynamicStringLabelValueClass(String value) { 151 | BaseMetricTest.performBasicTask(); 152 | } 153 | } 154 | 155 | public static class CountedConstructorWithDynamicLongLabelValueClass { 156 | 157 | @Counted(name = "constructor", labels = {"name1:$1"}) 158 | public CountedConstructorWithDynamicLongLabelValueClass(long rand, long value) { 159 | BaseMetricTest.performBasicTask(); 160 | } 161 | } 162 | 163 | public static class CountedConstructorWithDynamicNestedLabelValueClass { 164 | 165 | public static class Nester { 166 | public String getHello() { 167 | return "hello"; 168 | } 169 | } 170 | 171 | @Counted(name = "constructor", labels = {"name1:$0.hello"}) 172 | public CountedConstructorWithDynamicNestedLabelValueClass(Nester nester) { 173 | BaseMetricTest.performBasicTask(); 174 | } 175 | } 176 | 177 | public static class CountedConstructorWithDynamicNestedNonJavaBeanLabelValueClass { 178 | 179 | public static class Nester { 180 | public String hello() { 181 | return "hello"; 182 | } 183 | } 184 | 185 | @Counted(name = "constructor", labels = {"name1:$0.hello"}) 186 | public CountedConstructorWithDynamicNestedNonJavaBeanLabelValueClass(Nester nester) { 187 | BaseMetricTest.performBasicTask(); 188 | } 189 | } 190 | 191 | 192 | 193 | public static class CountedConstructorWithInvalidParamIndexLabelValueClass { 194 | 195 | @Counted(name = "constructor", labels = {"name1:$5"}) 196 | public CountedConstructorWithInvalidParamIndexLabelValueClass(long value) { 197 | BaseMetricTest.performBasicTask(); 198 | } 199 | } 200 | 201 | public static class CountedConstructorWithInvalidDynamicLabelValueClass { 202 | 203 | @Counted(name = "constructor", labels = {"name1:$badlabel"}) 204 | public CountedConstructorWithInvalidDynamicLabelValueClass(long value) { 205 | BaseMetricTest.performBasicTask(); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/ASMClassWriter.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer; 2 | 3 | import static java.util.logging.Level.FINER; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import java.util.logging.Logger; 10 | import org.objectweb.asm.ClassReader; 11 | import org.objectweb.asm.ClassWriter; 12 | import org.objectweb.asm.Opcodes; 13 | 14 | /** 15 | * We need to override the getCommonSuperClass method of ClassWriter as the default implementation (don't know why) 16 | * triggers loading of classes used in the common super class resolution. This causes linkage and circular loading 17 | * issues and has been a real nightmare to sort out.. There wasn't anywhere on the AMS documentation or warnings 18 | * that I found this.. 19 | * 20 | * The solution is taken from 21 | * https://github.com/naver/pinpoint/blob/master/profiler/src/main/java/com/navercorp/pinpoint/profiler/instrument/ASMClassWriter.java 22 | */ 23 | 24 | public final class ASMClassWriter extends ClassWriter { 25 | 26 | private static final Logger LOGGER = Logger.getLogger(ASMClassWriter.class.getName()); 27 | 28 | private static final String OBJECT_CLASS_INTERNAL_NAME = "java/lang/Object"; 29 | 30 | private ClassLoader classLoader; 31 | 32 | public ASMClassWriter(final int flags, final ClassLoader classLoader) { 33 | super(flags); 34 | this.classLoader = classLoader; 35 | } 36 | 37 | @Override 38 | protected String getCommonSuperClass(String classInternalName1, String classInternalName2) { 39 | return get(classInternalName1, classInternalName2); 40 | } 41 | 42 | 43 | private String get(final String classInternalName1, final String classInternalName2) { 44 | if (classInternalName1 == null || classInternalName1.equals(OBJECT_CLASS_INTERNAL_NAME) 45 | || classInternalName2 == null || classInternalName2.equals(OBJECT_CLASS_INTERNAL_NAME)) { 46 | // object is the root of the class hierarchy. 47 | return OBJECT_CLASS_INTERNAL_NAME; 48 | } 49 | 50 | if (classInternalName1.equals(classInternalName2)) { 51 | // two equal. 52 | return classInternalName1; 53 | } 54 | 55 | final ClassReader classReader1 = getClassReader(classInternalName1); 56 | if (classReader1 == null) { 57 | LOGGER.log(FINER, "Skip getCommonSuperClass(). not found class {0}", classInternalName1); 58 | return OBJECT_CLASS_INTERNAL_NAME; 59 | } 60 | 61 | final ClassReader classReader2 = getClassReader(classInternalName2); 62 | if (classReader2 == null) { 63 | LOGGER.log(FINER, "Skip getCommonSuperClass(). not found class {0}", classInternalName2); 64 | return OBJECT_CLASS_INTERNAL_NAME; 65 | } 66 | 67 | // interface. 68 | if (isInterface(classReader1)) { 69 | // or 70 | return getCommonInterface(classReader1, classReader2); 71 | } 72 | 73 | // interface. 74 | if (isInterface(classReader2)) { 75 | // 76 | return getCommonInterface(classReader2, classReader1); 77 | } 78 | 79 | // class. 80 | // 81 | return getCommonClass(classReader1, classReader2); 82 | } 83 | 84 | private boolean isInterface(final ClassReader classReader) { 85 | return (classReader.getAccess() & Opcodes.ACC_INTERFACE) != 0; 86 | } 87 | 88 | // or 89 | private String getCommonInterface(final ClassReader classReader1, final ClassReader classReader2) { 90 | final Set interfaceHierarchy = new HashSet(); 91 | traversalInterfaceHierarchy(interfaceHierarchy, classReader1); 92 | 93 | if (isInterface(classReader2)) { 94 | if (interfaceHierarchy.contains(classReader2.getClassName())) { 95 | return classReader2.getClassName(); 96 | } 97 | } 98 | 99 | final String interfaceInternalName = getImplementedInterface(interfaceHierarchy, classReader2); 100 | if (interfaceInternalName != null) { 101 | return interfaceInternalName; 102 | } 103 | return OBJECT_CLASS_INTERNAL_NAME; 104 | } 105 | 106 | private void traversalInterfaceHierarchy(final Set interfaceHierarchy, final ClassReader classReader) { 107 | if (classReader != null && interfaceHierarchy.add(classReader.getClassName())) { 108 | for (String interfaceInternalName : classReader.getInterfaces()) { 109 | traversalInterfaceHierarchy(interfaceHierarchy, getClassReader(interfaceInternalName)); 110 | } 111 | } 112 | } 113 | 114 | private String getImplementedInterface(final Set interfaceHierarchy, final ClassReader classReader) { 115 | ClassReader cr = classReader; 116 | while (cr != null) { 117 | final String[] interfaceInternalNames = cr.getInterfaces(); 118 | for (String name : interfaceInternalNames) { 119 | if (name != null && interfaceHierarchy.contains(name)) { 120 | return name; 121 | } 122 | } 123 | 124 | for (String name : interfaceInternalNames) { 125 | final String interfaceInternalName = getImplementedInterface(interfaceHierarchy, getClassReader(name)); 126 | if (interfaceInternalName != null) { 127 | return interfaceInternalName; 128 | } 129 | } 130 | 131 | final String superClassInternalName = cr.getSuperName(); 132 | if (superClassInternalName == null || superClassInternalName.equals(OBJECT_CLASS_INTERNAL_NAME)) { 133 | break; 134 | } 135 | cr = getClassReader(superClassInternalName); 136 | } 137 | 138 | return null; 139 | } 140 | 141 | private String getCommonClass(final ClassReader classReader1, final ClassReader classReader2) { 142 | final Set classHierarchy = new HashSet(); 143 | classHierarchy.add(classReader1.getClassName()); 144 | classHierarchy.add(classReader2.getClassName()); 145 | 146 | String superClassInternalName1 = classReader1.getSuperName(); 147 | if (!classHierarchy.add(superClassInternalName1)) { 148 | // find common super class. 149 | return superClassInternalName1; 150 | } 151 | 152 | String superClassInternalName2 = classReader2.getSuperName(); 153 | if (!classHierarchy.add(superClassInternalName2)) { 154 | // find common super class. 155 | return superClassInternalName2; 156 | } 157 | 158 | while (superClassInternalName1 != null || superClassInternalName2 != null) { 159 | if (superClassInternalName1 != null) { 160 | superClassInternalName1 = getSuperClassInternalName(superClassInternalName1); 161 | if (superClassInternalName1 != null) { 162 | if (!classHierarchy.add(superClassInternalName1)) { 163 | return superClassInternalName1; 164 | } 165 | } 166 | } 167 | 168 | if (superClassInternalName2 != null) { 169 | superClassInternalName2 = getSuperClassInternalName(superClassInternalName2); 170 | if (superClassInternalName2 != null) { 171 | if (!classHierarchy.add(superClassInternalName2)) { 172 | return superClassInternalName2; 173 | } 174 | } 175 | } 176 | } 177 | 178 | return OBJECT_CLASS_INTERNAL_NAME; 179 | } 180 | 181 | 182 | private String getSuperClassInternalName(final String classInternalName) { 183 | final ClassReader classReader = getClassReader(classInternalName); 184 | if (classReader == null) { 185 | return null; 186 | } 187 | 188 | return classReader.getSuperName(); 189 | } 190 | 191 | private ClassReader getClassReader(final String classInternalName) { 192 | if (classInternalName == null || classLoader == null) { 193 | return null; 194 | } 195 | 196 | InputStream in = null; 197 | try { 198 | in = classLoader.getResourceAsStream(classInternalName + ".class"); 199 | if (in != null) { 200 | return new ClassReader(in); 201 | } 202 | } catch (IOException ignored) { 203 | // not found class. 204 | } finally { 205 | if (in != null) { 206 | try { 207 | in.close(); 208 | } catch (IOException ignored) { 209 | } 210 | } 211 | } 212 | 213 | return null; 214 | } 215 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /metrics-agent-core/src/main/java/com/fleury/metrics/agent/config/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | import static java.util.logging.Level.FINE; 4 | 5 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | import com.fasterxml.jackson.core.JsonParser; 9 | import com.fasterxml.jackson.databind.DeserializationContext; 10 | import com.fasterxml.jackson.databind.DeserializationFeature; 11 | import com.fasterxml.jackson.databind.KeyDeserializer; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.module.SimpleModule; 14 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; 15 | import com.fleury.metrics.agent.model.Metric; 16 | 17 | 18 | import java.io.FileInputStream; 19 | import java.io.FileNotFoundException; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Set; 30 | import java.util.logging.Logger; 31 | import org.objectweb.asm.Type; 32 | 33 | /** 34 | * 35 | * @author Will Fleury 36 | */ 37 | public class Configuration { 38 | 39 | private static final Logger LOGGER = Logger.getLogger(Configuration.class.getName()); 40 | 41 | public static String dotToSlash(String name) { 42 | return name.replaceAll("\\.", "/"); 43 | } 44 | 45 | private final static ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()) { 46 | { 47 | configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 48 | configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true); 49 | setVisibilityChecker(getSerializationConfig().getDefaultVisibilityChecker() 50 | .withFieldVisibility(JsonAutoDetect.Visibility.ANY)); 51 | registerModule(new ConfigSimpleModule()); 52 | } 53 | }; 54 | 55 | public static Configuration createConfig(String filename) { 56 | if (filename == null) { 57 | return emptyConfiguration(); 58 | } 59 | 60 | LOGGER.log(FINE, "Found config file: {0}", filename); 61 | 62 | try { 63 | return createConfig(new FileInputStream(filename)); 64 | } catch (FileNotFoundException e) { 65 | throw new RuntimeException(e); 66 | } 67 | } 68 | 69 | public static Configuration createConfig(InputStream is) { 70 | try { 71 | return MAPPER.readValue(is, Configuration.class); 72 | } catch (Exception e) { 73 | throw new RuntimeException(e); 74 | } finally { 75 | try { 76 | is.close(); 77 | } catch (IOException e) {} 78 | } 79 | } 80 | 81 | public static Configuration emptyConfiguration() { 82 | return new Configuration(); 83 | } 84 | 85 | private final Set imports; 86 | private final Map> metrics; 87 | private final Map system; 88 | private final List whiteList; 89 | private final List blackList; 90 | 91 | private Configuration() { 92 | this(new HashMap>(), 93 | Collections.emptySet(), 94 | Collections.emptyMap(), 95 | Collections.emptyList(), 96 | Collections.emptyList()); 97 | } 98 | 99 | @JsonCreator 100 | public Configuration( 101 | @JsonProperty("metrics") Map> metrics, 102 | @JsonProperty("imports") Set imports, 103 | @JsonProperty("system") Map system, 104 | @JsonProperty("whiteList") List whiteList, 105 | @JsonProperty("blackList") List blackList) { 106 | 107 | this.imports = imports == null ? Collections.emptySet() : imports; 108 | 109 | this.metrics = metrics == null ? 110 | new HashMap>() : 111 | processClassImports(metrics, this.imports); //ensure fqn expanded from imports 112 | 113 | this.system = system == null ? Collections.emptyMap() : system; 114 | this.whiteList = whiteList == null ? Collections.emptyList() : whiteList; 115 | this.blackList = blackList == null ? Collections.emptyList() : blackList; 116 | } 117 | 118 | private static Map> processClassImports(Map> metrics, Set imports) { 119 | Map expandedKeys = fqnToMap(imports); 120 | 121 | Map> processed = new HashMap>(); 122 | for (Map.Entry> entry : metrics.entrySet()) { 123 | Key key = entry.getKey(); 124 | 125 | String fqn = expandedKeys.get(key.getClassName()); 126 | if (fqn == null) { 127 | fqn = key.getClassName(); 128 | } 129 | 130 | String descriptor = key.descriptor; 131 | 132 | Map fqnMap = getMethodDescriptorFQNMap(descriptor); 133 | for (String className : fqnMap.keySet()) { 134 | if (expandedKeys.containsKey(className)) { 135 | descriptor = descriptor.replaceAll(className, expandedKeys.get(className)); 136 | } 137 | } 138 | 139 | key = new Key(fqn, key.getMethod(), descriptor); 140 | 141 | processed.put(key, entry.getValue()); 142 | } 143 | 144 | return processed; 145 | } 146 | 147 | 148 | public boolean isMetric(String className) { 149 | for (Key key : metrics.keySet()) { 150 | if (key.className.equals(className)) { 151 | return true; 152 | } 153 | } 154 | 155 | return false; 156 | } 157 | 158 | public List findMetrics(String className) { 159 | if (metrics.isEmpty()) return Collections.emptyList(); 160 | 161 | List found = new ArrayList(); 162 | for (Key key : metrics.keySet()) { 163 | if (key.className.equals(className)) { 164 | found.addAll(metrics.get(key)); 165 | } 166 | } 167 | 168 | return found; 169 | } 170 | 171 | public List findMetrics(String className, String method, String descriptor) { 172 | Key key = new Key(className, method, descriptor); 173 | return metrics.containsKey(key) ? metrics.get(key) : Collections.emptyList(); 174 | } 175 | 176 | public void addMetric(Key key, Metric metric) { 177 | List keyMetrics = metrics.get(key); 178 | 179 | if (keyMetrics == null) { 180 | keyMetrics = new ArrayList(); 181 | metrics.put(key, keyMetrics); 182 | } 183 | 184 | keyMetrics.add(metric); 185 | } 186 | 187 | public Map getSystem() { 188 | return system; 189 | } 190 | 191 | public List getWhiteList() { 192 | return whiteList; 193 | } 194 | 195 | public List getBlackList() { 196 | return blackList; 197 | } 198 | 199 | public boolean isWhiteListed(String className) { 200 | if (whiteList.isEmpty()) return true; 201 | 202 | for (String white : whiteList) { 203 | if (className.startsWith(white)) { 204 | return true; 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | 211 | public boolean isBlackListed(String className) { 212 | if (blackList.isEmpty()) return false; 213 | 214 | for (String black : blackList) { 215 | if (className.startsWith(black)) { 216 | return true; 217 | } 218 | } 219 | 220 | return false; 221 | } 222 | 223 | @Override 224 | public String toString() { 225 | return "Configuration{" + 226 | "metrics=" + metrics + 227 | ", system=" + system + 228 | ", whiteList=" + whiteList + 229 | ", blackList=" + blackList + 230 | '}'; 231 | } 232 | 233 | public static Map fqnToMap(Collection classNames) { 234 | Map expandedKeys = new HashMap(); 235 | for (String fqn : classNames) { 236 | String className = fqn.substring(fqn.lastIndexOf("/") + 1, fqn.length()); 237 | expandedKeys.put(className, fqn); 238 | } 239 | 240 | return expandedKeys; 241 | } 242 | 243 | public static Map getMethodDescriptorFQNMap(String descriptor) { 244 | Type type = Type.getMethodType(descriptor); 245 | 246 | Set classes = new HashSet(); 247 | classes.add(type.getReturnType().getClassName()); 248 | 249 | Type[] arguments = type.getArgumentTypes(); 250 | if (arguments != null) { 251 | for (Type arg : arguments) { 252 | classes.add(arg.getClassName()); 253 | } 254 | } 255 | 256 | return fqnToMap(classes); 257 | } 258 | 259 | public static class Key { 260 | 261 | private final String className; 262 | private final String method; 263 | private final String descriptor; 264 | 265 | public Key(String className, String method, String descriptor) { 266 | this.className = className; 267 | this.method = method; 268 | this.descriptor = descriptor; 269 | } 270 | 271 | public String getClassName() { 272 | return className; 273 | } 274 | 275 | public String getMethod() { 276 | return method; 277 | } 278 | 279 | public String getDescriptor() { 280 | return descriptor; 281 | } 282 | 283 | @Override 284 | public int hashCode() { 285 | int hash = 3; 286 | hash = 29 * hash + (this.className != null ? this.className.hashCode() : 0); 287 | hash = 29 * hash + (this.method != null ? this.method.hashCode() : 0); 288 | hash = 29 * hash + (this.descriptor != null ? this.descriptor.hashCode() : 0); 289 | return hash; 290 | } 291 | 292 | @Override 293 | public boolean equals(Object obj) { 294 | if (obj == null) { 295 | return false; 296 | } 297 | if (getClass() != obj.getClass()) { 298 | return false; 299 | } 300 | final Key other = (Key) obj; 301 | if ((this.className == null) ? (other.className != null) : !this.className.equals(other.className)) { 302 | return false; 303 | } 304 | if ((this.method == null) ? (other.method != null) : !this.method.equals(other.method)) { 305 | return false; 306 | } 307 | if ((this.descriptor == null) ? (other.descriptor != null) : !this.descriptor.equals(other.descriptor)) { 308 | return false; 309 | } 310 | return true; 311 | } 312 | 313 | @Override 314 | public String toString() { 315 | return "Key{" + 316 | "className='" + className + '\'' + 317 | ", method='" + method + '\'' + 318 | ", desc='" + descriptor + '\'' + 319 | '}'; 320 | } 321 | } 322 | 323 | static class MetricKey extends KeyDeserializer { 324 | 325 | @Override 326 | public Object deserializeKey(final String key, final DeserializationContext ctxt) throws IOException { 327 | String className = dotToSlash(key.substring(0, key.lastIndexOf("."))); 328 | String methodName = key.substring(key.lastIndexOf(".") + 1, key.indexOf("(")); 329 | 330 | String desc = key.substring(key.indexOf("("), key.length()); 331 | 332 | return new Key(className, methodName, desc); 333 | } 334 | } 335 | 336 | static class ConfigSimpleModule extends SimpleModule { 337 | 338 | public ConfigSimpleModule() { 339 | addKeyDeserializer(Key.class, new MetricKey()); 340 | } 341 | 342 | } 343 | 344 | } 345 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [Overview](#overview) 2 | - [Motivation](#motivation) 3 | - [Code Bloat](#code-bloat) 4 | - [Instrumentation Metadata](#instrumentation-metadata) 5 | - [Annotations](#annotations) 6 | - [Configuration](#configuration) 7 | - [Class Imports](#class-imports) 8 | - [Metric Labels](#metric-labels) 9 | - [Dynamic Label Values](#dynamic-label-values) 10 | - [What we actually Transform](#what-we-actually-transform) 11 | - [Supported Languages](#supported-languages) 12 | - [Supported Metrics Systems](#supported-metrics-systems) 13 | - [Prometheus](#prometheus) 14 | - [Dropwizard](#dropwizard) 15 | - [Metric System Configuration](#metric-system-configuration) 16 | - [Adding JVM Level Metric Information](#adding-jvm-level-metric-information) 17 | - [Agent Reporting](#agent-reporting) 18 | - [Black & Black Lists](#black-and-white-lists) 19 | - [Logger Configuration](#logger-configuration) 20 | - [Performance](#performance) 21 | - [Dependencies](#dependencies) 22 | - [Binaries & Releases](#binaries-releases) 23 | - [Building](#building) 24 | - [Usage](#usage) 25 | - [Debugging](#debugging) 26 | - [Examples](#examples) 27 | 28 | # Overview 29 | 30 | ## Motivation 31 | Agent based bytecode instrumentation is a far more elegant, faster and safer approach to instrumenting code on the JVM. Programmatic addition of metrics into client code leads to severe code bloat and lack of clarity of the underlying business logic. 32 | 33 | Annotation driven instrumentation using dependency injection frameworks such as Spring or Guice are an attempt to reduce the coat bloat caused by manual instrumentation. However, a clear advantage of agent based bytecode instrumentation is quite simple, you don't need to be using Spring or Guice to benefit from it. Another issue with such annotation driven DI frameworks is that they can only inject the logic on code you own and 3rd party libraries cannot be instrumented. The agent does not care if the code you want to instrument is yours, a third party library or the JDK itself. 34 | 35 | The ability to quickly update a configuration file indicating the metrics and code locations we want to measure, and simply restart the application to begin gathering new measurements is invaluable. It saves a considerable amount of developer time and results in faster performance debugging sessions. 36 | 37 | Finally, because this library provides a plugable provider interface, it means switching between different reporting systems does not need to be an ordeal. I've worked on projects that have had two or three different metrics libraries used and converters going between each to the backend monitoring system. Simply put, this is vile and makes code bloat even worse. With this library, if you wish to change provider, simply add an implementation of the provider of choice. As the metrics are not in the source code, there is no further change required and no technical debt added. 38 | 39 | 40 | ### Code Bloat Problem 41 | 42 | Say we want to instrument a method which calls some third party library or service and tracks the number of failures (as exceptions thrown). To do this we need to track both the total number of method invocations and the number of failed invocations. Most of the time in modern Java libraries, exceptions are unchecked which allows them to propagate up to an appropriate handler without polluting the code base. 43 | 44 | The following is an example of a basic block of code which performs a basic service call prior 45 | to instrumentation 46 | 47 | ```java 48 | public Result performSomeTask() { 49 | return callSomeServiceMethodWhichCanThrowException(createArgs()); 50 | } 51 | ``` 52 | 53 | To instrument this programmatically we perform the following 54 | 55 | ```java 56 | // add class fields 57 | 58 | static final Counter total = Metrics.createCounter("requests_total"); 59 | static final Counter failed = Metrics.createCounter("requests_failed"); 60 | 61 | public Result performSomeTask() { 62 | total.inc(); 63 | 64 | Result result = null; 65 | try { 66 | //perform actual original call 67 | result = callSomeServiceMethodWhichCanThrowException(createArgs()); 68 | } catch (Exception e) { 69 | failed.inc(); 70 | throw e; 71 | } 72 | 73 | return result; 74 | } 75 | ``` 76 | 77 | Now lets add a timer to this also so we can see how long the method call takes. 78 | 79 | ```java 80 | // add class fields 81 | 82 | static final Counter total = Metrics.createCounter("requests_total"); 83 | static final Counter failed = Metrics.createCounter("requests_failed"); 84 | static final Timer timer = Metrics.createTimer("requests_timer"); 85 | 86 | public Result performSomeTask() { 87 | long startTime = System.nanoTime(); 88 | total.inc(); 89 | 90 | Result result = null; 91 | try { 92 | result = callSomeServiceMethodWhichCanThrowException(createArgs()); 93 | } catch (Exception e) { 94 | failed.inc(); 95 | throw e; 96 | } finally { 97 | timer.record(System.nanoTime() - startTime); 98 | } 99 | 100 | return result; 101 | } 102 | ``` 103 | 104 | WOW! That turned ugly fast! We started with 3 LOC (lines of code) representing the business logic and ended up with 17 LOC, 14 of which were due to our metrics. This has the potential to destroy the clarity of a code base. 105 | 106 | With agent based instrumentation, we can inject the exact same method bytecode as would be produced by writing it manually, but without touching the source. 107 | 108 | 109 | ## Instrumentation Metadata 110 | 111 | For those who like marking methods to measure programmatically, we provide annotations to do just that. We also provide a configuration driven system where you define the methods you want to instrument in a yaml format file. We encourage the configuration driven approach over annotations. 112 | 113 | How all the metric types are use should be self explanatory with the exception of Gauges. We use Gauges to track the number of invocations of a particular method or constructor that are `in flight`. That effectively means we increment the gauge value as the method enters and decrements it when it exits. This is very useful for things like Http Request Handlers etc where you want to know the number of in flight requests. 114 | 115 | 116 | ### Annotations 117 | 118 | ```java 119 | @Counted (name = "", labels = { }, doc = "") 120 | @Gauged (name = "", mode=in_flight, labels = { }, doc = "") 121 | @Timed (name = "", labels = { }, doc = "") 122 | @ExceptionCounted (name = "", labels = { }, doc = "") 123 | ``` 124 | 125 | Annotations are provided for all metric types and can be added to methods including 126 | constructors. 127 | 128 | ```java 129 | @Counted(name = "taskx_total", doc = "total invocations of task x") 130 | @Timed (name = "taskx_time", doc = "duration of task x") 131 | public Result performSomeTask() { 132 | //... 133 | } 134 | ``` 135 | 136 | ### Configuration 137 | 138 | metrics: 139 | {class name}.{method name}{method signature}: 140 | - type: Counted 141 | name: {name} 142 | doc: {metric documentation} 143 | labels: ['{name:value}', '{name:value}'] 144 | - type: Gauged 145 | name: {name} 146 | mode: {mode} 147 | doc: {metric documentation} 148 | labels: ['{name:value}'] 149 | - type: ExceptionCounted 150 | name: {name} 151 | doc: {metric documentation} 152 | labels: ['{name:value}'] 153 | - type: Timed 154 | name: {name} 155 | doc: {metric documentation} 156 | labels: ['{name:value}'] 157 | 158 | Each metric is defined on a per method basis. A method is uniquely identified by the 159 | combination of `{class name}.{method name}{method signature}`. As an example, if we 160 | wanted to instrument the following method via configuration instead of annotations 161 | 162 | ```java 163 | package com.fleury.test; 164 | .... 165 | 166 | public class TestClass { 167 | .... 168 | 169 | @Counted(name = "taskx_total", doc = "total invocations of task x") 170 | public Result performSomeTask() { 171 | ... 172 | } 173 | } 174 | ``` 175 | 176 | We write the configuration as follows 177 | 178 | metrics: 179 | com/fleury/test/TestClass.performSomeTask(Ljava/lang/String;)V: 180 | - type: Counted 181 | name: taskx_total 182 | doc: total invocations of task x 183 | 184 | 185 | Note the method signature is based on the method parameter types and return type. The parameter types are between the brackets `()` with the return type after. In this case we have a single String argument and the return type is void which results in `(Ljava/lang/String;)V`. [Here](http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html) is a good overview of Java method signature mappings. 186 | 187 | In previous versions we allowed the package name to be specified using `.` instead of the internal `/` separator. While this is still supported for the metrics configuration section, it is not supported anywhere else and should be updated to only have the `/` package separator. 188 | 189 | #### Class Imports 190 | 191 | To simplify the metrics definition section of the configuration, we allow an imports section. Here we can define the fully qualified class names for any classes we use or re-use in the definitions. This includes method type descriptors. 192 | 193 | 194 | imports: 195 | - com/fleury/test/TestClass 196 | - java/lang/Object 197 | - java/lang/String 198 | 199 | metrics: 200 | TestClass.performSomeTask(LString;)V: 201 | - type: Counted 202 | name: taskx_total 203 | doc: total invocations of task x 204 | 205 | TestClass.performSomeOtherTask(LString;)LObject;: 206 | - type: Counted 207 | name: tasky_total 208 | doc: total invocations of task y 209 | 210 | 211 | ### Metric Labels 212 | 213 | Labels are a concept in some reporting systems that allow for multi-dimensional metric capture and analysis. Labels are composed of name value pairs `({name}:{value})`. You can have up to a maximum of five labels per metric. See the Prometheus metric library guidelines on metric and label naming [here](https://prometheus.io/docs/practices/naming/). Dropwizard metrics doesn't support the concept of labels and so we use the label values as part of the metric name. We apply these in order and so a metric definition of 214 | 215 | ```java 216 | @Counted(name = "taskx_total", labels = {"name1:value1", "name2:value2"}) 217 | ``` 218 | 219 | would result in a metric name in the Dropwizard registry of `taskx_total.value1.value2`. 220 | 221 | 222 | #### Dynamic Label Values 223 | 224 | A powerful feature is the ability to set label values dynamic based on variables available on the method stack. Metric names cannot be dynamic. The way we specify dynamic label values is using the `${index}` syntax followed by the method argument index. The special value `$this` can be used to access the current instance reference in non static methods. We prevent usage of `$this` in constructors to prevent initialisation leakage. 225 | 226 | Note that we restrict the stack usage to the method arguments only. That is, we don't allow use of variables created within the method as that is a very fragile thing to do. The String representation as given by `String.valueOf()` of the parameter is used as the label value. That means for primitive types we perform boxing first and null objects will result in the String `"null"`. Argument indexes start at index `0` up to the number of `args.length - 1` (i.e. array index syntax). We manage any special logic that occurs with the actual location on the stack due to non static methods (`this` is index `0` on the stack) and static methods (no `this`). Therefore you can always assume index `0` is the first method argument. 227 | 228 | ```java 229 | @Counted (name = "service_total", labels = { "client:$0" }) 230 | public void callService(String client) 231 | ``` 232 | 233 | Each time this method is invoked it will use the value of the `client` parameter as the metric label value. We also support accessing nested property values. For example, `($1.httpMethod)` where `$1` is the first method parameter and is e.g. of type `HttpRequest`. This means you are essentially doing `httpRequest.getHttpMethod().toString();`. This nesting can be arbitrarily deep. We use `PropertyUtils` from the `commons-beanutils` library to perform the nested property reading. Typically this means you can only use JavaBeans conforming properties, however, we have added a `GenericBeanIntrospector` which allows for accessing properties in methods like `name()` via e.g. `$1.name` etc. This gives better cross languages support. 234 | 235 | 236 | ### What we actually Transform 237 | As we allow the use of annotations to register metrics to track, if no black/white lists are defined we must scan all classes as they are loaded and check for the annotations. However, we do not want to have to rewrite all of these classes if we have not changed anything. There are many reasons you want to modify as little as possible with an agent but the general motto is, only touch what you have to. Hence, we only rewrite classes which have been changed due to the addition of metrics and all other classes, even though scanned, are returned untouched to the classloader. 238 | 239 | ### Supported Languages 240 | As the agent works at the bytecode level, we support any language which runs on the JVM. Every language which compiles and runs on the JVM must obey by the bytecode rules. This simply means we need to understand the translation mechanisms of each language for the language level method name to the bytecode level. In Java this is usually 1:1 (excluding some generics fun). You can always examine the `javap` (the [Java Disassembler](http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javap.html)) command to view the bytecode contents in a more `Java` centric way. 241 | 242 | As an example. Lets take the followin Scala class 243 | ```scala 244 | class Person(val name:String) { 245 | } 246 | ``` 247 | If we run `javap` on this 248 | 249 | javap Person.class 250 | 251 | we get 252 | 253 | Compiled from "Person.scala" 254 | public class Person { 255 | private final java.lang.String name; // field 256 | public java.lang.String name(); // getter method 257 | public Person(java.lang.String); // constructor 258 | } 259 | 260 | You can do the same for Kotlin, Clojure and any other JVM language. Be aware that there may be some quirks in the naming translations and it may not always be as simple as shown above. 261 | 262 | ## Supported Metrics Systems 263 | 264 | ### Prometheus 265 | Prometheus is the primary supported metric system. The design of the metric meta data was driven by its design (name, labels, doc). It also has the most powerful reporting system and does not require the development / integration of a multitude of reporting sinks like Codeahale does (e.g. Graphite, Librato, etc). 266 | 267 | Also, given the fact that we use the multidimensional labels concept from Prometheus, if we use a system which doesn't support this notion then it is not nearly as powerful. 268 | 269 | 270 | ### Dropwizard 271 | 272 | Sometimes however, we don't want to go to all the hassle of setting up and entire metrics monitoring system if we don't have one already in place and are only interested in the metrics on a single JVM. 273 | This is where we would recommend Dropwizard as it provides summary statistics (percentiles, rates etc) out of the box for certain metric types. These are automatically exposed via JVM and can be viewed and graphed with the likes of the VisualVM JMX plugin. 274 | 275 | Some differences in metric types exist between Prometheus and Dropwizard. In particular, with Prometheus you don't decrement counters, instead you use gauges for values that increase and decrease. Supporting this approach with Dropwizard gauges with an agent is tricky as we need to maintain a class variable. Hence, we simply use counters to back gauges with Dropwizard. 276 | 277 | Profiling agents can sometimes be far to heavy to attach to a JVM for a prolonged period of time and impact performance when we only want to monitor certain methods / hotspots. Also, most of the time these tools do not provide summary metrics exception counts which can be recorded with this library. 278 | 279 | ### Metric System Configuration 280 | 281 | The metric systems configuration is passed as simple key-value pairs `(Map)` to the constructor of each MetricSystem via their provider. These key-values are defined in the "system" section of the agent configuration. 282 | 283 | metrics: 284 | ..... 285 | 286 | system: 287 | key1: value1 288 | 289 | The dropwizard implementation supports the property `domain` which allows to change the exposed JMX domain name. It defaults to the dropwizard default of `metrics`. See the next section for some shared properties around JVM level metrics. 290 | 291 | #### Adding JVM Level Metric Information 292 | 293 | Both Dropwizard and Prometheus support adding JVM level metrics information obtained from the JVM via MBeans for 294 | 295 | - gc 296 | - memory 297 | - classloading 298 | - threads 299 | 300 | To enable each, simply add the metrics you want to a `jvm` property in the `system` section of the configuration yaml. For example, to add `gc` and `memory` information to the registry used: 301 | 302 | system: 303 | jvm: 304 | - gc 305 | - memory 306 | 307 | 308 | #### Agent Reporting 309 | 310 | We start the default reporting (endpoint) methods on both metrics systems. For Dropwizard that is JMX (which can be scraped via other services or agents), and for Prometheus that is the HttpServer. The default port for the Prometheus endpoint is `9899` and it can be changed by specifying the property `httpPort` in the system configuration section as follows 311 | 312 | system: 313 | httpPort: 9899 314 | 315 | Additional reporting systems can be added for each agent programmatically if required. Alternatively an additional Java agent could be attached to send the JMX metrics to e.g. Graphite for Dropwizard. The benefit of this approach is that it doesn't care how many metric registries are started within the application or by the agent(s). 316 | 317 | 318 | ### Black & Black Lists 319 | 320 | Sometimes we only want to scan certain packages or classes which we wish to instrument. This could be to reduce the agent startup time or to work around problematic instrumentation situations. Note that the black and white lists do not take any annotations or metric configuration into account and essentially override them. 321 | 322 | To white list a class or package include the fully qualified class or package name under the `whiteList` property. If no white list is specified, then all classes are scanned and eligible for transforming. 323 | 324 | whiteList: 325 | - com/fleury/test/ClassName 326 | - com/fleury/package2 327 | 328 | To black list a class or package add the fully qualified class or package name under the `blackList` property. If a class or package is in both white and black list, the black list wins and the class will not be touched. 329 | 330 | blackList: 331 | - com/ 332 | 333 | ### Logger Configuration 334 | 335 | j.u.l is used for logging and can be configured by passing the agent argument `log-config:` to the agent with the path to the logger properties file. 336 | 337 | 338 | ## Performance 339 | We use the Java ASM bytecode manipulation library. This is the lowest level and fastest of all the bytecode libraries which is used by the likes of cglib. It allows us to inject bytecode in a precise way which means we can craft the exact same bytecode as if it was hand written. To make the metric system plugable, we chose to abstract the metric work behind a generic SPI interface. The bytecode which we inject uses the SPI which can in turn be swapped out without any change to our bytecode. This makes it very flexible but it comes at the cost of not being able to keep field level static variable references for our metrics. Instead we perform a lookup from a ConcurrentHashMap to get the metric by name in the SPIs. Note that JIT takes care of the additional method dispatches up to the Map by performing inlining. If we were using a single implementation we would inject the metrics fields as static variables at the top of each class. If someone wishes to fork this for a single metric system that would be the best way to go. See the Prometheus fork implementation for an example [https://github.com/willfleury/prometheus-metrics-agent](https://github.com/willfleury/prometheus-metrics-agent). 340 | 341 | It should be noted that as with hand crafted metrics, the additional bytecode and hence method size required to handle capturing all metrics could potentially lead to methods which might otherwise have been inlined or compiled by the JIT being skipped instead. This should be considered regardless off the instrumentation choice and if unsure, the appropriate JVM output should be checked (-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+PrintCompilation). 342 | 343 | 344 | ## Dependencies 345 | Very lightweight. 346 | 347 | asm 348 | jackson 349 | 350 | The client libraries for whatever metric provider you choose are also included. Note that the final agent binaries are shaded and all dependencies relocated to prevent possible conflicts. 351 | 352 | 353 | # Binaries & Releases 354 | 355 | See the releases section of the github repository for releases along with the prebuilt agent binaries for dropwizard and prometheus. 356 | 357 | # Building 358 | 359 | The module metrics-agent-dist has build profiles for both Prometheus and Dropwizard. 360 | 361 | mvn clean package -Pprometheus 362 | 363 | mvn clean package -Pdropwizard 364 | 365 | The uber jar can be found under `/target/metrics-agent.jar` 366 | 367 | # Usage 368 | 369 | The agent must be attached to the JVM at startup. It cannot be attached to a running JVM. 370 | 371 | -javaagent:metrics-agent.jar 372 | 373 | Example 374 | 375 | java -javaagent:metrics-agent.jar -jar myapp.jar 376 | 377 | Using the configuration file config.yaml is performed as follows 378 | 379 | java -javaagent:metrics-agent.jar=agent-config:agent.yaml -jar myapp.jar 380 | 381 | 382 | Using the configuration file config.yaml and logging configuration logger.properties is performed as follows 383 | 384 | java -javaagent:metrics-agent.jar=agent-config:agent.yaml,log-config:logger.properties -jar myapp.jar 385 | 386 | 387 | # Debugging 388 | 389 | Note if you want to debug the metrics agent you should put the debugger agent first. 390 | 391 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address= -javaagent:metrics-agent.jar myapp.jar 392 | 393 | 394 | # Examples 395 | 396 | We provide some example configurations for popular frameworks. They serve as examples for how to instrument others and can be combined as desired (e.g. you can have both dropwizard request metrics and hibernate in the same configuration). As you will see, it is very simple and light weight to add new frameworks. In the case where different major versions of frameworks required different classes and methods to be instrumented, this simply becomes the addition of a new configuration file for that version. 397 | 398 | - [Jersey](example-configurations/jersey.yaml) 399 | - [Dropwizard (via Jersey)](example-configurations/dropwizard.yaml) 400 | - [Tomcat Servlet, JSP, Jersey](example-configurations/tomcat.yaml) 401 | - [Hibernate](example-configurations/hibernate.yaml) 402 | 403 | --------------------------------------------------------------------------------