springboot integrates netty and sleuth with MDC to generate traceId

Posted by twostars on Mon, 29 Nov 2021 00:35:51 +0100

springboot integrates netty, uses slf4j's MDC to generate a traceId for link tracking, and sleuth works together [sleuth and MDC can choose not to use, so there is no need to write aop and annotations, and no need to introduce aop and sleuth].

1: The jar you need to depend on (choose according to your needs)

 <!--    version management        -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.5.RELEASE</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
      <dependencies>
        <!--    Link Tracking    -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <!--    netty    -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
		<!--    aop    -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

2: Write a netty service

There are two springboot initializations to start nettyserver, here you choose to implement the ApplicationRunner, and there are many ways to refer to the online tutorial

@Slf4j
@Component
public class NettyServer implements ApplicationRunner {

    private final NettyChannelHandler nettyChannelHandler;
    

    public NettyServer(NettyChannelHandler nettyChannelHandler) {
        this.nettyChannelHandler = nettyChannelHandler
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        NioEventLoopGroup boss = new NioEventLoopGroup(1);
        // Calculate default based on CPU
        NioEventLoopGroup work = new NioEventLoopGroup();
        ServerBootstrap sb = new ServerBootstrap();
        sb.group(boss, work);
        sb.channel(NioServerSocketChannel.class);
        sb.childHandler(nettyChannelHandler);
        // Here you can configure to yml and get
        ChannelFuture future = sb.bind(80);
        future.sync();
        log.info("spring start-up netty..................");
    }
}

3: Write a server handler

You only need to implement ChannelHandler classes here, but ChannelInitializer<>is generally implemented

@Component
public class NettyChannelHandler extends ChannelInitializer<NioSocketChannel> {

    private final ServerInboundHandler serverInboundHandler;

    public NettyChannelHandler(ServerInboundHandler serverInboundHandler) {
        this.serverInboundHandler = serverInboundHandler;
    }

    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        // Identify line breaks as a message [\r\n, if the client sends a message without carrying it, the server will not be able to identify a message]]
		//ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
        ch.pipeline().addLast(new LoggingHandler());
        ch.pipeline().addLast(serverInboundHandler);
    }
}

4: Write an inbound processor

The general choice is to inherit the ChannelInboundHandlerAdapter class, which has some default processing logic
Note: here @ChannelHandler.Sharable cannot be missing, if it is missing, only one client can be accessed, because this inbound is bound to the client (personally understood), here CHANNEL_MAP's key is a short channelId string obtained for the ChannelId object, but of course you can get a long one.

@Slf4j
@Component
@ChannelHandler.Sharable
public class ServerInboundHandler extends ChannelInboundHandlerAdapter {
    private final MessageServer messageServer;

    public static Map<String, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

    public ServerInboundHandler(MessageServer messageServer) {
        this.messageServer = messageServer;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Client Initialization name:{},ip:{}", ctx.name(), ctx.channel().remoteAddress().toString());
        // You can get short ones here, but you can also get long ones
        String channelId = ctx.channel().id().asShortText();
        if (!CHANNEL_MAP.containsKey(channelId)) {
            CHANNEL_MAP.put(channelId, ctx);
        }
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("Client Disconnect name:{},ip:{}", ctx.name(), ctx.channel().remoteAddress().toString());
        String channelId = ctx.channel().id().asShortText();
        CHANNEL_MAP.remove(channelId);
        super.channelInactive(ctx);
    }

    @MDCLog(name = "Message Write Channel")
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        log.info("Received Message data:{}", ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
        // [0] Create a new buf for recovery
        ByteBuf respBuf = ctx.alloc().buffer();
        // [1] Business dealing with messages
        messageServer.handleMessage(buf);
        // [2] Messages can be handed over to other services for processing, and replies can be written [ignore if no response is required for mechanisms that require message responses]
        respBuf.writeBytes("Reply".getBytes(StandardCharsets.UTF_8));
        // Note: Don't use ctx's writeAndFlush, he doesn't start scanning from the end and can't write
        ctx.channel().writeAndFlush(respBuf);
        super.channelRead(ctx, msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("netty Server Exception");
        super.exceptionCaught(ctx, cause);
    }

    /**
     * <p>Used to notify all clients and to work with ctx context map </p>
     * @param channelId Channel id
     * @param msg news
     */
    @MDCLog(name = "write log")
    public void channelWrite(String channelId, String msg) {
        log.info("send message channelId:{} msg:{}", channelId, msg);
        ChannelHandlerContext ctx = CHANNEL_MAP.get(channelId);
        if (msg != null) {
            ctx.channel().writeAndFlush(msg.getBytes(StandardCharsets.UTF_8));
        }
    }
}

5: Write an entry point for notes

This step is mainly to solve the problem that when the netty channel writes messages, the log does not have traceId, the log can not be traced, the problem is not easy to locate this annotation name and has no substantial effect, waiting for business needs development

@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface MDCLog {
    String name();
}

6: Write Aspect Tangent Points

The reason why MDC.get("X-B3-TraceId") is not empty in Before is that when the server writes to the client, it is often written by other services that call the netty service interface. When walking through http calls, if sleuth is configured, the traceId will be generated automatically. The eventGroup thread s of neety only need to be used directly, then used up and deleted. [You can of course not delete it, see the notes below]

@Aspect
@Slf4j
@Component
public class MDCAspect {

    @Pointcut("@annotation(com.gyg.netty.aop.MDCLog)")
    public void annotationPointcut() {
    }

    @Before("annotationPointcut()")
    public void aspectBefore(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        MDCLog mdcLog = method.getAnnotation(MDCLog.class);
        String name = mdcLog.name();
        // [0] The last traceId will be printed here
        log.info("Get into before intercept name:{}", name);
        // Handle entry business [log traceId generation here], insert related key s according to log framework format
        // This is slf4j, key uses X-B3-TraceId, you can see the configuration in the log configuration
        // It's best to configure an After here to delete traceId
        // [Delete every time you use it, but because of the coverage problem, you can also not delete it. The last traceId will appear if you don't delete [0]
        if (StringUtils.isEmpty(MDC.get("X-B3-TraceId"))) {
            MDC.put("X-B3-TraceId", UUID.randomUUID().toString().replace("-", "").substring(0, 16));
        }
    }

    @After("annotationPointcut()")
    public void aspectAfter(JoinPoint joinPoint) {
        log.info("after trigger");
        MDC.clear();
    }

}

7: Test connection and write to server

Simulate a client using netassist tools

Download Address

8: Analog server send

Simply send one using MVC mode

@Slf4j
@RestController
@RequestMapping("/")
public class TestController {

    private final ServerInboundHandler serverInboundHandler;

    public TestController(ServerInboundHandler serverInboundHandler) {
        this.serverInboundHandler = serverInboundHandler;
    }

    @PostMapping
    public void write(@RequestParam String message, @RequestParam String channelId) {
        log.info("Write command received");
        serverInboundHandler.channelWrite(channelId, message);
    }
}

If you need to print netty's own log (the one for the byte stream), you can configure it in NettyChannelHandler as follows

    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        // Identify line breaks as a message [specific protocol needs to be specific to the client, common here]
        //  ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
        ch.pipeline().addLast(new LoggingHandler());
        ch.pipeline().addLast(serverInboundHandler);
        // Byte Stream Log
        ch.pipeline().addLast(new LoggingHandler());
    }

Source has been placed in github
Source Address

**

Thanks for watching

**

Topics: Netty Spring Boot eureka