AgentSkillsCN

logging-observability

为 Spring Boot 应用程序提供 SLF4J/Logback 生产级日志记录、结构化 JSON 日志、用于请求追踪的 MDC、Micrometer 指标,以及健康指标。

SKILL.md
--- frontmatter
name: logging-observability
description: Production logging with SLF4J/Logback, structured JSON logs, MDC for request tracing, Micrometer metrics, and health indicators for Spring Boot applications.
version: "1.0.0"
author: GazApps
tags: [logging, slf4j, logback, micrometer, metrics, observability, tracing, mdc]
dependencies: [java-fundamentals, spring-boot-core]
compatibility: [antigravity, claude-code, gemini-cli]

Logging & Observability for Spring Boot

Production-grade logging, metrics, and health monitoring for Spring Boot 3.x applications.

Use this skill when

  • Setting up structured logging for a Spring Boot application
  • Adding request tracing with correlation IDs
  • Configuring JSON log output for production (ELK, Datadog, CloudWatch)
  • Adding Micrometer metrics (counters, timers, gauges)
  • Creating custom health indicators
  • Configuring Spring Boot Actuator endpoints
  • Implementing performance logging or method-level tracing
  • Masking sensitive data in log output
  • User mentions "logging", "metrics", "observability", "tracing", or "monitoring"

Do not use this skill when

  • User needs distributed tracing with Zipkin/Jaeger (use a dedicated tracing skill)
  • User needs full APM solution (Datadog agent, New Relic)
  • User needs log aggregation infrastructure setup (ELK stack deployment)
  • User only needs basic System.out.println debugging (use java-fundamentals)

Dependencies (pom.xml)

xml
<dependencies>
    <!-- Logging (included via spring-boot-starter) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- Structured JSON logging -->
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
        <version>7.4</version>
    </dependency>

    <!-- Actuator for health/metrics endpoints -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer Prometheus registry (optional, for /prometheus endpoint) -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- AOP for logging aspects -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

Logging Levels

LevelWhen to UseExample
ERRORSystem is broken, requires immediate attention. Failed operations that impact users.Database connection lost, payment processing failed, unrecoverable state
WARNUnexpected situation, system can recover. Potential issues that may need investigation.Retry succeeded after failure, deprecated API called, cache miss on expected hit
INFOKey business events and application lifecycle. What happened at a high level.User registered, order placed, application started, scheduled job completed
DEBUGDetailed flow for troubleshooting. Only enabled in development or when diagnosing issues.Method entry/exit, query parameters, intermediate computation results
TRACEVery fine-grained diagnostic info. Rarely enabled even in development.Full request/response bodies, loop iterations, byte-level data

Logger Creation

java
// Option 1: SLF4J direct (preferred — no extra dependency)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
}

// Option 2: Lombok @Slf4j (if Lombok is in the project)
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class OrderService {
    // log field is auto-generated
}

Parameterized Logging

Always use parameterized messages. Never concatenate strings.

java
// CORRECT: parameterized — no string concatenation cost if level is disabled
log.info("Order {} placed by user {} for amount {}", orderId, userId, amount);

// WRONG: string concatenation — always evaluated even if INFO is disabled
log.info("Order " + orderId + " placed by user " + userId);

// Logging exceptions — exception is ALWAYS the last argument, no placeholder
log.error("Failed to process order {}", orderId, exception);

// Guard expensive operations
if (log.isDebugEnabled()) {
    log.debug("Full order details: {}", order.toDetailedString());
}

Structured Logging with JSON

Use logstash-logback-encoder to produce JSON logs consumable by ELK, Datadog, CloudWatch.

java
import static net.logstash.logback.argument.StructuredArguments.*;

// Adds key-value pairs to the JSON log entry
log.info("Order placed", kv("orderId", orderId), kv("userId", userId), kv("amount", amount));

// Output: {"message":"Order placed","orderId":"ORD-123","userId":"USR-456","amount":99.99,...}

// keyValue() — includes in message AND JSON fields
log.info("Processing {}", keyValue("orderId", orderId));
// Output message: "Processing orderId=ORD-123" + JSON field "orderId":"ORD-123"

// value() — includes in message only, no JSON field
log.info("Processing order {}", value("orderId", orderId));
// Output message: "Processing order ORD-123"

MDC (Mapped Diagnostic Context)

MDC attaches key-value pairs to the current thread. Every log statement on that thread automatically includes the MDC values.

java
import org.slf4j.MDC;

// Set at request entry point (filter or interceptor)
MDC.put("correlationId", UUID.randomUUID().toString());
MDC.put("userId", authenticatedUser.getId());

try {
    // All log statements in this thread now include correlationId and userId
    log.info("Processing request");  // correlationId and userId are in the log output
    orderService.placeOrder(request);
} finally {
    MDC.clear();  // ALWAYS clear in a finally block to prevent thread pool leaks
}

Correlation ID Pattern

Every HTTP request gets a unique correlation ID. Pass it through all service calls.

java
// In a servlet filter (see examples/RequestLoggingFilter.java):
String correlationId = request.getHeader("X-Correlation-ID");
if (correlationId == null || correlationId.isBlank()) {
    correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);

// In Logback config, include it in every log line:
// Pattern: %d{ISO8601} [%X{correlationId}] %-5level %logger{36} - %msg%n
// JSON: automatically included by logstash-logback-encoder when in MDC

Logback Configuration

Place logback-spring.xml in src/main/resources/. Spring Boot profiles control which appenders are active.

xml
<!-- Dev profile: colorized console output -->
<springProfile name="dev">
    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
</springProfile>

<!-- Prod profile: JSON to stdout + rolling file -->
<springProfile name="prod">
    <root level="INFO">
        <appender-ref ref="JSON_CONSOLE" />
        <appender-ref ref="ASYNC_FILE" />
    </root>
</springProfile>

See examples/logback-spring.xml for the full configuration.

Micrometer Metrics

Micrometer is the metrics facade for Spring Boot (like SLF4J is for logging).

Counters — track totals

java
private final Counter orderCounter;

public OrderService(MeterRegistry registry) {
    this.orderCounter = Counter.builder("orders.placed.total")
        .description("Total orders placed")
        .tag("type", "standard")
        .register(registry);
}

public void placeOrder(Order order) {
    orderCounter.increment();
}

Timers — track duration

java
private final Timer orderTimer;

public OrderService(MeterRegistry registry) {
    this.orderTimer = Timer.builder("orders.processing.duration")
        .description("Order processing time")
        .publishPercentiles(0.5, 0.95, 0.99)
        .register(registry);
}

public void placeOrder(Order order) {
    orderTimer.record(() -> {
        // processing logic
    });
}

Gauges — track current value

java
private final AtomicInteger activeOrders = new AtomicInteger(0);

public OrderService(MeterRegistry registry) {
    Gauge.builder("orders.active.count", activeOrders, AtomicInteger::get)
        .description("Currently active orders")
        .register(registry);
}

Custom Health Indicators

java
@Component
public class DatabaseHealthIndicator extends AbstractHealthIndicator {

    @Override
    protected void doHealthCheck(Health.Builder builder) {
        // Check external dependency
        if (isDatabaseReachable()) {
            builder.up()
                .withDetail("database", "PostgreSQL")
                .withDetail("responseTime", "12ms");
        } else {
            builder.down()
                .withDetail("error", "Cannot connect to database");
        }
    }
}

Shows at GET /actuator/health:

json
{
  "status": "UP",
  "components": {
    "database": {
      "status": "UP",
      "details": { "database": "PostgreSQL", "responseTime": "12ms" }
    }
  }
}

Spring Boot Actuator Endpoints

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers
  endpoint:
    health:
      show-details: when_authorized
EndpointPurpose
/actuator/healthApplication health status
/actuator/metricsList all metric names
/actuator/metrics/{name}Specific metric value
/actuator/prometheusPrometheus scrape endpoint
/actuator/loggersView and change log levels at runtime
/actuator/infoApplication information

Change log level at runtime

bash
# View current level
curl http://localhost:8080/actuator/loggers/com.example.app

# Change to DEBUG (no restart needed)
curl -X POST http://localhost:8080/actuator/loggers/com.example.app \
  -H 'Content-Type: application/json' \
  -d '{"configuredLevel": "DEBUG"}'

Performance Logging Patterns

java
// Timed annotation (Micrometer) — auto-creates timer metric
@Timed(value = "service.orders.find", description = "Time to find orders")
public List<Order> findOrders(SearchCriteria criteria) {
    // method body
}

// Manual stopwatch for complex flows
long start = System.nanoTime();
try {
    // operation
} finally {
    long durationMs = (System.nanoTime() - start) / 1_000_000;
    log.info("Operation completed in {}ms", durationMs);
}

Sensitive Data Masking

Never log passwords, tokens, credit card numbers, SSNs, or PII.

java
// WRONG: logs the full credit card number
log.info("Payment with card {}", cardNumber);

// CORRECT: mask sensitive data
log.info("Payment with card {}", maskCard(cardNumber));

public static String maskCard(String card) {
    if (card == null || card.length() < 4) return "****";
    return "****-****-****-" + card.substring(card.length() - 4);
}

// WRONG: logs the entire request body which may contain passwords
log.debug("Request body: {}", requestBody);

// CORRECT: log only safe fields
log.debug("Login attempt for user {}", request.getUsername());

Logback Masking Pattern (logback-spring.xml)

xml
<!-- Replace patterns matching sensitive data in log messages -->
<conversionRule conversionWord="maskedMsg"
    converterClass="com.example.app.logging.SensitiveDataMaskingConverter" />

Code Quality Checklist

  • Loggers use LoggerFactory.getLogger(ClassName.class) or @Slf4j
  • All log messages use parameterized format (no string concatenation)
  • Exceptions logged with log.error("message", exception) (exception as last arg)
  • MDC cleared in finally blocks to prevent thread pool leaks
  • Correlation ID propagated through all service layers
  • logback-spring.xml uses Spring profiles (dev: console, prod: JSON)
  • No sensitive data (passwords, tokens, PII) in log messages
  • Business metrics tracked with Micrometer counters/timers
  • Custom health indicators for all external dependencies
  • Actuator endpoints secured in production
  • DEBUG/TRACE guarded with log.isDebugEnabled() for expensive operations
  • Rolling file policy configured to prevent disk space exhaustion
  • Async appender used in production for high-throughput logging

References

  • See references/logging-levels-guide.md for detailed level guidance
  • See references/actuator-endpoints.md for full Actuator reference
  • See examples/ for complete code examples