logback1.3.x配置详解与实践

共 495字,需浏览 1分钟

 ·

2022-02-14 21:16

前提

当前(2022-02前后)日志框架logback的最新版本1.3.0已经更新到1.3.0-alpha14版本,此版本为非stable版本,相对于最新稳定版1.2.10来说,虽然slf4j-api版本升级了,但使用的API大体不变,对于XML配置来看提供了import标签对于多appender来说可以简化配置。鉴于软件最新版本强迫症,这里基于1.3.0-alpha14版本分析一下常用的logback配置项以及一些实践经验。

日志等级

日志等级的定义见Level类:

序号日志级别备注
1OFFInteger.MAX_VALUE关闭日志打印
2TRACE5000-
3DEBUG10000-
4INFO20000-
5WARN30000-
6ERROR40000-
7ALLInteger.MIN_VALUE打印所有日志

日志等级的值越大,级别越高,级别由低到高(左到右)排列如下:

TRACE < DEBUG < INFO < WARN < ERROR

日志等级一般会作为日志事件的过滤条件或者查询条件,在一些特定组件中,可以通过配置项去决定丢弃低级别的日志事件或者忽略指定级别的日志事件。

依赖引入

因为当前的1.3.0-alpha14版本太过"新",大部分主流框架尚未集成,如果要尝鲜最好通过BOM全局指定对应依赖的版本:


<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
            <version>2.0.0-alpha6version>
        dependency>
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-classicartifactId>
            <version>1.3.0-alpha14version>
        dependency>
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-coreartifactId>
            <version>1.3.0-alpha14version>
        dependency>
    dependencies>
dependencyManagement>


<dependencies>
    <dependency>
        <groupId>org.slf4jgroupId>
        <artifactId>slf4j-apiartifactId>
    dependency>
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-coreartifactId>
    dependency>
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
    dependency>
dependencies>

logback.xml基本配置示例

1.2.x1.3.x提供的API基本没有改变,并且1.3.x向前兼容了旧的配置方式,提供了import标签用于简化class的指定:

  • 1.2.x(旧的配置方式)前的配置方式:

<configuration debug="false">

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
    appender>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT"/>
    root>
configuration>
  • 1.3.x可用的新配置方式:

<configuration debug="false">

    <import class="ch.qos.logback.core.ConsoleAppender"/>
    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>

    <appender name="STDOUT" class="ConsoleAppender">
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
    appender>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT"/>
    root>
configuration>

对于单个Appender配置来看,import标签的引入看起来无法简化配置,但是对于多Appender配置来看可以相对简化class的指定,例如:


<configuration debug="false">
    <property name="app" value="api-gateway"/>
    <property name="filename" value="server"/>

    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
    <import class="ch.qos.logback.core.rolling.RollingFileAppender"/>
    <import class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"/>
    <import class="ch.qos.logback.core.ConsoleAppender"/>
    <import class="ch.qos.logback.classic.AsyncAppender"/>
    <import class="ch.qos.logback.classic.filter.ThresholdFilter"/>
    <import class="cn.vlts.logback.IncludeLevelSetFilter"/>

    <appender name="INFO" class="RollingFileAppender">
        <file>/data/log-center/${app}/${filename}.logfile>
        <rollingPolicy class="TimeBasedRollingPolicy">
            <fileNamePattern>/data/log-center/${app}/${filename}.%d{yyyy-MM-dd}.logfileNamePattern>
            <maxHistory>14maxHistory>
        rollingPolicy>
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%npattern>
        encoder>
        <filter class="IncludeLevelSetFilter">
            <levels>INFO,WARNlevels>
            <onMatch>ACCEPTonMatch>
            <onMismatch>DENYonMismatch>
        filter>
    appender>

    <appender name="ERROR" class="RollingFileAppender">
        <file>/data/log-center/${app}/${filename}-error.logfile>
        <rollingPolicy class="TimeBasedRollingPolicy">
            <fileNamePattern>/data/log-center/${app}/${filename}-error.%d{yyyy-MM-dd}.logfileNamePattern>
            <maxHistory>14maxHistory>
        rollingPolicy>
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%npattern>
        encoder>
        <filter class="ThresholdFilter">
            <level>ERRORlevel>
        filter>
    appender>

    <appender name="STDOUT" class="ConsoleAppender">
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
         <filter class="ThresholdFilter">
            <level>DEBUGlevel>
        filter>
    appender>

    <appender name="ASYNC_INFO" class="AsyncAppender">
        <queueSize>1024queueSize>
        <discardingThreshold>0discardingThreshold>
        <appender-ref ref="INFO"/>
    appender>

    <appender name="ASYNC_ERROR" class="AsyncAppender">
        <queueSize>256queueSize>
        <discardingThreshold>0discardingThreshold>
        <appender-ref ref="ERROR"/>
    appender>
    
    <logger name="sun.rmi" level="error"/>
    <logger name="sun.net" level="error"/>
    <logger name="javax.management" level="error"/>
    <logger name="org.redisson" level="warn"/>
    <logger name="com.zaxxer" level="warn"/>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="ASYNC_INFO"/>
        <appender-ref ref="ASYNC_ERROR"/>
    root>
configuration>

上面的配置是某个API网关的logback.xml配置示例,这里用到了一个自定义Filter实现IncludeLevelSetFilter

// cn.vlts.logback.IncludeLevelSetFilter
public class IncludeLevelSetFilter extends AbstractMatcherFilter<ILoggingEvent{

    private String levels;

    private Set levelSet;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        return levelSet.contains(event.getLevel()) ? onMatch : onMismatch;
    }

    public void setLevels(String levels) {
        this.levels = levels;
        this.levelSet = Arrays.stream(levels.split(","))
                .map(item -> Level.toLevel(item, Level.INFO)).collect(Collectors.toSet());
    }

    @Override
    public void start() {
        if (Objects.nonNull(this.levels)) {
            super.start();
        }
    }
}

IncludeLevelSetFilter用于接受指定日志级别集合的日志记录,如果有更加精细的日志过滤条件(内置常用的LevelFilterThresholdFilter等无法满足实际需求),可以自行实现ch.qos.logback.core.filter.Filter接口定制日志事件过滤策略。这份文件定义了五个appender,其中有2个用于异步增强,核心appender3个:

  • STDOUTConsoleAppender,标准输出同步日志打印,级别为DEBUG或以上
  • ASYNC_INFOINFO):RollingFileAppender,异步滚动文件追加日志打印,级别为INFO或者WARN,追加到文件/data/log-center/api-gateway/server.log,归档文件格式为/data/log-center/api-gateway/server-${yyyy-MM-dd}.log.${compression_suffix},归档文件最多保存14个副本
  • ASYNC_ERRORERROR):RollingFileAppender,异步滚动文件追加日志打印,级别为ERROR,追加到文件/data/log-center/api-gateway/server-error.log,归档文件格式为/data/log-center/api-gateway/server-error-${yyyy-MM-dd}.log.${compression_suffix},归档文件最多保存14个副本

常用的Appender及其参数

常用的Appender有:

  • ConsoleAppender
  • FileAppender
  • RollingFileAppender
  • AsyncAppender

其中,RollingFileAppenderFileAppender的扩展(子类),现实场景中ConsoleAppenderRollingFileAppender的适用范围更广。从类继承关系上看,ConsoleAppenderFileAppender都支持定义Encoder,最常用的Encoder实现就是PatternLayoutEncoder,用于定制日志事件的最终输出格式。「关于Encoder,由于其参数格式太过灵活,参数众多,限于篇幅本文不会展开介绍」

ConsoleAppender

ConsoleAppender用于追加日志到控制台,对于Java应用来说就是追加到System.out或者System.errConsoleAppender支持的参数如下:

参数类型默认值描述
encoderch.qos.logback.core.encoder.EncoderPatternLayoutEncoder用于定义Encoder
targetStringSystem.out定义输出目标,可选值System.outSystem.err
withJansibooleanfalse是否支持Jansi,这是一个支持多彩ANSI编码的类库,用于输出彩色控制台字体

ConsoleAppender的使用例子如下:


<configuration debug="false">

    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
    <import class="ch.qos.logback.core.ConsoleAppender"/>

    <appender name="STDOUT" class="ConsoleAppender">
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
    appender>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT"/>
    root>
configuration>

RollingFileAppender

RollingFileAppenderFileAppender的子类,支持输出日志到文件中,并且支持通过滚动规则(RollingPolicy)的设置,可以安装内置或者自定义规则去分割、归档日志文件。RollingFileAppender支持的参数如下:

参数类型默认值描述
fileString-用于定义当前日志输出的目标文件
appendbooleantrue用于定义当前日志输出是否追加模式
rollingPolicych.qos.logback.core.rolling.RollingPolicy-日志文件滚动策略
triggeringPolicych.qos.logback.core.rolling.TriggeringPolicy-日志文件滚动时机触发策略
prudentbooleanfalse是否支持prudent模式(开启此模式会在FileLock保护下写入日志文件),FileAppender支持此模式

常用的RollingPolicy内置实现有:

  • TimeBasedRollingPolicy:最常用的日志滚动策略,基于日期时间进行滚动分割和归档
参数类型默认值描述
fileNamePatternString-文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.log.gz
maxHistoryint-最大归档文件数量
totalSizeCapFileSize-所有归档文件总大小的上限
cleanHistoryOnStartbooleanfalse标记为trueAppender启动时候清理(不合法的)归档日志文件
  • SizeAndTimeBasedRollingPolicy:基于日志文件大小或者日期时间进行滚动分割和归档
参数类型默认值描述
fileNamePatternString-文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.%i.log.gz
maxHistoryint-最大归档文件数量
totalSizeCapFileSize-所有归档文件总大小的上限
cleanHistoryOnStartbooleanfalse标记为trueAppender启动时候清理(不合法的)归档日志文件
  • FixedWindowRollingPolicy:基于日志文件大小或者日期时间进行滚动分割和归档
参数类型默认值描述
fileNamePatternString-文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.log.gz
minIndexint-窗口索引下界
maxIndexint-窗口索引上界

常用的TriggeringPolicy内置实现有:

  • SizeBasedTriggeringPolicy:基于文件大小的触发策略
  • DefaultTimeBasedFileNamingAndTriggeringPolicylogback内部使用):基于日期时间和文件名通过判断系统日期时间触发

这里值得注意的几点:

  • TimeBasedRollingPolicy自身也实现了TriggeringPolicy接口(委托到DefaultTimeBasedFileNamingAndTriggeringPolicy中执行),提供了兜底的日志文件滚动时机触发策略,所以在使用TimeBasedRollingPolicy的时候可以不需要指定具体的triggeringPolicy实例
  • SizeAndTimeBasedRollingPolicy使用了子组件SizeAndTimeBasedFNATP实现,旧版本一般使用SizeAndTimeBasedFNATP实现基于文件大小或者日期时间进行日志滚动归档功能,此组件在新版本中建议使用SizeAndTimeBasedRollingPolicy替代
  • logback会基于参数fileNamePattern中定义的文件名后缀去选择对应的归档日志文件压缩算法,例如.zip会选用ZIP压缩算法,.gz会选用GZIP压缩算法
  • SizeAndTimeBasedRollingPolicyFixedWindowRollingPolicyfileNamePattern参数都支持%i占位符,用于定义归档文件的索引值,其实索引为0
  • FixedWindowRollingPolicySizeBasedTriggeringPolicy组合使用可以实现基于文件大小进行日志滚动的功能(TimeBasedRollingPolicy的对标功能)

AsyncAppender

AsyncAppender用于异步记录日志,需要搭配其他类型的Appender使用,直观上看就是把"异步"功能赋予其他Appender实例。AsyncAppender支持的参数如下:

参数类型默认值描述
queueSizeint256存放日志事件的阻塞队列的最大容量
discardingThresholdintqueueSize / 5日志事件丢弃阈值,阻塞队列剩余容量小于此阈值,会丢弃除了WARNERROR级别的其他所有级别的日志事件,此阈值设置为0相当于不会丢弃任意日志事件
includeCallerDatabooleanfalse日志事件中是否包含调用者数据,设置为true会添加调用线程信息、MDC中的数据等
maxFlushTimeint1000异步日志写入工作线程退出的最大等待时间,单位为毫秒
neverBlockbooleanfalse是否永不阻塞(当前应用的调用线程),设置为true的时候队列满了会直接丢弃当前新添加的日志事件

需要通过标签关联一个已经存在的Appender实例到一个全新的AsyncAppender实例中,并且一个AsyncAppender实例是可以基于多个标签添加多个Appender实例,例如:


<configuration debug="false">

    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
    <import class="ch.qos.logback.core.ConsoleAppender"/>
    <import class="ch.qos.logback.classic.AsyncAppender"/>

    <appender name="STDOUT" class="ConsoleAppender">
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
    appender>

    <appender name="ASYNC_STDOUT" class="AsyncAppender">
        <queueSize>1024queueSize>
        <discardingThreshold>0discardingThreshold>
        <appender-ref ref="STDOUT"/>
        
    appender>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="ASYNC_STDOUT"/>
    root>
configuration>

指定配置文件进行初始化

logback内置的初始化策略(按照优先级顺序)如下:

  • 通过ClassPath中的logback-test.xml文件初始化
  • 通过ClassPath中的logback.xml文件初始化
  • 通过SPI的方式由ClassPath中的META-INF\services\ch.qos.logback.classic.spi.Configurator进行初始化
  • 如果前面三步都没有配置,则通过BasicConfigurator初始化,提供最基础的日志处理功能

可以通过命令行参数logback.configurationFile直接指定外部的logback配置文件(后缀必须为.xml或者.groovy),这种初始化方式会忽略内置的初始化策略,例如:

java -Dlogback.configurationFile=/path/conf/config.xml app.jar

或者设置系统参数(下面的Demo来自官方例子):

public class ServerMain {
    public static void main(String args[]) throws IOException, InterruptedException {
       // must be set before the first call to LoggerFactory.getLogger();
       // ContextInitializer.CONFIG_FILE_PROPERTY is set to "logback.configurationFile"
       System.setProperty(ContextInitializer.CONFIG_FILE_PROPERTY, "/path/to/config.xml");
       ...
    }   
}

这种方式要求尽量不能存在静态成员变量调用了LoggerFactory.getLogger()方法,因为有可能会导致提前使用内置的初始化策略进行初始化。

编程式初始化

为了完全控制logback的初始化,可以使用纯编程式进行设置(下面的编程式配置按照"最佳实践"中的配置文件进行编写):

import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.ThresholdFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import org.slf4j.LoggerFactory;

/**
 * @author throwable
 * @version v1
 * @description
 * @since 2022/2/13 13:09
 */

public class LogbackLauncher {

    public static void main(String[] args) throws Exception {
        LoggerContext loggerContext = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
        loggerContext.reset();
        Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
        // 移除所有Appender
        rootLogger.detachAndStopAllAppenders();
        // RollingFileAppender
        PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
        fileEncoder.setContext(loggerContext);
        fileEncoder.setPattern("[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%n");
        fileEncoder.start();
        RollingFileAppender fileAppender = new RollingFileAppender<>();
        fileAppender.setContext(loggerContext);
        fileAppender.setName("FILE");
        fileAppender.setFile("/data/log-center/api-gateway/server.log");
        fileAppender.setAppend(true);
        fileAppender.setEncoder(fileEncoder);
        ThresholdFilter fileFilter = new ThresholdFilter();
        fileFilter.setLevel("INFO");
        fileAppender.addFilter(fileFilter);
        TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy<>();
        rollingPolicy.setParent(fileAppender);
        rollingPolicy.setContext(loggerContext);
        rollingPolicy.setFileNamePattern("/data/log-center/api-gateway/server.%d{yyyy-MM-dd}.log.gz");
        rollingPolicy.setMaxHistory(14);
        rollingPolicy.start();
        fileAppender.setRollingPolicy(rollingPolicy);
        fileAppender.start();
        AsyncAppender asyncAppender = new AsyncAppender();
        asyncAppender.setName("ASYNC_FILE");
        asyncAppender.setContext(loggerContext);
        asyncAppender.setDiscardingThreshold(0);
        asyncAppender.setQueueSize(1024);
        asyncAppender.addAppender(fileAppender);
        asyncAppender.start();
        // ConsoleAppender
        PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
        consoleEncoder.setContext(loggerContext);
        consoleEncoder.setPattern("[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n");
        consoleEncoder.start();
        ConsoleAppender consoleAppender = new ConsoleAppender<>();
        consoleAppender.setContext(loggerContext);
        consoleAppender.setEncoder(consoleEncoder);
        ThresholdFilter consoleFilter = new ThresholdFilter();
        consoleFilter.setLevel("DEBUG");
        consoleAppender.addFilter(consoleFilter);
        consoleAppender.start();
        rootLogger.setLevel(Level.DEBUG);
        rootLogger.setAdditive(false);
        rootLogger.addAppender(consoleAppender);
        rootLogger.addAppender(asyncAppender);

        org.slf4j.Logger logger = LoggerFactory.getLogger(LogbackDemo1.class);
        logger.debug("debug nano => {}", System.nanoTime());
        logger.info("info nano => {}", System.nanoTime());
        logger.warn("warn nano => {}", System.nanoTime());
        logger.error("error nano => {}", System.nanoTime());
    }
}

最佳实践

实践中建议使用logback文档中提到的最常用的:RollingFileAppender + TimeBasedRollingPolicy + ConsoleAppender(这个是为了方便本地开发调试)组合。一般来说,日志文件最终会通过Filebeat等日志收集组件上传到ELK体系,在合理定义日志的输出格式(例如在输出格式中指定Level参数)前提下,其实可以不拆分不同级别的日志文件并且输出所有INFO或者以上级别的日志,最终在Kibana中也可以轻易通过参数level: ${LEVEL}进行不同级别的日志查询。而对于「性能要求比较高」的服务例如API网关,建议把RollingFileAppender关联到AsyncAppender实例中,在内存足够的前提下调大queueSize参数并且设置discardingThreshold = 0(队列满了不丢弃日志事件,有可能会阻塞调用线程,无法忍受可以自行扩展异步日志功能)。在服务器磁盘充足的前提下,一般对于归档日志的文件大小不设置上限,只设置最大归档文件数量,建议数量为14 ~ 30(也就是2周到1个月之间)。下面是一个模板:


<configuration debug="false">
    <property name="app" value="应用名,例如api-gateway"/>
    <property name="filename" value="文件名前缀,例如server"/>

    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
    <import class="ch.qos.logback.core.rolling.RollingFileAppender"/>
    <import class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"/>
    <import class="ch.qos.logback.core.ConsoleAppender"/>
    <import class="ch.qos.logback.classic.AsyncAppender"/>
    <import class="ch.qos.logback.classic.filter.ThresholdFilter"/>

    <appender name="FILE" class="RollingFileAppender">
        <file>/data/log-center/${app}/${filename}.logfile>
        <rollingPolicy class="TimeBasedRollingPolicy">
            <fileNamePattern>/data/log-center/${app}/${filename}.%d{yyyy-MM-dd}.logfileNamePattern>
            <maxHistory>14maxHistory>
        rollingPolicy>
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%npattern>
        encoder>
        <filter class="ThresholdFilter">
            <level>INFOlevel>
        filter>
    appender>

    <appender name="STDOUT" class="ConsoleAppender">
        <encoder class="PatternLayoutEncoder">
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%npattern>
        encoder>
        <filter class="ThresholdFilter">
            <level>DEBUGlevel>
        filter>
    appender>

    <appender name="ASYNC_FILE" class="AsyncAppender">
        <queueSize>1024queueSize>
        
        <discardingThreshold>0discardingThreshold>
        <appender-ref ref="FILE"/>
    appender>

    
    <logger name="sun.rmi" level="error"/>
    <logger name="sun.net" level="error"/>
    <logger name="javax.management" level="error"/>
    <logger name="org.redisson" level="warn"/>
    <logger name="com.zaxxer" level="warn"/>

    <root level="DEBUG" additivity="false">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="ASYNC_FILE"/>
    root>
configuration>

小结

这篇文章仅仅介绍logback最新版本的一些基本配置和实践经验,也作为一篇日后随时可以拿起使用的流水账笔记存档。

参考资料:

  • The logback manual

(本文完 c-2-d e-a-20220212 这一两个月的基金有点可怕)

浏览 45
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报