Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5762538
feat(java-agent): add rollbar-java-agent module skeleton
buongarzoni May 25, 2026
70573e5
feat(java-agent): instrument HttpURLConnection and java.net.http.Http…
buongarzoni May 25, 2026
3ee326d
feat(java-agent): instrument Apache HttpClient 4.x and 5.x
buongarzoni May 25, 2026
e5226c1
test(java-agent): add integration tests for instrumented HTTP clients
buongarzoni May 25, 2026
8ab880c
docs(java-agent): add README with installation and manual testing guide
buongarzoni May 25, 2026
ad0b2ac
chore(java-agent): upgrade wiremock to org.wiremock:wiremock:3.13.2
buongarzoni May 26, 2026
64e7df9
refactor(java-agent): replace resetForTesting with injectable Provide…
buongarzoni May 26, 2026
212ce90
fix(java-agent): resolve checkstyle violations in rollbar-java-agent
buongarzoni May 26, 2026
fe91c9a
fix(java-agent): resolve checkstyle violations in rollbar-java-agent
buongarzoni May 26, 2026
866b20b
fix(rollbar-java-agent): declare explicit dependency on jar task in test
buongarzoni May 26, 2026
6791575
feat(java-agent): instrument HttpClient.sendAsync() for async network…
buongarzoni Jun 1, 2026
ba9f0d8
fix(java-agent): fix Apache HC4 transformer to handle all execute() o…
buongarzoni Jun 1, 2026
02fe650
fix(java-agent): fix Apache HC5 transformer to handle all execute() o…
buongarzoni Jun 1, 2026
ebf0c39
docs: update readme of rollbar-java-agent
buongarzoni Jun 1, 2026
5259919
style(java-agent): fix checkstyle line length violations in HC4 and H…
buongarzoni Jun 1, 2026
d9237e4
fix(java-agent): record full URL in HC5 instrumentation
buongarzoni Jun 12, 2026
9510ed8
fix(java-agent): decouple getTelemetryTracker() from INSTANCE identity
buongarzoni Jun 12, 2026
439a3d2
refactor(java-agent): rename AgentTelemetryStore.init() to initForTes…
buongarzoni Jun 12, 2026
9b106d0
fix(java-agent): disable plain jar to prevent overwriting shaded arti…
buongarzoni Jun 29, 2026
ab969f1
fix(java-agent): always install HC4/HC5 transformers regardless of cl…
buongarzoni Jul 5, 2026
efd4c20
feat(java-agent): capture HttpURLConnection requests that skip getRes…
buongarzoni Jul 13, 2026
b46aa7b
fix(java-agent): strip query and userinfo in UrlSanitizer fallback path
buongarzoni Jul 13, 2026
7aadaaa
fix(java-agent): make shadowJar the sole artifact, prevent thin jar f…
buongarzoni Jul 13, 2026
97c3ac3
fix(java-agent): preserve host in UrlSanitizer for underscore hostnam…
buongarzoni Jul 13, 2026
ac57d62
fix(java-agent): prevent getInputStream advice recursion on connectio…
buongarzoni Jul 13, 2026
af9cdee
fix: lint
buongarzoni Jul 13, 2026
69fc32a
chore: update readme
buongarzoni Jul 13, 2026
cbab9d5
fix(java-agent): instrument Apache HC doExecute() to cover all execut…
buongarzoni Jul 13, 2026
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
202 changes: 202 additions & 0 deletions rollbar-java-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# 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` — `send()` and `sendAsync()` | 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, along with requests that fail before a response arrives (connection refused, DNS failure, timeout). Successful requests (< 400) produce no telemetry.

**Apache HC4/HC5:** every `execute(...)` overload is covered — the request-only forms, the target-host forms (`execute(HttpHost, request)`), and the response-handler forms. The agent instruments the protected `doExecute(HttpHost, request, context)` method that all of them converge on, rather than any individual `execute()` overload, so no dispatch path is missed. Requests issued through a target-host overload carry only a path, so the agent rejoins the host from the `HttpHost` argument to record a complete URL.

### HttpURLConnection entry points

`HttpURLConnection` is captured through three entry points, so a failed request is recorded regardless of how your code consumes the response:

| Entry point | Why it is covered |
|-------------|-------------------|
| `getResponseCode()` | The caller checks the status code explicitly. |
| `getInputStream()` | The caller reads the body directly and only ever sees the `IOException` that a 4xx/5xx throws. |
| `getErrorStream()` | The caller inspects the error stream after `connect()`, or after catching the `IOException` from `getInputStream()`. |

Exactly one event is recorded per connection, even when your code hits several of these entry points (for example `getInputStream()` throwing and then `getErrorStream()` being read) — the agent deduplicates on the connection instance.

## 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
```

This fat JAR is the module's only artifact — the thin `jar` task is disabled, and the shaded JAR is what Gradle consumers and the published Maven artifact resolve to. So the JAR you pass to `-javaagent:` and the JAR you put on the classpath (steps 2 and 3) are always the same file.

### 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 (connection refused, DNS failure, timeout) | Records a `Network error: <message>` telemetry event with `Level.CRITICAL` |
| The same request seen through several entry points | Deduplicated — one event per request |
| No Rollbar config wired | Events accumulate in the agent store (capacity 100); nothing is sent |

The agent never throws into your application: every advice body swallows all errors, so a failure inside the instrumentation cannot break an HTTP call.

## 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
```

## Internal API

Two methods exist for tests only. Do not call them in production code — use `RollbarAgent.getTelemetryTracker()` as shown above.

- `AgentTelemetryStore.initForTesting(Provider<Long> timestampProvider)` — replaces the internal tracker with one backed by the given timestamp provider, so tests can assert on event timestamps.
- `NetworkEventBridge.resetRecordedForTesting()` — clears the deduplication state, so events from a previous test do not suppress recording in the next one.

## 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.
65 changes: 65 additions & 0 deletions rollbar-java-agent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 {
enabled = false
}

// java-library wires tasks.jar into apiElements/runtimeElements; replace it with shadowJar so
// Gradle's variant system and vanniktech publishing both see the fat jar as the primary artifact.
listOf(configurations.apiElements, configurations.runtimeElements).forEach { cfg ->
cfg.configure {
outgoing.artifacts.clear()
outgoing.artifact(tasks.shadowJar)
}
}

tasks.shadowJar {
archiveClassifier.set("")
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"
)
}
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)
}
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 initForTesting(Provider<Long> timestampProvider) {
INSTANCE = new RollbarTelemetryEventTracker(timestampProvider, 100);
}
}
Loading
Loading