A Guide to Enhanced Debugging and Record-Keeping
This article demonstrates how to log, store, and retrieve third-party API interactions for improved debugging and communication using Spring Framework.
Join the DZone community and get the full member experience.
Join For FreeAs developers working with third-party REST APIs, it is often necessary to store the request and response details for potential future reference, especially when issues arise. This could serve as invaluable data to liaise with third-party vendors, providing a first-hand look at the raw interaction that occurred. In my role, I am constantly orchestrating API’s with multiple partner services. Therefore, I sought a generic solution to store these third-party API requests and responses seamlessly.
The Spring Framework's RestTemplate, a widely used synchronous HTTP client, proves to be a handy tool for consuming RESTful services. This framework provides an interface called ClientHttpRequestInterceptor
, which allows us to take certain actions on the request and response.
This article will guide you through a custom implementation of ClientHttpRequestInterceptor
aimed at logging third-party API interactions. Notably, this implementation will store the request and response payloads on S3 and save the file paths to a database. This way, we can easily track and retrieve the raw data for each interaction.
Here is the code for the service, which invokes an API with an external service.
@Service
public class ExtSaveEmployeeService {
private String baseUrl = "http://localhost:8080/api/v1";
private static final Logger logger = LoggerFactory.getLogger(ExtSaveEmployeeService.class);
@Autowired
ExternalApiRestTemplateFactory externalApiRestTemplateFactory;
public EmployeeResponse addEmployee(EmployeeRequest request, String accessToken) {
EmployeeResponse result = null;
try {
Date dt = new Date();
RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate(accessToken, "ADD_EMPLOYEE", "ADD_EMPLOYEE");
HttpHeaders headers = this.getHttpHeaders(accessToken);
HttpEntity<EmployeeRequest> entity = new HttpEntity<>(request, headers);
long startTime = System.currentTimeMillis();
String endPoint = this.baseUrl + "/employee";
ResponseEntity<EmployeeResponse> responseEntity = restTemplate.exchange(endPoint,
HttpMethod.POST, entity, EmployeeResponse.class);
result = responseEntity.getBody();
long endTime = System.currentTimeMillis();
long timeDifference = endTime - startTime;
logger.info("Time taken for API call : " + timeDifference);
} catch (HttpClientErrorException exception) {
logger.error("HttpClientErrorException occurred while calling ext API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (HttpStatusCodeException exception) {
logger.error("HttpStatusCodeException occurred while calling EXT API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (Exception ex) {
logger.error("An Error occurred, Detail error:", ex);
throw ex;
}
return result;
}
private HttpHeaders getHttpHeaders(String accessToken){
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
headers.set("Authorization", "Bearer " + accessToken);
return headers;
}
}
ExtSaveEmployeeService
is spring service class which is responsible for adding an employee by invoking an external API.
The below code snippet is important from the article's context:
RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate(accessToken, "ADD_EMPLOYEE", "ADD_EMPLOYEE");
externalApiRestTemplateFactory
is the factory implementation that returns the RestTemplate with a logging interceptor configured. Here is the class definition of the factory:
@Component
public class ExternalApiRestTemplateFactory {
@Autowired
FilePathRepository filePathRepository;
@Autowired
DocumentUploadService documentUploadService;
public RestTemplate getRestTemplate(String accessToken, String fileNamePrefix, String serviceName) {
SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
simpleClientHttpRequestFactory.setOutputStreaming(false);
ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(simpleClientHttpRequestFactory);
RestTemplate restTemplate = new RestTemplate(factory);
ExternalApiLoggingInterceptor loggingInterceptor = new ExternalApiLoggingInterceptor();
loggingInterceptor.setAccessToken(accessToken);
loggingInterceptor.setFileNamePrefix(fileNamePrefix);
loggingInterceptor.setServiceName(serviceName);
loggingInterceptor.setFilePathRepository(this.filePathRepository);
loggingInterceptor.setDocumentUploadService(this.documentUploadService);
restTemplate.setInterceptors(Collections.singletonList(loggingInterceptor));
return restTemplate;
}
}
The Factory implementation is responsible for creating RestTemplate instances and configuring the logging interceptor. All the required parameters and dependencies for logging the payloads are passed to the factory.
Here is the implementation of the logging interceptor:
public class ExternalApiLoggingInterceptor implements ClientHttpRequestInterceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private String accessToken;
private String fileNamePrefix;
private FilePathRepository filePathRepository;
private DocumentUploadService documentUploadService;
private String serviceName;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getFileNamePrefix() {
return fileNamePrefix;
}
public void setFileNamePrefix(String fileNamePrefix) {
this.fileNamePrefix = fileNamePrefix;
}
public String getServiceName() {
return serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public FilePathRepository getFilePathRepository() {
return filePathRepository;
}
public void setFilePathRepository(FilePathRepository filePathRepository) {
this.filePathRepository = filePathRepository;
}
public DocumentUploadService getDocumentUploadService() {
return documentUploadService;
}
public void setDocumentUploadService(DocumentUploadService documentUploadService) {
this.documentUploadService = documentUploadService;
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException {
try {
String requestBody = new String(body, Charset.defaultCharset());
this.saveFile(requestBody, "REQUEST");
} catch (Exception ex) {
logger.error("An error occurred while saving JSON request ExternalApiLoggingInterceptor::logRequest, detail error:", ex);
}
}
private void logResponse(ClientHttpResponse response) throws IOException {
try {
String responseBody = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
this.saveFile(responseBody, "RESPONSE");
} catch (Exception ex) {
logger.error("An error occurred while saving JSON response ExternalApiLoggingInterceptor::logResponse, detail error:", ex);
}
}
private void saveFile(String fileContent, String payloadType) {
try {
String timeStampSuffix = Long.toString(Instant.now().getEpochSecond());
String fileName = this.getFileNamePrefix() + "_" + payloadType + "_" + timeStampSuffix + ".json";
String documentPath = "external-partner/request-response/" + fileName;
ByteArrayResource resource = this.getDocumentByteArray(fileContent.getBytes(), fileName);
if (resource != null) {
//Update the file to Cloud AWS S3 or Azure equivalent.
DocumentsResponse documentsResponse = this.documentUploadService.updateDocument(resource, documentPath, accessToken);
if (documentsResponse != null && documentsResponse.getSavedFilePath() != null) {
//Save file path in database.
boolean result = this.filePathRepository.saveExtPayloadPaths(this.serviceName, documentsResponse.getSavedFilePath());
if (!result) {
logger.error("An occurred while saving the response file path.");
}
}
}
} catch (Exception ex) {
logger.error("An error occurred while saving req/res for APi call ExternalApiLoggingInterceptor::saveFile, detail error:", ex);
}
}
private ByteArrayResource getDocumentByteArray(byte[] responseContent, String fileName) {
try {
final ByteArrayResource byteArrayResource = new ByteArrayResource(responseContent) {
@Override
public String getFilename() {
return fileName;
}
};
return byteArrayResource;
} catch (Exception ex) {
logger.error("Exception - getDocumentByteArray - Error while response body byte array content." + ex.getMessage(), ex);
}
return null;
}
}
The ExternalApiLoggingInterceptor
class is an implementation of the ClientHttpRequestInterceptor
interface in Spring. The interceptor class intercepts HTTP requests and responses, saves them to a file stream, uploads the file stream to cloud storage, and the cloud storage file path then persists in the database. This can be useful for tracking requests and responses for debugging or record-keeping.
The code for DocumentUploadService
can be found in the GitHub repo. It's primarily storing the files in Amazon S3.
FilePathRepository
— this can be database-specific implementation. Depending on what kind of database you are using (e.g., SQL, NoSQL), the specific implementation of the repository might change, but the idea remains the same.
In conclusion, this article has provided a detailed solution for logging and storing third-party API interactions using the Spring Framework. We've covered the problem scenario necessitating the tracking of requests and responses and the use of Spring's RestTemplate and ClientHttpRequestInterceptor
interface. A significant feature of our solution is its capacity to save requests and responses to cloud storage and persist file paths in a database, providing an effective means of record-keeping and access when needed. This solution is particularly valuable when orchestrating multiple partner services, providing an effective tool for debugging and communication. Despite potential scalability limitations, this code can be modified to meet different contexts. As developers, we strive to create dynamic, robust, and scalable solutions like this to meet diverse requirements.
You can find the complete code implementation on this GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments