Skip to content

Reactor Netty 访问日志

在 Spring Cloud Gateway 中,访问日志记录了客户端请求的详细信息,这对于监控、审计和故障排查都非常重要。Reactor Netty 提供了内置的访问日志功能,可以帮助我们追踪 API 网关的请求流量。

什么是 Reactor Netty 访问日志

Reactor Netty 访问日志是基于 Netty 的非阻塞 HTTP 服务器的日志记录功能。它记录了每个 HTTP 请求的详细信息,包括:

  • 客户端 IP 地址
  • 请求方法和 URL
  • 响应状态码
  • 响应时间
  • 请求和响应的字节数

业务场景

在实际的微服务架构中,访问日志通常用于以下场景:

  1. 性能监控:通过分析响应时间,识别性能瓶颈
  2. 安全审计:记录可疑的访问行为,如大量 404 错误或异常 IP
  3. 业务分析:统计 API 调用频率,了解业务使用情况
  4. 故障排查:在出现问题时,快速定位问题请求

启用 Reactor Netty 访问日志

要启用 Reactor Netty 访问日志,必须设置 Java 系统属性,而不是 Spring Boot 配置属性。

方式一:JVM 启动参数

kotlin
// 在应用启动时添加 JVM 参数
// -Dreactor.netty.http.server.accessLogEnabled=true

方式二:代码中设置系统属性

kotlin
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class GatewayApplication

fun main(args: Array<String>) {
    // 在应用启动前设置系统属性
    System.setProperty("reactor.netty.http.server.accessLogEnabled", "true")
    SpringApplication.run(GatewayApplication::class.java, *args)
}

方式三:Docker 环境变量

yaml
# docker-compose.yml
version: "3.8"
services:
  gateway:
    image: your-gateway-app:latest
    environment:
      - JAVA_OPTS=-Dreactor.netty.http.server.accessLogEnabled=true
    ports:
      - "8080:8080"

配置访问日志输出

默认情况下,访问日志会输出到控制台。在生产环境中,我们通常需要将访问日志单独输出到文件中,便于日志收集和分析。

Logback 配置示例

创建或修改 logback-spring.xml 配置文件:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 访问日志文件 Appender -->
    <appender name="accessLog" class="ch.qos.logback.core.FileAppender">
        <file>logs/access_log.log</file>
        <encoder>
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>

    <!-- 异步 Appender 提高性能 -->
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="accessLog" />
        <queueSize>1024</queueSize>
        <discardingThreshold>0</discardingThreshold>
    </appender>

    <!-- Reactor Netty 访问日志专用 Logger -->
    <logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
        <appender-ref ref="async"/>
    </logger>

    <!-- 应用日志配置 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="console"/>
    </root>
</configuration>

日志滚动配置

对于生产环境,建议使用滚动日志以避免单个日志文件过大:

xml
<appender name="accessLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/access_log.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 按天滚动 -->
        <fileNamePattern>logs/access_log.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <!-- 保留30天的日志 -->
        <maxHistory>30</maxHistory>
        <!-- 单个文件最大100MB -->
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
    <encoder>
        <pattern>%msg%n</pattern>
    </encoder>
</appender>

访问日志格式详解

Reactor Netty 的访问日志使用类似 Apache 通用日志格式:

127.0.0.1 - - [08/Jun/2025:10:30:15 +0800] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"

各字段含义:

  • 127.0.0.1:客户端 IP 地址
  • -:远程用户标识(通常为 -
  • -:认证用户名(通常为 -
  • [08/Jun/2025:10:30:15 +0800]:请求时间戳
  • "GET /api/users HTTP/1.1":请求方法、URI 和协议版本
  • 200:HTTP 响应状态码
  • 1234:响应字节数
  • "-":引用页面(Referer)
  • "Mozilla/5.0":用户代理字符串

完整的 Gateway 应用示例

以下是一个完整的 Spring Cloud Gateway 应用示例,演示如何启用和配置访问日志:

kotlin
package com.example.gateway

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.context.annotation.Bean

@SpringBootApplication
class GatewayApplication {

    /**
     * 配置路由规则
     */
    @Bean
    fun customRouteLocator(builder: RouteLocatorBuilder): RouteLocator {
        return builder.routes()
            .route("user-service") { r ->
                r.path("/api/users/**")
                    .uri("http://localhost:8081")
            }
            .route("order-service") { r ->
                r.path("/api/orders/**")
                    .uri("http://localhost:8082")
            }
            .build()
    }
}

fun main(args: Array<String>) {
    // 启用 Reactor Netty 访问日志
    System.setProperty("reactor.netty.http.server.accessLogEnabled", "true")

    SpringApplication.run(GatewayApplication::class.java, *args)
}
java
package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class GatewayApplication {

    /**
     * 配置路由规则
     */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r ->
                r.path("/api/users/**")
                    .uri("http://localhost:8081")
            )
            .route("order-service", r ->
                r.path("/api/orders/**")
                    .uri("http://localhost:8082")
            )
            .build();
    }

    public static void main(String[] args) {
        // 启用 Reactor Netty 访问日志
        System.setProperty("reactor.netty.http.server.accessLogEnabled", "true");

        SpringApplication.run(GatewayApplication.class, args);
    }
}

性能考虑和最佳实践

1. 使用异步 Appender

访问日志的写入操作应该是异步的,避免阻塞请求处理线程。

xml
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="accessLog" />
    <!-- 队列大小,根据实际负载调整 -->
    <queueSize>1024</queueSize>
    <!-- 不丢弃任何日志 -->
    <discardingThreshold>0</discardingThreshold>
    <!-- 最大刷新时间 -->
    <maxFlushTime>5000</maxFlushTime>
</appender>

2. 合理的日志轮转策略

xml
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>logs/access_log.%d{yyyy-MM-dd-HH}.log</fileNamePattern>
    <!-- 保留7天的日志 -->
    <maxHistory>168</maxHistory>
    <!-- 总大小限制 -->
    <totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>

3. 监控和告警

在生产环境中,建议结合日志监控工具:

kotlin
@Component
class AccessLogMonitor {

    private val errorCount = AtomicLong(0)
    private val requestCount = AtomicLong(0)

    @EventListener
    fun handleAccessLog(event: AccessLogEvent) {
        requestCount.incrementAndGet()

        if (event.status >= 400) {
            errorCount.incrementAndGet()
        }

        // 错误率过高时发送告警
        if (errorCount.get() > requestCount.get() * 0.1) {
            sendAlert("Error rate too high: ${errorCount.get()}/${requestCount.get()}")
        }
    }

    private fun sendAlert(message: String) {
        // 发送告警通知
        println("ALERT: $message")
    }
}

与 ELK Stack 集成

在微服务环境中,通常使用 ELK (Elasticsearch, Logstash, Kibana) 栈来收集和分析访问日志:

yaml
# logstash 配置示例
input { file { path => "/var/log/gateway/access_log.log" start_position => "beginning" } }

filter { grok { match => { "message" => "%{IPORHOST:client_ip} - - \[%{HTTPDATE:timestamp}\] \"%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:status} %{NUMBER:bytes} \"%{DATA:referer}\" \"%{DATA:user_agent}\"" } }

date { match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ] } }

output { elasticsearch { hosts => ["elasticsearch:9200"] index => "gateway-access-logs-%{+YYYY.MM.dd}" } }

常见问题和解决方案

问题 1:访问日志没有输出

确保设置的是 Java 系统属性,而不是 Spring Boot 配置属性。

kotlin
// ❌ 错误的方式 - 在 application.yml 中配置
// reactor:
//   netty:
//     http:
//       server:
//         access-log-enabled: true

// ✅ 正确的方式 - 设置系统属性
System.setProperty("reactor.netty.http.server.accessLogEnabled", "true")

问题 2:日志格式不符合预期

访问日志格式是固定的,不能自定义。如果需要特定格式,可以通过自定义过滤器实现:

kotlin
@Component
class CustomAccessLogFilter : GlobalFilter, Ordered {

    private val logger = LoggerFactory.getLogger("custom.access.log")

    override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
        val startTime = System.currentTimeMillis()

        return chain.filter(exchange).doFinally {
            val request = exchange.request
            val response = exchange.response
            val duration = System.currentTimeMillis() - startTime

            // 自定义日志格式
            val logMessage = buildString {
                append("${request.remoteAddress?.address?.hostAddress} ")
                append("${request.method} ")
                append("${request.uri} ")
                append("${response.statusCode?.value()} ")
                append("${duration}ms")
            }

            logger.info(logMessage)
        }
    }

    override fun getOrder(): Int = Ordered.LOWEST_PRECEDENCE
}

总结

Reactor Netty 访问日志是 Spring Cloud Gateway 中重要的监控和审计功能。通过合理的配置和使用,可以帮助我们:

  1. 监控系统性能:及时发现性能问题
  2. 提升安全性:记录和分析访问行为
  3. 支持故障排查:快速定位问题根源
  4. 数据驱动决策:基于访问统计优化系统

在生产环境中,务必注意日志文件的大小和轮转策略,避免磁盘空间耗尽。同时,使用异步日志写入以确保不影响请求处理性能。