This article will teach you how to monitor spring boot application’s live logs online via websocket.
Create a project
poml
The spring-boot-starter-websocket
dependency needs to be added.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.springcloud</groupId>
<artifactId>springcloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<encoding>UTF-8</encoding>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- WebSocket Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>
</project>
|
LogChannel
Customize a WebSocket endpoint. It will store every connection initiated by the client. And it can push messages to all connections.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
package io.springcloud.demo.channel;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
@ServerEndpoint(value = "/channel/log")
@Slf4j
public class LogChannel {
public static final ConcurrentMap<String, LogChannel> CHANNELS = new ConcurrentHashMap<>();
private Session session;
@OnMessage(maxMessageSize = 1) // MaxMessage 1 byte
public void onMessage(String message) {
log.debug("Recv Message: {}", message);
try {
this.session.close(new CloseReason(CloseCodes.TOO_BIG, "This endpoint does not accept client messages"));
} catch (IOException e) {
log.error("Connection close error: id={}, err={}", this.session.getId(), e.getMessage());
}
}
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig) {
this.session = session;
this.session.setMaxIdleTimeout(0);
CHANNELS.put(this.session.getId(), this);
log.info("New client connection: id={}", this.session.getId());
}
@OnClose
public void onClose(CloseReason closeReason) {
log.info("Connection disconnected: id={}, err={}", this.session.getId(), closeReason);
CHANNELS.remove(this.session.getId());
}
@OnError
public void onError(Throwable throwable) throws IOException {
log.info("Connection Error: id={}, err={}", this.session.getId(), throwable);
this.session.close(new CloseReason(CloseCodes.UNEXPECTED_CONDITION, throwable.getMessage()));
}
/**
* Push messages to all clients
*
* @param message
*/
public static void push(Object message) {
CHANNELS.values().stream().forEach(endpoint -> {
if (endpoint.session.isOpen()) {
endpoint.session.getAsyncRemote().sendObject(message);
}
});
}
}
|
WebSocketConfiguration
Configure the custom WebSocket endpoint into ServerEndpointExporter
to enable it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package io.springcloud.demo.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import io.springcloud.demo.channel.LogChannel;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
ServerEndpointExporter serverEndpointExporter = new ServerEndpointExporter();
/**
* Add one or more classes annotated with `@ServerEndpoint`.
*/
serverEndpointExporter.setAnnotatedEndpointClasses(LogChannel.class);
return serverEndpointExporter;
}
}
|
2. Configuring Logback
spring boot uses logback as the logging framework by default. So there is no need to add additional dependencies.
WebSocketAppender
Customize a logback Appender implementation. Its main function is to push logs to the client via WebSocket.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package io.springcloud.demo.logback;
import java.nio.charset.StandardCharsets;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import io.springcloud.demo.channel.LogChannel;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class WebSocketAppender extends AppenderBase<ILoggingEvent> {
// encoder is required. And it has to have legal getter/setter methods.
private PatternLayoutEncoder encoder;
@Override
protected void append(ILoggingEvent eventObject) {
// Use encoder to encode logs.
byte[] data = this.encoder.encode(eventObject);
// Push to client.
LogChannel.push(new String(data, StandardCharsets.UTF_8));
}
}
|
application.yaml
Specify the Logback configuration file via logging.config
.
1
2
3
4
5
6
|
server:
port: 80
# xml configuration file for logback
logging:
config: classpath:logback-spring.xml
|
logback-spring.xml
Add a custom appender
implementation to the configuration file. and use it in the root
node.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<configuration>
<!-- Use some logging configuration predefined by spring boot. For example: formatting pattern of logs. -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- Console -->
<appender name="console"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- WebSocket -->
<appender name="websocket"
class="io.springcloud.demo.logback.WebSocketAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="console" />
<appender-ref ref="websocket" /> <!-- The logs will be output to the WebSocketAppender -->
</root>
</configuration>
|
3. Client
index.html
This is a very rudimentary client. It will connect to the logging endpoint using a websocket, and will output to the browser console after receiving log messages pushed from the server in real time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>app log</title>
</head>
<body>
Please check the live log of the application in the console.
<script type="text/javascript">
const websocket = new WebSocket("ws://localhost/channel/log");
websocket.onclose = e => {
console.log(`conn closed: code=${e.code}, reason=${e.reason}, wasClean=${e.wasClean}`)
}
websocket.onmessage = e => {
console.log(`${e.data}`);
}
websocket.onerror = e => {
console.log(`conn err`)
console.error(e)
}
websocket.onopen = e => {
console.log(`conn open: ${e}`);
}
</script>
</body>
</html>
|
Testing
After starting the application, use your browser to access the http://localhost
address and open the browser console. Wait for the server log output.
1
2
|
conn open: [object Event]
2022-03-15 13:22:16.976 INFO 13972 --- [XNIO-1 task-1] io.springcloud.demo.channel.LogChannel : New client connection: id=7jrVhs5OZyDEaXkSCOEDE8lSLQFktzaMrkJh88pd
|
Open another browser (or another tab) at this point. Visit the http://localhost/404
address. Then you will see the exception log message output from the browser console in the previous step.
1
2
3
4
5
6
7
8
9
10
11
|
2022-03-15 13:26:41.111 DEBUG 13972 --- [ XNIO-1 task-1] io.undertow.request.security : Attempting to authenticate /404, authentication required: false
2022-03-15 13:26:41.111 DEBUG 13972 --- [ XNIO-1 task-1] io.undertow.request.security : Authentication outcome was NOT_ATTEMPTED with method io.undertow.security.impl.CachedAuthenticatedSessionMechanism@b1b72d4 for /404
2022-03-15 13:26:41.111 DEBUG 13972 --- [ XNIO-1 task-1] io.undertow.request.security : Authentication result was ATTEMPTED for /404
2022-03-15 13:26:41.112 DEBUG 13972 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : GET "/404", parameters={}
2022-03-15 13:26:41.113 DEBUG 13972 --- [ XNIO-1 task-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
2022-03-15 13:26:41.114 DEBUG 13972 --- [ XNIO-1 task-1] o.s.w.s.r.ResourceHttpRequestHandler : Resource not found
2022-03-15 13:26:41.114 DEBUG 13972 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND
2022-03-15 13:26:41.114 DEBUG 13972 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error", parameters={}
2022-03-15 13:26:41.115 DEBUG 13972 --- [ XNIO-1 task-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
2022-03-15 13:26:41.117 DEBUG 13972 --- [ XNIO-1 task-1] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, text/html;q=0.8]
2022-03-15 13:26:41.117 DEBUG 13972 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 404
|
The next thing to do is to develop a nice client.