CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

82

Quality

82%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Validation failed for skills in this tile
One or more skills have errors that need to be fixed before they can move to Implementation and Discovery review.
Overview
Quality
Evals
Security
Files

http-exchanges.mdplugins/developer-kit-java/skills/spring-boot-actuator/references/

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

plugins

developer-kit-java

skills

README.md

tile.json