HTTP client abstraction for LangChain4j with synchronous/asynchronous execution and Server-Sent Events (SSE) streaming support
This guide covers HTTP request/response logging using the LoggingHttpClient decorator.
import dev.langchain4j.http.client.*;
import dev.langchain4j.http.client.log.LoggingHttpClient;
import java.time.Duration;
// Create base HTTP client
HttpClient baseClient = HttpClientBuilderLoader.loadHttpClientBuilder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(30))
.build();
// Wrap with logging
HttpClient loggingClient = new LoggingHttpClient(
baseClient,
true, // log requests
true // log responses
);
// Use the logging client
HttpRequest request = HttpRequest.builder()
.method(HttpMethod.POST)
.url("https://api.example.com/data")
.addHeader("Authorization", "Bearer secret-token-12345")
.body("{\"key\":\"value\"}")
.build();
SuccessfulHttpResponse response = loggingClient.execute(request);Output:
INFO HTTP request:
- method: POST
- url: https://api.example.com/data
- headers: [Authorization: Beare...45]
- body: {"key":"value"}
INFO HTTP response:
- status code: 200
- headers: [Content-Type: application/json]
- body: {"result":"success"}HttpClient loggingClient = new LoggingHttpClient(
baseClient,
true, // log requests
false // do NOT log responses
);HttpClient loggingClient = new LoggingHttpClient(
baseClient,
false, // do NOT log requests
true // log responses
);import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger customLogger = LoggerFactory.getLogger("com.myapp.HttpClient");
HttpClient loggingClient = new LoggingHttpClient(
baseClient,
true,
true,
customLogger
);The LoggingHttpClient automatically masks sensitive headers.
AuthorizationX-API-KeyX-Auth-Tokenapi-key (case-insensitive)Original: Bearer sk-1234567890abcdef
Masked: Beare...ef
Original: 12345
Masked: ...
Original: api_key_abc123xyz789
Masked: api_k...89import dev.langchain4j.http.client.sse.*;
HttpClient loggingClient = new LoggingHttpClient(baseClient, true, true);
HttpRequest request = HttpRequest.builder()
.method(HttpMethod.POST)
.url("https://api.example.com/stream")
.addHeader("Accept", "text/event-stream")
.body("{\"prompt\":\"Hello\"}")
.build();
loggingClient.execute(request, new ServerSentEventListener() {
@Override
public void onOpen(SuccessfulHttpResponse response) {
// Initial response is logged automatically
System.out.println("Stream opened");
}
@Override
public void onEvent(ServerSentEvent event) {
// Each event is logged automatically at DEBUG level
System.out.println("Received: " + event.data());
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error: " + throwable.getMessage());
}
});SSE Output:
INFO HTTP request:
- method: POST
- url: https://api.example.com/stream
- headers: [Accept: text/event-stream]
- body: {"prompt":"Hello"}
INFO HTTP response:
- status code: 200
- headers: [Content-Type: text/event-stream]
- body: null
DEBUG ServerSentEvent { event = null, data = "Hello" }
DEBUG ServerSentEvent { event = null, data = "world" }
DEBUG ServerSentEvent { event = "done", data = "" }<configuration>
<!-- Console appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- HTTP client logging -->
<logger name="dev.langchain4j.http.client.log.LoggingHttpClient" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration><Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="dev.langchain4j.http.client.log.LoggingHttpClient" level="info" />
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>To disable verbose SSE event logging while keeping request/response logging:
<!-- Logback -->
<logger name="dev.langchain4j.http.client.log.LoggingHttpClient" level="INFO" />This sets the level to INFO, which excludes DEBUG-level SSE event logs.
public class HttpClientFactory {
public static HttpClient create() {
HttpClient baseClient = HttpClientBuilderLoader.loadHttpClientBuilder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(30))
.build();
// Enable logging only in development
String env = System.getenv("ENVIRONMENT");
if ("development".equals(env) || "dev".equals(env)) {
return new LoggingHttpClient(baseClient, true, true);
}
return baseClient;
}
}public class ConfigurableLoggingClient {
public static HttpClient create(HttpClient baseClient) {
boolean logRequests = Boolean.parseBoolean(
System.getProperty("http.log.requests", "false")
);
boolean logResponses = Boolean.parseBoolean(
System.getProperty("http.log.responses", "false")
);
if (logRequests || logResponses) {
return new LoggingHttpClient(baseClient, logRequests, logResponses);
}
return baseClient;
}
}
// Usage with system properties:
// java -Dhttp.log.requests=true -Dhttp.log.responses=true ...import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HttpClientConfiguration {
@Value("${http.logging.enabled:false}")
private boolean loggingEnabled;
@Value("${http.logging.requests:true}")
private boolean logRequests;
@Value("${http.logging.responses:true}")
private boolean logResponses;
@Bean
public HttpClient httpClient() {
HttpClient baseClient = HttpClientBuilderLoader.loadHttpClientBuilder()
.connectTimeout(Duration.ofSeconds(10))
.readTimeout(Duration.ofSeconds(30))
.build();
if (loggingEnabled) {
return new LoggingHttpClient(baseClient, logRequests, logResponses);
}
return baseClient;
}
}http.logging.enabled=true
http.logging.requests=true
http.logging.responses=falseLogger apiLogger = LoggerFactory.getLogger("com.myapp.api.HttpClient");
Logger authLogger = LoggerFactory.getLogger("com.myapp.auth.HttpClient");
HttpClient apiClient = new LoggingHttpClient(baseClient, true, true, apiLogger);
HttpClient authClient = new LoggingHttpClient(baseClient, true, false, authLogger);<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- API client - log everything -->
<logger name="com.myapp.api.HttpClient" level="DEBUG" />
<!-- Auth client - log only errors -->
<logger name="com.myapp.auth.HttpClient" level="WARN" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>Logging overhead: Logging adds overhead to each request. Disable in production if performance is critical.
Large response bodies: Logging large response bodies can consume significant memory and I/O. Consider disabling response logging for endpoints returning large payloads.
SSE event logging: DEBUG-level logging of each SSE event can be very verbose. Set log level to INFO to suppress.
String concatenation: Logging performs string operations which can impact performance under high load.
public class SmartLoggingHttpClient extends LoggingHttpClient {
private final long maxBodyLength;
public SmartLoggingHttpClient(HttpClient delegate, long maxBodyLength) {
super(delegate, true, shouldLogResponses());
this.maxBodyLength = maxBodyLength;
}
private static boolean shouldLogResponses() {
// Only log response bodies if explicitly enabled
return "true".equals(System.getenv("LOG_HTTP_RESPONSES"));
}
// Custom implementation could check response size before logging
}Automatic masking: While common sensitive headers are automatically masked, review your specific headers to ensure no secrets are logged.
Custom headers: Headers with custom names containing secrets (e.g., X-Custom-Secret) should be manually reviewed.
Request/response bodies: Bodies are logged in full. Ensure request/response bodies don't contain sensitive data, or disable body logging.
Log file access: Ensure log files are properly secured with appropriate file system permissions.
Log aggregation: When using centralized logging systems, ensure logs are transmitted and stored securely.
Development vs Production: Enable verbose logging in development, minimal or no logging in production.
Selective logging: Log requests for debugging, but consider disabling response body logging in production.
Use custom loggers: Use different loggers for different API clients to control logging granularity.
Monitor log volume: SSE streaming can generate large volumes of logs. Set appropriate log levels.
Secure log storage: Ensure logs are stored securely and access is controlled.
Rotate logs: Implement log rotation to prevent disk space issues.
Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-http-client@1.11.0