diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e477d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.classpath +.project +.settings/ +target/ +.DS_Store +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0d36cf --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Sling Observability Weaving Hooks + +Library of weaving hooks to ease the process of observability in out-of-the-box code + +## Log Method Weaving Hook + +When all else fails, and you have no logs available because there are no log statements in the out-of-the-box classes, use this Log Method Weaving Hook to add a dynamic log statements + +### Usage + +Install the bundle in start level 1 and add an OSGi config for every method log you would like to add. + +Example, search for your classname, method name and amount of parameters you want to log and add an OSGi config `be.orbinson.sling.observability.weavinghooks.logmethod.LogMethodWeavingHookConfiguration~MyClass-doGet.cfg.json` + +```json +{ + "className": "my.package.MyClass", + "methodName": "doGet", + "amountOfParameters": 2 +} +``` + +To make the weaving hook work, either a framework restart of an entire java process restart is required. + +## Future + +- Add weaving hooks to create custom spans and metrics using OpenTelemetry \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f7f5d71 --- /dev/null +++ b/pom.xml @@ -0,0 +1,138 @@ + + + + 4.0.0 + + be.orbinson.sling + sling-observability-weavinghooks + bundle + 0.0.1-SNAPSHOT + + Sling Observability Weaving Hooks + + + + + org.apache.felix + maven-bundle-plugin + 5.1.9 + true + + ${basedir}/target/classes + + ${project.artifactId} + ${project.version} + asm,asm-util,asm-tree,asm-analysis + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + localhost + 8080 + admin + admin + + + + + auto-deploy + + + + org.apache.sling + sling-maven-plugin + 2.4.0 + + + http://${sling.host}:${sling.port}/apps/sling-observability-weaving-hooks/install/1 + + SlingPostServlet + ${sling.user} + ${sling.password} + + + + install-bundle + + install + + + + + + + + + + + + + org.osgi + osgi.core + 7.0.0 + provided + + + org.osgi + osgi.cmpn + 7.0.0 + provided + + + + + org.ow2.asm + asm + 9.7 + provided + + + org.ow2.asm + asm-util + 9.7 + provided + + + org.ow2.asm + asm-tree + 9.7 + provided + + + org.ow2.asm + asm-analysis + 9.7 + provided + + + + + org.slf4j + slf4j-api + 1.7.36 + provided + + + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + diff --git a/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodAdapter.java b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodAdapter.java new file mode 100644 index 0000000..36bd16c --- /dev/null +++ b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodAdapter.java @@ -0,0 +1,48 @@ +package be.orbinson.sling.observability.weavinghooks.logmethod; + +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import static org.objectweb.asm.Opcodes.*; + +/** + * Method visitor that will add a LoggerFactory log statement call of all parameters when entering a method + */ +public class LogMethodAdapter extends MethodVisitor { + + private final String classDescriptor; + private final String methodName; + private final String methodDescriptor; + private final String logLevel; + + public LogMethodAdapter(MethodVisitor mv, String classDescriptor, String methodName, String methodDescriptor, String logLevel) { + super(ASM9, mv); + this.classDescriptor = classDescriptor; + this.methodName = methodName; + this.methodDescriptor = methodDescriptor; + this.logLevel = logLevel; + } + + + @Override + public void visitCode() { + mv.visitVarInsn(ALOAD, 0); + mv.visitLdcInsn(Type.getType(classDescriptor)); + mv.visitMethodInsn(INVOKESTATIC, "org/slf4j/LoggerFactory", "getLogger", "(Ljava/lang/Class;)Lorg/slf4j/Logger;", false); + StringBuilder logString = new StringBuilder(methodName); + Type[] types = Type.getArgumentTypes(methodDescriptor); + for (int i = 0; i < types.length; i++) { + logString.append(String.format(", param_%s: <{}>", i + 1)); + } + mv.visitLdcInsn(logString.toString()); + mv.visitInsn(ICONST_0 + types.length); + mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); + for (int i = 0; i < types.length; i++) { + mv.visitInsn(DUP); + mv.visitInsn(ICONST_0 + i); + mv.visitVarInsn(ALOAD, i + 1); + mv.visitInsn(AASTORE); + } + mv.visitMethodInsn(INVOKEINTERFACE, "org/slf4j/Logger", logLevel.toLowerCase(), "(Ljava/lang/String;[Ljava/lang/Object;)V", true); + } +} diff --git a/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodClassVisitor.java b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodClassVisitor.java new file mode 100644 index 0000000..63d0137 --- /dev/null +++ b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodClassVisitor.java @@ -0,0 +1,54 @@ +package be.orbinson.sling.observability.weavinghooks.logmethod; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ClassVisitor that will add the {@link LogMethodAdapter} when the method name matches with the requested method name + */ +public class LogMethodClassVisitor extends ClassVisitor { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final String methodName; + private final String className; + private final String logLevel; + private String classDescriptor; + + public LogMethodClassVisitor(ClassVisitor cv, String className, String methodName, String logLevel) { + super(Opcodes.ASM9, cv); + this.cv = cv; + this.className = className; + this.methodName = methodName; + this.logLevel = logLevel; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + cv.visit(version, access, name, signature, superName, interfaces); + this.classDescriptor = Type.getObjectType(name).getDescriptor(); + } + + @Override + public MethodVisitor visitMethod( + int access, + String name, + String desc, + String signature, + String[] exceptions) { + + MethodVisitor mv; + mv = cv.visitMethod(access, name, desc, signature, exceptions); + if (mv != null) { + if (name.equals(methodName)) { + log.debug("Adding dynamic log method to class {} and method {}", className, methodName); + mv = new LogMethodAdapter(mv, classDescriptor, methodName, desc, logLevel); + } + } + return mv; + } +} \ No newline at end of file diff --git a/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHook.java b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHook.java new file mode 100644 index 0000000..8dd57cb --- /dev/null +++ b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHook.java @@ -0,0 +1,62 @@ +package be.orbinson.sling.observability.weavinghooks.logmethod; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.util.TraceClassVisitor; +import org.osgi.framework.hooks.weaving.WeavingHook; +import org.osgi.framework.hooks.weaving.WovenClass; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.PrintWriter; + +public class LogMethodWeavingHook implements WeavingHook { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final PrintWriter printWriter = new PrintWriter(System.out); + + private final String className; + private final String methodName; + private final String logLevel; + private final boolean enableTraceVisitor; + + public LogMethodWeavingHook(LogMethodWeavingHookConfiguration config) { + this.className = config.getClassName(); + this.methodName = config.getMethodName(); + this.logLevel = config.getLogLevel(); + this.enableTraceVisitor = config.isEnableTraceVisitor(); + } + + @Override + public void weave(WovenClass wovenClass) { + if (wovenClass.getClassName().equals(className)) { + log.debug("Adding dynamic log methods to class {}", wovenClass.getClassName()); + addLogMethodToClass(wovenClass); + addDynamicImports(wovenClass); + } + } + + private void addLogMethodToClass(WovenClass wovenClass) { + final ClassReader cr = new ClassReader(wovenClass.getBytes()); + final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + ClassVisitor logMethodClassVisitor; + if (enableTraceVisitor) { + final TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter); + logMethodClassVisitor = new LogMethodClassVisitor(tcv, className, methodName, logLevel); + } else { + logMethodClassVisitor = new LogMethodClassVisitor(cw, className, methodName, logLevel); + } + cr.accept(logMethodClassVisitor, 0); + wovenClass.setBytes(cw.toByteArray()); + } + + /** + * Required to add sl4fj as dynamic import if it would not be used in the bundle + */ + private void addDynamicImports(WovenClass wovenClass) { + wovenClass.getDynamicImports().add("org.slf4j"); + } + +} \ No newline at end of file diff --git a/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookConfiguration.java b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookConfiguration.java new file mode 100644 index 0000000..d0adf09 --- /dev/null +++ b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookConfiguration.java @@ -0,0 +1,64 @@ +package be.orbinson.sling.observability.weavinghooks.logmethod; + +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@Component( + configurationPolicy = ConfigurationPolicy.REQUIRE, + service = LogMethodWeavingHookConfiguration.class, + immediate = true +) +@Designate( + ocd = LogMethodWeavingHookConfiguration.Config.class, + factory = true +) +public class LogMethodWeavingHookConfiguration { + + @ObjectClassDefinition(name = "Sling Observability Weaving Hooks - Log Method Weaving Hook Configuration") + @interface Config { + @AttributeDefinition(description = "Class name where you want to add a dynamic log method") + String className(); + + @AttributeDefinition(description = "Method name") + String methodName(); + + @AttributeDefinition(description = "Log level") + String logLevel() default "info"; + + @AttributeDefinition(description = "Enable the trace visitor to show what the generated byte code is") + boolean enableTraceVisitor() default false; + } + + private String className; + private String methodName; + private String logLevel; + private boolean enableTraceVisitor; + + @Activate + void activate(Config config) { + this.className = config.className(); + this.methodName = config.methodName(); + this.logLevel = config.logLevel(); + this.enableTraceVisitor = config.enableTraceVisitor(); + } + + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + public String getLogLevel() { + return logLevel; + } + + public boolean isEnableTraceVisitor() { + return enableTraceVisitor; + } +} diff --git a/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookManager.java b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookManager.java new file mode 100644 index 0000000..71a67b6 --- /dev/null +++ b/src/main/java/be/orbinson/sling/observability/weavinghooks/logmethod/LogMethodWeavingHookManager.java @@ -0,0 +1,35 @@ +package be.orbinson.sling.observability.weavinghooks.logmethod; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.hooks.weaving.WeavingHook; +import org.osgi.service.component.annotations.*; + +import java.util.HashMap; +import java.util.Map; + +@Component(immediate = true, service = LogMethodWeavingHookManager.class) +public class LogMethodWeavingHookManager { + + private final Map> registrations = new HashMap<>(); + private final BundleContext bundleContext; + + @Activate + public LogMethodWeavingHookManager(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + @Reference(service = LogMethodWeavingHookConfiguration.class, cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY) + void bindLogMethodWeavingHookConfiguration(LogMethodWeavingHookConfiguration config) { + LogMethodWeavingHook weavingHook = new LogMethodWeavingHook(config); + ServiceRegistration reg = bundleContext.registerService(WeavingHook.class.getName(), weavingHook, null); + registrations.put(config, reg); + } + + void unbindLogMethodWeavingHookConfiguration(LogMethodWeavingHookConfiguration config) { + ServiceRegistration reg = registrations.get(config); + reg.unregister(); + registrations.remove(config); + } +} \ No newline at end of file diff --git a/src/test/java/be/orbinson/sling/observability/weavinghooks/test/ASMifierTester.java b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/ASMifierTester.java new file mode 100644 index 0000000..19dda29 --- /dev/null +++ b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/ASMifierTester.java @@ -0,0 +1,26 @@ +package be.orbinson.sling.observability.weavinghooks.test; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.util.ASMifier; +import org.objectweb.asm.util.TraceClassVisitor; + +import java.io.IOException; +import java.io.PrintWriter; + +public class ASMifierTester { + + /** + * Use this method to generate ASM for a specific method + **/ + @Test + public void generateASM() throws IOException { + String className = "be.orbinson.sling.observability.weavinghooks.test.MyClass"; + + PrintWriter output = new PrintWriter(System.out, true); + TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, new ASMifier(), output); + + int parsingOptions = 0; + new ClassReader(className).accept(traceClassVisitor, parsingOptions); + } +} diff --git a/src/test/java/be/orbinson/sling/observability/weavinghooks/test/MyClass.java b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/MyClass.java new file mode 100644 index 0000000..53ae6ea --- /dev/null +++ b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/MyClass.java @@ -0,0 +1,11 @@ +package be.orbinson.sling.observability.weavinghooks.test; + +import org.slf4j.LoggerFactory; + +public class MyClass { + + public boolean callMethod(String input, String secondParameter, String thirdParameter) { + LoggerFactory.getLogger(MyClass.class).info("{}", new Object[]{input, secondParameter, thirdParameter}); + return true; + } +} diff --git a/src/test/java/be/orbinson/sling/observability/weavinghooks/test/TextifierTester.java b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/TextifierTester.java new file mode 100644 index 0000000..2870342 --- /dev/null +++ b/src/test/java/be/orbinson/sling/observability/weavinghooks/test/TextifierTester.java @@ -0,0 +1,26 @@ +package be.orbinson.sling.observability.weavinghooks.test; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.util.Textifier; +import org.objectweb.asm.util.TraceClassVisitor; + +import java.io.IOException; +import java.io.PrintWriter; + +public class TextifierTester { + + /** + * Use this method to see the bytecode for a specific class + **/ + @Test + public void generateBytecode() throws IOException { + String className = "be.orbinson.osgi.log.method.weavinghook.test.MyClass"; + + PrintWriter output = new PrintWriter(System.out, true); + TraceClassVisitor traceClassVisitor = new TraceClassVisitor(null, new Textifier(), output); + + int parsingOptions = 0; + new ClassReader(className).accept(traceClassVisitor, parsingOptions); + } +}