Skip to content

Commit

Permalink
Scoped context (#2438)
Browse files Browse the repository at this point in the history
* Add ScopedContext and ResourceLogger

* ResourceLogger uses ScopedContext

* Use ExtendedLoggerWrapper

* Move ContextDataProviders to the API

* Update docs

* Remove ParameterizedMapMessage

* Remove ParameterizedMapMessage from test

* Fix typo

* Ensure unit test uses a unique file name

* Move non-API classes to core

* Revert unit test change. Correct site changes

* Fix comment

* Remove the Scopedcontext.Renderable interface

* Fix site conflicts

* Delegate `ScopedContext` functionality to interface

To provide more configurability for the `ScopedContext` service, this PR
moves its implementation details to `log4j-core` and replaces it with a
`ScopedContextProvider` interface.

In Log4j API only a NO-OP version of the provider is present, but each
implementation of the API can provide its own.

* Incorporate Piotr's changes - mostly

* Return raw object as it was stored. Add wrap methods

* More abstractions

* Fix javadoc comment

---------

Co-authored-by: Piotr P. Karwasz <piotr.github@karwasz.org>
  • Loading branch information
rgoers and ppkarwasz committed Apr 15, 2024
1 parent c88a546 commit 86ff06c
Show file tree
Hide file tree
Showing 52 changed files with 2,902 additions and 212 deletions.
11 changes: 6 additions & 5 deletions log4j-api-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<bnd-module-name>org.apache.logging.log4j.test</bnd-module-name>
<bnd-extra-package-options>
org.apache.commons.lang3.*;resolution:=optional,
org.assertj.*;resolution:=optional,
<!-- Both JUnit 4 and JUnit 5 are not required -->
org.junit.*;resolution:=optional,
org.hamcrest.*;resolution:=optional,
Expand All @@ -48,6 +49,7 @@
<bnd-extra-module-options>
<!-- Non-transitive static modules -->
junit;transitive=false,
org.assertj.core;transitive=false,
org.hamcrest;transitive=false,
org.junit.jupiter.api;transitive=false,
org.junitpioneer;transitive=false,
Expand All @@ -72,6 +74,10 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand Down Expand Up @@ -108,11 +114,6 @@
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Required for JSON support -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.Level;
Expand All @@ -28,6 +29,7 @@
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.spi.AbstractLogger;
import org.apache.logging.log4j.util.ProviderUtil;

/**
*
Expand Down Expand Up @@ -79,7 +81,12 @@ protected void log(
sb.append(' ');
}
sb.append(message.getFormattedMessage());
final Map<String, String> mdc = ThreadContext.getImmutableContext();
final Map<String, ?> contextMap =
ProviderUtil.getProvider().getScopedContextProvider().getContextMap();
final Map<String, String> mdc = new HashMap<>(ThreadContext.getImmutableContext());
if (contextMap != null && !contextMap.isEmpty()) {
contextMap.forEach((key, value) -> mdc.put(key, value.toString()));
}
if (!mdc.isEmpty()) {
sb.append(' ');
sb.append(mdc);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.logging.log4j.test.spi;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.logging.log4j.ScopedContext;
import org.apache.logging.log4j.spi.ScopedContextProvider;

/**
* Provides test that should be passed by all implementations of {@link ScopedContextProviderSuite}.
* @since 2.24.0
*/
public abstract class ScopedContextProviderSuite {

private static ScopedContext.Instance where(
final ScopedContextProvider provider, final String key, final Object value) {
return provider.newScopedContext(key, value);
}

protected static void testScope(final ScopedContextProvider scopedContext) {
where(scopedContext, "key1", "Log4j2")
.run(() -> assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2"));
where(scopedContext, "key1", "value1").run(() -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
where(scopedContext, "key2", "value2").run(() -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
assertThat(scopedContext.getValue("key2")).isEqualTo("value2");
});
});
}

private static void runWhere(
final ScopedContextProvider provider, final String key, final Object value, final Runnable task) {
provider.newScopedContext(key, value).run(task);
}

private static Future<Void> runWhere(
final ScopedContextProvider provider,
final String key,
final Object value,
final ExecutorService executorService,
final Runnable task) {
return provider.newScopedContext(key, value).run(executorService, task);
}

protected static void testRunWhere(final ScopedContextProvider scopedContext) {
runWhere(scopedContext, "key1", "Log4j2", () -> assertThat(scopedContext.getValue("key1"))
.isEqualTo("Log4j2"));
runWhere(scopedContext, "key1", "value1", () -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
runWhere(scopedContext, "key2", "value2", () -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("value1");
assertThat(scopedContext.getValue("key2")).isEqualTo("value2");
});
});
}

protected static void testRunThreads(final ScopedContextProvider scopedContext) {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue);
final long id = Thread.currentThread().getId();
final AtomicLong counter = new AtomicLong(0);
runWhere(scopedContext, "key1", "Log4j2", () -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
Future<?> future = runWhere(scopedContext, "key2", "value2", executorService, () -> {
assertNotEquals(Thread.currentThread().getId(), id);
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
counter.incrementAndGet();
});
assertDoesNotThrow(() -> {
future.get();
assertTrue(future.isDone());
assertThat(counter.get()).isEqualTo(1);
});
});
}

protected static void testThreads(final ScopedContextProvider scopedContext) throws Exception {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue);
final long id = Thread.currentThread().getId();
final AtomicLong counter = new AtomicLong(0);
where(scopedContext, "key1", "Log4j2").run(() -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
Future<?> future = where(scopedContext, "key2", "value2").run(executorService, () -> {
assertNotEquals(Thread.currentThread().getId(), id);
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
counter.incrementAndGet();
});
assertDoesNotThrow(() -> {
future.get();
assertTrue(future.isDone());
assertThat(counter.get()).isEqualTo(1);
});
});
}

protected static void testThreadException(final ScopedContextProvider scopedContext) throws Exception {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
final AtomicBoolean exceptionCaught = new AtomicBoolean(false);
ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue);
long id = Thread.currentThread().getId();
runWhere(scopedContext, "key1", "Log4j2", () -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
Future<?> future = where(scopedContext, "key2", "value2").run(executorService, () -> {
assertNotEquals(Thread.currentThread().getId(), id);
throw new NullPointerException("On purpose NPE");
});
assertThatThrownBy(future::get)
.hasRootCauseInstanceOf(NullPointerException.class)
.hasRootCauseMessage("On purpose NPE");
});
}

private static <R> R callWhere(
final ScopedContextProvider provider, final String key, final Object value, final Callable<R> task)
throws Exception {
return provider.newScopedContext(key, value).call(task);
}

private static <R> Future<R> callWhere(
final ScopedContextProvider provider,
final String key,
final Object value,
final ExecutorService executorService,
final Callable<R> task) {
return provider.newScopedContext(key, value).call(executorService, task);
}

protected static void testThreadCall(final ScopedContextProvider scopedContext) throws Exception {
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5);
ExecutorService executorService = new ThreadPoolExecutor(1, 2, 30, TimeUnit.SECONDS, workQueue);
final long id = Thread.currentThread().getId();
final AtomicInteger counter = new AtomicInteger(0);
int returnVal = callWhere(scopedContext, "key1", "Log4j2", () -> {
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
Future<Integer> future = callWhere(scopedContext, "key2", "value2", executorService, () -> {
assertNotEquals(Thread.currentThread().getId(), id);
assertThat(scopedContext.getValue("key1")).isEqualTo("Log4j2");
return counter.incrementAndGet();
});
Integer val = future.get();
assertTrue(future.isDone());
assertThat(counter.get()).isEqualTo(1);
return val;
});
assertThat(returnVal).isEqualTo(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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 org.apache.logging.log4j;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import org.apache.logging.log4j.test.TestLogger;
import org.apache.logging.log4j.test.TestLoggerContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Class Description goes here.
*/
public class ResourceLoggerTest {
@BeforeAll
public static void beforeAll() {
System.setProperty("log4j2.loggerContextFactory", TestLoggerContextFactory.class.getName());
}

@BeforeAll
public static void afterAll() {
System.clearProperty("log4j2.loggerContextFactory");
}

@Test
public void testFactory() throws Exception {
Connection connection = new Connection("Test", "dummy");
connection.useConnection();
MapSupplier mapSupplier = new MapSupplier(connection);
Logger logger = ResourceLogger.newBuilder()
.withClass(this.getClass())
.withSupplier(mapSupplier)
.build();
logger.debug("Hello, {}", "World");
Logger log = LogManager.getLogger(this.getClass().getName());
assertTrue(log instanceof TestLogger);
TestLogger testLogger = (TestLogger) log;
List<String> events = testLogger.getEntries();
assertThat(events, hasSize(1));
assertThat(events.get(0), containsString("Name=Test"));
assertThat(events.get(0), containsString("Type=dummy"));
assertThat(events.get(0), containsString("Count=1"));
assertThat(events.get(0), containsString("Hello, World"));
events.clear();
connection.useConnection();
logger.debug("Used the connection");
assertThat(events.get(0), containsString("Count=2"));
assertThat(events.get(0), containsString("Used the connection"));
events.clear();
connection = new Connection("NewConnection", "fiber");
connection.useConnection();
mapSupplier = new MapSupplier(connection);
logger = ResourceLogger.newBuilder().withSupplier(mapSupplier).build();
logger.debug("Connection: {}", "NewConnection");
assertThat(events, hasSize(1));
assertThat(events.get(0), containsString("Name=NewConnection"));
assertThat(events.get(0), containsString("Type=fiber"));
assertThat(events.get(0), containsString("Count=1"));
assertThat(events.get(0), containsString("Connection: NewConnection"));
events.clear();
}

private static class MapSupplier implements Supplier<Map<String, ?>> {

private final Connection connection;

public MapSupplier(final Connection connection) {
this.connection = connection;
}

@Override
public Map<String, ?> get() {
Map<String, String> map = new HashMap<>();
map.put("Name", connection.name);
map.put("Type", connection.type);
map.put("Count", Long.toString(connection.getCounter()));
return map;
}

@Override
public boolean equals(Object o) {
return o instanceof MapSupplier;
}

@Override
public int hashCode() {
return 77;
}
}

private static class Connection {

private final String name;
private final String type;
private final AtomicLong counter = new AtomicLong(0);

public Connection(final String name, final String type) {
this.name = name;
this.type = type;
}

public String getName() {
return name;
}

public String getType() {
return type;
}

public long getCounter() {
return counter.get();
}

public void useConnection() {
counter.incrementAndGet();
}
}
}

0 comments on commit 86ff06c

Please sign in to comment.