background

  • Springboot integrates numerous containers (Tomcat, Jetty, Undertow)

  • Undertow is a container with high concurrency performance. Since the default container is Tomcat, we usually dry out Tomcat’s JAR package and introduce Undertow’s JAR package, thus opening the Undertow container

  • The project needs to log AccessLog to save and query interface calls

  • By default, the AccessLog log file splits small files by day at dawn every day. By default, the generated file name is as follows:

    access_log.log access_log.2021-02-11.log
    Copy the code
  • By default, the AccessLog is not automatically deleted. After a long time, disk space may be insufficient

  • The company provides a functional proxy service for automatic log deletion (you can set the maximum log retention period), but the format of the log file name must be standardized. For example: must conform to “. Date format “(the date can be day and hour dimensions) as in:

    access_log.log.2021-02-11
    Copy the code
  • Access_log.2021-02-11. log does not meet the log file name standard, so the automatic log deletion agent cannot identify the log. The logs will be overlogged, and you have to manually delete the logs from the cluster, which takes time

  • The default Undertow cannot modify and customize file names. Although the prefix and suffix can be set, the rules are stiff, the position and format of the date in the file name cannot be adjusted, and the generated date ends with a “.” without a “.” at the beginning, and cannot meet the matching rules of the log deletion agent

    Accesslog: dir: "logs" # path enabled: true 'common' # Matching pattern of requests (which can match interface path, time, response code, IP, etc.), used to generate request log contentCopy the code

gripper

  • To solve the problem of AccessLog file names not being customizable, start with the Undertow source code
  • Find where the log file name was generated from the source code and rewrite this part of the logic

To solve the process

1. Open Undertow’s source package first

Find server. Handlers. Under the accesslog related accesslog processing class

2. Next, look at interfaces

The AccessLogReceiver interface has two implementations

DefaultAccessLogReceiver and JBossLoggingAccessLogReceiverCopy the code

package io.undertow.server.handlers.accesslog; /** * Interface that is used by the access log handler to send data to the log file manager. * * Implementations of this  interface must be thread safe. * * @author Stuart Douglas */ public interface AccessLogReceiver { void logMessage(final  String message); }Copy the code

If you look at the comments, you can see that this interface is related to handling log files, and then go to the implementation classes one by one

/** * Access log receiver that logs messages at INFO level. * * @author Stuart Douglas */ public class JBossLoggingAccessLogReceiver implements AccessLogReceiver { public static final String DEFAULT_CATEGORY = "io.undertow.accesslog"; private final Logger logger; public JBossLoggingAccessLogReceiver(final String category) { this.logger = Logger.getLogger(category); } public JBossLoggingAccessLogReceiver() { this.logger = Logger.getLogger(DEFAULT_CATEGORY); } @Override public void logMessage(String message) { logger.info(message); }}Copy the code

JBossLoggingAccessLogReceiver not to what the log file, simply log print, then look at another

If you look at the class annotation for DefaultAccessLogReceiver, you can probably guess where to look. Next, look for the constructor (where the variable is initialized).

public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, null); } public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, final String logNameSuffix) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, true); } public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate) { this(logWriteExecutor, outputDirectory.toPath(), logBaseName, logNameSuffix, rotate); } public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName) { this(logWriteExecutor, outputDirectory, logBaseName, null); } public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix) { this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, true); } public DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate) { this(logWriteExecutor, outputDirectory, logBaseName, logNameSuffix, rotate, null); } private DefaultAccessLogReceiver(final Executor logWriteExecutor, final Path outputDirectory, final String logBaseName, final String logNameSuffix, boolean rotate, LogFileHeaderGenerator fileHeader) { this.logWriteExecutor = logWriteExecutor; this.outputDirectory = outputDirectory; this.logBaseName = logBaseName; this.rotate = rotate; this.fileHeaderGenerator = fileHeader; this.logNameSuffix = (logNameSuffix ! = null) ? logNameSuffix : DEFAULT_LOG_SUFFIX; this.pendingMessages = new ConcurrentLinkedDeque<>(); this.defaultLogFile = outputDirectory.resolve(logBaseName + this.logNameSuffix); calculateChangeOverPoint(); }Copy the code

You can see that multiple constructors call one place where you can see the key parameters, such as prefixes, suffixes, and paths, that we configured in the configuration file. CalculateChangeOverPoint () is a special method, so let’s move on

private void calculateChangeOverPoint() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.add(Calendar.DATE, 1); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US); currentDateString = df.format(new Date()); // If there are existing default log files, If (files.exists (defaultLogFile)) {try {currentDateString = df.format(new) Date(Files.getLastModifiedTime(defaultLogFile).toMillis())); } catch(IOException e){// ignore. If an exception occurs, use the current date}} changeOverPoint = calendar.getTimeInmillis (); }Copy the code

As you can see, this class specifies the time format, which can only be the date “YYYY-MM-DD” and assigns currentDateString to the current time or the last modification time of access_log.log.

At the same time, record the milliseconds of changeOverPoint for tomorrow morning (for example, tomorrow is 2020-02-19 00:00:00), as a judgment basis to judge whether the current time is the next day.

Observing that this class also inherits Runnable and implements the run() method, you can see that writing to the AccessLog file is asynchronous by default

/** * processes all queued log messages */ @Override public void run() { if (! stateUpdater.compareAndSet(this, 1, 2)) { return; } if (forceLogRotation) { doRotate(); } else if (initialRun && files.exists (defaultLogFile)) {// If there is an existing log file, check whether long lm = 0 should be cut; try { lm = Files.getLastModifiedTime(defaultLogFile).toMillis(); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(e); } Calendar c = Calendar.getInstance(); c.setTimeInMillis(changeOverPoint); c.add(Calendar.DATE, -1); if (lm <= c.getTimeInMillis()) { doRotate(); } } initialRun = false; List<String> messages = new ArrayList<>(); String msg; For (int I = 0; i < 1000; ++i) { msg = pendingMessages.poll(); if (msg == null) { break; } messages.add(msg); } try { if (! Messages.isempty ()) {// Write the content to writeMessage(messages); }} finally {// ignore first}}Copy the code

We can look at writeMessage first; This method

Private void writeMessage(final List<String> messages) {// If (System.currentTimemillis () > changeOverPoint) {doRotate(); } try { if (writer == null) { boolean created = ! Files.exists(defaultLogFile); writer = Files.newBufferedWriter(defaultLogFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE); if(Files.size(defaultLogFile) == 0 && fileHeaderGenerator ! = null) { String header = fileHeaderGenerator.generateHeader(); if(header ! = null) { writer.write(header); writer.newLine(); writer.flush(); } } } for (String message : messages) { writer.write(message); writer.newLine(); } writer.flush(); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorWritingAccessLog(e); }}Copy the code

This method calls doRotate(); To do the log slicing, we move on to the doRotate() method

private void doRotate() { forceLogRotation = false; if (! rotate) { return; } try { if (writer ! = null) { writer.flush(); writer.close(); writer = null; } if (! Files.exists(defaultLogFile)) { return; } // Path newFile = outputDirectory. Resolve (logBaseName + currentDateString +"."+ logNameSuffix); int count = 0; While (files.exists (newFile)) {++count; newFile = outputDirectory.resolve(logBaseName + currentDateString + "-" + count + "." + logNameSuffix); } Files.move(defaultLogFile, newFile); } catch (IOException e) { UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(e); } finally { calculateChangeOverPoint(); }}Copy the code

You can see that the file name generation of newFile is dead (too bad, too inflexible)

So that’s where we need to rewrite it, and then how do we rewrite this piece of logic

3. Search for the overwritten link

If it is a bean that spring automatically assembly, then we just need to find a way to replace the call. If it is written out of new, it can only be found on the Internet layer by layer. Until you find where the Spring bean was created

Next, from the constructor, search for where the object was generated

You can see there are two places

Find the called flow by starting SpringBoot and making a breakpoint at each location to see if it reaches those locations (yes, it is low), You can find the AccessLogHttpHandlerFactory getHandler method created DefaultAccessLogReceiver object of this class

That can only move on the upper calls to find (by querying AccessLogHttpHandlerFactory constructor generated)

Can see UndertowWebServerFactoryDelegate this class generation AccessLogHttpHandlerFactory object

We continue to track down the caller by constructor + breakpoint

There are so many

Finally locate the factory class UndertowServletWebServerFactory, fancy new directly, and or private (man is a limit, I don’t behave, jo-jo!!!)

Keep looking for where the factory was generated

Finally find where the bean was created (only if you can get to that place (DIO))

And you can see there’s a little bit of conscience here

@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
Copy the code

If we do not provide the ServletWebServerFactory by default, we will go here. In other words, we can provide a custom ServletWebServerFactory Bean to override the logic above

4. Rewrite the file name generation rule

Once you find where the bean was created, you can register the new bean directly

@Configuration public class ServletWebServerFactoryConfig { @Bean public CustomUndertowServletWebServerFactory customUndertowServletWebServerFactory( ObjectProvider<UndertowDeploymentInfoCustomizer> deploymentInfoCustomizers, ObjectProvider<UndertowBuilderCustomizer> builderCustomizers) { CustomUndertowServletWebServerFactory factory = new CustomUndertowServletWebServerFactory(); factory.getDeploymentInfoCustomizers() .addAll(deploymentInfoCustomizers.orderedStream().collect(Collectors.toList())); factory.getBuilderCustomizers() .addAll(builderCustomizers.orderedStream().collect(Collectors.toList())); return factory; }}Copy the code

Need UndertowWebServerFactoryDelegate changes, combined with reflection, generate our custom CustomAccessLogHttpHandlerFactory

public class CustomUndertowServletWebServerFactory extends UndertowServletWebServerFactory {

  @Override
  protected UndertowServletWebServer getUndertowWebServer(Builder builder,
      DeploymentManager manager, int port) {
    Object delegate = ReflectUtil.getFieldValue(this, "delegate");
    List<HttpHandlerFactory> httpHandlerFactories = createHttpHandlerFactories(delegate, this,
        new CustomDeploymentManagerHttpHandlerFactory(manager));
    return new UndertowServletWebServer(builder, httpHandlerFactories, getContextPath(), port >= 0);
  }

  List<HttpHandlerFactory> createHttpHandlerFactories(Object delegate,
      AbstractConfigurableWebServerFactory webServerFactory,
      HttpHandlerFactory... initialHttpHandlerFactories) {
    boolean useForwardHeaders = (Boolean) ReflectUtil.getFieldValue(delegate, "useForwardHeaders");
    File accessLogDirectory = (File) ReflectUtil.getFieldValue(delegate, "accessLogDirectory");
    String accessLogPattern = (String) ReflectUtil.getFieldValue(delegate, "accessLogPattern");
    String accessLogPrefix = (String) ReflectUtil.getFieldValue(delegate, "accessLogPrefix");
    String accessLogSuffix = (String) ReflectUtil.getFieldValue(delegate, "accessLogSuffix");
    Boolean accessLogRotate = (Boolean) ReflectUtil.getFieldValue(delegate, "accessLogRotate");

    List<HttpHandlerFactory> factories = createHttpHandlerFactories(
        webServerFactory.getCompression(),
        useForwardHeaders, webServerFactory.getServerHeader(), webServerFactory.getShutdown(),
        initialHttpHandlerFactories);
    if (isAccessLogEnabled()) {
      factories
          .add(new CustomAccessLogHttpHandlerFactory(accessLogDirectory, accessLogPattern,
              accessLogPrefix, accessLogSuffix, accessLogRotate));
    }
    return factories;
  }

  static List<HttpHandlerFactory> createHttpHandlerFactories(Compression compression,
      boolean useForwardHeaders,
      String serverHeader, Shutdown shutdown, HttpHandlerFactory... initialHttpHandlerFactories) {
    List<HttpHandlerFactory> factories = new ArrayList<>(
        Arrays.asList(initialHttpHandlerFactories));
    if (compression != null && compression.getEnabled()) {
      factories.add(new CustomCompressionHttpHandlerFactory(compression));
    }
    if (useForwardHeaders) {
      factories.add(Handlers::proxyPeerAddress);
    }
    if (StringUtils.hasText(serverHeader)) {
      factories.add((next) -> Handlers.header(next, "Server", serverHeader));
    }
    if (shutdown == Shutdown.GRACEFUL) {
      factories.add(Handlers::gracefulShutdown);
    }
    return factories;
  }
}
Copy the code

In CustomAccessLogHttpHandlerFactory modify, convert our custom CustomDefaultAccessLogReceiver

Through the new class CustomDefaultAccessLogReceiver (this class was DefaultAccessLogReceiver copied the source code, after modified the files generated in a doRatate method to rule), rewrite doRatate method, Then change the file naming rules

Classes that are similar to other requirements need to be copied as well

conclusion

  • This project writing encountered practical problems and combined with the source code step by step analysis.
  • Through the constructor and breakpoint analysis, the call link is found.
  • By copying and replacing the upper-layer link Bean and part of the source code, the switch of the overall function is realized (everything is object).
  • Through the analysis of the source code to share, I hope to provide a solution to the problem.
  • If it is helpful to you, please click three links, pay more attention to me, and provide more sharing in the future.