diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 8f5c7ff..8b3bd0a 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -13,6 +13,8 @@ + + @@ -27,6 +29,8 @@ + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index d1d8003..1bdd182 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,7 +2,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 56b4fdf..825d96d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,8 @@ - + + + + diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ITraceProvider.java b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ITraceProvider.java new file mode 100644 index 0000000..db030f0 --- /dev/null +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/plugin/ITraceProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.api.plugin; + +/** + * This type can be used to connect Deep to a trace provider. This will allow Deep to create Spans dynamically based on the config of a + * tracepoint. + */ +public interface ITraceProvider { + + /** + * Create and return a new span. + * + * @param name the name of the span to create + * @return the new span + */ + ISpan createSpan(final String name); + + /** + * Get the current span from the underlying provider. + * + * @return the current active span, or {@code null} + */ + ISpan currentSpan(); + + /** + * This type describes a span for Deep to use as a dynamic Span, it gives a common interface for all {@link ITraceProvider}'s. + */ + interface ISpan extends AutoCloseable { + String name(); + + String traceId(); + + String spanId(); + } +} diff --git a/agent-api/src/main/java/com/intergral/deep/agent/api/spi/IDeepPlugin.java b/agent-api/src/main/java/com/intergral/deep/agent/api/spi/IDeepPlugin.java index ff92dc9..f29e14e 100644 --- a/agent-api/src/main/java/com/intergral/deep/agent/api/spi/IDeepPlugin.java +++ b/agent-api/src/main/java/com/intergral/deep/agent/api/spi/IDeepPlugin.java @@ -17,6 +17,7 @@ package com.intergral.deep.agent.api.spi; +import com.intergral.deep.agent.api.plugin.ITraceProvider; import com.intergral.deep.agent.api.reflection.IReflection; import com.intergral.deep.agent.api.settings.ISettings; @@ -30,6 +31,7 @@ * allow plugins to provide additional attributes to captured snapshots * {@link com.intergral.deep.agent.api.auth.IAuthProvider} - allow plugin to provide additional ways to authenticate * {@link ResourceProvider} - allow plugins to provide additional information for the resource definition + * {@link ITraceProvider} - allows plugins to connect deep to a tracing provider * * * Plugins will be instantiated via the default constructor and then the {@link #configure(ISettings, IReflection)} diff --git a/agent/pom.xml b/agent/pom.xml index 2a1e730..950c2fc 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -83,6 +83,12 @@ 1.0-SNAPSHOT provided + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + provided + diff --git a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java index 8a6c97d..653eee4 100644 --- a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java +++ b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java @@ -445,6 +445,21 @@ public IDeepPlugin get() { }; } + /** + * Get the first plugin that matches the given type. + * + * @param clazz the type of plugin we need + * @param the plugin type + * @return the discovered plugin or {@code null} if we couldn't find a plugin of this type. + */ + public T getPlugin(final Class clazz) { + final Collection plugins = this.getPlugins(clazz); + if (plugins.isEmpty()) { + return null; + } + return plugins.iterator().next(); + } + /** * Used to indicate an invalid config value. */ diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java index ac86336..2b9fa21 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java @@ -19,6 +19,8 @@ import com.intergral.deep.agent.Utils; import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; import com.intergral.deep.agent.api.plugin.LazyEvaluator; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.settings.Settings; @@ -26,9 +28,9 @@ import com.intergral.deep.agent.tracepoint.cf.CFEvaluator; import com.intergral.deep.agent.tracepoint.cf.CFFrameProcessor; import com.intergral.deep.agent.tracepoint.evaluator.EvaluatorService; -import com.intergral.deep.agent.tracepoint.inst.asm.Visitor; import com.intergral.deep.agent.types.TracePointConfig; import com.intergral.deep.agent.types.snapshot.EventSnapshot; +import java.io.Closeable; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -66,7 +68,7 @@ protected Boolean initialValue() { private static TracepointConfigService TRACEPOINT_SERVICE; private static PushService PUSH_SERVICE; - private static int offset; + private static int OFFSET; /** * Initialise the callback with the deep services. @@ -82,10 +84,12 @@ public static void init( Callback.SETTINGS = settings; Callback.TRACEPOINT_SERVICE = tracepointConfigService; Callback.PUSH_SERVICE = pushService; - if (Visitor.CALLBACK_CLASS == Callback.class) { - offset = 3; + // to avoid using the property Visitor.CALLBACK_CLASS (as this defaults to a java. class that makes tests complicated) + final String property = System.getProperty("deep.callback.class"); + if (property == null || !property.equals(Callback.class.getName())) { + Callback.OFFSET = 4; } else { - offset = 4; + Callback.OFFSET = 3; } } @@ -156,9 +160,9 @@ private static void commonCallback(final List tracepointIds, tracepointIds); StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - if (stack.length > offset) { + if (stack.length > OFFSET) { // Remove callBackProxy() + callBack() + commonCallback() + getStackTrace() entries to get to the real bp location - stack = Arrays.copyOfRange(stack, offset, stack.length); + stack = Arrays.copyOfRange(stack, OFFSET, stack.length); } final FrameProcessor frameProcessor = factory.provide(Callback.SETTINGS, @@ -314,4 +318,42 @@ public static void callBackFinally(final Set breakpointIds, // return String.format( "%s:%s", value.getRelPath(), value.getLineNo() ); // } // } + + /** + * Create a span using the tracepoint callback. + * + * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + * + * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + try { + final ITraceProvider plugin = SETTINGS.getPlugin(ITraceProvider.class); + if (plugin == null) { + return () -> { + }; + } + final ISpan span = plugin.createSpan(name); + + if (span == null) { + return () -> { + }; + } + return () -> { + try { + span.close(); + } catch (Throwable t) { + LOGGER.error("Cannot close span: {}", name, t); + } + }; + } catch (Throwable t) { + LOGGER.error("Cannot create span: {}", name, t); + return () -> { + }; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java index dc0a10e..778b836 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java @@ -295,7 +295,7 @@ public byte[] transform(final ClassLoader loader, className); // we use the method fileName as it strips all but the last of the internal class name for us final Collection matchedTracepoints = matchTracepoints(className, shortClassName); - // no breakpoints for this class or any CF classes + // no breakpoints for this class or any CF/JSP classes if (matchedTracepoints.isEmpty() && !this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) && !this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY)) { return null; diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java index 4b6799c..2ff2c8d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java @@ -17,11 +17,14 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.List; +import org.objectweb.asm.Opcodes; /** * Utilities for transforming the classes. @@ -31,6 +34,9 @@ public final class TransformerUtils { private TransformerUtils() { } + static final boolean ALLOW_LINE_NUMBERS = Boolean.getBoolean("deep.line.numbers"); + static final int LINE_OFFSET = Integer.getInteger("deep.line.offset", 20000); + static final boolean USE_SYNTHETIC = Boolean.parseBoolean(System.getProperty("deep.use.synthetic", "false")); private static final List EXCLUDE_PACKAGES = Collections.emptyList(); private static final List EXCLUDE_CONTAINS = Collections.emptyList(); @@ -124,4 +130,12 @@ public static boolean isExcludedClass(final String classname) { return false; } + + static boolean isStatic(final int acc) { + return (acc & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + } + + static boolean isAbstract(final int access) { + return (access & ACC_ABSTRACT) == ACC_ABSTRACT; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java index 79e6c91..3b55320 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java @@ -17,6 +17,9 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ASM7; import static org.objectweb.asm.Opcodes.ASM8; @@ -29,10 +32,13 @@ import static org.objectweb.asm.Opcodes.FSTORE; import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNULL; import static org.objectweb.asm.Opcodes.ILOAD; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.IRETURN; import static org.objectweb.asm.Opcodes.ISTORE; import static org.objectweb.asm.Opcodes.LLOAD; import static org.objectweb.asm.Opcodes.LSTORE; @@ -84,7 +90,6 @@ /** * This visitor is the main magic of deep. It deals with install the callbacks into the user code, based on the tracepoints. */ -@SuppressWarnings({"DuplicatedCode", "CommentedOutCode"}) public class Visitor extends ClassVisitor { // these are the local variables that we want to capture when using CF, if we try to get all locals we get verify errors. @@ -96,11 +101,15 @@ public class Visitor extends ClassVisitor { "parentPage"); private static final Logger LOGGER = LoggerFactory.getLogger(Visitor.class); private static final SkipException EXCEPTION = new SkipException(); - private static final boolean DEBUG = false; + private static final boolean DEBUG = Boolean.getBoolean("DEEP_VISITOR_DEBUG"); + private static final boolean INST_PRINTER_ENABLED = Boolean.getBoolean("DEEP_INTS_PRINTER_ENABLED"); private final Collection bps; private final boolean isCf; - private final Map> lineNos; + private final Map> lineNos = new HashMap<>(); + private final Map> methodBPs = new HashMap<>(); + + private final List mappedMethods = new ArrayList<>(); private String classname; private String superName; @@ -140,17 +149,20 @@ public Visitor(final ClassVisitor v, final Collection bps, fin super(ASM8, v); this.bps = bps; this.isCf = isCf; - lineNos = new HashMap<>(); for (final TracePointConfig bp : bps) { final long lineNo = bp.getLineNo(); - List list = lineNos.get(lineNo); + // method tracepoints do not have to define line numbers + if (lineNo != -1) { + final List list = lineNos.computeIfAbsent(lineNo, k -> new ArrayList<>()); - //noinspection Java8MapApi - if (list == null) { - list = new ArrayList<>(); - lineNos.put(lineNo, list); + list.add(bp); + } + // if we have a method name then track that + final String methodName = bp.getArg(TracePointConfig.METHOD_NAME, String.class, null); + if (methodName != null) { + final List list = this.methodBPs.computeIfAbsent(methodName, k -> new ArrayList<>()); + list.add(bp); } - list.add(bp); } } @@ -295,7 +307,7 @@ private boolean isReturn(final AbstractInsnNode node, final LabelNode start) { final int opcode = node.getOpcode(); switch (opcode) { case Opcodes.RETURN: - case Opcodes.IRETURN: + case IRETURN: case Opcodes.LRETURN: case Opcodes.FRETURN: case Opcodes.ARETURN: @@ -320,18 +332,58 @@ private boolean noneOrJustThis(final List localVariables) { return false; } + static String determineNewMethodName(final String name) { + return "$deep$" + name; + } + + /** + * We make the replaced methods private, so we can correct overridden methods. + * + * @return the access flags for the method + * @see Opcodes + */ + static int replacedMethodAcc(final boolean isStatic) { + if (TransformerUtils.USE_SYNTHETIC) { + return ACC_PRIVATE + ACC_SYNTHETIC + (isStatic ? ACC_STATIC : 0); + } + return ACC_PRIVATE + (isStatic ? ACC_STATIC : 0); + } + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + + // if the method is abstract then we cannot inject tracepoints - so skip it + if (TransformerUtils.isAbstract(access)) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + LOGGER.debug("visitMethod {} {}", classname, name); - final MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, + + // if we have a method tracepoint for this method name then rename the method + final String methodName; + final int methodAccess; + final List tracePointConfigs = this.methodBPs.get(name); + // if we have a method tracepoint for this method name then rename the method. + if (tracePointConfigs != null) { + // rename the method and make it private synthetic $deep$.. + methodName = determineNewMethodName(name); + methodAccess = replacedMethodAcc(TransformerUtils.isStatic(access)); + // record the change so we can fix them up later + this.mappedMethods.add(new MappedMethod(access, name, desc, signature, exceptions)); + } else { + methodName = name; + methodAccess = access; + } + + final MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, desc, signature, exceptions); - final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, access, name, + final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, methodAccess, methodName, desc, signature, exceptions); // MethodNode used to handle the maxes for us to make it simpler - return new MethodNode(ASM7, access, name, desc, signature, exceptions) { + return new MethodNode(ASM7, methodAccess, methodName, desc, signature, exceptions) { @Override public void visitEnd() { @@ -403,7 +455,7 @@ public void visitEnd() { // we use the return code to find the correct store code final int opStore; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opStore = ISTORE; break; case Opcodes.LRETURN: @@ -468,7 +520,7 @@ public void visitEnd() { // we use the return code to find the correct load code final int opLoad; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opLoad = ILOAD; break; case Opcodes.LRETURN: @@ -662,13 +714,11 @@ public void visitEnd() { } // Use this to debug the raw byte code instruction changes in the even the visitors fail - // if(changed) - // { - // for( AbstractInsnNode instruction : instructions ) - // { - // System.out.println(InsnPrinter.prettyPrint( instruction )); - // } - // } + if (changed && INST_PRINTER_ENABLED) { + for (AbstractInsnNode instruction : instructions) { + System.out.println(InsnPrinter.prettyPrint(instruction)); + } + } this.accept(jsrInlinerAdapter); @@ -814,6 +864,282 @@ private void processLocalVariables(final Set seenLabels, }; } + @Override + public void visitEnd() { + for (MappedMethod mappedMethod : mappedMethods) { + if (Type.getReturnType(mappedMethod.desc) == Type.VOID_TYPE) { + createMappedVoidMethod(mappedMethod); + } else { + createMappedReturnMethod(mappedMethod); + } + } + super.visitEnd(); + } + + private void createMappedReturnMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + final Type returnType = Type.getReturnType(mappedMethod.desc); + + methodVisitor.visitCode(); + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + methodVisitor.visitTryCatchBlock(label4, label5, label4, null); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(49 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label3); + } + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method with all parameters + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitVarInsn(returnType.getOpcode(ISTORE), 2 + offset); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label1); + } + + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label2); + } + + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label10); + } + + // change return/load opcodes based on return type of method + methodVisitor.visitVarInsn(returnType.getOpcode(ILOAD), 2 + offset); + methodVisitor.visitInsn(returnType.getOpcode(IRETURN)); + + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(53 + TransformerUtils.LINE_OFFSET, label4); + } + + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label6); + } + + Label label12 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label12); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label7); + } + + methodVisitor.visitVarInsn(ASTORE, 5 + offset); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(60 + TransformerUtils.LINE_OFFSET, label12); + } + + methodVisitor.visitVarInsn(ALOAD, 4 + offset); + methodVisitor.visitInsn(ATHROW); + Label label13 = new Label(); + methodVisitor.visitLabel(label13); + + // no need to visit local vars as this code is synthetic we won't be debugging it anyway + + methodVisitor.visitMaxs(1 + offset, 6 + offset); + methodVisitor.visitEnd(); + } + + private void createMappedVoidMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + + methodVisitor.visitCode(); + // try-catch labels + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(30 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + // Closable closable = Callback.span(method.name) + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + // try { + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(32 + TransformerUtils.LINE_OFFSET, label3); + } + + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + // } finally { + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + // try { + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + // closable.close() + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label1); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/io/Closeable"}, 0, null); + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label2); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + // } catch (IOException ignored) { + methodVisitor.visitVarInsn(ASTORE, 2 + offset); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(34 + TransformerUtils.LINE_OFFSET, label4); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"}); + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label12 = new Label(); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label12); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label6); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 2, new Object[]{Opcodes.TOP, "java/lang/Throwable"}, 0, null); + Label label13 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label13); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label7); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label13); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label13); + } + methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + methodVisitor.visitVarInsn(ALOAD, 3 + offset); + methodVisitor.visitInsn(ATHROW); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(42 + TransformerUtils.LINE_OFFSET, label10); + } + methodVisitor.visitFrame(Opcodes.F_CHOP, 2, null, 0, null); + methodVisitor.visitInsn(RETURN); + Label label14 = new Label(); + methodVisitor.visitLabel(label14); + methodVisitor.visitLocalVariable("this", "L" + this.classname + ";", null, label8, label14, 0); + methodVisitor.visitLocalVariable("closeable", "Ljava/io/Closeable;", null, label3, label14, 1); + methodVisitor.visitMaxs(1 + offset, 5 + offset); + methodVisitor.visitEnd(); + } + + /** * This is used in a comment on line 509 and is left in place for debugging. */ @@ -832,4 +1158,21 @@ static String prettyPrint(AbstractInsnNode insnNode) { return sw.toString(); } } + + static class MappedMethod { + + final int access; + final String name; + final String desc; + final String sign; + final String[] excp; + + public MappedMethod(final int access, final String name, final String desc, final String sign, final String[] excp) { + this.access = access; + this.name = name; + this.desc = desc; + this.sign = sign; + this.excp = excp; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java index 2ccc4b1..444f5a6 100644 --- a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java +++ b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java @@ -77,6 +77,11 @@ public class TracePointConfig { */ public static final String LOG_MSG = "log_msg"; + /** + * This is the key for the arg that defines a method tracepoint. + */ + public static final String METHOD_NAME = "method_name"; + private final String id; private final String path; private final int lineNo; diff --git a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java index 40b8b08..41bafd4 100644 --- a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java +++ b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java @@ -18,6 +18,7 @@ package java.com.intergral.deep; import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,4 +85,19 @@ public static void callBackFinally(final Set breakpointIds, final Map map) { Callback.callBackFinally(breakpointIds, map); } + + /** + * Create a span using the tracepoint callback. + * + * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + * + * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + return Callback.span(name); + } } diff --git a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin index 5f72396..fd5d00b 100644 --- a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin @@ -20,3 +20,5 @@ com.intergral.deep.agent.resource.EnvironmentResourceProvider com.intergral.deep.plugin.cf.CFPlugin com.intergral.deep.plugin.JavaPlugin com.intergral.deep.agent.api.auth.BasicAuthProvider +com.intergral.deep.plugin.PrometheusMetricsPlugin +com.intergral.deep.plugin.OtelPlugin diff --git a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java index 02b2a1f..35a851d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java @@ -191,6 +191,8 @@ void addPlugin() { assertEquals(1, settings.getPlugins().size()); + assertSame(plugin, settings.getPlugin(TestPlugin.class)); + assertNotNull(settings.getPluginByName(TestPlugin.class, TestPlugin.class.getName())); iDeepPluginIRegistration.unregister(); diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java new file mode 100644 index 0000000..c883e2f --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; +import com.intergral.deep.agent.push.PushService; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import java.io.Closeable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +class CallbackTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointConfigService tracepointConfigService = Mockito.mock(TracepointConfigService.class); + private final PushService pushService = Mockito.mock(PushService.class); + + @BeforeEach + void setUp() { + Callback.init(settings, tracepointConfigService, pushService); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn(final String name) { + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn2(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn3(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn4(final String name) throws Exception { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.doThrow(new RuntimeException("test")).when(iSpan).close(); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn5(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.doThrow(new RuntimeException("test")).when(iTraceProvider).createSpan(name); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java index 85f0bcd..e7ab14d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java @@ -24,8 +24,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import com.intergral.deep.agent.api.plugin.ITraceProvider; import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.spi.IDeepPlugin; import com.intergral.deep.agent.logging.Logger; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.push.PushUtils; @@ -168,7 +171,7 @@ void constructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -216,7 +219,7 @@ void constructor_end_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -282,7 +285,7 @@ void constructor_start_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -351,7 +354,7 @@ void setName() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -420,7 +423,7 @@ void getName_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -488,7 +491,7 @@ void getName_non_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -558,7 +561,7 @@ void errorSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -628,7 +631,7 @@ void throwSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -696,7 +699,7 @@ void catchSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -764,7 +767,7 @@ void finallySomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -836,7 +839,7 @@ void conditionalThrow() throws Exception { .pushSnapshot(argumentCaptor.capture(), Mockito.any()); assertThrows(InvocationTargetException.class, () -> method.invoke(myTest, 3, 2)); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -904,7 +907,7 @@ void breakSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -972,7 +975,7 @@ void continueSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1045,7 +1048,7 @@ void superConstructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1109,7 +1112,7 @@ void multipleTps_oneLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1178,7 +1181,7 @@ void multipleTps_nextLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1245,7 +1248,7 @@ void someFunctionWithABody() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1380,7 +1383,7 @@ void jspVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1425,7 +1428,7 @@ void luceeVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1437,4 +1440,141 @@ void luceeVisitorTest() throws Exception { assertEquals(3, stackFrame.getLineNumber()); assertEquals("testfile_cfm$cf", stackFrame.getClassName()); } + + @Test + void methodWrapperTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 151) + .withArg(TracePointConfig.METHOD_NAME, "someFunctionWithABody"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("someFunctionWithABody", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("someFunctionWithABody"); + } + + @Test + void methodWrapperVoidTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 51) + .withArg(TracePointConfig.METHOD_NAME, "setName"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("setName", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("setName"); + + } + + public static class MockTraceProvider implements IDeepPlugin, ITraceProvider { + + @Override + public ISpan createSpan(final String name) { + return new ISpan() { + @Override + public String name() { + return name; + } + + @Override + public String traceId() { + return "-1"; + } + + @Override + public String spanId() { + return "-2"; + } + + @Override + public void close() throws Exception { + + } + }; + } + + @Override + public ISpan currentSpan() { + return null; + } + } } \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java new file mode 100644 index 0000000..2ff8b10 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; +import java.io.IOException; + +/** + * This type is used as a template for generating the ASM code used in the {@link com.intergral.deep.agent.tracepoint.inst.asm.Visitor}. + * + * We use the ASM plugin for idea to simplify this process ASM Viewer. + */ +public class MockMixinTemplate { + + public void $deep$voidTemplate() { + } + + public void $deep$voidTemplate(final String arg1) { + } + + public void voidTemplate() { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public void voidTemplate(final String arg1) { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public int $deep$intTemplate() { + return -1; + } + + public int $deep$intTemplate(final String arg1) { + return -1; + } + + public int intTemplate() { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public int intTemplate(final String arg1) { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public Object $deep$objectTemplate() { + return null; + } + + public Object $deep$objectTemplate(final String arg1) { + return null; + } + + public Object objectTemplate() { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public Object objectTemplate(final String arg1) { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d952d4f..14054d6 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -28,6 +28,8 @@ + + diff --git a/deep/pom.xml b/deep/pom.xml index 1b55259..0473bb9 100644 --- a/deep/pom.xml +++ b/deep/pom.xml @@ -75,6 +75,12 @@ 1.0-SNAPSHOT compile + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + compile + diff --git a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin deleted file mode 100644 index 7f05f9c..0000000 --- a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2023 Intergral GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -com.intergral.deep.plugin.PrometheusMetricsPlugin diff --git a/examples/otel-example/pom.xml b/examples/otel-example/pom.xml new file mode 100644 index 0000000..e2950e0 --- /dev/null +++ b/examples/otel-example/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + com.intergral.deep.examples + examples + 1.0-SNAPSHOT + + + otel-example + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + + + com.intergral.deep + deep + 1.0-SNAPSHOT + compile + + + + com.intergral.deep + agent + 1.0-SNAPSHOT + provided + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + + + + \ No newline at end of file diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java new file mode 100644 index 0000000..1a3ba44 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +public class BaseTest { + + protected final Properties systemProps = System.getProperties(); + + + public String newId() { + return UUID.randomUUID().toString(); + } + + + public Map makeCharCountMap(final String str) { + final HashMap res = new HashMap(); + + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + final Integer cnt = res.get(c); + if (cnt == null) { + res.put(c, 0); + } else { + res.put(c, cnt + 1); + } + } + + return res; + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java new file mode 100644 index 0000000..62b18c5 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + + +import com.intergral.deep.Deep; +import com.intergral.deep.agent.api.IDeep; +import com.intergral.deep.agent.api.plugin.MetricDefinition; +import com.intergral.deep.agent.api.plugin.MetricDefinition.Label; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.api.DeepAPI; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This example expects the deep agent to be loaded via the javaagent vm option. + * + * See RunConfigurations for IDEA: + * + * Dynamic Load without JavaAgent + * Dynamic Load with JavaAgent + * + */ +public class Main { + + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ + public static void main(String[] args) throws Throwable { + // this is only needed in this example as we are using a local built module + // if using the dependency from maven you do not need to set the path + //noinspection DataFlowIssue + Path jarPath = Paths.get(Main.class.getResource("/").toURI()) + .getParent() + .getParent() + .getParent() + .getParent() + .resolve("agent/target/agent-1.0-SNAPSHOT.jar"); + + // Dynamically configure and attach the deep agent + Deep.config() + .setJarPath(jarPath.toAbsolutePath().toString()) + .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") + .setValue(ISettings.KEY_SERVICE_SECURE, false) + .start(); + + // different ways to get the API instance + final Deep instance = Deep.getInstance(); + System.out.println(instance.api().getVersion()); + System.out.println(instance.reflection()); + + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); + + final List labels = new ArrayList<>(); + // USe the API to create a tracepoint that will fire forever + final Map tpArgs = new HashMap<>(); + tpArgs.put("fire_count", "-1"); + tpArgs.put("log_msg", "This is a log message {this}"); + tpArgs.put("method_name", "message"); + + final OpenTelemetry openTelemetry = openTelemetry(); + + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + tpArgs, Collections.emptyList(), + Collections.singletonList( + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); + + Random random = new Random(0); + final SimpleTest ts = new SimpleTest("This is a test", 2); + final Tracer main = openTelemetry.getTracer("main"); + + //noinspection InfiniteLoopStatement + for (; ; ) { + final Span span = main.spanBuilder("loop").startSpan(); + try { + ts.message(ts.newId()); + } catch (Exception e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + //noinspection BusyWait + Thread.sleep(1000); + span.end(); + } + } + + private static OpenTelemetry openTelemetry() { + Resource resource = Resource.getDefault().toBuilder().put("service-name", "dice-server").put("service-version", "0.1.0").build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build()) + .setResource(resource) + .build(); + + SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setMeterProvider(sdkMeterProvider) + .setLoggerProvider(sdkLoggerProvider) + .setPropagators(ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .buildAndRegisterGlobal(); + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java new file mode 100644 index 0000000..760235b --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class SimpleTest extends BaseTest { + + public static Date NICE_START_DATE = new Date(); + + private final long startedAt = System.currentTimeMillis(); + private final String testName; + private final int maxExecutions; + public int cnt = 0; + private Map charCounter = new TreeMap(); + + + public SimpleTest(final String testName, final int maxExecutions) { + this.testName = testName; + this.maxExecutions = maxExecutions; + } + + + void message(final String uuid) throws Exception { + System.out.println(cnt + ":" + uuid); + cnt += 1; + + checkEnd(cnt, maxExecutions); + + final Map info = makeCharCountMap(uuid); + merge(charCounter, info); + if ((cnt % 30) == 0) { + dump(); + } + } + + + void merge(final Map charCounter, final Map newInfo) { + for (final Character c : newInfo.keySet()) { + final Integer i = newInfo.get(c); + + Integer curr = charCounter.get(c); + if (curr == null) { + charCounter.put(c, i); + } else { + charCounter.put(c, curr + i); + } + } + } + + + void dump() { + System.out.println(charCounter); + charCounter = new HashMap(); + } + + + static void checkEnd(final int val, final int max) throws Exception { + if (val > max) { + throw new Exception("Hit max executions " + val + " " + max); + } + } + + + @Override + public String toString() { + return getClass().getName() + ":" + testName + ":" + startedAt + "#" + System.identityHashCode( + this); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d483fcd..cecf403 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -38,6 +38,7 @@ agent-load dynamic-load prometheus-metrics-example + otel-example diff --git a/examples/prometheus-metrics-example/pom.xml b/examples/prometheus-metrics-example/pom.xml index bbb081e..597a3db 100644 --- a/examples/prometheus-metrics-example/pom.xml +++ b/examples/prometheus-metrics-example/pom.xml @@ -48,12 +48,6 @@ 1.0-SNAPSHOT provided - - com.intergral.deep.plugins - prometheus-metrics - 1.0-SNAPSHOT - - io.prometheus diff --git a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java index beffeff..5d3051a 100644 --- a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Throwable { .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, fireCount, Collections.emptyList(), Collections.singletonList( - new MetricDefinition("custom_metric", labels, "histogram", "this.cnt", "deep", "help message", "unit"))); + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); Random random = new Random(0); final SimpleTest ts = new SimpleTest("This is a test", 2); diff --git a/plugins/otel-plugin/pom.xml b/plugins/otel-plugin/pom.xml new file mode 100644 index 0000000..ef1bbc6 --- /dev/null +++ b/plugins/otel-plugin/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + com.intergral.deep.plugins + plugins + 1.0-SNAPSHOT + + + otel-plugin + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + com.intergral.deep + agent-api + 1.0-SNAPSHOT + compile + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-sdk + provided + + + io.opentelemetry + opentelemetry-sdk-metrics + provided + + + + \ No newline at end of file diff --git a/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java new file mode 100644 index 0000000..2c305cf --- /dev/null +++ b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.plugin; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IMetricProcessor; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.plugin.ISnapshotDecorator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.IConditional; +import com.intergral.deep.agent.api.spi.IDeepPlugin; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.HashMap; +import java.util.Map; + +/** + * This plugin provides a connection between Deep and Otel. Allowing: + * + * Metrics to be processed via Otel + * Traces to be created using Otel + * + */ +public class OtelPlugin implements IDeepPlugin, ITraceProvider, IMetricProcessor, IConditional, ISnapshotDecorator { + + @Override + public void counter(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final LongCounter build = deep.counterBuilder(name).setUnit(unit).setDescription(help).build(); + build.add(value.longValue()); + } + + @Override + public void gauge(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final ObservableDoubleMeasurement observableDoubleMeasurement = deep.gaugeBuilder(name).setUnit(unit).setDescription(help) + .buildObserver(); + observableDoubleMeasurement.record(value); + } + + @Override + public void histogram(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final DoubleHistogram build = deep.histogramBuilder(name).setUnit(unit).setDescription(help).build(); + build.record(value); + } + + @Override + public void summary(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + histogram(name, labels, namespace, help, unit, value); + } + + @Override + public ISpan createSpan(final String name) { + final TracerProvider tracerProvider = GlobalOpenTelemetry.getTracerProvider(); + if (tracerProvider == null) { + return null; + } + + final Tracer deep = tracerProvider.get("deep", DeepVersion.VERSION); + if (deep == null) { + return null; + } + + final Span span = deep.spanBuilder(name).setAttribute("deep", DeepVersion.VERSION).startSpan(); + return new ISpan() { + @Override + public String name() { + return name; + } + + + @Override + public String traceId() { + if (span != null) { + return span.getSpanContext().getTraceId(); + } + return null; + } + + @Override + public String spanId() { + if (span != null) { + return span.getSpanContext().getSpanId(); + } + return null; + } + + @Override + public void close() { + try { + span.end(); + } catch (Throwable ignored) { + + } + } + }; + } + + @Override + public ISpan currentSpan() { + final Span current = Span.current(); + final ReadableSpan readableSpan; + if (current instanceof ReadableSpan) { + readableSpan = (ReadableSpan) current; + } else { + readableSpan = null; + } + if (current == null) { + return null; + } + return new ISpan() { + @Override + public String name() { + if (readableSpan != null) { + return readableSpan.getName(); + } + return "unknown"; + } + + + @Override + public String traceId() { + return current.getSpanContext().getTraceId(); + } + + @Override + public String spanId() { + return current.getSpanContext().getSpanId(); + } + + @Override + public void close() { + throw new IllegalStateException("Cannot close external spans."); + } + }; + } + + @Override + public boolean isActive() { + try { + final Class ignored = GlobalOpenTelemetry.class; + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + final Map map = new HashMap<>(); + final ISpan iSpan = this.currentSpan(); + if (iSpan != null) { + map.put("trace_id", iSpan.traceId()); + map.put("span_id", iSpan.spanId()); + } + return Resource.create(map); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 47f43ab..9923424 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -31,6 +31,7 @@ java-plugin cf-plugin prometheus-metrics + otel-plugin com.intergral.deep.plugins diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index 266d801..f677ed5 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,10 +17,15 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.ArrayValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.common.v1.KeyValueList; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -78,6 +83,51 @@ public VariableID variableId() { }; } + public static String attributeByName(final String name, final Snapshot snapshot) { + final List attributesList = snapshot.getAttributesList(); + for (KeyValue keyValue : attributesList) { + if (keyValue.getKey().equals(name)) { + return keyValueAsString(keyValue.getValue()); + } + } + return null; + } + + private static String keyValueAsString(final AnyValue value) { + if (value.hasArrayValue()) { + final ArrayValue arrayValue = value.getArrayValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (AnyValue anyValue : arrayValue.getValuesList()) { + stringBuilder.append(keyValueAsString(anyValue)).append(","); + } + return stringBuilder.toString(); + } + if (value.hasStringValue()) { + return value.getStringValue(); + } + if (value.hasBoolValue()) { + return String.valueOf(value.getBoolValue()); + } + if (value.hasBytesValue()) { + return String.valueOf(value.getBytesValue()); + } + if (value.hasDoubleValue()) { + return String.valueOf(value.getDoubleValue()); + } + if (value.hasIntValue()) { + return String.valueOf(value.getIntValue()); + } + if (value.hasKvlistValue()) { + final KeyValueList kvlistValue = value.getKvlistValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (KeyValue anyValue : kvlistValue.getValuesList()) { + stringBuilder.append(anyValue.getKey()).append(":").append(keyValueAsString(anyValue.getValue())).append(","); + } + return stringBuilder.toString(); + } + return null; + } + public interface IVariableScan { boolean found();
* Plugins will be instantiated via the default constructor and then the {@link #configure(ISettings, IReflection)} diff --git a/agent/pom.xml b/agent/pom.xml index 2a1e730..950c2fc 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -83,6 +83,12 @@ 1.0-SNAPSHOT provided + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + provided + diff --git a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java index 8a6c97d..653eee4 100644 --- a/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java +++ b/agent/src/main/java/com/intergral/deep/agent/settings/Settings.java @@ -445,6 +445,21 @@ public IDeepPlugin get() { }; } + /** + * Get the first plugin that matches the given type. + * + * @param clazz the type of plugin we need + * @param the plugin type + * @return the discovered plugin or {@code null} if we couldn't find a plugin of this type. + */ + public T getPlugin(final Class clazz) { + final Collection plugins = this.getPlugins(clazz); + if (plugins.isEmpty()) { + return null; + } + return plugins.iterator().next(); + } + /** * Used to indicate an invalid config value. */ diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java index ac86336..2b9fa21 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/handler/Callback.java @@ -19,6 +19,8 @@ import com.intergral.deep.agent.Utils; import com.intergral.deep.agent.api.plugin.IEvaluator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; import com.intergral.deep.agent.api.plugin.LazyEvaluator; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.settings.Settings; @@ -26,9 +28,9 @@ import com.intergral.deep.agent.tracepoint.cf.CFEvaluator; import com.intergral.deep.agent.tracepoint.cf.CFFrameProcessor; import com.intergral.deep.agent.tracepoint.evaluator.EvaluatorService; -import com.intergral.deep.agent.tracepoint.inst.asm.Visitor; import com.intergral.deep.agent.types.TracePointConfig; import com.intergral.deep.agent.types.snapshot.EventSnapshot; +import java.io.Closeable; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -66,7 +68,7 @@ protected Boolean initialValue() { private static TracepointConfigService TRACEPOINT_SERVICE; private static PushService PUSH_SERVICE; - private static int offset; + private static int OFFSET; /** * Initialise the callback with the deep services. @@ -82,10 +84,12 @@ public static void init( Callback.SETTINGS = settings; Callback.TRACEPOINT_SERVICE = tracepointConfigService; Callback.PUSH_SERVICE = pushService; - if (Visitor.CALLBACK_CLASS == Callback.class) { - offset = 3; + // to avoid using the property Visitor.CALLBACK_CLASS (as this defaults to a java. class that makes tests complicated) + final String property = System.getProperty("deep.callback.class"); + if (property == null || !property.equals(Callback.class.getName())) { + Callback.OFFSET = 4; } else { - offset = 4; + Callback.OFFSET = 3; } } @@ -156,9 +160,9 @@ private static void commonCallback(final List tracepointIds, tracepointIds); StackTraceElement[] stack = Thread.currentThread().getStackTrace(); - if (stack.length > offset) { + if (stack.length > OFFSET) { // Remove callBackProxy() + callBack() + commonCallback() + getStackTrace() entries to get to the real bp location - stack = Arrays.copyOfRange(stack, offset, stack.length); + stack = Arrays.copyOfRange(stack, OFFSET, stack.length); } final FrameProcessor frameProcessor = factory.provide(Callback.SETTINGS, @@ -314,4 +318,42 @@ public static void callBackFinally(final Set breakpointIds, // return String.format( "%s:%s", value.getRelPath(), value.getLineNo() ); // } // } + + /** + * Create a span using the tracepoint callback. + * + * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + * + * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + try { + final ITraceProvider plugin = SETTINGS.getPlugin(ITraceProvider.class); + if (plugin == null) { + return () -> { + }; + } + final ISpan span = plugin.createSpan(name); + + if (span == null) { + return () -> { + }; + } + return () -> { + try { + span.close(); + } catch (Throwable t) { + LOGGER.error("Cannot close span: {}", name, t); + } + }; + } catch (Throwable t) { + LOGGER.error("Cannot create span: {}", name, t); + return () -> { + }; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java index dc0a10e..778b836 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java @@ -295,7 +295,7 @@ public byte[] transform(final ClassLoader loader, className); // we use the method fileName as it strips all but the last of the internal class name for us final Collection matchedTracepoints = matchTracepoints(className, shortClassName); - // no breakpoints for this class or any CF classes + // no breakpoints for this class or any CF/JSP classes if (matchedTracepoints.isEmpty() && !this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) && !this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY)) { return null; diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java index 4b6799c..2ff2c8d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java @@ -17,11 +17,14 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.List; +import org.objectweb.asm.Opcodes; /** * Utilities for transforming the classes. @@ -31,6 +34,9 @@ public final class TransformerUtils { private TransformerUtils() { } + static final boolean ALLOW_LINE_NUMBERS = Boolean.getBoolean("deep.line.numbers"); + static final int LINE_OFFSET = Integer.getInteger("deep.line.offset", 20000); + static final boolean USE_SYNTHETIC = Boolean.parseBoolean(System.getProperty("deep.use.synthetic", "false")); private static final List EXCLUDE_PACKAGES = Collections.emptyList(); private static final List EXCLUDE_CONTAINS = Collections.emptyList(); @@ -124,4 +130,12 @@ public static boolean isExcludedClass(final String classname) { return false; } + + static boolean isStatic(final int acc) { + return (acc & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + } + + static boolean isAbstract(final int access) { + return (access & ACC_ABSTRACT) == ACC_ABSTRACT; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java index 79e6c91..3b55320 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java @@ -17,6 +17,9 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ASM7; import static org.objectweb.asm.Opcodes.ASM8; @@ -29,10 +32,13 @@ import static org.objectweb.asm.Opcodes.FSTORE; import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNULL; import static org.objectweb.asm.Opcodes.ILOAD; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.IRETURN; import static org.objectweb.asm.Opcodes.ISTORE; import static org.objectweb.asm.Opcodes.LLOAD; import static org.objectweb.asm.Opcodes.LSTORE; @@ -84,7 +90,6 @@ /** * This visitor is the main magic of deep. It deals with install the callbacks into the user code, based on the tracepoints. */ -@SuppressWarnings({"DuplicatedCode", "CommentedOutCode"}) public class Visitor extends ClassVisitor { // these are the local variables that we want to capture when using CF, if we try to get all locals we get verify errors. @@ -96,11 +101,15 @@ public class Visitor extends ClassVisitor { "parentPage"); private static final Logger LOGGER = LoggerFactory.getLogger(Visitor.class); private static final SkipException EXCEPTION = new SkipException(); - private static final boolean DEBUG = false; + private static final boolean DEBUG = Boolean.getBoolean("DEEP_VISITOR_DEBUG"); + private static final boolean INST_PRINTER_ENABLED = Boolean.getBoolean("DEEP_INTS_PRINTER_ENABLED"); private final Collection bps; private final boolean isCf; - private final Map> lineNos; + private final Map> lineNos = new HashMap<>(); + private final Map> methodBPs = new HashMap<>(); + + private final List mappedMethods = new ArrayList<>(); private String classname; private String superName; @@ -140,17 +149,20 @@ public Visitor(final ClassVisitor v, final Collection bps, fin super(ASM8, v); this.bps = bps; this.isCf = isCf; - lineNos = new HashMap<>(); for (final TracePointConfig bp : bps) { final long lineNo = bp.getLineNo(); - List list = lineNos.get(lineNo); + // method tracepoints do not have to define line numbers + if (lineNo != -1) { + final List list = lineNos.computeIfAbsent(lineNo, k -> new ArrayList<>()); - //noinspection Java8MapApi - if (list == null) { - list = new ArrayList<>(); - lineNos.put(lineNo, list); + list.add(bp); + } + // if we have a method name then track that + final String methodName = bp.getArg(TracePointConfig.METHOD_NAME, String.class, null); + if (methodName != null) { + final List list = this.methodBPs.computeIfAbsent(methodName, k -> new ArrayList<>()); + list.add(bp); } - list.add(bp); } } @@ -295,7 +307,7 @@ private boolean isReturn(final AbstractInsnNode node, final LabelNode start) { final int opcode = node.getOpcode(); switch (opcode) { case Opcodes.RETURN: - case Opcodes.IRETURN: + case IRETURN: case Opcodes.LRETURN: case Opcodes.FRETURN: case Opcodes.ARETURN: @@ -320,18 +332,58 @@ private boolean noneOrJustThis(final List localVariables) { return false; } + static String determineNewMethodName(final String name) { + return "$deep$" + name; + } + + /** + * We make the replaced methods private, so we can correct overridden methods. + * + * @return the access flags for the method + * @see Opcodes + */ + static int replacedMethodAcc(final boolean isStatic) { + if (TransformerUtils.USE_SYNTHETIC) { + return ACC_PRIVATE + ACC_SYNTHETIC + (isStatic ? ACC_STATIC : 0); + } + return ACC_PRIVATE + (isStatic ? ACC_STATIC : 0); + } + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + + // if the method is abstract then we cannot inject tracepoints - so skip it + if (TransformerUtils.isAbstract(access)) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + LOGGER.debug("visitMethod {} {}", classname, name); - final MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, + + // if we have a method tracepoint for this method name then rename the method + final String methodName; + final int methodAccess; + final List tracePointConfigs = this.methodBPs.get(name); + // if we have a method tracepoint for this method name then rename the method. + if (tracePointConfigs != null) { + // rename the method and make it private synthetic $deep$.. + methodName = determineNewMethodName(name); + methodAccess = replacedMethodAcc(TransformerUtils.isStatic(access)); + // record the change so we can fix them up later + this.mappedMethods.add(new MappedMethod(access, name, desc, signature, exceptions)); + } else { + methodName = name; + methodAccess = access; + } + + final MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, desc, signature, exceptions); - final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, access, name, + final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, methodAccess, methodName, desc, signature, exceptions); // MethodNode used to handle the maxes for us to make it simpler - return new MethodNode(ASM7, access, name, desc, signature, exceptions) { + return new MethodNode(ASM7, methodAccess, methodName, desc, signature, exceptions) { @Override public void visitEnd() { @@ -403,7 +455,7 @@ public void visitEnd() { // we use the return code to find the correct store code final int opStore; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opStore = ISTORE; break; case Opcodes.LRETURN: @@ -468,7 +520,7 @@ public void visitEnd() { // we use the return code to find the correct load code final int opLoad; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opLoad = ILOAD; break; case Opcodes.LRETURN: @@ -662,13 +714,11 @@ public void visitEnd() { } // Use this to debug the raw byte code instruction changes in the even the visitors fail - // if(changed) - // { - // for( AbstractInsnNode instruction : instructions ) - // { - // System.out.println(InsnPrinter.prettyPrint( instruction )); - // } - // } + if (changed && INST_PRINTER_ENABLED) { + for (AbstractInsnNode instruction : instructions) { + System.out.println(InsnPrinter.prettyPrint(instruction)); + } + } this.accept(jsrInlinerAdapter); @@ -814,6 +864,282 @@ private void processLocalVariables(final Set seenLabels, }; } + @Override + public void visitEnd() { + for (MappedMethod mappedMethod : mappedMethods) { + if (Type.getReturnType(mappedMethod.desc) == Type.VOID_TYPE) { + createMappedVoidMethod(mappedMethod); + } else { + createMappedReturnMethod(mappedMethod); + } + } + super.visitEnd(); + } + + private void createMappedReturnMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + final Type returnType = Type.getReturnType(mappedMethod.desc); + + methodVisitor.visitCode(); + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + methodVisitor.visitTryCatchBlock(label4, label5, label4, null); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(49 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label3); + } + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method with all parameters + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitVarInsn(returnType.getOpcode(ISTORE), 2 + offset); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label1); + } + + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label2); + } + + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label10); + } + + // change return/load opcodes based on return type of method + methodVisitor.visitVarInsn(returnType.getOpcode(ILOAD), 2 + offset); + methodVisitor.visitInsn(returnType.getOpcode(IRETURN)); + + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(53 + TransformerUtils.LINE_OFFSET, label4); + } + + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label6); + } + + Label label12 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label12); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label7); + } + + methodVisitor.visitVarInsn(ASTORE, 5 + offset); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(60 + TransformerUtils.LINE_OFFSET, label12); + } + + methodVisitor.visitVarInsn(ALOAD, 4 + offset); + methodVisitor.visitInsn(ATHROW); + Label label13 = new Label(); + methodVisitor.visitLabel(label13); + + // no need to visit local vars as this code is synthetic we won't be debugging it anyway + + methodVisitor.visitMaxs(1 + offset, 6 + offset); + methodVisitor.visitEnd(); + } + + private void createMappedVoidMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + + methodVisitor.visitCode(); + // try-catch labels + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(30 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + // Closable closable = Callback.span(method.name) + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + // try { + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(32 + TransformerUtils.LINE_OFFSET, label3); + } + + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + // } finally { + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + // try { + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + // closable.close() + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label1); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/io/Closeable"}, 0, null); + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label2); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + // } catch (IOException ignored) { + methodVisitor.visitVarInsn(ASTORE, 2 + offset); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(34 + TransformerUtils.LINE_OFFSET, label4); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"}); + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label12 = new Label(); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label12); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label6); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 2, new Object[]{Opcodes.TOP, "java/lang/Throwable"}, 0, null); + Label label13 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label13); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label7); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label13); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label13); + } + methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + methodVisitor.visitVarInsn(ALOAD, 3 + offset); + methodVisitor.visitInsn(ATHROW); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(42 + TransformerUtils.LINE_OFFSET, label10); + } + methodVisitor.visitFrame(Opcodes.F_CHOP, 2, null, 0, null); + methodVisitor.visitInsn(RETURN); + Label label14 = new Label(); + methodVisitor.visitLabel(label14); + methodVisitor.visitLocalVariable("this", "L" + this.classname + ";", null, label8, label14, 0); + methodVisitor.visitLocalVariable("closeable", "Ljava/io/Closeable;", null, label3, label14, 1); + methodVisitor.visitMaxs(1 + offset, 5 + offset); + methodVisitor.visitEnd(); + } + + /** * This is used in a comment on line 509 and is left in place for debugging. */ @@ -832,4 +1158,21 @@ static String prettyPrint(AbstractInsnNode insnNode) { return sw.toString(); } } + + static class MappedMethod { + + final int access; + final String name; + final String desc; + final String sign; + final String[] excp; + + public MappedMethod(final int access, final String name, final String desc, final String sign, final String[] excp) { + this.access = access; + this.name = name; + this.desc = desc; + this.sign = sign; + this.excp = excp; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java index 2ccc4b1..444f5a6 100644 --- a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java +++ b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java @@ -77,6 +77,11 @@ public class TracePointConfig { */ public static final String LOG_MSG = "log_msg"; + /** + * This is the key for the arg that defines a method tracepoint. + */ + public static final String METHOD_NAME = "method_name"; + private final String id; private final String path; private final int lineNo; diff --git a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java index 40b8b08..41bafd4 100644 --- a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java +++ b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java @@ -18,6 +18,7 @@ package java.com.intergral.deep; import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,4 +85,19 @@ public static void callBackFinally(final Set breakpointIds, final Map map) { Callback.callBackFinally(breakpointIds, map); } + + /** + * Create a span using the tracepoint callback. + * + * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + * + * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + return Callback.span(name); + } } diff --git a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin index 5f72396..fd5d00b 100644 --- a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin @@ -20,3 +20,5 @@ com.intergral.deep.agent.resource.EnvironmentResourceProvider com.intergral.deep.plugin.cf.CFPlugin com.intergral.deep.plugin.JavaPlugin com.intergral.deep.agent.api.auth.BasicAuthProvider +com.intergral.deep.plugin.PrometheusMetricsPlugin +com.intergral.deep.plugin.OtelPlugin diff --git a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java index 02b2a1f..35a851d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java @@ -191,6 +191,8 @@ void addPlugin() { assertEquals(1, settings.getPlugins().size()); + assertSame(plugin, settings.getPlugin(TestPlugin.class)); + assertNotNull(settings.getPluginByName(TestPlugin.class, TestPlugin.class.getName())); iDeepPluginIRegistration.unregister(); diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java new file mode 100644 index 0000000..c883e2f --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; +import com.intergral.deep.agent.push.PushService; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import java.io.Closeable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +class CallbackTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointConfigService tracepointConfigService = Mockito.mock(TracepointConfigService.class); + private final PushService pushService = Mockito.mock(PushService.class); + + @BeforeEach + void setUp() { + Callback.init(settings, tracepointConfigService, pushService); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn(final String name) { + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn2(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn3(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn4(final String name) throws Exception { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.doThrow(new RuntimeException("test")).when(iSpan).close(); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn5(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.doThrow(new RuntimeException("test")).when(iTraceProvider).createSpan(name); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java index 85f0bcd..e7ab14d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java @@ -24,8 +24,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import com.intergral.deep.agent.api.plugin.ITraceProvider; import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.spi.IDeepPlugin; import com.intergral.deep.agent.logging.Logger; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.push.PushUtils; @@ -168,7 +171,7 @@ void constructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -216,7 +219,7 @@ void constructor_end_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -282,7 +285,7 @@ void constructor_start_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -351,7 +354,7 @@ void setName() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -420,7 +423,7 @@ void getName_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -488,7 +491,7 @@ void getName_non_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -558,7 +561,7 @@ void errorSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -628,7 +631,7 @@ void throwSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -696,7 +699,7 @@ void catchSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -764,7 +767,7 @@ void finallySomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -836,7 +839,7 @@ void conditionalThrow() throws Exception { .pushSnapshot(argumentCaptor.capture(), Mockito.any()); assertThrows(InvocationTargetException.class, () -> method.invoke(myTest, 3, 2)); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -904,7 +907,7 @@ void breakSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -972,7 +975,7 @@ void continueSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1045,7 +1048,7 @@ void superConstructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1109,7 +1112,7 @@ void multipleTps_oneLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1178,7 +1181,7 @@ void multipleTps_nextLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1245,7 +1248,7 @@ void someFunctionWithABody() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1380,7 +1383,7 @@ void jspVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1425,7 +1428,7 @@ void luceeVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1437,4 +1440,141 @@ void luceeVisitorTest() throws Exception { assertEquals(3, stackFrame.getLineNumber()); assertEquals("testfile_cfm$cf", stackFrame.getClassName()); } + + @Test + void methodWrapperTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 151) + .withArg(TracePointConfig.METHOD_NAME, "someFunctionWithABody"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("someFunctionWithABody", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("someFunctionWithABody"); + } + + @Test + void methodWrapperVoidTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 51) + .withArg(TracePointConfig.METHOD_NAME, "setName"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("setName", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("setName"); + + } + + public static class MockTraceProvider implements IDeepPlugin, ITraceProvider { + + @Override + public ISpan createSpan(final String name) { + return new ISpan() { + @Override + public String name() { + return name; + } + + @Override + public String traceId() { + return "-1"; + } + + @Override + public String spanId() { + return "-2"; + } + + @Override + public void close() throws Exception { + + } + }; + } + + @Override + public ISpan currentSpan() { + return null; + } + } } \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java new file mode 100644 index 0000000..2ff8b10 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; +import java.io.IOException; + +/** + * This type is used as a template for generating the ASM code used in the {@link com.intergral.deep.agent.tracepoint.inst.asm.Visitor}. + * + * We use the ASM plugin for idea to simplify this process ASM Viewer. + */ +public class MockMixinTemplate { + + public void $deep$voidTemplate() { + } + + public void $deep$voidTemplate(final String arg1) { + } + + public void voidTemplate() { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public void voidTemplate(final String arg1) { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public int $deep$intTemplate() { + return -1; + } + + public int $deep$intTemplate(final String arg1) { + return -1; + } + + public int intTemplate() { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public int intTemplate(final String arg1) { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public Object $deep$objectTemplate() { + return null; + } + + public Object $deep$objectTemplate(final String arg1) { + return null; + } + + public Object objectTemplate() { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public Object objectTemplate(final String arg1) { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d952d4f..14054d6 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -28,6 +28,8 @@ + + diff --git a/deep/pom.xml b/deep/pom.xml index 1b55259..0473bb9 100644 --- a/deep/pom.xml +++ b/deep/pom.xml @@ -75,6 +75,12 @@ 1.0-SNAPSHOT compile + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + compile + diff --git a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin deleted file mode 100644 index 7f05f9c..0000000 --- a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2023 Intergral GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -com.intergral.deep.plugin.PrometheusMetricsPlugin diff --git a/examples/otel-example/pom.xml b/examples/otel-example/pom.xml new file mode 100644 index 0000000..e2950e0 --- /dev/null +++ b/examples/otel-example/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + com.intergral.deep.examples + examples + 1.0-SNAPSHOT + + + otel-example + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + + + com.intergral.deep + deep + 1.0-SNAPSHOT + compile + + + + com.intergral.deep + agent + 1.0-SNAPSHOT + provided + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + + + + \ No newline at end of file diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java new file mode 100644 index 0000000..1a3ba44 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +public class BaseTest { + + protected final Properties systemProps = System.getProperties(); + + + public String newId() { + return UUID.randomUUID().toString(); + } + + + public Map makeCharCountMap(final String str) { + final HashMap res = new HashMap(); + + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + final Integer cnt = res.get(c); + if (cnt == null) { + res.put(c, 0); + } else { + res.put(c, cnt + 1); + } + } + + return res; + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java new file mode 100644 index 0000000..62b18c5 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + + +import com.intergral.deep.Deep; +import com.intergral.deep.agent.api.IDeep; +import com.intergral.deep.agent.api.plugin.MetricDefinition; +import com.intergral.deep.agent.api.plugin.MetricDefinition.Label; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.api.DeepAPI; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This example expects the deep agent to be loaded via the javaagent vm option. + * + * See RunConfigurations for IDEA: + * + * Dynamic Load without JavaAgent + * Dynamic Load with JavaAgent + * + */ +public class Main { + + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ + public static void main(String[] args) throws Throwable { + // this is only needed in this example as we are using a local built module + // if using the dependency from maven you do not need to set the path + //noinspection DataFlowIssue + Path jarPath = Paths.get(Main.class.getResource("/").toURI()) + .getParent() + .getParent() + .getParent() + .getParent() + .resolve("agent/target/agent-1.0-SNAPSHOT.jar"); + + // Dynamically configure and attach the deep agent + Deep.config() + .setJarPath(jarPath.toAbsolutePath().toString()) + .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") + .setValue(ISettings.KEY_SERVICE_SECURE, false) + .start(); + + // different ways to get the API instance + final Deep instance = Deep.getInstance(); + System.out.println(instance.api().getVersion()); + System.out.println(instance.reflection()); + + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); + + final List labels = new ArrayList<>(); + // USe the API to create a tracepoint that will fire forever + final Map tpArgs = new HashMap<>(); + tpArgs.put("fire_count", "-1"); + tpArgs.put("log_msg", "This is a log message {this}"); + tpArgs.put("method_name", "message"); + + final OpenTelemetry openTelemetry = openTelemetry(); + + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + tpArgs, Collections.emptyList(), + Collections.singletonList( + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); + + Random random = new Random(0); + final SimpleTest ts = new SimpleTest("This is a test", 2); + final Tracer main = openTelemetry.getTracer("main"); + + //noinspection InfiniteLoopStatement + for (; ; ) { + final Span span = main.spanBuilder("loop").startSpan(); + try { + ts.message(ts.newId()); + } catch (Exception e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + //noinspection BusyWait + Thread.sleep(1000); + span.end(); + } + } + + private static OpenTelemetry openTelemetry() { + Resource resource = Resource.getDefault().toBuilder().put("service-name", "dice-server").put("service-version", "0.1.0").build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build()) + .setResource(resource) + .build(); + + SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setMeterProvider(sdkMeterProvider) + .setLoggerProvider(sdkLoggerProvider) + .setPropagators(ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .buildAndRegisterGlobal(); + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java new file mode 100644 index 0000000..760235b --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class SimpleTest extends BaseTest { + + public static Date NICE_START_DATE = new Date(); + + private final long startedAt = System.currentTimeMillis(); + private final String testName; + private final int maxExecutions; + public int cnt = 0; + private Map charCounter = new TreeMap(); + + + public SimpleTest(final String testName, final int maxExecutions) { + this.testName = testName; + this.maxExecutions = maxExecutions; + } + + + void message(final String uuid) throws Exception { + System.out.println(cnt + ":" + uuid); + cnt += 1; + + checkEnd(cnt, maxExecutions); + + final Map info = makeCharCountMap(uuid); + merge(charCounter, info); + if ((cnt % 30) == 0) { + dump(); + } + } + + + void merge(final Map charCounter, final Map newInfo) { + for (final Character c : newInfo.keySet()) { + final Integer i = newInfo.get(c); + + Integer curr = charCounter.get(c); + if (curr == null) { + charCounter.put(c, i); + } else { + charCounter.put(c, curr + i); + } + } + } + + + void dump() { + System.out.println(charCounter); + charCounter = new HashMap(); + } + + + static void checkEnd(final int val, final int max) throws Exception { + if (val > max) { + throw new Exception("Hit max executions " + val + " " + max); + } + } + + + @Override + public String toString() { + return getClass().getName() + ":" + testName + ":" + startedAt + "#" + System.identityHashCode( + this); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d483fcd..cecf403 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -38,6 +38,7 @@ agent-load dynamic-load prometheus-metrics-example + otel-example diff --git a/examples/prometheus-metrics-example/pom.xml b/examples/prometheus-metrics-example/pom.xml index bbb081e..597a3db 100644 --- a/examples/prometheus-metrics-example/pom.xml +++ b/examples/prometheus-metrics-example/pom.xml @@ -48,12 +48,6 @@ 1.0-SNAPSHOT provided - - com.intergral.deep.plugins - prometheus-metrics - 1.0-SNAPSHOT - - io.prometheus diff --git a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java index beffeff..5d3051a 100644 --- a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Throwable { .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, fireCount, Collections.emptyList(), Collections.singletonList( - new MetricDefinition("custom_metric", labels, "histogram", "this.cnt", "deep", "help message", "unit"))); + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); Random random = new Random(0); final SimpleTest ts = new SimpleTest("This is a test", 2); diff --git a/plugins/otel-plugin/pom.xml b/plugins/otel-plugin/pom.xml new file mode 100644 index 0000000..ef1bbc6 --- /dev/null +++ b/plugins/otel-plugin/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + com.intergral.deep.plugins + plugins + 1.0-SNAPSHOT + + + otel-plugin + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + com.intergral.deep + agent-api + 1.0-SNAPSHOT + compile + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-sdk + provided + + + io.opentelemetry + opentelemetry-sdk-metrics + provided + + + + \ No newline at end of file diff --git a/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java new file mode 100644 index 0000000..2c305cf --- /dev/null +++ b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.plugin; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IMetricProcessor; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.plugin.ISnapshotDecorator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.IConditional; +import com.intergral.deep.agent.api.spi.IDeepPlugin; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.HashMap; +import java.util.Map; + +/** + * This plugin provides a connection between Deep and Otel. Allowing: + * + * Metrics to be processed via Otel + * Traces to be created using Otel + * + */ +public class OtelPlugin implements IDeepPlugin, ITraceProvider, IMetricProcessor, IConditional, ISnapshotDecorator { + + @Override + public void counter(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final LongCounter build = deep.counterBuilder(name).setUnit(unit).setDescription(help).build(); + build.add(value.longValue()); + } + + @Override + public void gauge(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final ObservableDoubleMeasurement observableDoubleMeasurement = deep.gaugeBuilder(name).setUnit(unit).setDescription(help) + .buildObserver(); + observableDoubleMeasurement.record(value); + } + + @Override + public void histogram(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final DoubleHistogram build = deep.histogramBuilder(name).setUnit(unit).setDescription(help).build(); + build.record(value); + } + + @Override + public void summary(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + histogram(name, labels, namespace, help, unit, value); + } + + @Override + public ISpan createSpan(final String name) { + final TracerProvider tracerProvider = GlobalOpenTelemetry.getTracerProvider(); + if (tracerProvider == null) { + return null; + } + + final Tracer deep = tracerProvider.get("deep", DeepVersion.VERSION); + if (deep == null) { + return null; + } + + final Span span = deep.spanBuilder(name).setAttribute("deep", DeepVersion.VERSION).startSpan(); + return new ISpan() { + @Override + public String name() { + return name; + } + + + @Override + public String traceId() { + if (span != null) { + return span.getSpanContext().getTraceId(); + } + return null; + } + + @Override + public String spanId() { + if (span != null) { + return span.getSpanContext().getSpanId(); + } + return null; + } + + @Override + public void close() { + try { + span.end(); + } catch (Throwable ignored) { + + } + } + }; + } + + @Override + public ISpan currentSpan() { + final Span current = Span.current(); + final ReadableSpan readableSpan; + if (current instanceof ReadableSpan) { + readableSpan = (ReadableSpan) current; + } else { + readableSpan = null; + } + if (current == null) { + return null; + } + return new ISpan() { + @Override + public String name() { + if (readableSpan != null) { + return readableSpan.getName(); + } + return "unknown"; + } + + + @Override + public String traceId() { + return current.getSpanContext().getTraceId(); + } + + @Override + public String spanId() { + return current.getSpanContext().getSpanId(); + } + + @Override + public void close() { + throw new IllegalStateException("Cannot close external spans."); + } + }; + } + + @Override + public boolean isActive() { + try { + final Class ignored = GlobalOpenTelemetry.class; + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + final Map map = new HashMap<>(); + final ISpan iSpan = this.currentSpan(); + if (iSpan != null) { + map.put("trace_id", iSpan.traceId()); + map.put("span_id", iSpan.spanId()); + } + return Resource.create(map); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 47f43ab..9923424 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -31,6 +31,7 @@ java-plugin cf-plugin prometheus-metrics + otel-plugin com.intergral.deep.plugins diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index 266d801..f677ed5 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,10 +17,15 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.ArrayValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.common.v1.KeyValueList; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -78,6 +83,51 @@ public VariableID variableId() { }; } + public static String attributeByName(final String name, final Snapshot snapshot) { + final List attributesList = snapshot.getAttributesList(); + for (KeyValue keyValue : attributesList) { + if (keyValue.getKey().equals(name)) { + return keyValueAsString(keyValue.getValue()); + } + } + return null; + } + + private static String keyValueAsString(final AnyValue value) { + if (value.hasArrayValue()) { + final ArrayValue arrayValue = value.getArrayValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (AnyValue anyValue : arrayValue.getValuesList()) { + stringBuilder.append(keyValueAsString(anyValue)).append(","); + } + return stringBuilder.toString(); + } + if (value.hasStringValue()) { + return value.getStringValue(); + } + if (value.hasBoolValue()) { + return String.valueOf(value.getBoolValue()); + } + if (value.hasBytesValue()) { + return String.valueOf(value.getBytesValue()); + } + if (value.hasDoubleValue()) { + return String.valueOf(value.getDoubleValue()); + } + if (value.hasIntValue()) { + return String.valueOf(value.getIntValue()); + } + if (value.hasKvlistValue()) { + final KeyValueList kvlistValue = value.getKvlistValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (KeyValue anyValue : kvlistValue.getValuesList()) { + stringBuilder.append(anyValue.getKey()).append(":").append(keyValueAsString(anyValue.getValue())).append(","); + } + return stringBuilder.toString(); + } + return null; + } + public interface IVariableScan { boolean found();
+ * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + *
+ * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + try { + final ITraceProvider plugin = SETTINGS.getPlugin(ITraceProvider.class); + if (plugin == null) { + return () -> { + }; + } + final ISpan span = plugin.createSpan(name); + + if (span == null) { + return () -> { + }; + } + return () -> { + try { + span.close(); + } catch (Throwable t) { + LOGGER.error("Cannot close span: {}", name, t); + } + }; + } catch (Throwable t) { + LOGGER.error("Cannot create span: {}", name, t); + return () -> { + }; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java index dc0a10e..778b836 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/TracepointInstrumentationService.java @@ -295,7 +295,7 @@ public byte[] transform(final ClassLoader loader, className); // we use the method fileName as it strips all but the last of the internal class name for us final Collection matchedTracepoints = matchTracepoints(className, shortClassName); - // no breakpoints for this class or any CF classes + // no breakpoints for this class or any CF/JSP classes if (matchedTracepoints.isEmpty() && !this.classPrefixTracepoints.containsKey(CFM_CLASS_KEY) && !this.classPrefixTracepoints.containsKey(JSP_CLASS_KEY)) { return null; diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java index 4b6799c..2ff2c8d 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/TransformerUtils.java @@ -17,11 +17,14 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; + import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.List; +import org.objectweb.asm.Opcodes; /** * Utilities for transforming the classes. @@ -31,6 +34,9 @@ public final class TransformerUtils { private TransformerUtils() { } + static final boolean ALLOW_LINE_NUMBERS = Boolean.getBoolean("deep.line.numbers"); + static final int LINE_OFFSET = Integer.getInteger("deep.line.offset", 20000); + static final boolean USE_SYNTHETIC = Boolean.parseBoolean(System.getProperty("deep.use.synthetic", "false")); private static final List EXCLUDE_PACKAGES = Collections.emptyList(); private static final List EXCLUDE_CONTAINS = Collections.emptyList(); @@ -124,4 +130,12 @@ public static boolean isExcludedClass(final String classname) { return false; } + + static boolean isStatic(final int acc) { + return (acc & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC; + } + + static boolean isAbstract(final int access) { + return (access & ACC_ABSTRACT) == ACC_ABSTRACT; + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java index 79e6c91..3b55320 100644 --- a/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java +++ b/agent/src/main/java/com/intergral/deep/agent/tracepoint/inst/asm/Visitor.java @@ -17,6 +17,9 @@ package com.intergral.deep.agent.tracepoint.inst.asm; +import static org.objectweb.asm.Opcodes.ACC_PRIVATE; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ASM7; import static org.objectweb.asm.Opcodes.ASM8; @@ -29,10 +32,13 @@ import static org.objectweb.asm.Opcodes.FSTORE; import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GOTO; +import static org.objectweb.asm.Opcodes.IFNULL; import static org.objectweb.asm.Opcodes.ILOAD; +import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; import static org.objectweb.asm.Opcodes.INVOKESPECIAL; import static org.objectweb.asm.Opcodes.INVOKESTATIC; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; +import static org.objectweb.asm.Opcodes.IRETURN; import static org.objectweb.asm.Opcodes.ISTORE; import static org.objectweb.asm.Opcodes.LLOAD; import static org.objectweb.asm.Opcodes.LSTORE; @@ -84,7 +90,6 @@ /** * This visitor is the main magic of deep. It deals with install the callbacks into the user code, based on the tracepoints. */ -@SuppressWarnings({"DuplicatedCode", "CommentedOutCode"}) public class Visitor extends ClassVisitor { // these are the local variables that we want to capture when using CF, if we try to get all locals we get verify errors. @@ -96,11 +101,15 @@ public class Visitor extends ClassVisitor { "parentPage"); private static final Logger LOGGER = LoggerFactory.getLogger(Visitor.class); private static final SkipException EXCEPTION = new SkipException(); - private static final boolean DEBUG = false; + private static final boolean DEBUG = Boolean.getBoolean("DEEP_VISITOR_DEBUG"); + private static final boolean INST_PRINTER_ENABLED = Boolean.getBoolean("DEEP_INTS_PRINTER_ENABLED"); private final Collection bps; private final boolean isCf; - private final Map> lineNos; + private final Map> lineNos = new HashMap<>(); + private final Map> methodBPs = new HashMap<>(); + + private final List mappedMethods = new ArrayList<>(); private String classname; private String superName; @@ -140,17 +149,20 @@ public Visitor(final ClassVisitor v, final Collection bps, fin super(ASM8, v); this.bps = bps; this.isCf = isCf; - lineNos = new HashMap<>(); for (final TracePointConfig bp : bps) { final long lineNo = bp.getLineNo(); - List list = lineNos.get(lineNo); + // method tracepoints do not have to define line numbers + if (lineNo != -1) { + final List list = lineNos.computeIfAbsent(lineNo, k -> new ArrayList<>()); - //noinspection Java8MapApi - if (list == null) { - list = new ArrayList<>(); - lineNos.put(lineNo, list); + list.add(bp); + } + // if we have a method name then track that + final String methodName = bp.getArg(TracePointConfig.METHOD_NAME, String.class, null); + if (methodName != null) { + final List list = this.methodBPs.computeIfAbsent(methodName, k -> new ArrayList<>()); + list.add(bp); } - list.add(bp); } } @@ -295,7 +307,7 @@ private boolean isReturn(final AbstractInsnNode node, final LabelNode start) { final int opcode = node.getOpcode(); switch (opcode) { case Opcodes.RETURN: - case Opcodes.IRETURN: + case IRETURN: case Opcodes.LRETURN: case Opcodes.FRETURN: case Opcodes.ARETURN: @@ -320,18 +332,58 @@ private boolean noneOrJustThis(final List localVariables) { return false; } + static String determineNewMethodName(final String name) { + return "$deep$" + name; + } + + /** + * We make the replaced methods private, so we can correct overridden methods. + * + * @return the access flags for the method + * @see Opcodes + */ + static int replacedMethodAcc(final boolean isStatic) { + if (TransformerUtils.USE_SYNTHETIC) { + return ACC_PRIVATE + ACC_SYNTHETIC + (isStatic ? ACC_STATIC : 0); + } + return ACC_PRIVATE + (isStatic ? ACC_STATIC : 0); + } + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + + // if the method is abstract then we cannot inject tracepoints - so skip it + if (TransformerUtils.isAbstract(access)) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + LOGGER.debug("visitMethod {} {}", classname, name); - final MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, + + // if we have a method tracepoint for this method name then rename the method + final String methodName; + final int methodAccess; + final List tracePointConfigs = this.methodBPs.get(name); + // if we have a method tracepoint for this method name then rename the method. + if (tracePointConfigs != null) { + // rename the method and make it private synthetic $deep$.. + methodName = determineNewMethodName(name); + methodAccess = replacedMethodAcc(TransformerUtils.isStatic(access)); + // record the change so we can fix them up later + this.mappedMethods.add(new MappedMethod(access, name, desc, signature, exceptions)); + } else { + methodName = name; + methodAccess = access; + } + + final MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, desc, signature, exceptions); - final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, access, name, + final JSRInlinerAdapter jsrInlinerAdapter = new JSRInlinerAdapter(methodVisitor, methodAccess, methodName, desc, signature, exceptions); // MethodNode used to handle the maxes for us to make it simpler - return new MethodNode(ASM7, access, name, desc, signature, exceptions) { + return new MethodNode(ASM7, methodAccess, methodName, desc, signature, exceptions) { @Override public void visitEnd() { @@ -403,7 +455,7 @@ public void visitEnd() { // we use the return code to find the correct store code final int opStore; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opStore = ISTORE; break; case Opcodes.LRETURN: @@ -468,7 +520,7 @@ public void visitEnd() { // we use the return code to find the correct load code final int opLoad; switch (node.getOpcode()) { - case Opcodes.IRETURN: + case IRETURN: opLoad = ILOAD; break; case Opcodes.LRETURN: @@ -662,13 +714,11 @@ public void visitEnd() { } // Use this to debug the raw byte code instruction changes in the even the visitors fail - // if(changed) - // { - // for( AbstractInsnNode instruction : instructions ) - // { - // System.out.println(InsnPrinter.prettyPrint( instruction )); - // } - // } + if (changed && INST_PRINTER_ENABLED) { + for (AbstractInsnNode instruction : instructions) { + System.out.println(InsnPrinter.prettyPrint(instruction)); + } + } this.accept(jsrInlinerAdapter); @@ -814,6 +864,282 @@ private void processLocalVariables(final Set seenLabels, }; } + @Override + public void visitEnd() { + for (MappedMethod mappedMethod : mappedMethods) { + if (Type.getReturnType(mappedMethod.desc) == Type.VOID_TYPE) { + createMappedVoidMethod(mappedMethod); + } else { + createMappedReturnMethod(mappedMethod); + } + } + super.visitEnd(); + } + + private void createMappedReturnMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + final Type returnType = Type.getReturnType(mappedMethod.desc); + + methodVisitor.visitCode(); + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + methodVisitor.visitTryCatchBlock(label4, label5, label4, null); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(49 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label3); + } + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method with all parameters + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitVarInsn(returnType.getOpcode(ISTORE), 2 + offset); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label1); + } + + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label2); + } + + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(51 + TransformerUtils.LINE_OFFSET, label10); + } + + // change return/load opcodes based on return type of method + methodVisitor.visitVarInsn(returnType.getOpcode(ILOAD), 2 + offset); + methodVisitor.visitInsn(returnType.getOpcode(IRETURN)); + + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(53 + TransformerUtils.LINE_OFFSET, label4); + } + + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(54 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(55 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(59 + TransformerUtils.LINE_OFFSET, label6); + } + + Label label12 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label12); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(57 + TransformerUtils.LINE_OFFSET, label7); + } + + methodVisitor.visitVarInsn(ASTORE, 5 + offset); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(60 + TransformerUtils.LINE_OFFSET, label12); + } + + methodVisitor.visitVarInsn(ALOAD, 4 + offset); + methodVisitor.visitInsn(ATHROW); + Label label13 = new Label(); + methodVisitor.visitLabel(label13); + + // no need to visit local vars as this code is synthetic we won't be debugging it anyway + + methodVisitor.visitMaxs(1 + offset, 6 + offset); + methodVisitor.visitEnd(); + } + + private void createMappedVoidMethod(final MappedMethod mappedMethod) { + final MethodVisitor methodVisitor = this.cv.visitMethod(mappedMethod.access, mappedMethod.name, mappedMethod.desc, mappedMethod.sign, + mappedMethod.excp); + final Type[] argumentTypes = Type.getArgumentTypes(mappedMethod.desc); + final int offset = argumentTypes.length; + + methodVisitor.visitCode(); + // try-catch labels + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + methodVisitor.visitTryCatchBlock(label0, label1, label2, "java/io/IOException"); + Label label3 = new Label(); + Label label4 = new Label(); + methodVisitor.visitTryCatchBlock(label3, label0, label4, null); + Label label5 = new Label(); + Label label6 = new Label(); + Label label7 = new Label(); + methodVisitor.visitTryCatchBlock(label5, label6, label7, "java/io/IOException"); + + Label label8 = new Label(); + methodVisitor.visitLabel(label8); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(30 + TransformerUtils.LINE_OFFSET, label8); + } + methodVisitor.visitLdcInsn(mappedMethod.name); + // Closable closable = Callback.span(method.name) + methodVisitor.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CALLBACK_CLASS), "span", + "(Ljava/lang/String;)Ljava/io/Closeable;", false); + methodVisitor.visitVarInsn(ASTORE, 1 + offset); + // try { + methodVisitor.visitLabel(label3); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(32 + TransformerUtils.LINE_OFFSET, label3); + } + + // load 'this' + methodVisitor.visitVarInsn(ALOAD, 0); + // load all the parameters + for (int i = 0; i < argumentTypes.length; i++) { + final Type argumentType = argumentTypes[i]; + methodVisitor.visitVarInsn(argumentType.getOpcode(ILOAD), i + 1); + } + // call original method + methodVisitor.visitMethodInsn(INVOKEVIRTUAL, this.classname, determineNewMethodName(mappedMethod.name), mappedMethod.desc, + false); + methodVisitor.visitLabel(label0); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label0); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label1); + // } finally { + Label label9 = new Label(); + methodVisitor.visitLabel(label9); + // try { + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label9); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + // closable.close() + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label1); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label1); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/io/Closeable"}, 0, null); + Label label10 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label2); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label2); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + // } catch (IOException ignored) { + methodVisitor.visitVarInsn(ASTORE, 2 + offset); + Label label11 = new Label(); + methodVisitor.visitLabel(label11); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label11); + } + methodVisitor.visitJumpInsn(GOTO, label10); + methodVisitor.visitLabel(label4); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(34 + TransformerUtils.LINE_OFFSET, label4); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"}); + methodVisitor.visitVarInsn(ASTORE, 3 + offset); + methodVisitor.visitLabel(label5); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(35 + TransformerUtils.LINE_OFFSET, label5); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitJumpInsn(IFNULL, label6); + Label label12 = new Label(); + methodVisitor.visitLabel(label12); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(36 + TransformerUtils.LINE_OFFSET, label12); + } + methodVisitor.visitVarInsn(ALOAD, 1 + offset); + methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/io/Closeable", "close", "()V", true); + methodVisitor.visitLabel(label6); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(40 + TransformerUtils.LINE_OFFSET, label6); + } + methodVisitor.visitFrame(Opcodes.F_APPEND, 2, new Object[]{Opcodes.TOP, "java/lang/Throwable"}, 0, null); + Label label13 = new Label(); + methodVisitor.visitJumpInsn(GOTO, label13); + methodVisitor.visitLabel(label7); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(38 + TransformerUtils.LINE_OFFSET, label7); + } + methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/io/IOException"}); + methodVisitor.visitVarInsn(ASTORE, 4 + offset); + methodVisitor.visitLabel(label13); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(41 + TransformerUtils.LINE_OFFSET, label13); + } + methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + methodVisitor.visitVarInsn(ALOAD, 3 + offset); + methodVisitor.visitInsn(ATHROW); + methodVisitor.visitLabel(label10); + if (TransformerUtils.ALLOW_LINE_NUMBERS) { + methodVisitor.visitLineNumber(42 + TransformerUtils.LINE_OFFSET, label10); + } + methodVisitor.visitFrame(Opcodes.F_CHOP, 2, null, 0, null); + methodVisitor.visitInsn(RETURN); + Label label14 = new Label(); + methodVisitor.visitLabel(label14); + methodVisitor.visitLocalVariable("this", "L" + this.classname + ";", null, label8, label14, 0); + methodVisitor.visitLocalVariable("closeable", "Ljava/io/Closeable;", null, label3, label14, 1); + methodVisitor.visitMaxs(1 + offset, 5 + offset); + methodVisitor.visitEnd(); + } + + /** * This is used in a comment on line 509 and is left in place for debugging. */ @@ -832,4 +1158,21 @@ static String prettyPrint(AbstractInsnNode insnNode) { return sw.toString(); } } + + static class MappedMethod { + + final int access; + final String name; + final String desc; + final String sign; + final String[] excp; + + public MappedMethod(final int access, final String name, final String desc, final String sign, final String[] excp) { + this.access = access; + this.name = name; + this.desc = desc; + this.sign = sign; + this.excp = excp; + } + } } diff --git a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java index 2ccc4b1..444f5a6 100644 --- a/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java +++ b/agent/src/main/java/com/intergral/deep/agent/types/TracePointConfig.java @@ -77,6 +77,11 @@ public class TracePointConfig { */ public static final String LOG_MSG = "log_msg"; + /** + * This is the key for the arg that defines a method tracepoint. + */ + public static final String METHOD_NAME = "method_name"; + private final String id; private final String path; private final int lineNo; diff --git a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java index 40b8b08..41bafd4 100644 --- a/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java +++ b/agent/src/main/java/java/com/intergral/deep/ProxyCallback.java @@ -18,6 +18,7 @@ package java.com.intergral.deep; import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,4 +85,19 @@ public static void callBackFinally(final Set breakpointIds, final Map map) { Callback.callBackFinally(breakpointIds, map); } + + /** + * Create a span using the tracepoint callback. + * + * This method will Always return a closable. This way the injected code never deals with anything but calling close. Even if close + * doesn't do anything. + * + * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + return Callback.span(name); + } } diff --git a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin index 5f72396..fd5d00b 100644 --- a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin @@ -20,3 +20,5 @@ com.intergral.deep.agent.resource.EnvironmentResourceProvider com.intergral.deep.plugin.cf.CFPlugin com.intergral.deep.plugin.JavaPlugin com.intergral.deep.agent.api.auth.BasicAuthProvider +com.intergral.deep.plugin.PrometheusMetricsPlugin +com.intergral.deep.plugin.OtelPlugin diff --git a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java index 02b2a1f..35a851d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java @@ -191,6 +191,8 @@ void addPlugin() { assertEquals(1, settings.getPlugins().size()); + assertSame(plugin, settings.getPlugin(TestPlugin.class)); + assertNotNull(settings.getPluginByName(TestPlugin.class, TestPlugin.class.getName())); iDeepPluginIRegistration.unregister(); diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java new file mode 100644 index 0000000..c883e2f --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; +import com.intergral.deep.agent.push.PushService; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import java.io.Closeable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +class CallbackTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointConfigService tracepointConfigService = Mockito.mock(TracepointConfigService.class); + private final PushService pushService = Mockito.mock(PushService.class); + + @BeforeEach + void setUp() { + Callback.init(settings, tracepointConfigService, pushService); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn(final String name) { + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn2(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn3(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn4(final String name) throws Exception { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.doThrow(new RuntimeException("test")).when(iSpan).close(); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn5(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.doThrow(new RuntimeException("test")).when(iTraceProvider).createSpan(name); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java index 85f0bcd..e7ab14d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java @@ -24,8 +24,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import com.intergral.deep.agent.api.plugin.ITraceProvider; import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.spi.IDeepPlugin; import com.intergral.deep.agent.logging.Logger; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.push.PushUtils; @@ -168,7 +171,7 @@ void constructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -216,7 +219,7 @@ void constructor_end_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -282,7 +285,7 @@ void constructor_start_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -351,7 +354,7 @@ void setName() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -420,7 +423,7 @@ void getName_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -488,7 +491,7 @@ void getName_non_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -558,7 +561,7 @@ void errorSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -628,7 +631,7 @@ void throwSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -696,7 +699,7 @@ void catchSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -764,7 +767,7 @@ void finallySomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -836,7 +839,7 @@ void conditionalThrow() throws Exception { .pushSnapshot(argumentCaptor.capture(), Mockito.any()); assertThrows(InvocationTargetException.class, () -> method.invoke(myTest, 3, 2)); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -904,7 +907,7 @@ void breakSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -972,7 +975,7 @@ void continueSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1045,7 +1048,7 @@ void superConstructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1109,7 +1112,7 @@ void multipleTps_oneLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1178,7 +1181,7 @@ void multipleTps_nextLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1245,7 +1248,7 @@ void someFunctionWithABody() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1380,7 +1383,7 @@ void jspVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1425,7 +1428,7 @@ void luceeVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1437,4 +1440,141 @@ void luceeVisitorTest() throws Exception { assertEquals(3, stackFrame.getLineNumber()); assertEquals("testfile_cfm$cf", stackFrame.getClassName()); } + + @Test + void methodWrapperTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 151) + .withArg(TracePointConfig.METHOD_NAME, "someFunctionWithABody"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("someFunctionWithABody", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("someFunctionWithABody"); + } + + @Test + void methodWrapperVoidTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 51) + .withArg(TracePointConfig.METHOD_NAME, "setName"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("setName", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("setName"); + + } + + public static class MockTraceProvider implements IDeepPlugin, ITraceProvider { + + @Override + public ISpan createSpan(final String name) { + return new ISpan() { + @Override + public String name() { + return name; + } + + @Override + public String traceId() { + return "-1"; + } + + @Override + public String spanId() { + return "-2"; + } + + @Override + public void close() throws Exception { + + } + }; + } + + @Override + public ISpan currentSpan() { + return null; + } + } } \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java new file mode 100644 index 0000000..2ff8b10 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; +import java.io.IOException; + +/** + * This type is used as a template for generating the ASM code used in the {@link com.intergral.deep.agent.tracepoint.inst.asm.Visitor}. + * + * We use the ASM plugin for idea to simplify this process ASM Viewer. + */ +public class MockMixinTemplate { + + public void $deep$voidTemplate() { + } + + public void $deep$voidTemplate(final String arg1) { + } + + public void voidTemplate() { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public void voidTemplate(final String arg1) { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public int $deep$intTemplate() { + return -1; + } + + public int $deep$intTemplate(final String arg1) { + return -1; + } + + public int intTemplate() { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public int intTemplate(final String arg1) { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public Object $deep$objectTemplate() { + return null; + } + + public Object $deep$objectTemplate(final String arg1) { + return null; + } + + public Object objectTemplate() { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public Object objectTemplate(final String arg1) { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d952d4f..14054d6 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -28,6 +28,8 @@ + + diff --git a/deep/pom.xml b/deep/pom.xml index 1b55259..0473bb9 100644 --- a/deep/pom.xml +++ b/deep/pom.xml @@ -75,6 +75,12 @@ 1.0-SNAPSHOT compile + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + compile + diff --git a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin deleted file mode 100644 index 7f05f9c..0000000 --- a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2023 Intergral GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -com.intergral.deep.plugin.PrometheusMetricsPlugin diff --git a/examples/otel-example/pom.xml b/examples/otel-example/pom.xml new file mode 100644 index 0000000..e2950e0 --- /dev/null +++ b/examples/otel-example/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + com.intergral.deep.examples + examples + 1.0-SNAPSHOT + + + otel-example + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + + + com.intergral.deep + deep + 1.0-SNAPSHOT + compile + + + + com.intergral.deep + agent + 1.0-SNAPSHOT + provided + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + + + + \ No newline at end of file diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java new file mode 100644 index 0000000..1a3ba44 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +public class BaseTest { + + protected final Properties systemProps = System.getProperties(); + + + public String newId() { + return UUID.randomUUID().toString(); + } + + + public Map makeCharCountMap(final String str) { + final HashMap res = new HashMap(); + + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + final Integer cnt = res.get(c); + if (cnt == null) { + res.put(c, 0); + } else { + res.put(c, cnt + 1); + } + } + + return res; + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java new file mode 100644 index 0000000..62b18c5 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + + +import com.intergral.deep.Deep; +import com.intergral.deep.agent.api.IDeep; +import com.intergral.deep.agent.api.plugin.MetricDefinition; +import com.intergral.deep.agent.api.plugin.MetricDefinition.Label; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.api.DeepAPI; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This example expects the deep agent to be loaded via the javaagent vm option. + * + * See RunConfigurations for IDEA: + * + * Dynamic Load without JavaAgent + * Dynamic Load with JavaAgent + * + */ +public class Main { + + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ + public static void main(String[] args) throws Throwable { + // this is only needed in this example as we are using a local built module + // if using the dependency from maven you do not need to set the path + //noinspection DataFlowIssue + Path jarPath = Paths.get(Main.class.getResource("/").toURI()) + .getParent() + .getParent() + .getParent() + .getParent() + .resolve("agent/target/agent-1.0-SNAPSHOT.jar"); + + // Dynamically configure and attach the deep agent + Deep.config() + .setJarPath(jarPath.toAbsolutePath().toString()) + .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") + .setValue(ISettings.KEY_SERVICE_SECURE, false) + .start(); + + // different ways to get the API instance + final Deep instance = Deep.getInstance(); + System.out.println(instance.api().getVersion()); + System.out.println(instance.reflection()); + + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); + + final List labels = new ArrayList<>(); + // USe the API to create a tracepoint that will fire forever + final Map tpArgs = new HashMap<>(); + tpArgs.put("fire_count", "-1"); + tpArgs.put("log_msg", "This is a log message {this}"); + tpArgs.put("method_name", "message"); + + final OpenTelemetry openTelemetry = openTelemetry(); + + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + tpArgs, Collections.emptyList(), + Collections.singletonList( + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); + + Random random = new Random(0); + final SimpleTest ts = new SimpleTest("This is a test", 2); + final Tracer main = openTelemetry.getTracer("main"); + + //noinspection InfiniteLoopStatement + for (; ; ) { + final Span span = main.spanBuilder("loop").startSpan(); + try { + ts.message(ts.newId()); + } catch (Exception e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + //noinspection BusyWait + Thread.sleep(1000); + span.end(); + } + } + + private static OpenTelemetry openTelemetry() { + Resource resource = Resource.getDefault().toBuilder().put("service-name", "dice-server").put("service-version", "0.1.0").build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build()) + .setResource(resource) + .build(); + + SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setMeterProvider(sdkMeterProvider) + .setLoggerProvider(sdkLoggerProvider) + .setPropagators(ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .buildAndRegisterGlobal(); + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java new file mode 100644 index 0000000..760235b --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class SimpleTest extends BaseTest { + + public static Date NICE_START_DATE = new Date(); + + private final long startedAt = System.currentTimeMillis(); + private final String testName; + private final int maxExecutions; + public int cnt = 0; + private Map charCounter = new TreeMap(); + + + public SimpleTest(final String testName, final int maxExecutions) { + this.testName = testName; + this.maxExecutions = maxExecutions; + } + + + void message(final String uuid) throws Exception { + System.out.println(cnt + ":" + uuid); + cnt += 1; + + checkEnd(cnt, maxExecutions); + + final Map info = makeCharCountMap(uuid); + merge(charCounter, info); + if ((cnt % 30) == 0) { + dump(); + } + } + + + void merge(final Map charCounter, final Map newInfo) { + for (final Character c : newInfo.keySet()) { + final Integer i = newInfo.get(c); + + Integer curr = charCounter.get(c); + if (curr == null) { + charCounter.put(c, i); + } else { + charCounter.put(c, curr + i); + } + } + } + + + void dump() { + System.out.println(charCounter); + charCounter = new HashMap(); + } + + + static void checkEnd(final int val, final int max) throws Exception { + if (val > max) { + throw new Exception("Hit max executions " + val + " " + max); + } + } + + + @Override + public String toString() { + return getClass().getName() + ":" + testName + ":" + startedAt + "#" + System.identityHashCode( + this); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d483fcd..cecf403 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -38,6 +38,7 @@ agent-load dynamic-load prometheus-metrics-example + otel-example diff --git a/examples/prometheus-metrics-example/pom.xml b/examples/prometheus-metrics-example/pom.xml index bbb081e..597a3db 100644 --- a/examples/prometheus-metrics-example/pom.xml +++ b/examples/prometheus-metrics-example/pom.xml @@ -48,12 +48,6 @@ 1.0-SNAPSHOT provided - - com.intergral.deep.plugins - prometheus-metrics - 1.0-SNAPSHOT - - io.prometheus diff --git a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java index beffeff..5d3051a 100644 --- a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Throwable { .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, fireCount, Collections.emptyList(), Collections.singletonList( - new MetricDefinition("custom_metric", labels, "histogram", "this.cnt", "deep", "help message", "unit"))); + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); Random random = new Random(0); final SimpleTest ts = new SimpleTest("This is a test", 2); diff --git a/plugins/otel-plugin/pom.xml b/plugins/otel-plugin/pom.xml new file mode 100644 index 0000000..ef1bbc6 --- /dev/null +++ b/plugins/otel-plugin/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + com.intergral.deep.plugins + plugins + 1.0-SNAPSHOT + + + otel-plugin + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + com.intergral.deep + agent-api + 1.0-SNAPSHOT + compile + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-sdk + provided + + + io.opentelemetry + opentelemetry-sdk-metrics + provided + + + + \ No newline at end of file diff --git a/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java new file mode 100644 index 0000000..2c305cf --- /dev/null +++ b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.plugin; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IMetricProcessor; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.plugin.ISnapshotDecorator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.IConditional; +import com.intergral.deep.agent.api.spi.IDeepPlugin; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.HashMap; +import java.util.Map; + +/** + * This plugin provides a connection between Deep and Otel. Allowing: + * + * Metrics to be processed via Otel + * Traces to be created using Otel + * + */ +public class OtelPlugin implements IDeepPlugin, ITraceProvider, IMetricProcessor, IConditional, ISnapshotDecorator { + + @Override + public void counter(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final LongCounter build = deep.counterBuilder(name).setUnit(unit).setDescription(help).build(); + build.add(value.longValue()); + } + + @Override + public void gauge(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final ObservableDoubleMeasurement observableDoubleMeasurement = deep.gaugeBuilder(name).setUnit(unit).setDescription(help) + .buildObserver(); + observableDoubleMeasurement.record(value); + } + + @Override + public void histogram(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final DoubleHistogram build = deep.histogramBuilder(name).setUnit(unit).setDescription(help).build(); + build.record(value); + } + + @Override + public void summary(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + histogram(name, labels, namespace, help, unit, value); + } + + @Override + public ISpan createSpan(final String name) { + final TracerProvider tracerProvider = GlobalOpenTelemetry.getTracerProvider(); + if (tracerProvider == null) { + return null; + } + + final Tracer deep = tracerProvider.get("deep", DeepVersion.VERSION); + if (deep == null) { + return null; + } + + final Span span = deep.spanBuilder(name).setAttribute("deep", DeepVersion.VERSION).startSpan(); + return new ISpan() { + @Override + public String name() { + return name; + } + + + @Override + public String traceId() { + if (span != null) { + return span.getSpanContext().getTraceId(); + } + return null; + } + + @Override + public String spanId() { + if (span != null) { + return span.getSpanContext().getSpanId(); + } + return null; + } + + @Override + public void close() { + try { + span.end(); + } catch (Throwable ignored) { + + } + } + }; + } + + @Override + public ISpan currentSpan() { + final Span current = Span.current(); + final ReadableSpan readableSpan; + if (current instanceof ReadableSpan) { + readableSpan = (ReadableSpan) current; + } else { + readableSpan = null; + } + if (current == null) { + return null; + } + return new ISpan() { + @Override + public String name() { + if (readableSpan != null) { + return readableSpan.getName(); + } + return "unknown"; + } + + + @Override + public String traceId() { + return current.getSpanContext().getTraceId(); + } + + @Override + public String spanId() { + return current.getSpanContext().getSpanId(); + } + + @Override + public void close() { + throw new IllegalStateException("Cannot close external spans."); + } + }; + } + + @Override + public boolean isActive() { + try { + final Class ignored = GlobalOpenTelemetry.class; + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + final Map map = new HashMap<>(); + final ISpan iSpan = this.currentSpan(); + if (iSpan != null) { + map.put("trace_id", iSpan.traceId()); + map.put("span_id", iSpan.spanId()); + } + return Resource.create(map); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 47f43ab..9923424 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -31,6 +31,7 @@ java-plugin cf-plugin prometheus-metrics + otel-plugin com.intergral.deep.plugins diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index 266d801..f677ed5 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,10 +17,15 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.ArrayValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.common.v1.KeyValueList; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -78,6 +83,51 @@ public VariableID variableId() { }; } + public static String attributeByName(final String name, final Snapshot snapshot) { + final List attributesList = snapshot.getAttributesList(); + for (KeyValue keyValue : attributesList) { + if (keyValue.getKey().equals(name)) { + return keyValueAsString(keyValue.getValue()); + } + } + return null; + } + + private static String keyValueAsString(final AnyValue value) { + if (value.hasArrayValue()) { + final ArrayValue arrayValue = value.getArrayValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (AnyValue anyValue : arrayValue.getValuesList()) { + stringBuilder.append(keyValueAsString(anyValue)).append(","); + } + return stringBuilder.toString(); + } + if (value.hasStringValue()) { + return value.getStringValue(); + } + if (value.hasBoolValue()) { + return String.valueOf(value.getBoolValue()); + } + if (value.hasBytesValue()) { + return String.valueOf(value.getBytesValue()); + } + if (value.hasDoubleValue()) { + return String.valueOf(value.getDoubleValue()); + } + if (value.hasIntValue()) { + return String.valueOf(value.getIntValue()); + } + if (value.hasKvlistValue()) { + final KeyValueList kvlistValue = value.getKvlistValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (KeyValue anyValue : kvlistValue.getValuesList()) { + stringBuilder.append(anyValue.getKey()).append(":").append(keyValueAsString(anyValue.getValue())).append(","); + } + return stringBuilder.toString(); + } + return null; + } + public interface IVariableScan { boolean found();
+ * We use {@link Closeable} here, so we can stick to java types in the injected code. This makes testing and injected code simpler. + * + * @param name the name of the span + * @return a {@link Closeable} to close the span + */ + public static Closeable span(final String name) { + return Callback.span(name); + } } diff --git a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin index 5f72396..fd5d00b 100644 --- a/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ b/agent/src/main/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin @@ -20,3 +20,5 @@ com.intergral.deep.agent.resource.EnvironmentResourceProvider com.intergral.deep.plugin.cf.CFPlugin com.intergral.deep.plugin.JavaPlugin com.intergral.deep.agent.api.auth.BasicAuthProvider +com.intergral.deep.plugin.PrometheusMetricsPlugin +com.intergral.deep.plugin.OtelPlugin diff --git a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java index 02b2a1f..35a851d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/settings/SettingsTest.java @@ -191,6 +191,8 @@ void addPlugin() { assertEquals(1, settings.getPlugins().size()); + assertSame(plugin, settings.getPlugin(TestPlugin.class)); + assertNotNull(settings.getPluginByName(TestPlugin.class, TestPlugin.class.getName())); iDeepPluginIRegistration.unregister(); diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java new file mode 100644 index 0000000..c883e2f --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/handler/CallbackTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.agent.tracepoint.handler; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.plugin.ITraceProvider.ISpan; +import com.intergral.deep.agent.push.PushService; +import com.intergral.deep.agent.settings.Settings; +import com.intergral.deep.agent.tracepoint.TracepointConfigService; +import java.io.Closeable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +class CallbackTest { + + private final Settings settings = Mockito.mock(Settings.class); + private final TracepointConfigService tracepointConfigService = Mockito.mock(TracepointConfigService.class); + private final PushService pushService = Mockito.mock(PushService.class); + + @BeforeEach + void setUp() { + Callback.init(settings, tracepointConfigService, pushService); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn(final String name) { + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn2(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn3(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn4(final String name) throws Exception { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + final ISpan iSpan = Mockito.mock(ISpan.class); + Mockito.doThrow(new RuntimeException("test")).when(iSpan).close(); + Mockito.when(iTraceProvider.createSpan(name)).thenReturn(iSpan); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } + + @ParameterizedTest + @ValueSource(strings = {"test", ""}) + @NullSource() + void spanMustAlwaysReturn5(final String name) { + final ITraceProvider iTraceProvider = Mockito.mock(ITraceProvider.class); + Mockito.doThrow(new RuntimeException("test")).when(iTraceProvider).createSpan(name); + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(iTraceProvider); + final Closeable test = Callback.span(name); + assertNotNull(test); + assertDoesNotThrow(test::close); + } +} \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java index 85f0bcd..e7ab14d 100644 --- a/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java +++ b/agent/src/test/java/com/intergral/deep/agent/tracepoint/inst/asm/VisitorTest.java @@ -24,8 +24,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import com.intergral.deep.agent.api.plugin.ITraceProvider; import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.spi.IDeepPlugin; import com.intergral.deep.agent.logging.Logger; import com.intergral.deep.agent.push.PushService; import com.intergral.deep.agent.push.PushUtils; @@ -168,7 +171,7 @@ void constructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -216,7 +219,7 @@ void constructor_end_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -282,7 +285,7 @@ void constructor_start_line() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -351,7 +354,7 @@ void setName() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -420,7 +423,7 @@ void getName_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -488,7 +491,7 @@ void getName_non_null_return() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -558,7 +561,7 @@ void errorSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -628,7 +631,7 @@ void throwSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -696,7 +699,7 @@ void catchSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -764,7 +767,7 @@ void finallySomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -836,7 +839,7 @@ void conditionalThrow() throws Exception { .pushSnapshot(argumentCaptor.capture(), Mockito.any()); assertThrows(InvocationTargetException.class, () -> method.invoke(myTest, 3, 2)); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -904,7 +907,7 @@ void breakSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -972,7 +975,7 @@ void continueSomething() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1045,7 +1048,7 @@ void superConstructor() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1109,7 +1112,7 @@ void multipleTps_oneLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1178,7 +1181,7 @@ void multipleTps_nextLine() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(2)) + Mockito.verify(pushService, times(2)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1245,7 +1248,7 @@ void someFunctionWithABody() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1380,7 +1383,7 @@ void jspVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1425,7 +1428,7 @@ void luceeVisitorTest() throws Exception { final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); - Mockito.verify(pushService, Mockito.times(1)) + Mockito.verify(pushService, times(1)) .pushSnapshot(argumentCaptor.capture(), Mockito.any()); final EventSnapshot value = argumentCaptor.getValue(); @@ -1437,4 +1440,141 @@ void luceeVisitorTest() throws Exception { assertEquals(3, stackFrame.getLineNumber()); assertEquals("testfile_cfm$cf", stackFrame.getClassName()); } + + @Test + void methodWrapperTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 151) + .withArg(TracePointConfig.METHOD_NAME, "someFunctionWithABody"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("someFunctionWithABody", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("someFunctionWithABody"); + } + + @Test + void methodWrapperVoidTest() throws Exception { + final MockTraceProvider traceProvider = new MockTraceProvider(); + final MockTraceProvider traceProviderSpy = Mockito.spy(traceProvider); + + Mockito.when(settings.getPlugin(ITraceProvider.class)).thenReturn(traceProviderSpy); + + final MockTracepointConfig tracepointConfig = new MockTracepointConfig( + "/agent/src/test/java/com/intergral/deep/test/target/BPTestTarget.java", 51) + .withArg(TracePointConfig.METHOD_NAME, "setName"); + + tracepointRef.set(Collections.singletonList(tracepointConfig)); + + final String name = "com/intergral/deep/test/target/BPTestTarget"; + final ByteClassLoader classLoader = ByteClassLoader.forFile(name); + + instrumentationService.processBreakpoints(Collections.singletonList(tracepointConfig)); + + final byte[] originalBytes = classLoader.getBytes(name); + final byte[] transformed = instrumentationService.transform(null, name, null, null, originalBytes); + // we do this here so each test can save the modified bytes, else as they all use the same target class they would stomp over each other + TransformerUtils.storeUnsafe(disPath, originalBytes, transformed, name + Thread.currentThread().getStackTrace()[1].getMethodName()); + + assertNotNull(transformed, "Failed to transform the test class!"); + assertNotEquals(originalBytes.length, transformed.length); + + final String clazzName = InstUtils.externalClassName(name); + classLoader.setBytes(clazzName, transformed); + + final Class> aClass = classLoader.loadClass(clazzName); + assertNotNull(aClass); + + final Constructor> constructor = aClass.getConstructor(String.class, int.class); + final Object myTest = constructor.newInstance(null, 4); + assertNotNull(myTest); + + final Method method = aClass.getDeclaredMethod("setName", String.class); + method.invoke(myTest, "some string"); + + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(EventSnapshot.class); + + Mockito.verify(pushService, times(1)) + .pushSnapshot(argumentCaptor.capture(), Mockito.any()); + + final EventSnapshot value = argumentCaptor.getValue(); + assertEquals(tracepointConfig.getId(), value.getTracepoint().getId()); + + final Snapshot snapshot = PushUtils.convertToGrpc(value); + + Mockito.verify(traceProviderSpy, times(1)).createSpan("setName"); + + } + + public static class MockTraceProvider implements IDeepPlugin, ITraceProvider { + + @Override + public ISpan createSpan(final String name) { + return new ISpan() { + @Override + public String name() { + return name; + } + + @Override + public String traceId() { + return "-1"; + } + + @Override + public String spanId() { + return "-2"; + } + + @Override + public void close() throws Exception { + + } + }; + } + + @Override + public ISpan currentSpan() { + return null; + } + } } \ No newline at end of file diff --git a/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java new file mode 100644 index 0000000..2ff8b10 --- /dev/null +++ b/agent/src/test/java/com/intergral/deep/test/MockMixinTemplate.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.test; + +import com.intergral.deep.agent.tracepoint.handler.Callback; +import java.io.Closeable; +import java.io.IOException; + +/** + * This type is used as a template for generating the ASM code used in the {@link com.intergral.deep.agent.tracepoint.inst.asm.Visitor}. + * + * We use the ASM plugin for idea to simplify this process ASM Viewer. + */ +public class MockMixinTemplate { + + public void $deep$voidTemplate() { + } + + public void $deep$voidTemplate(final String arg1) { + } + + public void voidTemplate() { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public void voidTemplate(final String arg1) { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public int $deep$intTemplate() { + return -1; + } + + public int $deep$intTemplate(final String arg1) { + return -1; + } + + public int intTemplate() { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public int intTemplate(final String arg1) { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public Object $deep$objectTemplate() { + return null; + } + + public Object $deep$objectTemplate(final String arg1) { + return null; + } + + public Object objectTemplate() { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public Object objectTemplate(final String arg1) { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d952d4f..14054d6 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -28,6 +28,8 @@ + + diff --git a/deep/pom.xml b/deep/pom.xml index 1b55259..0473bb9 100644 --- a/deep/pom.xml +++ b/deep/pom.xml @@ -75,6 +75,12 @@ 1.0-SNAPSHOT compile + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + compile + diff --git a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin deleted file mode 100644 index 7f05f9c..0000000 --- a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2023 Intergral GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -com.intergral.deep.plugin.PrometheusMetricsPlugin diff --git a/examples/otel-example/pom.xml b/examples/otel-example/pom.xml new file mode 100644 index 0000000..e2950e0 --- /dev/null +++ b/examples/otel-example/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + com.intergral.deep.examples + examples + 1.0-SNAPSHOT + + + otel-example + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + + + com.intergral.deep + deep + 1.0-SNAPSHOT + compile + + + + com.intergral.deep + agent + 1.0-SNAPSHOT + provided + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + + + + \ No newline at end of file diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java new file mode 100644 index 0000000..1a3ba44 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +public class BaseTest { + + protected final Properties systemProps = System.getProperties(); + + + public String newId() { + return UUID.randomUUID().toString(); + } + + + public Map makeCharCountMap(final String str) { + final HashMap res = new HashMap(); + + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + final Integer cnt = res.get(c); + if (cnt == null) { + res.put(c, 0); + } else { + res.put(c, cnt + 1); + } + } + + return res; + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java new file mode 100644 index 0000000..62b18c5 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + + +import com.intergral.deep.Deep; +import com.intergral.deep.agent.api.IDeep; +import com.intergral.deep.agent.api.plugin.MetricDefinition; +import com.intergral.deep.agent.api.plugin.MetricDefinition.Label; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.api.DeepAPI; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This example expects the deep agent to be loaded via the javaagent vm option. + * + * See RunConfigurations for IDEA: + * + * Dynamic Load without JavaAgent + * Dynamic Load with JavaAgent + * + */ +public class Main { + + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ + public static void main(String[] args) throws Throwable { + // this is only needed in this example as we are using a local built module + // if using the dependency from maven you do not need to set the path + //noinspection DataFlowIssue + Path jarPath = Paths.get(Main.class.getResource("/").toURI()) + .getParent() + .getParent() + .getParent() + .getParent() + .resolve("agent/target/agent-1.0-SNAPSHOT.jar"); + + // Dynamically configure and attach the deep agent + Deep.config() + .setJarPath(jarPath.toAbsolutePath().toString()) + .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") + .setValue(ISettings.KEY_SERVICE_SECURE, false) + .start(); + + // different ways to get the API instance + final Deep instance = Deep.getInstance(); + System.out.println(instance.api().getVersion()); + System.out.println(instance.reflection()); + + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); + + final List labels = new ArrayList<>(); + // USe the API to create a tracepoint that will fire forever + final Map tpArgs = new HashMap<>(); + tpArgs.put("fire_count", "-1"); + tpArgs.put("log_msg", "This is a log message {this}"); + tpArgs.put("method_name", "message"); + + final OpenTelemetry openTelemetry = openTelemetry(); + + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + tpArgs, Collections.emptyList(), + Collections.singletonList( + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); + + Random random = new Random(0); + final SimpleTest ts = new SimpleTest("This is a test", 2); + final Tracer main = openTelemetry.getTracer("main"); + + //noinspection InfiniteLoopStatement + for (; ; ) { + final Span span = main.spanBuilder("loop").startSpan(); + try { + ts.message(ts.newId()); + } catch (Exception e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + //noinspection BusyWait + Thread.sleep(1000); + span.end(); + } + } + + private static OpenTelemetry openTelemetry() { + Resource resource = Resource.getDefault().toBuilder().put("service-name", "dice-server").put("service-version", "0.1.0").build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build()) + .setResource(resource) + .build(); + + SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setMeterProvider(sdkMeterProvider) + .setLoggerProvider(sdkLoggerProvider) + .setPropagators(ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .buildAndRegisterGlobal(); + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java new file mode 100644 index 0000000..760235b --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class SimpleTest extends BaseTest { + + public static Date NICE_START_DATE = new Date(); + + private final long startedAt = System.currentTimeMillis(); + private final String testName; + private final int maxExecutions; + public int cnt = 0; + private Map charCounter = new TreeMap(); + + + public SimpleTest(final String testName, final int maxExecutions) { + this.testName = testName; + this.maxExecutions = maxExecutions; + } + + + void message(final String uuid) throws Exception { + System.out.println(cnt + ":" + uuid); + cnt += 1; + + checkEnd(cnt, maxExecutions); + + final Map info = makeCharCountMap(uuid); + merge(charCounter, info); + if ((cnt % 30) == 0) { + dump(); + } + } + + + void merge(final Map charCounter, final Map newInfo) { + for (final Character c : newInfo.keySet()) { + final Integer i = newInfo.get(c); + + Integer curr = charCounter.get(c); + if (curr == null) { + charCounter.put(c, i); + } else { + charCounter.put(c, curr + i); + } + } + } + + + void dump() { + System.out.println(charCounter); + charCounter = new HashMap(); + } + + + static void checkEnd(final int val, final int max) throws Exception { + if (val > max) { + throw new Exception("Hit max executions " + val + " " + max); + } + } + + + @Override + public String toString() { + return getClass().getName() + ":" + testName + ":" + startedAt + "#" + System.identityHashCode( + this); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d483fcd..cecf403 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -38,6 +38,7 @@ agent-load dynamic-load prometheus-metrics-example + otel-example diff --git a/examples/prometheus-metrics-example/pom.xml b/examples/prometheus-metrics-example/pom.xml index bbb081e..597a3db 100644 --- a/examples/prometheus-metrics-example/pom.xml +++ b/examples/prometheus-metrics-example/pom.xml @@ -48,12 +48,6 @@ 1.0-SNAPSHOT provided - - com.intergral.deep.plugins - prometheus-metrics - 1.0-SNAPSHOT - - io.prometheus diff --git a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java index beffeff..5d3051a 100644 --- a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Throwable { .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, fireCount, Collections.emptyList(), Collections.singletonList( - new MetricDefinition("custom_metric", labels, "histogram", "this.cnt", "deep", "help message", "unit"))); + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); Random random = new Random(0); final SimpleTest ts = new SimpleTest("This is a test", 2); diff --git a/plugins/otel-plugin/pom.xml b/plugins/otel-plugin/pom.xml new file mode 100644 index 0000000..ef1bbc6 --- /dev/null +++ b/plugins/otel-plugin/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + com.intergral.deep.plugins + plugins + 1.0-SNAPSHOT + + + otel-plugin + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + com.intergral.deep + agent-api + 1.0-SNAPSHOT + compile + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-sdk + provided + + + io.opentelemetry + opentelemetry-sdk-metrics + provided + + + + \ No newline at end of file diff --git a/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java new file mode 100644 index 0000000..2c305cf --- /dev/null +++ b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.plugin; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IMetricProcessor; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.plugin.ISnapshotDecorator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.IConditional; +import com.intergral.deep.agent.api.spi.IDeepPlugin; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.HashMap; +import java.util.Map; + +/** + * This plugin provides a connection between Deep and Otel. Allowing: + * + * Metrics to be processed via Otel + * Traces to be created using Otel + * + */ +public class OtelPlugin implements IDeepPlugin, ITraceProvider, IMetricProcessor, IConditional, ISnapshotDecorator { + + @Override + public void counter(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final LongCounter build = deep.counterBuilder(name).setUnit(unit).setDescription(help).build(); + build.add(value.longValue()); + } + + @Override + public void gauge(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final ObservableDoubleMeasurement observableDoubleMeasurement = deep.gaugeBuilder(name).setUnit(unit).setDescription(help) + .buildObserver(); + observableDoubleMeasurement.record(value); + } + + @Override + public void histogram(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final DoubleHistogram build = deep.histogramBuilder(name).setUnit(unit).setDescription(help).build(); + build.record(value); + } + + @Override + public void summary(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + histogram(name, labels, namespace, help, unit, value); + } + + @Override + public ISpan createSpan(final String name) { + final TracerProvider tracerProvider = GlobalOpenTelemetry.getTracerProvider(); + if (tracerProvider == null) { + return null; + } + + final Tracer deep = tracerProvider.get("deep", DeepVersion.VERSION); + if (deep == null) { + return null; + } + + final Span span = deep.spanBuilder(name).setAttribute("deep", DeepVersion.VERSION).startSpan(); + return new ISpan() { + @Override + public String name() { + return name; + } + + + @Override + public String traceId() { + if (span != null) { + return span.getSpanContext().getTraceId(); + } + return null; + } + + @Override + public String spanId() { + if (span != null) { + return span.getSpanContext().getSpanId(); + } + return null; + } + + @Override + public void close() { + try { + span.end(); + } catch (Throwable ignored) { + + } + } + }; + } + + @Override + public ISpan currentSpan() { + final Span current = Span.current(); + final ReadableSpan readableSpan; + if (current instanceof ReadableSpan) { + readableSpan = (ReadableSpan) current; + } else { + readableSpan = null; + } + if (current == null) { + return null; + } + return new ISpan() { + @Override + public String name() { + if (readableSpan != null) { + return readableSpan.getName(); + } + return "unknown"; + } + + + @Override + public String traceId() { + return current.getSpanContext().getTraceId(); + } + + @Override + public String spanId() { + return current.getSpanContext().getSpanId(); + } + + @Override + public void close() { + throw new IllegalStateException("Cannot close external spans."); + } + }; + } + + @Override + public boolean isActive() { + try { + final Class ignored = GlobalOpenTelemetry.class; + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + final Map map = new HashMap<>(); + final ISpan iSpan = this.currentSpan(); + if (iSpan != null) { + map.put("trace_id", iSpan.traceId()); + map.put("span_id", iSpan.spanId()); + } + return Resource.create(map); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 47f43ab..9923424 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -31,6 +31,7 @@ java-plugin cf-plugin prometheus-metrics + otel-plugin com.intergral.deep.plugins diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index 266d801..f677ed5 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,10 +17,15 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.ArrayValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.common.v1.KeyValueList; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -78,6 +83,51 @@ public VariableID variableId() { }; } + public static String attributeByName(final String name, final Snapshot snapshot) { + final List attributesList = snapshot.getAttributesList(); + for (KeyValue keyValue : attributesList) { + if (keyValue.getKey().equals(name)) { + return keyValueAsString(keyValue.getValue()); + } + } + return null; + } + + private static String keyValueAsString(final AnyValue value) { + if (value.hasArrayValue()) { + final ArrayValue arrayValue = value.getArrayValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (AnyValue anyValue : arrayValue.getValuesList()) { + stringBuilder.append(keyValueAsString(anyValue)).append(","); + } + return stringBuilder.toString(); + } + if (value.hasStringValue()) { + return value.getStringValue(); + } + if (value.hasBoolValue()) { + return String.valueOf(value.getBoolValue()); + } + if (value.hasBytesValue()) { + return String.valueOf(value.getBytesValue()); + } + if (value.hasDoubleValue()) { + return String.valueOf(value.getDoubleValue()); + } + if (value.hasIntValue()) { + return String.valueOf(value.getIntValue()); + } + if (value.hasKvlistValue()) { + final KeyValueList kvlistValue = value.getKvlistValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (KeyValue anyValue : kvlistValue.getValuesList()) { + stringBuilder.append(anyValue.getKey()).append(":").append(keyValueAsString(anyValue.getValue())).append(","); + } + return stringBuilder.toString(); + } + return null; + } + public interface IVariableScan { boolean found();
+ * We use the ASM plugin for idea to simplify this process ASM Viewer. + */ +public class MockMixinTemplate { + + public void $deep$voidTemplate() { + } + + public void $deep$voidTemplate(final String arg1) { + } + + public void voidTemplate() { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public void voidTemplate(final String arg1) { + final Closeable closeable = Callback.span("voidTemplate"); + try { + $deep$voidTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public int $deep$intTemplate() { + return -1; + } + + public int $deep$intTemplate(final String arg1) { + return -1; + } + + public int intTemplate() { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public int intTemplate(final String arg1) { + final Closeable closeable = Callback.span("intTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + public Object $deep$objectTemplate() { + return null; + } + + public Object $deep$objectTemplate(final String arg1) { + return null; + } + + public Object objectTemplate() { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } + + + public Object objectTemplate(final String arg1) { + final Closeable closeable = Callback.span("objectTemplate"); + try { + return $deep$intTemplate(arg1); + } finally { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ignored) { + // we ignore this exception + } + } + } +} diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index d952d4f..14054d6 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -28,6 +28,8 @@ + + diff --git a/deep/pom.xml b/deep/pom.xml index 1b55259..0473bb9 100644 --- a/deep/pom.xml +++ b/deep/pom.xml @@ -75,6 +75,12 @@ 1.0-SNAPSHOT compile + + com.intergral.deep.plugins + otel-plugin + 1.0-SNAPSHOT + compile + diff --git a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin b/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin deleted file mode 100644 index 7f05f9c..0000000 --- a/deep/src/main/java/resources/META-INF/services/com.intergral.deep.agent.api.spi.IDeepPlugin +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (C) 2023 Intergral GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# - -com.intergral.deep.plugin.PrometheusMetricsPlugin diff --git a/examples/otel-example/pom.xml b/examples/otel-example/pom.xml new file mode 100644 index 0000000..e2950e0 --- /dev/null +++ b/examples/otel-example/pom.xml @@ -0,0 +1,86 @@ + + + + + 4.0.0 + + com.intergral.deep.examples + examples + 1.0-SNAPSHOT + + + otel-example + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + + + com.intergral.deep + deep + 1.0-SNAPSHOT + compile + + + + com.intergral.deep + agent + 1.0-SNAPSHOT + provided + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-sdk-trace + + + io.opentelemetry + opentelemetry-sdk-metrics + + + io.opentelemetry + opentelemetry-exporter-logging + + + + \ No newline at end of file diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java new file mode 100644 index 0000000..1a3ba44 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/BaseTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; + +public class BaseTest { + + protected final Properties systemProps = System.getProperties(); + + + public String newId() { + return UUID.randomUUID().toString(); + } + + + public Map makeCharCountMap(final String str) { + final HashMap res = new HashMap(); + + for (int i = 0; i < str.length(); i++) { + final char c = str.charAt(i); + final Integer cnt = res.get(c); + if (cnt == null) { + res.put(c, 0); + } else { + res.put(c, cnt + 1); + } + } + + return res; + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java new file mode 100644 index 0000000..62b18c5 --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/Main.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + + +import com.intergral.deep.Deep; +import com.intergral.deep.agent.api.IDeep; +import com.intergral.deep.agent.api.plugin.MetricDefinition; +import com.intergral.deep.agent.api.plugin.MetricDefinition.Label; +import com.intergral.deep.agent.api.reflection.IReflection; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.api.DeepAPI; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.logging.LoggingMetricExporter; +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * This example expects the deep agent to be loaded via the javaagent vm option. + * + * See RunConfigurations for IDEA: + * + * Dynamic Load without JavaAgent + * Dynamic Load with JavaAgent + * + */ +public class Main { + + /** + * Main entry for example. + * + * @param args the startup arguments + * @throws Throwable if we error + */ + public static void main(String[] args) throws Throwable { + // this is only needed in this example as we are using a local built module + // if using the dependency from maven you do not need to set the path + //noinspection DataFlowIssue + Path jarPath = Paths.get(Main.class.getResource("/").toURI()) + .getParent() + .getParent() + .getParent() + .getParent() + .resolve("agent/target/agent-1.0-SNAPSHOT.jar"); + + // Dynamically configure and attach the deep agent + Deep.config() + .setJarPath(jarPath.toAbsolutePath().toString()) + .setValue(ISettings.KEY_SERVICE_URL, "localhost:43315") + .setValue(ISettings.KEY_SERVICE_SECURE, false) + .start(); + + // different ways to get the API instance + final Deep instance = Deep.getInstance(); + System.out.println(instance.api().getVersion()); + System.out.println(instance.reflection()); + + System.out.println(DeepAPI.api().getVersion()); + System.out.println(DeepAPI.reflection()); + + final List labels = new ArrayList<>(); + // USe the API to create a tracepoint that will fire forever + final Map tpArgs = new HashMap<>(); + tpArgs.put("fire_count", "-1"); + tpArgs.put("log_msg", "This is a log message {this}"); + tpArgs.put("method_name", "message"); + + final OpenTelemetry openTelemetry = openTelemetry(); + + DeepAPI.api() + .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, + tpArgs, Collections.emptyList(), + Collections.singletonList( + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); + + Random random = new Random(0); + final SimpleTest ts = new SimpleTest("This is a test", 2); + final Tracer main = openTelemetry.getTracer("main"); + + //noinspection InfiniteLoopStatement + for (; ; ) { + final Span span = main.spanBuilder("loop").startSpan(); + try { + ts.message(ts.newId()); + } catch (Exception e) { + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + + //noinspection BusyWait + Thread.sleep(1000); + span.end(); + } + } + + private static OpenTelemetry openTelemetry() { + Resource resource = Resource.getDefault().toBuilder().put("service-name", "dice-server").put("service-version", "0.1.0").build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build()) + .setResource(resource) + .build(); + + SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() + .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build()) + .setResource(resource) + .build(); + + return OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setMeterProvider(sdkMeterProvider) + .setLoggerProvider(sdkLoggerProvider) + .setPropagators(ContextPropagators.create( + TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) + .buildAndRegisterGlobal(); + } +} diff --git a/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java new file mode 100644 index 0000000..760235b --- /dev/null +++ b/examples/otel-example/src/main/java/com/intergral/deep/examples/SimpleTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.examples; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +public class SimpleTest extends BaseTest { + + public static Date NICE_START_DATE = new Date(); + + private final long startedAt = System.currentTimeMillis(); + private final String testName; + private final int maxExecutions; + public int cnt = 0; + private Map charCounter = new TreeMap(); + + + public SimpleTest(final String testName, final int maxExecutions) { + this.testName = testName; + this.maxExecutions = maxExecutions; + } + + + void message(final String uuid) throws Exception { + System.out.println(cnt + ":" + uuid); + cnt += 1; + + checkEnd(cnt, maxExecutions); + + final Map info = makeCharCountMap(uuid); + merge(charCounter, info); + if ((cnt % 30) == 0) { + dump(); + } + } + + + void merge(final Map charCounter, final Map newInfo) { + for (final Character c : newInfo.keySet()) { + final Integer i = newInfo.get(c); + + Integer curr = charCounter.get(c); + if (curr == null) { + charCounter.put(c, i); + } else { + charCounter.put(c, curr + i); + } + } + } + + + void dump() { + System.out.println(charCounter); + charCounter = new HashMap(); + } + + + static void checkEnd(final int val, final int max) throws Exception { + if (val > max) { + throw new Exception("Hit max executions " + val + " " + max); + } + } + + + @Override + public String toString() { + return getClass().getName() + ":" + testName + ":" + startedAt + "#" + System.identityHashCode( + this); + } +} diff --git a/examples/pom.xml b/examples/pom.xml index d483fcd..cecf403 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -38,6 +38,7 @@ agent-load dynamic-load prometheus-metrics-example + otel-example diff --git a/examples/prometheus-metrics-example/pom.xml b/examples/prometheus-metrics-example/pom.xml index bbb081e..597a3db 100644 --- a/examples/prometheus-metrics-example/pom.xml +++ b/examples/prometheus-metrics-example/pom.xml @@ -48,12 +48,6 @@ 1.0-SNAPSHOT provided - - com.intergral.deep.plugins - prometheus-metrics - 1.0-SNAPSHOT - - io.prometheus diff --git a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java index beffeff..5d3051a 100644 --- a/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java +++ b/examples/prometheus-metrics-example/src/main/java/com/intergral/deep/examples/Main.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Throwable { .registerTracepoint("com/intergral/deep/examples/SimpleTest", 46, fireCount, Collections.emptyList(), Collections.singletonList( - new MetricDefinition("custom_metric", labels, "histogram", "this.cnt", "deep", "help message", "unit"))); + new MetricDefinition("custom_metric", labels, "HISTOGRAM", "this.cnt", "deep", "help message", "unit"))); Random random = new Random(0); final SimpleTest ts = new SimpleTest("This is a test", 2); diff --git a/plugins/otel-plugin/pom.xml b/plugins/otel-plugin/pom.xml new file mode 100644 index 0000000..ef1bbc6 --- /dev/null +++ b/plugins/otel-plugin/pom.xml @@ -0,0 +1,70 @@ + + + + + 4.0.0 + + com.intergral.deep.plugins + plugins + 1.0-SNAPSHOT + + + otel-plugin + + + + + + + + io.opentelemetry + opentelemetry-bom + 1.33.0 + pom + import + + + + + + + com.intergral.deep + agent-api + 1.0-SNAPSHOT + compile + + + io.opentelemetry + opentelemetry-api + provided + + + io.opentelemetry + opentelemetry-sdk + provided + + + io.opentelemetry + opentelemetry-sdk-metrics + provided + + + + \ No newline at end of file diff --git a/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java new file mode 100644 index 0000000..2c305cf --- /dev/null +++ b/plugins/otel-plugin/src/main/java/com/intergral/deep/plugin/OtelPlugin.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2023 Intergral GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.intergral.deep.plugin; + +import com.intergral.deep.agent.api.DeepVersion; +import com.intergral.deep.agent.api.plugin.IMetricProcessor; +import com.intergral.deep.agent.api.plugin.ISnapshotContext; +import com.intergral.deep.agent.api.plugin.ISnapshotDecorator; +import com.intergral.deep.agent.api.plugin.ITraceProvider; +import com.intergral.deep.agent.api.resource.Resource; +import com.intergral.deep.agent.api.settings.ISettings; +import com.intergral.deep.agent.api.spi.IConditional; +import com.intergral.deep.agent.api.spi.IDeepPlugin; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.metrics.ObservableDoubleMeasurement; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.HashMap; +import java.util.Map; + +/** + * This plugin provides a connection between Deep and Otel. Allowing: + * + * Metrics to be processed via Otel + * Traces to be created using Otel + * + */ +public class OtelPlugin implements IDeepPlugin, ITraceProvider, IMetricProcessor, IConditional, ISnapshotDecorator { + + @Override + public void counter(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final LongCounter build = deep.counterBuilder(name).setUnit(unit).setDescription(help).build(); + build.add(value.longValue()); + } + + @Override + public void gauge(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final ObservableDoubleMeasurement observableDoubleMeasurement = deep.gaugeBuilder(name).setUnit(unit).setDescription(help) + .buildObserver(); + observableDoubleMeasurement.record(value); + } + + @Override + public void histogram(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + final MeterProvider meterProvider = GlobalOpenTelemetry.getMeterProvider(); + final Meter deep = meterProvider.get("deep"); + final DoubleHistogram build = deep.histogramBuilder(name).setUnit(unit).setDescription(help).build(); + build.record(value); + } + + @Override + public void summary(final String name, final Map labels, final String namespace, final String help, final String unit, + final Double value) { + histogram(name, labels, namespace, help, unit, value); + } + + @Override + public ISpan createSpan(final String name) { + final TracerProvider tracerProvider = GlobalOpenTelemetry.getTracerProvider(); + if (tracerProvider == null) { + return null; + } + + final Tracer deep = tracerProvider.get("deep", DeepVersion.VERSION); + if (deep == null) { + return null; + } + + final Span span = deep.spanBuilder(name).setAttribute("deep", DeepVersion.VERSION).startSpan(); + return new ISpan() { + @Override + public String name() { + return name; + } + + + @Override + public String traceId() { + if (span != null) { + return span.getSpanContext().getTraceId(); + } + return null; + } + + @Override + public String spanId() { + if (span != null) { + return span.getSpanContext().getSpanId(); + } + return null; + } + + @Override + public void close() { + try { + span.end(); + } catch (Throwable ignored) { + + } + } + }; + } + + @Override + public ISpan currentSpan() { + final Span current = Span.current(); + final ReadableSpan readableSpan; + if (current instanceof ReadableSpan) { + readableSpan = (ReadableSpan) current; + } else { + readableSpan = null; + } + if (current == null) { + return null; + } + return new ISpan() { + @Override + public String name() { + if (readableSpan != null) { + return readableSpan.getName(); + } + return "unknown"; + } + + + @Override + public String traceId() { + return current.getSpanContext().getTraceId(); + } + + @Override + public String spanId() { + return current.getSpanContext().getSpanId(); + } + + @Override + public void close() { + throw new IllegalStateException("Cannot close external spans."); + } + }; + } + + @Override + public boolean isActive() { + try { + final Class ignored = GlobalOpenTelemetry.class; + return true; + } catch (Throwable t) { + return false; + } + } + + @Override + public Resource decorate(final ISettings settings, final ISnapshotContext snapshot) { + final Map map = new HashMap<>(); + final ISpan iSpan = this.currentSpan(); + if (iSpan != null) { + map.put("trace_id", iSpan.traceId()); + map.put("span_id", iSpan.spanId()); + } + return Resource.create(map); + } +} diff --git a/plugins/pom.xml b/plugins/pom.xml index 47f43ab..9923424 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -31,6 +31,7 @@ java-plugin cf-plugin prometheus-metrics + otel-plugin com.intergral.deep.plugins diff --git a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java index 266d801..f677ed5 100644 --- a/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java +++ b/test-utils/src/main/java/com/intergral/deep/tests/snapshot/SnapshotUtils.java @@ -17,10 +17,15 @@ package com.intergral.deep.tests.snapshot; +import com.intergral.deep.proto.common.v1.AnyValue; +import com.intergral.deep.proto.common.v1.ArrayValue; +import com.intergral.deep.proto.common.v1.KeyValue; +import com.intergral.deep.proto.common.v1.KeyValueList; import com.intergral.deep.proto.tracepoint.v1.Snapshot; import com.intergral.deep.proto.tracepoint.v1.Variable; import com.intergral.deep.proto.tracepoint.v1.VariableID; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -78,6 +83,51 @@ public VariableID variableId() { }; } + public static String attributeByName(final String name, final Snapshot snapshot) { + final List attributesList = snapshot.getAttributesList(); + for (KeyValue keyValue : attributesList) { + if (keyValue.getKey().equals(name)) { + return keyValueAsString(keyValue.getValue()); + } + } + return null; + } + + private static String keyValueAsString(final AnyValue value) { + if (value.hasArrayValue()) { + final ArrayValue arrayValue = value.getArrayValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (AnyValue anyValue : arrayValue.getValuesList()) { + stringBuilder.append(keyValueAsString(anyValue)).append(","); + } + return stringBuilder.toString(); + } + if (value.hasStringValue()) { + return value.getStringValue(); + } + if (value.hasBoolValue()) { + return String.valueOf(value.getBoolValue()); + } + if (value.hasBytesValue()) { + return String.valueOf(value.getBytesValue()); + } + if (value.hasDoubleValue()) { + return String.valueOf(value.getDoubleValue()); + } + if (value.hasIntValue()) { + return String.valueOf(value.getIntValue()); + } + if (value.hasKvlistValue()) { + final KeyValueList kvlistValue = value.getKvlistValue(); + final StringBuilder stringBuilder = new StringBuilder(); + for (KeyValue anyValue : kvlistValue.getValuesList()) { + stringBuilder.append(anyValue.getKey()).append(":").append(keyValueAsString(anyValue.getValue())).append(","); + } + return stringBuilder.toString(); + } + return null; + } + public interface IVariableScan { boolean found();
+ * See RunConfigurations for IDEA: + *