diff --git a/log4j2/README.md b/log4j2/README.md
new file mode 100644
index 0000000..6f45b36
--- /dev/null
+++ b/log4j2/README.md
@@ -0,0 +1,270 @@
+# Reforge SDK Log4j2 Integration
+
+This module provides seamless integration between the Reforge SDK and Apache Log4j2, enabling dynamic log level management for your application.
+
+## Overview
+
+The Log4j2 integration uses an AbstractFilter to intercept logging calls and dynamically determine whether they should be logged based on log level configuration from Reforge. This allows you to:
+
+- **Centrally manage log levels** - Control logging across your entire application from the Reforge dashboard
+- **Real-time updates** - Change log levels without restarting your application
+- **Context-aware logging** - Different log levels for different loggers based on runtime context
+- **Performance** - Efficient filtering happens before log message construction
+
+## Installation
+
+Add the dependency to your `pom.xml`:
+
+```xml
+
+ com.reforge
+ sdk-log4j2
+ 1.0.3
+
+```
+
+### Compatibility
+
+This module is compatible with:
+- **Log4j2 2.x** - The module uses only stable Log4j2 Core APIs (AbstractFilter, LoggerContext) that have been stable across Log4j2 2.x versions
+- **SLF4J 1.7.x and 2.0.x** (both work with Log4j2)
+
+**Note:** Log4j2 and SLF4J are marked as `provided` dependencies - your application's versions will be used automatically. We compile against Log4j2 2.19.0, but the APIs used are stable across the 2.x line. Log4j2 works with both SLF4J 1.7.x and 2.0.x.
+
+## Usage
+
+### Basic Setup (Programmatic)
+
+The simplest approach is to install the filter programmatically during application startup:
+
+```java
+import com.reforge.sdk.Sdk;
+import com.reforge.sdk.Options;
+import com.reforge.sdk.log4j2.ReforgeLog4j2Filter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class MyApplication {
+ private static final Logger log = LogManager.getLogger(MyApplication.class);
+
+ public static void main(String[] args) {
+ // Initialize the Reforge SDK
+ Sdk sdk = new Sdk(new Options());
+
+ // Install the Log4j2 filter
+ ReforgeLog4j2Filter.install(sdk.loggerClient());
+
+ // Now all your logging will respect Reforge log levels
+ log.info("Application started with dynamic log levels");
+ }
+}
+```
+
+**Important:**
+- Install the filter as early as possible in your application startup, ideally right after initializing the Reforge SDK
+- Install after Log4j2 is initialized
+- Any dynamic reconfiguration of Log4j2 (e.g., via JMX or programmatic changes) will remove the filter and it will need to be reinstalled
+
+### Configuration
+
+By default, the integration looks for a config key named `log-levels.default` of type `LOG_LEVEL_V2`. The configuration is evaluated with the following context:
+
+- `reforge-sdk-logging.lang`: `"java"`
+- `reforge-sdk-logging.logger-path`: The name of the logger (e.g., `"com.example.MyClass"`)
+
+You can customize the config key:
+
+```java
+Options options = new Options().setLoggerKey("my.custom.log.config");
+Sdk sdk = new Sdk(options);
+ReforgeLog4j2Filter.install(sdk.loggerClient());
+```
+
+### Example Reforge Configuration
+
+In your Reforge dashboard, create a `LOG_LEVEL_V2` config with key `log-levels.default`:
+
+```yaml
+# Default to INFO for all loggers
+default: INFO
+
+# Set specific packages to DEBUG
+rules:
+ - criteria:
+ logger-path:
+ starts-with: "com.example.services"
+ value: DEBUG
+
+ # Only log errors in noisy third-party library
+ - criteria:
+ logger-path:
+ starts-with: "com.thirdparty.noisy"
+ value: ERROR
+```
+
+## How It Works
+
+Once installed, the filter intercepts **all** logging calls across your entire application:
+
+- Works with **all loggers** (no need to configure individual loggers)
+- Works with **all appenders** (console, file, rolling, syslog, async, etc.)
+- Filters happen **before** log messages are formatted (performance benefit)
+- No modification of your existing Log4j2 configuration needed
+
+## Performance Considerations
+
+- **Efficient filtering**: Log level checks happen before expensive message construction
+- **Recursion protection**: Built-in guard prevents performance issues from recursive logging
+- **Caching**: The Reforge SDK caches configuration data to minimize network calls
+
+## Thread Safety
+
+The integration is fully thread-safe and uses Log4j2's built-in filter infrastructure, which is designed for concurrent access.
+
+## FAQ
+
+### Do I need to configure individual loggers?
+
+**No!** The filter works at the LoggerContext level and automatically intercepts all logging calls across your entire application without needing to configure individual loggers.
+
+### Do I need to modify my existing log4j2.xml?
+
+**No!** The filter works alongside your existing Log4j2 configuration. You don't need to change your appenders, layouts, or logger settings.
+
+### Does this work with all Log4j2 appenders?
+
+**Yes!** The filter intercepts logging decisions before they reach any appenders. It works with:
+- Console appenders
+- File appenders
+- Rolling file appenders
+- Syslog appenders
+- Async appenders
+- JDBC appenders
+- Custom appenders
+
+### What's the performance impact?
+
+Minimal! The filter:
+- Runs before log message formatting (avoiding expensive string operations for filtered logs)
+- Uses a ThreadLocal recursion guard to prevent infinite loops
+- Only makes one SDK call per log statement
+- The SDK caches configuration data to minimize network overhead
+
+### Can I use this in Spring Boot applications?
+
+**Yes!** Just install the filter in your main application class or a `@PostConstruct` method:
+
+```java
+@SpringBootApplication
+public class MyApplication {
+ @Autowired
+ private Sdk reforgeSDK;
+
+ @PostConstruct
+ public void setupLogging() {
+ ReforgeLog4j2Filter.install(reforgeSDK.loggerClient());
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(MyApplication.class, args);
+ }
+}
+```
+
+### What happens if Log4j2 is reconfigured?
+
+If Log4j2 is dynamically reconfigured (via JMX, programmatic reconfiguration, or automatic file watching), the filter will be removed. You'll need to reinstall it after reconfiguration. Consider adding a reconfiguration listener if your application uses dynamic reconfiguration.
+
+## Troubleshooting
+
+### Filter not installing
+
+If you see errors during filter installation, ensure that:
+
+1. Log4j2 is actually on your classpath
+2. You're using Log4j2 (not Log4j 1.x or another logging implementation)
+3. The filter installation happens after Log4j2 initialization
+
+To verify Log4j2 is being used:
+```java
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LoggerContext;
+
+try {
+ LoggerContext context = (LoggerContext) LogManager.getContext(false);
+ System.out.println("Using Log4j2: " + context.getClass().getName());
+} catch (ClassCastException e) {
+ System.out.println("NOT using Log4j2 Core");
+}
+```
+
+### Log levels not changing
+
+If log levels aren't being respected:
+
+1. Verify the config key exists in Reforge (default: `log-levels.default`)
+2. Check that it's of type `LOG_LEVEL_V2`
+3. Ensure the Reforge SDK is initialized and ready
+4. Check the SDK logs for any errors during config retrieval
+5. Verify the filter is still installed (it may have been removed by reconfiguration)
+
+### Existing Log4j2 configuration overriding dynamic levels
+
+The filter runs as part of Log4j2's filter chain. However, if you have:
+- Appender-level filters with thresholds
+- Very restrictive context-wide filters
+- Logger-level settings that are more restrictive
+
+These may still apply. The filter controls whether a log event is created at all, but downstream filters and appender settings can still block events.
+
+### Filter removed after reconfiguration
+
+If your application uses Log4j2's configuration file watching or programmatic reconfiguration, the filter will be removed. Options:
+
+1. **Disable automatic reconfiguration** if not needed
+2. **Add a reconfiguration listener** that reinstalls the filter
+3. **Install via configuration** (though this requires static SDK access)
+
+## Example Application
+
+```java
+package com.example;
+
+import com.reforge.sdk.Sdk;
+import com.reforge.sdk.Options;
+import com.reforge.sdk.log4j2.ReforgeLog4j2Filter;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+public class MyApplication {
+ private static final Logger log = LogManager.getLogger(MyApplication.class);
+
+ public static void main(String[] args) {
+ // Initialize Reforge SDK
+ Sdk sdk = new Sdk(new Options());
+
+ // Install Log4j2 integration
+ ReforgeLog4j2Filter.install(sdk.loggerClient());
+
+ log.trace("This is a trace message"); // Filtered if level > TRACE
+ log.debug("This is a debug message"); // Filtered if level > DEBUG
+ log.info("This is an info message"); // Filtered if level > INFO
+ log.warn("This is a warning"); // Filtered if level > WARN
+ log.error("This is an error"); // Filtered if level > ERROR
+ log.fatal("This is fatal"); // Filtered if level > FATAL
+ }
+}
+```
+
+## Differences from Logback Integration
+
+The Log4j2 integration differs from Logback in a few ways:
+
+| Feature | Log4j2 | Logback |
+|---------|--------|---------|
+| Filter type | AbstractFilter | TurboFilter |
+| Filter scope | LoggerContext-wide | Framework-wide |
+| Reconfiguration | Filter removed on reconfig | Filter persists |
+| FATAL level | Native support | Maps to ERROR |
+
+Both integrations provide the same dynamic log level functionality with similar performance characteristics.
diff --git a/log4j2/pom.xml b/log4j2/pom.xml
new file mode 100644
index 0000000..43aca28
--- /dev/null
+++ b/log4j2/pom.xml
@@ -0,0 +1,63 @@
+
+
+ 4.0.0
+
+
+ com.reforge
+ sdk-parent
+ 1.1.0
+
+
+ sdk-log4j2
+ jar
+ Reforge SDK Log4j2 Integration
+ Log4j2 integration for Reforge SDK dynamic log level management
+
+
+
+
+ com.reforge
+ sdk
+ ${project.version}
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ provided
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
diff --git a/log4j2/src/main/java/com/reforge/sdk/log4j2/Log4jLevelMapper.java b/log4j2/src/main/java/com/reforge/sdk/log4j2/Log4jLevelMapper.java
new file mode 100644
index 0000000..1e47ed0
--- /dev/null
+++ b/log4j2/src/main/java/com/reforge/sdk/log4j2/Log4jLevelMapper.java
@@ -0,0 +1,26 @@
+package com.reforge.sdk.log4j2;
+
+import com.reforge.sdk.LogLevel;
+import org.apache.logging.log4j.Level;
+
+class Log4jLevelMapper {
+
+ static Level toLog4jLevel(LogLevel reforgeLevel) {
+ switch (reforgeLevel) {
+ case FATAL:
+ return Level.FATAL;
+ case ERROR:
+ return Level.ERROR;
+ case WARN:
+ return Level.WARN;
+ case INFO:
+ return Level.INFO;
+ case DEBUG:
+ return Level.DEBUG;
+ case TRACE:
+ return Level.TRACE;
+ default:
+ return Level.DEBUG;
+ }
+ }
+}
diff --git a/log4j2/src/main/java/com/reforge/sdk/log4j2/ReforgeLog4j2Filter.java b/log4j2/src/main/java/com/reforge/sdk/log4j2/ReforgeLog4j2Filter.java
new file mode 100644
index 0000000..8b4da21
--- /dev/null
+++ b/log4j2/src/main/java/com/reforge/sdk/log4j2/ReforgeLog4j2Filter.java
@@ -0,0 +1,284 @@
+package com.reforge.sdk.log4j2;
+
+import com.reforge.sdk.LogLevel;
+import com.reforge.sdk.LoggerClient;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.filter.AbstractFilter;
+import org.apache.logging.log4j.message.Message;
+
+/**
+ * Log4j2 filter that retrieves log levels from Reforge configuration.
+ *
+ *
This filter intercepts logging calls and dynamically determines whether they should
+ * be logged based on the log level configuration from Reforge, allowing centralized
+ * and real-time control over application logging levels.
+ *
+ *
To install this filter, call:
+ *
{@code
+ * Sdk sdk = new Sdk(new Options());
+ * ReforgeLog4j2Filter.install(sdk.loggerClient());
+ * }
+ *
+ * Important: Install the filter after Log4j2 is initialized. Any dynamic
+ * reconfiguration of Log4j2 will remove this filter and it will need to be reinstalled.
+ */
+public class ReforgeLog4j2Filter extends AbstractFilter {
+
+ private final ThreadLocal recursionCheck = ThreadLocal.withInitial(() -> false
+ );
+ private final LoggerClient loggerClient;
+
+ /**
+ * Installs ReforgeLog4j2Filter at the LoggerContext level.
+ * Call only after Log4j2 is initialized.
+ *
+ * Note: Any dynamic reconfiguration of Log4j2 will remove this filter.
+ *
+ * @param loggerClient the LoggerClient to use for retrieving log levels
+ * @throws IllegalStateException if Log4j2 Core is not being used
+ */
+ public static void install(LoggerClient loggerClient) {
+ org.apache.logging.log4j.spi.LoggerContext ctx = LogManager.getContext(false);
+
+ if (!(ctx instanceof LoggerContext)) {
+ throw new IllegalStateException(
+ "Cannot install ReforgeLog4j2Filter - LoggerContext is not Log4j2 Core. " +
+ "Found: " +
+ ctx.getClass().getName() +
+ ". " +
+ "Make sure log4j-core is on your classpath and properly configured."
+ );
+ }
+
+ LoggerContext loggerContext = (LoggerContext) ctx;
+ loggerContext.addFilter(new ReforgeLog4j2Filter(loggerClient));
+ loggerContext.updateLoggers();
+ }
+
+ public ReforgeLog4j2Filter(final LoggerClient loggerClient) {
+ this.loggerClient = loggerClient;
+ }
+
+ Result decide(final String loggerName, final Level level) {
+ if (recursionCheck.get()) {
+ return Result.NEUTRAL;
+ }
+
+ try {
+ recursionCheck.set(true);
+
+ LogLevel reforgeLogLevel = loggerClient.getLogLevel(loggerName);
+ Level calculatedMinLogLevelToAccept = Log4jLevelMapper.toLog4jLevel(
+ reforgeLogLevel
+ );
+
+ if (level.isMoreSpecificThan(calculatedMinLogLevelToAccept)) {
+ return Result.ACCEPT;
+ }
+ return Result.DENY;
+ } catch (Exception e) {
+ // If there's any error, fall back to neutral to avoid breaking logging
+ return Result.NEUTRAL;
+ } finally {
+ recursionCheck.set(false);
+ }
+ }
+
+ @Override
+ public Result filter(final LogEvent event) {
+ return decide(event.getLoggerName(), event.getLevel());
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final Message msg,
+ final Throwable t
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final Object msg,
+ final Throwable t
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object... params
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7,
+ final Object p8
+ ) {
+ return decide(logger.getName(), level);
+ }
+
+ @Override
+ public Result filter(
+ final Logger logger,
+ final Level level,
+ final Marker marker,
+ final String msg,
+ final Object p0,
+ final Object p1,
+ final Object p2,
+ final Object p3,
+ final Object p4,
+ final Object p5,
+ final Object p6,
+ final Object p7,
+ final Object p8,
+ final Object p9
+ ) {
+ return decide(logger.getName(), level);
+ }
+}
diff --git a/logback/README.md b/logback/README.md
new file mode 100644
index 0000000..5e64dc1
--- /dev/null
+++ b/logback/README.md
@@ -0,0 +1,275 @@
+# Reforge SDK Logback Integration
+
+This module provides seamless integration between the Reforge SDK and Logback, enabling dynamic log level management for your application.
+
+## Overview
+
+The Logback integration uses a Logback TurboFilter to intercept logging calls and dynamically determine whether they should be logged based on log level configuration from Reforge. This allows you to:
+
+- **Centrally manage log levels** - Control logging across your entire application from the Reforge dashboard
+- **Real-time updates** - Change log levels without restarting your application
+- **Context-aware logging** - Different log levels for different loggers based on runtime context
+- **Performance** - Efficient filtering happens before log message construction
+
+## Installation
+
+Add the dependency to your `pom.xml`:
+
+```xml
+
+ com.reforge
+ sdk-logback
+ 1.0.3
+
+```
+
+### Compatibility
+
+This module is compatible with:
+- **Logback 1.2.x** (with SLF4J 1.7.x)
+- **Logback 1.3.x** (with SLF4J 2.0.x)
+- **Logback 1.4.x** (with SLF4J 2.0.x)
+- **Logback 1.5.x** (with SLF4J 2.0.x)
+
+The module uses only stable Logback APIs that haven't changed across these versions.
+
+**Note:** Logback and SLF4J are marked as `provided` dependencies - your application's versions will be used automatically. Make sure your Logback and SLF4J versions are compatible with each other (Logback 1.3+ requires SLF4J 2.0+, Logback 1.2.x requires SLF4J 1.7.x).
+
+## Usage
+
+### Basic Setup (Programmatic)
+
+The simplest approach is to install the filter programmatically during application startup:
+
+```java
+import com.reforge.sdk.Sdk;
+import com.reforge.sdk.Options;
+import com.reforge.sdk.logback.ReforgeLogbackTurboFilter;
+
+public class MyApplication {
+ public static void main(String[] args) {
+ // Initialize the Reforge SDK
+ Sdk sdk = new Sdk(new Options());
+
+ // Install the Logback turbo filter
+ ReforgeLogbackTurboFilter.install(sdk.loggerClient());
+
+ // Now all your logging will respect Reforge log levels
+ Logger log = LoggerFactory.getLogger(MyApplication.class);
+ log.info("Application started with dynamic log levels");
+ }
+}
+```
+
+**Important:** Install the filter as early as possible in your application startup, ideally right after initializing the Reforge SDK.
+
+### Alternative: XML Configuration
+
+You can also configure the filter in your `logback.xml`, though this requires you to make the SDK instance available statically:
+
+```xml
+
+
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+```
+
+**Note:** The programmatic approach is recommended because it's simpler and doesn't require static SDK access.
+
+### How It Works
+
+Once installed, the TurboFilter intercepts **all** logging calls across your entire application:
+
+- Works with **all loggers** (no need to configure individual loggers)
+- Works with **all appenders** (console, file, syslog, etc.)
+- Filters happen **before** log messages are formatted (performance benefit)
+- No modification of your existing Logback configuration needed
+
+### Configuration
+
+By default, the integration looks for a config key named `log-levels.default` of type `LOG_LEVEL_V2`. The configuration is evaluated with the following context:
+
+- `reforge-sdk-logging.lang`: `"java"`
+- `reforge-sdk-logging.logger-path`: The name of the logger (e.g., `"com.example.MyClass"`)
+
+You can customize the config key:
+
+```java
+Options options = new Options().setLoggerKey("my.custom.log.config");
+Sdk sdk = new Sdk(options);
+ReforgeLogbackTurboFilter.install(sdk.loggerClient());
+```
+
+### Example Reforge Configuration
+
+In your Reforge dashboard, create a `LOG_LEVEL_V2` config with key `log-levels.default`:
+
+```yaml
+# Default to INFO for all loggers
+default: INFO
+
+# Set specific packages to DEBUG
+rules:
+ - criteria:
+ logger-path:
+ starts-with: "com.example.services"
+ value: DEBUG
+
+ # Only log errors in noisy third-party library
+ - criteria:
+ logger-path:
+ starts-with: "com.thirdparty.noisy"
+ value: ERROR
+```
+
+## How It Works
+
+1. When a log statement is called, Logback's TurboFilter mechanism intercepts it before the log message is constructed
+2. The filter calls `loggerClient.getLogLevel(loggerName)` to retrieve the configured log level from Reforge
+3. The retrieved level is compared against the log statement's level
+4. If the statement's level is high enough, it's allowed through; otherwise, it's filtered out
+5. A ThreadLocal recursion guard prevents infinite loops if logging occurs during config lookup
+
+## Performance Considerations
+
+- **Efficient filtering**: Log level checks happen before expensive message construction
+- **Recursion protection**: Built-in guard prevents performance issues from recursive logging
+- **Caching**: The Reforge SDK caches configuration data to minimize network calls
+
+## Thread Safety
+
+The integration is fully thread-safe and uses Logback's built-in TurboFilter infrastructure, which is designed for concurrent access.
+
+## FAQ
+
+### Do I need to configure individual loggers?
+
+**No!** Unlike some logging frameworks, Logback's TurboFilter mechanism works globally. Once installed, the filter automatically intercepts all logging calls across your entire application without needing to touch individual logger configurations.
+
+### Do I need to modify my existing logback.xml?
+
+**No!** The TurboFilter works alongside your existing Logback configuration. You don't need to change your appenders, encoders, or root logger settings.
+
+### How does this differ from Log4j integration?
+
+The Logback integration is simpler because:
+- **Logback**: Uses TurboFilters which work at the framework level
+- **Log4j**: Requires traversing and updating individual Logger objects
+
+With Logback, there's no need to maintain or update logger hierarchies.
+
+### Does this work with all Logback appenders?
+
+**Yes!** The TurboFilter intercepts logging decisions before they reach any appenders. It works with:
+- Console appenders
+- File appenders
+- Rolling file appenders
+- Syslog appenders
+- Async appenders
+- Custom appenders
+
+### What's the performance impact?
+
+Minimal! The TurboFilter:
+- Runs before log message formatting (avoiding expensive string operations for filtered logs)
+- Uses a ThreadLocal recursion guard to prevent infinite loops
+- Only makes one SDK call per log statement
+- The SDK caches configuration data to minimize network overhead
+
+### Can I use this in Spring Boot applications?
+
+**Yes!** Just install the filter in your main application class or a `@PostConstruct` method:
+
+```java
+@SpringBootApplication
+public class MyApplication {
+ @Autowired
+ private Sdk reforgeSDK;
+
+ @PostConstruct
+ public void setupLogging() {
+ ReforgeLogbackTurboFilter.install(reforgeSDK.loggerClient());
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(MyApplication.class, args);
+ }
+}
+```
+
+## Troubleshooting
+
+### Filter not installing
+
+If you see an error message like "Unable to install ReforgeLogbackTurboFilter", ensure that:
+
+1. Logback is actually on your classpath
+2. SLF4J is bound to Logback (not another logging implementation like Log4j or java.util.logging)
+3. The filter installation happens after Logback initialization
+
+To verify Logback is being used:
+```java
+import org.slf4j.LoggerFactory;
+import ch.qos.logback.classic.LoggerContext;
+
+ILoggerFactory factory = LoggerFactory.getILoggerFactory();
+if (factory instanceof LoggerContext) {
+ System.out.println("Using Logback!");
+} else {
+ System.out.println("NOT using Logback: " + factory.getClass().getName());
+}
+```
+
+### Log levels not changing
+
+If log levels aren't being respected:
+
+1. Verify the config key exists in Reforge (default: `log-levels.default`)
+2. Check that it's of type `LOG_LEVEL_V2`
+3. Ensure the Reforge SDK is initialized and ready
+4. Check the SDK logs for any errors during config retrieval
+
+### Existing Logback configuration overriding dynamic levels
+
+The TurboFilter runs before Logback's standard level checks. However, if you have very restrictive appender-level filters or threshold settings in your logback.xml, those may still apply. The TurboFilter controls whether a log event is created, but appenders can still apply their own filtering.
+
+## Example Application
+
+```java
+package com.example;
+
+import com.reforge.sdk.Sdk;
+import com.reforge.sdk.Options;
+import com.reforge.sdk.logback.ReforgeLogbackTurboFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MyApplication {
+ private static final Logger LOG = LoggerFactory.getLogger(MyApplication.class);
+
+ public static void main(String[] args) {
+ // Initialize Reforge SDK
+ Sdk sdk = new Sdk(new Options());
+
+ // Install Logback integration
+ ReforgeLogbackTurboFilter.install(sdk.loggerClient());
+
+ LOG.trace("This is a trace message"); // Filtered if level > TRACE
+ LOG.debug("This is a debug message"); // Filtered if level > DEBUG
+ LOG.info("This is an info message"); // Filtered if level > INFO
+ LOG.warn("This is a warning"); // Filtered if level > WARN
+ LOG.error("This is an error"); // Filtered if level > ERROR
+ }
+}
+```
diff --git a/logback/pom.xml b/logback/pom.xml
new file mode 100644
index 0000000..7705858
--- /dev/null
+++ b/logback/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+
+ com.reforge
+ sdk-parent
+ 1.1.0
+
+
+ sdk-logback
+ jar
+ Reforge SDK Logback Integration
+ Logback integration for Reforge SDK dynamic log level management
+
+
+
+
+ com.reforge
+ sdk
+ ${project.version}
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
diff --git a/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java b/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java
new file mode 100644
index 0000000..783ce5c
--- /dev/null
+++ b/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java
@@ -0,0 +1,54 @@
+package com.reforge.sdk.logback;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.turbo.TurboFilter;
+import ch.qos.logback.core.spi.FilterReply;
+import com.reforge.sdk.LogLevel;
+import com.reforge.sdk.LoggerClient;
+import org.slf4j.Marker;
+
+public abstract class BaseTurboFilter extends TurboFilter {
+
+ protected final LoggerClient loggerClient;
+ private final ThreadLocal recursionCheck = ThreadLocal.withInitial(() -> false
+ );
+
+ BaseTurboFilter(LoggerClient loggerClient) {
+ this.loggerClient = loggerClient;
+ }
+
+ abstract LogLevel getLogLevel(Logger logger, Level level);
+
+ @Override
+ public FilterReply decide(
+ Marker marker,
+ Logger logger,
+ Level level,
+ String s,
+ Object[] objects,
+ Throwable throwable
+ ) {
+ if (recursionCheck.get()) {
+ return FilterReply.NEUTRAL;
+ }
+
+ try {
+ recursionCheck.set(true);
+ LogLevel reforgeLogLevel = getLogLevel(logger, level);
+ Level calculatedMinLogLevelToAccept = LogbackLevelMapper.toLogbackLevel(
+ reforgeLogLevel
+ );
+
+ if (level.isGreaterOrEqual(calculatedMinLogLevelToAccept)) {
+ return FilterReply.ACCEPT;
+ }
+ return FilterReply.DENY;
+ } catch (Exception e) {
+ // If there's any error, fall back to neutral to avoid breaking logging
+ return FilterReply.NEUTRAL;
+ } finally {
+ recursionCheck.set(false);
+ }
+ }
+}
diff --git a/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java b/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java
new file mode 100644
index 0000000..0b4575c
--- /dev/null
+++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java
@@ -0,0 +1,26 @@
+package com.reforge.sdk.logback;
+
+import ch.qos.logback.classic.Level;
+import com.reforge.sdk.LogLevel;
+
+class LogbackLevelMapper {
+
+ static Level toLogbackLevel(LogLevel reforgeLevel) {
+ switch (reforgeLevel) {
+ case FATAL:
+ return Level.ERROR; // Logback doesn't have FATAL, map to ERROR
+ case ERROR:
+ return Level.ERROR;
+ case WARN:
+ return Level.WARN;
+ case INFO:
+ return Level.INFO;
+ case DEBUG:
+ return Level.DEBUG;
+ case TRACE:
+ return Level.TRACE;
+ default:
+ return Level.DEBUG;
+ }
+ }
+}
diff --git a/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java b/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java
new file mode 100644
index 0000000..c394994
--- /dev/null
+++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java
@@ -0,0 +1,28 @@
+package com.reforge.sdk.logback;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.turbo.TurboFilter;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.LoggerFactory;
+
+public class LogbackUtils {
+
+ static void installTurboFilter(TurboFilter turboFilter) {
+ ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
+
+ if (!(iLoggerFactory instanceof LoggerContext)) {
+ throw new IllegalStateException(
+ "Cannot install " +
+ turboFilter.getClass().getSimpleName() +
+ " - LoggerFactory is not a Logback LoggerContext. " +
+ "Found: " +
+ iLoggerFactory.getClass().getName() +
+ ". " +
+ "Make sure Logback is on your classpath and SLF4J is bound to Logback."
+ );
+ }
+
+ LoggerContext loggerContext = (LoggerContext) iLoggerFactory;
+ loggerContext.addTurboFilter(turboFilter);
+ }
+}
diff --git a/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java b/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java
new file mode 100644
index 0000000..1552f50
--- /dev/null
+++ b/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java
@@ -0,0 +1,41 @@
+package com.reforge.sdk.logback;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import com.reforge.sdk.LogLevel;
+import com.reforge.sdk.LoggerClient;
+
+/**
+ * Logback TurboFilter that retrieves log levels from Reforge configuration.
+ *
+ * This filter intercepts logging calls and dynamically determines whether they should
+ * be logged based on the log level configuration from Reforge, allowing centralized
+ * and real-time control over application logging levels.
+ *
+ * To install this filter, call:
+ *
{@code
+ * Sdk sdk = new Sdk(new Options());
+ * ReforgeLogbackTurboFilter.install(sdk.loggerClient());
+ * }
+ */
+public class ReforgeLogbackTurboFilter extends BaseTurboFilter {
+
+ ReforgeLogbackTurboFilter(LoggerClient loggerClient) {
+ super(loggerClient);
+ }
+
+ /**
+ * Installs the Reforge turbo filter into the Logback logging system.
+ *
+ * @param loggerClient the LoggerClient to use for retrieving log levels
+ * @throws IllegalStateException if Logback is not being used as the SLF4J implementation
+ */
+ public static void install(LoggerClient loggerClient) {
+ LogbackUtils.installTurboFilter(new ReforgeLogbackTurboFilter(loggerClient));
+ }
+
+ @Override
+ LogLevel getLogLevel(Logger logger, Level level) {
+ return loggerClient.getLogLevel(logger.getName());
+ }
+}
diff --git a/micronaut/pom.xml b/micronaut/pom.xml
index 1855df7..e7732bd 100644
--- a/micronaut/pom.xml
+++ b/micronaut/pom.xml
@@ -4,7 +4,7 @@
com.reforge
sdk-parent
- 1.0.3
+ 1.1.0
sdk-micronaut-extension
diff --git a/pom.xml b/pom.xml
index 9f1b014..c329ad6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,12 +11,14 @@
com.reforge
sdk-parent
- 1.0.3
+ 1.1.0
pom
Reforge SDK Parent POM
Parent POM for Reforge SDK modules providing feature flags, configuration management, and A/B testing capabilities
+ log4j2
+ logback
micronaut
sdk
@@ -39,6 +41,16 @@
+
+ ch.qos.logback
+ logback-classic
+ 1.4.12
+
+
+ ch.qos.logback
+ logback-core
+ 1.4.12
+
com.fasterxml.jackson.core
jackson-annotations
@@ -89,6 +101,16 @@
javax.annotation-api
1.3.2
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.19.0
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.19.0
+
org.assertj
assertj-core
diff --git a/sdk/pom.xml b/sdk/pom.xml
index d3889c4..9d73c2f 100644
--- a/sdk/pom.xml
+++ b/sdk/pom.xml
@@ -4,7 +4,7 @@
com.reforge
sdk-parent
- 1.0.3
+ 1.1.0
sdk
@@ -60,6 +60,12 @@
[1.4.12,)
test
+
+ ch.qos.logback
+ logback-core
+ [1.4.12,)
+ test
+
com.fasterxml.jackson.core
jackson-annotations
diff --git a/sdk/src/main/java/com/reforge/sdk/LogLevel.java b/sdk/src/main/java/com/reforge/sdk/LogLevel.java
new file mode 100644
index 0000000..4335ae7
--- /dev/null
+++ b/sdk/src/main/java/com/reforge/sdk/LogLevel.java
@@ -0,0 +1,55 @@
+package com.reforge.sdk;
+
+import cloud.prefab.domain.Prefab;
+
+/**
+ * Log level enum that wraps the underlying Prefab.LogLevel protobuf enum.
+ * Used to configure logging levels for different loggers in your application.
+ */
+public enum LogLevel {
+ TRACE(Prefab.LogLevel.TRACE),
+ DEBUG(Prefab.LogLevel.DEBUG),
+ INFO(Prefab.LogLevel.INFO),
+ WARN(Prefab.LogLevel.WARN),
+ ERROR(Prefab.LogLevel.ERROR),
+ FATAL(Prefab.LogLevel.FATAL);
+
+ private final Prefab.LogLevel protobufLogLevel;
+
+ LogLevel(Prefab.LogLevel protobufLogLevel) {
+ this.protobufLogLevel = protobufLogLevel;
+ }
+
+ /**
+ * @return the underlying protobuf LogLevel
+ */
+ public Prefab.LogLevel toProtobuf() {
+ return protobufLogLevel;
+ }
+
+ /**
+ * Converts a protobuf LogLevel to a Java LogLevel enum
+ * @param protobufLogLevel the protobuf log level
+ * @return the corresponding Java LogLevel, or DEBUG if the protobuf value is NOT_SET_LOG_LEVEL or unrecognized
+ */
+ public static LogLevel fromProtobuf(Prefab.LogLevel protobufLogLevel) {
+ switch (protobufLogLevel) {
+ case TRACE:
+ return TRACE;
+ case DEBUG:
+ return DEBUG;
+ case INFO:
+ return INFO;
+ case WARN:
+ return WARN;
+ case ERROR:
+ return ERROR;
+ case FATAL:
+ return FATAL;
+ case NOT_SET_LOG_LEVEL:
+ case UNRECOGNIZED:
+ default:
+ return DEBUG;
+ }
+ }
+}
diff --git a/sdk/src/main/java/com/reforge/sdk/LoggerClient.java b/sdk/src/main/java/com/reforge/sdk/LoggerClient.java
new file mode 100644
index 0000000..1b87909
--- /dev/null
+++ b/sdk/src/main/java/com/reforge/sdk/LoggerClient.java
@@ -0,0 +1,21 @@
+package com.reforge.sdk;
+
+/**
+ * Client for retrieving dynamically configured log levels from Reforge.
+ * Log levels can be configured per logger path with context-aware targeting.
+ */
+public interface LoggerClient {
+ /**
+ * Get the configured log level for a specific logger.
+ *
+ * This evaluates the configured logger key (from Options) with a context containing:
+ * - "reforge-sdk-logging.lang": "java"
+ * - "reforge-sdk-logging.logger-path": the provided loggerName
+ *
+ * The config must be of type LOG_LEVEL_V2.
+ *
+ * @param loggerName the name or path of the logger (e.g., "com.example.MyClass" or "com.example")
+ * @return the configured LogLevel for this logger, or DEBUG if no configuration is found
+ */
+ LogLevel getLogLevel(String loggerName);
+}
diff --git a/sdk/src/main/java/com/reforge/sdk/Options.java b/sdk/src/main/java/com/reforge/sdk/Options.java
index 0d32e07..6fcf917 100644
--- a/sdk/src/main/java/com/reforge/sdk/Options.java
+++ b/sdk/src/main/java/com/reforge/sdk/Options.java
@@ -71,6 +71,9 @@ public enum CollectContextMode {
@Nullable
private ContextSetReadable globalContext;
+ @Nullable
+ private String loggerKey = "log-levels.default";
+
public Options() {
setSdkKey(
Optional
@@ -287,6 +290,22 @@ public Options setGlobalContext(@Nullable ContextSetReadable globalContext) {
return this;
}
+ public Optional getLoggerKey() {
+ return Optional.ofNullable(loggerKey);
+ }
+
+ /**
+ * Sets the config key to use for log level configuration.
+ * This key should point to a LOG_LEVEL_V2 config that will be evaluated
+ * with context containing logger path information.
+ * @param loggerKey the config key for log level configuration
+ * @return Options
+ */
+ public Options setLoggerKey(@Nullable String loggerKey) {
+ this.loggerKey = loggerKey;
+ return this;
+ }
+
private String prefixAndValidate(String uri) {
String prefixed = httpsPrefix(uri);
try {
diff --git a/sdk/src/main/java/com/reforge/sdk/Sdk.java b/sdk/src/main/java/com/reforge/sdk/Sdk.java
index 2327a00..70c450d 100644
--- a/sdk/src/main/java/com/reforge/sdk/Sdk.java
+++ b/sdk/src/main/java/com/reforge/sdk/Sdk.java
@@ -2,6 +2,7 @@
import com.reforge.sdk.internal.ConfigClientImpl;
import com.reforge.sdk.internal.FeatureFlagClientImpl;
+import com.reforge.sdk.internal.LoggerClientImpl;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -13,6 +14,7 @@ public class Sdk implements AutoCloseable {
private final Options options;
private ConfigClientImpl configClient;
private FeatureFlagClient featureFlagClient;
+ private LoggerClient loggerClient;
private final AtomicBoolean closed;
public Sdk(Options options) {
@@ -58,6 +60,18 @@ public FeatureFlagClient featureFlagClient() {
return featureFlagClient;
}
+ public LoggerClient loggerClient() {
+ if (loggerClient == null) {
+ synchronized (this) {
+ if (loggerClient == null) {
+ loggerClient =
+ new LoggerClientImpl(configClientImpl(), options.getLoggerKey().orElse(null));
+ }
+ }
+ }
+ return loggerClient;
+ }
+
public Options getOptions() {
return options;
}
diff --git a/sdk/src/main/java/com/reforge/sdk/internal/LoggerClientImpl.java b/sdk/src/main/java/com/reforge/sdk/internal/LoggerClientImpl.java
new file mode 100644
index 0000000..c22c365
--- /dev/null
+++ b/sdk/src/main/java/com/reforge/sdk/internal/LoggerClientImpl.java
@@ -0,0 +1,89 @@
+package com.reforge.sdk.internal;
+
+import cloud.prefab.domain.Prefab;
+import com.reforge.sdk.ConfigClient;
+import com.reforge.sdk.LogLevel;
+import com.reforge.sdk.LoggerClient;
+import com.reforge.sdk.context.Context;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggerClientImpl implements LoggerClient {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LoggerClientImpl.class);
+ private static final String LOGGING_CONTEXT_NAME = "reforge-sdk-logging";
+ private static final String LANG_KEY = "lang";
+ private static final String LOGGER_PATH_KEY = "logger-path";
+ private static final String JAVA_VALUE = "java";
+
+ private final ConfigClient configClient;
+ private final String loggerKey;
+
+ public LoggerClientImpl(ConfigClient configClient, @Nullable String loggerKey) {
+ this.configClient = configClient;
+ this.loggerKey = loggerKey;
+ }
+
+ @Override
+ public LogLevel getLogLevel(String loggerName) {
+ if (loggerKey == null || loggerKey.isEmpty()) {
+ LOG.debug("No logger key configured, returning default level DEBUG");
+ return LogLevel.DEBUG;
+ }
+
+ try {
+ Context loggingContext = Context
+ .newBuilder(LOGGING_CONTEXT_NAME)
+ .put(LANG_KEY, JAVA_VALUE)
+ .put(LOGGER_PATH_KEY, loggerName)
+ .build();
+
+ Optional configValueMaybe = configClient.get(
+ loggerKey,
+ loggingContext
+ );
+
+ if (!configValueMaybe.isPresent()) {
+ LOG.debug(
+ "No log level configuration found for key '{}' and logger '{}', returning default level DEBUG",
+ loggerKey,
+ loggerName
+ );
+ return LogLevel.DEBUG;
+ }
+
+ Prefab.ConfigValue configValue = configValueMaybe.get();
+
+ if (!configValue.hasLogLevel()) {
+ LOG.warn(
+ "Config value for key '{}' is not a log level (type: {}), returning default level DEBUG",
+ loggerKey,
+ configValue.getTypeCase()
+ );
+ return LogLevel.DEBUG;
+ }
+
+ Prefab.LogLevel protobufLogLevel = configValue.getLogLevel();
+ LogLevel result = LogLevel.fromProtobuf(protobufLogLevel);
+
+ LOG.debug(
+ "Retrieved log level {} for logger '{}' from key '{}'",
+ result,
+ loggerName,
+ loggerKey
+ );
+
+ return result;
+ } catch (Exception e) {
+ LOG.debug(
+ "Error retrieving log level for logger '{}' from key '{}', returning default level DEBUG",
+ loggerName,
+ loggerKey,
+ e
+ );
+ return LogLevel.DEBUG;
+ }
+ }
+}
diff --git a/sdk/src/test/java/com/reforge/sdk/internal/LoggerClientImplTest.java b/sdk/src/test/java/com/reforge/sdk/internal/LoggerClientImplTest.java
new file mode 100644
index 0000000..8fcddb4
--- /dev/null
+++ b/sdk/src/test/java/com/reforge/sdk/internal/LoggerClientImplTest.java
@@ -0,0 +1,246 @@
+package com.reforge.sdk.internal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import cloud.prefab.domain.Prefab;
+import com.reforge.sdk.ConfigClient;
+import com.reforge.sdk.LogLevel;
+import com.reforge.sdk.config.ConfigValueUtils;
+import com.reforge.sdk.context.Context;
+import com.reforge.sdk.context.ContextSetReadable;
+import java.util.Optional;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class LoggerClientImplTest {
+
+ @Mock
+ ConfigClient configClient;
+
+ @Nested
+ class WithLoggerKey {
+
+ @Test
+ void getLogLevelReturnsConfiguredLevel() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.INFO)));
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.INFO);
+ }
+
+ @Test
+ void getLogLevelReturnsDebugWhenNoConfigFound() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.empty());
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+
+ @Test
+ void getLogLevelReturnsDebugWhenConfigIsWrongType() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from("not a log level")));
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+
+ @Test
+ void getLogLevelPassesCorrectContext() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(
+ ContextSetReadable.class
+ );
+ when(configClient.get(eq("log-levels.default"), contextCaptor.capture()))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.WARN)));
+
+ loggerClient.getLogLevel("com.example.MyClass");
+
+ ContextSetReadable capturedContext = contextCaptor.getValue();
+ Optional loggingContext = capturedContext.getByName("reforge-sdk-logging");
+
+ assertThat(loggingContext).isPresent();
+ assertThat(loggingContext.get().getProperties())
+ .containsEntry("lang", ConfigValueUtils.from("java"))
+ .containsEntry("logger-path", ConfigValueUtils.from("com.example.MyClass"));
+ }
+
+ @Test
+ void getLogLevelHandlesAllLogLevels() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ // Test TRACE
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.TRACE)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.TRACE);
+
+ // Test DEBUG
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.DEBUG)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.DEBUG);
+
+ // Test INFO
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.INFO)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.INFO);
+
+ // Test WARN
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.WARN)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.WARN);
+
+ // Test ERROR
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.ERROR)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.ERROR);
+
+ // Test FATAL
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.FATAL)));
+ assertThat(loggerClient.getLogLevel("test")).isEqualTo(LogLevel.FATAL);
+ }
+
+ @Test
+ void getLogLevelHandlesNotSetLogLevel() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenReturn(
+ Optional.of(ConfigValueUtils.from(Prefab.LogLevel.NOT_SET_LOG_LEVEL))
+ );
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+
+ @Test
+ void getLogLevelHandlesCustomLoggerKey() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "custom.logger.config"
+ );
+
+ when(configClient.get(eq("custom.logger.config"), any(ContextSetReadable.class)))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.ERROR)));
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.ERROR);
+ verify(configClient).get(eq("custom.logger.config"), any(ContextSetReadable.class));
+ }
+ }
+
+ @Nested
+ class WithoutLoggerKey {
+
+ @Test
+ void getLogLevelReturnsDebugWhenLoggerKeyIsNull() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(configClient, null);
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+
+ @Test
+ void getLogLevelReturnsDebugWhenLoggerKeyIsEmpty() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(configClient, "");
+
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+ }
+
+ @Nested
+ class EdgeCases {
+
+ @Test
+ void getLogLevelHandlesExceptionFromConfigClient() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ when(configClient.get(eq("log-levels.default"), any(ContextSetReadable.class)))
+ .thenThrow(new RuntimeException("Config error"));
+
+ // Should not throw, should return default
+ LogLevel result = loggerClient.getLogLevel("com.example.MyClass");
+
+ assertThat(result).isEqualTo(LogLevel.DEBUG);
+ }
+
+ @Test
+ void getLogLevelHandlesDifferentLoggerPaths() {
+ LoggerClientImpl loggerClient = new LoggerClientImpl(
+ configClient,
+ "log-levels.default"
+ );
+
+ ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(
+ ContextSetReadable.class
+ );
+ when(configClient.get(eq("log-levels.default"), contextCaptor.capture()))
+ .thenReturn(Optional.of(ConfigValueUtils.from(Prefab.LogLevel.INFO)));
+
+ // Test package-level logger
+ loggerClient.getLogLevel("com.example");
+ ContextSetReadable context1 = contextCaptor.getValue();
+ assertThat(context1.getByName("reforge-sdk-logging").get().getProperties())
+ .containsEntry("logger-path", ConfigValueUtils.from("com.example"));
+
+ // Test class-level logger
+ loggerClient.getLogLevel("com.example.MyClass");
+ ContextSetReadable context2 = contextCaptor.getValue();
+ assertThat(context2.getByName("reforge-sdk-logging").get().getProperties())
+ .containsEntry("logger-path", ConfigValueUtils.from("com.example.MyClass"));
+
+ // Test root logger
+ loggerClient.getLogLevel("ROOT");
+ ContextSetReadable context3 = contextCaptor.getValue();
+ assertThat(context3.getByName("reforge-sdk-logging").get().getProperties())
+ .containsEntry("logger-path", ConfigValueUtils.from("ROOT"));
+ }
+ }
+}