Skip to content

Logging Best Practices


Definition

Logging Best Practices


Structured Logging

// UNSTRUCTURED (bad for parsing)
log.info("User [email protected] placed order 12345 for $99.99");

// STRUCTURED (machine-parseable)
log.info("Order placed",
    kv("userId", "user-123"),
    kv("orderId", "12345"),
    kv("amount", 99.99),
    kv("currency", "USD")
);

// Output (JSON):
{
    "timestamp": "2024-01-15T10:30:00.123Z",
    "level": "INFO",
    "message": "Order placed",
    "userId": "user-123",
    "orderId": "12345",
    "amount": 99.99,
    "currency": "USD",
    "service": "order-service",
    "traceId": "abc-123"
}

// SLF4J with Logback/Log4j2
import static net.logstash.logback.argument.StructuredArguments.*;

logger.info("Order created",
    kv("orderId", order.getId()),
    kv("customerId", order.getCustomerId()),
    kv("total", order.getTotal()),
    kv("itemCount", order.getItems().size())
);

// With MDC (Mapped Diagnostic Context)
MDC.put("requestId", requestId);
MDC.put("userId", userId);
try {
    // All logs in this scope include requestId and userId
    logger.info("Processing request");
} finally {
    MDC.clear();
}

What to Log

// REQUEST/RESPONSE BOUNDARIES
@RestController
class OrderController {

    @PostMapping("/orders")
    public Order createOrder(@RequestBody CreateOrderRequest request) {
        logger.info("Received create order request",
            kv("customerId", request.getCustomerId()),
            kv("itemCount", request.getItems().size())
        );

        Order order = orderService.create(request);

        logger.info("Order created successfully",
            kv("orderId", order.getId()),
            kv("total", order.getTotal())
        );

        return order;
    }
}

// BUSINESS EVENTS
logger.info("Payment processed",
    kv("orderId", orderId),
    kv("paymentMethod", "CREDIT_CARD"),
    kv("amount", amount)
);

logger.info("User registered",
    kv("userId", user.getId()),
    kv("email", maskEmail(user.getEmail()))  // Masked!
);

// ERRORS WITH CONTEXT
try {
    processPayment(order);
} catch (PaymentException e) {
    logger.error("Payment processing failed",
        kv("orderId", order.getId()),
        kv("customerId", order.getCustomerId()),
        kv("amount", order.getTotal()),
        kv("paymentMethod", paymentMethod),
        kv("errorCode", e.getCode()),
        e  // Include stack trace
    );
    throw e;
}

// PERFORMANCE
long startTime = System.currentTimeMillis();
List<Product> products = productService.search(query);
long duration = System.currentTimeMillis() - startTime;

logger.info("Product search completed",
    kv("query", query),
    kv("resultCount", products.size()),
    kv("durationMs", duration)
);

What NOT to Log

// DON'T LOG SENSITIVE DATA

// BAD: Logging passwords
logger.info("Login attempt", kv("password", password));

// BAD: Logging full credit card
logger.info("Payment", kv("cardNumber", cardNumber));

// BAD: Logging PII without masking
logger.info("User data", kv("ssn", ssn));

// GOOD: Mask sensitive data
logger.info("Login attempt", kv("username", username));
logger.info("Payment", kv("cardLast4", cardNumber.substring(12)));
logger.info("User data", kv("ssnLast4", "***-**-" + ssn.substring(7)));

// DON'T LOG HIGH VOLUME INTERNAL OPERATIONS

// BAD: Logging inside tight loops
for (Item item : items) {
    logger.debug("Processing item", kv("itemId", item.getId()));
    process(item);
}

// GOOD: Log aggregates
logger.info("Processing items batch",
    kv("batchSize", items.size())
);
for (Item item : items) {
    process(item);
}
logger.info("Batch processing complete",
    kv("successCount", successCount),
    kv("failureCount", failureCount)
);

// SENSITIVE DATA PATTERNS TO AVOID
// - Passwords, API keys, tokens
// - SSN, credit card numbers
// - Health information
// - Personal addresses
// - Session IDs (can enable hijacking)

Correlation IDs

// CORRELATION ID: Trace requests across services

// Generate at entry point (API Gateway or first service)
@Component
class RequestIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) {
        String requestId = request.getHeader("X-Request-ID");
        if (requestId == null) {
            requestId = UUID.randomUUID().toString();
        }

        MDC.put("requestId", requestId);
        try {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("X-Request-ID", requestId);
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

// Propagate to downstream services
@Component
class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        String requestId = MDC.get("requestId");
        if (requestId != null) {
            template.header("X-Request-ID", requestId);
        }
    }
}

// Now logs across services share requestId:
// Order Service:
{"requestId": "abc-123", "message": "Creating order", ...}
// Payment Service:
{"requestId": "abc-123", "message": "Processing payment", ...}
// Inventory Service:
{"requestId": "abc-123", "message": "Reserving items", ...}

// Query all logs for a request:
// requestId:"abc-123"

Log Aggregation

Log Aggregation


Tips & Tricks

Tips & Tricks


  • *