Skip to content

Commit

Permalink
[JENKINS-51085] Auto-refresh LOGBack config when changed
Browse files Browse the repository at this point in the history
When the logback XML configuration has changed, reload the in-memory
logger transparently. If the reload fails, keep the existing well-known
configuration.

Change-Id: I99392adc2c9e3666616946d985c88f0a10dc1c09
  • Loading branch information
lucamilanesio committed May 8, 2018
1 parent 3122a79 commit aa2b301
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,33 @@ public interface Logback {
*/
Logback setLoggerName(String loggerName);


/**
* Get the current LOGBack logger name
*
* @return logger name
*/
String getLoggerName();

/**
* Send the message to LOGBack
*
* @param msg message to send to LOGBack
*/
void log(String msg);
}

/**
* Check for LOGBack configuration and reload when changed.
*
* @return true if the configuration has been reloaded
* @throws Exception if the configuration refresh failed
*/
public boolean refresh() throws Exception;

/**
* Return the SHA of the latest LOGBack configuration loaded.
*
* @return the URLSha of the last loaded configuration
*/
public URLSha getLastSha();
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,44 @@
package org.jenkins.plugins.statistics.gatherer.util;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LogbackFactory {
private static final Logger logger = Logger.getLogger(LogbackFactory.class.getName());
private static final int REFRESH_THREADS = 1;
private static final int REFRESH_INTERVAL_SECS = 60;
private static final ScheduledExecutorService refresher = Executors.newScheduledThreadPool(REFRESH_THREADS);
private static final Set<Logback> activeLogbacks = new HashSet<>();
private static ScheduledFuture<?> refresherTask;

public static Logback create(String loggerName) throws Exception {
public static synchronized Logback create(String loggerName) throws Exception {
Class<Logback> logback = (Class<Logback>) Class.forName(Logback.class.getName() + "Impl");
return logback.newInstance().setLoggerName(loggerName);
Logback logbackInstance = logback.newInstance().setLoggerName(loggerName);

activeLogbacks.add(logbackInstance);

if(refresherTask != null && !refresherTask.isCancelled()) {
refresherTask.cancel(true);
}

refresherTask = refresher.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
for (Logback logback: activeLogbacks) {
try {
logback.refresh();
} catch (Exception e) {
logger.log(Level.WARNING, "Unable to refresh LOGBack " + logback, e);
}
}
}
}, REFRESH_INTERVAL_SECS, REFRESH_INTERVAL_SECS, TimeUnit.SECONDS);

return logbackInstance;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,60 @@
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.core.joran.spi.JoranException;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

public class LogbackImpl implements Logback {
private static Logger logger;
private Logger logger;
private URLSha loggerSha;
private String loggerName;

@Override
public Logback setLoggerName(String loggerName) {
LoggerContext loggerContext = new LoggerContext();
ContextInitializer contextInitializer = new ContextInitializer(loggerContext);
try {
String configurationUrlString = PropertyLoader.getLogbackConfigXmlUrl();
if (configurationUrlString == null) {
throw new IllegalStateException("LOGBack XML configuration file not specified");
}

URL configurationUrl = new URL(configurationUrlString);
contextInitializer.configureByResource(configurationUrl);
logger = loggerContext.getLogger(loggerName);
this.loggerName = loggerName;
URL configurationUrl = getConfigurationURL();
initLogger(configurationUrl, loggerName);
return this;
} catch (JoranException e) {
throw new RuntimeException("Unable to configure logger", e);
} catch (MalformedURLException e) {
throw new RuntimeException("Unable to find LOGBack XML configuration", e);
} catch (IOException e) {
throw new RuntimeException("Unable to open LOGBack XML", e);
}
}

@Override
public String getLoggerName() {
return loggerName;
}

public URLSha getLastSha() {
return loggerSha;
}

@Override
public String toString() {
return super.toString();
}

private void initLogger(URL configurationUrl, String loggerName) throws JoranException, IOException {
LoggerContext loggerContext = new LoggerContext();
ContextInitializer contextInitializer = new ContextInitializer(loggerContext);
contextInitializer.configureByResource(configurationUrl);
this.loggerSha = new URLSha(configurationUrl);
this.logger = loggerContext.getLogger(loggerName);
}

private URL getConfigurationURL() throws MalformedURLException {
String configurationUrlString = PropertyLoader.getLogbackConfigXmlUrl();
if (configurationUrlString == null) {
throw new IllegalStateException("LOGBack XML configuration file not specified");
}

return new URL(configurationUrlString);
}

@Override
Expand All @@ -42,4 +71,19 @@ public void log(String msg) {
public Logger getLogger() {
return logger;
}

public boolean refresh() throws Exception {
if (loggerSha == null) {
return false;
}

URL configurationUrl = getConfigurationURL();
URLSha latestSha1 = new URLSha(getConfigurationURL());
if (!latestSha1.equals(loggerSha)) {
initLogger(configurationUrl, loggerName);
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jenkins.plugins.statistics.gatherer.util;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Objects;

public class URLSha {
private String value;

public URLSha(URL configurationUrl) throws IOException {
try (InputStream is = configurationUrl.openStream()) {
value = DigestUtils.sha1Hex(is);
}
}

@Override
public boolean equals(Object obj) {
return (obj instanceof URLSha) ? Objects.equals(this.value, ((URLSha) obj).value) : false;
}

@Override
public String toString() {
return "{sha1}" + value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.jenkins.plugins.statistics.gatherer.util;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class LogbackAbstractTest implements LogbackFixture {

public String newSimpleLogbackXmlUrl() throws IOException {
Path logbackXml = newSimpleLogbackXmlPath();
return logbackXml.toUri().toURL().toString();
}

public Path newSimpleLogbackXmlPath() throws IOException {
Path logbackXml = Files.createTempFile(LogbackImplTest.class.getName(), ".xml");
Files.write(logbackXml, SIMPLE_LOGBACK_XML.getBytes());
return logbackXml;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.jenkins.plugins.statistics.gatherer.util;

import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({PropertyLoader.class})
public class LogbackFactoryTest extends LogbackAbstractTest {
Path logbackXmlPath;

@Before
public void setup() throws IOException {
mockStatic(PropertyLoader.class);
logbackXmlPath = newSimpleLogbackXmlPath();
Files.write(logbackXmlPath, SIMPLE_LOGBACK_XML.getBytes());
when(PropertyLoader.getLogbackConfigXmlUrl()).thenReturn(logbackXmlPath.toUri().toURL().toString());
when(PropertyLoader.getShouldSendToLogback()).thenReturn(true);
}

@Test
public void givenFactory_whenCreating_thenNewLogbackIsCreated() throws Exception {
Logback logback = LogbackFactory.create("fooLogger");

assertThat(logback, is(notNullValue()));
assertThat(logback.getLoggerName(), is("fooLogger"));
}

@Test
public void givenFactory_whenChangingLogbackXML_thenShouldReload() throws Exception {
Logback logback = LogbackFactory.create("foo");
URLSha initialSha = logback.getLastSha();

Files.write(logbackXmlPath, (SIMPLE_LOGBACK_XML + "\n\t").getBytes());
logback.refresh();

assertThat(logback.getLastSha(), is(not(equalTo(initialSha))));
}

@Test
public void givenFactory_whenRefreshing_thenShouldNotReload() throws Exception {
Logback logback = LogbackFactory.create("foo");
URLSha initialSha = logback.getLastSha();

logback.refresh();

assertThat(logback.getLastSha(), is(equalTo(initialSha)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.jenkins.plugins.statistics.gatherer.util;

public interface LogbackFixture {

String SIMPLE_LOGBACK_XML = "<configuration>\n" +
"\n" +
" <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n" +
" <!-- encoders are assigned the type\n" +
" ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->\n" +
" <encoder>\n" +
" <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n" +
" </encoder>\n" +
" </appender>\n" +
"\n" +
" <root level=\"debug\">\n" +
" <appender-ref ref=\"STDOUT\" />\n" +
" </root>\n" +
"</configuration>";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

Expand All @@ -16,7 +15,6 @@
import java.nio.file.Files;
import java.nio.file.Path;

import static org.hamcrest.CoreMatchers.any;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
Expand All @@ -28,38 +26,24 @@

@RunWith(PowerMockRunner.class)
@PrepareForTest({PropertyLoader.class, Jenkins.class})
public class LogbackImplTest {
public class LogbackImplTest extends LogbackAbstractTest {

@Mock
Jenkins jenkinsMock;

Path logbackXml;
String logbackXml;

@Before
public void setup() throws IOException {
mockStatic(PropertyLoader.class);
mockStatic(Jenkins.class);

logbackXml = Files.createTempFile(LogbackImplTest.class.getName(), ".xml");
Files.write(logbackXml, ("<configuration>\n" +
"\n" +
" <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n" +
" <!-- encoders are assigned the type\n" +
" ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->\n" +
" <encoder>\n" +
" <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n" +
" </encoder>\n" +
" </appender>\n" +
"\n" +
" <root level=\"debug\">\n" +
" <appender-ref ref=\"STDOUT\" />\n" +
" </root>\n" +
"</configuration>").getBytes());
logbackXml = newSimpleLogbackXmlUrl();
}

@Test
public void givenLogbackAppender_whenLogbackIsConfigured_thenLoggerIsCreated() throws MalformedURLException {
when(PropertyLoader.getLogbackConfigXmlUrl()).thenReturn(logbackXml.toFile().toURI().toURL().toString());
when(PropertyLoader.getLogbackConfigXmlUrl()).thenReturn(logbackXml);
when(PropertyLoader.getShouldSendToLogback()).thenReturn(true);
when(Jenkins.getInstance()).thenReturn(jenkinsMock);
Plugin pluginMock = mock(Plugin.class);
Expand Down

0 comments on commit aa2b301

Please sign in to comment.