Skip to content
Open
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
176 changes: 176 additions & 0 deletions rollbar-java-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Rollbar Java Agent

A zero-code-change Java instrumentation agent that automatically captures HTTP network errors (4xx and 5xx responses) as Rollbar telemetry events.

It works by attaching to the JVM at startup via `-javaagent:` and using ByteBuddy to intercept HTTP calls across all major clients — no library dependencies or code changes are needed in your application.

## Instrumented HTTP clients

| Client | Condition |
|--------|-----------|
| `java.net.HttpURLConnection` | Always (JDK built-in) |
| `java.net.http.HttpClient` | Java 11+ only |
| Apache HttpClient 4.x (`org.apache.http`) | If present on classpath |
| Apache HttpClient 5.x (`org.apache.hc.client5`) | If present on classpath |

Only 4xx and 5xx responses are recorded. Successful requests (< 400) produce no telemetry.

## Requirements

- Java 11 or higher
- `rollbar-java` on the application classpath (for `Rollbar.init(...)`)

## Installation

### 1. Build the agent JAR

```bash
./gradlew :rollbar-java-agent:shadowJar
```

The fat JAR (with ByteBuddy bundled and relocated) is written to:

```
rollbar-java-agent/build/libs/rollbar-java-agent-<version>.jar
```

### 2. Add the agent JVM flag

Add `-javaagent:` to your JVM startup arguments, pointing at the JAR built above:

```
-javaagent:/path/to/rollbar-java-agent-<version>.jar
```

**Gradle:**
```kotlin
jvmArgs("-javaagent:/path/to/rollbar-java-agent-<version>.jar")
```

**Maven Surefire / Failsafe:**
```xml
<argLine>-javaagent:/path/to/rollbar-java-agent-<version>.jar</argLine>
```

**Docker / environment variable:**
```bash
JAVA_TOOL_OPTIONS="-javaagent:/path/to/rollbar-java-agent-<version>.jar"
```

### 3. Also add the JAR to your classpath

The agent JAR must also be available on the regular application classpath so that your code can call `RollbarAgent.getTelemetryTracker()`:

**Gradle:**
```kotlin
dependencies {
implementation(files("/path/to/rollbar-java-agent-<version>.jar"))
}
```

**Maven:**
```xml
<dependency>
<groupId>com.rollbar</groupId>
<artifactId>rollbar-java-agent</artifactId>
<version>${rollbar.version}</version>
</dependency>
```

### 4. Wire into your Rollbar configuration

```java
import com.rollbar.agent.RollbarAgent;
import com.rollbar.notifier.Rollbar;

import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken;

Rollbar rollbar = Rollbar.init(
withAccessToken("your-access-token")
.environment("production")
.telemetryEventTracker(RollbarAgent.getTelemetryTracker())
.build()
);
```

That's it. All HTTP calls your application makes from that point on will automatically produce telemetry events in the Rollbar error report for any 4xx or 5xx response.

## Behavior

| Scenario | Action |
|----------|--------|
| Response status `< 400` | No telemetry recorded |
| Response status `>= 400` | Records a network telemetry event with `Level.CRITICAL` |
| Connection failure / I/O error | Records an error telemetry event |
| No Rollbar config wired | Events accumulate in the agent store (capacity 100); nothing is sent |

## Security

URLs can carry sensitive data in query parameters or basic-auth credentials. The agent **strips userinfo, query parameters, and the URL fragment** before recording.

For example, a request to:
```
https://user:secret@api.example.com/charge?token=sk_live_abc#section
```
is recorded as:
```
https://api.example.com/charge
```

## Testing

### Automated tests

```bash
./gradlew :rollbar-java-agent:test
```

This runs the full test suite (WireMock-backed integration tests for each instrumented client).

### Manual smoke test

1. Build the agent JAR:
```bash
./gradlew :rollbar-java-agent:shadowJar
```

2. Write a small program that triggers a 4xx or 5xx:
```java
import com.rollbar.agent.RollbarAgent;
import com.rollbar.notifier.Rollbar;

import java.net.HttpURLConnection;
import java.net.URL;

import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken;

public class SmokeTest {
public static void main(String[] args) throws Exception {
Rollbar rollbar = Rollbar.init(
withAccessToken("your-access-token")
.environment("test")
.telemetryEventTracker(RollbarAgent.getTelemetryTracker())
.build()
);

// Trigger a 404 — captured as a telemetry event on the next error report
HttpURLConnection conn = (HttpURLConnection) new URL("https://httpstat.us/404").openConnection();
int code = conn.getResponseCode();
conn.disconnect();

System.out.println("Response: " + code);

// Send an error to Rollbar — the 404 telemetry event will appear alongside it
rollbar.error(new RuntimeException("smoke test error"));
}
}
```

3. Run with the agent:
```bash
java -javaagent:rollbar-java-agent/build/libs/rollbar-java-agent-<version>.jar \
-cp "rollbar-java-agent/build/libs/rollbar-java-agent-<version>.jar:your-app.jar" \
SmokeTest
```

4. Check your Rollbar dashboard — the error report for "smoke test error" should show a **Network** telemetry event for the 404 in the telemetry timeline.
56 changes: 56 additions & 0 deletions rollbar-java-agent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
plugins {
`java-library`
id("com.github.johnrengelman.shadow") version "8.1.1"
}

dependencies {
implementation("net.bytebuddy:byte-buddy:1.14.18")
implementation("net.bytebuddy:byte-buddy-agent:1.14.18")
api(project(":rollbar-api"))
implementation(project(":rollbar-java"))
compileOnly("org.apache.httpcomponents:httpclient:4.5.14")
compileOnly("org.apache.httpcomponents.client5:httpclient5:5.3.1")

testImplementation(platform("org.junit:junit-bom:5.14.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("org.wiremock:wiremock:3.13.2")
testImplementation("org.apache.httpcomponents:httpclient:4.5.14")
testImplementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
}

tasks.jar {
manifest {
attributes(
"Premain-Class" to "com.rollbar.agent.RollbarAgent",
"Agent-Class" to "com.rollbar.agent.RollbarAgent",
"Can-Redefine-Classes" to "true",
"Can-Retransform-Classes" to "true"
)
}
}

tasks.shadowJar {
archiveClassifier.set("")
relocate("net.bytebuddy", "com.rollbar.agent.shaded.bytebuddy")
mergeServiceFiles()
}

// Override root's Java 8 compatibility — this agent targets Java 11+ to support
// java.net.http.HttpClient instrumentation.
tasks.withType<JavaCompile>().configureEach {
options.release.set(11)
}

tasks.test {
useJUnitPlatform()
val agentJar = tasks.shadowJar.get().archiveFile.get().asFile
// Load as Java agent (instruments HTTP classes on startup)
jvmArgs("-javaagent:$agentJar")
// Also put on test classpath — the TCCL reflection bridge finds agent classes via the
// system classloader; mirrors production use where rollbar-java-agent is a Gradle/Maven dep
classpath += files(agentJar)
dependsOn(tasks.shadowJar)
dependsOn(tasks.jar)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.rollbar.agent;

import com.rollbar.api.payload.data.TelemetryEvent;
import com.rollbar.notifier.provider.Provider;
import com.rollbar.notifier.telemetry.RollbarTelemetryEventTracker;
import com.rollbar.notifier.telemetry.TelemetryEventTracker;

import java.util.List;

public final class AgentTelemetryStore {

private static volatile TelemetryEventTracker INSTANCE =
new RollbarTelemetryEventTracker(System::currentTimeMillis, 100);

private AgentTelemetryStore() {}

public static TelemetryEventTracker getInstance() {
return INSTANCE;
}

public static List<TelemetryEvent> getAll() {
return INSTANCE.getAll();
}

public static void init(Provider<Long> timestampProvider) {
INSTANCE = new RollbarTelemetryEventTracker(timestampProvider, 100);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.rollbar.agent;

import com.rollbar.api.payload.data.Level;
import com.rollbar.api.payload.data.Source;

import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;

/**
* Called by JDK-class advice via reflection to bridge the classloader gap.
*
* <p>ByteBuddy advice inlined into bootstrap/platform classloader classes (e.g.
* {@code HttpURLConnection}, {@code HttpClient}) cannot directly reference application-classloader
* classes. Advice code uses {@code Thread.currentThread().getContextClassLoader().loadClass(...)}
* to reach this class and delegates all Rollbar-specific logic here.
*/
public final class NetworkEventBridge {

// Tracks connections/responses already recorded to deduplicate re-entrant calls.
// WeakHashMap so entries are garbage-collected when the connection is released.
private static final Set<Object> RECORDED = Collections.newSetFromMap(
Collections.synchronizedMap(new WeakHashMap<>())
);

private NetworkEventBridge() {}

public static void resetRecordedForTesting() {
RECORDED.clear();
}

/**
* Marks the given key as recorded. Returns {@code true} if this is the first time,
* {@code false} if already recorded (duplicate/re-entrant call).
*/
public static boolean markAsRecorded(Object key) {
return RECORDED.add(key);
}

/**
* Records a network telemetry event for the given key if not already recorded.
*
* <p>Uses the key as a deduplication token — subsequent calls with the same key are ignored.
*/
public static void recordNetworkEvent(Object key, String method, String url, String statusCode) {
if (!markAsRecorded(key)) {
return; // deduplicate re-entrant calls for the same connection
}
AgentTelemetryStore.getInstance().recordNetworkEventFor(
Level.CRITICAL,
Source.SERVER,
method,
UrlSanitizer.sanitize(url),
statusCode
);
}

/**
* Records a manual error telemetry event with the given message.
*
* <p>Called when an HTTP request fails with an I/O exception rather than a status code.
*/
public static void recordError(String message) {
AgentTelemetryStore.getInstance().recordManualEventFor(
Level.CRITICAL,
Source.SERVER,
"Network error: " + (message != null ? message : "unknown")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.rollbar.agent;

import com.rollbar.agent.instrumentation.ApacheHttpClient4Instrumentation;
import com.rollbar.agent.instrumentation.ApacheHttpClient5Instrumentation;
import com.rollbar.agent.instrumentation.HttpUrlConnectionInstrumentation;
import com.rollbar.agent.instrumentation.JavaHttpClientInstrumentation;
import com.rollbar.notifier.telemetry.TelemetryEventTracker;
import java.lang.instrument.Instrumentation;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.matcher.ElementMatchers;

/**
* Java agent entry point. Attach with {@code -javaagent:/path/to/rollbar-java-agent.jar}.
*
* <p>Wire into your Rollbar configuration with:
* <pre>
* Rollbar.init(withAccessToken("...")
* .telemetryEventTracker(RollbarAgent.getTelemetryTracker())
* .build());
* </pre>
*/
public class RollbarAgent {

private RollbarAgent() {}

public static void premain(String args, Instrumentation inst) {
installInstrumentation(inst);
}

public static void agentmain(String args, Instrumentation inst) {
installInstrumentation(inst);
}

private static void installInstrumentation(Instrumentation inst) {
// Override ByteBuddy's default which ignores all java.* and javax.* classes,
// so we can instrument JDK HTTP clients (HttpURLConnection, HttpClient).
// We still ignore ByteBuddy's own classes to avoid instrumentation loops.
AgentBuilder builder = new AgentBuilder.Default()
.ignore(ElementMatchers.nameStartsWith("net.bytebuddy.")
.or(ElementMatchers.nameStartsWith("com.rollbar.agent.shaded.")))
.with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
.with(AgentBuilder.TypeStrategy.Default.REDEFINE);

HttpUrlConnectionInstrumentation.install(builder, inst);
JavaHttpClientInstrumentation.installIfAvailable(builder, inst);
ApacheHttpClient4Instrumentation.installIfAvailable(builder, inst);
ApacheHttpClient5Instrumentation.installIfAvailable(builder, inst);
}

public static TelemetryEventTracker getTelemetryTracker() {
return AgentTelemetryStore.getInstance();
}
}
Loading
Loading