or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

audit.mdbuiltin-endpoints.mdendpoint-framework.mdhttp-exchanges.mdindex.mdinfo-contributors.mdjmx-integration.mdmanagement-operations.mdoperation-invocation.mdsanitization.mdsecurity.mdweb-integration.md
tile.json

http-exchanges.mddocs/

HTTP Exchange Tracking

QUICK REFERENCE

Key Classes and Packages

// Core Types
org.springframework.boot.actuate.web.exchanges.HttpExchange
org.springframework.boot.actuate.web.exchanges.HttpExchange.Request
org.springframework.boot.actuate.web.exchanges.HttpExchange.Response
org.springframework.boot.actuate.web.exchanges.Include

// Repository
org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository
org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository

// Endpoint
org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint

// Recording Interfaces
org.springframework.boot.actuate.web.exchanges.RecordableHttpRequest
org.springframework.boot.actuate.web.exchanges.RecordableHttpResponse

Recording Strategy Selector

What HTTP data needs tracking?
│
├─ All traffic (headers, timing, principals)?
│  └─ Include.REQUEST_HEADERS, RESPONSE_HEADERS, PRINCIPAL,
│      TIME_TAKEN, REMOTE_ADDRESS
│
├─ Minimal tracking (performance monitoring)?
│  └─ Include.TIME_TAKEN, REMOTE_ADDRESS only
│
├─ Debug mode (everything including cookies)?
│  └─ All Include options
│
└─ Production (exclude sensitive data)?
   └─ Exclude AUTHORIZATION_HEADER, COOKIE_HEADERS

Include Options Quick Reference

OptionData CapturedSensitive?Default?
REQUEST_HEADERSRequest headers (excluding cookies/auth)PartialYes
RESPONSE_HEADERSResponse headers (excluding cookies)NoYes
COOKIE_HEADERSCookie headersYesNo
AUTHORIZATION_HEADERAuthorization headerYesNo
PRINCIPALAuthenticated userNoNo
SESSION_IDSession identifierYesNo
TIME_TAKENRequest durationNoYes
REMOTE_ADDRESSClient IP addressPartialNo

AGENT GUIDANCE

When to Track HTTP Exchanges

Always track:

  • API response times (TIME_TAKEN)
  • Error rates and status codes
  • Remote client addresses (security)

Consider tracking:

  • Request/response headers (debugging)
  • Authenticated principals (auditing)
  • Session IDs (correlation)

Don't track:

  • Request/response bodies (too large)
  • Authorization tokens (security risk)
  • Cookie values (privacy concern)

Pattern vs Anti-Pattern

PATTERN: Production-safe includes

// ✓ Safe for production
Set<Include> includes = Set.of(
    Include.TIME_TAKEN,
    Include.REMOTE_ADDRESS,
    Include.REQUEST_HEADERS,    // Safe (excludes auth)
    Include.RESPONSE_HEADERS,   // Safe (excludes cookies)
    Include.PRINCIPAL          // Non-sensitive
);

ANTI-PATTERN: Include everything

// ❌ Security risk
Set<Include> includes = EnumSet.allOf(Include.class);
// Exposes Authorization headers, cookies, session IDs!

PATTERN: Appropriate capacity

// ✓ Size based on traffic
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
    InMemoryHttpExchangeRepository repository =
        new InMemoryHttpExchangeRepository();

    // 1000 req/min * 60 min = last hour
    repository.setCapacity(60000);

    return repository;
}

ANTI-PATTERN: Unbounded storage

// ❌ Memory leak
@Bean
public HttpExchangeRepository httpExchangeRepository() {
    // Using unlimited custom implementation
    return new UnlimitedHttpExchangeRepository(); // Will grow forever!
}

PATTERN: Analysis-focused queries

// ✓ Structured analysis
List<HttpExchange> slowRequests = repository.findAll().stream()
    .filter(ex -> ex.getTimeTaken() != null)
    .filter(ex -> ex.getTimeTaken().toMillis() > 1000)
    .collect(Collectors.toList());

The HTTP exchange tracking framework records HTTP request-response exchanges for monitoring web traffic, analyzing API usage, and debugging HTTP interactions. It provides configurable capture options, in-memory storage, and actuator endpoint integration.

Capabilities

Core HTTP Exchange Types

/**
 * Interface for HTTP requests that can be recorded by the exchange tracking framework.
 * Implementations provide request metadata needed for exchange recording.
 *
 * Thread-safe: Implementations should be thread-safe
 * Package: org.springframework.boot.actuate.web.exchanges
 * @since 3.0.0
 */
public interface RecordableHttpRequest {

    /**
     * Return the HTTP method of the request.
     *
     * @return the HTTP method (never null)
     */
    String getMethod();

    /**
     * Return the URI of the request.
     *
     * @return the URI (never null)
     */
    URI getUri();

    /**
     * Return the headers of the request.
     *
     * @return map of header names to values (never null, may be empty)
     */
    Map<String, List<String>> getHeaders();

    /**
     * Return the remote address (client IP) of the request, if available.
     *
     * @return the remote address (may be null)
     */
    @Nullable String getRemoteAddress();
}

/**
 * Interface for HTTP responses that can be recorded by the exchange tracking framework.
 * Implementations provide response metadata needed for exchange recording.
 *
 * Thread-safe: Implementations should be thread-safe
 * Package: org.springframework.boot.actuate.web.exchanges
 * @since 3.0.0
 */
public interface RecordableHttpResponse {

    /**
     * Return the HTTP status code of the response.
     *
     * @return the status code
     */
    int getStatus();

    /**
     * Return the headers of the response.
     *
     * @return map of header names to values (never null, may be empty)
     */
    Map<String, List<String>> getHeaders();
}

/**
 * HTTP request and response exchange.
 *
 * Thread-safe: Immutable after construction
 * Nullability: Principal, session, timeTaken can be null
 * Since: Spring Boot 3.0+ (replaces HttpTrace from 2.x)
 */
public final class HttpExchange {

    /**
     * Create HTTP exchange.
     *
     * @param timestamp Exchange timestamp (non-null)
     * @param request Request details (non-null)
     * @param response Response details (non-null)
     * @param principal Authenticated principal (nullable)
     * @param session Session details (nullable)
     * @param timeTaken Request duration (nullable)
     */
    public HttpExchange(Instant timestamp, Request request, Response response,
                       @Nullable Principal principal, @Nullable Session session, @Nullable Duration timeTaken);

    /**
     * Start recording an exchange.
     *
     * @param request Recordable request (non-null)
     * @return Started exchange
     */
    public static Started start(RecordableHttpRequest request);

    /**
     * Start recording with custom clock.
     *
     * @param clock Clock for timestamps
     * @param request Recordable request
     * @return Started exchange
     */
    public static Started start(Clock clock, RecordableHttpRequest request);

    public Instant getTimestamp();
    public Request getRequest();
    public Response getResponse();
    public @Nullable Principal getPrincipal();
    public @Nullable Session getSession();
    public @Nullable Duration getTimeTaken();

    /**
     * Started exchange ready to be finished.
     */
    public static final class Started {

        /**
         * Finish exchange with response using system UTC clock and varargs includes.
         *
         * @param response Recordable response
         * @param principalSupplier Principal supplier
         * @param sessionIdSupplier Session ID supplier
         * @param includes What to include
         * @return Completed exchange
         */
        public HttpExchange finish(RecordableHttpResponse response,
                                  Supplier<@Nullable Principal> principalSupplier,
                                  Supplier<@Nullable String> sessionIdSupplier,
                                  Include... includes);

        /**
         * Finish exchange with response using custom clock and varargs includes.
         *
         * @param clock Clock for timestamp
         * @param response Recordable response
         * @param principalSupplier Principal supplier
         * @param sessionIdSupplier Session ID supplier
         * @param includes What to include
         * @return Completed exchange
         */
        public HttpExchange finish(Clock clock, RecordableHttpResponse response,
                                  Supplier<@Nullable Principal> principalSupplier,
                                  Supplier<@Nullable String> sessionIdSupplier,
                                  Include... includes);

        /**
         * Finish exchange with response using system UTC clock and Set of includes.
         *
         * @param response Recordable response
         * @param principalSupplier Principal supplier
         * @param sessionIdSupplier Session ID supplier
         * @param includes What to include as a Set
         * @return Completed exchange
         */
        public HttpExchange finish(RecordableHttpResponse response,
                                  Supplier<@Nullable Principal> principalSupplier,
                                  Supplier<@Nullable String> sessionIdSupplier,
                                  Set<Include> includes);

        /**
         * Finish exchange with response using custom clock and Set of includes.
         *
         * @param clock Clock for timestamp
         * @param response Recordable response
         * @param principalSupplier Principal supplier
         * @param sessionIdSupplier Session ID supplier
         * @param includes What to include as a Set
         * @return Completed exchange
         */
        public HttpExchange finish(Clock clock, RecordableHttpResponse response,
                                  Supplier<@Nullable Principal> principalSupplier,
                                  Supplier<@Nullable String> sessionIdSupplier,
                                  Set<Include> includes);
    }

    /**
     * HTTP request details.
     */
    public static final class Request {
        public Request(URI uri, @Nullable String remoteAddress, String method,
                      Map<String, List<String>> headers);

        public String getMethod();
        public URI getUri();
        public Map<String, List<String>> getHeaders();
        public @Nullable String getRemoteAddress();
    }

    /**
     * HTTP response details.
     */
    public static final class Response {
        public Response(int status, Map<String, List<String>> headers);

        public int getStatus();
        public Map<String, List<String>> getHeaders();
    }

    /**
     * Principal details.
     */
    public static final class Principal {
        public Principal(String name);
        public String getName();
    }

    /**
     * Session details.
     */
    public static final class Session {
        public Session(String id);
        public String getId();
    }
}

/**
 * Options for what to include in exchanges.
 *
 * Since: Spring Boot 3.0+
 */
public enum Include {
    REQUEST_HEADERS,
    RESPONSE_HEADERS,
    COOKIE_HEADERS,
    AUTHORIZATION_HEADER,
    PRINCIPAL,
    SESSION_ID,
    TIME_TAKEN,
    REMOTE_ADDRESS;

    /**
     * Default includes.
     *
     * @return TIME_TAKEN, REQUEST_HEADERS, RESPONSE_HEADERS
     */
    public static Set<Include> defaultIncludes();
}

HTTP Exchange Repository

Repository for storing and retrieving HTTP exchanges.

/**
 * Repository for storing and retrieving HTTP exchanges.
 *
 * Thread-safe: Implementations should be thread-safe
 * Package: org.springframework.boot.actuate.web.exchanges
 * @since 3.0.0
 */
public interface HttpExchangeRepository {

    /**
     * Return all HTTP exchanges currently stored in the repository.
     *
     * @return list of exchanges (never null, may be empty)
     */
    List<HttpExchange> findAll();

    /**
     * Add a new HTTP exchange to the repository.
     *
     * @param exchange the exchange to add (non-null)
     */
    void add(HttpExchange exchange);
}

/**
 * In-memory implementation of HttpExchangeRepository with circular buffer storage.
 *
 * Thread-safe: Yes (synchronized internally)
 * Default capacity: 100 exchanges
 * Package: org.springframework.boot.actuate.web.exchanges
 * @since 3.0.0
 */
public class InMemoryHttpExchangeRepository implements HttpExchangeRepository {

    /**
     * Create repository with default capacity (100).
     */
    public InMemoryHttpExchangeRepository();

    /**
     * Set maximum number of exchanges to store (circular buffer).
     * Older exchanges are automatically removed when capacity is exceeded.
     *
     * @param capacity maximum exchanges (must be positive)
     */
    public void setCapacity(int capacity);

    /**
     * Get current capacity setting.
     *
     * @return maximum number of exchanges
     */
    public int getCapacity();

    /**
     * Set whether exchanges should be listed in reverse order (newest first).
     * By default, exchanges are stored in reverse order.
     *
     * @param reverse true for reverse order (newest first), false for natural order (oldest first)
     */
    public void setReverse(boolean reverse);

    @Override
    public List<HttpExchange> findAll();

    @Override
    public void add(HttpExchange exchange);
}

HTTP Exchanges Endpoint

Actuator endpoint for exposing HTTP exchange information.

/**
 * Endpoint to expose HTTP exchange information via the actuator framework.
 *
 * Thread-safe: Yes
 * Endpoint ID: "httpexchanges"
 * Package: org.springframework.boot.actuate.web.exchanges
 * @since 3.0.0
 */
@Endpoint(id = "httpexchanges")
public class HttpExchangesEndpoint {

    /**
     * Create endpoint with exchange repository.
     *
     * @param repository exchange repository (non-null)
     * @throws IllegalArgumentException if repository is null
     */
    public HttpExchangesEndpoint(HttpExchangeRepository repository);

    /**
     * Get all recorded HTTP exchanges.
     * Exposed as a read operation via the actuator endpoint.
     *
     * @return descriptor containing all exchanges (never null)
     */
    @ReadOperation
    public HttpExchangesDescriptor httpExchanges();

    /**
     * Response descriptor for HTTP exchanges endpoint.
     * Implements OperationResponseBody for proper actuator serialization.
     */
    public static final class HttpExchangesDescriptor implements OperationResponseBody {

        /**
         * Get the list of HTTP exchanges.
         *
         * @return list of exchanges (never null)
         */
        public List<HttpExchange> getExchanges();
    }
}

COMPLETE WORKING EXAMPLES

Example 1: Basic HTTP Exchange Tracking

package com.example.actuator;

import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.HttpExchangesEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuration for HTTP exchange tracking.
 *
 * Thread-safe: Yes
 * Auto-recording: Yes (via auto-configuration)
 * Since: Application 1.0
 */
@Configuration
public class HttpExchangeConfiguration {

    /**
     * In-memory HTTP exchange repository.
     *
     * @return Repository storing last 1000 exchanges
     */
    @Bean
    public InMemoryHttpExchangeRepository httpExchangeRepository() {
        InMemoryHttpExchangeRepository repository =
            new InMemoryHttpExchangeRepository();

        // Store last 1000 exchanges
        repository.setCapacity(1000);

        // Newest first (default)
        repository.setReverse(false);

        return repository;
    }

    /**
     * HTTP exchanges endpoint.
     *
     * @param repository Exchange repository
     * @return Configured endpoint
     */
    @Bean
    public HttpExchangesEndpoint httpExchangesEndpoint(
            HttpExchangeRepository repository) {
        return new HttpExchangesEndpoint(repository);
    }
}

// Configuration in application.yml
/*
management:
  endpoints:
    web:
      exposure:
        include: httpexchanges

  httpexchanges:
    recording:
      enabled: true
      include:
        - request-headers
        - response-headers
        - principal
        - time-taken
        - remote-address
*/

Example 2: HTTP Analytics Service

package com.example.actuator;

import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Service for analyzing HTTP traffic patterns.
 *
 * Thread-safe: Yes
 * Performance: O(n) for most operations
 * Since: Application 1.0
 */
@Service
public class HttpAnalyticsService {

    private final HttpExchangeRepository repository;

    public HttpAnalyticsService(HttpExchangeRepository repository) {
        this.repository = repository;
    }

    /**
     * Get average response time across all requests.
     *
     * @return Average duration in milliseconds
     */
    public double getAverageResponseTime() {
        return repository.findAll().stream()
            .map(HttpExchange::getTimeTaken)
            .filter(Objects::nonNull)
            .mapToLong(Duration::toMillis)
            .average()
            .orElse(0.0);
    }

    /**
     * Get request count by endpoint.
     *
     * @return Map of endpoint paths to request counts
     */
    public Map<String, Long> getEndpointCallCounts() {
        return repository.findAll().stream()
            .collect(Collectors.groupingBy(
                ex -> ex.getRequest().getUri().getPath(),
                Collectors.counting()
            ));
    }

    /**
     * Find slow requests (> threshold).
     *
     * @param thresholdMs Threshold in milliseconds
     * @return List of slow requests
     */
    public List<SlowRequest> findSlowRequests(long thresholdMs) {
        return repository.findAll().stream()
            .filter(ex -> ex.getTimeTaken() != null)
            .filter(ex -> ex.getTimeTaken().toMillis() > thresholdMs)
            .map(ex -> new SlowRequest(
                ex.getRequest().getUri().toString(),
                ex.getTimeTaken().toMillis(),
                ex.getResponse().getStatus(),
                ex.getRequest().getRemoteAddress()
            ))
            .sorted(Comparator.comparingLong(SlowRequest::durationMs).reversed())
            .collect(Collectors.toList());
    }

    /**
     * Get status code distribution.
     *
     * @return Map of status codes to counts
     */
    public Map<Integer, Long> getStatusCodeDistribution() {
        return repository.findAll().stream()
            .collect(Collectors.groupingBy(
                ex -> ex.getResponse().getStatus(),
                Collectors.counting()
            ));
    }

    /**
     * Get error rate (4xx and 5xx responses).
     *
     * @return Error rate as percentage (0-100)
     */
    public double getErrorRate() {
        List<HttpExchange> exchanges = repository.findAll();
        if (exchanges.isEmpty()) {
            return 0.0;
        }

        long errors = exchanges.stream()
            .map(ex -> ex.getResponse().getStatus())
            .filter(status -> status >= 400)
            .count();

        return (errors * 100.0) / exchanges.size();
    }

    /**
     * Get requests by client IP address.
     *
     * @return Map of IP addresses to request counts
     */
    public Map<String, Long> getRequestsByIpAddress() {
        return repository.findAll().stream()
            .map(ex -> ex.getRequest().getRemoteAddress())
            .filter(Objects::nonNull)
            .collect(Collectors.groupingBy(
                ip -> ip,
                Collectors.counting()
            ));
    }

    /**
     * Get top slowest endpoints.
     *
     * @param limit Number of results
     * @return List of endpoints with average response times
     */
    public List<EndpointStats> getTopSlowestEndpoints(int limit) {
        return repository.findAll().stream()
            .filter(ex -> ex.getTimeTaken() != null)
            .collect(Collectors.groupingBy(
                ex -> ex.getRequest().getUri().getPath(),
                Collectors.averagingLong(ex -> ex.getTimeTaken().toMillis())
            ))
            .entrySet().stream()
            .map(entry -> new EndpointStats(entry.getKey(), entry.getValue()))
            .sorted(Comparator.comparingDouble(EndpointStats::avgDurationMs).reversed())
            .limit(limit)
            .collect(Collectors.toList());
    }

    /**
     * Detect potential brute force attacks.
     *
     * @param failureThreshold Number of failures to trigger alert
     * @return List of suspicious IP addresses
     */
    public List<SecurityAlert> detectBruteForceAttempts(int failureThreshold) {
        Map<String, Long> failuresByIp = repository.findAll().stream()
            .filter(ex -> ex.getResponse().getStatus() == 401 ||
                         ex.getResponse().getStatus() == 403)
            .map(ex -> ex.getRequest().getRemoteAddress())
            .filter(Objects::nonNull)
            .collect(Collectors.groupingBy(ip -> ip, Collectors.counting()));

        return failuresByIp.entrySet().stream()
            .filter(entry -> entry.getValue() >= failureThreshold)
            .map(entry -> new SecurityAlert(
                entry.getKey(),
                entry.getValue(),
                "Potential brute force attack"
            ))
            .collect(Collectors.toList());
    }

    public record SlowRequest(String uri, long durationMs, int status, String remoteAddress) {}
    public record EndpointStats(String endpoint, double avgDurationMs) {}
    public record SecurityAlert(String ipAddress, long failureCount, String reason) {}
}

Example 3: Performance Monitoring Dashboard

package com.example.actuator;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Controller for HTTP performance dashboard.
 *
 * Thread-safe: Yes (via Spring MVC)
 * Since: Application 1.0
 */
@Controller
@RequestMapping("/admin/http-monitoring")
public class HttpMonitoringDashboard {

    private final HttpAnalyticsService analyticsService;

    public HttpMonitoringDashboard(HttpAnalyticsService analyticsService) {
        this.analyticsService = analyticsService;
    }

    /**
     * Display HTTP monitoring dashboard.
     *
     * @param model Spring MVC model
     * @return View name
     */
    @GetMapping
    public String dashboard(Model model) {
        // Overall metrics
        model.addAttribute("avgResponseTime", analyticsService.getAverageResponseTime());
        model.addAttribute("errorRate", analyticsService.getErrorRate());

        // Endpoint statistics
        model.addAttribute("endpointCounts", analyticsService.getEndpointCallCounts());
        model.addAttribute("slowestEndpoints", analyticsService.getTopSlowestEndpoints(10));

        // Status codes
        model.addAttribute("statusDistribution", analyticsService.getStatusCodeDistribution());

        // Security
        model.addAttribute("securityAlerts", analyticsService.detectBruteForceAttempts(10));
        model.addAttribute("requestsByIp", analyticsService.getRequestsByIpAddress());

        // Slow requests
        model.addAttribute("slowRequests", analyticsService.findSlowRequests(1000));

        return "http-monitoring-dashboard";
    }
}

TESTING EXAMPLES

Test 1: HTTP Analytics Service

package com.example.actuator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import java.net.URI;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;

class HttpAnalyticsServiceTest {

    private InMemoryHttpExchangeRepository repository;
    private HttpAnalyticsService service;

    @BeforeEach
    void setUp() {
        repository = new InMemoryHttpExchangeRepository();
        service = new HttpAnalyticsService(repository);

        // Add test data
        addExchange("/api/users", 200, Duration.ofMillis(100), "192.168.1.1");
        addExchange("/api/users", 200, Duration.ofMillis(150), "192.168.1.2");
        addExchange("/api/orders", 200, Duration.ofMillis(2000), "192.168.1.1"); // Slow
        addExchange("/api/orders", 500, Duration.ofMillis(50), "192.168.1.3"); // Error
        addExchange("/api/login", 401, Duration.ofMillis(30), "192.168.1.4"); // Auth failure
    }

    @Test
    void getAverageResponseTime_CalculatesCorrectly() {
        double avg = service.getAverageResponseTime();

        assertThat(avg).isEqualTo(466.0); // (100+150+2000+50+30)/5
    }

    @Test
    void getEndpointCallCounts_CountsCorrectly() {
        Map<String, Long> counts = service.getEndpointCallCounts();

        assertThat(counts).containsEntry("/api/users", 2L);
        assertThat(counts).containsEntry("/api/orders", 2L);
        assertThat(counts).containsEntry("/api/login", 1L);
    }

    @Test
    void findSlowRequests_FindsRequestsAboveThreshold() {
        List<HttpAnalyticsService.SlowRequest> slow = service.findSlowRequests(1000);

        assertThat(slow).hasSize(1);
        assertThat(slow.get(0).uri()).contains("/api/orders");
        assertThat(slow.get(0).durationMs()).isEqualTo(2000);
    }

    @Test
    void getStatusCodeDistribution_CountsByStatus() {
        Map<Integer, Long> distribution = service.getStatusCodeDistribution();

        assertThat(distribution).containsEntry(200, 3L);
        assertThat(distribution).containsEntry(500, 1L);
        assertThat(distribution).containsEntry(401, 1L);
    }

    @Test
    void getErrorRate_CalculatesCorrectly() {
        double errorRate = service.getErrorRate();

        assertThat(errorRate).isEqualTo(40.0); // 2 errors out of 5 = 40%
    }

    @Test
    void detectBruteForceAttempts_DetectsSuspiciousActivity() {
        // Add more auth failures from same IP
        for (int i = 0; i < 15; i++) {
            addExchange("/api/login", 401, Duration.ofMillis(30), "192.168.1.100");
        }

        List<HttpAnalyticsService.SecurityAlert> alerts =
            service.detectBruteForceAttempts(10);

        assertThat(alerts).hasSize(1);
        assertThat(alerts.get(0).ipAddress()).isEqualTo("192.168.1.100");
        assertThat(alerts.get(0).failureCount()).isEqualTo(15);
    }

    private void addExchange(String path, int status, Duration timeTaken, String remoteAddress) {
        HttpExchange.Request request = new HttpExchange.Request(
            URI.create("http://localhost:8080" + path),
            remoteAddress,
            "GET",
            Map.of()
        );

        HttpExchange.Response response = new HttpExchange.Response(status, Map.of());

        HttpExchange exchange = new HttpExchange(
            Instant.now(),
            request,
            response,
            null,
            null,
            timeTaken
        );

        repository.add(exchange);
    }
}

TROUBLESHOOTING

Common Error: No Exchanges Recorded

Problem: GET /actuator/httpexchanges returns empty

Causes:

  1. Recording disabled
  2. Repository not configured
  3. Requests not captured

Solutions:

# Solution 1: Enable recording
management:
  httpexchanges:
    recording:
      enabled: true

# Solution 2: Configure repository
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
    return new InMemoryHttpExchangeRepository();
}

# Solution 3: Check filter is registered (auto-configured by default)

Common Error: Memory Issues

Problem: Application uses too much memory

Causes:

  1. Capacity too high
  2. Large headers included
  3. High traffic volume

Solutions:

// Solution 1: Reduce capacity
repository.setCapacity(100); // Lower for high-traffic apps

// Solution 2: Exclude large headers
management:
  httpexchanges:
    recording:
      include:
        - time-taken
        - remote-address
        # Exclude headers for memory savings

// Solution 3: Use persistent repository
@Bean
public HttpExchangeRepository persistentRepository(DataSource ds) {
    return new JdbcHttpExchangeRepository(ds);
}

Common Error: Sensitive Data Exposure

Problem: Authorization tokens visible in exchanges

Causes:

  1. AUTHORIZATION_HEADER included
  2. COOKIE_HEADERS included
  3. Wrong include configuration

Solutions:

# Exclude sensitive headers
management:
  httpexchanges:
    recording:
      include:
        - request-headers  # Safe (auto-excludes auth)
        - response-headers # Safe (auto-excludes cookies)
        - time-taken
      # Don't include:
      # - authorization-header
      # - cookie-headers

PERFORMANCE NOTES

Capacity Planning

// Calculate based on traffic patterns
// Example: 1000 req/min, want 1 hour history
// Capacity = 1000 * 60 = 60,000

@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
    int requestsPerMinute = 1000;
    int historyMinutes = 60;
    int capacity = requestsPerMinute * historyMinutes;

    InMemoryHttpExchangeRepository repository =
        new InMemoryHttpExchangeRepository();
    repository.setCapacity(capacity);

    return repository;
}

Recording Overhead

// Recording adds ~1-2ms overhead per request
// Minimize included data for high-traffic endpoints

// ✓ Minimal overhead
Set<Include> minimalIncludes = Set.of(
    Include.TIME_TAKEN,
    Include.REMOTE_ADDRESS
);

// ❌ Higher overhead
Set<Include> maximalIncludes = EnumSet.allOf(Include.class);

Query Performance

// findAll() loads all exchanges into memory
// For large repositories, this can be expensive

// ✓ Good - filter in stream
List<HttpExchange> recent = repository.findAll().stream()
    .filter(ex -> ex.getTimestamp().isAfter(cutoff))
    .limit(100)
    .collect(Collectors.toList());

// For very large datasets, implement custom repository with filtering

Cross-References

  • For endpoint exposure: Built-in Endpoints
  • For audit framework: Audit Framework
  • For web integration: Web Integration
  • For security: Security Integration