5

I want to log responses using Spring's RestTemplate. At first I thought I should do it using a ClientHttpRequestInterceptor, but since every set of requests should be logged to a different file, I figured out I should create a new RestTemplate with a new ClientHttpRequestInterceptor for every set of requests (though all other configurations are the same).

But I saw that RestTemplate is pretty expensive object to create, when about 70% of the creation time, is creating the MessageConverters.

So - is there a better way to implement this requirement ? Is it possible to share MessageConverters across multiple RestTemplate instances, hence, improve performance significantly ?

EDIT Here is a sample of my code:

protected MyResponse<MyInterface> getInterfaces(RestTemplate restClient, RestParams inputParams, String cookie, File outputFile) {

    Request devInterfacesRequest = getRequest(RequestType.INTERFACES, inputParams, cookie);

    return restClient.postForObject(inputParams.getUrl(), devInterfacesRequest , new ParameterizedTypeReference<MyResponse<MyInterface>>(){});
}

Where can I add the outputFile ?

  • So your requirement is to log RestTemplate responses? (at the end of execute(...) method?). By the way. What's your SpringFramework version? – Laiv Apr 28 '16 at 7:50
  • Yep. Exactly where the interceptor runs. – Nati Apr 28 '16 at 7:52
  • Are you familiar with Spring AOP? ;-). If don't I will try to offer to you a simple answer to give you some hints – Laiv Apr 28 '16 at 7:53
  • A little bit. Could you elaborate? I'm running Spring latest version. – Nati Apr 28 '16 at 7:53
  • How can I send a different file name to the Aspect? – Nati Apr 28 '16 at 7:55
1

Well I have been thinking in this question and this's what I get:

Restrictions

Citing question's owner:

  1. To implement my own RestTemplate is to heavy

But I saw that RestTemplate is pretty expensive object to create, when about 70% of the creation time, is creating the MessageConverters.

  1. RestTemplate is not thread safe after construction. But it's not after construction.See more Is RestTemplate thread Safe?

Conceptually, it is very similar to the JdbcTemplate, JmsTemplate, and the various other templates found in the Spring Framework and other portfolio projects. This means, for instance, that the RestTemplate is thread-safe once constructed

This last feature is suggesting to don't modify a RestTemplate after its construction. So I neither would add any new Interceptor nor remove it

  1. We miss many details about the system and it's requirements, boundaries,etc...

  2. We don't know what these files are for. So it compromises the adequacy of the solution I propose.

What do we have?

If I'm right, we have Appache Log4J configured. (@Nati told me by chat).

So I have decided to use Log4j as logger tool. However, please check out Drawbacks section. (hope someone else reading this answer could dispel my doubts)

Why Log4j instead of my own File management? Because I wanted to delegate to Log4j the next features: access to files, usage of patterns, usage of filters (optional), usage of levels, ...

Solution

LoggerHelper

I have implemented a helper in order to make the solution portable through any layer with the less coupling possible.

package org.myworkouts.helper;

import java.io.File;
import java.io.IOException;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;


public class LoggerHelper {

    // Helper's own logger
    private static final Logger LOGGER = LogManager
            .getLogger(LoggerHelper.class);
    private static final String DEFAULT_LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n";
    private static final String LOGGER_PREFIX = "logger";
    private static final String APPENDER_PREFIX = "appender";

    private static LoggerHelper instance = null;

    // Singleton
    private LoggerHelper() {
        super();
    }

    public static LoggerHelper getInstance() {
        if (instance == null) {
            instance = new LoggerHelper();
        }
        return instance;
    }

    /**
     * Get the logger by its name or create if never was instanciated previously
     * 
     * @throws IOException
     */
    public Logger getCustomFileLogger(String filePath) throws IOException {
        String loggerName = resolveLoggerName(filePath);

        Logger logger = LogManager.exists(loggerName);
        if (logger == null) {
            LOGGER.info("Creating new Logger: ".concat(loggerName));
            logger = LogManager.getLogger(loggerName);
        }
        setLevel(logger, Level.INFO).addFileAppender(logger, filePath);
        return logger;
    }

    private LoggerHelper addFileAppender(Logger logger, String filePath)
            throws IOException {
        if (logger != null) {
            String appenderName = resolveAppenderName(filePath);
            if (logger.getAppender(appenderName) == null) {
                logger.addAppender(new FileAppender(new PatternLayout(
                        DEFAULT_LOG_PATTERN), filePath, Boolean.TRUE));
            }
        }
        return this;
    }

    private LoggerHelper setLevel(Logger logger, Level level) {
        if (logger != null) {
            logger.setLevel(level);
        }
        return this;
    }

    /* Optionl. Generate a custom name for a new Appender */
    private String resolveAppenderName(String filePath) {
        return APPENDER_PREFIX.concat(File.pathSeparator).concat(
                getNameWithoutExtension(filePath));
    }

    /* Optionl. Generate a custom name for a new Logger */
    private String resolveLoggerName(String filePath) {
        return LOGGER_PREFIX.concat(File.pathSeparator).concat(
                getNameWithoutExtension(filePath));
    }

    //Took from com.google.common.io.Files
    //I had it in my workspace but I didn't want to use it at this
    //class
    private String getNameWithoutExtension(String filePath) {
        String fileName = new File(filePath).getName();
        int dotIndex = fileName.lastIndexOf('.');
        return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
    }
}

Implementation

Attaching the solution to owner's code

protected MyResponse<MyInterface> getInterfaces(RestTemplate restClient, RestParams inputParams, String cookie, File outputFile) {

    Request devInterfacesRequest = getRequest(RequestType.INTERFACES, inputParams, cookie);

     MyResponse<MyInterface> response =  restClient.postForObject(inputParams.getUrl(), devInterfacesRequest , new ParameterizedTypeReference<MyResponse<MyInterface>>(){});
     try{
           Logger customLogger = LoggerHelper.getInstance().getCustomFileLogger(outputFile.getPath());
           //... we can also to parse it to Json, XML, whatever...
           customLogger.info(response.toString());
     }catch(IOException ex){
          //... what do we have to do if logger could not be instanciated?
     }
     return response;
}

Note: Implementation could be also moved to getInterfaces caller who has (I suppouse) a forbucle and also decide the outputFile. Then getCustomFileLogger(...) would be invoked just one time.

Test

public class LoggerHelperTest {

    public static final Logger LOGGER = LogManager.getLogger(LoggerHelperTest.class);

    @Test
    public void getCustomLoggerTest() throws IOException{
        Logger customLogger = LoggerHelper.getInstance().getCustomFileLogger("file.txt");
        Assert.assertNotNull("CustomLogger should not be null",customLogger);
        Assert.assertEquals("CustomLogger's level should be INFO by default",Level.INFO, customLogger.getLevel());
        Assert.assertTrue("CustomLogger's name should contain file's name on its name",customLogger.getName().contains("file"));
        customLogger.info("Trace 1");
        customLogger.info("Trace 2");
        customLogger.info("Trace 3");
    }
}

file.txt

The output I got

2016-05-03 13:38:56 INFO  logger:file:47 - Trace 1
2016-05-03 13:38:56 INFO  logger:file:48 - Trace 2
2016-05-03 13:38:56 INFO  logger:file:49 - Trace 3

Doubts

This's a suggestion. It's opened to suggestions or improvements. I also have my own doubts about my solution. (I didn't stress the solution):

  1. Is File outputFile locked (opened) by any other process? Then IOException will be thrown.
  2. Home any differents outputFile are? 10,100,1000? Can we hold that much of Loggers in Log4j?
  3. Are these outputFile going to be moved in runtime? If any is moved/deleted/replaced log4j wont be able to keep logging in it.
  4. Shall we catch IOException instead of let it go?. I delegate to LoggerHelper user to decice what to do...

I have used Log4j because I have had in mind that the requirement is to keep loggers separated printing everyone on its own file. But I expect they only contain log traces (for further reviews or debugs).

However if file's content matters, if content is important for us, the solution is not suitable.

If content matters and I don't want to implement a file management, I would think on storage these logs in noSQL db's (mongoDB or any other more suitable for our platform) which also is capable to support a high volume of data. Data can be obtained later with proper requests

  • Thank you for your detailed answer, but I wanted to log the response "on the wire". With your implementation, in case I have a conversion error, the response won't be logged. – Nati May 3 '16 at 19:40
  • If to log Is so critic, then do the conversion inside my try/catch. If conversion fails, let the runtime exception go... Anyways any good mapper/converter should not fail. If you do your own file management a IOExcpetion can be thrown any time. The problem is still there. Unless you wish also log 'HTTP request/ response headers, body, status, etc. Then my solution is not suitable for you – Laiv May 3 '16 at 19:48
  • The thing is conversion may drop unmapped fields, and I want the exact response, as received. Char by char. – Nati May 3 '16 at 19:53
  • Then you need to log HttpClient response I guess. And this component is out of your scope. Lets see if anyone else comes with a suitable solution:-) – Laiv May 3 '16 at 20:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.