Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ByteBuddy agent #69

Merged
merged 6 commits into from
Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 76 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,19 @@ If you have library code that doesn't pass around `ILoggerFactory` and doesn't l

I like this approach better than the annotation or aspect-oriented programming approaches, because it is completely transparent to the code and gives the same performance as inline code. I use a `ThreadLocal` logger here, as it gives me more control over logging capabilities than using MDC would, but there are many options available.

There are two ways you can instrument code. The first way is to do it in process, after the JVM has loaded. The second way is to load the java agent before the JVM starts, which lets you instrument classes on the system classloader.

### Instrumenting Application Code

The in process instrumentation is done with `com.tersesystems.logback.bytebuddy.LoggingInstrumentationByteBuddyBuilder`, which takes in some configuration and then installs itself on the byte buddy agent.

```java
new LoggingInstrumentationByteBuddyBuilder()
.builderFromConfig(loggingAdviceConfig)
.with(debugListener)
.installOnByteBuddyAgent();
```

This is driven from configuration, so with the following code:

```java
Expand Down Expand Up @@ -680,30 +693,32 @@ bytebuddy {
We can start up the agent, add in the builder and run through the methods:

```java
public class AgentBasedTest {
public class InProcessInstrumentationExample {

public static void main(String[] args) throws Exception {
// Helps if you install the byte buddy agents before anything else at all happens...
ByteBuddyAgent.install();

Logger logger = LoggerFactory.getLogger(InProcessInstrumentationExample.class);
LoggingInstrumentationAdvice.setLoggerResolver(new FixedLoggerResolver(logger));

Config config = ConfigFactory.load();
List<String> classNames = config.getStringList("bytebuddy.classNames");
List<String> methodNames = config.getStringList("bytebuddy.methodNames");
ClassAdviceConfig classAdviceConfig = ClassAdviceConfig.create(classNames, methodNames);
List<String> classNames = config.getStringList("logback.bytebuddy.classNames");
List<String> methodNames = config.getStringList("logback.bytebuddy.methodNames");
LoggingAdviceConfig loggingAdviceConfig = LoggingAdviceConfig.create(classNames, methodNames);

// The debugging listener shows what classes are being picked up by the instrumentation
Listener debugListener = createDebugListener(classNames);
new ClassAdviceAgentBuilder()
.builderFromConfig(classAdviceConfig)
new LoggingInstrumentationByteBuddyBuilder()
.builderFromConfig(loggingAdviceConfig)
.with(debugListener)
.installOnByteBuddyAgent();

Logger logger = LoggerFactory.getLogger(AgentBasedTest.class);
ThreadLocalLogger.setLogger(logger);

// No code change necessary here, you can wrap completely in the agent...
ClassCalledByAgent classCalledByAgent = new ClassCalledByAgent();
classCalledByAgent.printStatement();
classCalledByAgent.printArgument("42");

try {
classCalledByAgent.throwException("hello world");
} catch (Exception e) {
Expand Down Expand Up @@ -731,6 +746,58 @@ I am a simple println, printing 42

The `[Byte Buddy]` statements up top are caused by the debug listener, and let you know that Byte Buddy has successfully instrumented the class.

### Instrumenting System Classes

Instrumenting system level classes is a bit more involved, but can be done in configuration.

> **NOTE**: There are some limitations to instrumenting code. You cannot instrument native methods like `java.lang.System.currentTimeMillis()` for example.

First, you set the java agent, either directly on the command line:

```bash
java \
-javaagent:path/to/logback-bytebuddy-x.x.x.jar=debug \
-Dconfig.file=conf/application.conf \
-Dlogback.configurationFile=conf/logback-test.xml \
com.example.PreloadedInstrumentationExample
```

or by using `JAVA_TOOLS_OPTIONS` environment variable

```bash
export JAVA_TOOLS_OPTIONS="..."
```

and then in `application.conf`:

```hocon
logback.bytebuddy {
classNames = [ "java.lang.Thread" ]
methodNames = [ "run" ]
}
```

and the code as follows:

```java
public class PreloadedInstrumentationExample {
public static void main(String[] args) throws Exception {
Thread thread = Thread.currentThread();
thread.run();
}
}
```

yields

```text
[Byte Buddy] DISCOVERY java.lang.Thread [null, null, loaded=true]
[Byte Buddy] TRANSFORM java.lang.Thread [null, null, loaded=true]
[Byte Buddy] COMPLETE java.lang.Thread [null, null, loaded=true]
92 TRACE java.lang.Thread - entering: java.lang.Thread.run() with arguments=[]
93 TRACE java.lang.Thread - exiting: java.lang.Thread.run() with arguments=[] => returnType=void
```

## Censoring Sensitive Information

There may be sensitive information that you don't want to show up in the logs. You can get around this by passing your information through a censor. This is a custom bit of code written for Logback, but it's not too complex.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ signing.gnupg.keyName = D66D1D69
# $ HISTCONTROL=ignoreboth ./gradlew clean sign -Psigning.gnupg.passphrase=123456 --info
# signing.gnupg.passphrase = [CENSORED]

bytebuddyVersion = 1.9.9
bytebuddyVersion = 1.10.1
junitVersion = 4.12
junitJupiterVersion = 5.0.1
junitVintageVersion = 4.12.1
Expand Down
36 changes: 34 additions & 2 deletions logback-bytebuddy/logback-bytebuddy.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
plugins {
id 'application'
}

dependencies {
compile "org.slf4j:slf4j-api:$slf4jVersion"
compile "net.bytebuddy:byte-buddy:$bytebuddyVersion"
compile "net.bytebuddy:byte-buddy-agent:$bytebuddyVersion"
compile group: 'com.typesafe', name: 'config', version: "$configVersion"
compile "ch.qos.logback:logback-classic:$logbackVersion"
compile "net.logstash.logback:logstash-logback-encoder:$logstashVersion"
}

jar {
manifest {
attributes(
"Agent-Class": "com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent",
"Can-Redefine-Classes": true,
"Can-Retransform-Classes": true,
"Boot-Class-Path": "byte-buddy-${bytebuddyVersion}.jar slf4j-api-${slf4jVersion}.jar config-${configVersion}.jar logback-core-${logbackVersion}.jar logback-classic-${logbackVersion}.jar logstash-logback-encoder-${logstashVersion}.jar logback-bytebuddy-${version}.jar",
"Premain-Class": "com.tersesystems.logback.bytebuddy.LogbackInstrumentationAgent"
)
}
}

testCompile "ch.qos.logback:logback-classic:$logbackVersion"
testCompile group: 'com.typesafe', name: 'config', version: '1.3.4'
// I had trouble getting a working example in Gradle using a fresh JVM and an agent, so I cheated.
// Here's what I did instead:
//
// gradle distZip
// cd logback-bytebuddy/build/distributions
// unzip logback-bytebuddy-0.0.0.zip
// cd logback-bytebuddy-0.0.0
// bin/logback-bytebuddy
application {
mainClassName = 'com.tersesystems.logback.bytebuddy.PreloadedInstrumentationExample'
applicationDefaultJvmArgs = [
"-Dconfig.file=$buildDir/../src/test/resources/application.conf",
"-Dlogback.configurationFile=$buildDir/../src/test/resources/logback-test.xml",
'-javaagent:lib/logback-bytebuddy-0.0.0.jar',
]
}

config {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import java.util.Collection;
import java.util.List;

/**
* Utils for LoggingAdviceConfig.
*/
public final class ClassAdviceUtils {

public static ElementMatcher.Junction<? super MethodDescription> methodMatcher(Collection<String> methodNames) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-License-Identifier: CC0-1.0
*
* Copyright 2018-2019 Will Sargent.
*
* Licensed under the CC0 Public Domain Dedication;
* You may obtain a copy of the License at
*
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package com.tersesystems.logback.bytebuddy;

import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;

import java.util.Objects;

/**
* Returns the logger with the class that was instrumented.
*/
public class DeclaringTypeLoggerResolver implements LoggerResolver {

private final ILoggerFactory loggerFactory;

public DeclaringTypeLoggerResolver(ILoggerFactory loggerFactory) {
this.loggerFactory = Objects.requireNonNull(loggerFactory);
}

@Override
public Logger resolve(String origin) {
int firstPipe = origin.indexOf('|');
String declaringType = origin.substring(0, firstPipe);
return loggerFactory.getLogger(declaringType);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SPDX-License-Identifier: CC0-1.0
*
* Copyright 2018-2019 Will Sargent.
*
* Licensed under the CC0 Public Domain Dedication;
* You may obtain a copy of the License at
*
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package com.tersesystems.logback.bytebuddy;

import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.util.Objects;

/**
* Configuration with a POJO.
*/
public class DefaultLoggingAdviceConfig implements LoggingAdviceConfig {
private final ElementMatcher.Junction<? super TypeDescription> typeMatcher;
private final ElementMatcher.Junction<? super MethodDescription> methodMatcher;

public DefaultLoggingAdviceConfig(ElementMatcher.Junction<? super TypeDescription> typeMatcher,
ElementMatcher.Junction<? super MethodDescription> methodMatcher) {
this.typeMatcher = Objects.requireNonNull(typeMatcher);
this.methodMatcher = Objects.requireNonNull(methodMatcher);
}

public ElementMatcher.Junction<? super MethodDescription> methods() {
return methodMatcher;
}

public ElementMatcher.Junction<? super TypeDescription> types() {
return typeMatcher;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* SPDX-License-Identifier: CC0-1.0
*
* Copyright 2018-2019 Will Sargent.
*
* Licensed under the CC0 Public Domain Dedication;
* You may obtain a copy of the License at
*
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package com.tersesystems.logback.bytebuddy;

import org.slf4j.Logger;

import static java.util.Objects.requireNonNull;

/**
* Always returns the same logger.
*/
public class FixedLoggerResolver implements LoggerResolver {
private final Logger logger;

public FixedLoggerResolver(Logger logger) {
this.logger = requireNonNull(logger);
}

@Override
public Logger resolve(String origin) {
return logger;
}
}
Loading