Java observability
A Claude Code plugin and marketplace providing a skills library for high-quality Java development.
npx -y skills add mtkhawaja/java-skills --skill java-observabilityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when adding, reviewing, or retrofitting logging, metrics, tracing, or diagnostics in Java — including "improve the logging/observability of this code" requests. Covers the logging facade with parameterized structured messages, the full level palette (DEBUG/INFO/WARN/ERROR) across a unit of work, correlation IDs / MDC, never logging secrets/PII, preserving stack traces, and Micrometer / OpenTelemetry. Catches string-concatenated logs, System.out, sensitive-data logging, and missing correlation/telemetry.
SKILL.md
7.2 KB, as published. Nobody here has run it
Java Observability
Overview
Make a running system diagnosable. Three pillars: logs (what happened), metrics (how much/how often), traces (where time went).
Logging
- Use the project's facade — prefer SLF4J; else the project standard (Log4j2). Never
introduce a new framework (including
java.util.logging). Never useSystem.out/System.err. - Parameterized, not concatenated:
log.info("created order {}", id)— never"created order " + id. Placeholders defer string building until the level is enabled. - Levels deliberately: TRACE (rare low-level), DEBUG (diagnostics), INFO (major events), WARN (recoverable/unexpected), ERROR (failures needing investigation).
- Preserve stack traces: pass the exception as the last arg (
log.error("charge failed {}", id, ex)), never justex.getMessage(). - Log once at the boundary where the error is best understood; not in tight loops/getters.
- Never log sensitive data: no passwords, tokens, card numbers, PII, or full request/response payloads. Log identifiers and counts (IDs, status, sizes), not whole objects.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class PaymentProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(PaymentProcessor.class);
public Receipt process(final Payment payment) {
LOGGER.info("Processing payment customerId={} amount={}", payment.customerId(), payment.amount());
// never log cardNumber/cvv/tokens
...
}
}
Structured logging & correlation
- Prefer structured/key-value output (logstash/ECS encoder, or consistent
key={}pairs) so logs are queryable, not just human-readable. - Carry a correlation/trace id across a request via MDC (
MDC.put("traceId", id)); set it at the entry boundary (filter/interceptor) and clear it infinallyto avoid thread-pool leakage. For keys you add deeper in the flow,MDC.remove(key)just those infinally—MDC.clear()there would wipe the context the boundary owns. Put stable context (userId, operation) in MDC too, not hand-concatenated into every message.
MDC.put("traceId", traceId);
try {
handle(request);
} finally {
MDC.clear(); // prevent context bleeding across pooled threads
}
Metrics & tracing
- Metrics: use the project's metrics facade — Micrometer (
MeterRegistry) in Spring — for counters/timers/gauges; don't hand-roll. Name consistently (orders.placed), tag with low cardinality (avoid user ids as tags). Time critical paths with aTimer. - Tracing: prefer OpenTelemetry (or the project's tracer). Don't manually thread span/trace ids through method signatures — propagate via context (and mirror the trace id into MDC so logs correlate with spans). Instrument at boundaries (HTTP, messaging, DB), not every method.
- Emit telemetry as a side concern: it must not change control flow or behavior.
public final class PaymentProcessor {
private final Timer chargeTimer;
public PaymentProcessor(final MeterRegistry registry) {
this.chargeTimer = registry.timer("payments.charge"); // stable name, no per-user tags
}
public Receipt charge(final Payment payment) {
return chargeTimer.record(() -> gateway.charge(payment));
}
}
Time in finally. Manual timing (elapsed-time logs, Timer.Sample) must stop/log in a
finally block — placed after the call, an exception skips it, losing timing for exactly the
calls you care about. Timer.record(...) does this for you.
// ❌ skipped when charge() throws
final long start = System.nanoTime();
final Receipt receipt = gateway.charge(payment);
LOGGER.debug("charge durationMs={}", (System.nanoTime() - start) / 1_000_000);
// ✅ recorded on success and failure alike
final Timer.Sample sample = Timer.start(registry);
try {
return gateway.charge(payment);
} finally {
sample.stop(chargeTimer);
}
Retrofitting existing code
Asked to "improve the logging/observability" of existing code? Work per unit of work (request/message/job), not per statement:
- Correlate first: MDC ids at the entry boundary, cleared in
finally(MDC.removefor keys added mid-flow). - Replace offenders:
System.out/printStackTrace()/ concatenation → facade +key={}pairs. - Narrate the unit of work with the full level palette:
- INFO — start and outcome: identifiers + counts, one line each, never per item
- DEBUG — per-item / per-step detail inside loops and branches
- WARN — recoverable anomalies: retries, fallbacks, skipped items, partial success
(
WARN "Fulfilled 3 of 5 lines"beats an INFO that hides the mismatch in two numbers) - ERROR — the unit of work failed; exception as last arg, once, at the boundary
- Measure rates/latency instead of logging them: a
Timeron the critical path,Counters for failure/skip events. - Sweep before finishing: no PII/secrets/payloads crept in; telemetry changed no behavior.
Common mistakes
| Rationalization | Reality |
|---|---|
""user: " + name" | Use {} placeholders — concatenation allocates even when the level is off. |
"java.util.logging is built in" | Use the project facade (SLF4J/Log4j2); don't add a framework. |
| "Log the whole request to debug" | Never log secrets/PII/full payloads; log identifiers. |
"log.error(e.getMessage())" | Drops the stack trace — pass the exception. |
| "I'll pass the traceId as a parameter everywhere" | Use MDC / context propagation; clear MDC in finally. |
"A userId tag on the metric is handy" | High-cardinality tags blow up metrics storage; tag low-cardinality only. |
| "Log the duration after the call returns" | An exception skips it — stop/log timing in finally, or use Timer.record. |
Red flags — stop
+inside alog.*call;System.out/System.err; a new logging framework- A secret/token/card number/PII or whole payload in a log call;
log.error(e.getMessage()) MDC.putwithout a matchingMDC.clear()infinally- Hand-rolled counters/timers instead of Micrometer; high-cardinality metric tags
- A duration log or
Timer.Sample.stopthat isn't in afinallyblock - Telemetry that alters control flow