Skip to content

Commit

Permalink
Add Tomcat-specific context selector
Browse files Browse the repository at this point in the history
Add a context selector that uses the `WebappProperties` of the context
classloader to select a different logger context for each web
application and global Tomcat code.

Closes #105.
  • Loading branch information
ppkarwasz committed Jan 29, 2024
1 parent 561b255 commit 1c8e57d
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 33 deletions.
20 changes: 17 additions & 3 deletions log4j-tomcat-env/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,49 @@
</properties>

<dependencies>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-juli</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-juli</artifactId>
<scope>provided</scope>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright © 2023 Piotr P. Karwasz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.copernik.log4j.tomcat.env;

import java.net.URI;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.async.AsyncLoggerContext;

public class TomcatAsyncContextSelector extends TomcatContextSelector {

@Override
protected LoggerContext createContext(final String name, final URI configLocation) {
final AsyncLoggerContext context = new AsyncLoggerContext(name, null, configLocation);
context.addShutdownListener(this);
return context;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright © 2023 Piotr P. Karwasz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.copernik.log4j.tomcat.env;

import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.juli.WebappProperties;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.impl.ContextAnchor;
import org.apache.logging.log4j.core.selector.ContextSelector;
import org.apache.logging.log4j.spi.LoggerContextShutdownAware;
import org.apache.logging.log4j.status.StatusLogger;

public class TomcatContextSelector implements ContextSelector, LoggerContextShutdownAware {

private static final String GLOBAL_CONTEXT_NAME = "-tomcat";
private static final Logger LOGGER = StatusLogger.getLogger();

private final AtomicReference<LoggerContext> GLOBAL_CONTEXT = new AtomicReference<>();
private final ConcurrentMap<String, AtomicReference<WeakReference<LoggerContext>>> CONTEXT_MAP =
new ConcurrentHashMap<>();

@Override
public boolean hasContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
if (currentContext && ContextAnchor.THREAD_CONTEXT.get() != null) {
return true;
}
final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
if (tccl instanceof WebappProperties) {
final String name = getContextName((WebappProperties) tccl);
final AtomicReference<WeakReference<LoggerContext>> ref = CONTEXT_MAP.get(name);
if (ref == null) {
return false;
}
final WeakReference<LoggerContext> weakRef = ref.get();
return weakRef != null && weakRef.get() != null;
}
return GLOBAL_CONTEXT.get() != null;
}

@Override
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final boolean currentContext) {
return getContext(fqcn, loader, null, currentContext);
}

@Override
public LoggerContext getContext(
final String fqcn,
final ClassLoader loader,
final Entry<String, Object> entry,
final boolean currentContext) {
return getContext(fqcn, loader, entry, currentContext, null);
}

@Override
public LoggerContext getContext(
final String fqcn, final ClassLoader loader, final boolean currentContext, final URI configLocation) {
return getContext(fqcn, loader, null, currentContext, configLocation);
}

@Override
public LoggerContext getContext(
final String fqcn,
final ClassLoader loader,
final Entry<String, Object> entry,
final boolean currentContext,
final URI configLocation) {
if (currentContext) {
final LoggerContext ctx = ContextAnchor.THREAD_CONTEXT.get();
if (ctx != null) {
return ctx;
}
}
final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
final LoggerContext ctx;
if (tccl instanceof WebappProperties) {
final String name = getContextName((WebappProperties) tccl);
final AtomicReference<WeakReference<LoggerContext>> ref =
CONTEXT_MAP.computeIfAbsent(name, ignored -> new AtomicReference<>());
final WeakReference<LoggerContext> weakRef = ref.get();
final LoggerContext oldCtx = weakRef != null ? weakRef.get() : null;
if (oldCtx != null) {
ctx = oldCtx;
} else {
ctx = createContext(name, configLocation);
ref.compareAndSet(weakRef, new WeakReference<>(ctx));
}
} else {
ctx = getGlobal();
}
if (entry != null) {
final String key = entry.getKey();
if (ctx.getObject(entry.getKey()) == null) {
LOGGER.debug("Setting logger context key {} to {}.", entry.getKey(), entry.getValue());
ctx.putObject(entry.getKey(), entry.getValue());
} else if (!ctx.getObject(key).equals(entry.getValue())) {
LOGGER.warn(
"Existing logger context has {} associated to the key {}. Can not change it to {}.",
ctx.getObject(key),
key,
entry.getValue());
}
}
if (ctx.getConfigLocation() == null && configLocation != null) {
LOGGER.debug("Setting configuration to {}.", configLocation);
ctx.setConfigLocation(configLocation);
} else if (ctx.getConfigLocation() != null
&& configLocation != null
&& !ctx.getConfigLocation().equals(configLocation)) {
LOGGER.warn(
"Existing logger context has configuration {}. Can not change it to {}",
ctx.getConfigLocation(),
configLocation);
}
return ctx;
}

@Override
public List<LoggerContext> getLoggerContexts() {
final List<LoggerContext> loggerContexts = new ArrayList<>(CONTEXT_MAP.size() + 1);
CONTEXT_MAP.values().forEach(ref -> {
final WeakReference<LoggerContext> weakRef = ref.get();
final LoggerContext ctx = weakRef != null ? weakRef.get() : null;
if (ctx != null) {
loggerContexts.add(ctx);
}
});
final LoggerContext global = GLOBAL_CONTEXT.get();
if (global != null) {
loggerContexts.add(global);
}
return loggerContexts;
}

@Override
public void removeContext(final LoggerContext context) {
CONTEXT_MAP.remove(context.getName());
GLOBAL_CONTEXT.compareAndSet(context, null);
}

@Override
public boolean isClassLoaderDependent() {
return false;
}

protected LoggerContext createContext(final String name, final URI configLocation) {
final LoggerContext context = new LoggerContext(name, null, configLocation);
context.addShutdownListener(this);
return context;
}

private String getContextName(final WebappProperties props) {
return String.format("/%s/%s/%s", props.getServiceName(), props.getHostName(), props.getWebappName());
}

private LoggerContext getGlobal() {
final LoggerContext ctx = GLOBAL_CONTEXT.get();
if (ctx != null) {
return ctx;
}
GLOBAL_CONTEXT.compareAndSet(null, createContext(GLOBAL_CONTEXT_NAME, null));
return GLOBAL_CONTEXT.get();
}

@Override
public void contextShutdown(final org.apache.logging.log4j.spi.LoggerContext loggerContext) {
if (loggerContext instanceof LoggerContext) {
CONTEXT_MAP.remove(((LoggerContext) loggerContext).getName());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright © 2024 Piotr P. Karwasz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.copernik.log4j.tomcat.env;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;

import java.io.IOException;
import java.util.Collections;
import org.apache.juli.WebappProperties;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;

class AbstractClassLoaderTest {
protected static final String ENGINE_NAME = "Catalina";
protected static final String HOST_NAME = "localhost";
protected static final String CONTEXT_NAME = "/myapp";
private static ClassLoader originalTccl;

@BeforeAll
public static void setupContextClassloader() throws IOException {
originalTccl = Thread.currentThread().getContextClassLoader();
final ClassLoader tccl = mock(ClassLoader.class, withSettings().extraInterfaces(WebappProperties.class));
final WebappProperties props = (WebappProperties) tccl;
when(props.getServiceName()).thenReturn(ENGINE_NAME);
when(props.getHostName()).thenReturn(HOST_NAME);
when(props.getWebappName()).thenReturn(CONTEXT_NAME);
// to prevent an NPE
when(tccl.getResources(anyString())).thenReturn(Collections.emptyEnumeration());
Thread.currentThread().setContextClassLoader(tccl);
}

@AfterAll
public static void clearContextClassloader() {
Thread.currentThread().setContextClassLoader(originalTccl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright © 2024 Piotr P. Karwasz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.copernik.log4j.tomcat.env;

import static org.assertj.core.api.Assertions.assertThat;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.async.AsyncLogger;
import org.apache.logging.log4j.core.selector.ContextSelector;
import org.junit.jupiter.api.Test;

class GlobalTomcatContextSelectorTest {

@Test
void contextSelector() {
final ContextSelector selector = new TomcatContextSelector();
final LoggerContext context = selector.getContext(null, null, false);
assertThat(context.getName()).isEqualTo("-tomcat");
}

@Test
void asyncContextSelector() {
final ContextSelector selector = new TomcatAsyncContextSelector();
final LoggerContext context = selector.getContext(null, null, false);
assertThat(context.getName()).isEqualTo("-tomcat");
assertThat(context.getLogger(LogManager.ROOT_LOGGER_NAME)).isInstanceOf(AsyncLogger.class);
}
}
Loading

0 comments on commit 1c8e57d

Please sign in to comment.