├── AUTHORS ├── screenshot.png ├── .gitignore ├── promagent-framework ├── promagent-internal │ ├── src │ │ ├── test │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── promagent │ │ │ │ └── internal │ │ │ │ ├── instrumentationtests │ │ │ │ ├── classes │ │ │ │ │ ├── Fruit.java │ │ │ │ │ ├── StaticFinalExample.java │ │ │ │ │ ├── IReturnedAndThrownExample.java │ │ │ │ │ ├── ReturnedAndThrownExample.java │ │ │ │ │ ├── IParameterTypesExample.java │ │ │ │ │ └── ParameterTypesExample.java │ │ │ │ ├── hooks │ │ │ │ │ ├── OnlyAfterHook.java │ │ │ │ │ ├── OnlyBeforeHook.java │ │ │ │ │ ├── StaticFinalTestHook.java │ │ │ │ │ ├── LifecycleHookSkipFalse.java │ │ │ │ │ ├── LifecycleHookSkipTrue.java │ │ │ │ │ ├── TwoHooks.java │ │ │ │ │ ├── ReturnedAndThrownHook.java │ │ │ │ │ └── ParameterTypesHook.java │ │ │ │ ├── Util.java │ │ │ │ ├── Instrumentor.java │ │ │ │ ├── MethodCallCounter.java │ │ │ │ ├── StaticFinalTest.java │ │ │ │ ├── LifecycleTest.java │ │ │ │ ├── ReturnedAndThrownTest.java │ │ │ │ └── ParameterTypesTest.java │ │ │ │ ├── HookMetadataTest.java │ │ │ │ └── HookMetadataParserTest.java │ │ └── main │ │ │ └── java │ │ │ └── io │ │ │ └── promagent │ │ │ └── internal │ │ │ ├── HookInstance.java │ │ │ ├── jmx │ │ │ ├── ExporterMBean.java │ │ │ ├── MetricMBean.java │ │ │ ├── Exporter.java │ │ │ ├── Metric.java │ │ │ └── PromagentCollectorRegistry.java │ │ │ ├── HookException.java │ │ │ ├── BuiltInServer.java │ │ │ ├── PromagentAdvice.java │ │ │ ├── HookMetadata.java │ │ │ ├── Promagent.java │ │ │ └── Delegator.java │ └── pom.xml ├── promagent-api │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── promagent │ │ ├── annotations │ │ ├── After.java │ │ ├── Before.java │ │ ├── Returned.java │ │ ├── Hook.java │ │ └── Thrown.java │ │ └── hookcontext │ │ ├── MetricDef.java │ │ └── MetricsStore.java ├── promagent-exporter │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── promagent │ │ └── exporter │ │ └── PromagentExporterServlet.java ├── promagent-loader │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── promagent │ │ └── loader │ │ └── PromagentLoader.java ├── promagent-agent │ ├── src │ │ ├── main │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── promagent │ │ │ │ └── agent │ │ │ │ ├── Promagent.java │ │ │ │ ├── PerDeploymentClassLoader.java │ │ │ │ ├── ClassLoaderCache.java │ │ │ │ └── JarFiles.java │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── promagent │ │ │ └── agent │ │ │ └── JarFilesTest.java │ └── pom.xml ├── promagent-maven-plugin │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── promagent │ │ └── plugin │ │ ├── ManifestTransformer.java │ │ ├── JarFileNames.java │ │ ├── PromagentMojo.java │ │ ├── AgentJar.java │ │ └── AgentDependencies.java └── pom.xml ├── .travis.yml ├── promagent-example ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── promagent │ │ │ ├── hooks │ │ │ ├── HttpContext.java │ │ │ ├── ServletHook.java │ │ │ └── JdbcHook.java │ │ │ └── collectors │ │ │ └── JmxCollector.java │ └── test │ │ └── java │ │ └── io │ │ └── promagent │ │ └── it │ │ ├── SpringIT.java │ │ └── WildflyIT.java └── pom.xml ├── JAVA_9_DEMO.md └── LICENSE /AUTHORS: -------------------------------------------------------------------------------- 1 | Fabian Stäber -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fstab/promagent/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dependency-reduced-pom.xml 3 | target/ 4 | **/*.iml 5 | **/.classpath 6 | **/.project 7 | **/.settings 8 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/Fruit.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | /** 4 | * Example of object oriented inheritance. 5 | */ 6 | public class Fruit { 7 | public static class Orange extends Fruit{} 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: java 3 | jdk: 4 | - oraclejdk8 5 | services: 6 | - docker 7 | script: 8 | - mvn -f promagent-framework/pom.xml clean install 9 | - mvn -f promagent-example/pom-with-docker-tests.xml clean verify -Pwildfly 10 | - mvn -f promagent-example/pom-with-docker-tests.xml clean verify -Pspring 11 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/HookInstance.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal; 2 | 3 | public class HookInstance { 4 | 5 | private final Object instance; 6 | private final boolean isRecursiveCall; // true if we have taken an existing instance from the ThreadLocal 7 | 8 | public HookInstance(Object instance, boolean isRecursiveCall) { 9 | this.instance = instance; 10 | this.isRecursiveCall = isRecursiveCall; 11 | } 12 | 13 | public Object getInstance() { 14 | return instance; 15 | } 16 | 17 | public boolean isRecursiveCall() { 18 | return isRecursiveCall; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/StaticFinalExample.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | public class StaticFinalExample { 4 | 5 | public String helloPublic(String name) { 6 | return "hello public " + name; 7 | } 8 | 9 | public final String helloPublicFinal(String name) { 10 | return "hello public final " + name; 11 | } 12 | 13 | public static String helloPublicStatic(String name) { 14 | return "hello public static " + name; 15 | } 16 | 17 | public static String helloPublicStaticFinal(String name) { 18 | return "hello public static final " + name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/IReturnedAndThrownExample.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | public interface IReturnedAndThrownExample { 7 | 8 | // TODO: Some more return types that should be tested: Enums, Lambdas 9 | 10 | void returnVoid(Fruit f); 11 | 12 | int returnPrimitive(Fruit.Orange orange); 13 | 14 | Fruit returnObject(); 15 | 16 | int[] returnArray(int... params); 17 | 18 | List returnGenerics(T fruit); 19 | 20 | String throwsRuntimeException(int a, Fruit.Orange b); 21 | 22 | int throwsCheckedException() throws IOException; 23 | } 24 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | 10 | promagent-api 11 | promagent-api API for implementing hooks 12 | 13 | jar 14 | 15 | 16 | 17 | io.prometheus 18 | simpleclient_common 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/jmx/ExporterMBean.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.jmx; 16 | 17 | public interface ExporterMBean { 18 | String getTextFormat(); 19 | } 20 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/OnlyAfterHook.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.After; 4 | import io.promagent.annotations.Hook; 5 | import io.promagent.hookcontext.MetricsStore; 6 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 7 | import io.promagent.internal.instrumentationtests.classes.Fruit; 8 | 9 | /** 10 | * Test hook with no @Before method. 11 | */ 12 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample") 13 | public class OnlyAfterHook { 14 | 15 | public OnlyAfterHook(MetricsStore m) {} 16 | 17 | @After(method = "objects") 18 | public void after(Object o, Fruit f, Fruit.Orange x) { 19 | MethodCallCounter.observe(this, "after", o, f, x); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/OnlyBeforeHook.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.Before; 4 | import io.promagent.annotations.Hook; 5 | import io.promagent.hookcontext.MetricsStore; 6 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 7 | import io.promagent.internal.instrumentationtests.classes.Fruit; 8 | 9 | /** 10 | * Test hook with no @After method 11 | */ 12 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample") 13 | public class OnlyBeforeHook { 14 | 15 | public OnlyBeforeHook(MetricsStore m) {} 16 | 17 | @Before(method = "objects") 18 | public void before(Object o, Fruit f, Fruit.Orange x) { 19 | MethodCallCounter.observe(this, "before", o, f, x); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/HookException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | public class HookException extends RuntimeException { 18 | 19 | public HookException(String message) { 20 | super(message); 21 | } 22 | 23 | public HookException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /promagent-example/src/main/java/io/promagent/hooks/HttpContext.java: -------------------------------------------------------------------------------- 1 | package io.promagent.hooks; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | 7 | /** 8 | * Store the HTTP context in a thread-local, so that we know which database call was triggered by which REST service. 9 | */ 10 | class HttpContext { 11 | 12 | static class Key {} 13 | 14 | static final Key HTTP_METHOD = new Key<>(); 15 | static final Key HTTP_PATH = new Key<>(); 16 | 17 | private static final ThreadLocal> threadLocal = ThreadLocal.withInitial(HashMap::new); 18 | 19 | static void put(Key key, T value) { 20 | threadLocal.get().put(key, value); 21 | } 22 | 23 | @SuppressWarnings("unchecked") 24 | static Optional get(Key key) { 25 | return Optional.ofNullable((T) threadLocal.get().get(key)); 26 | } 27 | 28 | static void clear(Key... keys) { 29 | for (Key key : keys) { 30 | threadLocal.get().remove(key); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /promagent-framework/promagent-exporter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | 10 | promagent-exporter 11 | promagent.war 12 | 13 | 14 | false 15 | 16 | 17 | war 18 | 19 | 20 | 21 | javax.servlet 22 | javax.servlet-api 23 | 4.0.1 24 | provided 25 | 26 | 27 | 28 | 29 | promagent 30 | 31 | 32 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/jmx/MetricMBean.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.jmx; 16 | 17 | import java.util.Map; 18 | 19 | public interface MetricMBean { 20 | 21 | /** 22 | * Get the values in a representation that can be used in MXBeans in JMX. 23 | *

24 | * The result is a map of labels -> value. 25 | * The labels themselves are represented as a key -> value map. 26 | */ 27 | Map, Double> getValues(); 28 | } 29 | -------------------------------------------------------------------------------- /promagent-framework/promagent-loader/README.md: -------------------------------------------------------------------------------- 1 | promagent-loader 2 | ---------------- 3 | 4 | Experimental tool for loading the promagent into an existing JVM. Usage: 5 | 6 | ```bash 7 | java -cp $JAVA_HOME/lib/tools.jar:/path/to/promagent-loader.jar io.promagent.loader.PromagentLoader -agent /path/to/promagent.jar -port 9300 -pid 8 | ``` 9 | 10 | The `promagent-loader` uses OpenJDK API and will probably not work with other Java VMs. 11 | 12 | With JDK 8 or earlier, the file `$JAVA_HOME/lib/tools.jar` must exist. With JDK 9 or higher, the loader runs without this external dependency, because the classes have been moved into the Java runtime. 13 | 14 | The `/path/to/promagent.jar` must be an absolute path, not a relative path. 15 | 16 | The port is the TCP port for the exporter, like when promagent is attached on JVM startup with parameter `-javaagent:/path/to/promagent.jar=port=9300`. 17 | 18 | The `` is the PID of the Java process that the agent should attach to. It can be found with the `jps` command. 19 | 20 | See [https://github.com/raphw/byte-buddy/tree/master/byte-buddy-agent](https://github.com/raphw/byte-buddy/tree/master/byte-buddy-agent) for a more portable implementation of an agent loader. 21 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/StaticFinalTestHook.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.After; 4 | import io.promagent.annotations.Before; 5 | import io.promagent.annotations.Hook; 6 | import io.promagent.hookcontext.MetricsStore; 7 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 8 | 9 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.StaticFinalExample") 10 | public class StaticFinalTestHook { 11 | 12 | public StaticFinalTestHook(MetricsStore m) {} 13 | 14 | @Before(method = { 15 | "helloPublic", 16 | "helloPublicFinal", 17 | "helloPublicStatic", 18 | "helloPublicStaticFinal" 19 | }) 20 | public void before(String name) { 21 | MethodCallCounter.observe(this, "before", name); 22 | } 23 | 24 | @After(method = { 25 | "helloPublic", 26 | "helloPublicFinal", 27 | "helloPublicStatic", 28 | "helloPublicStaticFinal" 29 | }) 30 | public void after(String name) { 31 | MethodCallCounter.observe(this, "after", name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/ReturnedAndThrownExample.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | import java.io.IOException; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | public class ReturnedAndThrownExample implements IReturnedAndThrownExample { 8 | 9 | @Override 10 | public void returnVoid(Fruit f) {} 11 | 12 | @Override 13 | public int returnPrimitive(Fruit.Orange orange) { 14 | return 42; 15 | } 16 | 17 | @Override 18 | public Fruit returnObject() { 19 | return new Fruit.Orange(); 20 | } 21 | 22 | @Override 23 | public int[] returnArray(int... params) { 24 | return new int[] {23, 42}; 25 | } 26 | 27 | @Override 28 | public List returnGenerics(T fruit) { 29 | return Collections.singletonList(fruit); 30 | } 31 | 32 | @Override 33 | public String throwsRuntimeException(int a, Fruit.Orange b) { 34 | Object n = null; 35 | return n.toString(); // throws NullPointerException 36 | } 37 | 38 | @Override 39 | public int throwsCheckedException() throws IOException { 40 | throw new IOException(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/LifecycleHookSkipFalse.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.After; 4 | import io.promagent.annotations.Before; 5 | import io.promagent.annotations.Hook; 6 | import io.promagent.hookcontext.MetricsStore; 7 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 8 | import io.promagent.internal.instrumentationtests.classes.Fruit; 9 | import io.promagent.internal.instrumentationtests.classes.Fruit.Orange; 10 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 11 | 12 | /** 13 | * Instrument all methods in {@link ParameterTypesExample}. 14 | */ 15 | @Hook( 16 | instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample", 17 | skipNestedCalls = false 18 | ) 19 | public class LifecycleHookSkipFalse { 20 | 21 | public LifecycleHookSkipFalse(MetricsStore m) {} 22 | 23 | @Before(method = "recursive") 24 | public void before(int n) { 25 | MethodCallCounter.observe(this, "before", n); 26 | } 27 | 28 | @After(method = "recursive") 29 | public void after(int n) { 30 | MethodCallCounter.observe(this, "after", n); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/LifecycleHookSkipTrue.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.After; 4 | import io.promagent.annotations.Before; 5 | import io.promagent.annotations.Hook; 6 | import io.promagent.hookcontext.MetricsStore; 7 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 8 | import io.promagent.internal.instrumentationtests.classes.Fruit; 9 | import io.promagent.internal.instrumentationtests.classes.Fruit.Orange; 10 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Instrument all methods in {@link ParameterTypesExample}. 16 | */ 17 | @Hook( 18 | instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample", 19 | skipNestedCalls = true 20 | ) 21 | public class LifecycleHookSkipTrue { 22 | 23 | public LifecycleHookSkipTrue(MetricsStore m) {} 24 | 25 | @Before(method = "recursive") 26 | public void before(int n) { 27 | MethodCallCounter.observe(this, "before", n); 28 | } 29 | 30 | @After(method = "recursive") 31 | public void after(int n) { 32 | MethodCallCounter.observe(this, "after", n); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/TwoHooks.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.Before; 4 | import io.promagent.annotations.Hook; 5 | import io.promagent.hookcontext.MetricsStore; 6 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 7 | import io.promagent.internal.instrumentationtests.classes.Fruit; 8 | 9 | /** 10 | * Two hooks instrumenting the same class. 11 | */ 12 | public class TwoHooks { 13 | 14 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample") 15 | public static class HookOne { 16 | 17 | public HookOne(MetricsStore m) {} 18 | 19 | @Before(method = "objects") 20 | public void before(Object o, Fruit f, Fruit.Orange x) { 21 | MethodCallCounter.observe(this, "before", o, f, x); 22 | } 23 | } 24 | 25 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample") 26 | public static class HookTwo { 27 | 28 | public HookTwo(MetricsStore m) {} 29 | 30 | @Before(method = "objects") 31 | public void before(Object o, Fruit f, Fruit.Orange x) { 32 | MethodCallCounter.observe(this, "before", o, f, x); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/annotations/After.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.annotations; 16 | 17 | import java.lang.annotation.ElementType; 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | * The method annotated with @After is executed before exiting the instrumented method. 24 | * The method annotated with @After must have exactly the same parameters as the instrumented method. 25 | * The "method" parameter are the names of the instrumented methods. 26 | */ 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @Target(ElementType.METHOD) 29 | public @interface After { 30 | String[] method(); 31 | } 32 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/annotations/Before.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.annotations; 16 | 17 | import java.lang.annotation.ElementType; 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | * The method annotated with @Before is executed before entering the instrumented method. 24 | * The method annotated with @Before must have exactly the same parameters as the instrumented method. 25 | * The "method" parameter are the names of the instrumented methods. 26 | */ 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @Target(ElementType.METHOD) 29 | public @interface Before { 30 | String[] method(); 31 | } 32 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/src/main/java/io/promagent/agent/Promagent.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.agent; 16 | 17 | import java.lang.instrument.Instrumentation; 18 | 19 | /** 20 | * We want as little dependencies as possible on the system class loader, 21 | * so the actual agent is loaded in its own class loader and this agent delegates to it. 22 | */ 23 | public class Promagent { 24 | 25 | public static void premain(String agentArgs, Instrumentation inst) throws Exception { 26 | ClassLoader agentClassLoader = ClassLoaderCache.getInstance().currentClassLoader(); 27 | Class agentClass = agentClassLoader.loadClass("io.promagent.internal.Promagent"); 28 | agentClass.getMethod("premain", String.class, Instrumentation.class).invoke(null, agentArgs, inst); 29 | } 30 | 31 | public static void agentmain(String agentArgs, Instrumentation inst) throws Exception { 32 | premain(agentArgs, inst); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/IParameterTypesExample.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | import io.promagent.internal.instrumentationtests.Instrumentor; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Example methods to be instrumented with hooks. 9 | * We use an interface to call these methods, because the actual implementation will come from a temporary 10 | * class loader defined in {@link Instrumentor}, which cannot be used directly. 11 | */ 12 | public interface IParameterTypesExample { 13 | 14 | // TODO: add tests for enums and lambdas 15 | 16 | void noParam(); 17 | 18 | void primitiveTypes(byte b, short s, int i, long l, float f, double d, boolean x, char c); 19 | 20 | void boxedTypes(Byte b, Short s, Integer i, Long l, Float f, Double d, Boolean x, Character c); 21 | 22 | void objects(Object o, Fruit f, Fruit.Orange x); 23 | 24 | void primitiveArrays(byte[] b, short[] s, int[] i, long[] l, float[] f, double[] d, boolean[] x, char[] c); 25 | 26 | void boxedArrays(Byte[] b, Short[] s, Integer[] i, Long[] l, Float[] f, Double[] d, Boolean[] x, Character[] c); 27 | 28 | void objectArrays(Object[] o, Fruit[] f, Fruit.Orange[] x); 29 | 30 | void generics(List objectList, List fruitList, List orangeList); 31 | 32 | void varargsExplicit(Object... args); 33 | 34 | void varargsImplicit(Object[] args); 35 | 36 | void varargsMixed(String s, String... more); 37 | 38 | void recursive(int nRecursiveCalls); 39 | } 40 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/jmx/Exporter.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.jmx; 16 | 17 | import io.prometheus.client.CollectorRegistry; 18 | import io.prometheus.client.exporter.common.TextFormat; 19 | 20 | import java.io.IOException; 21 | import java.io.StringWriter; 22 | 23 | public class Exporter implements ExporterMBean { 24 | 25 | private final CollectorRegistry registry; 26 | 27 | public Exporter(CollectorRegistry registry) { 28 | this.registry = registry; 29 | } 30 | 31 | @Override 32 | public String getTextFormat() { 33 | try { 34 | StringWriter result = new StringWriter(); 35 | TextFormat.write004(result, registry.metricFamilySamples()); 36 | return result.toString(); 37 | } catch (IOException e) { 38 | throw new RuntimeException("Unexpected error when writing metrics to a String: " + e.getMessage(), e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/jmx/Metric.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.jmx; 16 | 17 | import io.prometheus.client.Collector; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | public class Metric implements MetricMBean { 23 | 24 | private final Collector metric; 25 | 26 | Metric(Collector metric) { 27 | this.metric = metric; 28 | } 29 | 30 | /** 31 | * @see MetricMBean#getValues() 32 | */ 33 | @Override 34 | public Map, Double> getValues() { 35 | Map, Double> result = new HashMap<>(); 36 | for (Collector.MetricFamilySamples samples : metric.collect()) { 37 | for (Collector.MetricFamilySamples.Sample sample : samples.samples) { 38 | Map labels = new HashMap<>(); 39 | for (int i = 0; i < sample.labelNames.size(); i++) { 40 | labels.put(sample.labelNames.get(i), sample.labelValues.get(i)); 41 | } 42 | result.put(labels, sample.value); 43 | } 44 | } 45 | return result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/classes/ParameterTypesExample.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.classes; 2 | 3 | import io.promagent.internal.instrumentationtests.Instrumentor; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * These methods will be instrumented by {@link Instrumentor}. 9 | */ 10 | public class ParameterTypesExample implements IParameterTypesExample { 11 | 12 | @Override 13 | public void noParam() {} 14 | 15 | @Override 16 | public void primitiveTypes(byte b, short s, int i, long l, float f, double d, boolean x, char c) {} 17 | 18 | @Override 19 | public void boxedTypes(Byte b, Short s, Integer i, Long l, Float f, Double d, Boolean x, Character c) {} 20 | 21 | @Override 22 | public void objects(Object o, Fruit f, Fruit.Orange x) {} 23 | 24 | @Override 25 | public void primitiveArrays(byte[] b, short[] s, int[] i, long[] l, float[] f, double[] d, boolean[] x, char[] c) {} 26 | 27 | @Override 28 | public void boxedArrays(Byte[] b, Short[] s, Integer[] i, Long[] l, Float[] f, Double[] d, Boolean[] x, Character[] c) {} 29 | 30 | @Override 31 | public void objectArrays(Object[] o, Fruit[] f, Fruit.Orange[] x) {} 32 | 33 | @Override 34 | public void generics(List objectList, List fruitList, List orangeList) {} 35 | 36 | @Override 37 | public void varargsExplicit(Object... args) {} 38 | 39 | @Override 40 | public void varargsImplicit(Object[] args) {} 41 | 42 | @Override 43 | public void varargsMixed(String s, String... more) {} 44 | 45 | @Override 46 | public void recursive(int nRecursiveCalls) { 47 | if (nRecursiveCalls > 0) { 48 | recursive(nRecursiveCalls - 1); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | 10 | promagent-agent 11 | promagent-agent loaded from the system class loader 12 | 13 | jar 14 | 15 | 16 | 17 | org.junit.jupiter 18 | junit-jupiter-api 19 | test 20 | 21 | 22 | 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-jar-plugin 28 | 29 | 30 | 31 | io.promagent.agent.Promagent 32 | io.promagent.agent.Promagent 33 | true 34 | true 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/Util.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests; 2 | 3 | import io.promagent.agent.ClassLoaderCache; 4 | import io.promagent.hookcontext.MetricsStore; 5 | import io.promagent.internal.HookMetadata; 6 | import io.promagent.internal.HookMetadataParser; 7 | import org.mockito.Mockito; 8 | 9 | import java.io.IOException; 10 | import java.lang.reflect.Field; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.SortedSet; 16 | import java.util.stream.Stream; 17 | 18 | class Util { 19 | 20 | static SortedSet loadHookMetadata(Class... hooks) throws IOException, ClassNotFoundException { 21 | List classesDir = new ArrayList<>(); 22 | classesDir.add(Paths.get(Util.class.getProtectionDomain().getCodeSource().getLocation().getPath())); 23 | HookMetadataParser parser = new HookMetadataParser(classesDir); 24 | return parser.parse(className -> Stream.of(hooks).anyMatch(hookClass -> hookClass.getName().equals(className))); 25 | } 26 | 27 | static ClassLoaderCache mockClassLoaderCache() throws NoSuchFieldException, IllegalAccessException { 28 | ClassLoaderCache mockedClassLoaderCache = Mockito.mock(ClassLoaderCache.class); 29 | Mockito.when(mockedClassLoaderCache.currentClassLoader()).thenReturn(Thread.currentThread().getContextClassLoader()); 30 | Field instance = ClassLoaderCache.class.getDeclaredField("instance"); 31 | instance.setAccessible(true); 32 | instance.set(null, mockedClassLoaderCache); 33 | return mockedClassLoaderCache; 34 | } 35 | 36 | static MetricsStore mockMetricsStore() { 37 | return Mockito.mock(MetricsStore.class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/annotations/Returned.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.annotations; 16 | 17 | import java.lang.annotation.ElementType; 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | * A parameter annotated with @Returned can be used in a Hook's @After method to capture the return value of the instrumented method. 24 | * 25 | * Example: In order to instrument the following method: 26 | * 27 | *
28 |  *     int sum(int a, int b) {...}
29 |  * 
30 | * 31 | * A Hook could use an @After method like this: 32 | * 33 | *
34 |  *    {@literal @}After(method = "sum")
35 |  *     void after(int a, int b, @Returned int sum) {...}
36 |  * 
37 | * 38 | * The parameter annotated with @Returned is optional, if the hook does not use the return value, the parameter can be omitted. 39 | *

40 | * If the instrumented method terminates exceptionally, the type's default value is assigned to the parameter, 41 | * i.e. {@code 0} for numeric types and {@code null} for reference types. 42 | */ 43 | @Retention(RetentionPolicy.RUNTIME) 44 | @Target(ElementType.PARAMETER) 45 | public @interface Returned {} 46 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/HookMetadataTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import org.junit.jupiter.api.Test; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import static org.junit.jupiter.api.Assertions.*; 23 | 24 | class HookMetadataTest { 25 | 26 | private HookMetadata.LexicographicalComparator> comparator = new HookMetadata.LexicographicalComparator<>(); 27 | 28 | @Test 29 | void testComparatorEqualSize() { 30 | List list1 = Arrays.asList("a", "b", "c", "e"); 31 | List list2 = Arrays.asList("a", "b", "d", "e"); 32 | assertEquals(-1, comparator.compare(list1, list2)); 33 | } 34 | 35 | @Test 36 | void testComparatorDifferentSize() { 37 | List list1 = Arrays.asList("a", "b", "c"); 38 | List list2 = Arrays.asList("a", "b"); 39 | assertEquals(1, comparator.compare(list1, list2)); 40 | } 41 | 42 | @Test 43 | void testComparatorEqual() { 44 | List list1 = new ArrayList<>(); 45 | List list2 = new ArrayList<>(); 46 | assertEquals(0, comparator.compare(list1, list2)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /promagent-framework/promagent-loader/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | io.promagent 7 | promagent-framework 8 | 1.0-SNAPSHOT 9 | 10 | 11 | promagent-loader 12 | promagent-loader attach promagent to a running JVM 13 | 14 | jar 15 | 16 | 17 | 18 | org.junit.jupiter 19 | junit-jupiter-api 20 | test 21 | 22 | 23 | 24 | com.sun 25 | tools 26 | 1.8 27 | system 28 | ${java.home}/../lib/tools.jar 29 | 30 | 31 | 32 | 33 | 34 | 35 | org.apache.maven.plugins 36 | maven-jar-plugin 37 | 38 | 39 | 40 | io.promagent.loader.PromagentLoader 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /promagent-framework/promagent-exporter/src/main/java/io/promagent/exporter/PromagentExporterServlet.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.exporter; 16 | 17 | import javax.management.ObjectName; 18 | import javax.servlet.annotation.WebServlet; 19 | import javax.servlet.http.HttpServlet; 20 | import javax.servlet.http.HttpServletRequest; 21 | import javax.servlet.http.HttpServletResponse; 22 | import java.io.IOException; 23 | import java.lang.management.ManagementFactory; 24 | 25 | /** 26 | * This servlet simply calls the ExporterMBean via JMX and provides the result. 27 | */ 28 | @WebServlet("/") 29 | public class PromagentExporterServlet extends HttpServlet { 30 | 31 | @Override 32 | public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 33 | try { 34 | String result = (String) ManagementFactory.getPlatformMBeanServer() 35 | .getAttribute(new ObjectName("io.promagent:type=exporter"), "TextFormat"); 36 | response.getWriter().println(result); 37 | } catch (Exception e) { 38 | response.setStatus(500); 39 | response.getWriter().println("Failed to load Exporter MBean. Are you sure the Prometheus agent is running?"); 40 | e.printStackTrace(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/Instrumentor.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests; 2 | 3 | import io.promagent.internal.HookMetadata; 4 | import io.promagent.internal.Promagent; 5 | import io.promagent.internal.PromagentAdvice; 6 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 7 | import net.bytebuddy.ByteBuddy; 8 | import net.bytebuddy.asm.Advice; 9 | import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; 10 | 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.SortedSet; 14 | 15 | import static io.promagent.internal.Promagent.getInstruments; 16 | 17 | /** 18 | * Create an instrumented version of {@link ParameterTypesExample} for {@link ParameterTypesTest}. 19 | * The implementation is as close as possible to the instrumentation in {@link Promagent}. 20 | */ 21 | public class Instrumentor { 22 | 23 | /** 24 | * Returns a copy of {@link ParameterTypesExample} which will be instrumented and loaded from a temporary class loader. 25 | */ 26 | public static T instrument(Class classToBeInstrumented, SortedSet hookMetadata) throws Exception { 27 | Map> instruments = getInstruments(hookMetadata); 28 | Set instrumentedMethods = instruments.get(classToBeInstrumented.getName()); 29 | // For examples of byte buddy tests, see net.bytebuddy.asm.AdviceTest in the byte buddy source code. 30 | return new ByteBuddy() 31 | .redefine(classToBeInstrumented) 32 | .visit(Advice.to(PromagentAdvice.class).on(Promagent.matchAnyMethodIn(instrumentedMethods))) 33 | .make() 34 | .load(Instrumentor.class.getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST) 35 | .getLoaded() 36 | .newInstance(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/annotations/Hook.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.annotations; 16 | 17 | import java.lang.annotation.ElementType; 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | * The @Hook annotation indicates that the annotated class is a Hook. 24 | * The parameter {@link Hook#instruments()} defines which classes or interfaces are instrumented by that Hook. 25 | * The method annotations {@link Before} and {@link After} define which methods should be instrumented within that class or interface. 26 | */ 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @Target(ElementType.TYPE) 29 | public @interface Hook { 30 | 31 | /** 32 | * List of classes or interfaces to be instrumented. 33 | */ 34 | String[] instruments(); 35 | 36 | /** 37 | * If true, nested calls are skipped. 38 | * Nested means that the instrumented method calls another method that is instrumented with the same hook. 39 | * In most cases, you would only be interested in the outer call, so the default is {@code true}. 40 | * If set to {@code false}, nested calls will also be instrumented. 41 | * For nested calls, the same Hook instance is re-used. 42 | * For outer calls, a new Hook instance is created. 43 | */ 44 | boolean skipNestedCalls() default true; 45 | } 46 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/annotations/Thrown.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.annotations; 16 | 17 | import java.lang.annotation.ElementType; 18 | import java.lang.annotation.Retention; 19 | import java.lang.annotation.RetentionPolicy; 20 | import java.lang.annotation.Target; 21 | 22 | /** 23 | * A parameter annotated with @Thrown can be used in a Hook's @After method to capture an Exception thrown in an instrumented method. 24 | * 25 | * Example: In order to instrument the following method: 26 | * 27 | *

28 |  *     int div(int a, int b) {
29 |  *         return a / b;
30 |  *     }
31 |  * 
32 | * 33 | * A Hook could use an @After method like this: 34 | * 35 | *
36 |  *    {@literal @}After(method = "div")
37 |  *     void after(int a, int b, @Returned int result, @Thrown Throwable exception) {...}
38 |  * 
39 | * 40 | * In case everything goes well, the {@code result} will be the return value of {@code div()}, and {@code excption} will be {@code null}. 41 | * If {@code b} is {@code 0}, the {@code result} will be {@code 0}, and {@code exception} will be an {@link ArithmeticException} (division by zero). 42 | *

43 | * The parameter annotated with @Thrown is optional, if the hook does not use the exception, the parameter can be omitted. 44 | */ 45 | @Retention(RetentionPolicy.RUNTIME) 46 | @Target(ElementType.PARAMETER) 47 | public @interface Thrown {} 48 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/hookcontext/MetricDef.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.hookcontext; 16 | 17 | import io.prometheus.client.Collector; 18 | import io.prometheus.client.CollectorRegistry; 19 | 20 | import java.util.function.BiFunction; 21 | 22 | /** 23 | * See {@link #MetricDef(String, BiFunction)} 24 | */ 25 | public class MetricDef { 26 | 27 | private final String metricName; 28 | private final BiFunction producer; 29 | 30 | /** 31 | * See {@link MetricsStore}. 32 | * @param metricName Name of the Prometheus metric, like "http_requests_total". 33 | * @param producer Function to create a new metric and register it with the registry. 34 | * This function will only be called once, subsequent calls to {@link MetricsStore#createOrGet(MetricDef)} 35 | * will return the previously created metric with the specified name. 36 | * The two parameters are the metricName as specified above, 37 | * and the Prometheus registry where the new metric should be registered. 38 | * For an example see JavaDoc for {@link MetricsStore}. 39 | */ 40 | public MetricDef(String metricName, BiFunction producer) { 41 | this.metricName = metricName; 42 | this.producer = producer; 43 | } 44 | 45 | String getMetricName() { 46 | return metricName; 47 | } 48 | 49 | BiFunction getProducer() { 50 | return producer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /promagent-example/src/main/java/io/promagent/collectors/JmxCollector.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.collectors; 16 | 17 | import com.sun.management.OperatingSystemMXBean; 18 | import io.prometheus.client.Collector; 19 | 20 | import java.lang.management.ManagementFactory; 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import java.util.List; 25 | 26 | // TODO -- the following is an experiment supporting collectors directly (in addition to hooks) 27 | // TODO -- This class is not loaded by default, see commented-out lines in io.promagent.internal.Promagent.premain() 28 | // See JmxCollector in jmx_exporter 29 | public class JmxCollector extends Collector implements Collector.Describable { 30 | 31 | @Override 32 | public List collect() { 33 | List result = new ArrayList<>(); 34 | result.add(collectOperatingSystemMetrics()); 35 | return Collections.unmodifiableList(result); 36 | } 37 | 38 | @Override 39 | public List describe() { 40 | return new ArrayList<>(); 41 | } 42 | 43 | private MetricFamilySamples collectOperatingSystemMetrics() { 44 | OperatingSystemMXBean operatingSystemMXBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); 45 | MetricFamilySamples.Sample cpuLoad = new MetricFamilySamples.Sample("process_cpu_load", new ArrayList<>(), new ArrayList<>(), operatingSystemMXBean.getProcessCpuLoad()); 46 | return new MetricFamilySamples(cpuLoad.name, Type.GAUGE, "recent cpu usage for the whole system", Arrays.asList(cpuLoad)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /promagent-framework/promagent-api/src/main/java/io/promagent/hookcontext/MetricsStore.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.hookcontext; 16 | 17 | import io.prometheus.client.Collector; 18 | import io.prometheus.client.CollectorRegistry; 19 | 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.ConcurrentMap; 22 | import java.util.function.BiFunction; 23 | 24 | /** 25 | * Instead of creating Prometheus Metrics directly, Hooks should use the {@link MetricsStore} like this: 26 | *

27 |  * Counter httpRequestsTotal = metricsStore.createOrGet(new MetricDef<>(
28 |  *         "http_requests_total",
29 |  *         (name, registry) -> Counter.build()
30 |  *                 .name(name)
31 |  *                 .labelNames("method", "path", "status")
32 |  *                 .help("Total number of http requests.")
33 |  *                 .register(registry)
34 |  * ));
35 |  * 
36 | * The Promgent framework will take care that each metric is created only once and re-used across re-deployments in an application server. 37 | */ 38 | public class MetricsStore { 39 | 40 | private final CollectorRegistry registry; 41 | private final ConcurrentMap metrics = new ConcurrentHashMap<>(); 42 | 43 | public MetricsStore(CollectorRegistry registry) { 44 | this.registry = registry; 45 | } 46 | 47 | /** 48 | * See {@link MetricsStore} and {@link MetricDef#MetricDef(String, BiFunction)}. 49 | */ 50 | @SuppressWarnings("unchecked") 51 | public T createOrGet(MetricDef metricDef) { 52 | return (T) metrics.computeIfAbsent(metricDef.getMetricName(), s -> metricDef.getProducer().apply(metricDef.getMetricName(), registry)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | 10 | promagent-internal 11 | promagent-internal classes shared across deployments 12 | 13 | jar 14 | 15 | 16 | 17 | 18 | ${project.groupId} 19 | promagent-agent 20 | ${project.version} 21 | provided 22 | 23 | 24 | 25 | ${project.groupId} 26 | promagent-api 27 | ${project.version} 28 | provided 29 | 30 | 31 | 32 | net.bytebuddy 33 | byte-buddy 34 | 1.9.4 35 | 36 | 37 | 38 | commons-io 39 | commons-io 40 | 2.6 41 | 42 | 43 | 44 | io.prometheus 45 | simpleclient_common 46 | 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter-api 51 | test 52 | 53 | 54 | 55 | org.mockito 56 | mockito-core 57 | 2.23.0 58 | test 59 | 60 | 61 | 62 | javax.servlet 63 | javax.servlet-api 64 | 4.0.1 65 | test 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | 10 | promagent-maven-plugin 11 | promagent-maven-plugin 12 | 13 | maven-plugin 14 | 15 | 16 | 17 | 18 | io.promagent 19 | promagent-internal 20 | 1.0-SNAPSHOT 21 | runtime 22 | 23 | 24 | 25 | io.promagent 26 | promagent-agent 27 | 1.0-SNAPSHOT 28 | runtime 29 | 30 | 31 | 32 | io.promagent 33 | promagent-api 34 | 1.0-SNAPSHOT 35 | runtime 36 | 37 | 38 | 39 | org.apache.maven 40 | maven-plugin-api 41 | 3.6.0 42 | provided 43 | 44 | 45 | 46 | org.apache.maven 47 | maven-core 48 | 3.6.0 49 | provided 50 | 51 | 52 | 53 | org.apache.maven.plugin-tools 54 | maven-plugin-annotations 55 | 3.6.0 56 | provided 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-plugin-plugin 66 | 3.5 67 | 68 | promagent 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/src/main/java/io/promagent/agent/PerDeploymentClassLoader.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.agent; 16 | 17 | import java.io.InputStream; 18 | import java.net.URL; 19 | import java.net.URLClassLoader; 20 | 21 | /** 22 | * Class loader for loading Hooks (like ServletHook or JdbcHook). 23 | *

24 | * There is one instance per deployment in an application server, because hook classes may 25 | * reference classes from the deployment, e.g. as parameters to the before() and after() methods. 26 | *

27 | * However, loading shared classes like the Prometheus client library is delegated to the {@link #sharedClassLoader}, 28 | * because the Prometheus metric registry should be accessible across all deployments within an application server. 29 | */ 30 | class PerDeploymentClassLoader extends URLClassLoader { 31 | 32 | private final URLClassLoader sharedClassLoader; // for loading the Prometheus client library 33 | 34 | PerDeploymentClassLoader(URL[] perDeploymentJars, URLClassLoader sharedClassLoader, ClassLoader parent) { 35 | super(perDeploymentJars, parent); 36 | this.sharedClassLoader = sharedClassLoader; 37 | } 38 | 39 | @Override 40 | public Class loadClass(String name) throws ClassNotFoundException { 41 | try { 42 | return sharedClassLoader.loadClass(name); // The Prometheus client library should all have the same initiating loader across deployments. 43 | } catch (ClassNotFoundException e) { 44 | return super.loadClass(name); // Hooks should have different initiating loaders if the context loader differs. 45 | } 46 | } 47 | 48 | // Called by Byte buddy to load the PromagentAdvice. 49 | @Override 50 | public InputStream getResourceAsStream(String name) { 51 | InputStream result = sharedClassLoader.getResourceAsStream(name); 52 | if (result == null) { 53 | result = super.getResourceAsStream(name); 54 | } 55 | return result; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/MethodCallCounter.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | 5 | import java.util.*; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * Track method calls, used in {@link ParameterTypesTest} 10 | */ 11 | public class MethodCallCounter { 12 | 13 | public static void reset() { 14 | captures.clear(); 15 | } 16 | 17 | public static void observe(Object hook, String methodName, Object... args) { 18 | captures.add(new Capture(hook, methodName, args)); 19 | } 20 | 21 | public static void assertNumCalls(int expectedNumberOfCalls, Class hookClass, String hookMethod, Object... expectedArgs) { 22 | List matching = captures.stream() 23 | .filter(c -> c.hook.getClass().equals(hookClass)) 24 | .filter(c -> c.hookMethod.equals(hookMethod)) 25 | .filter(c -> Arrays.equals(expectedArgs, c.args)) 26 | .collect(Collectors.toList()); 27 | Assertions.assertEquals(expectedNumberOfCalls, matching.size()); 28 | } 29 | 30 | // special case for the varargsMixed test 31 | public static void assertNumCalls(int expectedNumberOfCalls, Class hookClass, String hookMethod, String firstString, String... moreStrings) { 32 | List matching = captures.stream() 33 | .filter(c -> c.hook.getClass().equals(hookClass)) 34 | .filter(c -> c.hookMethod.equals(hookMethod)) 35 | .filter(c -> c.args.length == 2) 36 | .filter(c -> Objects.equals(firstString, c.args[0])) 37 | .filter(c -> Arrays.equals(moreStrings, (String[]) c.args[1])) 38 | .collect(Collectors.toList()); 39 | Assertions.assertEquals(expectedNumberOfCalls, matching.size()); 40 | } 41 | 42 | public static void assertNumHookInstances(int expectedNumberOfInstances, Class hookClass) { 43 | long actualNumberOfInstances = captures.stream() 44 | .filter(c -> c.hook.getClass().equals(hookClass)) 45 | .map(c -> c.hook) 46 | .distinct() 47 | .count(); 48 | Assertions.assertEquals(expectedNumberOfInstances, (int) actualNumberOfInstances); 49 | } 50 | 51 | private static class Capture { 52 | final Object hook; 53 | final String hookMethod; 54 | final Object[] args; 55 | 56 | Capture(Object hook, String hookMethod, Object[] args) { 57 | this.hook = hook; 58 | this.hookMethod = hookMethod; 59 | this.args = args; 60 | } 61 | } 62 | 63 | private static final List captures = Collections.synchronizedList(new ArrayList<>()); 64 | 65 | private MethodCallCounter() {} 66 | } 67 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/src/test/java/io/promagent/agent/JarFilesTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.agent; 16 | 17 | import org.junit.jupiter.api.Test; 18 | 19 | import static io.promagent.agent.JarFiles.findAgentJarFromCmdline; 20 | import static java.util.Arrays.asList; 21 | import static org.junit.jupiter.api.Assertions.*; 22 | 23 | class JarFilesTest { 24 | 25 | @Test 26 | void testCmdlineParserWildfly() { 27 | // The command line arguments are taken from the Wildfly application server example. 28 | String[] cmdlineArgs = new String[]{ 29 | "-D[Standalone]", 30 | "-Xbootclasspath/p:/tmp/wildfly-10.1.0.Final/modules/system/layers/base/org/jboss/logmanager/main/jboss-logmanager-2.0.4.Final.jar", 31 | "-Djboss.modules.system.pkgs=org.jboss.logmanager,io.promagent.agent", 32 | "-Djava.util.logging.manager=org.jboss.logmanager.LogManager", 33 | "-javaagent:../promagent/promagent-dist/target/promagent.jar=port=9300", 34 | "-Dorg.jboss.boot.log.file=/tmp/wildfly-10.1.0.Final/standalone/log/server.log", 35 | "-Dlogging.configuration=file:/tmp/wildfly-10.1.0.Final/standalone/configuration/logging.properties" 36 | }; 37 | assertEquals("../promagent/promagent-dist/target/promagent.jar", findAgentJarFromCmdline(asList(cmdlineArgs)).toString()); 38 | } 39 | 40 | @Test 41 | void testCmdlineParserVersioned() { 42 | String[] cmdlineArgs = new String[] { 43 | "-javaagent:promagent-1.0-SNAPSHOT.jar" 44 | }; 45 | assertEquals("promagent-1.0-SNAPSHOT.jar", findAgentJarFromCmdline(asList(cmdlineArgs)).toString()); 46 | } 47 | 48 | @Test() 49 | void testCmdlineParserFailed() { 50 | String[] cmdlineArgs = new String[] { 51 | "-javaagent:/some/other/agent.jar", 52 | "-jar", 53 | "promagent.jar" 54 | }; 55 | Exception e = assertThrows(Exception.class, () -> findAgentJarFromCmdline(asList(cmdlineArgs))); 56 | // The exception should contain some message indicating promagent.jar was not found. 57 | assertTrue(e.getMessage().contains("promagent.jar")); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/src/main/java/io/promagent/plugin/ManifestTransformer.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.plugin; 16 | 17 | import org.apache.maven.plugin.MojoExecutionException; 18 | import org.apache.maven.plugin.descriptor.PluginDescriptor; 19 | 20 | import java.io.ByteArrayInputStream; 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.util.jar.Attributes; 25 | import java.util.jar.JarEntry; 26 | import java.util.jar.Manifest; 27 | 28 | class ManifestTransformer { 29 | 30 | private static final Attributes.Name PREMAIN_CLASS = new Attributes.Name("Premain-Class"); 31 | private static final Attributes.Name CREATED_BY = new Attributes.Name("Created-By"); 32 | 33 | private final String pluginArtifactId; 34 | private final String pluginVersion; 35 | 36 | ManifestTransformer(PluginDescriptor pluginDescriptor) { 37 | pluginArtifactId = pluginDescriptor.getArtifactId(); 38 | pluginVersion = pluginDescriptor.getVersion(); 39 | } 40 | 41 | boolean canTransform(JarEntry jarEntry) { 42 | return "META-INF/MANIFEST.MF".equals(jarEntry.getName()); 43 | } 44 | 45 | InputStream transform(InputStream inputStream) throws MojoExecutionException { 46 | try (InputStream in = inputStream) { // No need for new variable in Java 9. 47 | Manifest manifest = new Manifest(in); 48 | Attributes attributes = manifest.getMainAttributes(); 49 | if (!attributes.containsKey(PREMAIN_CLASS)) { 50 | throw new MojoExecutionException(PREMAIN_CLASS + " not found in MANIFEST.MF. This is a bug in promagent-maven-plugin."); 51 | } 52 | attributes.put(CREATED_BY, pluginArtifactId + ":" + pluginVersion); 53 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 54 | manifest.write(out); 55 | return new ByteArrayInputStream(out.toByteArray()); 56 | } catch (IOException e) { 57 | throw new MojoExecutionException("Failed to transform MANIFEST.MF: " + e.getMessage(), e); 58 | } 59 | } 60 | 61 | JarEntry transform(JarEntry entry) { 62 | return new JarEntry(entry.getName()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/src/main/java/io/promagent/plugin/JarFileNames.java: -------------------------------------------------------------------------------- 1 | package io.promagent.plugin; 2 | 3 | import org.apache.maven.plugin.MojoExecutionException; 4 | import org.apache.maven.project.MavenProject; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | 11 | public class JarFileNames { 12 | 13 | private final Path finalName; // The agent JAR file. 14 | private final Path defaultFinalName; // The hook implementation's JAR within the agent JAR. 15 | private final Path nameAfterMove; // The hook implementation's JAR in the target/ directory. 16 | 17 | private JarFileNames(Path finalName, Path defaultFinalName, Path nameAfterMove) { 18 | this.finalName = finalName; 19 | this.defaultFinalName = defaultFinalName; 20 | this.nameAfterMove = nameAfterMove; 21 | } 22 | 23 | public Path getFinalName() { 24 | return finalName; 25 | } 26 | 27 | public Path getDefaultFinalName() { 28 | return defaultFinalName; 29 | } 30 | 31 | public Path getNameAfterMove() { 32 | return nameAfterMove; 33 | } 34 | 35 | public static JarFileNames renameOrigJarFile(MavenProject project) throws MojoExecutionException { 36 | Path finalName = makePath(project.getBuild().getFinalName(), project); 37 | if (!Files.exists(finalName)) { 38 | throw new MojoExecutionException(finalName.getFileName() + ": file not found. This happens if promagent-maven-plugin is called before the original artifact was created."); 39 | } 40 | Path defaultFinalName = makePath(project.getArtifactId() + "-" + project.getArtifact().getVersion(), project); 41 | if (!Files.exists(defaultFinalName)) { 42 | mv(finalName, defaultFinalName); 43 | return new JarFileNames(finalName, defaultFinalName, defaultFinalName); 44 | } else { 45 | Path defaultFinalNameWithSuffix = makePath(project.getArtifactId() + "-" + project.getArtifact().getVersion() + "-orig", project); 46 | mv(finalName, defaultFinalNameWithSuffix); 47 | return new JarFileNames(finalName, defaultFinalName, defaultFinalNameWithSuffix); 48 | } 49 | } 50 | 51 | private static Path makePath(String fileNameWithoutExtension, MavenProject project) { 52 | String extension = project.getArtifact().getArtifactHandler().getExtension(); // "jar" 53 | return Paths.get(project.getBuild().getDirectory(), fileNameWithoutExtension + "." + extension); 54 | } 55 | 56 | private static void mv(Path src, Path dest) throws MojoExecutionException { 57 | try { 58 | Files.move(src, dest); 59 | } catch (IOException e) { 60 | throw new MojoExecutionException("Failed to rename " + src.getFileName() + ": " + e.getMessage(), e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /promagent-example/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.promagent 6 | promagent-example 7 | 1.0-SNAPSHOT 8 | 9 | promagent example 10 | 11 | jar 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | UTF-8 17 | 1.8 18 | 1.8 19 | 1.0-SNAPSHOT 20 | 21 | 22 | 23 | 24 | 25 | io.promagent 26 | promagent-api 27 | ${promagent.framework.version} 28 | provided 29 | 30 | 31 | 32 | 33 | javax.servlet 34 | javax.servlet-api 35 | 4.0.1 36 | provided 37 | 38 | 39 | 40 | 41 | org.junit.jupiter 42 | junit-jupiter-api 43 | 5.3.1 44 | test 45 | 46 | 47 | 48 | com.squareup.okhttp3 49 | okhttp 50 | 3.11.0 51 | test 52 | 53 | 54 | 55 | 56 | 57 | promagent 58 | 59 | 60 | io.promagent 61 | promagent-maven-plugin 62 | ${promagent.framework.version} 63 | 64 | 65 | promagent 66 | package 67 | 68 | build 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/StaticFinalTest.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests; 2 | 3 | import static io.promagent.internal.Promagent.getInstruments; 4 | 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Modifier; 7 | import java.lang.reflect.Type; 8 | import java.util.Map; 9 | import java.util.Set; 10 | import java.util.SortedSet; 11 | 12 | import io.promagent.agent.ClassLoaderCache; 13 | import io.promagent.hookcontext.MetricsStore; 14 | import io.promagent.internal.Delegator; 15 | import io.promagent.internal.HookMetadata; 16 | import io.promagent.internal.Promagent; 17 | import io.promagent.internal.PromagentAdvice; 18 | import io.promagent.internal.instrumentationtests.classes.StaticFinalExample; 19 | import io.promagent.internal.instrumentationtests.hooks.StaticFinalTestHook; 20 | import net.bytebuddy.ByteBuddy; 21 | import net.bytebuddy.asm.Advice; 22 | import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | 26 | class StaticFinalTest { 27 | 28 | private Object example; 29 | 30 | @BeforeEach 31 | void setUp() throws Exception { 32 | SortedSet hookMetadata = Util.loadHookMetadata(StaticFinalTestHook.class); 33 | ClassLoaderCache classLoaderCache = Util.mockClassLoaderCache(); 34 | 35 | MetricsStore metricsStore = Util.mockMetricsStore(); 36 | Delegator.init(hookMetadata, metricsStore, classLoaderCache); 37 | MethodCallCounter.reset(); 38 | 39 | Map> instruments = getInstruments(hookMetadata); 40 | Set instrumentedMethods = instruments.get(StaticFinalExample.class.getName()); 41 | example = new ByteBuddy() 42 | .redefine(StaticFinalExample.class) 43 | .visit(Advice.to(PromagentAdvice.class).on(Promagent.matchAnyMethodIn(instrumentedMethods))) 44 | .make() 45 | .load(this.getClass().getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST) 46 | .getLoaded() 47 | .newInstance(); 48 | } 49 | 50 | @Test 51 | void testPublicStaticMethod() throws Exception { 52 | int expectedTotalHookCalls = 0; 53 | for (String methodName : new String[] { 54 | "helloPublic", 55 | "helloPublicFinal", 56 | "helloPublicStatic", 57 | "helloPublicStaticFinal" 58 | }) { 59 | Method method = example.getClass().getMethod(methodName, String.class); 60 | method.invoke(example, "world"); 61 | expectedTotalHookCalls++; 62 | MethodCallCounter.assertNumCalls(expectedTotalHookCalls, StaticFinalTestHook.class, "before", new Object[]{"world"}); 63 | MethodCallCounter.assertNumCalls(expectedTotalHookCalls, StaticFinalTestHook.class, "after", new Object[]{"world"}); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/src/main/java/io/promagent/plugin/PromagentMojo.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.plugin; 16 | 17 | import org.apache.maven.artifact.Artifact; 18 | import org.apache.maven.plugin.AbstractMojo; 19 | import org.apache.maven.plugin.MojoExecutionException; 20 | import org.apache.maven.plugin.descriptor.PluginDescriptor; 21 | import org.apache.maven.plugins.annotations.Component; 22 | import org.apache.maven.plugins.annotations.LifecyclePhase; 23 | import org.apache.maven.plugins.annotations.Mojo; 24 | import org.apache.maven.plugins.annotations.ResolutionScope; 25 | import org.apache.maven.project.MavenProject; 26 | 27 | import static io.promagent.plugin.AgentJar.Directory.PER_DEPLOYMENT_JARS; 28 | import static io.promagent.plugin.AgentJar.Directory.SHARED_JARS; 29 | 30 | @Mojo(name = "build", aggregator = true, defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.RUNTIME) 31 | public class PromagentMojo extends AbstractMojo { 32 | 33 | @Component 34 | private MavenProject project; 35 | 36 | @Component 37 | private PluginDescriptor pluginDescriptor; 38 | 39 | @Override 40 | public void execute() throws MojoExecutionException { 41 | 42 | JarFileNames jarFileNames = JarFileNames.renameOrigJarFile(project); 43 | AgentDependencies agentDependencies = AgentDependencies.init(pluginDescriptor); 44 | 45 | try (AgentJar agentJar = AgentJar.create(jarFileNames.getFinalName().toFile())) { 46 | // Add extracted agent classes 47 | agentJar.extractJar(agentDependencies.getAgentArtifact().getFile(), new ManifestTransformer(pluginDescriptor)); 48 | // Add project jar 49 | agentDependencies.assertNoConflict(project.getArtifact()); 50 | agentJar.addFile(jarFileNames.getNameAfterMove().toFile(), jarFileNames.getDefaultFinalName().getFileName().toString(), PER_DEPLOYMENT_JARS); 51 | // Add project dependencies 52 | for (Artifact artifact : project.getArtifacts()) { 53 | agentDependencies.assertNoConflict(artifact); 54 | agentJar.addFile(artifact.getFile(), SHARED_JARS); 55 | } 56 | // Add agent internal jars 57 | for (Artifact artifact : agentDependencies.getDependencies()) { 58 | agentJar.addFile(artifact.getFile(), SHARED_JARS); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/jmx/PromagentCollectorRegistry.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.jmx; 16 | 17 | import io.prometheus.client.Collector; 18 | import io.prometheus.client.CollectorRegistry; 19 | import io.prometheus.client.SimpleCollector; 20 | 21 | import javax.management.MalformedObjectNameException; 22 | import javax.management.ObjectName; 23 | import java.lang.management.ManagementFactory; 24 | import java.lang.reflect.Field; 25 | 26 | /** 27 | * This is like the regular {@link CollectorRegistry}, except that when you {@link #register(Collector)} a metric, 28 | * the metric will also be registered as an MBean in the JMX platform server. 29 | */ 30 | public class PromagentCollectorRegistry extends CollectorRegistry { 31 | 32 | @Override 33 | public void register(Collector metric) { 34 | super.register(metric); 35 | try { 36 | ManagementFactory.getPlatformMBeanServer().registerMBean(new Metric(metric), makeObjectName((SimpleCollector) metric)); 37 | } catch (Exception e) { 38 | throw new RuntimeException("Failed to register Prometheus metric: " + e.getMessage(), e); 39 | } 40 | } 41 | 42 | private static ObjectName makeObjectName(SimpleCollector metric) throws MalformedObjectNameException { 43 | return makeObjectName(getFullName(metric)); 44 | } 45 | 46 | private static ObjectName makeObjectName(String fullname) throws MalformedObjectNameException { 47 | return new ObjectName("io.promagent:type=metrics,name=" + fullname); 48 | } 49 | 50 | private static String getFullName(SimpleCollector metric) { 51 | // Unfortunately, there is no public API to get the 'fullname' of a metric. We use reflection to get it anyway. 52 | try { 53 | Field field = SimpleCollector.class.getDeclaredField("fullname"); 54 | field.setAccessible(true); 55 | return (String) field.get(metric); 56 | } catch (IllegalAccessException | NoSuchFieldException e) { 57 | throw new RuntimeException("Failed to access " + metric.getClass().getName() + ".fullname. " + 58 | "This is probably because the internal implementation of the Prometheus client library has changed. " + 59 | "You should adapt the Promagent accordingly.", e); 60 | } 61 | } 62 | 63 | public void registerNoJmx(Collector collector) { 64 | super.register(collector); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/ReturnedAndThrownHook.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.*; 4 | import io.promagent.hookcontext.MetricsStore; 5 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 6 | import io.promagent.internal.instrumentationtests.classes.Fruit; 7 | 8 | import java.util.List; 9 | 10 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ReturnedAndThrownExample") 11 | public class ReturnedAndThrownHook { 12 | 13 | public ReturnedAndThrownHook(MetricsStore m) {} 14 | 15 | @Before(method = "returnVoid") 16 | public void beforeVoid(Fruit f) { 17 | MethodCallCounter.observe(this, "beforeVoid", f); 18 | } 19 | 20 | @Before(method = "returnVoid") 21 | public void afterVoid(Fruit f) { 22 | MethodCallCounter.observe(this, "afterVoid", f); 23 | } 24 | 25 | @Before(method = "returnPrimitive") 26 | public void beforePrimitive(Fruit.Orange o) { 27 | MethodCallCounter.observe(this, "beforePrimitive", o); 28 | } 29 | 30 | @After(method = "returnPrimitive") 31 | public void afterPrimitive(Fruit.Orange o, @Returned int i) { 32 | MethodCallCounter.observe(this, "afterPrimitive", o, i); 33 | } 34 | 35 | @Before(method = "returnObject") 36 | public void beforeObject() { 37 | MethodCallCounter.observe(this, "beforeObject"); 38 | } 39 | 40 | @After(method = "returnObject") 41 | public void afterObject(@Returned Fruit f) { 42 | MethodCallCounter.observe(this, "afterObject", f); 43 | } 44 | 45 | @Before(method = "returnArray") 46 | public void beforeArray(int... params) { 47 | MethodCallCounter.observe(this, "beforeArray", new Object[]{params}); 48 | } 49 | 50 | @After(method = "returnArray") 51 | public void afterArray(@Returned int[] ret, int... params) { 52 | MethodCallCounter.observe(this, "afterArray", ret, params); 53 | } 54 | 55 | @Before(method = "returnGenerics") 56 | public void beforeGenerics(T fruit) { 57 | MethodCallCounter.observe(this, "beforeGenerics", fruit); 58 | } 59 | 60 | @After(method = "returnGenerics") 61 | public void afterGenerics(T fruit, @Returned List ret) { 62 | MethodCallCounter.observe(this, "afterGenerics", fruit, ret); 63 | } 64 | 65 | @Before(method = "throwsRuntimeException") 66 | public void beforeThrowsRuntimeException(int a, Fruit.Orange o) { 67 | MethodCallCounter.observe(this, "beforeThrowsRuntimeException", a, o); 68 | } 69 | 70 | @After(method = "throwsRuntimeException") 71 | public void afterThrowsRuntimeException(int a, Fruit.Orange o, @Returned String ret, @Thrown Throwable e) { 72 | MethodCallCounter.observe(this, "afterThrowsRuntimeException", a, o, ret, e); 73 | } 74 | 75 | @Before(method = "throwsCheckedException") 76 | public void beforeThrowsCheckedException() { 77 | MethodCallCounter.observe(this, "beforeThrowsCheckedException"); 78 | } 79 | 80 | @After(method = "throwsCheckedException") 81 | public void afterThrowsCheckedException(@Returned int ret, @Thrown Throwable e) { 82 | MethodCallCounter.observe(this, "afterThrowsCheckedException", ret, e); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /promagent-framework/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.promagent 6 | promagent-framework 7 | 1.0-SNAPSHOT 8 | 9 | pom 10 | promagent framework 11 | 12 | 13 | 14 | Apache License, Version 2.0 15 | https://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | UTF-8 21 | UTF-8 22 | 1.8 23 | 1.8 24 | 5.3.1 25 | 26 | 27 | 28 | 29 | 30 | promagent-loader 31 | 32 | promagent-agent 33 | promagent-api 34 | promagent-internal 35 | promagent-exporter 36 | promagent-loader 37 | promagent-maven-plugin 38 | 39 | 40 | 41 | 42 | default 43 | 44 | true 45 | 46 | 47 | promagent-agent 48 | promagent-api 49 | promagent-internal 50 | promagent-exporter 51 | promagent-maven-plugin 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | io.prometheus 61 | simpleclient_common 62 | 0.5.0 63 | 64 | 65 | org.junit.jupiter 66 | junit-jupiter-api 67 | ${junit5.version} 68 | test 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.apache.maven.plugins 78 | maven-surefire-plugin 79 | 80 | 2.22.1 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /promagent-framework/promagent-loader/src/main/java/io/promagent/loader/PromagentLoader.java: -------------------------------------------------------------------------------- 1 | package io.promagent.loader; 2 | 3 | import com.sun.tools.attach.AgentLoadException; 4 | import com.sun.tools.attach.VirtualMachine; 5 | import com.sun.tools.attach.VirtualMachineDescriptor; 6 | 7 | public class PromagentLoader { 8 | 9 | public static void main(String[] args) throws Exception { 10 | int pid = getIntArg(args, "-pid"); 11 | int port = getIntArg(args, "-port"); 12 | String agentJar = getStringArg(args, "-agent"); 13 | PromagentLoader.loadPromagent(agentJar, pid, port); 14 | } 15 | 16 | private static void loadPromagent(String agentJar, int pid, int port) throws Exception { 17 | VirtualMachineDescriptor vmd = findVirtualMachine(Integer.toString(pid)); 18 | if (vmd == null) { 19 | System.err.println("No Java process found with PID " + pid); 20 | System.exit(-1); 21 | } 22 | VirtualMachine vm = null; 23 | try { 24 | vm = VirtualMachine.attach(vmd); 25 | vm.loadAgent(agentJar, "port=" + port); 26 | } catch (AgentLoadException e) { 27 | System.err.println("Failed to attach agent: " + getMessage(e)); 28 | } finally { 29 | if (vm != null) { 30 | vm.detach(); 31 | } 32 | } 33 | } 34 | 35 | private static VirtualMachineDescriptor findVirtualMachine(String pid) { 36 | for (VirtualMachineDescriptor vmd : VirtualMachine.list()) { 37 | if (vmd.id().equalsIgnoreCase(pid)) { 38 | return vmd; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | private static String getMessage(AgentLoadException e) { 45 | switch (e.getMessage()) { 46 | case "-4": 47 | return "Insuffient memory"; 48 | case "100": 49 | return "Agent JAR not found or no Agent-Class attribute"; 50 | case "101": 51 | return "Unable to add JAR file to system class path"; 52 | case "102": 53 | return "Agent JAR loaded but agent failed to initialize"; 54 | default: 55 | return e.getMessage(); 56 | } 57 | } 58 | 59 | private static int getIntArg(String[] args, String option) { 60 | String stringArg = getStringArg(args, option); 61 | try { 62 | return Integer.parseInt(stringArg); 63 | } catch (NumberFormatException e) { 64 | System.err.println(option + " " + stringArg + ": invalid argument"); 65 | System.exit(-1); 66 | return 0; // will never happen 67 | } 68 | } 69 | 70 | private static String getStringArg(String[] args, String option) { 71 | for (int pos : new int[]{0, 2, 4}) { 72 | if (args.length < pos + 2) { 73 | printUsageAndExit(); 74 | } 75 | if (option.equals(args[pos])) { 76 | return args[pos+1]; 77 | } 78 | } 79 | printUsageAndExit(); 80 | return null; // will never happen 81 | } 82 | 83 | private static void printUsageAndExit() { 84 | System.err.println("Usage: java -cp $JAVA_HOME/lib/tools.jar:/path/to/promagent-loader.jar io.promagent.loader.PromagentLoader -agent /path/to/promagent.jar -port 9300 -pid "); 85 | System.exit(-1); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/BuiltInServer.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import com.sun.net.httpserver.HttpExchange; 18 | import com.sun.net.httpserver.HttpServer; 19 | import io.prometheus.client.CollectorRegistry; 20 | import io.prometheus.client.exporter.common.TextFormat; 21 | 22 | import java.io.IOException; 23 | import java.io.StringWriter; 24 | import java.net.InetSocketAddress; 25 | import java.util.Collections; 26 | 27 | /** 28 | * Use the Java runtime's built-in {@link HttpServer} to export Prometheus metrics. 29 | */ 30 | class BuiltInServer { 31 | 32 | static void run(String host, String portString, CollectorRegistry registry) throws Exception { 33 | try { 34 | int port = Integer.parseInt(portString); 35 | InetSocketAddress address = host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port); 36 | HttpServer httpServer = HttpServer.create(address, 10); 37 | httpServer.createContext("/", httpExchange -> { 38 | if ("/metrics".equals(httpExchange.getRequestURI().getPath())) { 39 | respondMetrics(registry, httpExchange); 40 | } else { 41 | respondRedirect(httpExchange); 42 | } 43 | }); 44 | httpServer.start(); 45 | } catch (NumberFormatException e) { 46 | throw new RuntimeException("Failed to parse command line arguments: '" + portString + "' is not a valid port number."); 47 | } 48 | } 49 | 50 | private static void respondMetrics(CollectorRegistry registry, HttpExchange httpExchange) throws IOException { 51 | StringWriter respBodyWriter = new StringWriter(); 52 | respBodyWriter.write("# Metrics will become visible when they are updated for the first time.\n"); 53 | TextFormat.write004(respBodyWriter, registry.metricFamilySamples()); 54 | byte[] respBody = respBodyWriter.toString().getBytes("UTF-8"); 55 | httpExchange.getResponseHeaders().put("Context-Type", Collections.singletonList("text/plain; charset=UTF-8")); 56 | httpExchange.sendResponseHeaders(200, respBody.length); 57 | httpExchange.getResponseBody().write(respBody); 58 | httpExchange.getResponseBody().close(); 59 | } 60 | 61 | private static void respondRedirect(HttpExchange httpExchange) throws IOException { 62 | byte[] respBody = "Metrics are provided on the /metrics endpoint.".getBytes("UTF-8"); 63 | httpExchange.getResponseHeaders().add("Location", "/metrics"); 64 | httpExchange.getResponseHeaders().put("Context-Type", Collections.singletonList("text/plain; charset=UTF-8")); 65 | httpExchange.sendResponseHeaders(302, respBody.length); 66 | httpExchange.getResponseBody().write(respBody); 67 | httpExchange.getResponseBody().close(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/src/main/java/io/promagent/agent/ClassLoaderCache.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.agent; 16 | 17 | import java.net.MalformedURLException; 18 | import java.net.URL; 19 | import java.net.URLClassLoader; 20 | import java.nio.file.Path; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * ClassLoaderCache stores the class loaders used for loading the Promagent modules, 27 | * i.e. promagent-hooks, promagent-annotations, promagent-internals, and their dependencies. 28 | *

29 | * For the Hooks (like ServletHook or JdbcHook) there is one class loader per deployment, 30 | * because hook classes may reference classes from the deployment, 31 | * e.g. as parameters to the before() and after() methods. 32 | * All other modules and their dependencies are loaded through a shared class loader. 33 | *

34 | * When {@link #currentClassLoader()} is called for the first time within a class loader context, 35 | * a new {@link PerDeploymentClassLoader} is created on the fly. 36 | * Repeated calls in the same context yield the same {@link PerDeploymentClassLoader}. 37 | */ 38 | public class ClassLoaderCache { 39 | 40 | private static ClassLoaderCache instance; 41 | 42 | // TODO: The cache does not free class loaders when applications are undeployed. Maybe use WeakHashMap? 43 | private final Map cache = new HashMap<>(); 44 | private final URLClassLoader sharedClassLoader; // shared across multiple deployments 45 | private final List perDeploymentJars; // one class loader for each deployment for these JARs 46 | 47 | private ClassLoaderCache(JarFiles jarFiles) { 48 | sharedClassLoader = new URLClassLoader(pathsToURLs(jarFiles.getSharedJars())); 49 | perDeploymentJars = jarFiles.getPerDeploymentJars(); 50 | } 51 | 52 | public static synchronized ClassLoaderCache getInstance() { 53 | if (instance == null) { 54 | instance = new ClassLoaderCache(JarFiles.extract()); 55 | } 56 | return instance; 57 | } 58 | 59 | public List getPerDeploymentJars() { 60 | return perDeploymentJars; 61 | } 62 | 63 | public synchronized ClassLoader currentClassLoader() { 64 | ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 65 | if (! cache.containsKey(contextClassLoader)) { 66 | cache.put(contextClassLoader, new PerDeploymentClassLoader(pathsToURLs(perDeploymentJars), sharedClassLoader, contextClassLoader)); 67 | } 68 | return cache.get(contextClassLoader); 69 | } 70 | 71 | private static URL[] pathsToURLs(List paths) { 72 | try { 73 | URL[] result = new URL[paths.size()]; 74 | for (int i=0; i` section in kitchensink's `pom.xml` to disable the `enforce-java-version` execution: 46 | 47 | ```xml 48 | 49 | 50 | maven-enforcer-plugin 51 | 52 | 53 | enforce-java-version 54 | none 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | 2. The JAXB APIs are no longer contained on the default class path in Java 9. Add it explicitly to the `` section in kitchensink's `pom.xml`: 62 | 63 | ```xml 64 | 65 | javax.xml.bind 66 | jaxb-api 67 | 2.3.0 68 | 69 | ``` 70 | 71 | Now `kitchensink` should build successfully with JDK9. 72 | 73 | ```bash 74 | mvn clean package 75 | cd ../.. 76 | ``` 77 | 78 | Download and extract the [Wildfly application server](http://wildfly.org/). 79 | 80 | ```bash 81 | curl -O http://download.jboss.org/wildfly/11.0.0.CR1/wildfly-11.0.0.CR1.tar.gz 82 | tar xfz wildfly-11.0.0.CR1.tar.gz 83 | cd wildfly-11.0.0.CR1 84 | ``` 85 | 86 | Run the Wildfly application server with the Promagent attached. 87 | 88 | ```bash 89 | LOGMANAGER_JAR=$(find $(pwd) -name 'jboss-logmanager-*.jar') 90 | export JAVA_OPTS=" 91 | -Xbootclasspath/a:${LOGMANAGER_JAR} 92 | -Dsun.util.logging.disableCallerCheck=true 93 | -Djboss.modules.system.pkgs=org.jboss.logmanager,io.promagent.agent 94 | -Djava.util.logging.manager=org.jboss.logmanager.LogManager 95 | -javaagent:../promagent/promagent-dist/target/promagent.jar=port=9300 96 | ${JAVA_OPTS} 97 | " 98 | ./bin/standalone.sh 99 | ``` 100 | 101 | In a new Shell window, deploy the quickstart application. 102 | 103 | ```bash 104 | cd wildfly-11.0.0.CR1 105 | ./bin/jboss-cli.sh --connect --command="deploy ../wildfly-11.0.0.CR1-quickstarts/kitchensink/target/kitchensink.war" 106 | ``` 107 | 108 | Go to [http://localhost:8080/kitchensink](http://localhost:8080/kitchensink) to view the quickstart application, 109 | go to [http://localhost:9300/metrics](http://localhost:9300/metrics) to view the Prometheus metrics. 110 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/PromagentAdvice.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import io.promagent.agent.ClassLoaderCache; 18 | import net.bytebuddy.implementation.bytecode.assign.Assigner; 19 | 20 | import java.lang.reflect.Method; 21 | import java.util.List; 22 | 23 | import static net.bytebuddy.asm.Advice.*; 24 | 25 | public class PromagentAdvice { 26 | 27 | // TODO: 28 | // Should we move this class into it's own maven module 29 | // to make clear that it cannot reference other classes from promagent-internal? 30 | 31 | @OnMethodEnter 32 | @SuppressWarnings("unchecked") 33 | public static List before( 34 | @This(optional = true) Object that, 35 | @Origin Method method, 36 | @AllArguments Object[] args 37 | ) { 38 | // that is null when instrumenting static methods. 39 | Class clazz = that != null ? that.getClass() : method.getDeclaringClass(); 40 | try { 41 | // The following code is equivalent to: 42 | // return Delegator.before(that, method, args); 43 | // However, the Delegator class will not be available in the context of the instrumented method, 44 | // so we must use our agent class loader to load the Delegator class and do the call via reflection. 45 | Class delegator = ClassLoaderCache.getInstance().currentClassLoader().loadClass("io.promagent.internal.Delegator"); 46 | Method beforeMethod = delegator.getMethod("before", Class.class, Method.class, Object[].class); 47 | return (List) beforeMethod.invoke(null, clazz, method, args); 48 | } catch (Exception e) { 49 | System.err.println("Error executing Prometheus hook on " + clazz.getSimpleName()); 50 | e.printStackTrace(); 51 | return null; 52 | } 53 | } 54 | 55 | @OnMethodExit(onThrowable = Throwable.class) 56 | public static void after( 57 | @Enter List hooks, 58 | @This(optional = true) Object that, 59 | @Origin Method method, 60 | @AllArguments Object[] args, 61 | @Return(typing = Assigner.Typing.DYNAMIC) Object returned, // support void == null and int == Integer 62 | @Thrown Throwable thrown 63 | ) { 64 | try { 65 | // The following code is equivalent to: 66 | // Delegator.after(hooks, method, args); 67 | // However, the Delegator class will not be available in the context of the instrumented method, 68 | // so we must use our agent class loader to load the Delegator class and do the call via reflection. 69 | Class delegator = ClassLoaderCache.getInstance().currentClassLoader().loadClass("io.promagent.internal.Delegator"); 70 | Method afterMethod = delegator.getMethod("after", List.class, Method.class, Object[].class, Object.class, Throwable.class); 71 | afterMethod.invoke(null, hooks, method, args, returned, thrown); 72 | } catch (Exception e) { 73 | Class clazz = that != null ? that.getClass() : method.getDeclaringClass(); 74 | System.err.println("Error executing Prometheus hook on " + clazz.getSimpleName()); 75 | e.printStackTrace(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /promagent-example/src/test/java/io/promagent/it/SpringIT.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.it; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertTrue; 19 | 20 | import java.util.Arrays; 21 | 22 | import okhttp3.MediaType; 23 | import okhttp3.OkHttpClient; 24 | import okhttp3.Request; 25 | import okhttp3.RequestBody; 26 | import okhttp3.Response; 27 | import org.junit.jupiter.api.Test; 28 | 29 | public class SpringIT { 30 | 31 | /** 32 | * Run some HTTP queries against a Docker container from image promagent/spring-promagent. 33 | *

34 | * The Docker container is started by the maven-docker-plugin when running mvn verify -Pspring. 35 | */ 36 | @Test 37 | public void testSpring() throws Exception { 38 | OkHttpClient client = new OkHttpClient(); 39 | Request metricsRequest = new Request.Builder().url(System.getProperty("promagent.url") + "/metrics").build(); 40 | 41 | // Execute two POST requests 42 | Response restResponse = client.newCall(makePostRequest("Frodo", "Baggins")).execute(); 43 | assertEquals(201, restResponse.code()); 44 | restResponse = client.newCall(makePostRequest("Bilbo", "Baggins")).execute(); 45 | assertEquals(201, restResponse.code()); 46 | 47 | // Query Prometheus metrics from promagent 48 | Response metricsResponse = client.newCall(metricsRequest).execute(); 49 | String[] metricsLines = metricsResponse.body().string().split("\n"); 50 | 51 | String httpRequestsTotal = Arrays.stream(metricsLines) 52 | .filter(m -> m.contains("http_requests_total")) 53 | .filter(m -> m.contains("method=\"POST\"")) 54 | .filter(m -> m.contains("path=\"/people\"")) 55 | .filter(m -> m.contains("status=\"201\"")) 56 | .findFirst().orElseThrow(() -> new Exception("http_requests_total metric not found.")); 57 | 58 | assertTrue(httpRequestsTotal.endsWith("2.0"), "Value should be 2.0 for " + httpRequestsTotal); 59 | 60 | String sqlQueriesTotal = Arrays.stream(metricsLines) 61 | .filter(m -> m.contains("sql_queries_total")) 62 | // The following regular expression tests for this string, but allows the parameters 'id', 'fist_name', 'last_name' to change order: 63 | // query="insert into person (first_name, last_name, id)" 64 | .filter(m -> m.matches(".*query=\"insert into person \\((?=.*id)(?=.*first_name)(?=.*last_name).*\\) values \\(...\\)\".*")) 65 | .filter(m -> m.contains("method=\"POST\"")) 66 | .filter(m -> m.contains("path=\"/people\"")) 67 | .findFirst().orElseThrow(() -> new Exception("sql_queries_total metric not found.")); 68 | 69 | assertTrue(sqlQueriesTotal.endsWith("2.0"), "Value should be 2.0 for " + sqlQueriesTotal); 70 | } 71 | 72 | private Request makePostRequest(String firstName, String lastName) { 73 | return new Request.Builder() 74 | .url(System.getProperty("deployment.url") + "/people") 75 | .method("POST", RequestBody.create( 76 | MediaType.parse("application/json"), 77 | "{ \"firstName\" : \"" + firstName + "\", \"lastName\" : \"" + lastName + "\" }")) 78 | .build(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /promagent-example/src/test/java/io/promagent/it/WildflyIT.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.it; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | 19 | import java.util.Arrays; 20 | 21 | import okhttp3.OkHttpClient; 22 | import okhttp3.Request; 23 | import okhttp3.Response; 24 | import org.junit.jupiter.api.Assertions; 25 | import org.junit.jupiter.api.Test; 26 | 27 | public class WildflyIT { 28 | 29 | /** 30 | * Run some HTTP queries against a Docker container from image promagent/wildfly-kitchensink-promagent. 31 | *

32 | * The Docker container is started by the maven-docker-plugin when running mvn verify -Pwildfly. 33 | */ 34 | @Test 35 | public void testWildfly() throws Exception { 36 | OkHttpClient client = new OkHttpClient(); 37 | Request restRequest = new Request.Builder().url(System.getProperty("deployment.url") + "/rest/members").build(); 38 | 39 | // Execute REST call 40 | Response restResponse = client.newCall(restRequest).execute(); 41 | Assertions.assertEquals(restResponse.code(), 200); 42 | Assertions.assertTrue(restResponse.body().string().contains("John Smith")); 43 | 44 | Thread.sleep(100); // metric is incremented after servlet has written the response, wait a little to get the updated metric 45 | assertMetrics(client, "1.0"); 46 | 47 | // Execute REST call again 48 | restResponse = client.newCall(restRequest).execute(); 49 | Assertions.assertEquals(restResponse.code(), 200); 50 | Assertions.assertTrue(restResponse.body().string().contains("John Smith")); 51 | 52 | Thread.sleep(100); // metric is incremented after servlet has written the response, wait a little to get the updated metric 53 | assertMetrics(client, "2.0"); 54 | } 55 | 56 | private void assertMetrics(OkHttpClient client, String nCalls) throws Exception { 57 | 58 | Request metricsRequest = new Request.Builder().url(System.getProperty("promagent.url") + "/metrics").build(); 59 | Response metricsResponse = client.newCall(metricsRequest).execute(); 60 | String[] metricsLines = metricsResponse.body().string().split("\n"); 61 | 62 | String httpRequestsTotal = Arrays.stream(metricsLines) 63 | .filter(m -> m.contains("http_requests_total")) 64 | .filter(m -> m.contains("method=\"GET\"")) 65 | .filter(m -> m.contains("path=\"/wildfly-kitchensink/rest/members\"")) 66 | .filter(m -> m.contains("status=\"200\"")) 67 | .findFirst().orElseThrow(() -> new Exception("http_requests_total metric not found.")); 68 | 69 | assertTrue(httpRequestsTotal.endsWith(nCalls), "Value should be " + nCalls + " for " + httpRequestsTotal); 70 | 71 | String sqlQueriesTotal = Arrays.stream(metricsLines) 72 | .filter(m -> m.contains("sql_queries_total")) 73 | .filter(m -> m.matches(".*?query=\"select .*?id .*?email .*?name .*?phone_number .*? from Member .*?\".*?")) 74 | .filter(m -> m.contains("method=\"GET\"")) 75 | .filter(m -> m.contains("path=\"/wildfly-kitchensink/rest/members\"")) 76 | .findFirst().orElseThrow(() -> new Exception("sql_queries_total metric not found.")); 77 | 78 | assertTrue(sqlQueriesTotal.endsWith(nCalls), "Value should be " + nCalls + " for " + sqlQueriesTotal); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/HookMetadataParserTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import io.promagent.annotations.*; 18 | import org.junit.jupiter.api.Assertions; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import javax.servlet.ServletRequest; 23 | import javax.servlet.ServletResponse; 24 | import java.io.IOException; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.SortedSet; 30 | 31 | class HookMetadataParserTest { 32 | 33 | private HookMetadataParser parser; 34 | 35 | @BeforeEach 36 | void setUp() { 37 | List classesDir = new ArrayList<>(); 38 | classesDir.add(Paths.get(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath())); 39 | this.parser = new HookMetadataParser(classesDir); 40 | } 41 | 42 | @Hook(instruments = { 43 | "javax.servlet.Servlet", 44 | "javax.servlet.Filter" 45 | }) 46 | private static class ServletTestHook { 47 | 48 | @Before(method = {"service", "doFilter"}) 49 | public void before(ServletRequest request, ServletResponse response) {} 50 | 51 | @After(method = {"service", "doFilter"}) 52 | public void after(ServletRequest request, ServletResponse response) throws Exception {} 53 | } 54 | 55 | @Hook(instruments = "com.example.Some") 56 | private static class PrimitiveTypesTestHook { 57 | 58 | @Before(method = "arrayArgs") 59 | void before(Object[] a, int[] b, String[] c) {} 60 | 61 | @Before(method = "noArgs") 62 | void before() {} 63 | 64 | @Before(method = "primitiveArgs") 65 | void before(boolean a, char b, byte c, short d, int f, float g, long h, double i) {} 66 | 67 | @Before(method = "boxedArgs") 68 | void before(Boolean a, Character b, Byte c, Short d, Integer f, Float g, Long h, Double i) {} 69 | } 70 | 71 | @Hook(instruments = "com.example.ReturnThrown") 72 | private static class ReturnedAndThrownTestHook { 73 | @After(method = "div") 74 | void after(int a, int b, @Returned int result, @Thrown Throwable exception) {} 75 | } 76 | 77 | @Test 78 | void testServletHook() throws ClassNotFoundException, IOException { 79 | String expected = ServletTestHook.class.getName() + " instruments [javax.servlet.Filter, javax.servlet.Servlet]:\n" + 80 | " * doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)\n" + 81 | " * service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)"; 82 | SortedSet result = parser.parse(className -> className.equals(ServletTestHook.class.getName())); 83 | Assertions.assertEquals(1, result.size()); 84 | Assertions.assertEquals(expected, result.first().toString()); 85 | } 86 | 87 | @Test 88 | void testPrimitiveTypes() throws ClassNotFoundException, IOException { 89 | String expected = PrimitiveTypesTestHook.class.getName() + " instruments [com.example.Some]:\n" + 90 | " * arrayArgs(java.lang.Object[], int[], java.lang.String[])\n" + 91 | " * boxedArgs(java.lang.Boolean, java.lang.Character, java.lang.Byte, java.lang.Short, java.lang.Integer, java.lang.Float, java.lang.Long, java.lang.Double)\n" + 92 | " * noArgs()\n" + 93 | " * primitiveArgs(boolean, char, byte, short, int, float, long, double)"; 94 | SortedSet result = parser.parse(className -> className.equals(PrimitiveTypesTestHook.class.getName())); 95 | Assertions.assertEquals(1, result.size()); 96 | Assertions.assertEquals(expected, result.first().toString()); 97 | } 98 | 99 | @Test 100 | void testReturnedAndThrown() throws IOException, ClassNotFoundException { 101 | String expected = ReturnedAndThrownTestHook.class.getName() + " instruments [com.example.ReturnThrown]:\n" + 102 | " * div(int, int)"; 103 | SortedSet result = parser.parse(className -> className.equals(ReturnedAndThrownTestHook.class.getName())); 104 | Assertions.assertEquals(1, result.size()); 105 | Assertions.assertEquals(expected, result.first().toString()); 106 | } 107 | 108 | @Test 109 | void testNoHook() throws ClassNotFoundException, IOException { 110 | // Use HookMetadataParserTest as an example of a class that does not have any @Hook annotation. 111 | SortedSet result = parser.parse(className -> className.equals(HookMetadataParserTest.class.getName())); 112 | Assertions.assertTrue(result.isEmpty()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/HookMetadata.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import java.util.*; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * Metadata for a Hook class as parsed from the @Hook, @Before, and @After annotations. 22 | */ 23 | public class HookMetadata implements Comparable { 24 | 25 | private final String hookClassName; 26 | private final SortedSet instruments; 27 | private final SortedSet methods; 28 | 29 | public static class MethodSignature implements Comparable { 30 | 31 | private final String methodName; 32 | private final List parameterTypes; 33 | 34 | public MethodSignature(String methodName, List parameterTypes) { 35 | this.methodName = methodName; 36 | this.parameterTypes = Collections.unmodifiableList(new ArrayList<>(parameterTypes)); 37 | } 38 | 39 | public String getMethodName() { 40 | return methodName; 41 | } 42 | 43 | public List getParameterTypes() { 44 | return parameterTypes; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return methodName + "(" + String.join(", ", parameterTypes) + ")"; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object o) { 54 | return o != null && getClass() == o.getClass() && compareTo((MethodSignature) o) == 0; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return Objects.hash(toString()); 60 | } 61 | 62 | @Override 63 | public int compareTo(MethodSignature o) { 64 | return Comparator 65 | .comparing(MethodSignature::getMethodName) 66 | .thenComparing(MethodSignature::getParameterTypes, new LexicographicalComparator<>()) 67 | .compare(this, o); 68 | } 69 | } 70 | 71 | public HookMetadata(String hookClassName, Collection instruments, Collection methods) { 72 | this.hookClassName = hookClassName; 73 | this.instruments = Collections.unmodifiableSortedSet(new TreeSet<>(instruments)); 74 | this.methods = Collections.unmodifiableSortedSet(new TreeSet<>(methods)); 75 | } 76 | 77 | public String getHookClassName() { 78 | return hookClassName; 79 | } 80 | 81 | public SortedSet getInstruments() { 82 | return instruments; 83 | } 84 | 85 | public SortedSet getMethods() { 86 | return methods; 87 | } 88 | 89 | @Override 90 | public String toString() { // TODO: instruments is a Set 91 | String delimiter = System.lineSeparator() + " * "; 92 | return hookClassName + " instruments [" + String.join(", ", instruments) + "]:" + delimiter + String.join(delimiter, strings(methods)); 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | return o != null && getClass() == o.getClass() && compareTo((HookMetadata) o) == 0; 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | return Objects.hash(toString()); 103 | } 104 | 105 | @Override 106 | public int compareTo(HookMetadata o) { 107 | return Comparator 108 | .comparing(HookMetadata::getHookClassName) 109 | .thenComparing(HookMetadata::getInstruments, new LexicographicalComparator<>()) 110 | .thenComparing(HookMetadata::getMethods, new LexicographicalComparator<>()) 111 | .compare(this, o); 112 | } 113 | 114 | private List strings(Collection list) { 115 | return list.stream().map(Object::toString).collect(Collectors.toList()); 116 | } 117 | 118 | /** 119 | * Compare SortedSet instances or List instances in lexicographical order. 120 | */ 121 | static class LexicographicalComparator, T extends Iterable> implements Comparator { 122 | @Override 123 | public int compare(T list1, T list2) { 124 | Iterator iterator1 = list1.iterator(); 125 | Iterator iterator2 = list2.iterator(); 126 | while (iterator1.hasNext() && iterator2.hasNext()) { 127 | int result = iterator1.next().compareTo(iterator2.next()); 128 | if (result != 0) { 129 | return result; 130 | } 131 | } 132 | if (iterator1.hasNext()) { 133 | return 1; 134 | } 135 | if (iterator2.hasNext()) { 136 | return -1; 137 | } 138 | return 0; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /promagent-example/src/main/java/io/promagent/hooks/ServletHook.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.hooks; 16 | 17 | import io.promagent.annotations.After; 18 | import io.promagent.annotations.Before; 19 | import io.promagent.annotations.Hook; 20 | import io.promagent.hookcontext.MetricDef; 21 | import io.promagent.hookcontext.MetricsStore; 22 | import io.prometheus.client.Counter; 23 | import io.prometheus.client.Summary; 24 | 25 | import javax.servlet.ServletRequest; 26 | import javax.servlet.ServletResponse; 27 | import javax.servlet.http.HttpServletRequest; 28 | import javax.servlet.http.HttpServletResponse; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | import static io.promagent.hooks.HttpContext.HTTP_METHOD; 32 | import static io.promagent.hooks.HttpContext.HTTP_PATH; 33 | 34 | @Hook(instruments = { 35 | "javax.servlet.Servlet", 36 | "javax.servlet.Filter" 37 | }) 38 | public class ServletHook { 39 | 40 | private final Counter httpRequestsTotal; 41 | private final Summary httpRequestsDuration; 42 | private long startTime = 0; 43 | 44 | public ServletHook(MetricsStore metricsStore) { 45 | 46 | httpRequestsTotal = metricsStore.createOrGet(new MetricDef<>( 47 | "http_requests_total", 48 | (name, registry) -> Counter.build() 49 | .name(name) 50 | .labelNames("method", "path", "status") 51 | .help("Total number of http requests.") 52 | .register(registry) 53 | )); 54 | 55 | httpRequestsDuration = metricsStore.createOrGet(new MetricDef<>( 56 | "http_request_duration", 57 | (name, registry) -> Summary.build() 58 | .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error 59 | .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error 60 | .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error 61 | .name(name) 62 | .labelNames("method", "path", "status") 63 | .help("Duration for serving the http requests in seconds.") 64 | .register(registry) 65 | )); 66 | } 67 | 68 | private String stripPathParameters(String path) { 69 | 70 | // The URL path may include path parameters. 71 | // For example, REST URLs for querying an item might look like this: 72 | // 73 | // /item/1 74 | // /item/2 75 | // /item/3 76 | // etc. 77 | // 78 | // We don't want to create a new Prometheus label for each of these paths. 79 | // Rather, we want a single label like this: 80 | // 81 | // /item/{id} 82 | // 83 | // This method replaces path parameters with placeholders. It is application specific and 84 | // should be adapted depending on the actual paths in an application. 85 | // For the demo, we just replace all numbers with {id}. 86 | 87 | return path 88 | .replaceAll("/[0-9]+", "/{id}") 89 | .replaceAll("/;jsessionid=\\w*", "") 90 | .replaceAll("/$", "") 91 | .replaceAll("\\?.*", ""); // Also remove path parameters, like "?jsessionid=..." 92 | } 93 | 94 | @Before(method = {"service", "doFilter"}) 95 | public void before(ServletRequest request, ServletResponse response) { 96 | if (HttpServletRequest.class.isAssignableFrom(request.getClass()) && HttpServletResponse.class.isAssignableFrom(response.getClass())) { 97 | HttpServletRequest req = (HttpServletRequest) request; 98 | HttpContext.put(HTTP_METHOD, req.getMethod()); 99 | HttpContext.put(HTTP_PATH, stripPathParameters(req.getRequestURI())); 100 | startTime = System.nanoTime(); 101 | } 102 | } 103 | 104 | // Return Werte und Exceptions als Parameter 105 | @After(method = {"service", "doFilter"}) 106 | public void after(ServletRequest request, ServletResponse response/*, @Returned int i, @Thrown Throwable t*/) throws Exception { 107 | if (HttpServletRequest.class.isAssignableFrom(request.getClass()) && HttpServletResponse.class.isAssignableFrom(response.getClass())) { 108 | HttpServletResponse resp = (HttpServletResponse) response; 109 | try { 110 | double duration = ((double) System.nanoTime() - startTime) / (double) TimeUnit.SECONDS.toNanos(1L); 111 | String method = HttpContext.get(HTTP_METHOD).get(); 112 | String path = HttpContext.get(HTTP_PATH).get(); 113 | httpRequestsTotal.labels(method, path, Integer.toString(resp.getStatus())).inc(); 114 | httpRequestsDuration.labels(method, path, Integer.toString(resp.getStatus())).observe(duration); 115 | } finally { 116 | HttpContext.clear(HTTP_METHOD, HTTP_PATH); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/LifecycleTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.instrumentationtests; 16 | 17 | import io.promagent.agent.ClassLoaderCache; 18 | import io.promagent.hookcontext.MetricsStore; 19 | import io.promagent.internal.Delegator; 20 | import io.promagent.internal.HookMetadata; 21 | import io.promagent.internal.instrumentationtests.classes.Fruit; 22 | import io.promagent.internal.instrumentationtests.classes.IParameterTypesExample; 23 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 24 | import io.promagent.internal.instrumentationtests.hooks.*; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.*; 29 | import java.util.concurrent.ExecutorService; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | class LifecycleTest { 34 | 35 | private IParameterTypesExample parameterTypesExample; 36 | 37 | @BeforeEach 38 | void setUp() throws Exception { 39 | SortedSet hookMetadata = Util.loadHookMetadata( 40 | LifecycleHookSkipTrue.class, 41 | LifecycleHookSkipFalse.class 42 | ); 43 | ClassLoaderCache classLoaderCache = Util.mockClassLoaderCache(); 44 | parameterTypesExample = Instrumentor.instrument(ParameterTypesExample.class, hookMetadata); 45 | MetricsStore metricsStore = Util.mockMetricsStore(); 46 | Delegator.init(hookMetadata, metricsStore, classLoaderCache); 47 | MethodCallCounter.reset(); 48 | } 49 | 50 | /** 51 | * Expected behavior: 52 | *

    53 | *
  • For LifecycleHookSkipTrue, only the first call to parameterTypesExample.recursive() should be instrumented, 54 | * all recursive calls should not be instrumented. 55 | *
  • For LifecycleHookSkipFalse, all calls should be instrumented, but recursive calls are executed with the existing Hook instance, 56 | * while non-recursive calls are executed with a new instance. 57 | *
58 | */ 59 | @Test 60 | void testLivecycle() throws Exception { 61 | List runConfigs = Arrays.asList( 62 | new RecursiveRunConfig(3, 4), 63 | new RecursiveRunConfig(2, 3), 64 | new RecursiveRunConfig(2, 4) 65 | ); 66 | ExecutorService executor = Executors.newFixedThreadPool(runConfigs.size()); 67 | for (RecursiveRunConfig runConfig : runConfigs) { 68 | executor.submit(() -> { 69 | for (int i=0; i cfg.nRecursiveCalls) 82 | .max() 83 | .getAsInt(); 84 | 85 | for (int nRecursiveCalls = maxNRecursiveCalls; nRecursiveCalls >= 0; nRecursiveCalls--) { 86 | final int n = nRecursiveCalls; // copy to final variable so it can be used in lambda 87 | int expectedNumCallsSkipFalse = runConfigs.stream() 88 | .filter(cfg -> cfg.nRecursiveCalls >= n) // recursive calls executed: >= n 89 | .mapToInt(cfg -> cfg.nRuns) 90 | .sum(); 91 | int expectedNumCallsSkipTrue = runConfigs.stream() 92 | .filter(cfg -> cfg.nRecursiveCalls == n) // recursive calls skipped: == n 93 | .mapToInt(cfg -> cfg.nRuns) 94 | .sum(); 95 | MethodCallCounter.assertNumCalls(expectedNumCallsSkipFalse, LifecycleHookSkipFalse.class, "before", nRecursiveCalls); 96 | MethodCallCounter.assertNumCalls(expectedNumCallsSkipTrue, LifecycleHookSkipTrue.class, "before", nRecursiveCalls); 97 | } 98 | // Number of instances should be the same because recursive calls are executed with an existing instance. 99 | MethodCallCounter.assertNumHookInstances(runConfigs.stream().mapToInt(cfg -> cfg.nRuns).sum(), LifecycleHookSkipFalse.class); 100 | MethodCallCounter.assertNumHookInstances(runConfigs.stream().mapToInt(cfg -> cfg.nRuns).sum(), LifecycleHookSkipTrue.class); 101 | } 102 | 103 | private static class RecursiveRunConfig { 104 | final int nRuns; // number of runs within the same thread 105 | final int nRecursiveCalls; // number of recursive calls within each run 106 | 107 | private RecursiveRunConfig(int nRuns, int nRecursiveCalls) { 108 | this.nRuns = nRuns; 109 | this.nRecursiveCalls = nRecursiveCalls; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/ReturnedAndThrownTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.instrumentationtests; 16 | 17 | import io.promagent.agent.ClassLoaderCache; 18 | import io.promagent.hookcontext.MetricsStore; 19 | import io.promagent.internal.Delegator; 20 | import io.promagent.internal.HookMetadata; 21 | import io.promagent.internal.instrumentationtests.classes.Fruit; 22 | import io.promagent.internal.instrumentationtests.classes.IReturnedAndThrownExample; 23 | import io.promagent.internal.instrumentationtests.classes.ReturnedAndThrownExample; 24 | import io.promagent.internal.instrumentationtests.hooks.ReturnedAndThrownHook; 25 | import org.junit.jupiter.api.Assertions; 26 | import org.junit.jupiter.api.BeforeEach; 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.util.List; 30 | import java.util.SortedSet; 31 | 32 | class ReturnedAndThrownTest { 33 | 34 | private IReturnedAndThrownExample returnedAndThrownExample; 35 | private final Fruit.Orange orange = new Fruit.Orange(); 36 | 37 | @BeforeEach 38 | void setUp() throws Exception { 39 | SortedSet hookMetadata = Util.loadHookMetadata( 40 | ReturnedAndThrownHook.class 41 | ); 42 | ClassLoaderCache classLoaderCache = Util.mockClassLoaderCache(); 43 | returnedAndThrownExample = Instrumentor.instrument(ReturnedAndThrownExample.class, hookMetadata); 44 | MetricsStore metricsStore = Util.mockMetricsStore(); 45 | Delegator.init(hookMetadata, metricsStore, classLoaderCache); 46 | MethodCallCounter.reset(); 47 | } 48 | 49 | @Test 50 | void testVoid() { 51 | for (int n=1; n<=2; n++) { 52 | returnedAndThrownExample.returnVoid(orange); 53 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "beforeVoid", orange); 54 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "afterVoid", orange); 55 | } 56 | } 57 | 58 | @Test 59 | void testPrimitive() { 60 | for (int n=1; n<=2; n++) { 61 | int ret = returnedAndThrownExample.returnPrimitive(orange); 62 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "beforePrimitive", orange); 63 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "afterPrimitive", orange, ret); 64 | } 65 | } 66 | 67 | @Test 68 | void testObject() { 69 | Fruit ret1 = returnedAndThrownExample.returnObject(); 70 | Fruit ret2 = returnedAndThrownExample.returnObject(); 71 | MethodCallCounter.assertNumCalls(2, ReturnedAndThrownHook.class, "beforeObject"); 72 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterObject", ret1); 73 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterObject", ret2); 74 | } 75 | 76 | @Test 77 | void testArray() { 78 | int[] params = {1, 2, 3}; 79 | int[] ret1 = returnedAndThrownExample.returnArray(params); 80 | int[] ret2 = returnedAndThrownExample.returnArray(params); 81 | MethodCallCounter.assertNumCalls(2, ReturnedAndThrownHook.class, "beforeArray", new Object[]{params}); 82 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterArray", ret1, params); 83 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterArray", ret2, params); 84 | } 85 | 86 | @Test 87 | void testGenerics() { 88 | for (int n=1; n<=2; n++) { 89 | List ret = returnedAndThrownExample.returnGenerics(orange); 90 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "beforeGenerics", orange); 91 | // The following works because List.equals() compares the elements within the lists, not the lists itself. 92 | // Therefore both return values are considered equal, because they are both lists containing the same element. 93 | MethodCallCounter.assertNumCalls(n, ReturnedAndThrownHook.class, "afterGenerics", orange, ret); 94 | } 95 | } 96 | 97 | @Test 98 | void testRuntimeException() { 99 | try { 100 | returnedAndThrownExample.throwsRuntimeException(7, orange); 101 | } catch (Exception e) { 102 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "beforeThrowsRuntimeException", 7, orange); 103 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterThrowsRuntimeException", 7, orange, null, e); 104 | return; // success 105 | } 106 | Assertions.fail("exception not thrown."); 107 | } 108 | 109 | @Test 110 | void testCheckedException() { 111 | try { 112 | returnedAndThrownExample.throwsCheckedException(); 113 | } catch (Exception e) { 114 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "beforeThrowsCheckedException"); 115 | MethodCallCounter.assertNumCalls(1, ReturnedAndThrownHook.class, "afterThrowsCheckedException", 0, e); 116 | return; // success 117 | } 118 | Assertions.fail("exception not thrown."); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/src/main/java/io/promagent/plugin/AgentJar.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.plugin; 16 | 17 | import org.apache.maven.plugin.MojoExecutionException; 18 | import org.codehaus.plexus.util.IOUtil; 19 | 20 | import java.io.*; 21 | import java.util.Enumeration; 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | import java.util.jar.JarEntry; 25 | import java.util.jar.JarFile; 26 | import java.util.jar.JarOutputStream; 27 | 28 | class AgentJar implements AutoCloseable { 29 | 30 | private final String jarName; 31 | private final JarOutputStream jarOutputStream; 32 | private final Set content = new HashSet<>(); 33 | 34 | enum Directory { 35 | SHARED_JARS("shared-jars/"), // Directory entries in JAR file must end in '/' 36 | PER_DEPLOYMENT_JARS("per-deployment-jars/"); 37 | 38 | private final String name; 39 | 40 | Directory(String name) { 41 | this.name = name; 42 | } 43 | 44 | public String getName() { 45 | return name; 46 | } 47 | } 48 | 49 | private AgentJar(String jarName, JarOutputStream jarOutputStream) { 50 | this.jarName = jarName; 51 | this.jarOutputStream = jarOutputStream; 52 | } 53 | 54 | static AgentJar create(File jarFile) throws MojoExecutionException { 55 | try { 56 | JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(jarFile))); 57 | return new AgentJar(jarFile.getName(), jarOutputStream); 58 | } catch (IOException e) { 59 | throw new MojoExecutionException("Error creating " + jarFile.getName() + ": " + e.getMessage(), e); 60 | } 61 | } 62 | 63 | void addFile(File srcFile, String targetFileName, Directory targetDir) throws MojoExecutionException { 64 | String destPath = targetDir.getName() + targetFileName; 65 | if (content.contains(destPath)) { 66 | return; 67 | } 68 | makeDirsRecursively(destPath); 69 | content.add(destPath); 70 | try (InputStream in = new FileInputStream(srcFile)) { 71 | jarOutputStream.putNextEntry(new JarEntry(destPath)); 72 | IOUtil.copy(in, jarOutputStream); 73 | } catch (IOException e) { 74 | throw new MojoExecutionException("Error adding " + srcFile.getName() + " to target JAR: " + e.getMessage(), e); 75 | } 76 | } 77 | 78 | void addFile(File srcFile, Directory targetDir) throws MojoExecutionException { 79 | addFile(srcFile, srcFile.getName(), targetDir); 80 | } 81 | 82 | void extractJar(File jar, ManifestTransformer manifestTransformer) throws MojoExecutionException { 83 | try (JarFile jarFile = new JarFile(jar)) { 84 | for (Enumeration jarEntries = jarFile.entries(); jarEntries.hasMoreElements(); ) { 85 | JarEntry jarEntry = jarEntries.nextElement(); 86 | if (manifestTransformer.canTransform(jarEntry)) { 87 | jarEntry = manifestTransformer.transform(jarEntry); 88 | } 89 | if (!jarEntry.isDirectory() && !content.contains(jarEntry.getName())) { 90 | content.add(jarEntry.getName()); 91 | makeDirsRecursively(jarEntry.getName()); 92 | try (InputStream in = getInputStream(jarEntry, jarFile, manifestTransformer)) { 93 | jarOutputStream.putNextEntry(jarEntry); 94 | IOUtil.copy(in, jarOutputStream); 95 | } 96 | } 97 | } 98 | } catch (IOException e) { 99 | throw new MojoExecutionException("Error adding " + jar.getName() + " to target JAR: " + e.getMessage(), e); 100 | } 101 | } 102 | 103 | private InputStream getInputStream(JarEntry jarEntry, JarFile jarFile, ManifestTransformer manifestTransformer) throws IOException, MojoExecutionException { 104 | InputStream in = jarFile.getInputStream(jarEntry); 105 | if (manifestTransformer.canTransform(jarEntry)) { 106 | in = manifestTransformer.transform(in); 107 | } 108 | return in; 109 | } 110 | 111 | private void makeDirsRecursively(String path) throws MojoExecutionException { 112 | String[] parts = path.split("/+"); 113 | String segment = ""; 114 | for (int i=0; i( 43 | "sql_queries_total", 44 | (name, registry) -> Counter.build() 45 | .name(name) 46 | .labelNames("method", "path", "query") 47 | .help("Total number of sql queries.") 48 | .register(registry) 49 | )); 50 | 51 | sqlQueriesDuration = metricsStore.createOrGet(new MetricDef<>( 52 | "sql_query_duration", 53 | (name, registry) -> Summary.build() 54 | .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error 55 | .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error 56 | .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error 57 | .name(name) 58 | .labelNames("method", "path", "query") 59 | .help("Duration for serving the sql queries in seconds.") 60 | .register(registry) 61 | )); 62 | } 63 | 64 | private String stripValues(String query) { 65 | // We want the structure of the query as labels, not the actual values. 66 | // Therefore, we replace: 67 | // insert into Member (id, name, email, phone_number) values (0, 'John Smith', 'john.smith@mailinator.com', '2125551212') 68 | // with 69 | // insert into Member (id, name, email, phone_number) values (...) 70 | return query.replaceAll("values\\s*\\(.*?\\)", "values (...)"); 71 | } 72 | 73 | // --- before 74 | 75 | @Before(method = {"execute", "executeQuery", "executeUpdate", "executeLargeUpdate", "prepareStatement", "prepareCall"}) 76 | public void before(String sql) { 77 | startTime = System.nanoTime(); 78 | } 79 | 80 | @Before(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 81 | public void before(String sql, int autoGeneratedKeys) { 82 | before(sql); 83 | } 84 | 85 | @Before(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 86 | public void before(String sql, int[] columnIndexes) { 87 | before(sql); 88 | } 89 | 90 | @Before(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 91 | public void before(String sql, String[] columnNames) { 92 | before(sql); 93 | } 94 | 95 | @Before(method = {"prepareStatement", "prepareCall"}) 96 | public void before(String sql, int resultSetType, int resultSetConcurrency) { 97 | before(sql); 98 | } 99 | 100 | @Before(method = {"prepareStatement", "prepareCall"}) 101 | public void before(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) { 102 | before(sql); 103 | } 104 | 105 | // --- after 106 | 107 | @After(method = {"execute", "executeQuery", "executeUpdate", "executeLargeUpdate", "prepareStatement", "prepareCall"}) 108 | public void after(String sql) throws Exception { 109 | double duration = ((double) System.nanoTime() - startTime) / (double) TimeUnit.SECONDS.toNanos(1L); 110 | String method = HttpContext.get(HTTP_METHOD).orElse("no http context"); 111 | String path = HttpContext.get(HTTP_PATH).orElse("no http context"); 112 | String query = stripValues(sql); 113 | sqlQueriesTotal.labels(method, path, query).inc(); 114 | sqlQueriesDuration.labels(method, path, query).observe(duration); 115 | } 116 | 117 | @After(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 118 | public void after(String sql, int autoGeneratedKeys) throws Exception { 119 | after(sql); 120 | } 121 | 122 | @After(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 123 | public void after(String sql, int[] columnIndexes) throws Exception { 124 | after(sql); 125 | } 126 | 127 | @After(method = {"execute", "executeUpdate", "executeLargeUpdate", "prepareStatement"}) 128 | public void after(String sql, String[] columnNames) throws Exception { 129 | after(sql); 130 | } 131 | 132 | @After(method = {"prepareStatement", "prepareCall"}) 133 | public void after(String sql, int resultSetType, int resultSetConcurrency) throws Exception { 134 | after(sql); 135 | } 136 | 137 | @After(method = {"prepareStatement", "prepareCall"}) 138 | public void after(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws Exception { 139 | after(sql); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /promagent-framework/promagent-agent/src/main/java/io/promagent/agent/JarFiles.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.agent; 16 | 17 | import java.io.IOException; 18 | import java.lang.management.ManagementFactory; 19 | import java.net.URISyntaxException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | import java.security.CodeSource; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.Enumeration; 27 | import java.util.List; 28 | import java.util.function.Predicate; 29 | import java.util.jar.JarEntry; 30 | import java.util.jar.JarFile; 31 | import java.util.regex.Matcher; 32 | import java.util.regex.Pattern; 33 | 34 | /** 35 | * The promagent.jar contains a lib/ directory with JARs from the promagent modules and their dependencies. 36 | * This class provides URLs to these JAR files. 37 | */ 38 | class JarFiles { 39 | 40 | // The separation between "per deployment JARs" and "shared JARs" is done because 41 | // in an application server some classes must be loaded for each deployment, 42 | // while other classes can be shared across multiple deployments. 43 | // Currently, only the "promagent-hooks" module is in the "per deployment JARs", 44 | // all other modules and their dependencies are in the "shared JARs". 45 | 46 | private final List perDeploymentJars; // classes loaded for each deployment 47 | private final List sharedJars; // classes shared across multiple deployments 48 | 49 | private JarFiles(List perDeploymentJars, List sharedJars) { 50 | this.perDeploymentJars = Collections.unmodifiableList(perDeploymentJars); 51 | this.sharedJars = Collections.unmodifiableList(sharedJars); 52 | } 53 | 54 | List getPerDeploymentJars() { 55 | return perDeploymentJars; 56 | } 57 | 58 | List getSharedJars() { 59 | return sharedJars; 60 | } 61 | 62 | /** 63 | * Theoretically we could return a list of jar:/ URLs without extracting the JARs, 64 | * but the URLClassLoader has a bug such that jar:/ URLs cannot be used. Therefore, we have 65 | * to extract the JARs and return a list of file:/ URLs. 66 | * See https://bugs.openjdk.java.net/browse/JDK-4735639 67 | */ 68 | static JarFiles extract() { 69 | List perDeploymentJars = new ArrayList<>(); 70 | List sharedJars = new ArrayList<>(); 71 | Path agentJar = findAgentJar(); 72 | List extractedJars; 73 | try { 74 | Path tmpDir = Files.createTempDirectory("promagent-"); 75 | tmpDir.toFile().deleteOnExit(); 76 | extractedJars = unzip(agentJar, tmpDir, entry -> entry.getName().endsWith(".jar")); 77 | } catch (IOException e) { 78 | throw new RuntimeException("Failed to load promagent.jar: " + e.getMessage(), e); 79 | } 80 | for (Path jar : extractedJars) { 81 | if (jar.getParent().getFileName().toString().equals("per-deployment-jars")) { 82 | perDeploymentJars.add(jar); 83 | } else { 84 | sharedJars.add(jar); 85 | } 86 | } 87 | return new JarFiles(perDeploymentJars, sharedJars); 88 | } 89 | 90 | private static List unzip(Path jarFile, Path destDir, Predicate filter) throws IOException { 91 | List result = new ArrayList<>(); 92 | try (JarFile agentJar = new JarFile(jarFile.toFile())) { 93 | Enumeration jarEntries = agentJar.entries(); 94 | while (jarEntries.hasMoreElements()) { 95 | JarEntry jarEntry = jarEntries.nextElement(); 96 | if (filter.test(jarEntry)) { 97 | Path destFile = destDir.resolve(jarEntry.getName()); 98 | if (!destFile.getParent().toFile().exists()) { 99 | if (!destFile.getParent().toFile().mkdirs()) { 100 | throw new IOException("Failed to make directory: " + destFile.getParent()); 101 | } 102 | } 103 | Files.copy(agentJar.getInputStream(jarEntry), destFile); 104 | result.add(destFile); 105 | } 106 | } 107 | } 108 | return result; 109 | } 110 | 111 | private static Path findAgentJar() { 112 | CodeSource cs = Promagent.class.getProtectionDomain().getCodeSource(); 113 | if (cs != null) { 114 | return findAgentJarFromCodeSource(cs); 115 | } else { 116 | // This happens if the Promagent class is loaded from the bootstrap class loader, 117 | // i.e. in addition to the command line argument -javaagent:/path/to/promagent.jar, 118 | // the argument -Xbootclasspath/p:/path/to/promagent.jar is used. 119 | return findAgentJarFromCmdline(ManagementFactory.getRuntimeMXBean().getInputArguments()); 120 | } 121 | } 122 | 123 | private static Path findAgentJarFromCodeSource(CodeSource cs) { 124 | try { 125 | return Paths.get(cs.getLocation().toURI()); 126 | } catch (URISyntaxException e) { 127 | throw new RuntimeException("Failed to load promagent.jar from " + cs.getLocation() + ": " + e.getMessage(), e); 128 | } 129 | } 130 | 131 | static Path findAgentJarFromCmdline(List cmdlineArgs) { 132 | Pattern p = Pattern.compile("^-javaagent:(.*promagent([^/]*).jar)(=.*)?$"); 133 | for (String arg : cmdlineArgs) { 134 | Matcher m = p.matcher(arg); 135 | if (m.matches()) { 136 | return Paths.get(m.group(1)); 137 | } 138 | } 139 | throw new RuntimeException("Failed to locate promagent.jar file."); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/hooks/ParameterTypesHook.java: -------------------------------------------------------------------------------- 1 | package io.promagent.internal.instrumentationtests.hooks; 2 | 3 | import io.promagent.annotations.After; 4 | import io.promagent.annotations.Before; 5 | import io.promagent.annotations.Hook; 6 | import io.promagent.hookcontext.MetricsStore; 7 | import io.promagent.internal.instrumentationtests.MethodCallCounter; 8 | import io.promagent.internal.instrumentationtests.classes.Fruit; 9 | import io.promagent.internal.instrumentationtests.classes.Fruit.Orange; 10 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Instrument all methods in {@link ParameterTypesExample}. 16 | */ 17 | @Hook(instruments = "io.promagent.internal.instrumentationtests.classes.ParameterTypesExample") 18 | public class ParameterTypesHook { 19 | 20 | public ParameterTypesHook(MetricsStore m) {} 21 | 22 | @Before(method = "noParam") 23 | public void before() { 24 | MethodCallCounter.observe(this, "before"); 25 | } 26 | 27 | @After(method = "noParam") 28 | public void after() { 29 | MethodCallCounter.observe(this, "after"); 30 | } 31 | 32 | @Before(method = {"primitiveTypes", "boxedTypes"}) // "boxedTypes" should be ignored because different method signature 33 | public void before(byte b, short s, int i, long l, float f, double d, boolean x, char c) { 34 | MethodCallCounter.observe(this, "before", b, s, i, l, f, d, x, c); 35 | } 36 | 37 | @After(method = {"primitiveTypes", "boxedTypes"}) // "boxedTypes" should be ignored because different method signature 38 | public void after(byte b, short s, int i, long l, float f, double d, boolean x, char c) { 39 | MethodCallCounter.observe(this, "after", b, s, i, l, f, d, x, c); 40 | } 41 | 42 | @Before(method = {"primitiveTypes", "boxedTypes"}) // "primitiveTypes" should be ignored because different method signature 43 | public void before(Byte b, Short s, Integer i, Long l, Float f, Double d, Boolean x, Character c) { 44 | MethodCallCounter.observe(this, "before", b, s, i, l, f, d, x, c); 45 | } 46 | 47 | @After(method = {"primitiveTypes", "boxedTypes"}) // "primitiveTypes" should be ignored because different method signature 48 | public void after(Byte b, Short s, Integer i, Long l, Float f, Double d, Boolean x, Character c) { 49 | MethodCallCounter.observe(this, "after", b, s, i, l, f, d, x, c); 50 | } 51 | 52 | @Before(method = "objects") 53 | public void before(Object o, Fruit f, Orange x) { 54 | MethodCallCounter.observe(this, "before", o, f, x); 55 | } 56 | 57 | @After(method = "objects") 58 | public void after(Object o, Fruit f, Orange x) { 59 | MethodCallCounter.observe(this, "after", o, f, x); 60 | } 61 | 62 | @Before(method = "objects") 63 | public void before2(Object o, Fruit f, Orange x) { 64 | MethodCallCounter.observe(this, "before2", o, f, x); 65 | } 66 | 67 | @After(method = "objects") 68 | public void after2(Object o, Fruit f, Orange x) { 69 | MethodCallCounter.observe(this, "after2", o, f, x); 70 | } 71 | 72 | @Before(method = "objects") // should not be called, because method signature differs 73 | public void beforeTooLoose(Object o, Fruit f, Fruit x) { 74 | MethodCallCounter.observe(this, "beforeTooLoose", o, f, x); 75 | } 76 | 77 | @Before(method = "objects") // should not be called, because method signature differs 78 | public void beforeTooStrict(Object o, Orange f, Orange x) { 79 | MethodCallCounter.observe(this, "beforeTooStrict", o, f, x); 80 | } 81 | 82 | @Before(method = "objects") 83 | @After(method = "objects") 84 | public void beforeAndAfter(Object o, Fruit f, Orange x) { 85 | MethodCallCounter.observe(this, "beforeAndAfter", o, f, x); 86 | } 87 | 88 | @Before(method = "primitiveArrays") 89 | public void before(byte[] b, short[] s, int[] i, long[] l, float[] f, double[] d, boolean[] x, char[] c) { 90 | MethodCallCounter.observe(this, "before", b, s, i, l, f, d, x, c); 91 | } 92 | 93 | @After(method = "primitiveArrays") 94 | public void after(byte[] b, short[] s, int[] i, long[] l, float[] f, double[] d, boolean[] x, char[] c) { 95 | MethodCallCounter.observe(this, "after", b, s, i, l, f, d, x, c); 96 | } 97 | 98 | @Before(method = "boxedArrays") 99 | public void before(Byte[] b, Short[] s, Integer[] i, Long[] l, Float[] f, Double[] d, Boolean[] x, Character[] c) { 100 | MethodCallCounter.observe(this, "before", b, s, i, l, f, d, x, c); 101 | } 102 | 103 | @After(method = "boxedArrays") 104 | public void after(Byte[] b, Short[] s, Integer[] i, Long[] l, Float[] f, Double[] d, Boolean[] x, Character[] c) { 105 | MethodCallCounter.observe(this, "after", b, s, i, l, f, d, x, c); 106 | } 107 | 108 | @Before(method = "objectArrays") 109 | public void before(Object[] o, Fruit[] f, Orange[] x) { 110 | MethodCallCounter.observe(this, "before", o, f, x); 111 | } 112 | 113 | @After(method = "objectArrays") 114 | public void after(Object[] o, Fruit[] f, Orange[] x) { 115 | MethodCallCounter.observe(this, "after", o, f, x); 116 | } 117 | 118 | @Before(method = "generics") 119 | public void before(List o, List f, List x) { 120 | MethodCallCounter.observe(this, "before", o, f, x); 121 | } 122 | 123 | @After(method = "generics") 124 | public void after(List o, List f, List x) { 125 | MethodCallCounter.observe(this, "after", o, f, x); 126 | } 127 | 128 | @Before(method = "varargsExplicit") 129 | public void before(Object... args) { 130 | MethodCallCounter.observe(this, "before", args); 131 | } 132 | 133 | @After(method = "varargsExplicit") 134 | public void after(Object... args) { 135 | MethodCallCounter.observe(this, "after", args); 136 | } 137 | 138 | @Before(method = "varargsImplicit") 139 | public void before2(Object[] args) { 140 | MethodCallCounter.observe(this, "before", args); 141 | } 142 | 143 | @After(method = "varargsImplicit") 144 | public void after2(Object[] args) { 145 | MethodCallCounter.observe(this, "after", args); 146 | } 147 | 148 | @Before(method = "varargsMixed") 149 | public void before(String s, String... more) { 150 | MethodCallCounter.observe(this, "before", s, more); 151 | } 152 | 153 | @After(method = "varargsMixed") 154 | public void after(String s, String... more) { 155 | MethodCallCounter.observe(this, "after", s, more); 156 | } 157 | 158 | @Before(method = "recursive") 159 | public void before(int n) { 160 | MethodCallCounter.observe(this, "before", n); 161 | } 162 | 163 | @After(method = "recursive") 164 | public void after(int n) { 165 | MethodCallCounter.observe(this, "after", n); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/Promagent.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import static net.bytebuddy.matcher.ElementMatchers.isAbstract; 18 | import static net.bytebuddy.matcher.ElementMatchers.named; 19 | import static net.bytebuddy.matcher.ElementMatchers.not; 20 | import static net.bytebuddy.matcher.ElementMatchers.takesArgument; 21 | import static net.bytebuddy.matcher.ElementMatchers.takesArguments; 22 | 23 | import javax.management.ObjectName; 24 | import java.lang.instrument.Instrumentation; 25 | import java.lang.management.ManagementFactory; 26 | import java.nio.file.Path; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Set; 31 | import java.util.SortedSet; 32 | import java.util.TreeMap; 33 | import java.util.TreeSet; 34 | 35 | import io.promagent.agent.ClassLoaderCache; 36 | import io.promagent.hookcontext.MetricsStore; 37 | import io.promagent.internal.HookMetadata.MethodSignature; 38 | import io.promagent.internal.jmx.Exporter; 39 | import io.promagent.internal.jmx.PromagentCollectorRegistry; 40 | import net.bytebuddy.agent.builder.AgentBuilder; 41 | import net.bytebuddy.description.method.MethodDescription; 42 | import net.bytebuddy.matcher.ElementMatcher; 43 | import net.bytebuddy.matcher.ElementMatchers; 44 | 45 | public class Promagent { 46 | 47 | public static void premain(String agentArgs, Instrumentation inst) { 48 | try { 49 | PromagentCollectorRegistry registry = new PromagentCollectorRegistry(); 50 | ManagementFactory.getPlatformMBeanServer().registerMBean(new Exporter(registry), new ObjectName("io.promagent:type=exporter")); 51 | Map args = parseCmdline(agentArgs); 52 | if (args.containsKey("port")) { 53 | BuiltInServer.run(args.get("host"), args.get("port"), registry); 54 | } 55 | ClassLoaderCache classLoaderCache = ClassLoaderCache.getInstance(); 56 | List hookJars = classLoaderCache.getPerDeploymentJars(); 57 | SortedSet hookMetadata = new HookMetadataParser(hookJars).parse(); 58 | MetricsStore metricsStore = new MetricsStore(registry); 59 | Delegator.init(hookMetadata, metricsStore, classLoaderCache); 60 | printHookMetadata(hookMetadata); 61 | 62 | AgentBuilder agentBuilder = new AgentBuilder.Default(); 63 | agentBuilder = applyHooks(agentBuilder, hookMetadata, classLoaderCache); 64 | agentBuilder 65 | .disableClassFormatChanges() 66 | // .with(AgentBuilder.Listener.StreamWriting.toSystemError()) // use this to see exceptions thrown in instrumented code 67 | .with(AgentBuilder.RedefinitionStrategy.REDEFINITION) 68 | .with(AgentBuilder.TypeStrategy.Default.REDEFINE) 69 | .installOn(inst); 70 | 71 | // TODO -- the following is an experiment supporting collectors directly (in addition to hooks) 72 | // io.prometheus.client.Collector jmxCollector = (io.prometheus.client.Collector) classLoaderCache.currentClassLoader().loadClass("io.promagent.collectors.JmxCollector").newInstance(); 73 | // registry.registerNoJmx(jmxCollector); 74 | } catch (Throwable t) { 75 | t.printStackTrace(); 76 | } 77 | } 78 | 79 | /** 80 | * Add {@link ElementMatcher} for the hooks. 81 | */ 82 | private static AgentBuilder applyHooks(AgentBuilder agentBuilder, SortedSet hookMetadata, ClassLoaderCache classLoaderCache) { 83 | Map> instruments = getInstruments(hookMetadata); 84 | for (Map.Entry> entry : instruments.entrySet()) { 85 | String instrumentedClassName = entry.getKey(); 86 | Set instrumentedMethods = entry.getValue(); 87 | agentBuilder = agentBuilder 88 | .type(ElementMatchers.hasSuperType(named(instrumentedClassName))) 89 | .transform(new AgentBuilder.Transformer.ForAdvice() 90 | .include(classLoaderCache.currentClassLoader()) // must be able to load PromagentAdvice 91 | .advice(matchAnyMethodIn(instrumentedMethods), PromagentAdvice.class.getName()) 92 | ); 93 | } 94 | return agentBuilder; 95 | } 96 | 97 | /** 98 | * key: name of instrumented class or interface, value: set of instrumented methods for that class or interface 99 | */ 100 | public static Map> getInstruments(Set hooks) { 101 | Map> result = new TreeMap<>(); 102 | for (HookMetadata hookMetadata : hooks) { 103 | for (String instruments : hookMetadata.getInstruments()) { 104 | if (!result.containsKey(instruments)) { 105 | result.put(instruments, new TreeSet<>()); 106 | } 107 | result.get(instruments).addAll(hookMetadata.getMethods()); 108 | } 109 | } 110 | return result; 111 | } 112 | 113 | /** 114 | * Returns a byte buddy matcher matching any method contained in methodSignatures. 115 | */ 116 | public static ElementMatcher matchAnyMethodIn(Set methodSignatures) { 117 | ElementMatcher.Junction methodMatcher = ElementMatchers.none(); 118 | for (MethodSignature methodSignature : methodSignatures) { 119 | ElementMatcher.Junction junction = ElementMatchers 120 | .named(methodSignature.getMethodName()) 121 | .and(not(isAbstract())) 122 | .and(takesArguments(methodSignature.getParameterTypes().size())); 123 | for (int i = 0; i < methodSignature.getParameterTypes().size(); i++) { 124 | junction = junction.and(takesArgument(i, named(methodSignature.getParameterTypes().get(i)))); 125 | } 126 | methodMatcher = methodMatcher.or(junction); 127 | } 128 | return methodMatcher; 129 | } 130 | 131 | /** 132 | * Parse a comma-separated list of key/value pairs. Example: "host=localhost,port=9300" 133 | */ 134 | private static Map parseCmdline(String agentArgs) { 135 | Map result = new HashMap<>(); 136 | if (agentArgs != null) { 137 | for (String keyValueString : agentArgs.split(",")) { 138 | String[] keyValue = keyValueString.split("="); 139 | if (keyValue.length != 2) { 140 | throw new RuntimeException("Failed to parse command line arguments '" + agentArgs + "'. " + 141 | "Expecting a comma-separated list of key/value pairs, as for example 'host=localhost,port=9300'."); 142 | } 143 | result.put(keyValue[0], keyValue[1]); 144 | } 145 | } 146 | return result; 147 | } 148 | 149 | private static void printHookMetadata(SortedSet hookMetadata) { 150 | System.out.println("Promagent instrumenting the following classes or interfaces:"); 151 | for (HookMetadata m : hookMetadata) { 152 | System.out.println(m); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /promagent-framework/promagent-maven-plugin/src/main/java/io/promagent/plugin/AgentDependencies.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.plugin; 16 | 17 | import org.apache.maven.artifact.Artifact; 18 | import org.apache.maven.plugin.MojoExecutionException; 19 | import org.apache.maven.plugin.descriptor.PluginDescriptor; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.List; 24 | import java.util.Optional; 25 | import java.util.function.Predicate; 26 | 27 | import static java.util.stream.Collectors.toList; 28 | 29 | /** 30 | * Hard-coded list of JAR files that must go into the agent.jar. 31 | * Versions are resolved through Maven. 32 | * If the user adds a duplicate JAR with a different version, the plugin fails. 33 | */ 34 | class AgentDependencies { 35 | 36 | private final String pluginGroupId; 37 | private final List dependencies; 38 | 39 | /** 40 | * Hard-coded dependencies without versions (versions are resolved dynamically). 41 | */ 42 | private static class ExpectedDependency { 43 | 44 | final String groupId; 45 | final String artifactId; 46 | 47 | private ExpectedDependency(String groupId, String artifactId) { 48 | this.groupId = groupId; 49 | this.artifactId = artifactId; 50 | } 51 | } 52 | 53 | private AgentDependencies(String pluginGroupId, List dependencies) { 54 | this.pluginGroupId = pluginGroupId; 55 | this.dependencies = dependencies; 56 | } 57 | 58 | static AgentDependencies init(PluginDescriptor pluginDescriptor) throws MojoExecutionException { 59 | 60 | String pluginGroupId = pluginDescriptor.getGroupId(); 61 | String pluginArtifactId = pluginDescriptor.getArtifactId(); 62 | 63 | List expectedDependencies = Arrays.asList( 64 | new ExpectedDependency(pluginGroupId, "promagent-agent"), 65 | new ExpectedDependency(pluginGroupId, "promagent-internal"), 66 | new ExpectedDependency(pluginGroupId, "promagent-api"), 67 | new ExpectedDependency("io.prometheus", "simpleclient_common"), 68 | new ExpectedDependency("io.prometheus", "simpleclient"), 69 | new ExpectedDependency("net.bytebuddy", "byte-buddy"), 70 | new ExpectedDependency("commons-io", "commons-io") 71 | ); 72 | 73 | List actualDependencies = resolveVersions(pluginDescriptor, pluginArtifactId, expectedDependencies); 74 | failUnlessComplete(actualDependencies, expectedDependencies, pluginArtifactId); 75 | return new AgentDependencies(pluginGroupId, actualDependencies); 76 | } 77 | 78 | /** 79 | * Artifact for the promagent-agent module. 80 | */ 81 | Artifact getAgentArtifact() { 82 | return dependencies.stream() 83 | .filter(isAgent()) 84 | .findFirst() 85 | .get(); // We know it's present. 86 | } 87 | 88 | /** 89 | * Artifact for all other runtime dependencies except promagent-agent. 90 | */ 91 | List getDependencies() { 92 | return dependencies.stream() 93 | .filter(isAgent().negate()) 94 | .collect(toList()); 95 | } 96 | 97 | void assertNoConflict(Artifact artifact) throws MojoExecutionException { 98 | Optional builtInVersion = dependencies.stream() 99 | .filter(dependency -> dependency.getGroupId().equals(artifact.getGroupId())) 100 | .filter(dependency -> dependency.getArtifactId().equals(artifact.getArtifactId())) 101 | .map(Artifact::getVersion) 102 | .findFirst(); 103 | if (builtInVersion.isPresent() && ! builtInVersion.get().equals(artifact.getVersion())) { 104 | String artifactName = artifact.getGroupId() + ":" + artifact.getArtifactId(); 105 | throw new MojoExecutionException("Conflicting dependencies: Your project includes " + artifactName + 106 | " version " + artifact.getVersion() + " but the promagent-maven-plugin is built with version " + builtInVersion.get()); 107 | } 108 | } 109 | 110 | private static List resolveVersions(PluginDescriptor pluginDescriptor, String pluginArtifactId, List expectedDependencies) throws MojoExecutionException { 111 | List actualDependencies = new ArrayList<>(); 112 | for (Artifact artifact : pluginDescriptor.getArtifacts()) { 113 | if (! isExpected(artifact, expectedDependencies)) { 114 | continue; 115 | } 116 | if (isKnown(artifact, actualDependencies)) { 117 | continue; 118 | } 119 | failOnVersionConflict(artifact, actualDependencies, pluginArtifactId); 120 | actualDependencies.add(artifact); 121 | } 122 | return actualDependencies; 123 | } 124 | 125 | private Predicate isAgent() { 126 | return artifact -> artifact.getGroupId().equals(pluginGroupId) && artifact.getArtifactId().equals("promagent-agent"); 127 | } 128 | 129 | 130 | private static Predicate expectedDependencyMatcher(Artifact artifact) { 131 | return expectedDependency -> expectedDependency.groupId.equals(artifact.getGroupId()) && 132 | expectedDependency.artifactId.equals(artifact.getArtifactId()); 133 | } 134 | 135 | private static Predicate expectedDependencyMatcher(ExpectedDependency expectedDependency) { 136 | return artifact -> expectedDependencyMatcher(artifact).test(expectedDependency); 137 | } 138 | 139 | private static Predicate artifactMatcherWithoutVersion(Artifact artifact) { 140 | return other -> artifact.getGroupId().equals(other.getGroupId()) && 141 | artifact.getArtifactId().equals(other.getArtifactId()); 142 | } 143 | 144 | private static Predicate artifactMatcherWithVersion(Artifact artifact) { 145 | return other -> artifactMatcherWithoutVersion(artifact).test(other) && 146 | artifact.getVersion().equals(other.getVersion()); 147 | } 148 | 149 | private static boolean isExpected(Artifact artifact, List expectedDependencies) { 150 | return expectedDependencies.stream().anyMatch(expectedDependencyMatcher(artifact)); 151 | } 152 | 153 | private static boolean isKnown(Artifact artifact, List knownArtifacts) { 154 | return knownArtifacts.stream().anyMatch(artifactMatcherWithVersion(artifact)); 155 | } 156 | 157 | private static void failOnVersionConflict(Artifact artifact, List knownArtifacts, String pluginArtifactId) throws MojoExecutionException { 158 | Optional conflictingVersion = knownArtifacts.stream() 159 | .filter(artifactMatcherWithoutVersion(artifact)) 160 | .filter(artifactMatcherWithVersion(artifact).negate()) // same version -> not conflicting 161 | .findFirst() 162 | .map(Artifact::getVersion); 163 | if (conflictingVersion.isPresent()) { 164 | String artifactName = artifact.getGroupId() + artifact.getArtifactId(); 165 | throw new MojoExecutionException("version conflict in " + pluginArtifactId + ": " + artifactName + " found in version " + artifact.getVersion() + " and version " + conflictingVersion.get()); 166 | } 167 | } 168 | 169 | private static void failUnlessComplete(List actualDependencies, List expectedDependencies, String pluginArtifactId) throws MojoExecutionException { 170 | for (ExpectedDependency expected : expectedDependencies) { 171 | if (actualDependencies.stream().noneMatch(expectedDependencyMatcher(expected))) { 172 | String dependencyName = expected.groupId + ":" + expected.artifactId; 173 | throw new MojoExecutionException("Plugin dependency " + dependencyName + " missing. This is a bug in " + pluginArtifactId + "."); 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/test/java/io/promagent/internal/instrumentationtests/ParameterTypesTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal.instrumentationtests; 16 | 17 | import io.promagent.agent.ClassLoaderCache; 18 | import io.promagent.hookcontext.MetricsStore; 19 | import io.promagent.internal.Delegator; 20 | import io.promagent.internal.HookMetadata; 21 | import io.promagent.internal.instrumentationtests.classes.Fruit; 22 | import io.promagent.internal.instrumentationtests.classes.IParameterTypesExample; 23 | import io.promagent.internal.instrumentationtests.classes.ParameterTypesExample; 24 | import io.promagent.internal.instrumentationtests.hooks.OnlyAfterHook; 25 | import io.promagent.internal.instrumentationtests.hooks.OnlyBeforeHook; 26 | import io.promagent.internal.instrumentationtests.hooks.ParameterTypesHook; 27 | import io.promagent.internal.instrumentationtests.hooks.TwoHooks; 28 | import org.junit.jupiter.api.BeforeEach; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import java.util.*; 32 | import java.util.concurrent.ExecutorService; 33 | import java.util.concurrent.Executors; 34 | import java.util.concurrent.TimeUnit; 35 | 36 | class ParameterTypesTest { 37 | 38 | private IParameterTypesExample parameterTypesExample; 39 | 40 | private final byte b = (byte) 0x23; 41 | private final short s = (short) 42; 42 | private final int i = 7; 43 | private final long l = 3L; 44 | private final float f = 0.4f; 45 | private final double d = 0.5d; 46 | private final boolean x = true; 47 | private final char c = 'a'; 48 | 49 | private final Fruit.Orange obj1 = new Fruit.Orange(); 50 | private final Fruit.Orange obj2 = new Fruit.Orange(); 51 | private final Fruit.Orange obj3 = new Fruit.Orange(); 52 | 53 | @BeforeEach 54 | void setUp() throws Exception { 55 | SortedSet hookMetadata = Util.loadHookMetadata( 56 | ParameterTypesHook.class, 57 | TwoHooks.HookOne.class, 58 | TwoHooks.HookTwo.class, 59 | OnlyBeforeHook.class, 60 | OnlyAfterHook.class 61 | ); 62 | ClassLoaderCache classLoaderCache = Util.mockClassLoaderCache(); 63 | parameterTypesExample = Instrumentor.instrument(ParameterTypesExample.class, hookMetadata); 64 | MetricsStore metricsStore = Util.mockMetricsStore(); 65 | Delegator.init(hookMetadata, metricsStore, classLoaderCache); 66 | MethodCallCounter.reset(); 67 | } 68 | 69 | @Test 70 | void testNullArgs() { 71 | for (int n=1; n<=2; n++) { 72 | parameterTypesExample.objects(null, obj2, null); 73 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", null, obj2, null); 74 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", null, obj2, null); 75 | } 76 | } 77 | 78 | @Test 79 | void testBeforeOrAfterMissing() { 80 | for (int n=1; n<=2; n++) { 81 | parameterTypesExample.objects(obj1, obj2, obj3); 82 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", obj1, obj2, obj3); 83 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", obj1, obj2, obj3); 84 | MethodCallCounter.assertNumCalls(n, OnlyBeforeHook.class, "before", obj1, obj2, obj3); 85 | MethodCallCounter.assertNumCalls(n, OnlyAfterHook.class, "after", obj1, obj2, obj3); 86 | } 87 | } 88 | 89 | @Test 90 | void testNoArg() { 91 | for (int i=1; i<=2; i++) { 92 | parameterTypesExample.noParam(); 93 | MethodCallCounter.assertNumCalls(i, ParameterTypesHook.class, "before"); 94 | MethodCallCounter.assertNumCalls(i, ParameterTypesHook.class, "after"); 95 | } 96 | } 97 | 98 | @Test 99 | void testPrimitiveTypes() { 100 | for (int n=1; n<=2; n++) { 101 | parameterTypesExample.primitiveTypes(b, s, i, l, f, d, x, c); 102 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", b, s, i, l, f, d, x, c); 103 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", b, s, i, l, f, d, x, c); 104 | } 105 | } 106 | 107 | @Test 108 | void testBoxedTypes() { 109 | for (int n=1; n<=2; n++) { 110 | parameterTypesExample.boxedTypes(b, s, i, l, f, d, x, c); 111 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", b, s, i, l, f, d, x, c); 112 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", b, s, i, l, f, d, x, c); 113 | } 114 | } 115 | 116 | @Test 117 | void testObjects() { 118 | for (int n=1; n<=2; n++) { 119 | parameterTypesExample.objects(obj1, obj2, obj3); 120 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", obj1, obj2, obj3); 121 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before2", obj1, obj2, obj3); 122 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", obj1, obj2, obj3); 123 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after2", obj1, obj2, obj3); 124 | MethodCallCounter.assertNumCalls(0, ParameterTypesHook.class, "beforeTooLoose", obj1, obj2, obj3); 125 | MethodCallCounter.assertNumCalls(0, ParameterTypesHook.class, "beforeTooStrict", obj1, obj2, obj3); 126 | MethodCallCounter.assertNumCalls(2*n, ParameterTypesHook.class, "beforeAndAfter", obj1, obj2, obj3); 127 | } 128 | } 129 | 130 | @Test 131 | void testTwoHooks() { 132 | for (int n=1; n<=2; n++) { 133 | parameterTypesExample.objects(obj1, obj2, obj3); 134 | MethodCallCounter.assertNumCalls(n, TwoHooks.HookOne.class, "before", obj1, obj2, obj3); 135 | MethodCallCounter.assertNumCalls(n, TwoHooks.HookTwo.class, "before", obj1, obj2, obj3); 136 | } 137 | } 138 | 139 | @Test 140 | void testPrimitiveArrays() { 141 | byte[] byteArray = new byte[] {b}; 142 | short[] shortArray = new short[] {s}; 143 | int[] intArray = new int[] {i}; 144 | long[] longArray = new long[] {l}; 145 | float[] floatArray = new float[] {f}; 146 | double[] doubleArray = new double[] {d}; 147 | boolean[] booleanArray = new boolean[] {x}; 148 | char[] charArray = new char[] {c}; 149 | for (int n=1; n<=2; n++) { 150 | parameterTypesExample.primitiveArrays(byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 151 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 152 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 153 | } 154 | } 155 | 156 | @Test 157 | void testBoxedArrays() { 158 | Byte[] byteArray = new Byte[] {b}; 159 | Short[] shortArray = new Short[] {s}; 160 | Integer[] intArray = new Integer[] {i}; 161 | Long[] longArray = new Long[] {l}; 162 | Float[] floatArray = new Float[] {f}; 163 | Double[] doubleArray = new Double[] {d}; 164 | Boolean[] booleanArray = new Boolean[] {x}; 165 | Character[] charArray = new Character[] {c}; 166 | for (int n=1; n<=2; n++) { 167 | parameterTypesExample.boxedArrays(byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 168 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 169 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", byteArray, shortArray, intArray, longArray, floatArray, doubleArray, booleanArray, charArray); 170 | } 171 | } 172 | 173 | @Test 174 | void testObjectArrays() { 175 | Object[] arr1 = new Object[] {obj1, obj2}; 176 | Fruit[] arr2 = new Fruit[] {obj3}; 177 | Fruit.Orange[] arr3 = new Fruit.Orange[0]; 178 | for (int n=1; n<=2; n++) { 179 | parameterTypesExample.objectArrays(arr1, arr2, arr3); 180 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", arr1, arr2, arr3); 181 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", arr1, arr2, arr3); 182 | } 183 | } 184 | 185 | @Test 186 | void testGenerics() { 187 | List list1 = Arrays.asList(obj1, obj2); 188 | List list2 = Collections.singletonList(obj3); 189 | List list3 = new ArrayList<>(); 190 | for (int n=1; n<=2; n++) { 191 | parameterTypesExample.generics(list1, list2, list3); 192 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", list1, list2, list3); 193 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", list1, list2, list3); 194 | } 195 | } 196 | 197 | @Test 198 | void testVarargsExplicit() { 199 | for (int n=1; n<=2; n++) { 200 | parameterTypesExample.varargsExplicit(obj1, obj2, obj3); 201 | parameterTypesExample.varargsExplicit(); 202 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before"); 203 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after"); 204 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", obj1, obj2, obj3); 205 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", obj1, obj2, obj3); 206 | } 207 | } 208 | 209 | @Test 210 | void testVarargsImplicit() { 211 | Object[] arr1 = new Object[] {obj1}; 212 | Object[] arr2 = new Object[] {}; 213 | Object[] arr3 = null; 214 | for (int n=1; n<=2; n++) { 215 | parameterTypesExample.varargsImplicit(arr1); 216 | parameterTypesExample.varargsImplicit(arr2); 217 | parameterTypesExample.varargsImplicit(arr3); 218 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", arr1); 219 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", arr1); 220 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", arr2); 221 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", arr2); 222 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", arr3); 223 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", arr3); 224 | } 225 | } 226 | 227 | @Test 228 | void testVarargsMixed() { 229 | for (int n=1; n<=2; n++) { 230 | parameterTypesExample.varargsMixed("hello"); 231 | parameterTypesExample.varargsMixed("hello", "world"); 232 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", "hello"); 233 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", "hello"); 234 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "before", "hello", "world"); 235 | MethodCallCounter.assertNumCalls(n, ParameterTypesHook.class, "after", "hello", "world"); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /promagent-framework/promagent-internal/src/main/java/io/promagent/internal/Delegator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Promagent Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package io.promagent.internal; 16 | 17 | import io.promagent.agent.ClassLoaderCache; 18 | import io.promagent.annotations.After; 19 | import io.promagent.annotations.Before; 20 | import io.promagent.annotations.Returned; 21 | import io.promagent.annotations.Thrown; 22 | import io.promagent.hookcontext.MetricsStore; 23 | 24 | import java.lang.annotation.Annotation; 25 | import java.lang.reflect.Method; 26 | import java.util.*; 27 | import java.util.stream.Collectors; 28 | import java.util.stream.Stream; 29 | 30 | /** 31 | * Delegator is called from the Byte Buddy Advice, and calls the Hook's @Before and @After methods. 32 | *

33 | * TODO: This is called often, should be performance optimized, e.g. caching hook method handles, etc. 34 | */ 35 | public class Delegator { 36 | 37 | private static Delegator instance; // not thread-safe, but it is set only once in the agent's premain method. 38 | 39 | private final SortedSet hookMetadata; 40 | private final MetricsStore metricsStore; 41 | private final ClassLoaderCache classLoaderCache; 42 | private final ThreadLocal, Object>> threadLocal; 43 | 44 | private Delegator(SortedSet hookMetadata, MetricsStore metricsStore, ClassLoaderCache classLoaderCache) { 45 | this.hookMetadata = hookMetadata; 46 | this.metricsStore = metricsStore; 47 | this.classLoaderCache = classLoaderCache; 48 | this.threadLocal = ThreadLocal.withInitial(HashMap::new); 49 | } 50 | 51 | public static void init(SortedSet hookMetadata, MetricsStore metricsStore, ClassLoaderCache classLoaderCache) { 52 | instance = new Delegator(hookMetadata, metricsStore, classLoaderCache); 53 | } 54 | 55 | /** 56 | * Should be called from the Advice's @OnMethodEnter method. Returns the list of Hooks to be passed on to after() 57 | */ 58 | public static List before(Class interceptedClass, Method interceptedMethod, Object[] args) { 59 | return instance.doBefore(interceptedClass, interceptedMethod, args); 60 | } 61 | 62 | private List doBefore(Class interceptedClass, Method interceptedMethod, Object[] args) { 63 | List hookInstances = loadFromThreadLocalOrCreate(interceptedClass, interceptedMethod); 64 | for (HookInstance hookInstance : hookInstances) { 65 | invokeBefore(hookInstance.getInstance(), interceptedMethod, args); 66 | } 67 | return hookInstances; 68 | } 69 | 70 | /** 71 | * Should be called from the Advice's @OnMethodExit method. First parameter is the list of hooks returned by before() 72 | */ 73 | public static void after(List hookInstances, Method interceptedMethod, Object[] args, Object returned, Throwable thrown) { 74 | instance.doAfter(hookInstances, interceptedMethod, args, returned, thrown); 75 | } 76 | 77 | private void doAfter(List hookInstances, Method interceptedMethod, Object[] args, Object returned, Throwable thrown) { 78 | if (hookInstances != null) { 79 | for (HookInstance hookInstance : hookInstances) { 80 | invokeAfter(hookInstance.getInstance(), interceptedMethod, args, returned, thrown); 81 | if (!hookInstance.isRecursiveCall()) { 82 | threadLocal.get().remove(hookInstance.getInstance().getClass()); 83 | } 84 | } 85 | } 86 | } 87 | 88 | private List loadFromThreadLocalOrCreate(Class interceptedClass, Method interceptedMethod) { 89 | return hookMetadata.stream() 90 | .filter(hook -> classOrInterfaceMatches(interceptedClass, hook)) 91 | .filter(hook -> methodNameAndNumArgsMatch(interceptedMethod, hook)) 92 | .map(hook -> loadHookClass(hook)) 93 | .filter(hookClass -> argumentTypesMatch(hookClass, interceptedMethod)) 94 | .filter(hookClass -> ! shouldBeSkipped(hookClass)) 95 | .map(hookClass -> loadFromTheadLocalOrCreate(hookClass)) 96 | .collect(Collectors.toList()); 97 | } 98 | 99 | private static boolean classOrInterfaceMatches(Class classToBeInstrumented, HookMetadata hook) { 100 | Set classesAndInterfaces = getAllSuperClassesAndInterfaces(classToBeInstrumented); 101 | return hook.getInstruments().stream().anyMatch(classesAndInterfaces::contains); 102 | } 103 | 104 | private static Set getAllSuperClassesAndInterfaces(Class clazz) { 105 | Set result = new HashSet<>(); 106 | addAllSuperClassesAndInterfaces(clazz, result); 107 | return result; 108 | } 109 | 110 | private static void addAllSuperClassesAndInterfaces(Class clazz, Set result) { 111 | if (clazz == null) { 112 | return; 113 | } 114 | if (result.contains(clazz.getName())) { 115 | return; 116 | } 117 | result.add(clazz.getName()); 118 | for (Class ifc : clazz.getInterfaces()) { 119 | addAllSuperClassesAndInterfaces(ifc, result); 120 | } 121 | addAllSuperClassesAndInterfaces(clazz.getSuperclass(), result); 122 | } 123 | 124 | private static boolean methodNameAndNumArgsMatch(Method interceptedMethod, HookMetadata hook) { 125 | return hook.getMethods().stream() 126 | .anyMatch(instrumentedMethod -> methodNameAndNumArgsMatch(interceptedMethod, instrumentedMethod)); 127 | } 128 | 129 | private static boolean methodNameAndNumArgsMatch(Method interceptedMethod, HookMetadata.MethodSignature instrumentedMethod) { 130 | if (!interceptedMethod.getName().equals(instrumentedMethod.getMethodName())) { 131 | return false; 132 | } 133 | if (interceptedMethod.getParameterCount() != instrumentedMethod.getParameterTypes().size()) { 134 | return false; 135 | } 136 | return true; 137 | } 138 | 139 | private Class loadHookClass(HookMetadata hook) { 140 | try { 141 | return classLoaderCache.currentClassLoader().loadClass(hook.getHookClassName()); 142 | } catch (ClassNotFoundException e) { 143 | throw new HookException("Failed to load Hook class " + hook.getHookClassName() + ": " + e.getMessage(), e); 144 | } 145 | } 146 | 147 | private static boolean argumentTypesMatch(Class hookClass, Method interceptedMethod) { 148 | List before = findHookMethods(Before.class, hookClass, interceptedMethod); 149 | List after = findHookMethods(After.class, hookClass, interceptedMethod); 150 | return ! (before.isEmpty() && after.isEmpty()); 151 | } 152 | 153 | private static List findHookMethods(Class annotation, Class hookClass, Method interceptedMethod) throws HookException { 154 | return Stream.of(hookClass.getDeclaredMethods()) 155 | .filter(method -> method.isAnnotationPresent(annotation)) 156 | .filter(method -> getMethodNames(method.getAnnotation(annotation)).contains(interceptedMethod.getName())) 157 | .filter(method -> parameterTypesMatch(method, interceptedMethod)) 158 | .collect(Collectors.toList()); 159 | } 160 | 161 | private static List getMethodNames(Annotation annotation) throws HookException { 162 | if (Before.class.isAssignableFrom(annotation.getClass())) { 163 | return Arrays.asList(((Before) annotation).method()); 164 | } else if (After.class.isAssignableFrom(annotation.getClass())) { 165 | return Arrays.asList(((After) annotation).method()); 166 | } else { 167 | throw new HookException("Unsupported Annotation: @" + annotation.getClass().getSimpleName() + "."); 168 | } 169 | } 170 | 171 | // TODO: We could extend this to find the "closest" match, like in Java method calls. 172 | private static boolean parameterTypesMatch(Method hookMethod, Method interceptedMethod) { 173 | List> hookParameterTypes = stripReturnedAndThrown(hookMethod); 174 | if (hookParameterTypes.size() != interceptedMethod.getParameterCount()) { 175 | return false; 176 | } 177 | for (int i = 0; i < hookParameterTypes.size(); i++) { 178 | Class hookParam = hookParameterTypes.get(i); 179 | Class interceptedParam = interceptedMethod.getParameterTypes()[i]; 180 | if (!hookParam.equals(interceptedParam)) { 181 | return false; 182 | } 183 | } 184 | return true; 185 | } 186 | 187 | private static List> stripReturnedAndThrown(Method hookMethod) { 188 | Class[] allTypes = hookMethod.getParameterTypes(); 189 | Annotation[][] annotations = hookMethod.getParameterAnnotations(); 190 | if (allTypes.length != annotations.length) { 191 | throw new HookException("Method.getParameterAnnotations() returned an unexpected value. This is a bug in promagent."); 192 | } 193 | List> result = new ArrayList<>(); 194 | for (int i=0; i Returned.class.equals(a) || Thrown.class.equals(a))) { 198 | result.add(allTypes[i]); 199 | } 200 | } 201 | return result; 202 | } 203 | 204 | private boolean shouldBeSkipped(Class hookClass) { 205 | return hookClass.getAnnotation(io.promagent.annotations.Hook.class).skipNestedCalls() 206 | && threadLocal.get().containsKey(hookClass); 207 | } 208 | 209 | private HookInstance loadFromTheadLocalOrCreate(Class hookClass) { 210 | Object existingHookInstance = threadLocal.get().get(hookClass); 211 | if (existingHookInstance != null) { 212 | return new HookInstance(existingHookInstance, true); 213 | } else { 214 | String errMsg = "Failed to create new instance of hook " + hookClass.getSimpleName() + ": "; 215 | try { 216 | Object newHookInstance = hookClass.getConstructor(MetricsStore.class).newInstance(metricsStore); 217 | threadLocal.get().put(hookClass, newHookInstance); 218 | return new HookInstance(newHookInstance, false); 219 | } catch (NoSuchMethodException e) { 220 | throw new HookException(errMsg + "Hook classes must have a public constructor with a single parameter of type " + MetricsStore.class.getSimpleName(), e); 221 | } catch (Exception e) { 222 | throw new HookException(errMsg + e.getMessage(), e); 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * Invoke the matching Hook methods annotated with @Before 229 | */ 230 | private static void invokeBefore(Object hookInstance, Method interceptedMethod, Object[] args) throws HookException { 231 | invoke(Before.class, hookInstance, interceptedMethod, args, null, null); 232 | } 233 | 234 | /** 235 | * Invoke the matching Hook methods annotated with @After 236 | */ 237 | private static void invokeAfter(Object hookInstance, Method interceptedMethod, Object[] args, Object returned, Throwable thrown) throws HookException { 238 | invoke(After.class, hookInstance, interceptedMethod, args, returned, thrown); 239 | } 240 | 241 | private static void invoke(Class annotation, Object hookInstance, Method interceptedMethod, Object[] args, Object returned, Throwable thrown) throws HookException { 242 | if (args.length != interceptedMethod.getParameterCount()) { 243 | throw new IllegalArgumentException("Number of provided arguments is " + args.length + ", but interceptedMethod expects " + interceptedMethod.getParameterCount() + " argument(s)."); 244 | } 245 | for (Method method : findHookMethods(annotation, hookInstance.getClass(), interceptedMethod)) { 246 | try { 247 | if (!method.isAccessible()) { 248 | method.setAccessible(true); 249 | } 250 | method.invoke(hookInstance, addReturnedAndThrownArgs(method, args, returned, thrown)); 251 | } catch (Exception e) { 252 | throw new HookException("Failed to call " + method.getName() + "() on " + hookInstance.getClass().getSimpleName() + ": " + e.getMessage(), e); 253 | } 254 | } 255 | } 256 | 257 | private static Object[] addReturnedAndThrownArgs(Method hookMethod, Object[] args, Object returned, Throwable thrown) { 258 | Annotation[][] annotations = hookMethod.getParameterAnnotations(); 259 | List result = new ArrayList<>(); 260 | int arg = 0; 261 | for (Annotation[] annotation : annotations) { 262 | if (Arrays.stream(annotation) 263 | .map(Annotation::annotationType) 264 | .anyMatch(Returned.class::equals)) { 265 | result.add(returned); 266 | } else if (Arrays.stream(annotation) 267 | .map(Annotation::annotationType) 268 | .anyMatch(Thrown.class::equals)) { 269 | result.add(thrown); 270 | } else { 271 | result.add(args[arg++]); 272 | } 273 | } 274 | return result.toArray(); 275 | } 276 | } 277 | --------------------------------------------------------------------------------