# Java & Spring Boot

BugWatch has no Java library to install. Java and Kotlin services report through the **OpenTelemetry Java agent**, which BugWatch receives natively over OTLP: errors, your application logs, distributed traces and JVM metrics, with no code change to start.

Verified against `opentelemetry-javaagent` **2.31.1** on JDK 17 / Spring Boot 3.3.

## Why an agent instead of an SDK

The agent already instruments Spring MVC, WebFlux, JDBC, Kafka, Redis, gRPC and your logging framework. A BugWatch-specific Java SDK would re-implement that and give you less. You also keep a vendor-neutral wire format: the same process can be pointed anywhere that speaks OTLP.

> The Android SDK (`cloud.newinstance:bugwatch`) is **not** usable here. It is an Android library that needs an `android.app.Application`, so it cannot load in a server JVM. See [Mobile → Android](/mobile) for app work.

## 1. Download the agent

```sh
curl -L -o opentelemetry-javaagent.jar \
  https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
```

## 2. Point it at BugWatch

```sh
OTEL_SERVICE_NAME=my-spring-app
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.newinstance.cloud
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=KEYID:secret
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_LOGS_EXPORTER=otlp
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.4.2
OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_MDC_ATTRIBUTES_INCLUDED=*
OTEL_JAVA_DISABLED_RESOURCE_PROVIDERS=io.opentelemetry.instrumentation.resources.ProcessResourceProvider
```

Use the project's DSN key (Settings → DSN keys). The key binds every event to one project **and one environment**, so `sk_live_` and `sk_test_` keys are not interchangeable. `service.version` becomes the release on every issue, which is what gives you regression tracking per deploy.

The last two lines matter more than they look:

- `…MDC_ATTRIBUTES_INCLUDED=*` sends your MDC as searchable tags. The older `…capture-mdc-attributes` key still works but is **deprecated** and logs a warning on 2.31+.
- `OTEL_JAVA_DISABLED_RESOURCE_PROVIDERS=…ProcessResourceProvider` stops the agent attaching `process.command_args` to every event. Left on, that tag carries your **full JVM command line**, so any secret passed as a `-D` flag rides along on every issue, and it eats tag budget (the cap is 50 per event). Disabling the provider dropped a sample issue from 24 tags to 16 with no loss of `service.*` or MDC. Note that `OTEL_EXPERIMENTAL_RESOURCE_DISABLED_KEYS` does **not** work on the Java agent; use the provider list.

  BugWatch also redacts `process.command_args` and `process.command_line` to `[REDACTED]` on arrival, for logs, traces and metrics alike, so you are covered even if you skip this line. Prefer setting it anyway: the provider list means the data never leaves your process, which is a stronger guarantee than masking it after transmission, and it keeps the tag slot free. If you have other attributes to mask, add their keys under Project settings → data redaction.

## 3. Run with the agent

```sh
java -javaagent:./opentelemetry-javaagent.jar -jar target/my-spring-app.jar
```

Gradle: add it to `bootRun` `jvmArgs`. Containers: `JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar`, which needs no change to your entrypoint.

## 4. Catch unhandled exceptions

Spring returns a 500 for an unhandled exception, but what reaches BugWatch depends on whether anything logged the throwable. Make it explicit:

```java
@RestControllerAdvice
public class BugWatchExceptionReporter {

    private static final Logger log = LoggerFactory.getLogger(BugWatchExceptionReporter.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Void> report(Exception ex) {
        log.error("Unhandled exception", ex);
        return ResponseEntity.internalServerError().build();
    }
}
```

Already have a `@ControllerAdvice`? Just confirm it passes the exception as the **second** argument to the logger. `log.error("Payment failed: " + ex.getMessage())` throws the stack trace away; `log.error("Payment failed", ex)` keeps it.

## 5. Log from your own code

This is the whole custom-instrumentation story: **there is no BugWatch API to call.** Your existing SLF4J logger is the instrumentation. Every line the app logs is already being shipped.

```java
private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);

public void applyCoupon(String code, String orderId) {
    MDC.put("order.id", orderId);
    MDC.put("customer.tier", tierFor(orderId));
    try {
        redeem(code);
    } catch (IllegalArgumentException ex) {
        log.error("Coupon rejected at checkout", ex);
    } finally {
        MDC.clear();
    }
}
```

That caught exception becomes its own issue, grouped as `java.lang.IllegalArgumentException`, separate from your unhandled 500s, and carries `order.id` and `customer.tier` as tags you can filter on. Clear the MDC in a `finally`: pooled request threads are reused, and stale MDC will mislabel the next request.

## What becomes an issue, and what becomes a log

| You write | Where it lands |
|-----------|----------------|
| `log.error("msg", ex)` | **Issue**, grouped by exception type + stack trace |
| `log.error("msg")` (no throwable) | **Issue**, grouped by message, type `Log` |
| `log.warn` / `info` / `debug` / `trace` | [Logs](/logs), searchable, not an issue |
| Uncaught exception through Spring MVC | **Issue**, once something logs the throwable |

SLF4J levels map straight through: TRACE→trace, DEBUG→debug, INFO→info, WARN→warn, ERROR→error. Anything carrying a throwable is raised to error even if logged at a lower level. The issue cutoff is a per-project setting (Project settings → issue level threshold), `error` by default.

**Volume matters.** The agent captures *everything* Logback emits, framework and startup lines included. A trivial Spring Boot app serving five requests produced 42 events, of which 3 were issues. Set your Logback root level deliberately before pointing production at it, and check your plan's event allowance.

## Correlation and the rest of the stack

Log records carry the active `traceId` and `spanId`, so an issue links to the exact request that produced it in **Trace Explorer**. Traces and JVM metrics (heap, GC, threads, HTTP server timings) flow from the same agent with no extra setup.

## Without the agent

If you cannot attach a `-javaagent`, add the OpenTelemetry Logback appender (`io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0`) and keep the same `OTEL_EXPORTER_OTLP_*` variables. You get logs and errors, but not the automatic traces, metrics or framework instrumentation.

## Troubleshooting

- Nothing at all: check the key is this project's DSN key, and that `OTEL_EXPORTER_OTLP_ENDPOINT` is the **base** URL. The agent appends `/v1/logs`, `/v1/traces` and `/v1/metrics` itself.
- Traces but no issues: nothing is logging the throwable. Add the advice in step 4.
- Stack traces look short: make sure you pass the exception as the second logger argument rather than interpolating `ex.getMessage()`.
- Add `-Dotel.javaagent.debug=true` to see what the agent exports.
