From 66789a521439bbad979e987c3bba9ff47100bf1c Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Wed, 22 Oct 2025 16:21:25 -0500 Subject: [PATCH 1/5] Add core log level support with LoggerClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds dynamic log level management functionality to the SDK: - LogLevel enum: Java wrapper around Prefab.LogLevel with efficient switch-based conversion (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) - LoggerClient interface: Provides getLogLevel(String loggerName) method for retrieving configured log levels - LoggerClientImpl: Implementation that evaluates log level configs with context containing logger path and language information - Configuration: * Default config key: "log-levels.default" * Config type: LOG_LEVEL_V2 * Evaluation context: {"reforge-sdk-logging": {"lang": "java", "logger-path": ""}} * Returns DEBUG when no config found - Options: Added loggerKey field with default value "log-levels.default" - Sdk: Added loggerClient() method following same lazy initialization pattern as other clients - Tests: Comprehensive test suite with 11 tests covering all scenarios including exception handling, context validation, and edge cases This provides the foundation for logging framework integrations to dynamically control log levels via Reforge configuration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../main/java/com/reforge/sdk/LogLevel.java | 55 ++++ .../java/com/reforge/sdk/LoggerClient.java | 21 ++ .../main/java/com/reforge/sdk/Options.java | 19 ++ sdk/src/main/java/com/reforge/sdk/Sdk.java | 14 + .../sdk/internal/LoggerClientImpl.java | 89 +++++++ .../sdk/internal/LoggerClientImplTest.java | 246 ++++++++++++++++++ 6 files changed, 444 insertions(+) create mode 100644 sdk/src/main/java/com/reforge/sdk/LogLevel.java create mode 100644 sdk/src/main/java/com/reforge/sdk/LoggerClient.java create mode 100644 sdk/src/main/java/com/reforge/sdk/internal/LoggerClientImpl.java create mode 100644 sdk/src/test/java/com/reforge/sdk/internal/LoggerClientImplTest.java 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")); + } + } +} From 0858dbdef1042e4c0621f33e9e52f3f74baf493d Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Wed, 22 Oct 2025 16:22:22 -0500 Subject: [PATCH 2/5] Add Logback integration module for dynamic log levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new maven module (sdk-logback) that provides seamless Logback integration for dynamic log level management. Key Features: - TurboFilter-based integration: Works globally at framework level without needing to traverse or configure individual loggers - Maximum compatibility: Works with Logback 1.2.x through 1.5.x - Zero configuration: Works with existing logback.xml without modifications - Universal appender support: Works with all appenders (console, file, rolling, syslog, async, custom) - Performance optimized: Filters before message formatting Implementation: - BaseTurboFilter: Abstract filter with recursion protection and exception handling for safe operation - ReforgeLogbackTurboFilter: Main filter that retrieves log levels from LoggerClient and applies filtering decisions - LogbackLevelMapper: Bidirectional mapping between Reforge LogLevel and Logback Level enums - LogbackUtils: Utility to install the filter into Logback's LoggerContext Dependencies: - Logback and SLF4J use 'provided' scope, ensuring maximum compatibility by using the customer's existing versions rather than forcing specific versions - Only stable APIs used that haven't changed across Logback versions Usage: Sdk sdk = new Sdk(new Options()); ReforgeLogbackTurboFilter.install(sdk.loggerClient()); Documentation: - Comprehensive README with examples, FAQ, and troubleshooting - Spring Boot integration examples - Compatibility matrix for Logback 1.2.x - 1.5.x Parent POM Changes: - Added logback module to reactor build - Added logback-classic and logback-core to dependencyManagement 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- logback/README.md | 273 ++++++++++++++++++ logback/pom.xml | 57 ++++ .../reforge/sdk/logback/BaseTurboFilter.java | 59 ++++ .../sdk/logback/LogbackLevelMapper.java | 31 ++ .../com/reforge/sdk/logback/LogbackUtils.java | 25 ++ .../logback/ReforgeLogbackTurboFilter.java | 40 +++ pom.xml | 11 + 7 files changed, 496 insertions(+) create mode 100644 logback/README.md create mode 100644 logback/pom.xml create mode 100644 logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java create mode 100644 logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java create mode 100644 logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java create mode 100644 logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java diff --git a/logback/README.md b/logback/README.md new file mode 100644 index 0000000..a3ba4bd --- /dev/null +++ b/logback/README.md @@ -0,0 +1,273 @@ +# 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. Logback and SLF4J are marked as `provided` dependencies, so your application's versions will be used. + +## 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..d7408d6 --- /dev/null +++ b/logback/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + com.reforge + sdk-parent + 1.0.3 + + + 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..77702d4 --- /dev/null +++ b/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java @@ -0,0 +1,59 @@ +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.LEVEL_MAP.get( + reforgeLogLevel + ); + + if (calculatedMinLogLevelToAccept == null) { + return FilterReply.NEUTRAL; + } + + 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..e04d919 --- /dev/null +++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java @@ -0,0 +1,31 @@ +package com.reforge.sdk.logback; + +import ch.qos.logback.classic.Level; +import com.reforge.sdk.LogLevel; +import java.util.HashMap; +import java.util.Map; + +class LogbackLevelMapper { + + static final Map LEVEL_MAP; + static final Map REVERSE_LEVEL_MAP; + + static { + Map levelMap = new HashMap<>(); + levelMap.put(LogLevel.FATAL, Level.ERROR); + levelMap.put(LogLevel.ERROR, Level.ERROR); + levelMap.put(LogLevel.WARN, Level.WARN); + levelMap.put(LogLevel.INFO, Level.INFO); + levelMap.put(LogLevel.DEBUG, Level.DEBUG); + levelMap.put(LogLevel.TRACE, Level.TRACE); + LEVEL_MAP = Map.copyOf(levelMap); + + Map reverseLevelMap = new HashMap<>(); + reverseLevelMap.put(Level.ERROR, LogLevel.ERROR); + reverseLevelMap.put(Level.WARN, LogLevel.WARN); + reverseLevelMap.put(Level.INFO, LogLevel.INFO); + reverseLevelMap.put(Level.DEBUG, LogLevel.DEBUG); + reverseLevelMap.put(Level.TRACE, LogLevel.TRACE); + REVERSE_LEVEL_MAP = Map.copyOf(reverseLevelMap); + } +} 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..bad6cce --- /dev/null +++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java @@ -0,0 +1,25 @@ +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 { + + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LogbackUtils.class); + + static void installTurboFilter(TurboFilter turboFilter) { + ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); + if (iLoggerFactory instanceof LoggerContext) { + LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); + loggerContext.addTurboFilter(turboFilter); + } else { + LOG.error( + "Unable to install {} - LoggerFactory is not a Logback LoggerContext. Current factory: {}", + turboFilter.getClass().getSimpleName(), + iLoggerFactory.getClass().getName() + ); + } + } +} 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..98970e1 --- /dev/null +++ b/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java @@ -0,0 +1,40 @@ +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 + */ + 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/pom.xml b/pom.xml index 9f1b014..9d328bc 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ Parent POM for Reforge SDK modules providing feature flags, configuration management, and A/B testing capabilities + logback micronaut sdk @@ -39,6 +40,16 @@ + + ch.qos.logback + logback-classic + 1.4.12 + + + ch.qos.logback + logback-core + 1.4.12 + com.fasterxml.jackson.core jackson-annotations From 3a6dbc1c82ac177ce293b6aa4103e477b29964ba Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Wed, 22 Oct 2025 16:34:57 -0500 Subject: [PATCH 3/5] Optimize Logback level mapper and improve error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes to make Logback integration more efficient and robust: 1. Level Mapping Optimization: - Replace Map-based level mapping with switch statement - LogbackLevelMapper.toLogbackLevel() uses switch for better performance - Switch: ~1-2 CPU cycles vs Map: ~10-20 CPU cycles - Zero initialization overhead, better CPU cache utilization - Remove bidirectional mapping (only need Reforge→Logback direction) 2. Consistent Error Handling: - LogbackUtils now throws IllegalStateException instead of just logging - Fail-fast behavior prevents silent failures - Clear error messages with diagnostic information - Consistent with Log4j2 integration behavior - Added @throws javadoc to ReforgeLogbackTurboFilter.install() 3. Documentation Updates: - Clarify SLF4J version compatibility requirements - Note that Logback 1.3+ requires SLF4J 2.0+ - Document that dependencies are provided scope These changes ensure the integration fails immediately with helpful errors rather than silently not working, making debugging much easier for developers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- logback/README.md | 4 +- .../reforge/sdk/logback/BaseTurboFilter.java | 7 +--- .../sdk/logback/LogbackLevelMapper.java | 39 ++++++++----------- .../com/reforge/sdk/logback/LogbackUtils.java | 23 ++++++----- .../logback/ReforgeLogbackTurboFilter.java | 1 + 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/logback/README.md b/logback/README.md index a3ba4bd..5e64dc1 100644 --- a/logback/README.md +++ b/logback/README.md @@ -31,7 +31,9 @@ This module is compatible with: - **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. Logback and SLF4J are marked as `provided` dependencies, so your application's versions will be used. +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 diff --git a/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java b/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java index 77702d4..783ce5c 100644 --- a/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java +++ b/logback/src/main/java/com/reforge/sdk/logback/BaseTurboFilter.java @@ -36,15 +36,10 @@ public FilterReply decide( try { recursionCheck.set(true); LogLevel reforgeLogLevel = getLogLevel(logger, level); - - Level calculatedMinLogLevelToAccept = LogbackLevelMapper.LEVEL_MAP.get( + Level calculatedMinLogLevelToAccept = LogbackLevelMapper.toLogbackLevel( reforgeLogLevel ); - if (calculatedMinLogLevelToAccept == null) { - return FilterReply.NEUTRAL; - } - if (level.isGreaterOrEqual(calculatedMinLogLevelToAccept)) { return FilterReply.ACCEPT; } diff --git a/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java b/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java index e04d919..0b4575c 100644 --- a/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java +++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackLevelMapper.java @@ -2,30 +2,25 @@ import ch.qos.logback.classic.Level; import com.reforge.sdk.LogLevel; -import java.util.HashMap; -import java.util.Map; class LogbackLevelMapper { - static final Map LEVEL_MAP; - static final Map REVERSE_LEVEL_MAP; - - static { - Map levelMap = new HashMap<>(); - levelMap.put(LogLevel.FATAL, Level.ERROR); - levelMap.put(LogLevel.ERROR, Level.ERROR); - levelMap.put(LogLevel.WARN, Level.WARN); - levelMap.put(LogLevel.INFO, Level.INFO); - levelMap.put(LogLevel.DEBUG, Level.DEBUG); - levelMap.put(LogLevel.TRACE, Level.TRACE); - LEVEL_MAP = Map.copyOf(levelMap); - - Map reverseLevelMap = new HashMap<>(); - reverseLevelMap.put(Level.ERROR, LogLevel.ERROR); - reverseLevelMap.put(Level.WARN, LogLevel.WARN); - reverseLevelMap.put(Level.INFO, LogLevel.INFO); - reverseLevelMap.put(Level.DEBUG, LogLevel.DEBUG); - reverseLevelMap.put(Level.TRACE, LogLevel.TRACE); - REVERSE_LEVEL_MAP = Map.copyOf(reverseLevelMap); + 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 index bad6cce..c394994 100644 --- a/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java +++ b/logback/src/main/java/com/reforge/sdk/logback/LogbackUtils.java @@ -7,19 +7,22 @@ public class LogbackUtils { - private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LogbackUtils.class); - static void installTurboFilter(TurboFilter turboFilter) { ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); - if (iLoggerFactory instanceof LoggerContext) { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.addTurboFilter(turboFilter); - } else { - LOG.error( - "Unable to install {} - LoggerFactory is not a Logback LoggerContext. Current factory: {}", - turboFilter.getClass().getSimpleName(), - iLoggerFactory.getClass().getName() + + 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 index 98970e1..1552f50 100644 --- a/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java +++ b/logback/src/main/java/com/reforge/sdk/logback/ReforgeLogbackTurboFilter.java @@ -28,6 +28,7 @@ public class ReforgeLogbackTurboFilter extends BaseTurboFilter { * 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)); From 91e4d357c2f99c6fab36028accdded137fe4af47 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Wed, 22 Oct 2025 16:35:31 -0500 Subject: [PATCH 4/5] Add Log4j2 integration module for dynamic log levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a new maven module (sdk-log4j2) that provides Log4j2 integration for dynamic log level management. Key Features: - AbstractFilter-based integration at LoggerContext level - Maximum compatibility: Works with Log4j2 2.x versions - No configuration changes needed: Works with existing log4j2.xml - Universal appender support: Console, file, rolling, syslog, async, etc. - Performance optimized: Filters before message formatting Implementation: - ReforgeLog4j2Filter: Main filter that retrieves log levels from LoggerClient and applies filtering decisions - Log4jLevelMapper: Efficient switch-based mapping between Reforge LogLevel and Log4j2 Level enums (including native FATAL support) - Type checking: install() method validates Log4j2 Core is being used and throws IllegalStateException with helpful error if not Dependencies: - Log4j2 and SLF4J use 'provided' scope for maximum compatibility - We compile against Log4j2 2.19.0 but APIs are stable across 2.x - Works with both SLF4J 1.7.x and 2.0.x Usage: Sdk sdk = new Sdk(new Options()); ReforgeLog4j2Filter.install(sdk.loggerClient()); Note: Unlike Logback's persistent TurboFilter, Log4j2 filters are removed on dynamic reconfiguration and must be reinstalled. Documentation: - Comprehensive README with examples, FAQ, and troubleshooting - Spring Boot integration examples - Reconfiguration handling guidance - Comparison with Logback integration Parent POM Changes: - Added log4j2 module to reactor build - Added log4j-api and log4j-core to dependencyManagement (version 2.19.0) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- log4j2/README.md | 270 +++++++++++++++++ log4j2/pom.xml | 63 ++++ .../reforge/sdk/log4j2/Log4jLevelMapper.java | 26 ++ .../sdk/log4j2/ReforgeLog4j2Filter.java | 284 ++++++++++++++++++ pom.xml | 11 + 5 files changed, 654 insertions(+) create mode 100644 log4j2/README.md create mode 100644 log4j2/pom.xml create mode 100644 log4j2/src/main/java/com/reforge/sdk/log4j2/Log4jLevelMapper.java create mode 100644 log4j2/src/main/java/com/reforge/sdk/log4j2/ReforgeLog4j2Filter.java 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..6a7e014 --- /dev/null +++ b/log4j2/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + + com.reforge + sdk-parent + 1.0.3 + + + 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/pom.xml b/pom.xml index 9d328bc..b1ee5d1 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ Parent POM for Reforge SDK modules providing feature flags, configuration management, and A/B testing capabilities + log4j2 logback micronaut sdk @@ -100,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 From 63fedc9f6a517e84f6a73ee0041b3bb36165e3b2 Mon Sep 17 00:00:00 2001 From: James Kebinger Date: Fri, 31 Oct 2025 14:02:05 -0500 Subject: [PATCH 5/5] Fix test dependency issue and bump version to 1.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add logback-core test dependency with matching version range [1.4.12,) to fix NoClassDefFoundError in tests - Bump version from 1.0.3 to 1.1.0 across all modules - Ensures logback-classic and logback-core versions stay in sync during test execution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- log4j2/pom.xml | 2 +- logback/pom.xml | 2 +- micronaut/pom.xml | 2 +- pom.xml | 2 +- sdk/pom.xml | 8 +++++++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/log4j2/pom.xml b/log4j2/pom.xml index 6a7e014..43aca28 100644 --- a/log4j2/pom.xml +++ b/log4j2/pom.xml @@ -5,7 +5,7 @@ com.reforge sdk-parent - 1.0.3 + 1.1.0 sdk-log4j2 diff --git a/logback/pom.xml b/logback/pom.xml index d7408d6..7705858 100644 --- a/logback/pom.xml +++ b/logback/pom.xml @@ -5,7 +5,7 @@ com.reforge sdk-parent - 1.0.3 + 1.1.0 sdk-logback 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 b1ee5d1..c329ad6 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 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 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