支付系统中日志的具体落地,实现快速发现,快速报警,指标分析

日志规范 V1.0

适用范围:适用于通用服务日志(如nginx/redis/mysql日志)之外的所有线上落盘日志,包括服务端业务日志,BI日志,移动端APP日志。

实施范围:要求所有新业务模块以及深度重构的模块实施日志规范,鼓励现有业务模块通过日志组件替换或改造来实施标准日志规范

主要设计思路与折衷

  1. 引入数据标签DLTAG,便于日志分类与提取
  2. 日志内容使用格式化key/value,实现日志特定字段与数据的统一化解析
  3. 日志格式与BI日志保持兼容(双竖线分隔),实现所有业务日志规范的统一,同时最大程度降低BI日志及处理系统改造的成本
  4. 引入默认DLTAG(_undef)与默认key(_msg=)机制,无缝兼容老日志改造,降低非结构化业务日志的研发成本
  5. 引入Trace技术,融入日志规范要求,实现请求数据的跨服务自动关联,基于日志数据分析进行系统拓扑刻画

一、 日志基本要求

l 日志基础分类

  1. FATAL: 严重错误日志,若出现则代表模块需要退出,或者出现重大运行错误
  2. **[可选]**ERROR:普通错误日志,请求处理中出现非预期错误,但模块可以继续服务无需退出
  3. WARNING: 警告日志
  4. [可选] STRACE:引入特定日志级别, 记录trace信息同时配合采样逻辑。 放在warning和info日志等级间
  5. INFO(NOTICE): 系统通知日志,不包括单个请求的处理日志,例如系统的启动、停止、词表配置重载
  6. TRACE: 请求跟踪日志,只包括单个请求的处理和通知日志,例如请求的入口、出口、外部调用
  7. DEBUG: 代码调试日志

l 日志存储与拆分

  1. 当前写入的日志名必须固定,不允许增加变化的前缀或者后缀
  2. 日志可以根据量级按小时或者按天切分,切分格式建议追加时间后缀,如didi.log.2015122310
  3. 同类日志在所有机器上的部署路径必须保持一致
  4. 线上服务不允许开启DEBUG级别日志
  5. [可选] 日志文件以模块名为前缀,以"."分隔
  6. 错误日志以独立文件存储,建议拆分为:
    1. 模块名.log:记录INFO/TRACE/DEBUG日志
    2. 模块名.log.wf:记录FATAL/ERROR/WARNING日志

l 日志内容基础要求

  1. 单条日志不允许跨多行(Java和Python的异常信息除外)
  2. 单条日志最大长度不超过4096(待讨论确定),否则需要截断
  3. 日志中所有功能字符必须为英文字符,例如冒号、分号、逗号等
  4. 单条日志必须包含时间字段,建议精确到毫秒,带有时区,格式要求为: yyyy-MM-ddTHH:mm:ss.SSSZ,如"2015-12-23T15:19:25.089+0800"

l 不允许记录的日志

  1. 数据库:除数据库名外的所有连接信息。例数据库IP、数据库用户名、数据库密码
  2. 用户:除用户UID、用户名外的所有用户信息。例用户邮箱、用户密码等
  3. 第三方接口:与第三方接口之间的权限验证等信息。例TOKEN、密钥等

二、 日志格式要求

l 日志内容及格式要求

  1. 格式示例: [INFO][2015-12-02T00:00:07.099+0800][github.com/dataapi/handler.DataApi:212] dltag||key1=value1||key2=value2||key3=value3||……

  2. 格式说明:

    1. 单条日志分为三个部分:”固定日志头 数据标签DLTAG||KV字段列表“固定日志头DLTAG之间用空格( )进行区分、DLTAGKV列表之间用**双竖线(||)**分隔

    2. 固定日志头必须包括三个域:[日志级别][时间戳][扩展区域] ,且各个域中不允许出现空格

      • 日志级别包括FATAL,WARNING,STRACE,NOTICE,TRACE,DEBUG
      • 时间戳格式要求:yyyy-MM-ddTHH:mm:ss.SSSZ 带有毫秒,带有时区,日期与时间通过字母T分隔(eg: 2015-12-02T00:00:07.099+0800)
      • 扩展区域用户可以自行定义,大部分场景用来打印”代码行/类名/函数名“,建议的内部格式为: 全路径文件名:行号:类名:函数名(不做细节要求)。
    3. 数据标签DLTAG用来标识该条日志记录的内容以及日志中能获得的字段信息,与BI日志中的public前缀功能相同,请务必阅读**【DLTAG规范及内容定义】**,一些重要内容摘要如下:

      • 业务日志(非BI日志)中的DLTAG必须以下划线(_)为前缀,目的是为了与BI日志的数据标签做区分,防止重复
      • 公司级的DLTAG统一使用_com_为前缀,例如_com_request_in, _com_request_out, _com_http_success
      • 每个事业部可以设置自己的DLTAG用来做记录业务相关annotation,如:gs(专车),taxi(出租车),beatles(顺风车)
      • 每个DLTAG都需要有必选和可选的key列表以及value类型描述,事业部新增的DLTAG和其中的key的映射会被自动采样并记录到Schema-日志规范服务中心
      • DLTAG的默认值为_undef,没有具体含义的日志或者无需定义专门DLTAG的日志可以使用默认值
    4. KV字段列表

      中包含结构化的key与value数据

      1. 各个key/value组合以双竖线(||)分隔
      2. key与value之间使用等号(=)链接
      3. key/value的字段内容中需要避免出现双竖线,key的内容中需要避免出现等号,如果出现,可以通过日志组件进行自动转义
      4. key的默认值为_msg,没有具体含义的key可以使用默认值,一般搭配_undef的数据标签使用
      5. 相同含义的key要求使用相同的名称,例如order_id,driver_id,具体可见**【DLTAG规范及内容定义】**
  3. 目前抽取的公共DLTAG列表,要求所有模块必须输出这些日志

含义DLTAG基础必选项(注:不可缺少,必打出的key)建议可选key备注
请求入口_com_request_intraceid spanid uri (当前接口)argscallee的access日志①建议使用uri表达接口名称
请求出口_com_request_outtraceid spanid uri (当前接口)proc_timeerrnoresponseerrmsgcallee的access日志①proc_time建议缺省单位s,并且缺省单位。 如果不是建议方式,至少需要在一个模块内部保持一致。②建议使用uri表达接口名称
请求外部thrift接口_com_thrift_success/failuretraceid spanid cspanid uri (当前接口)interface (下游接口)proc_timeerrnohost(远程请求地址)porterrmsgcaller的调用外部thrift接口的日志,一般在请求拿到response后记录
请求外部http接口_com_http_success/failuretraceidspanidcspaniduri (当前接口) url (下游接口)proc_timeerrnohost(远程请求地址)porterrmsgcaller的调用外部http接口的日志,一般在请求拿到response后记录
请求外部mysql_com_mysql_success/failuretraceid****spaniduri (当前接口)method_name(select、update等)proc_timeerrnohost(远程请求地址)porttableerrmsgcaller的调用mysql服务的日志,一般在请求拿到response后记录
请求外部redis_com_redis_success/failuretraceid****spaniduri (当前接口)method_name(set、get等)proc_timeerrnohost(远程请求地址)porterrmsgcaller的调用redis服务的日志,一般在请求拿到response后记录
远程dubbo请求_com_dubbo_success_com_dubbo_failuretraceidspanidcspaniduri (当前接口) url (下游接口) proc_timeerrnohost(远程请求地址)porterrmsg①url 表达远程接口②uri 表达当前接口若A—>B则 url =B,uri =A
其他_undef_msgtraceid****spanid**① _undef不属于 _com型通用dltag② 需要与 _com型通用日志配合使用,并且正确使用traceid spanid,来记录系统行为。 **

l 日志兼容问题

  1. 业务日志中会有很多无需kv化的日志,多数只输出一个message,对于此类日志可以使用默认DLTAG和key来进行兼容,

    默认DLTAG=_undef。默认key为_msg

    1. 调整前: [INFO] [2015-12-02 00:00:07.099+0800][github.com/dataapi/handler.DataApi:212] params error. fail over
    2. 调整后: [INFO] [2015-12-02 00:00:07.099+0800][github.com/dataapi/handler.DataApi:212] _undef||_msg=params error. fail over
    3. 注意:日志格式调整可能会涉及到依赖日志的应用(比如odin监控规则)的调整。
  2. 老模块改造中,兼容旧的日志打印接口:新的日志组件可以保留老的接口,即允许用户不传入DLTAG,仍然只传入一个msg,但新日志组件会自动将日志打印为如下格式:

    1. [INFO] [2015-12-02 00:00:07.099+0800][github.com/dataapi/handler.DataApi:212] _undef||_msg=params error. fail over

三、 trace相关信息

l trace信息

日志记录同时记录相关的trace信息。用来串联跨模块请求的日志。

  1. 引入traceid

    ,用来唯一描述一次端到端的请求访问。需要业务代码配合进行trace信息的生成,解出,传递,记录。

    1. 生成:在APP端上或者nginx服务器生成的uuid。Nginx trace id
    2. 解出:在请求进入的时候将请求中的traceid解出。
    3. 传递:在访问外部http,thrift等网络通信请求的时候将traceid植入在请求中传递。
    4. 记录:在请求入口,请求出口,访问外部http,thrift等网络通信请求的时候将traceid记录进入日志。(或者任意一条业务日志记录的时候都将traceid记录)
  2. 引入spanid,cspanid(childSpanid)

    。 用来描述一个请求访问(一个trace)在一个模块内部的唯一标示(一个span)。需要业务代码配合进行span信息的生成,解出,传递,记录。

    – 以下的解释站在callee的角度来描述了,请细细体会

    1. 生成:每个模块被调用方指定spanid(如果没有则生成一个spanid)
    2. 解出:在请求进入的时候将请求中的cspanid解出,作为自己的spanid。
    3. 传递:在访问外部http,thrift等网络请求的时候,生成cspanid,将cspanid植入在请求中传递。
    4. 记录:在请求入口,请求出口记录spanid;访问外部http,thrift等网络通信请求的时候将spanid,cspanid记录进入日志。
  3. 依据语言特性,php,java的日志组件(保存在TLS里面)可以自动在日志中加入traceid和spanid。其他语言日志组件需要显示传入

  4. 一个带有trace信息的日志:
    [NOTICE][2016-03-28T12:29:20.999+0800][line=/home/xiaoju/webroot/gulfstream/application/pay/v1/helpers/log_helper.php +442 class=? function=::log_request] _com_request_in||traceid=0af289a156f8b3200e6f5344345e7e02||spanid=f4b6a0ec58a4f503||cspanid=5df56b99411f4fab||logid=408994954500||uri=/gulfstream/pay/v1/wxpayinterface/jsSDKSign||url=/gulfstream/pay/v1/wxpayinterface/jsSDKSign||from=10.242.154.79

用一个图来说明一下dltag,traceid,spanid和cspanid在trace链条串联的作用。

这是跨模块调用的trace信息传递的图。

这几条格式化日志是在一次具体的调用中,接口dPullOrder调用接口getDriverInfo产生的日志中的几个字段。

①红框内,属于同一个traceid(0af2a17358346a0627fd437e199dd102)。整个调用输入一个调用栈产生的日志信息,traceid在这个调用栈的请求内透传。

②黄框内,是该调用(trace)在模块1(dPullOrder)产生的所有日志,属于一个spanid(62541ca1663d0b9d)。在模块1处理这个调用中spanid保持一致。

③篮框内,是该调用(trace)在模块2(getDriverInfo)产生的所有日志,属于同一个spanid(6254ecd55a2c05d2)。在模块2处理这个调用中spanid保持一致。

同时模块1(dPullOrder)调用模块2(getDriverInfo)的时候,产生日志将两个spanid关联在一起,构建成62541ca1663d0b9d->6254ecd55a2c05d2的调用关系。

④绿框内,是该调用在模块1产生的业务标注日志(可能是错误异常或者业务打点)。trace聚合会将他们聚合在一起。

l 收益

  1. 系统整体关联图的自动生成
  2. 监控与定位
    1. 单请求功能Trace能力:可以索引任何请求相关联的上下游模块处理、外部服务调用,协助提供一个请求在各个模块的处理信息、快速查找错误日志
    2. 单请求性能Trace能力:可以生成任意请求在各个调用链模块的耗时信息,快速定位性能瓶颈模块
    3. 接口稳定性DrillDown能力:一个接口出现不稳定情况,可以进行关联服务的DrillDown分析,自动分析关联的下游接口与外部服务稳定性
  3. 系统容量预估与监控
  4. 架构风险、性能分析

四、 [可选] 把脉服务分析

l 系统架构

l 需要在日志中打印的信息

(1)接口性能以及实时抽样Traceid

需要按照日志规范在接口出口处打印dltag=_com_request_out、uri=xxx、proc_time=xxx(proc_time建议缺省单位s,并且缺省单位)这三组kv。

(2)错误日志统计

**统计WARNING以及以上级别的日志条数。**如果需要按照uri以及errno来统计,则需要在所有错误日志中加入uri=xxx以及errno=xxx,同时加上errmsg=xxx的话,错误聚类效果更优。

五、 [可选] 风险分析

风险分析日志规范如下:

key含义
dltag_com_http_success、_com_http_failure、_com_thrift_success、_com_thrift_failure
traceidtraceid
caller_func调用方接口
callee_func被调用方接口
proc_time请求耗时
errno错误码
errmsg错误信息
callee_host被调用方ip

示例:

[NOTICE][2018-04-17T11:49:28.285+0800][line=/home/xiaoju/webroot/gulfstream/application/driver/v2/vendor/disf/spl/src/Trace/Trace.php +641 class=“Disf”\SPL\Trace function=Disf\SPL\Trace::afterRpcHook]

_com_http_success||traceid=xxxxxxxxxxxxxxxxxx||caller_func=/xxxxx/xxxxxxxxxxxxx||callee_func=http://xxx.xxx.xxx.xxx:xxxx/xxx/xxxxxxx||errno=0||errmsg=xxx||proc_time=169||callee_host=xxx.xxx.xxx.xxx

日志样例

[INFO][2023-11-06T19:30:03.457+0800][com.xiaoju.manhattan.pay.controller.cashier.OrderController:248] _undef||spanid=004180007c4adef8||traceid=0aa2ff086548ce38a27c3d2343c6f286||_msg=收银台查询订单,queryOrderRespDTO:{"bankName":"农业银行","bankNo":"622848*********5610","bizCode":"","bizMsg":"","closeTime":"2023-11-07 17:37:30","createTime":"2023-11-06 17:37:30","failCode":"","failReason":"","merchantId":"M0002182","orderAmount":23580,"orderNo":"DD0004308020231103155626Uebfag","orderStatus":0,"payAmount":23580,"payChannel":0,"payId":"23110617373000021820c42ae9183qfX","payInfos":[{"bizCode":"","bizMsg":"","failCode":"","failReason":"","payAmount":23580,"payChannel":0,"payElement":"{\"cardNo\":\"uqfn7X*********uzg6/IL8usBwBw4EQ/HUVtP1SM44=\"}","payMethod":200,"status":0}],"payStatus":0,"pmCode":"200","productType":"CASHIER_PAY"}

实现思路

应用程序改造

@Component
@Slf4j
public class AccessFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse)response);
        RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
        // 获取请求路径
        String path = requestWrapper.getRequestURI();
        String traceId = requestWrapper.getHeader(LogTag.traceIdName);
        String caller = requestWrapper.getHeader(LogTag.caller_func);
        if(traceId == null) {
            traceId = UUID.randomUUID().toString().replace("-","");
        }
        MDC.put(LogTag.caller_func,caller);
        MDC.put(LogTag.callee_func,path);
        MDC.put(LogTag.traceIdName,traceId);
        MDC.put(LogTag.spanIdName, UUID.randomUUID().toString().replace("-","").substring(0,25));
        long startTime = System.currentTimeMillis();
        MDC.put(LogTag.dltag,LogTag._com_request_in);

        log.info(requestWrapper.getRequestBody());
        try {
            MDC.put(LogTag.dltag,LogTag._undef);
            chain.doFilter(requestWrapper, responseWrapper);
        } finally {
            String content = new String(responseWrapper.getResponseData(), "utf-8");
            ResultMsg resultMsg = JSON.parseObject(content, ResultMsg.class);
            long endTime = System.currentTimeMillis();
            MDC.put(LogTag.proc_time,Long.toString(endTime - startTime));
            MDC.put(LogTag.errno,Integer.toString(resultMsg.getErrorCode()));
            MDC.put(LogTag.errmsg,resultMsg.getErrorMsg());
            MDC.put(LogTag.dltag,LogTag._com_request_out);
            log.info(content);
            MDC.clear();
            response.getWriter().write(content);
        }
    }


    private static class MyPrintWriter extends PrintWriter {
        public MyPrintWriter(Writer out) {
            super(out);
        }

        @Override
        public String toString() {
            return super.out.toString();
        }
    }
}
/**
自定义实现日志插件
 * @author weijinhao
 * @date 2023/11/18 22:01
 */
@Plugin(name = "Log4jEncodeLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
public class LogPlugin extends AbstractStringLayout {

    private String formt = "[level][data][method] [dltag]";
    private PatternLayout patternLayout;

    private List<String> logKey;


    protected LogPlugin(Charset charset,String pattern) {
        super(charset);
        patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
        logKey = new ArrayList<>();
        logKey.add(LogTag.traceIdName);
        logKey.add(LogTag.spanIdName);
        logKey.add(LogTag.caller_func);
        logKey.add(LogTag.callee_func);
        logKey.add(LogTag.errno);
        logKey.add(LogTag.errmsg);
        logKey.add(LogTag.proc_time);
    }

    protected LogPlugin(Charset aCharset, byte[] header, byte[] footer) {
        super(aCharset, header, footer);

    }

    protected LogPlugin(Configuration config, Charset aCharset, Serializer headerSerializer, Serializer footerSerializer) {
        super(config, aCharset, headerSerializer, footerSerializer);
    }

    @Override
    public String toSerializable(LogEvent event) {
        String dltag = LogTag.dltag;
        StringBuffer log = new StringBuffer();
        //[%level][%data][%method] %DLTAG
        final StackTraceElement element = event.getSource();
        String logString = new String(formt);
        logString = logString.replace("level",event.getLevel().name());
        Instant instant = Instant.ofEpochMilli(event.getInstant().getEpochMillisecond());
        logString = logString.replace("data", instant.toString());
        logString = logString.replace("method",element.toString());

        ReadOnlyStringMap contextData = event.getContextData();
        Map<String, String> contextMap = contextData.toMap();
        String dltagContent = contextMap.get(dltag);
        if(dltagContent == null) {
            dltagContent = "_undef";
        }
        logString = logString.replace(dltag,dltagContent);
        log.append(logString);
        HashMap<String, String> sortMap = new HashMap<>();
        sortMap.putAll(contextMap);

        for (String keyName : logKey) {
            String value = sortMap.get(keyName);
            String tag = "||" + keyName + "=" + (value == null ? "" : value);
            log.append(tag);
            sortMap.remove(keyName);
        }

        for (Map.Entry<String, String> entry : sortMap.entrySet()) {
            if(!StringUtils.equals(dltag,entry.getKey())) {
                String key = entry.getKey();
                String value = entry.getValue();
                String tag = "||" + key + "=" + (value == null ? "" : value);
                log.append(tag);
            }
        }

        String message = patternLayout.toSerializable(event);
        log.append("||" + "_msg=" + message );
        return log.toString();
    }

    @PluginFactory
    public static Layout createLayout(
            @PluginAttribute(value = "pattern") final String pattern,
            @PluginAttribute(value = "charset") final Charset charset) {
        return new LogPlugin(charset,pattern);
    }
}
/**

 * @author weijinhao
 * @date 2023/11/18 18:02
 */
public class LogTag {
    public static String dltag = "dltag";
    public static  String traceIdName = "traceId";
    public static  String spanIdName = "spanId";
    public static  String caller_func = "caller_func";
    public static  String callee_func = "callee_func";
    public static  String proc_time = "proc_time";
    public static  String errno = "errno";
    public static  String errmsg = "errmsg";


    public static String _com_request_in ="_com_request_in";
    public static String _com_request_out = "_com_request_out";
    public static String _undef ="_undef";
    public static String _msg = "_msg";
}

log4j2.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<!--packages 参数指定插件包路径,多个路径用逗号隔开-->
<Configuration status="INFO" name="XMLConfigTest" package="com.log.demo.common.log">
    <Properties>
        <Property name="PATTERN">
            %m%n
        </Property>
        <property name="MODULE_NAME">demo</property>
        <property name="LOG_HOME">/home/xiaoju/logs</property>
    </Properties>

    <Appenders>
        <Console name="STDOUT">
            <Log4jEncodeLayout  pattern="${PATTERN}" charset="UTF-8"/>
        </Console>
        <RollingFile name="ROLLINGFILE" fileName="${LOG_HOME}/${MODULE_NAME}.log"
                     filePattern="${LOG_HOME}/${MODULE_NAME}-%d{yyyy-MM-dd}-%i.log">
            <Log4jEncodeLayout  pattern="${PATTERN}" charset="UTF-8"/>
            <Policies>
                <TimeBasedTriggeringPolicy modulate="true"
                                           interval="24" />
                <SizeBasedTriggeringPolicy size="100MB"/>
            </Policies>
            <DefaultRolloverStrategy max="100">
                <Delete basePath="${LOG_HOME}" maxDepth="3">
                    <IfFileName glob="*/${MODULE_NAME}-*.log"/>
                    <IfLastModified age="30d"/> <!-- 这里保留30天 -->
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Root>
        <Logger name="com.snbc.vems" level="INFO" additivity="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Logger>
        <Logger name="org.apache" level="WARN" additivity="false" includeLocation="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Logger>
        <Logger name="org.mybatis" level="WARN" additivity="false" includeLocation="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Logger>
        <Logger name="org.hibernate" level="WARN" additivity="false" includeLocation="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Logger>
        <Logger name="org.springframework" level="WARN" additivity="false" includeLocation="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="ROLLINGFILE"/>
        </Logger>
    </Loggers>
</Configuration>

项目git地址: https://gitee.com/weijinhao/log.git

上面我们有了日志的具体格式了,我们通过代码也实现了该日志格式。
之后我们就需要使用logstash对日志进行处理,之后输出到es中供我们监控.

logstash 的学习思路主要有一下几点

  1. logstash 中使用的数据类型 参见:[Structure of a Config File | Logstash Reference 6.1] | Elastic
  2. logstash 中使用的条件表达是 参见: [Accessing Event Data and Fields in the Configuration | Logstash Reference 6.1] | Elastic
  3. 然后就是 logstash 的input 输入,我们调试可以使用 stdin标准输入
  4. 在之后就是filter,比较常用的filter有dissect(分隔符) grovk(正则表达式) date(日期类型转化) mutate(类型或字段修改),等几种表达式
  5. 最后就是输出了,比如es 的 ,我们调试就使用 stdout 就好了

logstash 的pipeline 配置

# The # character at the beginning of a line indicates a comment. Use
# comments to describe your configuration.
input {
  #stdin {}
  file {
    path => "/home/xiaoju/logs/**/*"
    sincedb_path => "/home/xiaoju/logstash/logstash-6.1.1/data/log1"
    start_position => "beginning"
    codec => multiline  {
          pattern => "^\["
          charset => "UTF-8"
          what => "previous"
          negate => true
    }
  }        
}
# The filter part of this file is commented out to indicate that it is
# optional.
#[INFO][2023-11-20T09:50:44.872Z][com.log.demo.filter.AccessFilter.doFilter(AccessFilter.java:47)] [_com_request_in]||traceId=a9d350051e834dd58fc0288caf920365||spanId=dcc4e8d0058143e9a898d29ed||callee_func=/test||caller_func=||_msg={
# [INFO][2023-11-20T09:50:44.349Z][com.log.demo.filter.AccessFilter.doFilter(AccessFilter.java:59)] [_com_request_out]||traceId=dbba05f9f404450f927f150f45ac8bc7||spanId=5102773f6c884d95800dbc122||errno=10000||caller_func=||callee_func=/test||errmsg##=success||proc_time=153||_msg={"errorCode":10000,"errorMsg":"success","bizContent":null}


filter {
  dissect {
    mapping => {
      "message" => "[%{level}][%{data}][%{}] [%{action}]||traceId=%{traceId}||spanId=%{spanId}||caller_func=%{caller_func}||callee_func=%{callee_func}||errno=%{errno}||errmsg=%{errmsg}||proc_time=%{proc_time}||%{}"
    }
  }
  date {
    match => [ "data",  "ISO8601" ]
  }
  mutate {
    convert => { 
      "errno" => "integer"
      "proc_time" => "integer" 
    }
  }
  
}
  
output {
  #stdout {
   # codec => rubydebug
  #}
  elasticsearch {
    hosts => ["192.168.88.102:9200","192.168.88.103:9200"]
    index => "goslin-gateway-log"
  }
}

这之后我们的数据就到了es中了,那我们就可以建立虚拟图,之后建立disboard了,当然需要具备一些 es聚合分析相关的语法才能更好的创建图表.

关于如何添加报警群通知

如果我们是自己的es的话,我觉得可以起一个定时任务,来进行报警规则的查询,之后在查询es如果查询到报警条数之后进行一个报警通知的生成,之后通知到飞书里面去,通知的话比较简单,就是一个hook接口的调用。飞书开放平台地址如下:自定义机器人使用指南 - 开发指南 - 开发文档 - 飞书开放平台 (feishu.cn) 实现伪代码如下:

		<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.9</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.68</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
package com.log.demo.uitls;

import com.alibaba.fastjson.JSON;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author weijinhao
 * @date 2024/3/14 22:59
 */
public class HttpClientUtils {


    public static String doJson(String url, String json, Map<String, Object> headers) throws Exception {
        return doJson(url,json,null,null,headers);
    }

    public static String doJson(String url, String json,Integer connectTimeout, Integer readTimeout, Map<String, Object> headers) throws Exception {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpResponse response = null;
        try {
            httpPost.setHeader("Content-type", "application/json");
            httpPost.setEntity(new StringEntity(json, "UTF-8"));

            if(headers != null) {
                for (Map.Entry<String, Object> entry : headers.entrySet()) {
                    // 封装请求参数
                    httpPost.setHeader(entry.getKey(),String.valueOf(entry.getValue()));
                }
            }
            RequestConfig.Builder custom = RequestConfig.custom();
            if(connectTimeout != null) {
                custom.setConnectTimeout(connectTimeout);
            }
            if(readTimeout != null) {
                custom.setConnectionRequestTimeout(readTimeout);
            }

            httpPost.setConfig(custom.build());

            response = httpclient.execute(httpPost);
            return EntityUtils.toString(response.getEntity(), "UTF-8");//获得返回的结果
        }catch (Exception e) {
            System.out.println(JSON.toJSON(e));
        }
        finally {
            // 6、释放资源
            if (response != null) {
                response.close();
            }
            httpclient.close();
        }
        return null;
    }



    public static String doPost(String url, Map<String, Object> params, Map<String, Object> headers)  throws Exception{
        return doPost(url,params,null,null,headers);
    }


    public static  String doPost(String url, Map<String, Object> params,Integer connectTimeout, Integer readTimeout,  Map<String, Object> headers) throws Exception {

        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        try{
            // 1、声明 HttpPost对象
            HttpPost request = new HttpPost(url);

            RequestConfig.Builder custom = RequestConfig.custom();
            if(connectTimeout != null) {
                custom.setConnectTimeout(connectTimeout);
            }
            if(readTimeout != null) {
                custom.setConnectionRequestTimeout(readTimeout);
            }

            request.setConfig(custom.build());
            // 2、封装请求参数,请求数据表单
            // 是否是带参数的请求
            UrlEncodedFormEntity urlEncodedFormEntity = encapRequestParams(params);

            request.setEntity(urlEncodedFormEntity);

            if(headers != null) {
                for (Map.Entry<String, Object> entry : headers.entrySet()) {
                    // 封装请求参数
                    request.setHeader(entry.getKey(),String.valueOf(entry.getValue()));
                }
            }

            UrlEncodedFormEntity entity = encapRequestParams(params);
            // 3、设置请求数据表单
            request.setEntity(entity);
            // 4、通过HttpClient执行请求
            response = httpclient.execute(request);
            // 5、解析返回结果,封装返回对象HttpResultData
            HttpEntity responseEntity = response.getEntity();
            return responseEntity.getContent().toString();

        } finally{
            // 6、释放资源
            if (response != null)
            {
                response.close();
            }
            httpclient.close();
        }

    }



    public static String doGet(String url, Map<String, String> params,Integer connectTimeout, Integer readTimeout,  Map<String, String> headers) throws Exception{

        // 创建Httpclient对象
        CloseableHttpClient httpclient = HttpClients.createDefault();

        // 定义请求的参数
        URIBuilder uriBuilder = new URIBuilder(url);
        // 是否是带参数的请求
        if (params != null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                // 封装请求参数
                uriBuilder.addParameter(entry.getKey(), entry.getValue());
            }
        }

        // 创建http GET请求
        HttpGet httpGet = new HttpGet(uriBuilder.toString());
        RequestConfig.Builder custom = RequestConfig.custom();
        if(connectTimeout != null) {
            custom.setConnectTimeout(connectTimeout);
        }
        if(readTimeout != null) {
            custom.setConnectionRequestTimeout(readTimeout);
        }

        httpGet.setConfig(custom.build());
        if(headers != null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                // 封装请求参数
                httpGet.setHeader(entry.getKey(),String.valueOf(entry.getValue()));
            }
        }


        CloseableHttpResponse response = null;
        try
        {
            // 执行请求
            response = httpclient.execute(httpGet);
            return EntityUtils.toString(response.getEntity(), "UTF-8");
        } finally
        {
            if (response != null)
            {
                response.close();
            }
            httpclient.close();
        }
    }


    public static String doGet(String url) throws Exception{
        return doGet(url,null,null,null,null);

    }


    public static UrlEncodedFormEntity encapRequestParams(Map<String, Object> map) throws Exception {
        // 是否是带参数的请求
        if (map != null) {
            // 声明表单容器
            List<NameValuePair> parameters = new ArrayList<>();
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                // 封装容器
                BasicNameValuePair nameValuePair = new BasicNameValuePair(entry.getKey(), entry.getValue().toString());
                parameters.add(nameValuePair);
            }
            // 创建表单实体
            UrlEncodedFormEntity entity = new UrlEncodedFormEntity(parameters, "UTF-8");

            return entity;
        }
        return null;

    }
}

@Slf4j
public class AlarmFeiShuUtils {


    public static boolean alarmToFeishu(String title, String errorMsg)  {


        String url = "https://open.feishu.cn/open-apis/bot/v2/hook/9e068731-0447-4c92-bbae-c0f89ff99bb1";
        String privateKey = "p5eKYKhj7e4CK7vydf3ngf";
        String responseString = null;
        try {

            JSONObject request = new JSONObject();
            Instant now = Instant.now();
            request.put("timestamp", now.getEpochSecond());
            request.put("sign",GenSign(privateKey,now.getEpochSecond()));

            request.put("msg_type","post");


            JSONObject content = new JSONObject();
            JSONObject post = new JSONObject();
            JSONObject zh_cn = new JSONObject();

            //文件体
            JSONArray finalcontent = new JSONArray();

            JSONArray item = new JSONArray();

            JSONObject text = new JSONObject();
            text.put("tag","text");
            text.put("text",errorMsg);

            JSONObject at = new JSONObject();
            at.put("tag","at");
            at.put("user_id","all");

            item.add(text);
            item.add(at);
            finalcontent.add(item);


            zh_cn.put("title",title);
            zh_cn.put("content",finalcontent);
            post.put("zh_cn",zh_cn);
            content.put("post",post);

            request.put("content",content);
            responseString = HttpClientUtils.doJson(url, request.toJSONString(), null);
        }catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        if(responseString == null || responseString.isEmpty()) {
            return false;
        }
        FeiShuReponse feiShuReponse = JSON.parseObject(responseString, FeiShuReponse.class);

        if(feiShuReponse.getCode() != 0) {
            return false;
        }
        return true;
    }


    public static String GenSign(String secret, long timestamp) throws NoSuchAlgorithmException, InvalidKeyException {
        //把timestamp+"\n"+密钥当做签名字符串
        String stringToSign = timestamp + "\n" + secret;
        //使用HmacSHA256算法计算签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] signData = mac.doFinal(new byte[]{});
        return new String(com.sun.org.apache.xerces.internal.impl.dv.util.Base64.encode(signData));
    }

}

package com.log.demo.dto;

/**
 * @author weijinhao
 * @date 2024/4/25 17:12
 */
public class FeiShuReponse {


    /**
     * StatusCode : 0
     * StatusMessage : success
     * code : 0
     * data : {}
     * msg : success
     */

    private int StatusCode;
    private String StatusMessage;
    private int code;
    private DataBean data;
    private String msg;

    public int getStatusCode() {
        return StatusCode;
    }

    public void setStatusCode(int StatusCode) {
        this.StatusCode = StatusCode;
    }

    public String getStatusMessage() {
        return StatusMessage;
    }

    public void setStatusMessage(String StatusMessage) {
        this.StatusMessage = StatusMessage;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public DataBean getData() {
        return data;
    }

    public void setData(DataBean data) {
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public static class DataBean {
    }
}

package com.log.demo.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author weijinhao
 * @date 2024/4/25 18:25
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class AlarmRules {
    // 单位s
    private int alarmTime ;
    private int alarmCount;
    private String title;
    private String format;
}

报警的模拟代码

package com.log.demo;

import com.log.demo.dto.AlarmRules;
import com.log.demo.uitls.AlarmFeiShuUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.Instant;

@SpringBootTest
@Slf4j
class DemoApplicationTests {

    @Test
    void contextLoads() {
    }
    //模拟从es定时任务查询
    @Test
    void test() {
        //从报警规则中查询出一个来
        AlarmRules rules = new AlarmRules(100, 100, "xxx业务异常报警", "xxxxx");

        Instant startTime = Instant.now().minusSeconds(rules.getAlarmTime());

        //进行日志的查询, 假设我们查询出来了100条以上,那我们就拿其中的一个去匹配一下  以开始时间 + 规则 进行查询
        int selectCount = 100;

        String msg = "xxx业务异常报警";
        String traceId = "e858f17e77c7424c9a57fff2708d716f";
        String spanId = "bb63ed22c8224e0889b5a77b8";
        String _msg = "java.lang.Exception: update mysql fail\n" +
                "\tat com.log.demo.controller.TestController.test(TestController.java:23) ~[classes/:?]\n" +
                "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_201]\n" +
                "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_201]\n" +
                "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_201]\n" +
                "\tat java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_201]\n" +
                "\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:681) ~[tomcat-embed-core-9.0.62.jar:4.0.FR]\n" +
                "\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.19.jar:5.3.19]\n" +
                "\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.62.jar:4.0.FR]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat com.log.demo.filter.AccessFilter.doFilter(AccessFilter.java:50) ~[classes/:?]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.19.jar:5.3.19]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:890) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.62.jar:9.0.62]\n" +
                "\tat java.lang.Thread.run(Thread.java:748) ~[?:1.8.0_201]";

        if (rules.getAlarmCount() <= selectCount) {
            String content = "msg:%s \r\n" +
                    "traceId:%s \r\n" +
                    "spanId:%s \r\n" +
                    "_msg: %s \r\n";

            String format = String.format(content, msg, traceId, spanId, _msg);
            AlarmFeiShuUtils.alarmToFeishu(rules.getTitle(), format);

        }
    }
}

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现基于在线课程日志的学情分析系统的主要功能需要涉及很多方面,包括数据获取,数据处理,数据分析和可视化等。在使用IntelliJ IDEA进行开发时,可以使用Java语言作为主要的开发语言,使用Spring Boot作为框架,使用MySQL作为数据库进行开发。 以下是一个简单的示例,演示如何使用IntelliJ IDEA实现基于在线课程日志的学情分析系统的主要功能。 首先,我们需要创建一个Spring Boot项目。在IntelliJ IDEA选择File -> New -> Project,然后选择Spring Initializr。选择所需的依赖项,例如Spring Web,Spring Data JPA和MySQL Driver,并创建项目。 接下来,我们需要创建一个实体类来表示在线课程日志。这个实体类应该包含每个日志记录的信息,例如时间戳,用户ID,课程ID和动作类型等。示例代码如下: ```java @Entity public class CourseLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private LocalDateTime timestamp; private Long userId; private Long courseId; private String actionType; // getters and setters } ``` 接下来,我们需要创建一个Repository接口来处理与CourseLog实体相关的数据库操作。示例代码如下: ```java @Repository public interface CourseLogRepository extends JpaRepository<CourseLog, Long> { List<CourseLog> findByUserId(Long userId); List<CourseLog> findByCourseId(Long courseId); } ``` 然后,我们需要创建一个Service类来处理数据处理和分析逻辑。例如,我们可以创建一个名为CourseLogService的类,并在其实现各种数据处理和分析方法。示例代码如下: ```java @Service public class CourseLogService { @Autowired private CourseLogRepository courseLogRepository; public List<CourseLog> getLogsByUserId(Long userId) { return courseLogRepository.findByUserId(userId); } public List<CourseLog> getLogsByCourseId(Long courseId) { return courseLogRepository.findByCourseId(courseId); } public Map<String, Long> getActionCountByCourse(Long courseId) { List<CourseLog> logs = courseLogRepository.findByCourseId(courseId); Map<String, Long> actionCount = new HashMap<>(); for (CourseLog log : logs) { String actionType = log.getActionType(); if (actionCount.containsKey(actionType)) { actionCount.put(actionType, actionCount.get(actionType) + 1); } else { actionCount.put(actionType, 1L); } } return actionCount; } } ``` 最后,我们可以创建一个Controller类来处理HTTP请求,并返回JSON格式的数据。例如,我们可以创建一个名为CourseLogController的类,并在其实现各种请求处理方法。示例代码如下: ```java @RestController public class CourseLogController { @Autowired private CourseLogService courseLogService; @GetMapping("/logs/user/{userId}") public List<CourseLog> getLogsByUserId(@PathVariable Long userId) { return courseLogService.getLogsByUserId(userId); } @GetMapping("/logs/course/{courseId}") public List<CourseLog> getLogsByCourseId(@PathVariable Long courseId) { return courseLogService.getLogsByCourseId(courseId); } @GetMapping("/logs/course/{courseId}/action") public Map<String, Long> getActionCountByCourse(@PathVariable Long courseId) { return courseLogService.getActionCountByCourse(courseId); } } ``` 以上是一个简单的示例,演示如何使用IntelliJ IDEA实现基于在线课程日志的学情分析系统的主要功能。但是,这只是一个基础示例,实际开发需要更复杂的逻辑和更详细的代码实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值