Skip to content

Commit 267e753

Browse files
authored
Implement JSON Logger configuration (#74)
1 parent d0d4667 commit 267e753

33 files changed

Lines changed: 1201 additions & 71 deletions

File tree

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ mockito = "5.20.0"
2121
assertj-core = "4.0.0-M1"
2222
# https://mvnrepository.com/artifact/org.awaitility/awaitility
2323
awaitility = "4.3.0"
24+
# https://mvnrepository.com/artifact/tools.jackson.core/jackson-core/3.2.0
25+
jackson = "3.2.0"
2426

2527
[libraries]
2628
project-reactor-core = { module = "io.projectreactor:reactor-core", version.ref = "project-reactor" }
@@ -40,6 +42,8 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
4042
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
4143
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj-core" }
4244
awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" }
45+
jackson-core = { module = "tools.jackson.core:jackson-core", version.ref = "jackson" }
46+
jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
4347

4448
[bundles]
4549
mail = ["jakarta-mail-api", "angus-mail"]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package javasabr.rlib.common.util;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.InputStream;
5+
import java.nio.charset.StandardCharsets;
6+
import java.util.Map;
7+
import java.util.Set;
8+
import java.util.stream.Collectors;
9+
10+
public class ResourceClassLoader extends ClassLoader {
11+
12+
private final Map<String, byte[]> resources;
13+
private final Set<String> hiddenResources;
14+
15+
public ResourceClassLoader(Map<String, String> resources, Set<String> hiddenResources) {
16+
super(Thread
17+
.currentThread()
18+
.getContextClassLoader());
19+
this.resources = resources
20+
.entrySet()
21+
.stream()
22+
.collect(Collectors.toUnmodifiableMap(
23+
Map.Entry::getKey,
24+
entry -> entry.getValue().getBytes(StandardCharsets.UTF_8)));
25+
this.hiddenResources = Set.copyOf(hiddenResources);
26+
}
27+
28+
@Override
29+
public InputStream getResourceAsStream(String name) {
30+
byte[] loaded = resources.get(name);
31+
if (loaded != null) {
32+
return new ByteArrayInputStream(loaded);
33+
}
34+
if (hiddenResources.contains(name)) {
35+
return null;
36+
}
37+
return super.getResourceAsStream(name);
38+
}
39+
}

rlib-logger-api/src/main/java/javasabr/rlib/logger/api/LoggerFactory.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
public interface LoggerFactory {
99

10+
String ROOT_LOGGER_NAME = "ROOT";
11+
1012
/**
1113
* Creates or gets a logger with the specified name.
1214
*
@@ -26,12 +28,12 @@ public interface LoggerFactory {
2628
Logger getLogger(Class<?> type);
2729

2830
/**
29-
* Returns the default logger.
31+
* Returns the root logger.
3032
*
31-
* @return the default logger
33+
* @return the root logger
3234
* @since 10.0.0
3335
*/
34-
Logger getDefault();
36+
Logger getRootLogger();
3537

3638
/**
3739
* Returns the logger service.

rlib-logger-api/src/main/java/javasabr/rlib/logger/api/LoggerManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public class LoggerManager {
5959
* @since 10.0.0
6060
*/
6161
public static Logger getDefaultLogger() {
62-
return LOGGER_FACTORY.getDefault();
62+
return LOGGER_FACTORY.getRootLogger();
6363
}
6464

6565
/**

rlib-logger-api/src/main/java/javasabr/rlib/logger/api/impl/NoOpsLoggerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public Logger getLogger(Class<?> type) {
2020
}
2121

2222
@Override
23-
public Logger getDefault() {
23+
public Logger getRootLogger() {
2424
return NO_OPS_LOGGER;
2525
}
2626

rlib-logger-impl-json/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
plugins {
2+
id("configure-java")
3+
id("configure-publishing")
4+
}
5+
6+
dependencies {
7+
api projects.rlibLoggerImpl
8+
api libs.jackson.core
9+
api libs.jackson.databind
10+
testImplementation testFixtures(projects.rlibCommon)
11+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package javasabr.rlib.logger.impl.config.loader.json;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.lang.reflect.Constructor;
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Optional;
10+
import java.util.Set;
11+
import java.util.stream.Collectors;
12+
import javasabr.rlib.common.util.StringUtils;
13+
import javasabr.rlib.logger.api.LoggerLevel;
14+
import javasabr.rlib.logger.impl.config.LoggerConfig;
15+
import javasabr.rlib.logger.impl.config.consumer.LogMessageConsumer;
16+
import javasabr.rlib.logger.impl.config.consumer.impl.ConsoleMessageConsumer;
17+
import javasabr.rlib.logger.impl.config.consumer.impl.CustomLogMessageConsumer;
18+
import javasabr.rlib.logger.impl.config.impl.LoggerConfigBuilder;
19+
import javasabr.rlib.logger.impl.config.loader.LoggerConfigLoader;
20+
import javasabr.rlib.logger.impl.config.loader.json.dto.JsonLoggerConfigDto;
21+
import javasabr.rlib.logger.impl.config.loader.json.dto.JsonLoggerConfigDto.ConsumerDto;
22+
import javasabr.rlib.logger.impl.config.loader.json.dto.JsonLoggerConfigDto.LoggerDto;
23+
import javasabr.rlib.logger.impl.config.loader.json.dto.JsonLoggerConfigDto.RenderDto;
24+
import javasabr.rlib.logger.impl.config.render.LogMessageRender;
25+
import javasabr.rlib.logger.impl.config.render.impl.CustomLogMessageRender;
26+
import javasabr.rlib.logger.impl.config.render.impl.SimpleLogMessageRender;
27+
import javasabr.rlib.logger.impl.config.render.impl.pattern.PatternLogMessageRender;
28+
import lombok.AccessLevel;
29+
import lombok.experimental.FieldDefaults;
30+
import tools.jackson.databind.ObjectMapper;
31+
32+
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
33+
public class JsonLoggerConfigLoader implements LoggerConfigLoader {
34+
35+
public static final String FILE_MAIN = "rlib.logger.json";
36+
public static final String FILE_TEST = "rlib.logger-test.json";
37+
38+
ObjectMapper objectMapper = new ObjectMapper();
39+
40+
@Override
41+
public Optional<LoggerConfig> tryToLoad() {
42+
ClassLoader classLoader = Thread
43+
.currentThread()
44+
.getContextClassLoader();
45+
InputStream configStream = classLoader.getResourceAsStream(FILE_TEST);
46+
if (configStream == null) {
47+
configStream = classLoader.getResourceAsStream(FILE_MAIN);
48+
}
49+
if (configStream == null) {
50+
return Optional.empty();
51+
} else {
52+
return Optional.of(load(configStream));
53+
}
54+
}
55+
56+
@Override
57+
public int order() {
58+
return LoggerConfigLoader.ORDER_NORMAL;
59+
}
60+
61+
private LoggerConfig load(InputStream inputStream) {
62+
try (var in = inputStream) {
63+
JsonLoggerConfigDto configDto = objectMapper.readValue(in, JsonLoggerConfigDto.class);
64+
return load(configDto);
65+
} catch (IOException e) {
66+
throw new RuntimeException(e);
67+
}
68+
}
69+
70+
private LoggerConfig load(JsonLoggerConfigDto configDto) {
71+
72+
var builder = new LoggerConfigBuilder();
73+
74+
List<CreatedRender> createdRenders = configDto
75+
.renders()
76+
.stream()
77+
.map(this::createRender)
78+
.toList();
79+
80+
Map<String, LogMessageRender> renderMap = createdRenders
81+
.stream()
82+
.collect(Collectors.toMap(CreatedRender::name, CreatedRender::render));
83+
84+
List<CreatedConsumer> createdConsumers = configDto
85+
.consumers()
86+
.stream()
87+
.map(consumerDto -> createConsumer(renderMap, consumerDto))
88+
.toList();
89+
90+
Map<String, LogMessageConsumer> consumerMap = createdConsumers
91+
.stream()
92+
.collect(Collectors.toMap(CreatedConsumer::name, CreatedConsumer::consumer));
93+
94+
for (LoggerDto loggerDto : configDto.loggers()) {
95+
if (loggerDto.level() != null) {
96+
builder.registerLoggerLevel(loggerDto.name(), loggerDto.level());
97+
}
98+
Set<String> consumerNames = loggerDto.consumerNames();
99+
if (!consumerNames.isEmpty()) {
100+
for (String consumerName : consumerNames) {
101+
LogMessageConsumer consumer = consumerMap.get(consumerName);
102+
if (consumer == null) {
103+
throw new RuntimeException("Consumer " + consumerName + " not found");
104+
}
105+
LoggerLevel level = loggerDto.level();
106+
if (level == null) {
107+
level = LoggerLevel.TRACE;
108+
}
109+
builder.registerLoggerConsumer(loggerDto.name(), level, consumer);
110+
}
111+
}
112+
}
113+
114+
return builder.build();
115+
}
116+
117+
CreatedRender createRender(RenderDto renderDto) {
118+
LogMessageRender render = switch (renderDto.type()) {
119+
case SIMPLE -> new SimpleLogMessageRender();
120+
case PATTERN -> new PatternLogMessageRender(renderDto.args());
121+
case CUSTOM -> createCustomRender(renderDto);
122+
};
123+
return new CreatedRender(renderDto.name(), render);
124+
}
125+
126+
LogMessageRender createCustomRender(RenderDto renderDto) {
127+
String className = renderDto.className();
128+
if (StringUtils.isBlank(className)) {
129+
throw new IllegalArgumentException("'class' attribute is required for custom render");
130+
}
131+
ClassLoader classLoader = Thread
132+
.currentThread()
133+
.getContextClassLoader();
134+
try {
135+
var targetClass = (Class<? extends CustomLogMessageRender>) classLoader.loadClass(className);
136+
Constructor<? extends CustomLogMessageRender> constructor = targetClass.getDeclaredConstructor(Map.class);
137+
return constructor.newInstance(renderDto.args());
138+
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException |
139+
IllegalAccessException e) {
140+
throw new RuntimeException(e);
141+
}
142+
}
143+
144+
CreatedConsumer createConsumer(Map<String, LogMessageRender> renderMap, ConsumerDto consumerDto) {
145+
LogMessageRender render = renderMap.get(consumerDto.renderName());
146+
if (render == null) {
147+
throw new IllegalArgumentException("Unknown render with name: " + consumerDto.renderName());
148+
}
149+
LogMessageConsumer consumer = switch (consumerDto.type()) {
150+
case CONSOLE -> new ConsoleMessageConsumer(render);
151+
case CUSTOM -> createCustomConsumer(render, consumerDto);
152+
};
153+
return new CreatedConsumer(consumerDto.name(), consumer);
154+
}
155+
156+
LogMessageConsumer createCustomConsumer(LogMessageRender render, ConsumerDto consumerDto) {
157+
String className = consumerDto.className();
158+
if (StringUtils.isBlank(className)) {
159+
throw new IllegalArgumentException("'class' attribute is required for custom consumer");
160+
}
161+
ClassLoader classLoader = Thread
162+
.currentThread()
163+
.getContextClassLoader();
164+
try {
165+
var targetClass = (Class<? extends CustomLogMessageConsumer>) classLoader.loadClass(className);
166+
Constructor<? extends CustomLogMessageConsumer> constructor = targetClass
167+
.getDeclaredConstructor(LogMessageRender.class, Map.class);
168+
return constructor.newInstance(render, consumerDto.args());
169+
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | InstantiationException |
170+
IllegalAccessException e) {
171+
throw new RuntimeException(e);
172+
}
173+
}
174+
175+
private record CreatedRender(String name, LogMessageRender render) {}
176+
private record CreatedConsumer(String name, LogMessageConsumer consumer) {}
177+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package javasabr.rlib.logger.impl.config.loader.json;
2+
3+
import javasabr.rlib.collections.array.Array;
4+
import javasabr.rlib.logger.impl.config.loader.LoggerConfigLoader;
5+
import javasabr.rlib.logger.impl.config.loader.spi.LoggerConfigLoadersProvider;
6+
7+
public class JsonLoggerConfigLoadersProvider implements LoggerConfigLoadersProvider {
8+
9+
private static final Array<LoggerConfigLoader> LOADERS = Array.of(new JsonLoggerConfigLoader());
10+
11+
@Override
12+
public Array<LoggerConfigLoader> getLoggerConfigLoaders() {
13+
return LOADERS;
14+
}
15+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package javasabr.rlib.logger.impl.config.loader.json.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Set;
7+
import javasabr.rlib.common.util.StringUtils;
8+
import javasabr.rlib.logger.api.LoggerLevel;
9+
import org.jspecify.annotations.Nullable;
10+
11+
public record JsonLoggerConfigDto(
12+
List<LoggerDto> loggers,
13+
List<RenderDto> renders,
14+
List<ConsumerDto> consumers) {
15+
16+
public JsonLoggerConfigDto {
17+
loggers = loggers == null ? List.of() : List.copyOf(loggers);
18+
renders = renders == null ? List.of() : List.copyOf(renders);
19+
consumers = consumers == null ? List.of() : List.copyOf(consumers);
20+
}
21+
22+
public record LoggerDto(
23+
String name,
24+
@Nullable LoggerLevel level,
25+
@JsonProperty("consumers") Set<String> consumerNames) {
26+
27+
public LoggerDto {
28+
if (StringUtils.isBlank(name)) {
29+
throw new IllegalArgumentException("name is blank");
30+
}
31+
consumerNames = consumerNames == null ? Set.of() : Set.copyOf(consumerNames);
32+
}
33+
}
34+
35+
public record RenderDto(
36+
String name,
37+
RenderType type,
38+
@JsonProperty("class") @Nullable String className,
39+
Map<String, Object> args) {
40+
41+
public RenderDto {
42+
if (StringUtils.isBlank(name)) {
43+
throw new IllegalArgumentException("name is blank");
44+
} else if (type == null) {
45+
throw new IllegalArgumentException("type is null");
46+
}
47+
args = args == null ? Map.of() : Map.copyOf(args);
48+
}
49+
}
50+
51+
public enum RenderType {
52+
SIMPLE,
53+
PATTERN,
54+
CUSTOM
55+
}
56+
57+
public record ConsumerDto(
58+
String name,
59+
ConsumerType type,
60+
@JsonProperty("render") String renderName,
61+
@JsonProperty("class") @Nullable String className,
62+
Map<String, Object> args) {
63+
64+
public ConsumerDto {
65+
if (StringUtils.isBlank(name)) {
66+
throw new IllegalArgumentException("name is blank");
67+
} else if (type == null) {
68+
throw new IllegalArgumentException("type is null");
69+
}
70+
args = args == null ? Map.of() : Map.copyOf(args);
71+
}
72+
}
73+
74+
public enum ConsumerType {
75+
CONSOLE,
76+
CUSTOM
77+
}
78+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@NullMarked
2+
package javasabr.rlib.logger.impl.config.loader.json.dto;
3+
4+
import org.jspecify.annotations.NullMarked;

0 commit comments

Comments
 (0)