Java observability
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.From its SKILL.md
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.
SKILL.md
7.2 KB, ~1.6k tokens by cl100k_base, 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
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 1 of the 12 instructions most monitoring observability skills give in ~1.6k tokens
Counted across 530 of the 532 authors here whose files we hold, read 2026-09-06
- Use structured JSON loggingin 40 of 530, across 36 files
- Link every alert to a runbookin 29 of 530, across 27 files
- Attach correlation IDs to every log linein 19 of 530, across 16 files
- Alert on symptoms rather than causesin 19 of 530, across 17 files
- Use OpenTelemetry for distributed tracinghere, and in 15 of 530, across 14 files
- Alert on symptoms users feelin 15 of 530, across 13 files
- Implement health check endpointsin 14 of 530, across 10 files
- Inspect existing dashboards firstin 12 of 530, across 4 files
- Build the minimum useful boardin 12 of 530, across 4 files
- Start from operator questionsin 12 of 530, across 4 files
- Propagate trace context across boundariesin 11 of 530, across 10 files
- Include trace id in all log entriesin 10 of 530, across 9 files
Said here and by no other author read
- Use the project logging facade
- Use parameterized logging placeholders
- Preserve stack traces in error logs
- Never log sensitive data or PII
- Use Micrometer for metrics and counters
- Stop timing measurements in finally blocks
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.