├── .gitignore ├── prometheus-metrics-agent-core ├── src │ ├── main │ │ ├── resources │ │ │ └── logging.properties │ │ └── java │ │ │ └── com │ │ │ └── fleury │ │ │ └── metrics │ │ │ └── agent │ │ │ ├── transformer │ │ │ ├── visitors │ │ │ │ ├── injectors │ │ │ │ │ ├── Injector.java │ │ │ │ │ ├── CounterInjector.java │ │ │ │ │ ├── ExceptionCounterInjector.java │ │ │ │ │ ├── InjectorFactory.java │ │ │ │ │ ├── TimerInjector.java │ │ │ │ │ ├── GaugeInjector.java │ │ │ │ │ ├── TimedExceptionCountedInjector.java │ │ │ │ │ └── AbstractInjector.java │ │ │ │ ├── AnnotationMethodVisitor.java │ │ │ │ ├── AnnotationClassVisitor.java │ │ │ │ ├── MetricAnnotationAttributeVisitor.java │ │ │ │ ├── MetricAdapter.java │ │ │ │ ├── StaticInitializerMethodVisitor.java │ │ │ │ └── MetricClassVisitor.java │ │ │ ├── util │ │ │ │ ├── CollectionUtil.java │ │ │ │ ├── OpCodeUtil.java │ │ │ │ └── AnnotationUtil.java │ │ │ ├── AnnotatedMetricClassTransformer.java │ │ │ └── ASMClassWriter.java │ │ │ ├── reporter │ │ │ ├── PrometheusMetricSystemFactory.java │ │ │ └── PrometheusMetricSystem.java │ │ │ ├── config │ │ │ ├── LoggerUtil.java │ │ │ ├── ArgParser.java │ │ │ └── Configuration.java │ │ │ ├── Agent.java │ │ │ ├── model │ │ │ ├── MetricType.java │ │ │ ├── LabelValidator.java │ │ │ ├── LabelUtil.java │ │ │ └── Metric.java │ │ │ └── introspector │ │ │ └── GenericClassIntrospector.java │ └── test │ │ ├── resources │ │ └── config │ │ │ └── sample.yaml │ │ └── java │ │ └── com │ │ └── fleury │ │ └── metrics │ │ └── agent │ │ ├── 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 │ │ └── reporter │ │ └── TestMetricReader.java └── pom.xml ├── prometheus-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 ├── LICENSE.md └── README.md /.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 | -------------------------------------------------------------------------------- /prometheus-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 -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-annotation/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | prometheus-metrics-agent 7 | 0.0.7-SNAPSHOT 8 | 9 | prometheus-metrics-agent-annotation 10 | metrics-agent-annotation 11 | jar 12 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/PrometheusMetricSystemFactory.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 PrometheusMetricSystemFactory { 10 | 11 | public static final PrometheusMetricSystemFactory INSTANCE = new PrometheusMetricSystemFactory(); 12 | 13 | public PrometheusMetricSystem metrics; 14 | 15 | public void init(Map configuration) { 16 | metrics = new PrometheusMetricSystem(configuration); 17 | } 18 | 19 | public PrometheusMetricSystem get() { 20 | return metrics; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | httpPort: 9090 34 | jvm: 35 | - gc 36 | - memory 37 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/src/main/java/com/fleury/metrics/agent/config/LoggerUtil.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.config; 2 | 3 | import com.fleury.metrics.agent.Agent; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.logging.LogManager; 7 | 8 | public class LoggerUtil { 9 | 10 | public static void initializeLogging(String resource) { 11 | InputStream in = null; 12 | try { 13 | in = Agent.class.getResourceAsStream(resource); 14 | 15 | if (in == null) { 16 | throw new NullPointerException("Logger configuration " + resource + " not found"); 17 | } 18 | 19 | LogManager.getLogManager().readConfiguration(in); 20 | } catch (Exception e) { 21 | throw new RuntimeException("Unable to initialize agent logging with config: " + resource, e); 22 | } finally { 23 | if (in != null) { 24 | try { 25 | in.close(); 26 | } catch (IOException ignored) { } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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.PrometheusMetricSystemFactory; 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 | PrometheusMetricSystemFactory.INSTANCE.init(config.getSystem()); 25 | 26 | instrumentation.addTransformer( 27 | new AnnotatedMetricClassTransformer(config), 28 | instrumentation.isRetransformClassesSupported()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 io.prometheus.client.Counter; 8 | import io.prometheus.client.Gauge; 9 | import io.prometheus.client.Histogram; 10 | import org.objectweb.asm.Type; 11 | 12 | /** 13 | * 14 | * @author Will Fleury 15 | */ 16 | public enum MetricType { 17 | 18 | Counted(Counted.class, Counter.class), 19 | Gauged(Gauged.class, Gauge.class), 20 | Timed(Timed.class, Histogram.class), 21 | ExceptionCounted(ExceptionCounted.class, Counter.class); 22 | 23 | private final Class annotation; 24 | private final Class coreType; 25 | private final String desc; 26 | 27 | MetricType(Class annotation, Class coreType) { 28 | this.annotation = annotation; 29 | this.coreType = coreType; 30 | this.desc = Type.getDescriptor(annotation); 31 | } 32 | 33 | public Class getAnnotation() { 34 | return annotation; 35 | } 36 | 37 | public Class getCoreType() { 38 | return coreType; 39 | } 40 | 41 | public String getDesc() { 42 | return desc; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.fleury 5 | prometheus-metrics-agent 6 | 0.0.7-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 | 0.10 20 | 2.4.0 21 | 1.9.3 22 | 23 | 4.11 24 | 2.5 25 | 26 | 27 | 28 | 29 | prometheus-metrics-agent-annotation 30 | prometheus-metrics-agent-core 31 | 32 | 33 | 34 | 35 | junit 36 | junit 37 | ${junit.version} 38 | test 39 | 40 | 41 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.MetricType.Counted; 5 | 6 | import com.fleury.metrics.agent.model.Metric; 7 | import org.objectweb.asm.Type; 8 | import org.objectweb.asm.commons.AdviceAdapter; 9 | 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.recordCount(COUNTER, labels);
24 |  *
25 |  *     //original method code
26 |  * }
27 |  * 
28 | * 29 | * @author Will Fleury 30 | */ 31 | public class CounterInjector extends AbstractInjector { 32 | 33 | private static final String METHOD = "recordCount"; 34 | private static final String SIGNATURE = Type.getMethodDescriptor( 35 | Type.VOID_TYPE, 36 | Type.getType(Counted.getCoreType()), Type.getType(String[].class)); 37 | 38 | private final Metric metric; 39 | 40 | public CounterInjector(Metric metric, AdviceAdapter aa, String className, Type[] argTypes, int access) { 41 | super(aa, className, argTypes, access); 42 | this.metric = metric; 43 | } 44 | 45 | @Override 46 | public void injectAtMethodEnter() { 47 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(metric), Type.getDescriptor(Counted.getCoreType())); 48 | injectLabelsToStack(metric); 49 | 50 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 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 | 12 | /** 13 | * 14 | * @author Will Fleury 15 | */ 16 | public class AnnotationClassVisitor extends ClassVisitor { 17 | 18 | private boolean isInterface; 19 | private String className; 20 | private Configuration config; 21 | 22 | public AnnotationClassVisitor(ClassVisitor cv, Configuration config) { 23 | super(ASM5, cv); 24 | this.config = config; 25 | } 26 | 27 | @Override 28 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 29 | super.visit(version, access, name, signature, superName, interfaces); 30 | this.className = name; 31 | this.isInterface = (access & ACC_INTERFACE) != 0; 32 | } 33 | 34 | @Override 35 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 36 | MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 37 | 38 | boolean isSyntheticMethod = (access & ACC_SYNTHETIC) != 0; 39 | 40 | if (!isInterface && !isSyntheticMethod && mv != null && 41 | config.isWhiteListed(className) && !config.isBlackListed(className)) { 42 | mv = new AnnotationMethodVisitor(mv, config, className, name, desc); 43 | } 44 | 45 | return mv; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/src/test/java/com/fleury/metrics/agent/reporter/TestMetricReader.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import io.prometheus.client.CollectorRegistry; 4 | 5 | /** 6 | * 7 | * @author Will Fleury 8 | */ 9 | public class TestMetricReader { 10 | 11 | private final CollectorRegistry registry; 12 | 13 | public TestMetricReader(CollectorRegistry registry) { 14 | this.registry = registry; 15 | } 16 | 17 | public long getCount(String name) { 18 | return getRegistryValue(name); 19 | } 20 | 21 | public long getCount(String name, String[] labelNames, String[] labelValues) { 22 | return getRegistryValue(name, labelNames, labelValues); 23 | } 24 | 25 | public TimerResult getTimes(String name) { 26 | return new TimerResult( 27 | getRegistryValue(name + "_sum"), 28 | getRegistryValue(name + "_count")); 29 | } 30 | 31 | public TimerResult getTimes(String name, String[] labelNames, String[] labelValues) { 32 | return new TimerResult( 33 | getRegistryValue(name + "_sum", labelNames, labelValues), 34 | getRegistryValue(name + "_count", labelNames, labelValues)); 35 | } 36 | 37 | private long getRegistryValue(String name) { 38 | Double value = registry.getSampleValue(name); 39 | return value == null ? 0 : value.longValue(); 40 | } 41 | 42 | private long getRegistryValue(String name, String[] labelNames, String[] labelValues) { 43 | Double value = registry.getSampleValue(name, labelNames, labelValues); 44 | return value == null ? 0 : value.longValue(); 45 | } 46 | 47 | public static class TimerResult { 48 | public final long sum; 49 | public final long count; 50 | 51 | public TimerResult(long sum, long count) { 52 | this.sum = sum; 53 | this.count = count; 54 | } 55 | } 56 | 57 | public void reset() { 58 | CollectorRegistry.defaultRegistry.clear(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /prometheus-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.Timed; 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 CountTimerInvocationsClass bytecode.. 17 | * This confused the annotation scanning as annotations are placed on both the real and synthetic method.. Therefore 18 | * we need to check the method access code to ensure its not synthetic. 19 | * 20 | * public timed(Lcom/fleury/metrics/agent/transformer/asm/injectors/OverrideMethodAnnotationTest$B;)V 21 | * @Lcom/fleury/metrics/agent/annotation/Timed;(name="timed") 22 | * ... 23 | * 24 | * 25 | * public synthetic bridge timed(Lcom/fleury/metrics/agent/transformer/asm/injectors/OverrideMethodAnnotationTest$A;)V 26 | * @Lcom/fleury/metrics/agent/annotation/Timed;(name="timed") 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(CountTimerInvocationsClass.class); 36 | 37 | Object obj = clazz.newInstance(); 38 | 39 | obj.getClass().getMethod("timed", B.class).invoke(obj, new Object[] {new B()}); 40 | 41 | assertEquals(1, metrics.getTimes("timed").count); 42 | } 43 | 44 | public static class A { } 45 | 46 | public static class B extends A { } 47 | 48 | public static class BaseClass { 49 | public void timed(T value) { } 50 | } 51 | 52 | public static class CountTimerInvocationsClass extends BaseClass { 53 | 54 | @Override 55 | @Timed(name = "timed") 56 | public void timed(B value) { 57 | BaseMetricTest.performBasicTask(); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /prometheus-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 | } 57 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.MetricType.Counted; 5 | 6 | import com.fleury.metrics.agent.model.Metric; 7 | import org.objectweb.asm.Label; 8 | import org.objectweb.asm.Type; 9 | import org.objectweb.asm.commons.AdviceAdapter; 10 | 11 | /** 12 | * Transforms from 13 | * 14 | *
15 |  * public void someMethod() {
16 |  *     //original method code
17 |  * }
18 |  * 
19 | * 20 | * To 21 | * 22 | *
23 |  * public void someMethod() {
24 |  *     try {
25 |  *
26 |  *         //original method code
27 |  *
28 |  *     } catch (Throwable t) {
29 |  *         PrometheusMetricSystem.recordCount(COUNTER, labels);
30 |  *         throw t;
31 |  *     }
32 |  * }
33 |  * 
34 | * 35 | * @author Will Fleury 36 | */ 37 | public class ExceptionCounterInjector extends AbstractInjector { 38 | 39 | private static final String METHOD = "recordCount"; 40 | private static final String SIGNATURE = Type.getMethodDescriptor( 41 | Type.VOID_TYPE, 42 | Type.getType(Counted.getCoreType()), Type.getType(String[].class)); 43 | 44 | private final Metric metric; 45 | 46 | private Label startFinally; 47 | 48 | public ExceptionCounterInjector(Metric metric, AdviceAdapter aa, String className, Type[] argTypes, int access) { 49 | super(aa, className, argTypes, access); 50 | this.metric = metric; 51 | } 52 | 53 | @Override 54 | public void injectAtMethodEnter() { 55 | startFinally = new Label(); 56 | aa.visitLabel(startFinally); 57 | } 58 | 59 | @Override 60 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 61 | Label endFinally = new Label(); 62 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 63 | aa.visitLabel(endFinally); 64 | 65 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(metric), Type.getDescriptor(Counted.getCoreType())); 66 | injectLabelsToStack(metric); 67 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 68 | 69 | aa.visitInsn(ATHROW); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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, String className, 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, className, 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, className, argTypes, access)); 36 | } 37 | 38 | return injectors; 39 | } 40 | 41 | public static Injector createInjector(Metric metric, AdviceAdapter adviceAdapter, String className, Type[] argTypes, int access) { 42 | switch (metric.getType()) { 43 | case Counted: 44 | return new CounterInjector(metric, adviceAdapter, className, argTypes, access); 45 | 46 | case Gauged: 47 | return new GaugeInjector(metric, adviceAdapter, className, argTypes, access); 48 | 49 | case ExceptionCounted: 50 | return new ExceptionCounterInjector(metric, adviceAdapter, className, argTypes, access); 51 | 52 | case Timed: 53 | return new TimerInjector(metric, adviceAdapter, className, argTypes, access); 54 | 55 | default: 56 | throw new IllegalStateException("unknown metric type: " + metric.getType()); 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prometheus-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().type(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.name(value.toString()); 35 | } else if ("doc".equals(name)) { 36 | metricBuilder.doc(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.mode(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.labels(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 | -------------------------------------------------------------------------------- /prometheus-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.LabelUtil.validateLabelValues; 4 | import static com.fleury.metrics.agent.model.Metric.mapByType; 5 | import static java.util.logging.Level.FINE; 6 | 7 | import com.fleury.metrics.agent.model.Metric; 8 | import com.fleury.metrics.agent.model.MetricType; 9 | import com.fleury.metrics.agent.transformer.visitors.injectors.Injector; 10 | import com.fleury.metrics.agent.transformer.visitors.injectors.InjectorFactory; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.logging.Logger; 15 | import org.objectweb.asm.MethodVisitor; 16 | import org.objectweb.asm.Type; 17 | import org.objectweb.asm.commons.AdviceAdapter; 18 | 19 | /** 20 | * 21 | * @author Will Fleury 22 | */ 23 | public class MetricAdapter extends AdviceAdapter { 24 | 25 | private static final Logger LOGGER = Logger.getLogger(AdviceAdapter.class.getName()); 26 | 27 | private final Map metrics; 28 | private final Type[] argTypes; 29 | private final String className; 30 | private final String methodName; 31 | private final int access; 32 | 33 | private List injectors; 34 | 35 | public MetricAdapter(MethodVisitor mv, String className, int access, String name, String desc, List metadata) { 36 | super(ASM5, mv, access, name, desc); 37 | 38 | this.className = className; 39 | this.methodName = name; 40 | this.argTypes = Type.getArgumentTypes(desc); 41 | this.access = access; 42 | this.metrics = mapByType(metadata); 43 | } 44 | 45 | @Override 46 | protected void onMethodEnter() { 47 | if (metrics.isEmpty()) { 48 | injectors = Collections.emptyList(); 49 | return; 50 | } 51 | 52 | LOGGER.log(FINE, "Metrics found on : {0}.{1}", new Object[] {className, methodName}); 53 | 54 | injectors = InjectorFactory.createInjectors(metrics, this, className, argTypes, access); 55 | validateLabels(); 56 | 57 | for (Injector injector : injectors) { 58 | injector.injectAtMethodEnter(); 59 | } 60 | } 61 | 62 | @Override 63 | public void visitMaxs(int maxStack, int maxLocals) { 64 | for (Injector injector : injectors) { 65 | injector.injectAtVisitMaxs(maxStack, maxLocals); 66 | } 67 | 68 | mv.visitMaxs(maxStack, maxLocals); 69 | } 70 | 71 | @Override 72 | protected void onMethodExit(int opcode) { 73 | for (Injector injector : injectors) { 74 | injector.injectAtMethodExit(opcode); 75 | } 76 | } 77 | 78 | private void validateLabels() { 79 | for (Metric metric : metrics.values()) { 80 | validateLabelValues(methodName, metric.getLabels(), argTypes); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.MetricType.Timed; 5 | 6 | import com.fleury.metrics.agent.model.Metric; 7 | import org.objectweb.asm.Label; 8 | import org.objectweb.asm.Type; 9 | import org.objectweb.asm.commons.AdviceAdapter; 10 | 11 | /** 12 | * Transforms from 13 | * 14 | *
15 |  * public void someMethod() {
16 |  *     //original method code
17 |  * }
18 |  * 
19 | * 20 | * To 21 | * 22 | *
23 |  * public void someMethod() {
24 |  *     long startTimer = System.nanoTime();
25 |  *     try {
26 |  *
27 |  *         //original method code
28 |  *
29 |  *     } finally {
30 |  *         PrometheusMetricSystem.recordTime(TIMER, labels);
31 |  *     }
32 |  * }
33 |  * 
34 | * 35 | * @author Will Fleury 36 | */ 37 | public class TimerInjector extends AbstractInjector { 38 | 39 | private static final String METHOD = "recordTime"; 40 | private static final String SIGNATURE = Type.getMethodDescriptor( 41 | Type.VOID_TYPE, 42 | Type.getType(Timed.getCoreType()), Type.getType(String[].class), Type.LONG_TYPE); 43 | 44 | private final Metric metric; 45 | 46 | private int startTimeVar; 47 | private Label startFinally; 48 | 49 | public TimerInjector(Metric metric, AdviceAdapter aa, String className, Type[] argTypes, int access) { 50 | super(aa, className, argTypes, access); 51 | this.metric = metric; 52 | } 53 | 54 | @Override 55 | public void injectAtMethodEnter() { 56 | startFinally = new Label(); 57 | startTimeVar = aa.newLocal(Type.LONG_TYPE); 58 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 59 | aa.visitVarInsn(LSTORE, startTimeVar); 60 | aa.visitLabel(startFinally); 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 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(metric), Type.getDescriptor(Timed.getCoreType())); 82 | injectLabelsToStack(metric); 83 | 84 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 85 | aa.visitVarInsn(LLOAD, startTimeVar); 86 | aa.visitInsn(LSUB); 87 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, METHOD, SIGNATURE, false); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.MetricType.Gauged; 5 | 6 | import com.fleury.metrics.agent.model.Metric; 7 | import org.objectweb.asm.Label; 8 | import org.objectweb.asm.Type; 9 | import org.objectweb.asm.commons.AdviceAdapter; 10 | 11 | /** 12 | * Only currently supports IN_FLIGHT mode which means it tracks the number of method calls in flight. 13 | * Transforms from 14 | * 15 | *
16 |  * public void someMethod() {
17 |  *     //original method code
18 |  * }
19 |  * 
20 | * 21 | * To 22 | * 23 | *
24 |  * public void someMethod() {
25 |  *     PrometheusMetricSystem.recordGaugeInc(GAUGE, labels);
26 |  *     try {
27 |  *
28 |  *         //original method code
29 |  *
30 |  *     } finally {
31 |  *         PrometheusMetricSystem.recordGaugeDec(GAUGE, labels);
32 |  *     }
33 |  * }
34 |  * 
35 | * 36 | * @author Will Fleury 37 | */ 38 | public class GaugeInjector extends AbstractInjector { 39 | 40 | private static final String INC_METHOD = "recordGaugeInc"; 41 | private static final String DEC_METHOD = "recordGaugeDec"; 42 | private static final String SIGNATURE = Type.getMethodDescriptor( 43 | Type.VOID_TYPE, 44 | Type.getType(Gauged.getCoreType()), Type.getType(String[].class)); 45 | 46 | private final Metric metric; 47 | 48 | private Label startFinally; 49 | 50 | public GaugeInjector(Metric metric, AdviceAdapter aa, String className, Type[] argTypes, int access) { 51 | super(aa, className, argTypes, access); 52 | this.metric = metric; 53 | } 54 | 55 | @Override 56 | public void injectAtMethodEnter() { 57 | startFinally = new Label(); 58 | aa.visitLabel(startFinally); 59 | 60 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(metric), Type.getDescriptor(Gauged.getCoreType())); 61 | injectLabelsToStack(metric); 62 | 63 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, INC_METHOD, SIGNATURE, false); 64 | } 65 | 66 | @Override 67 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 68 | Label endFinally = new Label(); 69 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 70 | aa.visitLabel(endFinally); 71 | 72 | onFinally(ATHROW); 73 | aa.visitInsn(ATHROW); 74 | } 75 | 76 | @Override 77 | public void injectAtMethodExit(int opcode) { 78 | if (opcode != ATHROW) { 79 | onFinally(opcode); 80 | } 81 | } 82 | 83 | private void onFinally(int opcode) { 84 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(metric), Type.getDescriptor(Gauged.getCoreType())); 85 | injectLabelsToStack(metric); 86 | 87 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, DEC_METHOD, SIGNATURE, false); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /prometheus-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 type(MetricType type) { 84 | this.type = type; 85 | return this; 86 | } 87 | 88 | public MetricBuilder name(String name) { 89 | this.name = name; 90 | return this; 91 | } 92 | 93 | public MetricBuilder doc(String doc) { 94 | this.doc = doc; 95 | return this; 96 | } 97 | 98 | public MetricBuilder labels(List labels) { 99 | this.labels = labels; 100 | return this; 101 | } 102 | 103 | public MetricBuilder mode(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 | -------------------------------------------------------------------------------- /prometheus-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 visitAnnotation happens after visitFieldInsn in ClassVisitor). 50 | scanMetricAnnotations(loader, cr); 51 | 52 | // rewrite only if metric found & white listed or not blacklisted 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 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/src/main/java/com/fleury/metrics/agent/transformer/visitors/StaticInitializerMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.transformer.visitors; 2 | 3 | import static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.LabelUtil.getLabelNames; 5 | import static com.fleury.metrics.agent.transformer.util.CollectionUtil.isNotEmpty; 6 | 7 | import com.fleury.metrics.agent.model.Metric; 8 | import com.fleury.metrics.agent.reporter.PrometheusMetricSystem; 9 | import com.fleury.metrics.agent.transformer.util.OpCodeUtil; 10 | import java.util.List; 11 | import org.objectweb.asm.MethodVisitor; 12 | import org.objectweb.asm.Type; 13 | import org.objectweb.asm.commons.AdviceAdapter; 14 | 15 | 16 | public class StaticInitializerMethodVisitor extends AdviceAdapter { 17 | 18 | private final List classMetrics; 19 | private final String className; 20 | 21 | public StaticInitializerMethodVisitor(MethodVisitor mv, List classMetrics, String className, int access, String name, String desc) { 22 | super(ASM5, mv, access, name, desc); 23 | 24 | this.className = className; 25 | this.classMetrics = classMetrics; 26 | } 27 | 28 | @Override 29 | public void visitCode() { 30 | super.visitCode(); 31 | 32 | for (Metric metric : classMetrics) { 33 | addMetric(metric); 34 | } 35 | } 36 | 37 | private void addMetric(Metric metric) { 38 | // load name 39 | super.visitLdcInsn(metric.getName()); 40 | 41 | // load labels 42 | if (isNotEmpty(metric.getLabels())) { 43 | if (metric.getLabels().size() > 5) { 44 | throw new IllegalStateException("Maximum labels per metric is 5. " 45 | + metric.getName() + " has " + metric.getLabels().size()); 46 | } 47 | 48 | super.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(metric.getLabels().size())); 49 | super.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); 50 | 51 | List labelNames = getLabelNames(metric.getLabels()); 52 | for (int i = 0; i < labelNames.size(); i++) { 53 | super.visitInsn(DUP); 54 | super.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(i)); 55 | super.visitLdcInsn(labelNames.get(i)); 56 | super.visitInsn(AASTORE); 57 | } 58 | } 59 | // or null if non labels 60 | else { 61 | super.visitInsn(ACONST_NULL); 62 | } 63 | 64 | // load doc 65 | super.visitLdcInsn(metric.getDoc() == null ? "empty doc" : metric.getDoc()); 66 | 67 | // call PrometheusMetricSystem.createAndRegisterCounted/Timed/Gauged(...) 68 | super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrometheusMetricSystem.class), 69 | "createAndRegister" + metric.getType().name(), 70 | Type.getMethodDescriptor( 71 | Type.getType(metric.getType().getCoreType()), 72 | Type.getType(String.class), Type.getType(String[].class), Type.getType(String.class)), 73 | false); 74 | 75 | // store metric in class static field 76 | super.visitFieldInsn(PUTSTATIC, className, staticFinalFieldName(metric), 77 | Type.getDescriptor(metric.getType().getCoreType())); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /prometheus-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 | import static com.fleury.metrics.agent.reporter.TestMetricReader.TimerResult; 6 | 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 TimedExceptionCountedInjectorTest extends BaseMetricTest { 18 | 19 | @Test 20 | public void shouldRecordConstructorInvocationStatistics() throws Exception { 21 | Class clazz = execute(TimedExceptionCountedConstructorClass.class); 22 | 23 | Object obj = clazz.newInstance(); 24 | 25 | TimerResult value = metrics.getTimes("constructor_timer", new String[] {"type"}, new String[]{"timed"}); 26 | assertEquals(1, value.count); 27 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 28 | 29 | assertEquals(0, metrics.getCount("constructor_exceptions", new String[] {"type"}, new String[]{"exception"})); 30 | } 31 | 32 | @Test 33 | public void shouldRecordMethodInvocationWhenExceptionThrownStatistics() throws Exception { 34 | Class clazz = execute(TimedExceptionCountedMethodClassWithException.class); 35 | 36 | Object obj = clazz.newInstance(); 37 | 38 | boolean exceptionOccured = false; 39 | try { 40 | obj.getClass().getMethod("timed").invoke(obj); 41 | } 42 | catch (InvocationTargetException e) { 43 | exceptionOccured = true; 44 | } 45 | 46 | assertTrue(exceptionOccured); 47 | 48 | TimerResult value = metrics.getTimes("method_timer", new String[] {"type"}, new String[]{"timed"}); 49 | assertEquals(1, value.count); 50 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 51 | 52 | assertEquals(1, metrics.getCount("method_exceptions", new String[] {"type"}, new String[]{"exception"})); 53 | } 54 | 55 | public static class TimedExceptionCountedConstructorClass { 56 | 57 | @Timed(name = "constructor_timer", labels = {"type:timed"}) 58 | @ExceptionCounted(name = "constructor_exceptions", labels = {"type:exception"}) 59 | public TimedExceptionCountedConstructorClass() { 60 | try { 61 | Thread.sleep(10L); 62 | } 63 | catch (InterruptedException e) { 64 | } 65 | } 66 | } 67 | 68 | public static class TimedExceptionCountedMethodClassWithException { 69 | 70 | @Timed(name = "method_timer", labels = {"type:timed"}) 71 | @ExceptionCounted(name = "method_exceptions", labels = {"type:exception"}) 72 | public void timed() { 73 | try { 74 | Thread.sleep(10L); 75 | callService(); 76 | } 77 | catch (InterruptedException e) { 78 | } 79 | } 80 | 81 | public final void callService() { 82 | BaseMetricTest.performBasicTask(); 83 | throw new RuntimeException(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /prometheus-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.TestMetricReader; 8 | import com.fleury.metrics.agent.transformer.AnnotatedMetricClassTransformer; 9 | import io.prometheus.client.CollectorRegistry; 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 TestMetricReader metrics; 29 | 30 | @Before 31 | public void setup() { 32 | metrics = new TestMetricReader(CollectorRegistry.defaultRegistry); 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 | -------------------------------------------------------------------------------- /prometheus-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[] {"exception"}, 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 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static org.objectweb.asm.Opcodes.ACC_FINAL; 5 | import static org.objectweb.asm.Opcodes.ACC_INTERFACE; 6 | import static org.objectweb.asm.Opcodes.ACC_PUBLIC; 7 | import static org.objectweb.asm.Opcodes.ACC_STATIC; 8 | import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; 9 | import static org.objectweb.asm.Opcodes.ASM5; 10 | import static org.objectweb.asm.Opcodes.RETURN; 11 | 12 | import com.fleury.metrics.agent.config.Configuration; 13 | import com.fleury.metrics.agent.model.Metric; 14 | import java.util.List; 15 | import org.objectweb.asm.ClassVisitor; 16 | import org.objectweb.asm.MethodVisitor; 17 | import org.objectweb.asm.Type; 18 | import org.objectweb.asm.commons.JSRInlinerAdapter; 19 | 20 | /** 21 | * 22 | * @author Will Fleury 23 | */ 24 | public class MetricClassVisitor extends ClassVisitor { 25 | 26 | private boolean isInterface; 27 | private String className; 28 | private int classVersion; 29 | private boolean visitedStaticBlock = false; 30 | private Configuration config; 31 | private List classMetrics; 32 | 33 | public MetricClassVisitor(ClassVisitor cv, Configuration config) { 34 | super(ASM5, cv); 35 | this.config = config; 36 | } 37 | 38 | @Override 39 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 40 | super.visit(version, access, name, signature, superName, interfaces); 41 | this.classVersion = version; 42 | this.className = name; 43 | this.isInterface = (access & ACC_INTERFACE) != 0; 44 | 45 | this.classMetrics = config.findMetrics(className); 46 | 47 | // add the static final metric fields (Counter,Histogram,Gauge) to the class 48 | for (Metric metric : classMetrics) { 49 | super.visitField( 50 | ACC_PUBLIC + ACC_FINAL + ACC_STATIC, 51 | staticFinalFieldName(metric), 52 | Type.getDescriptor(metric.getType().getCoreType()), null, null).visitEnd(); 53 | } 54 | } 55 | 56 | @Override 57 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 58 | MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); 59 | 60 | boolean isSyntheticMethod = (access & ACC_SYNTHETIC) != 0; 61 | boolean isStaticMethod = (access & ACC_STATIC) != 0; 62 | 63 | // instrument the method 64 | if (!isInterface && !isSyntheticMethod && mv != null) { 65 | List metadata = config.findMetrics(className, name, desc); 66 | 67 | mv = new MetricAdapter(mv, className, access, name, desc, metadata); 68 | mv = new JSRInlinerAdapter(mv, access, name, desc, signature, exceptions); 69 | } 70 | 71 | // initialize static fields if the static initializer block already exists in the class 72 | if (name.equals("") && isStaticMethod && mv != null) { 73 | visitedStaticBlock = true; 74 | 75 | mv = new StaticInitializerMethodVisitor(mv, classMetrics, className, access, name, desc); 76 | } 77 | 78 | return mv; 79 | } 80 | 81 | @Override 82 | public void visitEnd() { 83 | // add static initializer block (method) to initialize static fields 84 | if (!visitedStaticBlock) { 85 | MethodVisitor mv = super.visitMethod(ACC_STATIC, "", "()V", null, null); 86 | mv = new StaticInitializerMethodVisitor(mv, classMetrics, className, ACC_STATIC, "", "()V"); 87 | 88 | mv.visitCode(); 89 | mv.visitInsn(RETURN); 90 | mv.visitMaxs(0, 0); 91 | mv.visitEnd(); 92 | } 93 | 94 | super.visitEnd(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /prometheus-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 static com.fleury.metrics.agent.config.Configuration.staticFinalFieldName; 4 | import static com.fleury.metrics.agent.model.MetricType.Counted; 5 | import static com.fleury.metrics.agent.model.MetricType.Timed; 6 | 7 | import com.fleury.metrics.agent.model.Metric; 8 | import org.objectweb.asm.Label; 9 | import org.objectweb.asm.Type; 10 | import org.objectweb.asm.commons.AdviceAdapter; 11 | 12 | /** 13 | * Transforms from 14 | * 15 | *
 16 |  * public void someMethod() {
 17 |  *     //original method code
 18 |  * }
 19 |  * 
20 | * 21 | * To 22 | * 23 | *
 24 |  * public void someMethod() {
 25 |  *     long startTimer = System.nanoTime();
 26 |  *     try {
 27 |  *
 28 |  *         //original method code
 29 |  *
 30 |  *     } catch (Throwable t) {
 31 |  *         PrometheusMetricSystem.recordCount(COUNTER, labels);
 32 |  *         throw t;
 33 |  *     } finally {
 34 |  *         PrometheusMetricSystem.recordTime(TIMER, labels);
 35 |  *     }
 36 |  * }
 37 |  * 
38 | * 39 | * @author Will Fleury 40 | */ 41 | public class TimedExceptionCountedInjector extends AbstractInjector { 42 | 43 | private static final String EXCEPTION_COUNT_METHOD = "recordCount"; 44 | private static final String EXCEPTION_COUNT_SIGNATURE = Type.getMethodDescriptor( 45 | Type.VOID_TYPE, 46 | Type.getType(Counted.getCoreType()), Type.getType(String[].class)); 47 | 48 | private static final String TIMER_METHOD = "recordTime"; 49 | private static final String TIMER_SIGNATURE = Type.getMethodDescriptor( 50 | Type.VOID_TYPE, 51 | Type.getType(Timed.getCoreType()), Type.getType(String[].class), Type.LONG_TYPE); 52 | 53 | private final Metric timerMetric; 54 | private final Metric exceptionMetric; 55 | 56 | private int startTimeVar; 57 | private Label startFinally; 58 | 59 | public TimedExceptionCountedInjector(Metric timerMetric, Metric exceptionMetric, AdviceAdapter aa, 60 | String className, Type[] argTypes, int access) { 61 | super(aa, className, argTypes, access); 62 | this.timerMetric = timerMetric; 63 | this.exceptionMetric = exceptionMetric; 64 | } 65 | 66 | @Override 67 | public void injectAtMethodEnter() { 68 | startFinally = new Label(); 69 | startTimeVar = aa.newLocal(Type.LONG_TYPE); 70 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 71 | aa.visitVarInsn(LSTORE, startTimeVar); 72 | aa.visitLabel(startFinally); 73 | } 74 | 75 | @Override 76 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 77 | Label endFinally = new Label(); 78 | aa.visitTryCatchBlock(startFinally, endFinally, endFinally, null); 79 | aa.visitLabel(endFinally); 80 | 81 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(exceptionMetric), Type.getDescriptor(Counted.getCoreType())); 82 | injectLabelsToStack(exceptionMetric); 83 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, EXCEPTION_COUNT_METHOD, 84 | EXCEPTION_COUNT_SIGNATURE, false); 85 | 86 | onFinally(ATHROW); 87 | aa.visitInsn(ATHROW); 88 | } 89 | 90 | @Override 91 | public void injectAtMethodExit(int opcode) { 92 | if (opcode != ATHROW) { 93 | onFinally(opcode); 94 | } 95 | } 96 | 97 | private void onFinally(int opcode) { 98 | aa.visitFieldInsn(GETSTATIC, className, staticFinalFieldName(timerMetric), Type.getDescriptor(Timed.getCoreType())); 99 | injectLabelsToStack(timerMetric); 100 | 101 | aa.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); 102 | aa.visitVarInsn(LLOAD, startTimeVar); 103 | aa.visitInsn(LSUB); 104 | aa.visitMethodInsn(INVOKESTATIC, METRIC_REPORTER_CLASSNAME, TIMER_METHOD, TIMER_SIGNATURE, false); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /prometheus-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 | import static com.fleury.metrics.agent.reporter.TestMetricReader.TimerResult; 6 | 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 TimerInjectorTest extends BaseMetricTest { 17 | 18 | @Test 19 | public void shouldTimeConstructorInvocation() throws Exception { 20 | Class clazz = execute(TimedConstructorClass.class); 21 | 22 | Object obj = clazz.newInstance(); 23 | 24 | TimerResult value = metrics.getTimes("constructor"); 25 | assertEquals(1, value.count); 26 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 27 | } 28 | 29 | @Test 30 | public void shouldTimeMethodInvocation() throws Exception { 31 | Class clazz = execute(TimedMethodClass.class); 32 | 33 | Object obj = clazz.newInstance(); 34 | 35 | obj.getClass().getMethod("timed").invoke(obj); 36 | 37 | TimerResult value = metrics.getTimes("timed"); 38 | assertEquals(1, value.count); 39 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 40 | } 41 | 42 | @Test 43 | public void shouldTimeMethodWithLabelsInvocation() throws Exception { 44 | Class clazz = execute(TimedMethodWithLabelsClass.class); 45 | 46 | Object obj = clazz.newInstance(); 47 | 48 | obj.getClass().getMethod("timed").invoke(obj); 49 | 50 | TimerResult value = metrics.getTimes("timed", new String[] {"name1"}, new String[]{"value1"}); 51 | assertEquals(1, value.count); 52 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 53 | } 54 | 55 | @Test 56 | public void shouldTimeMethodInvocationWhenExceptionThrown() throws Exception { 57 | Class clazz = execute(TimedMethodClassWithException.class); 58 | 59 | Object obj = clazz.newInstance(); 60 | 61 | boolean exceptionOccured = false; 62 | try { 63 | obj.getClass().getMethod("timed").invoke(obj); 64 | } 65 | catch (InvocationTargetException e) { 66 | exceptionOccured = true; 67 | } 68 | 69 | assertTrue(exceptionOccured); 70 | 71 | TimerResult value = metrics.getTimes("timed"); 72 | assertEquals(1, value.count); 73 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 74 | } 75 | 76 | public static class TimedConstructorClass { 77 | 78 | @Timed(name = "constructor") 79 | public TimedConstructorClass() { 80 | try { 81 | Thread.sleep(10L); 82 | } 83 | catch (InterruptedException e) { 84 | } 85 | } 86 | } 87 | 88 | public static class TimedMethodClass { 89 | 90 | @Timed(name = "timed") 91 | public void timed() { 92 | try { 93 | Thread.sleep(10L); 94 | } 95 | catch (InterruptedException e) { 96 | } 97 | } 98 | } 99 | 100 | public static class TimedMethodWithLabelsClass { 101 | 102 | @Timed(name = "timed", labels = {"name1:value1"}) 103 | public void timed() { 104 | try { 105 | Thread.sleep(10L); 106 | } 107 | catch (InterruptedException e) { 108 | } 109 | } 110 | } 111 | 112 | public static class TimedMethodClassWithException { 113 | 114 | @Timed(name = "timed") 115 | public void timed() { 116 | try { 117 | Thread.sleep(10L); 118 | callService(); 119 | } 120 | catch (InterruptedException e) { 121 | } 122 | } 123 | 124 | private void callService() { 125 | BaseMetricTest.performBasicTask(); 126 | throw new RuntimeException(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /prometheus-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 | .type(MetricType.Counted) 34 | .name("constructor") 35 | .labels(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[] {"label1"}, 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[] {"label1"}, 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 | -------------------------------------------------------------------------------- /prometheus-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 | import static com.fleury.metrics.agent.reporter.TestMetricReader.TimerResult; 6 | 7 | import com.fleury.metrics.agent.annotation.Counted; 8 | import com.fleury.metrics.agent.annotation.ExceptionCounted; 9 | import com.fleury.metrics.agent.annotation.Timed; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.util.concurrent.TimeUnit; 12 | import org.junit.Test; 13 | 14 | /** 15 | * 16 | * @author Will Fleury 17 | */ 18 | public class MixedInjectorTest extends BaseMetricTest { 19 | 20 | @Test 21 | public void shouldRecordConstructorInvocationStatistics() throws Exception { 22 | Class clazz = execute(MixedMetricConstructorClass.class); 23 | 24 | Object obj = clazz.newInstance(); 25 | 26 | TimerResult value = metrics.getTimes("constructor_timed", new String[] {"type"}, new String[]{"timed"}); 27 | assertEquals(1, value.count); 28 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 29 | 30 | assertEquals(1, metrics.getCount("constructor_count", new String[] {"type"}, new String[]{"counted"})); 31 | assertEquals(0, metrics.getCount("constructor_exceptions", new String[] {"type"}, new String[]{"exception"})); 32 | } 33 | 34 | @Test 35 | public void shouldRecordMethodInvocationStatistics() throws Exception { 36 | Class clazz = execute(MixedMetricMethodClass.class); 37 | 38 | Object obj = clazz.newInstance(); 39 | 40 | obj.getClass().getMethod("timed").invoke(obj); 41 | 42 | TimerResult value = metrics.getTimes("timed_timed", new String[] {"type"}, new String[]{"timed"}); 43 | assertEquals(1, value.count); 44 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 45 | 46 | assertEquals(1, metrics.getCount("timed_count", new String[] {"type"}, new String[]{"counted"})); 47 | assertEquals(0, metrics.getCount("timed_exceptions", new String[] {"type"}, new String[]{"exception"})); 48 | } 49 | 50 | @Test 51 | public void shouldRecordMethodInvocationWhenExceptionThrownStatistics() throws Exception { 52 | Class clazz = execute(MixedMetricMethodClassWithException.class); 53 | 54 | Object obj = clazz.newInstance(); 55 | 56 | boolean exceptionOccured = false; 57 | try { 58 | obj.getClass().getMethod("timed").invoke(obj); 59 | } 60 | catch (InvocationTargetException e) { 61 | exceptionOccured = true; 62 | } 63 | 64 | assertTrue(exceptionOccured); 65 | 66 | TimerResult value = metrics.getTimes("timed_timed", new String[] {"type"}, new String[]{"timed"}); 67 | assertEquals(1, value.count); 68 | assertTrue(value.sum >= TimeUnit.NANOSECONDS.toMillis(10L)); 69 | 70 | assertEquals(1, metrics.getCount("timed_exceptions", new String[] {"type"}, new String[]{"exception"})); 71 | assertEquals(1, metrics.getCount("timed_count", new String[] {"type"}, new String[]{"counted"})); 72 | } 73 | 74 | public static class MixedMetricConstructorClass { 75 | 76 | @Timed(name = "constructor_timed", labels = {"type:timed"}) 77 | @ExceptionCounted(name = "constructor_exceptions", labels = {"type:exception"}) 78 | @Counted(name = "constructor_count", labels = {"type:counted"}) 79 | public MixedMetricConstructorClass() { 80 | try { 81 | Thread.sleep(10L); 82 | } 83 | catch (InterruptedException e) { 84 | } 85 | } 86 | } 87 | 88 | public static class MixedMetricMethodClass { 89 | 90 | @Timed(name = "timed_timed", labels = {"type:timed"}) 91 | @ExceptionCounted(name = "timed_exceptions", labels = {"type:exception"}) 92 | @Counted(name = "timed_count", labels = {"type:counted"}) 93 | public void timed() { 94 | try { 95 | Thread.sleep(10L); 96 | } 97 | catch (InterruptedException e) { 98 | } 99 | } 100 | } 101 | 102 | public static class MixedMetricMethodClassWithException { 103 | 104 | @Timed(name = "timed_timed", labels = {"type:timed"}) 105 | @ExceptionCounted(name = "timed_exceptions", labels = {"type:exception"}) 106 | @Counted(name = "timed_count", labels = {"type:counted"}) 107 | public void timed() { 108 | try { 109 | Thread.sleep(10L); 110 | callService(); 111 | } 112 | catch (InterruptedException e) { 113 | } 114 | } 115 | 116 | public final void callService() { 117 | BaseMetricTest.performBasicTask(); 118 | throw new RuntimeException(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /prometheus-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.PrometheusMetricSystem; 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(PrometheusMetricSystem.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 | protected final String className; 37 | 38 | public AbstractInjector(AdviceAdapter aa, String className, Type[] argTypes, int access) { 39 | this.aa = aa; 40 | this.className = className; 41 | this.argTypes = argTypes; 42 | this.access = access; 43 | } 44 | 45 | @Override 46 | public void injectAtMethodEnter() { 47 | } 48 | 49 | @Override 50 | public void injectAtVisitMaxs(int maxStack, int maxLocals) { 51 | } 52 | 53 | @Override 54 | public void injectAtMethodExit(int opcode) { 55 | } 56 | 57 | protected void injectLabelsToStack(Metric metric) { 58 | List labelValues = LabelUtil.getLabelValues(metric.getLabels()); 59 | 60 | if (isNotEmpty(labelValues)) { 61 | aa.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(labelValues.size())); 62 | aa.visitTypeInsn(ANEWARRAY, Type.getInternalName(String.class)); 63 | 64 | for (int i = 0; i < labelValues.size(); i++) { 65 | aa.visitInsn(DUP); 66 | aa.visitInsn(OpCodeUtil.getIConstOpcodeForInteger(i)); 67 | injectLabelValueToStack(labelValues.get(i)); 68 | } 69 | 70 | } else { 71 | aa.visitInsn(ACONST_NULL); 72 | } 73 | } 74 | 75 | private void injectLabelValueToStack(String labelValue) { 76 | if (!isTemplatedLabelValue(labelValue)) { 77 | aa.visitLdcInsn(labelValue); 78 | } 79 | else { 80 | if (isThis(labelValue)) { 81 | aa.visitVarInsn(ALOAD, 0); //aa.loadThis(); 82 | } 83 | 84 | else { 85 | int argIndex = getLabelVarIndex(labelValue); 86 | 87 | boxParameterAndLoad(argIndex); 88 | } 89 | 90 | if (isLabelVarNested(labelValue)) { 91 | aa.visitLdcInsn(getNestedLabelVar(labelValue)); 92 | 93 | aa.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PropertyUtils.class), 94 | "getNestedProperty", 95 | Type.getMethodDescriptor( 96 | Type.getType(Object.class), 97 | Type.getType(Object.class), Type.getType(String.class)), 98 | false); 99 | } 100 | 101 | aa.visitMethodInsn(INVOKESTATIC, Type.getInternalName(String.class), 102 | "valueOf", 103 | Type.getMethodDescriptor( 104 | Type.getType(String.class), 105 | Type.getType(Object.class)), 106 | false); 107 | } 108 | 109 | aa.visitInsn(AASTORE); 110 | } 111 | 112 | private void boxParameterAndLoad(int argIndex) { 113 | Type type = argTypes[argIndex]; 114 | int stackIndex = getStackIndex(argIndex); 115 | 116 | switch (type.getSort()) { 117 | case Type.OBJECT: //no need to box Object 118 | aa.visitVarInsn(ALOAD, stackIndex); 119 | break; 120 | 121 | default: 122 | // aa.loadArg(argIndex); //doesn't work... 123 | aa.visitVarInsn(type.getOpcode(Opcodes.ILOAD), stackIndex); 124 | aa.valueOf(type); 125 | break; 126 | } 127 | } 128 | 129 | private int getStackIndex(int arg) { 130 | int index = (access & Opcodes.ACC_STATIC) == 0 ? 1 : 0; 131 | for (int i = 0; i < arg; i++) { 132 | index += argTypes[i].getSize(); 133 | } 134 | return index; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/src/main/java/com/fleury/metrics/agent/reporter/PrometheusMetricSystem.java: -------------------------------------------------------------------------------- 1 | package com.fleury.metrics.agent.reporter; 2 | 3 | import static com.fleury.metrics.agent.config.Configuration.YAML_MAPPER; 4 | import static java.util.logging.Level.WARNING; 5 | 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 io.prometheus.jmx.JmxCollector; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.logging.Logger; 21 | 22 | /** 23 | * The static methods in this class are called from the bytecode we instrument. Hence do not change any static methods 24 | * here related and/or method signatures unless you change the corresponding bytecode. 25 | * 26 | * @author Will Fleury 27 | */ 28 | public class PrometheusMetricSystem { 29 | 30 | private static final Logger LOGGER = Logger.getLogger(PrometheusMetricSystem.class.getName()); 31 | 32 | private static final int DEFAULT_HTTP_PORT = 9899; 33 | 34 | public static Counter createAndRegisterCounted(String name, String[] labels, String doc) { 35 | Counter.Builder builder = Counter.build().name(name).help(doc); 36 | if (labels != null) { 37 | builder.labelNames(labels); 38 | } 39 | 40 | return builder.register(); 41 | } 42 | 43 | public static Counter createAndRegisterExceptionCounted(String name, String[] labels, String doc) { 44 | Counter.Builder builder = Counter.build().name(name).help(doc); 45 | if (labels != null) { 46 | builder.labelNames(labels); 47 | } 48 | 49 | return builder.register(); 50 | } 51 | 52 | public static Gauge createAndRegisterGauged(String name, String[] labels, String doc) { 53 | Gauge.Builder builder = Gauge.build().name(name).help(doc); 54 | if (labels != null) { 55 | builder.labelNames(labels); 56 | } 57 | 58 | return builder.register(); 59 | } 60 | 61 | public static Histogram createAndRegisterTimed(String name, String[] labels, String doc) { 62 | Histogram.Builder builder = Histogram.build().name(name).help(doc); 63 | if (labels != null) { 64 | builder.labelNames(labels); 65 | } 66 | 67 | return builder.register(); 68 | } 69 | 70 | public static void recordCount(Counter counter, String[] labels) { 71 | if (labels != null) { 72 | counter.labels(labels).inc(); 73 | } else { 74 | counter.inc(); 75 | } 76 | } 77 | 78 | public static void recordCount(Counter counter, String[] labels, long n) { 79 | if (labels != null) { 80 | counter.labels(labels).inc(n); 81 | } else { 82 | counter.inc(n); 83 | } 84 | } 85 | 86 | public static void recordGaugeInc(Gauge gauge, String[] labelValues) { 87 | if (labelValues != null) { 88 | gauge.labels(labelValues).inc(); 89 | } else { 90 | gauge.inc(); 91 | } 92 | } 93 | 94 | public static void recordGaugeDec(Gauge gauge, String[] labelValues) { 95 | if (labelValues != null) { 96 | gauge.labels(labelValues).dec(); 97 | } else { 98 | gauge.dec(); 99 | } 100 | } 101 | 102 | public static void recordTime(Histogram histogram, String[] labels, long duration) { 103 | if (labels != null) { 104 | histogram.labels(labels).observe(duration); 105 | } else { 106 | histogram.observe(duration); 107 | } 108 | } 109 | 110 | private final Map configuration; 111 | 112 | protected PrometheusMetricSystem(Map configuration) { 113 | this.configuration = configuration; 114 | 115 | new StandardExports().register(); 116 | 117 | addJvmMetrics(configuration); 118 | 119 | addJmxCollector(configuration); 120 | 121 | startDefaultEndpoint(); 122 | } 123 | 124 | private void startDefaultEndpoint() { 125 | Thread thread = new Thread(new Runnable() { 126 | @Override 127 | public void run() { 128 | int port = DEFAULT_HTTP_PORT; 129 | 130 | if (configuration.containsKey("httpPort")) { 131 | port = Integer.parseInt((String)configuration.get("httpPort")); 132 | } 133 | 134 | try { 135 | LOGGER.fine("Starting Prometheus HttpServer on port " + port); 136 | 137 | new HTTPServer(port); 138 | 139 | } catch (Exception e) { //widen scope in case of ClassNotFoundException on non oracle/sun JVM 140 | LOGGER.log(WARNING, "Unable to register Prometheus HttpServer on port " + port, e); 141 | } 142 | } 143 | }); 144 | thread.setDaemon(true); 145 | thread.start(); 146 | } 147 | 148 | private void addJmxCollector(Map configuration) { 149 | if (!configuration.containsKey("jmx")) { 150 | return; 151 | } 152 | 153 | try { 154 | String jmxConfig = YAML_MAPPER.writeValueAsString(configuration.get("jmx")); 155 | new JmxCollector(jmxConfig).register(); 156 | } catch (Exception e) { 157 | LOGGER.log(WARNING, "Problem starting JmxCollector", e); 158 | } 159 | } 160 | 161 | private void addJvmMetrics(Map configuration) { 162 | if (!configuration.containsKey("jvm")) { 163 | return; 164 | } 165 | Set jvmMetrics = new HashSet((List)configuration.get("jvm")); 166 | if (jvmMetrics.contains("gc")) { 167 | new GarbageCollectorExports().register(); 168 | } 169 | 170 | if (jvmMetrics.contains("threads")) { 171 | new ThreadExports().register(); 172 | } 173 | 174 | if (jvmMetrics.contains("memory")) { 175 | new MemoryPoolsExports().register(); 176 | } 177 | 178 | if (jvmMetrics.contains("classloader")) { 179 | new ClassLoadingExports().register(); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /prometheus-metrics-agent-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.fleury 6 | prometheus-metrics-agent 7 | 0.0.7-SNAPSHOT 8 | 9 | prometheus-metrics-agent-core 10 | metrics-agent-core 11 | jar 12 | 13 | 14 | 15 | ${project.groupId} 16 | prometheus-metrics-agent-annotation 17 | ${project.version} 18 | 19 | 20 | 21 | io.prometheus 22 | simpleclient 23 | ${prometheus.version} 24 | 25 | 26 | 27 | io.prometheus 28 | simpleclient_hotspot 29 | ${prometheus.version} 30 | 31 | 32 | 33 | io.prometheus.jmx 34 | collector 35 | ${prometheus.jmx.version} 36 | 37 | 38 | 39 | io.prometheus 40 | simpleclient_httpserver 41 | ${prometheus.version} 42 | 43 | 44 | 45 | org.ow2.asm 46 | asm 47 | ${asm.version} 48 | 49 | 50 | 51 | org.ow2.asm 52 | asm-commons 53 | ${asm.version} 54 | 55 | 56 | 57 | com.fasterxml.jackson.core 58 | jackson-databind 59 | ${jackson.version} 60 | 61 | 62 | 63 | com.fasterxml.jackson.dataformat 64 | jackson-dataformat-yaml 65 | ${jackson.version} 66 | 67 | 68 | 69 | commons-beanutils 70 | commons-beanutils 71 | ${commons.beanutils.version} 72 | 73 | 74 | 75 | org.ow2.asm 76 | asm-util 77 | ${asm.version} 78 | test 79 | 80 | 81 | 82 | org.ow2.asm 83 | asm-analysis 84 | ${asm.version} 85 | test 86 | 87 | 88 | 89 | commons-io 90 | commons-io 91 | ${commons.io.version} 92 | test 93 | 94 | 95 | 96 | 97 | 98 | ${agent.artifact.name} 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-shade-plugin 103 | 2.3 104 | 105 | 106 | package 107 | 108 | shade 109 | 110 | 111 | 112 | 113 | *:* 114 | 115 | 116 | 117 | 118 | *:* 119 | 120 | META-INF/*.SF 121 | META-INF/*.DSA 122 | META-INF/*.RSA 123 | META-INF/*.RSA 124 | 125 | 126 | 127 | 128 | 129 | io.prometheus 130 | com.fleury.shaded.io.prometheus 131 | 132 | 133 | org 134 | com.fleury.shaded.org 135 | 136 | 137 | com.fasterxml.jackson 138 | com.fleury.shaded.com.fasterxml.jackson 139 | 140 | 141 | 142 | 143 | 144 | 145 | com.fleury.metrics.agent.Agent 146 | true 147 | 148 | NotSuitableAsMain 149 | Metrics Agent 150 | 1.0 151 | Will Fleury 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /prometheus-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[] {"name1", "name2"}, new String[]{"value1", "value2"}); 17 | } 18 | 19 | @Test 20 | public void shouldCountConstructorInvocationWithoutLabels() throws Exception { 21 | testInvocation(CountedConstructorWithoutLabelsClass.class, new String[] {}, new String[] {}); 22 | } 23 | 24 | @Test 25 | public void shouldCountConstructorInvocationWithEmptyLabels() throws Exception { 26 | testInvocation(CountedConstructorWithEmptyLabelsClass.class, new String[] {}, new String[] {}); 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[] {"name1"}, 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[] labelNames, String[] labelValues) throws Exception { 76 | Class clazz = execute(instrumentClazz); 77 | 78 | clazz.newInstance(); 79 | 80 | assertEquals(1, metrics.getCount("constructor", labelNames, 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", new String[] {"name1"}, 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", new String[] {"name1"}, 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 | public static class CountedConstructorWithInvalidParamIndexLabelValueClass { 192 | 193 | @Counted(name = "constructor", labels = {"name1:$5"}) 194 | public CountedConstructorWithInvalidParamIndexLabelValueClass(long value) { 195 | BaseMetricTest.performBasicTask(); 196 | } 197 | } 198 | 199 | public static class CountedConstructorWithInvalidDynamicLabelValueClass { 200 | 201 | @Counted(name = "constructor", labels = {"name1:$badlabel"}) 202 | public CountedConstructorWithInvalidDynamicLabelValueClass(long value) { 203 | BaseMetricTest.performBasicTask(); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /prometheus-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 | -------------------------------------------------------------------------------- /prometheus-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 | public static String staticFinalFieldName(Metric metric) { 46 | return ("metrics$" + metric.getName()+"$"+metric.getType()).toUpperCase(); 47 | } 48 | 49 | public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()) { 50 | { 51 | configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 52 | configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true); 53 | setVisibilityChecker(getSerializationConfig().getDefaultVisibilityChecker() 54 | .withFieldVisibility(JsonAutoDetect.Visibility.ANY)); 55 | registerModule(new ConfigSimpleModule()); 56 | } 57 | }; 58 | 59 | public static Configuration createConfig(String filename) { 60 | if (filename == null) { 61 | return emptyConfiguration(); 62 | } 63 | 64 | LOGGER.log(FINE, "Found config file: {0}", filename); 65 | 66 | try { 67 | return createConfig(new FileInputStream(filename)); 68 | } catch (FileNotFoundException e) { 69 | throw new RuntimeException(e); 70 | } 71 | } 72 | 73 | public static Configuration createConfig(InputStream is) { 74 | try { 75 | return YAML_MAPPER.readValue(is, Configuration.class); 76 | } catch (Exception e) { 77 | throw new RuntimeException(e); 78 | } finally { 79 | try { 80 | is.close(); 81 | } catch (IOException e) {} 82 | } 83 | } 84 | 85 | public static Configuration emptyConfiguration() { 86 | return new Configuration(); 87 | } 88 | 89 | private final Set imports; 90 | private final Map> metrics; 91 | private final Map system; 92 | private final List whiteList; 93 | private final List blackList; 94 | 95 | private Configuration() { 96 | this(new HashMap>(), 97 | Collections.emptySet(), 98 | Collections.emptyMap(), 99 | Collections.emptyList(), 100 | Collections.emptyList()); 101 | } 102 | 103 | @JsonCreator 104 | public Configuration( 105 | @JsonProperty("metrics") Map> metrics, 106 | @JsonProperty("imports") Set imports, 107 | @JsonProperty("system") Map system, 108 | @JsonProperty("whiteList") List whiteList, 109 | @JsonProperty("blackList") List blackList) { 110 | 111 | this.imports = imports == null ? Collections.emptySet() : imports; 112 | 113 | this.metrics = metrics == null ? 114 | new HashMap>() : 115 | processClassImports(metrics, this.imports); //ensure fqn expanded from imports 116 | 117 | this.system = system == null ? Collections.emptyMap() : system; 118 | this.whiteList = whiteList == null ? Collections.emptyList() : whiteList; 119 | this.blackList = blackList == null ? Collections.emptyList() : blackList; 120 | } 121 | 122 | private static Map> processClassImports(Map> metrics, Set imports) { 123 | Map expandedKeys = fqnToMap(imports); 124 | 125 | Map> processed = new HashMap>(); 126 | for (Map.Entry> entry : metrics.entrySet()) { 127 | Key key = entry.getKey(); 128 | 129 | String fqn = expandedKeys.get(key.getClassName()); 130 | if (fqn == null) { 131 | fqn = key.getClassName(); 132 | } 133 | 134 | String descriptor = key.descriptor; 135 | 136 | Map fqnMap = getMethodDescriptorFQNMap(descriptor); 137 | for (String className : fqnMap.keySet()) { 138 | if (expandedKeys.containsKey(className)) { 139 | descriptor = descriptor.replaceAll(className, expandedKeys.get(className)); 140 | } 141 | } 142 | 143 | key = new Key(fqn, key.getMethod(), descriptor); 144 | 145 | processed.put(key, entry.getValue()); 146 | } 147 | 148 | return processed; 149 | } 150 | 151 | 152 | public boolean isMetric(String className) { 153 | for (Key key : metrics.keySet()) { 154 | if (key.className.equals(className)) { 155 | return true; 156 | } 157 | } 158 | 159 | return false; 160 | } 161 | 162 | public List findMetrics(String className) { 163 | if (metrics.isEmpty()) return Collections.emptyList(); 164 | 165 | List found = new ArrayList(); 166 | for (Key key : metrics.keySet()) { 167 | if (key.className.equals(className)) { 168 | found.addAll(metrics.get(key)); 169 | } 170 | } 171 | 172 | return found; 173 | } 174 | 175 | public List findMetrics(String className, String method, String descriptor) { 176 | Key key = new Key(className, method, descriptor); 177 | return metrics.containsKey(key) ? metrics.get(key) : Collections.emptyList(); 178 | } 179 | 180 | public void addMetric(Key key, Metric metric) { 181 | List keyMetrics = metrics.get(key); 182 | 183 | if (keyMetrics == null) { 184 | keyMetrics = new ArrayList(); 185 | metrics.put(key, keyMetrics); 186 | } 187 | 188 | keyMetrics.add(metric); 189 | } 190 | 191 | public Map getSystem() { 192 | return system; 193 | } 194 | 195 | public List getWhiteList() { 196 | return whiteList; 197 | } 198 | 199 | public List getBlackList() { 200 | return blackList; 201 | } 202 | 203 | public boolean isWhiteListed(String className) { 204 | if (whiteList.isEmpty()) return true; 205 | 206 | for (String white : whiteList) { 207 | if (className.startsWith(white)) { 208 | return true; 209 | } 210 | } 211 | 212 | return false; 213 | } 214 | 215 | public boolean isBlackListed(String className) { 216 | if (blackList.isEmpty()) return false; 217 | 218 | for (String black : blackList) { 219 | if (className.startsWith(black)) { 220 | return true; 221 | } 222 | } 223 | 224 | return false; 225 | } 226 | 227 | @Override 228 | public String toString() { 229 | return "Configuration{" + 230 | "metrics=" + metrics + 231 | ", system=" + system + 232 | ", whiteList=" + whiteList + 233 | ", blackList=" + blackList + 234 | '}'; 235 | } 236 | 237 | public static Map fqnToMap(Collection classNames) { 238 | Map expandedKeys = new HashMap(); 239 | for (String fqn : classNames) { 240 | String className = fqn.substring(fqn.lastIndexOf("/") + 1, fqn.length()); 241 | expandedKeys.put(className, fqn); 242 | } 243 | 244 | return expandedKeys; 245 | } 246 | 247 | public static Map getMethodDescriptorFQNMap(String descriptor) { 248 | Type type = Type.getMethodType(descriptor); 249 | 250 | Set classes = new HashSet(); 251 | classes.add(type.getReturnType().getClassName()); 252 | 253 | Type[] arguments = type.getArgumentTypes(); 254 | if (arguments != null) { 255 | for (Type arg : arguments) { 256 | classes.add(arg.getClassName()); 257 | } 258 | } 259 | 260 | return fqnToMap(classes); 261 | } 262 | 263 | public static class Key { 264 | 265 | private final String className; 266 | private final String method; 267 | private final String descriptor; 268 | 269 | public Key(String className, String method, String descriptor) { 270 | this.className = className; 271 | this.method = method; 272 | this.descriptor = descriptor; 273 | } 274 | 275 | public String getClassName() { 276 | return className; 277 | } 278 | 279 | public String getMethod() { 280 | return method; 281 | } 282 | 283 | public String getDescriptor() { 284 | return descriptor; 285 | } 286 | 287 | @Override 288 | public int hashCode() { 289 | int hash = 3; 290 | hash = 29 * hash + (this.className != null ? this.className.hashCode() : 0); 291 | hash = 29 * hash + (this.method != null ? this.method.hashCode() : 0); 292 | hash = 29 * hash + (this.descriptor != null ? this.descriptor.hashCode() : 0); 293 | return hash; 294 | } 295 | 296 | @Override 297 | public boolean equals(Object obj) { 298 | if (obj == null) { 299 | return false; 300 | } 301 | if (getClass() != obj.getClass()) { 302 | return false; 303 | } 304 | final Key other = (Key) obj; 305 | if ((this.className == null) ? (other.className != null) : !this.className.equals(other.className)) { 306 | return false; 307 | } 308 | if ((this.method == null) ? (other.method != null) : !this.method.equals(other.method)) { 309 | return false; 310 | } 311 | if ((this.descriptor == null) ? (other.descriptor != null) : !this.descriptor.equals(other.descriptor)) { 312 | return false; 313 | } 314 | return true; 315 | } 316 | 317 | @Override 318 | public String toString() { 319 | return "Key{" + 320 | "className='" + className + '\'' + 321 | ", method='" + method + '\'' + 322 | ", desc='" + descriptor + '\'' + 323 | '}'; 324 | } 325 | } 326 | 327 | static class MetricKey extends KeyDeserializer { 328 | 329 | @Override 330 | public Object deserializeKey(final String key, final DeserializationContext ctxt) throws IOException { 331 | String className = dotToSlash(key.substring(0, key.lastIndexOf("."))); 332 | String methodName = key.substring(key.lastIndexOf(".") + 1, key.indexOf("(")); 333 | 334 | String desc = key.substring(key.indexOf("("), key.length()); 335 | 336 | return new Key(className, methodName, desc); 337 | } 338 | } 339 | 340 | static class ConfigSimpleModule extends SimpleModule { 341 | 342 | public ConfigSimpleModule() { 343 | addKeyDeserializer(Key.class, new MetricKey()); 344 | } 345 | 346 | } 347 | 348 | } 349 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | - [Motivation](#motivation) 3 | - [Code Bloat Problem](#code-bloat-problem) 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 | - [Agent Configuration](#agent-configuration) 13 | - [Prometheus Configuration](#prometheus-configuration) 14 | - [JVM Metrics](#jvm-metrics) 15 | - [JMX Metrics](#jmx-metrics) 16 | - [Agent Reporting](#agent-reporting) 17 | - [Black & White Lists](#black-and-white-lists) 18 | - [Logger Configuration](#logger-configuration) 19 | - [Performance](#performance) 20 | - [Dependencies](#dependencies) 21 | - [Binaries & Releases](#binaries-releases) 22 | - [Building](#building) 23 | - [Usage](#usage) 24 | - [Debugging](#debugging) 25 | - [Examples](#examples) 26 | 27 | 28 | Forked from the following agent project [https://github.com/willfleury/metrics-agent](https://github.com/willfleury/metrics-agent) and customised specifically for Prometheus and performance. 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 | This metrics agent performs bytecode instrumentation as if you had written the metrics manually, thereby minimising any impact to performance or stack trace readability while allowing one to keep the code clean from metric pollution. 38 | 39 | 40 | ### Code Bloat Problem 41 | 42 | Lets illustrate the code bloat problem. 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 to instrumentation 45 | 46 | ```java 47 | public Result performSomeTask() { 48 | return callSomeServiceMethodWhichCanThrowException(createArgs()); 49 | } 50 | ``` 51 | 52 | To instrument this programmatically we perform the following 53 | 54 | ```java 55 | // add class fields 56 | 57 | static final Counter total = Metrics.createCounter("requests_total"); 58 | static final Counter failed = Metrics.createCounter("requests_failed"); 59 | 60 | public Result performSomeTask() { 61 | total.inc(); 62 | 63 | Result result = null; 64 | try { 65 | //perform actual original call 66 | result = callSomeServiceMethodWhichCanThrowException(createArgs()); 67 | } catch (Exception e) { 68 | failed.inc(); 69 | throw e; 70 | } 71 | 72 | return result; 73 | } 74 | ``` 75 | 76 | Now lets add a timer to this also so we can see how long the method call takes. 77 | 78 | ```java 79 | // add class fields 80 | 81 | static final Counter total = Metrics.createCounter("requests_total"); 82 | static final Counter failed = Metrics.createCounter("requests_failed"); 83 | static final Timer timer = Metrics.createTimer("requests_timer"); 84 | 85 | public Result performSomeTask() { 86 | long startTime = System.nanoTime(); 87 | total.inc(); 88 | 89 | Result result = null; 90 | try { 91 | result = callSomeServiceMethodWhichCanThrowException(createArgs()); 92 | } catch (Exception e) { 93 | failed.inc(); 94 | throw e; 95 | } finally { 96 | timer.record(System.nanoTime() - startTime); 97 | } 98 | 99 | return result; 100 | } 101 | ``` 102 | 103 | 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. 104 | 105 | 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. 106 | 107 | 108 | ## Instrumentation Metadata 109 | 110 | 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. 111 | 112 | 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. 113 | 114 | 115 | ### Annotations 116 | 117 | ```java 118 | @Counted (name = "", labels = { }, doc = "") 119 | @Gauged (name = "", mode=in_flight, labels = { }, doc = "") 120 | @Timed (name = "", labels = { }, doc = "") 121 | @ExceptionCounted (name = "", labels = { }, doc = "") 122 | ``` 123 | 124 | Annotations are provided for all metric types and can be added to methods including 125 | constructors. 126 | 127 | ```java 128 | @Counted(name = "taskx_total", doc = "total invocations of task x") 129 | @Timed (name = "taskx_time", doc = "duration of task x") 130 | public Result performSomeTask() { 131 | //... 132 | } 133 | ``` 134 | 135 | ### Configuration 136 | 137 | metrics: 138 | {class name}.{method name}{method signature}: 139 | - type: Counted 140 | name: {name} 141 | doc: {metric documentation} 142 | labels: ['{name:value}', '{name:value}'] 143 | - type: Gauged 144 | name: {name} 145 | mode: {mode} 146 | doc: {metric documentation} 147 | labels: ['{name:value}'] 148 | - type: ExceptionCounted 149 | name: {name} 150 | doc: {metric documentation} 151 | labels: ['{name:value}'] 152 | - type: Timed 153 | name: {name} 154 | doc: {metric documentation} 155 | labels: ['{name:value}'] 156 | 157 | Each metric is defined on a per method basis. A method is uniquely identified by the 158 | combination of `{class name}.{method name}{method signature}`. As an example, if we 159 | wanted to instrument the following method via configuration instead of annotations 160 | 161 | ```java 162 | package com.fleury.test; 163 | .... 164 | 165 | public class TestClass { 166 | .... 167 | 168 | @Counted(name = "taskx_total", doc = "total invocations of task x") 169 | public Result performSomeTask() { 170 | ... 171 | } 172 | } 173 | ``` 174 | 175 | We write the configuration as follows 176 | 177 | metrics: 178 | com/fleury/test/TestClass.performSomeTask()V: 179 | - type: Counted 180 | name: taskx_total 181 | doc: total invocations of task x 182 | 183 | 184 | 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 no parameters and the return type is void which results in `()V`. [Here](http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html) is a good overview of Java method signature mappings. 185 | 186 | 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. 187 | 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 | 212 | ### Metric Labels 213 | 214 | 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/). 215 | 216 | 217 | #### Dynamic Label Values 218 | 219 | 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. 220 | 221 | 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. 222 | 223 | ```java 224 | @Counted (name = "service_total", labels = { "client:$0" }) 225 | public void callService(String client) 226 | ``` 227 | 228 | 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. 229 | 230 | 231 | ### What we actually Transform 232 | 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. 233 | 234 | ### Supported Languages 235 | 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. 236 | 237 | As an example. Lets take the followin Scala class 238 | ```scala 239 | class Person(val name:String) { 240 | } 241 | ``` 242 | If we run `javap` on this 243 | 244 | javap Person.class 245 | 246 | we get 247 | 248 | Compiled from "Person.scala" 249 | public class Person { 250 | private final java.lang.String name; // field 251 | public java.lang.String name(); // getter method 252 | public Person(java.lang.String); // constructor 253 | } 254 | 255 | 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. 256 | 257 | 258 | ## Agent Configuration 259 | 260 | ### Prometheus Configuration 261 | 262 | #### JVM Metrics 263 | Prometheus supports adding JVM level metrics information obtained from the JVM via MBeans for 264 | 265 | - gc 266 | - memory 267 | - classloading 268 | - threads 269 | 270 | 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: 271 | 272 | system: 273 | jvm: 274 | - gc 275 | - memory 276 | 277 | #### JMX Metrics 278 | We also include the Prometheus JmxCollector from the [JmxExporter](https://github.com/prometheus/jmx_exporter) project in this agent as it allows collecting other JMX metrics from the JVM one might want to. To enable the JmxCollector simply add a `jmx` section in the system configuration and add configuration as shown in the [JmxExporter](https://github.com/prometheus/jmx_exporter) project. The configuration is passed straight through to the JmxCollector constructor. 279 | 280 | system: 281 | jmx: 282 | startDelaySeconds: 0 283 | whitelistObjectNames: ["org.apache.cassandra.metrics:*"] 284 | blacklistObjectNames: ["org.apache.cassandra.metrics:type=ColumnFamily,*"] 285 | rules: 286 | - pattern: 'org.apache.cassandra.metrics<>Value: (\d+)' 287 | name: cassandra_$1_$2 288 | value: $3 289 | valueFactor: 0.001 290 | labels: {} 291 | help: "Cassandra metric $1 $2" 292 | type: GAUGE 293 | attrNameSnakeCase: false 294 | 295 | 296 | ### Agent Reporting 297 | 298 | We start the default reporting (endpoint) for Prometheus which 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 299 | 300 | system: 301 | httpPort: 9899 302 | 303 | Support for push based reporting could be easily added and made configurable. 304 | 305 | 306 | ### Black and White Lists 307 | 308 | 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. 309 | 310 | 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. 311 | 312 | whiteList: 313 | - com/fleury/test/ClassName 314 | - com/fleury/package2 315 | 316 | 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. 317 | 318 | blackList: 319 | - com/ 320 | 321 | ### Logger Configuration 322 | 323 | 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. 324 | 325 | 326 | ## Performance 327 | We use the Java ASM bytecode manipulation library. This is the lowest level bytecode manipulation library and is the basis of most other higher level libraries such as 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. We create static level fields to hold the metric references which means there is no lookup required when performing an operation on the metric. This is again how you would write it manually if taking care for speed. 328 | 329 | 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). 330 | 331 | 332 | ## Dependencies 333 | Very lightweight. 334 | 335 | asm 336 | jackson 337 | prometheus 338 | 339 | Note that the final agent binaries are shaded and all dependencies relocated to prevent possible conflicts. 340 | 341 | 342 | # Binaries & Releases 343 | 344 | See the releases section of the github repository for releases along with the prebuilt agent binaries. 345 | 346 | # Building 347 | 348 | mvn clean package 349 | 350 | The uber jar can be found under `/target/metrics-agent.jar` 351 | 352 | # Usage 353 | 354 | The agent must be attached to the JVM at startup. It cannot be attached to a running JVM. 355 | 356 | -javaagent:metrics-agent.jar 357 | 358 | Example 359 | 360 | java -javaagent:metrics-agent.jar -jar myapp.jar 361 | 362 | Using the configuration file config.yaml is performed as follows 363 | 364 | java -javaagent:metrics-agent.jar=agent-config:agent.yaml -jar myapp.jar 365 | 366 | 367 | Using the configuration file config.yaml and logging configuration logger.properties is performed as follows 368 | 369 | java -javaagent:metrics-agent.jar=agent-config:agent.yaml,log-config:logger.properties -jar myapp.jar 370 | 371 | 372 | # Debugging 373 | 374 | Note if you want to debug the metrics agent you should put the debugger agent first. 375 | 376 | -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address= -javaagent:metrics-agent.jar myapp.jar 377 | 378 | 379 | # Examples 380 | 381 | 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. 382 | 383 | - [Jersey](example-configurations/jersey.yaml) 384 | - [Dropwizard (via Jersey)](example-configurations/dropwizard.yaml) 385 | - [Tomcat Servlet, JSP, Jersey](example-configurations/tomcat.yaml) 386 | - [Hibernate](example-configurations/hibernate.yaml) --------------------------------------------------------------------------------