Skip to content

Commit 7c9474e

Browse files
committed
feat: introduce TenantLogger and TenantLoggerFactory for tenant-aware logging
1 parent 0fcc858 commit 7c9474e

File tree

3 files changed

+373
-0
lines changed

3 files changed

+373
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package com.tcn.exile.logger;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.MDC;
5+
import org.slf4j.spi.LocationAwareLogger;
6+
import org.slf4j.event.Level;
7+
import org.slf4j.LoggerFactory;
8+
9+
public class TenantLogger {
10+
private final Logger logger;
11+
private final LocationAwareLogger locationAwareLogger;
12+
private static final String FQCN = TenantLogger.class.getName();
13+
private final String tenant;
14+
15+
public TenantLogger(Logger logger) {
16+
this.logger = logger;
17+
this.locationAwareLogger = (logger instanceof LocationAwareLogger) ? (LocationAwareLogger) logger : null;
18+
this.tenant = null;
19+
}
20+
21+
public TenantLogger(String name, String tenant) {
22+
this.logger = LoggerFactory.getLogger(name);
23+
this.locationAwareLogger = (logger instanceof LocationAwareLogger) ? (LocationAwareLogger) logger : null;
24+
this.tenant = tenant;
25+
}
26+
27+
public void trace(String tenant, String msg) {
28+
withTenant(tenant, () -> log(Level.TRACE, msg, (Object[])null));
29+
}
30+
31+
public void trace(String tenant, String format, Object arg) {
32+
withTenant(tenant, () -> log(Level.TRACE, format, arg));
33+
}
34+
35+
public void trace(String tenant, String format, Object arg1, Object arg2) {
36+
withTenant(tenant, () -> log(Level.TRACE, format, arg1, arg2));
37+
}
38+
39+
public void trace(String tenant, String format, Object... arguments) {
40+
withTenant(tenant, () -> log(Level.TRACE, format, arguments));
41+
}
42+
43+
public void trace(String tenant, String msg, Throwable t) {
44+
withTenant(tenant, () -> logThrowable(Level.TRACE, msg, t));
45+
}
46+
47+
public void debug(String tenant, String msg) {
48+
withTenant(tenant, () -> log(Level.DEBUG, msg, (Object[])null));
49+
}
50+
51+
public void debug(String tenant, String format, Object arg) {
52+
withTenant(tenant, () -> log(Level.DEBUG, format, arg));
53+
}
54+
55+
public void debug(String tenant, String format, Object arg1, Object arg2) {
56+
withTenant(tenant, () -> log(Level.DEBUG, format, arg1, arg2));
57+
}
58+
59+
public void debug(String tenant, String format, Object... arguments) {
60+
withTenant(tenant, () -> log(Level.DEBUG, format, arguments));
61+
}
62+
63+
public void debug(String tenant, String msg, Throwable t) {
64+
withTenant(tenant, () -> logThrowable(Level.DEBUG, msg, t));
65+
}
66+
67+
public void info(String tenant, String msg) {
68+
withTenant(tenant, () -> log(Level.INFO, msg, (Object[])null));
69+
}
70+
71+
public void info(String tenant, String format, Object arg) {
72+
withTenant(tenant, () -> log(Level.INFO, format, arg));
73+
}
74+
75+
public void info(String tenant, String format, Object arg1, Object arg2) {
76+
withTenant(tenant, () -> log(Level.INFO, format, arg1, arg2));
77+
}
78+
79+
public void info(String tenant, String format, Object... arguments) {
80+
withTenant(tenant, () -> log(Level.INFO, format, arguments));
81+
}
82+
83+
public void info(String tenant, String msg, Throwable t) {
84+
withTenant(tenant, () -> logThrowable(Level.INFO, msg, t));
85+
}
86+
87+
public void warn(String tenant, String msg) {
88+
withTenant(tenant, () -> log(Level.WARN, msg, (Object[])null));
89+
}
90+
91+
public void warn(String tenant, String format, Object arg) {
92+
withTenant(tenant, () -> log(Level.WARN, format, arg));
93+
}
94+
95+
public void warn(String tenant, String format, Object arg1, Object arg2) {
96+
withTenant(tenant, () -> log(Level.WARN, format, arg1, arg2));
97+
}
98+
99+
public void warn(String tenant, String format, Object... arguments) {
100+
withTenant(tenant, () -> log(Level.WARN, format, arguments));
101+
}
102+
103+
public void warn(String tenant, String msg, Throwable t) {
104+
withTenant(tenant, () -> logThrowable(Level.WARN, msg, t));
105+
}
106+
107+
public void error(String tenant, String msg) {
108+
withTenant(tenant, () -> log(Level.ERROR, msg, (Object[])null));
109+
}
110+
111+
public void error(String tenant, String format, Object arg) {
112+
withTenant(tenant, () -> log(Level.ERROR, format, arg));
113+
}
114+
115+
public void error(String tenant, String format, Object arg1, Object arg2) {
116+
withTenant(tenant, () -> log(Level.ERROR, format, arg1, arg2));
117+
}
118+
119+
public void error(String tenant, String format, Object... arguments) {
120+
withTenant(tenant, () -> log(Level.ERROR, format, arguments));
121+
}
122+
123+
public void error(String tenant, String msg, Throwable t) {
124+
withTenant(tenant, () -> logThrowable(Level.ERROR, msg, t));
125+
}
126+
127+
private void withTenant(String tenant, Runnable loggingAction) {
128+
String tenantToUse = tenant != null ? tenant : this.tenant;
129+
if (tenantToUse == null) {
130+
throw new IllegalArgumentException("Tenant cannot be null");
131+
}
132+
try {
133+
MDC.put("tenant", tenantToUse);
134+
loggingAction.run();
135+
} finally {
136+
MDC.remove("tenant");
137+
}
138+
}
139+
140+
private void logThrowable(Level level, String msg, Throwable t) {
141+
if (locationAwareLogger != null) {
142+
locationAwareLogger.log(null, FQCN, toLocationAwareLevel(level), msg, null, t);
143+
} else {
144+
switch (level) {
145+
case TRACE: logger.trace(msg, t); break;
146+
case DEBUG: logger.debug(msg, t); break;
147+
case INFO: logger.info(msg, t); break;
148+
case WARN: logger.warn(msg, t); break;
149+
case ERROR: logger.error(msg, t); break;
150+
}
151+
}
152+
}
153+
154+
private void log(Level level, String format, Object... args) {
155+
if (locationAwareLogger != null) {
156+
// Safely handle null arguments array
157+
Object[] safeArgs = args != null ? args : new Object[0];
158+
locationAwareLogger.log(null, FQCN, toLocationAwareLevel(level), format, safeArgs, null);
159+
} else {
160+
switch (level) {
161+
case TRACE:
162+
if (args == null) logger.trace(format);
163+
else logger.trace(format, args);
164+
break;
165+
case DEBUG:
166+
if (args == null) logger.debug(format);
167+
else logger.debug(format, args);
168+
break;
169+
case INFO:
170+
if (args == null) logger.info(format);
171+
else logger.info(format, args);
172+
break;
173+
case WARN:
174+
if (args == null) logger.warn(format);
175+
else logger.warn(format, args);
176+
break;
177+
case ERROR:
178+
if (args == null) logger.error(format);
179+
else logger.error(format, args);
180+
break;
181+
}
182+
}
183+
}
184+
185+
private void log(Level level, String format, Object arg) {
186+
if (locationAwareLogger != null) {
187+
locationAwareLogger.log(null, FQCN, toLocationAwareLevel(level), format, new Object[]{arg}, null);
188+
} else {
189+
switch (level) {
190+
case TRACE: logger.trace(format, arg); break;
191+
case DEBUG: logger.debug(format, arg); break;
192+
case INFO: logger.info(format, arg); break;
193+
case WARN: logger.warn(format, arg); break;
194+
case ERROR: logger.error(format, arg); break;
195+
}
196+
}
197+
}
198+
199+
private void log(Level level, String format, Object arg1, Object arg2) {
200+
if (locationAwareLogger != null) {
201+
locationAwareLogger.log(null, FQCN, toLocationAwareLevel(level), format, new Object[]{arg1, arg2}, null);
202+
} else {
203+
switch (level) {
204+
case TRACE: logger.trace(format, arg1, arg2); break;
205+
case DEBUG: logger.debug(format, arg1, arg2); break;
206+
case INFO: logger.info(format, arg1, arg2); break;
207+
case WARN: logger.warn(format, arg1, arg2); break;
208+
case ERROR: logger.error(format, arg1, arg2); break;
209+
}
210+
}
211+
}
212+
213+
private int toLocationAwareLevel(Level level) {
214+
switch (level) {
215+
case TRACE: return LocationAwareLogger.TRACE_INT;
216+
case DEBUG: return LocationAwareLogger.DEBUG_INT;
217+
case INFO: return LocationAwareLogger.INFO_INT;
218+
case WARN: return LocationAwareLogger.WARN_INT;
219+
case ERROR: return LocationAwareLogger.ERROR_INT;
220+
default: throw new IllegalArgumentException("Unknown level: " + level);
221+
}
222+
}
223+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.tcn.exile.logger;
2+
3+
import org.slf4j.ILoggerFactory;
4+
import org.slf4j.LoggerFactory;
5+
import org.slf4j.Logger;
6+
7+
public class TenantLoggerFactory {
8+
9+
private static final TenantLoggerFactory INSTANCE =
10+
new TenantLoggerFactory();
11+
12+
private TenantLoggerFactory() {}
13+
14+
public static TenantLoggerFactory getInstance() {
15+
return INSTANCE;
16+
}
17+
18+
19+
20+
public static TenantLogger getLogger(Class<?> clazz, String tenant) {
21+
return new TenantLogger(clazz.getName(), tenant);
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.tcn.exile.logger;
2+
3+
4+
5+
import ch.qos.logback.classic.Level;
6+
import ch.qos.logback.classic.Logger;
7+
import ch.qos.logback.classic.spi.ILoggingEvent;
8+
import ch.qos.logback.core.read.ListAppender;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.slf4j.LoggerFactory;
12+
import org.slf4j.MDC;
13+
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
public class TenantLoggerTest {
19+
private TenantLogger tenantLogger;
20+
private Logger logbackLogger;
21+
private ListAppender<ILoggingEvent> listAppender;
22+
23+
@BeforeEach
24+
public void setUp() {
25+
// Get the SLF4J logger and cast to Logback logger
26+
logbackLogger = (Logger) LoggerFactory.getLogger(TenantLoggerTest.class);
27+
tenantLogger = new TenantLogger(logbackLogger);
28+
29+
// Create and attach a ListAppender to capture log events
30+
listAppender = new ListAppender<>();
31+
listAppender.start();
32+
logbackLogger.addAppender(listAppender);
33+
34+
// Clear MDC before each test
35+
MDC.clear();
36+
}
37+
38+
@Test
39+
public void testInfoLogWithTenant() {
40+
String tenant = "tenant1";
41+
String message = "Test info message";
42+
43+
tenantLogger.info(tenant, message);
44+
45+
List<ILoggingEvent> logs = listAppender.list;
46+
assertEquals(1, logs.size(), "Exactly one log event should be captured");
47+
48+
ILoggingEvent event = logs.get(0);
49+
assertEquals(Level.INFO, event.getLevel(), "Log level should be INFO");
50+
assertEquals(message, event.getFormattedMessage(), "Log message should match");
51+
assertEquals(tenant, event.getMDCPropertyMap().get("tenant"), "Tenant should be set in MDC");
52+
53+
// Skip caller data checks as they might not be available in this environment
54+
}
55+
56+
@Test
57+
public void testDebugLogWithArguments() {
58+
String tenant = "tenant2";
59+
String format = "Test debug with args: {} and {}";
60+
String arg1 = "value1";
61+
String arg2 = "value2";
62+
63+
tenantLogger.debug(tenant, format, arg1, arg2);
64+
65+
List<ILoggingEvent> logs = listAppender.list;
66+
assertEquals(1, logs.size(), "Exactly one log event should be captured");
67+
68+
ILoggingEvent event = logs.get(0);
69+
assertEquals(Level.DEBUG, event.getLevel(), "Log level should be DEBUG");
70+
assertEquals("Test debug with args: value1 and value2", event.getFormattedMessage(), "Log message should match formatted string");
71+
assertEquals(tenant, event.getMDCPropertyMap().get("tenant"), "Tenant should be set in MDC");
72+
73+
// Skip caller data checks as they might not be available in this environment
74+
}
75+
76+
@Test
77+
public void testErrorLogWithThrowable() {
78+
String tenant = "tenant3";
79+
String message = "Test error message";
80+
Exception exception = new RuntimeException("Test exception");
81+
82+
tenantLogger.error(tenant, message, exception);
83+
84+
List<ILoggingEvent> logs = listAppender.list;
85+
assertEquals(1, logs.size(), "Exactly one log event should be captured");
86+
87+
ILoggingEvent event = logs.get(0);
88+
assertEquals(Level.ERROR, event.getLevel(), "Log level should be ERROR");
89+
assertEquals(message, event.getFormattedMessage(), "Log message should match");
90+
assertEquals(tenant, event.getMDCPropertyMap().get("tenant"), "Tenant should be set in MDC");
91+
92+
// Updated throwable assertion to check class name and message instead of object equality
93+
if (event.getThrowableProxy() != null) {
94+
assertEquals(exception.getClass().getName(), event.getThrowableProxy().getClassName(), "Throwable class should match");
95+
assertEquals(exception.getMessage(), event.getThrowableProxy().getMessage(), "Throwable message should match");
96+
} else {
97+
fail("ThrowableProxy is null");
98+
}
99+
100+
// Skip caller data checks as they might not be available in this environment
101+
}
102+
103+
@Test
104+
public void testNullTenantThrowsException() {
105+
String message = "Test message";
106+
107+
IllegalArgumentException exception = assertThrows(
108+
IllegalArgumentException.class,
109+
() -> tenantLogger.info(null, message),
110+
"Null tenant should throw IllegalArgumentException"
111+
);
112+
113+
assertEquals("Tenant cannot be null", exception.getMessage(), "Exception message should match");
114+
assertTrue(listAppender.list.isEmpty(), "No log events should be captured");
115+
}
116+
117+
@Test
118+
public void testMDCIsClearedAfterLogging() {
119+
String tenant = "tenant4";
120+
String message = "Test message";
121+
122+
tenantLogger.info(tenant, message);
123+
124+
assertNull(MDC.get("tenant"), "MDC should be cleared after logging");
125+
assertEquals(1, listAppender.list.size(), "One log event should be captured");
126+
}
127+
}

0 commit comments

Comments
 (0)