Finally working it is!
This commit is contained in:
parent
84374f5d0d
commit
5d368dd063
11 changed files with 371 additions and 53 deletions
17
README.md
17
README.md
|
|
@ -1 +1,18 @@
|
|||
Experiment with java22 and Rust to monitor exceptions in a JVM
|
||||
|
||||
Running:
|
||||
* Update the path to the rust lib (temp fix) in ExceptionLogger for your setup
|
||||
* mvn clean install
|
||||
* cd rustlib; cargo build
|
||||
* create a minimal class in a separate project
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) throws Throwable {
|
||||
throw new Throwable();
|
||||
}
|
||||
}
|
||||
```
|
||||
* run it with (adjust paths):
|
||||
``` bash
|
||||
java22 -javaagent:$EXCEPTIONAL_PROJECT/exceptional/agent/target/exceptional-agent-1.0-SNAPSHOT.jar --enable-preview -classpath $YOUR_CLASSPATH Main
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@
|
|||
<maven.compiler.target>22</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.github.shautvast.exceptional</groupId>
|
||||
<artifactId>exceptional-lib</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
|
@ -38,7 +47,9 @@
|
|||
<Premain-Class>com.github.shautvast.exceptional.Agent</Premain-Class>
|
||||
<Can-Retransform-Classes>true</Can-Retransform-Classes>
|
||||
<Can-Redefine-Classes>true</Can-Redefine-Classes>
|
||||
<Boot-Class-Path>/Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar</Boot-Class-Path>
|
||||
<Boot-Class-Path>
|
||||
/Users/Shautvast/dev/exceptional/lib/target/exceptional-lib-1.0-SNAPSHOT.jar
|
||||
</Boot-Class-Path>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
|
|
|
|||
|
|
@ -1,80 +1,80 @@
|
|||
package com.github.shautvast.exceptional;
|
||||
|
||||
import java.lang.classfile.*;
|
||||
import java.lang.classfile.instruction.ReturnInstruction;
|
||||
import java.lang.classfile.ClassBuilder;
|
||||
import java.lang.classfile.ClassElement;
|
||||
import java.lang.classfile.ClassFile;
|
||||
import java.lang.classfile.MethodModel;
|
||||
import java.lang.classfile.instruction.ThrowInstruction;
|
||||
import java.lang.constant.ClassDesc;
|
||||
import java.lang.constant.MethodTypeDesc;
|
||||
import java.lang.instrument.ClassDefinition;
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.lang.reflect.AccessFlag;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
public class Agent {
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private static final String EXCEPTIONLOGGER = ExceptionLogger.class.getName();
|
||||
|
||||
public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception {
|
||||
System.err.println("--->Exceptional agent active");
|
||||
// add transformer
|
||||
instrumentation.addTransformer(new ClassFileTransformer() {
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
|
||||
return instrumentThrowable(className, classfileBuffer);
|
||||
return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
|
||||
return instrumentThrowable(className, classfileBuffer);
|
||||
return instrumentExceptionLoggerAtThrow(className, classfileBuffer);
|
||||
}
|
||||
}, true);
|
||||
|
||||
// we only want to redefine Throwable
|
||||
byte[] bytecode = Files.readAllBytes(
|
||||
Path.of(Agent.class.getResource("/java/lang/Throwable.class").toURI()));
|
||||
instrumentation.redefineClasses(new ClassDefinition(Throwable.class, bytecode));
|
||||
}
|
||||
|
||||
private static byte[] instrumentThrowable(String className, byte[] bytecode) {
|
||||
// we only want to instrument Throwable
|
||||
// or rather,,, is this the right way? This way we also intercept new any Exception that is not thrown
|
||||
// But,,, who does that?? (famous last words)
|
||||
if (className.equals("java/lang/Throwable")) {
|
||||
ClassFile classFile = ClassFile.of();
|
||||
ClassModel classModel = classFile.parse(bytecode);
|
||||
return instrumentConstructors(classFile, classModel);
|
||||
} else {
|
||||
return bytecode;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] instrumentConstructors(ClassFile classFile, ClassModel classModel) {
|
||||
/*
|
||||
* Every throw opcode will be preceded by a call to our ExceptionLogger
|
||||
*/
|
||||
private static byte[] instrumentExceptionLoggerAtThrow(String className, byte[] classfileBuffer) {
|
||||
var classFile = ClassFile.of();
|
||||
var classModel = classFile.parse(classfileBuffer);
|
||||
return classFile.build(classModel.thisClass().asSymbol(), classBuilder -> {
|
||||
for (ClassElement ce : classModel) {
|
||||
if (ce instanceof MethodModel mm && mm.methodName().toString().equals("<init>")) {
|
||||
instrumentMethodWithMyExceptionLogger(classBuilder, mm);
|
||||
continue;
|
||||
}
|
||||
// not all methods, TODO include synthetic?
|
||||
if (ce instanceof MethodModel methodModel && !methodModel.flags().has(AccessFlag.ABSTRACT)
|
||||
&& !methodModel.flags().has(AccessFlag.NATIVE)
|
||||
&& !methodModel.flags().has(AccessFlag.BRIDGE)) {
|
||||
transform(classBuilder, methodModel);
|
||||
} else {
|
||||
// keep all other class elements
|
||||
classBuilder.with(ce);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void instrumentMethodWithMyExceptionLogger(ClassBuilder classBuilder, MethodModel methodModel) {
|
||||
methodModel.code().ifPresent(code ->
|
||||
classBuilder.withMethod(methodModel.methodName(), methodModel.methodType(), methodModel.flags().flagsMask(),
|
||||
methodBuilder ->
|
||||
methodBuilder.transformCode(code, ((builder, element) -> {
|
||||
if (element instanceof ReturnInstruction) {
|
||||
builder.aload(0); // load `this` on the stack, ie the Throwable instance
|
||||
builder.invokestatic( // call my code
|
||||
ClassDesc.of("com.github.shautvast.exceptional.ExceptionLogger"), "log",
|
||||
private static void transform(ClassBuilder classBuilder, MethodModel methodModel) {
|
||||
methodModel.code().ifPresent(code -> classBuilder.withMethod(
|
||||
methodModel.methodName(), // copy name, type and modifiers from the original
|
||||
methodModel.methodType(),
|
||||
methodModel.flags().flagsMask(),
|
||||
methodBuilder -> {
|
||||
// keep the existing annotations (and other method elements) in place
|
||||
methodModel.forEachElement(methodBuilder::with);
|
||||
// change the code
|
||||
methodBuilder.transformCode(code, (builder, element) -> {
|
||||
// this way of instrumenting may miss the already loaded classes, java.lang.String for example.
|
||||
// May need to circumvent that
|
||||
if (element instanceof ThrowInstruction) {
|
||||
builder.dup(); // on top of the stack is the current exception instance
|
||||
// duplicate it to make sure the `athrow` op has something to throw
|
||||
// after the invoke to ExceptionLogger has popped one off
|
||||
builder.invokestatic( // call my code with the exception as argument
|
||||
ClassDesc.of(EXCEPTIONLOGGER), "log",
|
||||
MethodTypeDesc.ofDescriptor("(Ljava/lang/Throwable;)V"));
|
||||
builder.return_();
|
||||
} else {
|
||||
builder.with(element); // leave any other bytecode in place
|
||||
}
|
||||
})
|
||||
)));
|
||||
builder.with(element); // leave any other element in place
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
63
lib/dependency-reduced-pom.xml
Normal file
63
lib/dependency-reduced-pom.xml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<parent>
|
||||
<artifactId>exceptional-parent</artifactId>
|
||||
<groupId>org.github.shautvast.exceptional</groupId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>exceptional-lib</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>22</source>
|
||||
<target>22</target>
|
||||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration />
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>RELEASE</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.target>22</maven.compiler.target>
|
||||
<maven.compiler.source>22</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
30
lib/pom.xml
30
lib/pom.xml
|
|
@ -18,6 +18,19 @@
|
|||
<maven.compiler.target>22</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.16.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>RELEASE</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
@ -30,8 +43,23 @@
|
|||
<compilerArgs>--enable-preview</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -1,11 +1,38 @@
|
|||
package com.github.shautvast.exceptional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.lang.foreign.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
|
||||
@SuppressWarnings("unused") // this code is called from the instrumented code
|
||||
public class ExceptionLogger {
|
||||
private static final Arena arena = Arena.ofConfined();
|
||||
private static final Linker linker = Linker.nativeLinker();
|
||||
// //TODO relative path, or configurable
|
||||
private static final SymbolLookup rustlib = SymbolLookup.libraryLookup("/Users/Shautvast/dev/exceptional/rustlib/target/debug/librustlib.dylib", arena);
|
||||
private final static MethodHandle logNative;
|
||||
private final static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
MemorySegment logFunction = rustlib.find("log_java_exception").orElseThrow();
|
||||
logNative = linker.downcallHandle(logFunction, FunctionDescriptor.ofVoid(
|
||||
ValueLayout.ADDRESS
|
||||
));
|
||||
}
|
||||
|
||||
// how does this behave in a multithreaded context??
|
||||
// probably need a ringbuffer with fixed memory to make this work efficiently
|
||||
public static void log(Throwable throwable) {
|
||||
System.out.print("Logging exception:");
|
||||
Arrays.stream(throwable.getStackTrace()).forEach(System.out::println);
|
||||
try {
|
||||
// use json for now because of ease of integration
|
||||
if (throwable != null) {
|
||||
String json = objectMapper.writeValueAsString(throwable);
|
||||
var data = arena.allocateFrom(json); // reuse instead of reallocating?
|
||||
logNative.invoke(data); // invoke the rust function
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.github.shautvast.exceptional;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ExceptionLoggerTest {
|
||||
|
||||
@Test
|
||||
void test(){
|
||||
ExceptionLogger.log(new Throwable());
|
||||
}
|
||||
|
||||
}
|
||||
96
rustlib/Cargo.lock
generated
Normal file
96
rustlib/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustlib"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
13
rustlib/Cargo.toml
Normal file
13
rustlib/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "rustlib"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
serde_json = { version = "1.0", features = [] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
32
rustlib/src/Throwable.rs
Normal file
32
rustlib/src/Throwable.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Throwable {
|
||||
cause: Option<Box<Throwable>>,
|
||||
#[serde(rename (deserialize = "stackTrace"))]
|
||||
stack_trace: Vec<Stacktrace>,
|
||||
message: Option<String>,
|
||||
suppressed: Vec<String>,
|
||||
#[serde(rename (deserialize = "localizedMessage"))]
|
||||
localized_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Stacktrace{
|
||||
#[serde(rename (deserialize = "classLoaderName"))]
|
||||
class_loader_name: Option<String>,
|
||||
#[serde(rename (deserialize = "moduleName"))]
|
||||
module_name: Option<String>,
|
||||
#[serde(rename (deserialize = "moduleVersion"))]
|
||||
module_version: Option<String>,
|
||||
#[serde(rename (deserialize = "methodName"))]
|
||||
method_name: Option<String>,
|
||||
#[serde(rename (deserialize = "fileName"))]
|
||||
file_name: Option<String>,
|
||||
#[serde(rename (deserialize = "lineNumber"))]
|
||||
line_number: Option<u32>,
|
||||
#[serde(rename (deserialize = "className"))]
|
||||
class_name: Option<String>,
|
||||
#[serde(rename (deserialize = "nativeMethod"))]
|
||||
native_method: Option<bool>,
|
||||
}
|
||||
17
rustlib/src/lib.rs
Normal file
17
rustlib/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
mod throwable;
|
||||
|
||||
use std::ffi::{c_char, CStr};
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn log_java_exception(raw_string: *const c_char) {
|
||||
let c_str = unsafe { CStr::from_ptr(raw_string) };
|
||||
let string = c_str.to_str();
|
||||
if let Ok(json) = string {
|
||||
println!("receiving {}", json);
|
||||
let error: throwable::Throwable = serde_json::from_str(json).unwrap();
|
||||
println!("{:?}", error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {}
|
||||
Loading…
Add table
Reference in a new issue