Skip to content

Commit 37b777c

Browse files
json logging module
1 parent 6147ba7 commit 37b777c

File tree

8 files changed

+220
-0
lines changed

8 files changed

+220
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
plugins {
2+
id 'info.ankin.projects.library-conventions'
3+
id 'info.ankin.projects.spring-conventions'
4+
}
5+
6+
dependencies {
7+
implementation('org.springframework.boot:spring-boot-starter-web')
8+
implementation(project(':lib:spring:json-logging-listener'))
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package info.ankin.how.spring.logging.json.demo;
2+
3+
import info.ankin.how.spring.logging.json.JsonFormatListenerConfig;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.boot.ApplicationRunner;
6+
import org.springframework.boot.WebApplicationType;
7+
import org.springframework.boot.autoconfigure.SpringBootApplication;
8+
import org.springframework.boot.builder.SpringApplicationBuilder;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
12+
@SpringBootApplication
13+
class JsonFormatSlf4jListenerDemoApplication {
14+
public static void main(String[] args) {
15+
System.setProperty("logging.level.info.ankin", "debug");
16+
System.setProperty("json-logging-listener.enabled", "true");
17+
new SpringApplicationBuilder(JsonFormatSlf4jListenerDemoApplication.class)
18+
.web(WebApplicationType.NONE)
19+
.run(args);
20+
}
21+
22+
@Slf4j
23+
@Configuration
24+
static class Other {
25+
@Bean
26+
ApplicationRunner applicationRunner(JsonFormatListenerConfig.Props listenerProps) {
27+
return args -> log.info("listenerProps: {}", listenerProps);
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
plugins {
2+
id 'info.ankin.projects.library-conventions'
3+
id 'info.ankin.projects.spring-conventions'
4+
}
5+
6+
version '1.0.0'
7+
8+
dependencies {
9+
implementation 'ch.qos.logback:logback-classic'
10+
implementation 'org.springframework.boot:spring-boot'
11+
implementation 'org.springframework.boot:spring-boot-autoconfigure'
12+
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package info.ankin.how.spring.logging.json;
2+
3+
import lombok.Data;
4+
import lombok.experimental.Accessors;
5+
import org.springframework.boot.autoconfigure.AutoConfiguration;
6+
import org.springframework.boot.context.properties.ConfigurationProperties;
7+
import org.springframework.context.annotation.ComponentScan;
8+
import org.springframework.stereotype.Component;
9+
10+
@AutoConfiguration
11+
@ComponentScan(basePackageClasses = JsonFormatListenerConfig.class)
12+
public class JsonFormatListenerConfig {
13+
14+
@Data
15+
@Accessors(chain = true)
16+
@ConfigurationProperties(Props.PREFIX)
17+
@Component
18+
public static class Props {
19+
public static final String PREFIX = "json-logging-listener";
20+
21+
/**
22+
* should the listener attempt to set the json encoder on all registered appender instances
23+
*/
24+
boolean enabled = true;
25+
26+
/**
27+
* fail if any appender instances are not configurable (supported appender types: console)
28+
*/
29+
boolean strict = false;
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package info.ankin.how.spring.logging.json;
2+
3+
import ch.qos.logback.classic.LoggerContext;
4+
import ch.qos.logback.classic.spi.ILoggingEvent;
5+
import ch.qos.logback.core.Appender;
6+
import ch.qos.logback.core.ConsoleAppender;
7+
import ch.qos.logback.core.OutputStreamAppender;
8+
import ch.qos.logback.core.encoder.Encoder;
9+
import lombok.extern.slf4j.Slf4j;
10+
import net.logstash.logback.composite.LogstashVersionJsonProvider;
11+
import net.logstash.logback.encoder.LogstashEncoder;
12+
import org.slf4j.ILoggerFactory;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.boot.ConfigurableBootstrapContext;
16+
import org.springframework.boot.SpringApplication;
17+
import org.springframework.boot.SpringApplicationRunListener;
18+
import org.springframework.boot.context.properties.bind.Binder;
19+
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
20+
import org.springframework.core.annotation.Order;
21+
import org.springframework.core.env.ConfigurableEnvironment;
22+
23+
import java.util.List;
24+
import java.util.Set;
25+
import java.util.stream.StreamSupport;
26+
27+
import static info.ankin.how.spring.logging.json.JsonFormatListenerConfig.Props;
28+
29+
/**
30+
* the order must be positive to run after Application Listeners (EventPublishingRunListener)
31+
*
32+
* @see org.springframework.boot.context.logging.LoggingApplicationListener
33+
* @see org.springframework.boot.context.event.ApplicationPreparedEvent
34+
*/
35+
@Slf4j
36+
@Order(10)
37+
public class JsonFormatSlf4jListener implements SpringApplicationRunListener {
38+
public static final String NO_LOGBACK_MESSAGE =
39+
"No configuration occurred because logger is not logback: " +
40+
"logger factory is not a (logback) logger context";
41+
public static final String STRICT_MESSAGE =
42+
"JsonFormatSlf4jListener cannot let you proceed with un-configured appender instances. " +
43+
"This would result in logs being encoded without being formatted as json " +
44+
"and this is not allowed when the strict option is selected.";
45+
public static final String STRICT_MESSAGE_NO_LOGBACK = STRICT_MESSAGE + " " + NO_LOGBACK_MESSAGE;
46+
public static final String STRICT_PROP = Props.PREFIX + ".strict";
47+
48+
static final Set<Class<? extends Encoder<?>>> KNOWN_JSON_ENCODERS = Set.of(
49+
LogstashEncoder.class
50+
);
51+
52+
// see parent class documentation
53+
@SuppressWarnings("unused")
54+
JsonFormatSlf4jListener(SpringApplication springApplication, String[] args) {
55+
}
56+
57+
@Override
58+
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
59+
ConfigurableEnvironment environment) {
60+
Props props = Binder.get(environment).bindOrCreate(Props.PREFIX, Props.class);
61+
62+
if (!props.isEnabled()) {
63+
System.err.println("JsonFormatSlf4jListener is not enabled");
64+
return;
65+
}
66+
67+
ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
68+
69+
if (!(iLoggerFactory instanceof LoggerContext loggerContext)) {
70+
System.err.println(NO_LOGBACK_MESSAGE);
71+
if (props.isStrict())
72+
throw new InvalidConfigurationPropertyValueException(STRICT_PROP, true, STRICT_MESSAGE_NO_LOGBACK);
73+
return;
74+
}
75+
76+
Iterable<Appender<ILoggingEvent>> appenderIterator =
77+
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME)::iteratorForAppenders;
78+
79+
List<Appender<ILoggingEvent>> appenderList =
80+
StreamSupport.stream(appenderIterator.spliterator(), false)
81+
.toList();
82+
83+
var consoleAppenderList = appenderList.stream()
84+
.filter(ConsoleAppender.class::isInstance)
85+
.map(e -> (ConsoleAppender<ILoggingEvent>) e)
86+
.toList();
87+
88+
if (props.isStrict() && consoleAppenderList.size() > appenderList.size()) {
89+
System.err.println(STRICT_MESSAGE);
90+
throw new InvalidConfigurationPropertyValueException(STRICT_PROP, props.isStrict(), STRICT_MESSAGE);
91+
}
92+
93+
if (consoleAppenderList.isEmpty()) {
94+
log.debug("no console appender to modify, returning");
95+
return;
96+
}
97+
98+
var encoderList = consoleAppenderList.stream()
99+
.map(OutputStreamAppender::getEncoder)
100+
.filter(e -> KNOWN_JSON_ENCODERS.stream().noneMatch(c -> c.isInstance(e)))
101+
.toList();
102+
103+
if (encoderList.isEmpty()) {
104+
log.debug("all encoders already KNOWN_JSON_ENCODERS ({}), returning", KNOWN_JSON_ENCODERS);
105+
return;
106+
}
107+
108+
LogstashEncoder logstashEncoder = new LogstashEncoder();
109+
110+
// example of removing a default provider:
111+
logstashEncoder.getProviders().getProviders().stream()
112+
.filter(LogstashVersionJsonProvider.class::isInstance).findAny()
113+
.ifPresent(logstashEncoder.getProviders()::removeProvider);
114+
115+
// don't forget to start logback components
116+
logstashEncoder.start();
117+
118+
int fixed = 0;
119+
for (ConsoleAppender<ILoggingEvent> consoleAppender : consoleAppenderList) {
120+
if (encoderList.contains(consoleAppender.getEncoder())) {
121+
consoleAppender.setEncoder(logstashEncoder);
122+
fixed += 1;
123+
}
124+
}
125+
126+
log.debug("fixed {} encoders on root's {} console appender instances", fixed, consoleAppenderList.size());
127+
}
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#org.springframework.context.ApplicationListener=\
2+
# org.demo.SomeApplicationListener
3+
4+
org.springframework.boot.SpringApplicationRunListener=\
5+
info.ankin.how.spring.logging.json.JsonFormatSlf4jListener
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
info.ankin.how.spring.logging.json.JsonFormatListenerConfig

settings.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ include 'lib:ds'
1010
include 'lib:git:git-http-backend'
1111
include 'lib:spring:https-customizer'
1212
include 'lib:spring:https-customizer-autoconfigure'
13+
include 'lib:spring:json-logging-listener'
14+
include 'lib:spring:json-logging-listener-demo'
1315
include 'lib:spring:app'
1416
include 'lib:throwables'
1517
include 'plugins:gradle-plugins:code-artifact-credentials'

0 commit comments

Comments
 (0)