Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,30 @@ object LoggingContext extends mutable.Map[String, String] {

override def isEmpty: Boolean = ThreadContext.isEmpty

/**
* Runs the given block with the provided context data effectively added to the
* {@link ThreadContext} and removed after the block completes.
*
* @param context the map of key-value pairs to add to the context
* @param body the block of code to execute
* @tparam R the return type of the block
* @return the result of the block
* @since 13.2.0
*/
def withContext[R](context: Map[String, String])(body: => R): R = {
val oldState = context.keys.map(key => key -> ThreadContext.get(key)).toMap
ThreadContext.putAll(context.asJava)
try {
body
} finally {
oldState.foreach { case (key, oldValue) =>
if (oldValue == null) {
ThreadContext.remove(key)
} else {
ThreadContext.put(key, oldValue)
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,58 @@ class LoggingContextTest extends AnyFunSuite with Matchers {
result shouldBe Set("key1" -> "value1", "key2" -> "value2")
}

test("withContext should add and remove values") {
LoggingContext.clear()
LoggingContext.withContext(Map("key" -> "value")) {
LoggingContext.get("key") shouldBe Some("value")
}
LoggingContext.contains("key") shouldBe false
}

test("withContext should save and restore existing values") {
LoggingContext.clear()
LoggingContext += "key" -> "oldValue"

LoggingContext.withContext(Map("key" -> "newValue")) {
LoggingContext.get("key") shouldBe Some("newValue")
}

// Harus balik ke oldValue, bukan dihapus
LoggingContext.get("key") shouldBe Some("oldValue")
}

test("withContext should handle nested contexts") {
LoggingContext.clear()

LoggingContext.withContext(Map("outer" -> "outerValue", "common" -> "outerCommon")) {
LoggingContext.get("outer") shouldBe Some("outerValue")
LoggingContext.get("common") shouldBe Some("outerCommon")

LoggingContext.withContext(Map("inner" -> "innerValue", "common" -> "innerCommon")) {
LoggingContext.get("outer") shouldBe Some("outerValue")
LoggingContext.get("inner") shouldBe Some("innerValue")
// Inner harus override outer
LoggingContext.get("common") shouldBe Some("innerCommon")
}

// Pas keluar inner, common harus balik ke outerCommon
LoggingContext.get("common") shouldBe Some("outerCommon")
LoggingContext.contains("inner") shouldBe false
}

LoggingContext.isEmpty shouldBe true
}

test("withContext should cleanup on exception") {
LoggingContext.clear()
intercept[RuntimeException] {
LoggingContext.withContext(Map("key" -> "value")) {
throw new RuntimeException("Test exception")
}
}

// Pastikan tetap bersih meski ada error
LoggingContext.contains("key") shouldBe false
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,30 @@ object LoggingContext extends mutable.Map[String, String] {

override def isEmpty: Boolean = ThreadContext.isEmpty

/**
* Runs the given block with the provided context data effectively added to the
* {@link ThreadContext} and removed after the block completes.
*
* @param context the map of key-value pairs to add to the context
* @param body the block of code to execute
* @tparam R the return type of the block
* @return the result of the block
* @since 13.2.0
*/
def withContext[R](context: Map[String, String])(body: => R): R = {
val oldState = context.keys.map(key => key -> ThreadContext.get(key)).toMap
ThreadContext.putAll(context.asJava)
try {
body
} finally {
oldState.foreach { case (key, oldValue) =>
if (oldValue == null) {
ThreadContext.remove(key)
} else {
ThreadContext.put(key, oldValue)
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,30 @@ object LoggingContext extends mutable.Map[String, String] {

override def isEmpty: Boolean = ThreadContext.isEmpty

/**
* Runs the given block with the provided context data effectively added to the
* {@link ThreadContext} and removed after the block completes.
*
* @param context the map of key-value pairs to add to the context
* @param body the block of code to execute
* @tparam R the return type of the block
* @return the result of the block
* @since 13.2.0
*/
def withContext[R](context: Map[String, String])(body: => R): R = {
val oldState = context.keys.map(key => key -> ThreadContext.get(key)).toMap
ThreadContext.putAll(context.asJava)
try {
body
} finally {
oldState.foreach { case (key, oldValue) =>
if (oldValue == null) {
ThreadContext.remove(key)
} else {
ThreadContext.put(key, oldValue)
}
}
}
}

}
10 changes: 10 additions & 0 deletions src/changelog/.13.x.x/add-logging-context-with-context.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://logging.apache.org/xml/ns"
xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
type="added">
<issue id="97" link="https://github.com/apache/logging-log4j-scala/pull/97"/>
<description format="asciidoc">
Added `LoggingContext.withContext` to allow running a block of code with a populated `ThreadContext` that is automatically cleared upon completion.
</description>
</entry>
17 changes: 17 additions & 0 deletions src/site/antora/modules/ROOT/pages/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,20 @@ logger.debug(s"Logging in user ${user.getName} with birthday ${user.calcBirthday
Most logging implementations use a hierarchical scheme for matching logger names with logging configuration.
In this scheme the logger name hierarchy is represented by `.` characters in the logger name, in a fashion very similar to the hierarchy used for Java/Scala package names.
The `Logger` property added by the `Logging` trait follows this convention: the trait ensures the `Logger` is automatically named according to the class it is being used in.

[#scoped-context]
== Scoped Context (Loan Pattern)

To ensure that the `ThreadContext` is properly cleaned up after execution—especially in asynchronous environments where thread reuse can lead to context leaks—you can use the `LoggingContext.withContext` method.

[source,scala]
----
import org.apache.logging.log4j.scala.LoggingContext

// Context is automatically cleared after the block finishes
LoggingContext.withContext(Map("userId" -> "user-123", "role" -> "admin")) {
logger.info("This log has context data")
// Do some work...
}
// Context is clean here
----