Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:34 +08:00
commit 390afca02b
220 changed files with 86013 additions and 0 deletions

View File

@@ -0,0 +1,504 @@
# 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)
```java
@Configuration
public class HttpExchangesConfiguration {
@Bean
public InMemoryHttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
}
```
### Custom Repository Size
```java
@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:
```yaml
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
```java
@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
```java
@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
```java
@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:
```json
{
"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
```java
@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
```java
@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
```yaml
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
```java
@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
```yaml
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
```