Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot/spring-boot-actuator/references/http-exchanges.md
2025-11-29 18:28:30 +08:00

14 KiB

HTTP Exchanges

You can enable recording of HTTP exchanges by providing a bean of type HttpExchangeRepository in your application's configuration. For convenience, Spring Boot offers InMemoryHttpExchangeRepository, which, by default, stores the last 100 request-response exchanges. InMemoryHttpExchangeRepository is limited compared to tracing solutions, and we recommend using it only for development environments. For production environments, we recommend using a production-ready tracing or observability solution, such as Zipkin or OpenTelemetry. Alternatively, you can create your own HttpExchangeRepository.

You can use the httpexchanges endpoint to obtain information about the request-response exchanges that are stored in the HttpExchangeRepository.

Basic Configuration

In-Memory Repository (Development)

@Configuration
public class HttpExchangesConfiguration {

    @Bean
    public InMemoryHttpExchangeRepository httpExchangeRepository() {
        return new InMemoryHttpExchangeRepository();
    }
}

Custom Repository Size

@Configuration
public class HttpExchangesConfiguration {

    @Bean
    public InMemoryHttpExchangeRepository httpExchangeRepository() {
        return new InMemoryHttpExchangeRepository(1000); // Store last 1000 exchanges
    }
}

Custom HTTP Exchange Recording

To customize the items that are included in each recorded exchange, use the management.httpexchanges.recording.include configuration property:

management:
  httpexchanges:
    recording:
      include:
        - request-headers
        - response-headers
        - cookie-headers
        - authorization-header
        - principal
        - remote-address
        - session-id
        - time-taken

Available options:

  • request-headers: Include request headers
  • response-headers: Include response headers
  • cookie-headers: Include cookie headers
  • authorization-header: Include authorization header
  • principal: Include principal information
  • remote-address: Include remote address
  • session-id: Include session ID
  • time-taken: Include request processing time

Custom HTTP Exchange Repository

Database-backed Repository

@Entity
@Table(name = "http_exchanges")
public class HttpExchangeEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "timestamp")
    private Instant timestamp;
    
    @Column(name = "method")
    private String method;
    
    @Column(name = "uri", length = 2000)
    private String uri;
    
    @Column(name = "status")
    private Integer status;
    
    @Column(name = "time_taken")
    private Long timeTaken;
    
    @Column(name = "principal")
    private String principal;
    
    @Column(name = "remote_address")
    private String remoteAddress;
    
    @Column(name = "session_id")
    private String sessionId;
    
    @Lob
    @Column(name = "request_headers")
    private String requestHeaders;
    
    @Lob
    @Column(name = "response_headers")
    private String responseHeaders;
    
    // Constructors, getters, setters
}

@Repository
public interface HttpExchangeEntityRepository extends JpaRepository<HttpExchangeEntity, Long> {
    
    List<HttpExchangeEntity> findTop100ByOrderByTimestampDesc();
    
    @Modifying
    @Query("DELETE FROM HttpExchangeEntity h WHERE h.timestamp < :cutoff")
    void deleteOlderThan(@Param("cutoff") Instant cutoff);
}

@Component
public class DatabaseHttpExchangeRepository implements HttpExchangeRepository {

    private final HttpExchangeEntityRepository repository;
    private final ObjectMapper objectMapper;

    public DatabaseHttpExchangeRepository(HttpExchangeEntityRepository repository) {
        this.repository = repository;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public List<HttpExchange> findAll() {
        return repository.findTop100ByOrderByTimestampDesc()
                .stream()
                .map(this::toHttpExchange)
                .collect(Collectors.toList());
    }

    @Override
    public void add(HttpExchange httpExchange) {
        HttpExchangeEntity entity = toEntity(httpExchange);
        repository.save(entity);
    }

    private HttpExchangeEntity toEntity(HttpExchange exchange) {
        HttpExchangeEntity entity = new HttpExchangeEntity();
        entity.setTimestamp(exchange.getTimestamp());
        
        HttpExchange.Request request = exchange.getRequest();
        entity.setMethod(request.getMethod());
        entity.setUri(request.getUri().toString());
        entity.setPrincipal(exchange.getPrincipal() != null ? 
                          exchange.getPrincipal().getName() : null);
        entity.setRemoteAddress(request.getRemoteAddress());
        
        if (exchange.getResponse() != null) {
            entity.setStatus(exchange.getResponse().getStatus());
        }
        
        entity.setTimeTaken(exchange.getTimeTaken() != null ? 
                          exchange.getTimeTaken().toMillis() : null);
        
        try {
            entity.setRequestHeaders(objectMapper.writeValueAsString(request.getHeaders()));
            if (exchange.getResponse() != null) {
                entity.setResponseHeaders(objectMapper.writeValueAsString(
                    exchange.getResponse().getHeaders()));
            }
        } catch (Exception e) {
            // Handle serialization error
        }
        
        return entity;
    }

    private HttpExchange toHttpExchange(HttpExchangeEntity entity) {
        // Implement conversion from entity to HttpExchange
        // This is complex due to HttpExchange being immutable
        // Consider using a builder pattern or reflection
        return null; // Simplified for brevity
    }

    @Scheduled(fixedRate = 3600000) // Clean up every hour
    public void cleanup() {
        Instant cutoff = Instant.now().minus(Duration.ofDays(7));
        repository.deleteOlderThan(cutoff);
    }
}

Filtered HTTP Exchange Repository

@Component
public class FilteredHttpExchangeRepository implements HttpExchangeRepository {

    private final HttpExchangeRepository delegate;
    private final Set<String> excludePaths;
    private final Set<String> excludeUserAgents;

    public FilteredHttpExchangeRepository(HttpExchangeRepository delegate) {
        this.delegate = delegate;
        this.excludePaths = Set.of("/actuator/health", "/actuator/metrics", "/favicon.ico");
        this.excludeUserAgents = Set.of("kube-probe", "ELB-HealthChecker");
    }

    @Override
    public List<HttpExchange> findAll() {
        return delegate.findAll();
    }

    @Override
    public void add(HttpExchange httpExchange) {
        if (shouldRecord(httpExchange)) {
            delegate.add(httpExchange);
        }
    }

    private boolean shouldRecord(HttpExchange exchange) {
        String path = exchange.getRequest().getUri().getPath();
        
        // Skip health check and monitoring endpoints
        if (excludePaths.contains(path)) {
            return false;
        }
        
        // Skip requests from monitoring tools
        String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent");
        if (userAgent != null && excludeUserAgents.stream().anyMatch(userAgent::contains)) {
            return false;
        }
        
        // Skip successful static resource requests
        if (path.startsWith("/static/") || path.startsWith("/css/") || path.startsWith("/js/")) {
            return exchange.getResponse() == null || exchange.getResponse().getStatus() >= 400;
        }
        
        return true;
    }
}

Async HTTP Exchange Recording

Async Repository Wrapper

@Component
public class AsyncHttpExchangeRepository implements HttpExchangeRepository {

    private final HttpExchangeRepository delegate;
    private final TaskExecutor taskExecutor;

    public AsyncHttpExchangeRepository(HttpExchangeRepository delegate, 
                                     @Qualifier("httpExchangeTaskExecutor") TaskExecutor taskExecutor) {
        this.delegate = delegate;
        this.taskExecutor = taskExecutor;
    }

    @Override
    public List<HttpExchange> findAll() {
        return delegate.findAll();
    }

    @Override
    public void add(HttpExchange httpExchange) {
        taskExecutor.execute(() -> {
            try {
                delegate.add(httpExchange);
            } catch (Exception e) {
                // Log error but don't let it affect the main request
                log.error("Failed to record HTTP exchange", e);
            }
        });
    }
}

@Configuration
public class HttpExchangeTaskExecutorConfiguration {

    @Bean("httpExchangeTaskExecutor")
    public TaskExecutor httpExchangeTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("http-exchange-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        executor.initialize();
        return executor;
    }
}

HTTP Exchanges Endpoint

Accessing HTTP Exchanges

GET /actuator/httpexchanges

Response format:

{
  "exchanges": [
    {
      "timestamp": "2023-12-01T10:30:00.123Z",
      "request": {
        "method": "GET",
        "uri": "http://localhost:8080/api/users/123",
        "headers": {
          "accept": ["application/json"],
          "user-agent": ["Mozilla/5.0..."]
        },
        "remoteAddress": "192.168.1.100"
      },
      "response": {
        "status": 200,
        "headers": {
          "content-type": ["application/json"],
          "content-length": ["256"]
        }
      },
      "principal": {
        "name": "john.doe"
      },
      "session": {
        "id": "JSESSIONID123"
      },
      "timeTaken": "PT0.025S"
    }
  ]
}

Securing the Endpoint

@Configuration
public class HttpExchangesSecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain httpExchangesSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
            .requestMatcher(EndpointRequest.to("httpexchanges"))
            .authorizeHttpRequests(requests -> 
                requests.anyRequest().hasRole("ADMIN"))
            .httpBasic(withDefaults())
            .build();
    }
}

Custom HTTP Exchange Information

Including Custom Data

@Component
public class CustomHttpExchangeRepository implements HttpExchangeRepository {

    private final InMemoryHttpExchangeRepository delegate;

    public CustomHttpExchangeRepository() {
        this.delegate = new InMemoryHttpExchangeRepository();
    }

    @Override
    public List<HttpExchange> findAll() {
        return delegate.findAll();
    }

    @Override
    public void add(HttpExchange httpExchange) {
        HttpExchange enrichedExchange = enrichExchange(httpExchange);
        delegate.add(enrichedExchange);
    }

    private HttpExchange enrichExchange(HttpExchange original) {
        // Add custom information to the exchange
        // Note: HttpExchange is immutable, so we need to create a wrapper
        // or use reflection to modify internal state
        
        // For demonstration, we'll just add it normally
        // In practice, you might need to create a custom implementation
        return original;
    }
}

@Component
public class HttpExchangeEnricher {

    public void enrich(HttpServletRequest request, HttpServletResponse response) {
        // Add custom attributes that can be picked up by the repository
        request.setAttribute("custom.trace.id", getTraceId());
        request.setAttribute("custom.user.role", getUserRole());
        request.setAttribute("custom.api.version", getApiVersion(request));
    }

    private String getTraceId() {
        // Get from tracing context
        return "trace-123";
    }

    private String getUserRole() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getAuthorities().toString() : "anonymous";
    }

    private String getApiVersion(HttpServletRequest request) {
        return request.getHeader("API-Version");
    }
}

Performance Considerations

Configuration for Production

management:
  httpexchanges:
    recording:
      include:
        - time-taken
        - principal
        - remote-address
      # Exclude detailed headers to reduce memory usage
      exclude:
        - request-headers
        - response-headers
  endpoint:
    httpexchanges:
      enabled: false  # Disable in production for security

Custom Sampling

@Component
public class SamplingHttpExchangeRepository implements HttpExchangeRepository {

    private final HttpExchangeRepository delegate;
    private final Random random = new Random();
    private final double samplingRate;

    public SamplingHttpExchangeRepository(HttpExchangeRepository delegate,
                                        @Value("${app.http-exchanges.sampling-rate:0.1}") double samplingRate) {
        this.delegate = delegate;
        this.samplingRate = samplingRate;
    }

    @Override
    public List<HttpExchange> findAll() {
        return delegate.findAll();
    }

    @Override
    public void add(HttpExchange httpExchange) {
        if (random.nextDouble() < samplingRate) {
            delegate.add(httpExchange);
        }
    }
}

Best Practices

  1. Production Use: Disable HTTP exchanges endpoint in production or secure it properly
  2. Memory Management: Use limited-size repositories to prevent memory leaks
  3. Sensitive Data: Be careful not to log sensitive information in headers
  4. Performance: Consider async recording for high-throughput applications
  5. Sampling: Use sampling in production to reduce overhead
  6. Retention: Implement cleanup policies for stored exchanges
  7. Security: Ensure recorded data doesn't contain credentials or tokens

Production Configuration Example

management:
  endpoint:
    httpexchanges:
      enabled: false  # Disabled in production
  httpexchanges:
    recording:
      include:
        - time-taken
        - principal
        - remote-address
      exclude:
        - authorization-header
        - cookie-headers
        - request-headers
        - response-headers

logging:
  level:
    org.springframework.boot.actuate.web.exchanges: WARN