Skip to content

Commit 54c9b0c

Browse files
jkebingerclaude
andauthored
Add dynamic log level management with Logback and Log4j2 integrations (#20)
* Add core log level support with LoggerClient 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": "<loggerName>"}} * 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 <noreply@anthropic.com> * Add Logback integration module for dynamic log levels 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 <noreply@anthropic.com> * Optimize Logback level mapper and improve error handling 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 <noreply@anthropic.com> * Add Log4j2 integration module for dynamic log levels 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 <noreply@anthropic.com> * Fix test dependency issue and bump version to 1.1.0 - 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e5bb3f commit 54c9b0c

19 files changed

Lines changed: 1599 additions & 3 deletions

File tree

log4j2/README.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Reforge SDK Log4j2 Integration
2+
3+
This module provides seamless integration between the Reforge SDK and Apache Log4j2, enabling dynamic log level management for your application.
4+
5+
## Overview
6+
7+
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:
8+
9+
- **Centrally manage log levels** - Control logging across your entire application from the Reforge dashboard
10+
- **Real-time updates** - Change log levels without restarting your application
11+
- **Context-aware logging** - Different log levels for different loggers based on runtime context
12+
- **Performance** - Efficient filtering happens before log message construction
13+
14+
## Installation
15+
16+
Add the dependency to your `pom.xml`:
17+
18+
```xml
19+
<dependency>
20+
<groupId>com.reforge</groupId>
21+
<artifactId>sdk-log4j2</artifactId>
22+
<version>1.0.3</version>
23+
</dependency>
24+
```
25+
26+
### Compatibility
27+
28+
This module is compatible with:
29+
- **Log4j2 2.x** - The module uses only stable Log4j2 Core APIs (AbstractFilter, LoggerContext) that have been stable across Log4j2 2.x versions
30+
- **SLF4J 1.7.x and 2.0.x** (both work with Log4j2)
31+
32+
**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.
33+
34+
## Usage
35+
36+
### Basic Setup (Programmatic)
37+
38+
The simplest approach is to install the filter programmatically during application startup:
39+
40+
```java
41+
import com.reforge.sdk.Sdk;
42+
import com.reforge.sdk.Options;
43+
import com.reforge.sdk.log4j2.ReforgeLog4j2Filter;
44+
import org.apache.logging.log4j.LogManager;
45+
import org.apache.logging.log4j.Logger;
46+
47+
public class MyApplication {
48+
private static final Logger log = LogManager.getLogger(MyApplication.class);
49+
50+
public static void main(String[] args) {
51+
// Initialize the Reforge SDK
52+
Sdk sdk = new Sdk(new Options());
53+
54+
// Install the Log4j2 filter
55+
ReforgeLog4j2Filter.install(sdk.loggerClient());
56+
57+
// Now all your logging will respect Reforge log levels
58+
log.info("Application started with dynamic log levels");
59+
}
60+
}
61+
```
62+
63+
**Important:**
64+
- Install the filter as early as possible in your application startup, ideally right after initializing the Reforge SDK
65+
- Install after Log4j2 is initialized
66+
- Any dynamic reconfiguration of Log4j2 (e.g., via JMX or programmatic changes) will remove the filter and it will need to be reinstalled
67+
68+
### Configuration
69+
70+
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:
71+
72+
- `reforge-sdk-logging.lang`: `"java"`
73+
- `reforge-sdk-logging.logger-path`: The name of the logger (e.g., `"com.example.MyClass"`)
74+
75+
You can customize the config key:
76+
77+
```java
78+
Options options = new Options().setLoggerKey("my.custom.log.config");
79+
Sdk sdk = new Sdk(options);
80+
ReforgeLog4j2Filter.install(sdk.loggerClient());
81+
```
82+
83+
### Example Reforge Configuration
84+
85+
In your Reforge dashboard, create a `LOG_LEVEL_V2` config with key `log-levels.default`:
86+
87+
```yaml
88+
# Default to INFO for all loggers
89+
default: INFO
90+
91+
# Set specific packages to DEBUG
92+
rules:
93+
- criteria:
94+
logger-path:
95+
starts-with: "com.example.services"
96+
value: DEBUG
97+
98+
# Only log errors in noisy third-party library
99+
- criteria:
100+
logger-path:
101+
starts-with: "com.thirdparty.noisy"
102+
value: ERROR
103+
```
104+
105+
## How It Works
106+
107+
Once installed, the filter intercepts **all** logging calls across your entire application:
108+
109+
- Works with **all loggers** (no need to configure individual loggers)
110+
- Works with **all appenders** (console, file, rolling, syslog, async, etc.)
111+
- Filters happen **before** log messages are formatted (performance benefit)
112+
- No modification of your existing Log4j2 configuration needed
113+
114+
## Performance Considerations
115+
116+
- **Efficient filtering**: Log level checks happen before expensive message construction
117+
- **Recursion protection**: Built-in guard prevents performance issues from recursive logging
118+
- **Caching**: The Reforge SDK caches configuration data to minimize network calls
119+
120+
## Thread Safety
121+
122+
The integration is fully thread-safe and uses Log4j2's built-in filter infrastructure, which is designed for concurrent access.
123+
124+
## FAQ
125+
126+
### Do I need to configure individual loggers?
127+
128+
**No!** The filter works at the LoggerContext level and automatically intercepts all logging calls across your entire application without needing to configure individual loggers.
129+
130+
### Do I need to modify my existing log4j2.xml?
131+
132+
**No!** The filter works alongside your existing Log4j2 configuration. You don't need to change your appenders, layouts, or logger settings.
133+
134+
### Does this work with all Log4j2 appenders?
135+
136+
**Yes!** The filter intercepts logging decisions before they reach any appenders. It works with:
137+
- Console appenders
138+
- File appenders
139+
- Rolling file appenders
140+
- Syslog appenders
141+
- Async appenders
142+
- JDBC appenders
143+
- Custom appenders
144+
145+
### What's the performance impact?
146+
147+
Minimal! The filter:
148+
- Runs before log message formatting (avoiding expensive string operations for filtered logs)
149+
- Uses a ThreadLocal recursion guard to prevent infinite loops
150+
- Only makes one SDK call per log statement
151+
- The SDK caches configuration data to minimize network overhead
152+
153+
### Can I use this in Spring Boot applications?
154+
155+
**Yes!** Just install the filter in your main application class or a `@PostConstruct` method:
156+
157+
```java
158+
@SpringBootApplication
159+
public class MyApplication {
160+
@Autowired
161+
private Sdk reforgeSDK;
162+
163+
@PostConstruct
164+
public void setupLogging() {
165+
ReforgeLog4j2Filter.install(reforgeSDK.loggerClient());
166+
}
167+
168+
public static void main(String[] args) {
169+
SpringApplication.run(MyApplication.class, args);
170+
}
171+
}
172+
```
173+
174+
### What happens if Log4j2 is reconfigured?
175+
176+
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.
177+
178+
## Troubleshooting
179+
180+
### Filter not installing
181+
182+
If you see errors during filter installation, ensure that:
183+
184+
1. Log4j2 is actually on your classpath
185+
2. You're using Log4j2 (not Log4j 1.x or another logging implementation)
186+
3. The filter installation happens after Log4j2 initialization
187+
188+
To verify Log4j2 is being used:
189+
```java
190+
import org.apache.logging.log4j.LogManager;
191+
import org.apache.logging.log4j.core.LoggerContext;
192+
193+
try {
194+
LoggerContext context = (LoggerContext) LogManager.getContext(false);
195+
System.out.println("Using Log4j2: " + context.getClass().getName());
196+
} catch (ClassCastException e) {
197+
System.out.println("NOT using Log4j2 Core");
198+
}
199+
```
200+
201+
### Log levels not changing
202+
203+
If log levels aren't being respected:
204+
205+
1. Verify the config key exists in Reforge (default: `log-levels.default`)
206+
2. Check that it's of type `LOG_LEVEL_V2`
207+
3. Ensure the Reforge SDK is initialized and ready
208+
4. Check the SDK logs for any errors during config retrieval
209+
5. Verify the filter is still installed (it may have been removed by reconfiguration)
210+
211+
### Existing Log4j2 configuration overriding dynamic levels
212+
213+
The filter runs as part of Log4j2's filter chain. However, if you have:
214+
- Appender-level filters with thresholds
215+
- Very restrictive context-wide filters
216+
- Logger-level settings that are more restrictive
217+
218+
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.
219+
220+
### Filter removed after reconfiguration
221+
222+
If your application uses Log4j2's configuration file watching or programmatic reconfiguration, the filter will be removed. Options:
223+
224+
1. **Disable automatic reconfiguration** if not needed
225+
2. **Add a reconfiguration listener** that reinstalls the filter
226+
3. **Install via configuration** (though this requires static SDK access)
227+
228+
## Example Application
229+
230+
```java
231+
package com.example;
232+
233+
import com.reforge.sdk.Sdk;
234+
import com.reforge.sdk.Options;
235+
import com.reforge.sdk.log4j2.ReforgeLog4j2Filter;
236+
import org.apache.logging.log4j.LogManager;
237+
import org.apache.logging.log4j.Logger;
238+
239+
public class MyApplication {
240+
private static final Logger log = LogManager.getLogger(MyApplication.class);
241+
242+
public static void main(String[] args) {
243+
// Initialize Reforge SDK
244+
Sdk sdk = new Sdk(new Options());
245+
246+
// Install Log4j2 integration
247+
ReforgeLog4j2Filter.install(sdk.loggerClient());
248+
249+
log.trace("This is a trace message"); // Filtered if level > TRACE
250+
log.debug("This is a debug message"); // Filtered if level > DEBUG
251+
log.info("This is an info message"); // Filtered if level > INFO
252+
log.warn("This is a warning"); // Filtered if level > WARN
253+
log.error("This is an error"); // Filtered if level > ERROR
254+
log.fatal("This is fatal"); // Filtered if level > FATAL
255+
}
256+
}
257+
```
258+
259+
## Differences from Logback Integration
260+
261+
The Log4j2 integration differs from Logback in a few ways:
262+
263+
| Feature | Log4j2 | Logback |
264+
|---------|--------|---------|
265+
| Filter type | AbstractFilter | TurboFilter |
266+
| Filter scope | LoggerContext-wide | Framework-wide |
267+
| Reconfiguration | Filter removed on reconfig | Filter persists |
268+
| FATAL level | Native support | Maps to ERROR |
269+
270+
Both integrations provide the same dynamic log level functionality with similar performance characteristics.

log4j2/pom.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>com.reforge</groupId>
7+
<artifactId>sdk-parent</artifactId>
8+
<version>1.1.0</version>
9+
</parent>
10+
11+
<artifactId>sdk-log4j2</artifactId>
12+
<packaging>jar</packaging>
13+
<name>Reforge SDK Log4j2 Integration</name>
14+
<description>Log4j2 integration for Reforge SDK dynamic log level management</description>
15+
16+
<dependencies>
17+
<!-- Reforge SDK -->
18+
<dependency>
19+
<groupId>com.reforge</groupId>
20+
<artifactId>sdk</artifactId>
21+
<version>${project.version}</version>
22+
</dependency>
23+
24+
<!-- Log4j2 (provided scope - use customer's version) -->
25+
<dependency>
26+
<groupId>org.apache.logging.log4j</groupId>
27+
<artifactId>log4j-api</artifactId>
28+
<scope>provided</scope>
29+
</dependency>
30+
31+
<dependency>
32+
<groupId>org.apache.logging.log4j</groupId>
33+
<artifactId>log4j-core</artifactId>
34+
<scope>provided</scope>
35+
</dependency>
36+
37+
<!-- SLF4J API (provided scope - use customer's version) -->
38+
<dependency>
39+
<groupId>org.slf4j</groupId>
40+
<artifactId>slf4j-api</artifactId>
41+
<scope>provided</scope>
42+
</dependency>
43+
44+
<!-- Test dependencies -->
45+
<dependency>
46+
<groupId>org.assertj</groupId>
47+
<artifactId>assertj-core</artifactId>
48+
<scope>test</scope>
49+
</dependency>
50+
51+
<dependency>
52+
<groupId>org.junit.jupiter</groupId>
53+
<artifactId>junit-jupiter</artifactId>
54+
<scope>test</scope>
55+
</dependency>
56+
57+
<dependency>
58+
<groupId>org.mockito</groupId>
59+
<artifactId>mockito-core</artifactId>
60+
<scope>test</scope>
61+
</dependency>
62+
</dependencies>
63+
</project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.reforge.sdk.log4j2;
2+
3+
import com.reforge.sdk.LogLevel;
4+
import org.apache.logging.log4j.Level;
5+
6+
class Log4jLevelMapper {
7+
8+
static Level toLog4jLevel(LogLevel reforgeLevel) {
9+
switch (reforgeLevel) {
10+
case FATAL:
11+
return Level.FATAL;
12+
case ERROR:
13+
return Level.ERROR;
14+
case WARN:
15+
return Level.WARN;
16+
case INFO:
17+
return Level.INFO;
18+
case DEBUG:
19+
return Level.DEBUG;
20+
case TRACE:
21+
return Level.TRACE;
22+
default:
23+
return Level.DEBUG;
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)