SpringBoot与Loki的那些事

news2024/11/25 15:44:03

因为网上好多都没有通过Loki的API自己实现对日志监控系统,所以我就下定决心自己出一版关于loki与springboot的博文供大家参考,这个可以说是比较实用,很适合中小型企业。因此我酝酿了挺久了,对于loki的研究也比较久,希望各位读者能有新的收获。

简介

Loki是Grafana Labs团队的开源项目,可以组成一个功能齐全的日志堆栈。Loki是一个水平可扩展,高可用性,多租户的日志聚合系统。它的设计非常经济高效且易于操作,因为它不会为日志内容编制索引,而是为每个日志流编制一组标签。Loki是用来存储日志和处理查询,需要通过promtail来收集日志,也可是通过后端的logback等日志框架来收集日志,通过grafana提供的loki可视化查看日志,当然了loki也提供了API,可以根据自己的需求来自己实现可视化界面,能够减少三方插件的使用。

安装

上一篇文章已经介绍了如何安装以及使用Grafana+loki+promtail进行搭建日志系统,blog.csdn.net/qq_43843951…可以看看这篇文章。接下来笔者要介绍的是通过Loki的API编写自己可视化界面,并且通过logback来实现收集日志。 大致的结构如图

​简单介绍一下,主要就是通过springboot后端的logback日志框架来收集日志,在推送到loki中存储,loki执行对日志的查询,通过API根据标签等信息去查询日志并且在自定义的前端界面中展示。

整体思路

其实宏观来看,要达成这个需求说起来是十分简单的,只需配置logback配置,在通过MDC写入、收集日志,这里可以好多的写法,可以是通过反射写入日志,也可以是在需要打印的地方写入日志,并且是将日志区分为不同的标签。在前端就可以根据所定义的标签来查看相应的日志。前端获取日志信息逻辑也很简单,就只是通过Loki提供的API获取每行的日志。接下来我就一一详细的介绍SpringBoot与Loki的那些事。 可以查看此图便于理解:

Loki实战开发

接下来就详细讲解笔者在实战开发中是如何编写的,本次介绍只是对编写的代码进行详讲,对于代码可能不会全部粘贴,不然冗余起来效果不好,各位读者可以各自发挥,更加完善。其实整个业务也不难,基本都是loki自身提供的API,读者可以通过Loki官方网站grafana.com/docs/loki/l… 去进一步对Loki的API进行查阅,后面笔者可能也会出一篇来专门对Loki的API以及配置进行介绍。好了,废话不多说,马上进入正题。

springboot中的配置

首先需要配置向Loki推送日志,也就是需要通过Loki的API:POST /loki/api/v1/push ,可以直接将地址通过appender写死在logback日志框架中,但是在项目开发中,要考虑到环境的不同,应该是能够根据需要来修改loki服务器的地址,因此将loki的服务器地址配置在application-dev.yml中。

loki:
  url: http://localhost:3100/loki/api/v1

配置logback日志框架

先获取yml配置的地址,通过appender添加到日志框架中,当然,配置客户端也不一定是LogBack框架,还有Log4j2框架也是能够使用的,具体配置可以看官网github.com/loki4j/loki… 和 github.com/tkowalcz/tj… ,本章只对loki进行讲解,对于日志框架,后期也会一一列出,各位读者有什么不了解的,可以先到网上查阅资料。因为笔者不是部署多台Loki服务器,不同的系统采用system这个标签来进行区分。

<springProperty scope="context" name="lokiUrl" source="loki.url"/>
<property name="LOKI_URL" value="${lokiUrl}"/>

<!--添加loki-->
<appender name="lokiAppender" class="com.github.loki4j.logback.Loki4jAppender">
    <batchTimeoutMs>1000</batchTimeoutMs>
    <http class="com.github.loki4j.logback.ApacheHttpSender">
        <url>${LOKI_URL}/push</url>
    </http>
    <format>
        <label>
            <pattern>system=${SYSTEM_NAME},level=%level,logType=%X{log_file_type:-logType}</pattern>
        </label>
        <message>
            <pattern>${log.pattern}</pattern>
        </message>
        <sortByTime>true</sortByTime>
    </format>
</appender>

注解与切面写入日志

自定义注解,并且设置日志标签值。

/**
 * @author: lyd
 * @description: 自定义日志注解,用作LOKI日志分类
 * @Date: 2022/10/10
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD})
@Documented
public @interface LokiLog {
    LokiLogType type() default LokiLogType.DEFAULT;
}

通过枚举的方式来定义日志类型的标签值

/**
 * @author: lyd
 * @description: 枚举便签值 - 类型自己定义
 * @Date: 2022/10/11
 */
public enum LokiLogType {
    DEFAULT("默认"),
    A("A"),
    B("B"),
    C("C");

    private String desc;

    LokiLogType(String desc) {
     this.desc=desc;
    }

    public String getDesc() {
        return desc;
    }

}

编写切面,写入日志(详情可以参照这篇文章 cloud.tencent.com/developer/a…),内部通过MDC.put("log_file_type", logType.getDesc());(MDC ( Mapped Diagnostic Contexts ),它是一个线程安全的存放诊断日志的容器。可以参照:www.jianshu.com/p/1dea7479e…)可以理解为log_file_type是标签名,logType.getDesc()是标签值。

/**
 * @author: lyd
 * @description: 自定义日志切面:https://cloud.tencent.com/developer/article/1655923
 * @Date: 2022/10/10
 */
@Aspect
@Slf4j
@Component
public class LokiLogAspect {
    /**
     * 切到所有OperatorLog注解修饰的方法
     */
    @Pointcut("@annotation(org.nl.wms.log.LokiLog)")
    public void operatorLog() {
        // 空方法
    }

    /**
     * 利用@Around环绕增强
     *
     * @return
     */
    @Around("operatorLog()")
    public synchronized Object around(ProceedingJoinPoint pjp) throws Throwable {
//        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//        HttpServletRequest request = attributes.getRequest();
//        HttpServletResponse response = attributes.getResponse();

        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        LokiLog lokiLog = method.getAnnotation(LokiLog.class);

        // 获取描述信息
        LokiLogType logType = lokiLog.type();

        MDC.put("log_file_type", logType.getDesc());
        log.info("输入参数:" + JSONObject.toJSONString(pjp.getArgs()));

        Object proceed = pjp.proceed();

        log.info("返回参数:" + JSONObject.toJSONString(proceed));
        MDC.remove("log_file_type");
        return proceed;

    }
}

使用注解,在方法中引用注解即可

@LokiLog(type = LokiLogType.A)

前端界面与后端接口

前端界面介绍起来可能比较麻烦,毕竟写的代码也比较多,这里就选取讲解,代码量比较多,也不会是全部代码粘贴,样式之类的,我相信读者会根据自己的需求去实现,这里主要的是记录开发的思路。

日志的初步获取

前端的界面就如图,本次是以el-admin这个为基础制作的demo。

​查找日志是需要通过标签与标签值来获取日志信息,因此首先需要的是携带标签对到后端访问Loki的API拿到数据,读者可以查阅官网的API,结合着学习。

​一开始当vue视图渲染的时候,就会从后端获取loki日志标签,具体后端接口的业务代码如下:

/**
 * 获取labels和values树
 *
 * @return
 */
@Override
public JSONArray getLabelsValues() {
    JSONArray result = new JSONArray();
    // 获取所有标签
    String labelString = HttpUtil.get(lokiUrl + "/labels", CharsetUtil.CHARSET_UTF_8);
    JSONObject parse = (JSONObject) JSONObject.parse(labelString);
    JSONArray labels = parse.getJSONArray("data");
    for (int i=0; i<labels.size(); i++) {
        // 获取标签下的所有值
        String valueString = HttpUtil.get(lokiUrl + "/label/" + labels.getString(i) + "/values", CharsetUtil.CHARSET_UTF_8);
        JSONObject parse2 = (JSONObject) JSONObject.parse(valueString);
        JSONArray values = parse2.getJSONArray("data");
        JSONArray children = new JSONArray();
        // 组成树形状态 两级
        for (int j=0; j<values.size(); j++) {
            JSONObject leaf = new JSONObject();
            leaf.put("label", values.getString(j));
            leaf.put("value", values.getString(j));
            children.add(leaf);
        }

        JSONObject node = new JSONObject();
        node.put("label", labels.getString(i));
        node.put("value", labels.getString(i));
        node.put("children", children);
        result.add(node);
    }
    return result;
}

核心代码就只有通过Hutool工具包去访问API获取标签HttpUtil.get(lokiUrl + "/labels", CharsetUtil.CHARSET_UTF_8); 以及 获取标签值HttpUtil.get(lokiUrl + "/label/" + labels.getString(i) + "/values", CharsetUtil.CHARSET_UTF_8); 因为我的前端是用elment-ui的树来接收的,因此我就将返回的数据设计成相应的形式。

<el-form-item label="日志标签">
  <el-cascader
    v-model="labelAndValue"
    :options="labelsOptions"
    placeholder="请选择标签"
    @change="queryData"
  />
</el-form-item>

模糊查找与更多参数

loki提供了相应的API来进行模糊查找日志,无非就是通过loki的API携带关键字进行模糊查找日志,笔者的做法是获取含有关键字的日志内容。

"/query_range?query={system=\"" + systemName + "\", " + logLabel + "=\"" + logLabelValue + "\"} |= `" + text + "`"

并且还能够通过时间段来查询,笔者实现了的效果如图

​不仅可以通过关键字,还有时间段时间范围以及查找的方向和一次性显示的条数,最好是建议不要超过1000条数据,滚动步数是实现滚动下拉的时候获取新的日志数据的条目数。 后端代码如下,简单介绍一下,就是提供所需要的查询条件来对日志进行筛选。不管是获取日志数据还是滚动下拉获取的日志数据都可以通用这个接口,然而主要的参数设置可以在前端进行打磨,以下代码还有优化的空间,毕竟当时刚开始写的时候没考虑这么多。

@Override
    public JSONObject getLogData(JSONObject json) {
        String logLabel = "";
        String logLabelValue = "";
        Long start = 0L;
        Long end = 0L;
        String text = "";
        String limit = "100";
        String direction = "backward";
        if (json.get("logLabel") != null) logLabel = json.getString("logLabel");
        if (json.get("logLabelValue") != null) logLabelValue = json.getString("logLabelValue");
        if (json.get("text") != null) text = json.getString("text");
        if (json.get("start") != null) start = json.getLong("start");
        if (json.get("end") != null) end = json.getLong("end");
        if (json.get("limits") != null) limit = json.getString("limits");
        if (json.get("direction") != null) direction = json.getString("direction");
        /**
         * 组织参数
         * 纳秒数
         * 1660037391880000000
         * 1641453208415000000
         * http://localhost:3100/loki/api/v1/query_range?query={host="localhost"} |= ``&limit=1500&start=1641453208415000000&end=1660027623419419002
         */
        JSONObject parse = null;
        String query = lokiUrl + "/query_range?query={system=\"" + systemName + "\", " + logLabel + "=\"" + logLabelValue + "\"} |= `" + text + "`";
        String result = "";
        if (start==0L) {
            result = HttpUtil.get(query + "&limit=" + limit + "&direction=" + direction, CharsetUtil.CHARSET_UTF_8);
        } else {
            result = HttpUtil.get(query + "&limit=" + limit + "&start=" + start + "&end=" + end + "&direction=" + direction, CharsetUtil.CHARSET_UTF_8);
        }
        try {
            parse = (JSONObject) JSONObject.parse(result);
        } catch (Exception e) {
//            reslut的值可能为:too many outstanding requests,无法转化成Json
            System.out.println("reslut:" + result);
//            e.printStackTrace();
        }
        return parse;
    }

前端的逻辑是比较复杂的,因为需要做大量的赋值与设置。 前端js方法代码,主要是对参数数据的组织,这里需要注意的是,因为loki需要的是纳秒级别的时间戳,这里就需要十分注意前端js的精度。还有一点就是,如果后端日志是有颜色标签的,那么前端直接渲染就会显示标签,所以这里需要进行相应的处理,就是用过AnsiUp插件进行操作,详细看此篇文章:blog.csdn.net/qq_43843951…

queryData() {
  console.log(this.labelAndValue)
  // 清空查询数据
  this.clearParam()
  if (this.labelAndValue.length > 0) {
    queryParam.logLabel = this.labelAndValue[0]
    queryParam.logLabelValue = this.labelAndValue[1]
  }
  if (queryParam.logLabelValue === null) { // 判空
    this.$message({
      showClose: true,
      message: '请选择标签',
      type: 'warning'
    })
    this.showEmpty = true
    this.emptyText = '请选择标签'
    return
  }
  if (this.timeRange.length !== 0) { // 如果是输入时间范围
    queryParam.start = (new Date(this.timeRange[0]).getTime() * 1000000).toString()
    queryParam.end = (new Date(this.timeRange[1]).getTime() * 1000000).toString()
  }
  if (this.timeZoneValue) {
    const time = new Date()
    queryParam.start = ((time.getTime() - this.timeZoneValue) * 1000000).toString()
    queryParam.end = (time.getTime() * 1000000).toString()
  }
  if (this.text) {
    queryParam.text = this.text.replace(/^\s*|\s*$/g, '') // 去空
  }
  if (this.limits) {
    queryParam.limits = this.limits
  }
  queryParam.direction = this.direction
  var ansi_up = new AnsiUp()
  logOperation.getLogData(queryParam).then(res => {
    this.showEmpty = false
    if (res.data.result.length === 1) {
      this.logs = res.data.result[0].values
      for (const i in res.data.result[0].values) {
        this.logs[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1])
      }
    } else if (res.data.result.length > 1) {
      // 清空
      this.logs = []
      for (const j in res.data.result) { // 用push的方式将所有日志数组添加进去
        for (const values_index in res.data.result[j].values) {
          this.logs.push(res.data.result[j].values[values_index])
        }
      }
      for (const k in this.logs) {
        this.logs[k][1] = ansi_up.ansi_to_html(this.logs[k][1])
      }
      if (this.direction === 'backward') { // 由于使用公共标签会导致时间顺序错乱,因此对二维数组进行排序
        this.logs.sort((a, b) => b[0] - a[0])
      } else {
        this.logs.sort((a, b) => a[0] - b[0])
      }
    } else {
      this.showEmpty = true
      this.emptyText = '暂无日志信息,请选择时间段试试'
    }
  })
}

通过AnsiUp插件可以将带有颜色标签的日志以颜色展示,代码如下:

<div style="margin: 3px; min-height: 80vh;">
  <!--数据判空-->
  <el-empty v-if="showEmpty" :description="emptyText" />
  <!--数据加载-->
  <el-card v-else shadow="hover" style="width: 100%" class="log-warpper">
    <div style="width: 100%">
      <div v-for="(log, index) in logs" :key="index">
        <div style="margin-bottom: 5px; font-size: 12px;" v-html="log[1]" />
      </div>
    </div>
  </el-card>
</div>

向后端请求日志返回的结果是如下图所示

​滚动追加日志

其实下拉滚动的代码与上面直接获取日志的是差不多的,只是在数据的追加是不一样的做法,这里需要注意的是要考虑日志的展示是正序还是逆序,不同的顺序计算时间范围是不一样的,就如下代码

if (this.direction === 'backward') { // 设置时间区间
  queryParam.start = (this.logs[this.logs.length - 1][0] - zone).toString()
  queryParam.end = this.logs[this.logs.length - 1][0]
} else {
  queryParam.start = this.logs[this.logs.length - 1][0]
  queryParam.end = (parseFloat(this.logs[this.logs.length - 1][0]) + parseFloat(zone.toString())).toString()
}

在滚动获取日志的思路是获取最后一条数据的时间,往后推一定的时间差,所以需要考虑是正序还是倒序,默认是6小时。

mounted() {
  window.addEventListener('scroll', this.handleScroll)
}
methods: {
    handleScroll() { // 滚动事件
      const scrollTop = document.documentElement.scrollTop// 滚动高度
      const clientHeight = document.documentElement.clientHeight// 可视高度
      const scrollHeight = document.documentElement.scrollHeight// 内容高度
      const bottomest = Math.ceil(scrollTop + clientHeight)
      if (bottomest >= scrollHeight) {
        // 加载新数据
        queryParam.limits = this.scrollStep
        queryParam.direction = this.direction
        // 获取时间差
        let zone = queryParam.end - queryParam.start
        if (this.timeRange.length) { // 如果是输入时间范围
          zone = ((new Date(this.timeRange[1]).getTime() - new Date(this.timeRange[0]).getTime()) * 1000000).toString()
        }
        if (this.timeZoneValue) {
          zone = this.timeZoneValue * 1000000
        }
        if (zone === 0) {
          zone = 3600 * 1000 * 6
        }
        if (this.direction === 'backward') { // 设置时间区间
          queryParam.start = (this.logs[this.logs.length - 1][0] - zone).toString()
          queryParam.end = this.logs[this.logs.length - 1][0]
        } else {
          queryParam.start = this.logs[this.logs.length - 1][0]
          queryParam.end = (parseFloat(this.logs[this.logs.length - 1][0]) + parseFloat(zone.toString())).toString()
        }
        var ansi_up = new AnsiUp()
        logOperation.getLogData(queryParam).then(res => {
          console.log(res)
          this.showEmpty = false
          if (res.data.result.length === 1) {
            // 如果返回的日志是一样的就不显示
            if (res.data.result[0].values.length === 1 && ansi_up.ansi_to_html(res.data.result[0].values[0][1]) === this.logs[this.logs.length - 1][1]) {
              this.$notify({
                title: '警告',
                duration: 1000,
                message: '当前时间段日志已最新!',
                type: 'warning'
              })
              return
            }
            const log = res.data.result[0].values
            for (const i in res.data.result[0].values) {
              log[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1])
              this.logs.push(log[i])
            }
          } else if (res.data.result.length > 1) {
            const tempArray = [] // 数据需要处理,由于是追加数组,所以需要用额外变量来存放
            // 刷新就是添加,不清空原数组
            for (const j in res.data.result) { // 用push的方式将所有日志数组添加进去
              for (const values_index in res.data.result[j].values) {
                tempArray.push(res.data.result[j].values[values_index])
              }
            }
            if (this.direction === 'backward') { // 由于使用公共标签会导致时间顺序错乱,因此对二维数组进行排序
              tempArray.sort((a, b) => b[0] - a[0])
            } else {
              tempArray.sort((a, b) => a[0] - b[0])
            }
            for (const k in tempArray) {
              tempArray[k][1] = ansi_up.ansi_to_html(tempArray[k][1]) // 数据转换
              this.logs.push(tempArray[k]) // 追加数据
            }
          } else {
            this.$notify({
              title: '警告',
              duration: 1000,
              message: '暂无以往日志数据!',
              type: 'warning'
            })
          }
        })
      }
    }
}

定时刷新日志

当然,日志的获取也是需要实时刷新的,这种不仅可以使用定时器还能够使用websocket,笔者使用的是定时器,因为这个写起来比较简单。相关的代码以及解析如下: 视图

<el-form-item>
  <el-dropdown split-button type="primary" size="mini" @click="queryData">
    查询{{ runStatu }}
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item v-for="(item, index) in runStatuOptions" :key="index" @click.native="startInterval(item)">{{ item.label }}</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</el-form-item>

方法代码 代码大致也和上面两种情况是类似的,思路是获取当前时间前(时间差)的时间到当前时间的日志信息。这里不需要管日志的时序方向,只需要做好始终时间,注意纳秒级别,还有定时器不要忘记销毁。

startInterval(item) {
  this.runStatu = item.label
  console.log(item.value)
  if (item.value !== 0) {
    this.timer = setInterval(() => { // 定时刷新
      this.intervalLogs()
    }, item.value)
  } else {
    console.log('销毁了')
    clearInterval(this.timer)
  }
},
intervalLogs() { // 定时器的方法
  // 组织参数
  // 设置开始时间和结束时间
  // 开始为现在时间
  const start = new Date()
  const end = new Date()
  // 时差判断
  let zone = queryParam.end - queryParam.start
  if (this.timeRange.length) { // 如果是输入时间范围
    zone = ((new Date(this.timeRange[1]).getTime() - new Date(this.timeRange[0]).getTime()) * 1000000).toString()
  }
  if (this.timeZoneValue) {
    zone = this.timeZoneValue * 1000000
  }
  if (zone === 0) { // 防止空指针
    start.setTime(start.getTime() - 3600 * 1000 * 6)
    queryParam.start = (start.getTime() * 1000000).toString()
  } else {
    queryParam.start = (start.getTime() * 1000000 - zone).toString()
  }
  queryParam.end = (end.getTime() * 1000000).toString()
  queryParam.limits = this.limits
  console.log('定时器最后参数:', queryParam)
  var ansi_up = new AnsiUp() // 后端日志格式转化
  logOperation.getLogData(queryParam).then(res => {
    console.log('res', res)
    this.showEmpty = false
    debugger
    if (res.data.result.length === 1) {
      this.logs = res.data.result[0].values
      for (const i in res.data.result[0].values) { // 格式转换
        this.logs[i][1] = ansi_up.ansi_to_html(res.data.result[0].values[i][1])
      }
    } else if (res.data.result.length > 1) {
      // 清空
      this.logs = []
      for (const j in res.data.result) { // 用push的方式将所有日志数组添加进去
        for (const values_index in res.data.result[j].values) {
          this.logs.push(res.data.result[j].values[values_index])
        }
      }
      for (const k in this.logs) {
        this.logs[k][1] = ansi_up.ansi_to_html(this.logs[k][1])
      }
      if (this.direction === 'backward') { // 由于使用公共标签会导致时间顺序错乱,因此对二维数组进行排序
        this.logs.sort((a, b) => b[0] - a[0])
      } else {
        this.logs.sort((a, b) => a[0] - b[0])
      }
    } else {
      this.showEmpty = true
      this.emptyText = '暂无日志信息,请选择时间段试试'
    }
  })
}

最后粘一小段展示的界面

​总结

loki是轻量级的分布式日志查询框架,特别适合中小型企业,尤其是工业项目,在项目上线的时候可以通过这样的一个界面来观察日志,确实能够得到很大的帮助,但是这个loki不是特别的稳定,最为常见的是会出现ERP ERROR,这种错误是最头疼的,个人感觉可能是计算机或者网络的因素造成。

这篇文章出的比较不容易,希望读者详细看一下,如果有问题可以直接提出来,希望读者能学到新的知识,让我们一起学习!如果觉得还不错或者有用处的话,希望能够给我点个赞点个收藏,谢谢观看!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/13799.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文精读《OFT: Orthographic Feature Transform for Monocular 3D Object Detection》

OFT: Orthographic Feature Transform for Monocular 3D Object Detection 文章目录OFT: Orthographic Feature Transform for Monocular 3D Object Detection论文精读摘要&#xff08;Abstract&#xff09;1. 介绍&#xff08;Introduction&#xff09;2. 相关工作&#xff08…

给开源项目做一个漂亮简洁的版本迭代更新图,生成固定链接复制到介绍中、公众号菜单链接中、博客中等

背景 开源项目的版本迭代与更新经常需要更新迭代文档&#xff0c;但是readme.md没有比较美观一点的效果&#xff0c;所以文本分享一种第三方的方式&#xff1a;用TexSpire的免费在线文档分享功能&#xff0c;手机、PC、Pad都可以适配。 效果预览 使用 第一步&#xff1a;创…

浅谈 async/await 和生成器

浅谈 async/await async/await 是ES8规范新增的&#xff0c;使得以同步方式写的代码异步运行不再是白日梦&#xff0c;进一步让代码逻辑更加清晰。 为什么新增 async/await 下面有这样一个需求&#xff1a;有两个请求&#xff0c;请求 1 的结果是请求 2 的参数&#xff0c;所…

机器学习6——EM算法与高斯混合模型GMM

前置内容 Jensen不等式 高斯混合模型 多元高斯模型 拉格朗日乘子法 主要内容 EM算法&#xff08;Expectation-Maximization&#xff09;&#xff0c;期望-最大化。 用于保证收敛到MLE&#xff08;最大似然估计&#xff09;。主要用于求解包含隐变量的混合模型&#xff0c;主要…

R生成三线表

R生成三线表table1包测试数据生成制作三线表Tableone包加载包 探索数据类型数据整理分类构建Table函数Tableone函数细节主要基于table1 或tableone 包table1包 测试数据生成 datadata.frame(性别sample(c("男","女"), 1000,replaceT),年龄round(rnorm(10…

2021年认证杯SPSSPRO杯数学建模A题(第一阶段)医学图像的配准全过程文档及程序

2021年认证杯SPSSPRO杯数学建模 A题 医学图像的配准 原题再现&#xff1a; 图像的配准是图像处理领域中的一个典型问题和技术难点&#xff0c;其目的在于比较或融合同一对象在不同条件下获取的图像。例如为了更好地综合多种信息来辨识不同组织或病变&#xff0c;医生可能使用…

5年自动化测试,终于进字节跳动了,年薪30w其实也并非触不可及

一些碎碎念 什么都做了&#xff0c;和什么都没做其实是一样的&#xff0c;走出“瞎忙活”的安乐窝&#xff0c;才是避开弯路的最佳路径。希望我的经历能帮助到有需要的朋友。 在测试行业已经混了5个年头了&#xff0c;以前经常听到开发对我说&#xff0c;天天的点点点有意思没…

java计算机毕业设计springboot+vue+elementUI永加乡精准扶贫信息管理系统

项目介绍 系统设计的主要意义在于&#xff0c;一方面&#xff0c;对于网站来讲&#xff0c;系统上线后可以带来很大的便利性&#xff0c;精准扶贫网站管理属于非常细致的管理模式&#xff0c;要求数据量大&#xff0c;计算机管理可以提高精确性&#xff0c;更为便利的就是信息…

NF-κB 信号通路调节细胞因子转录

NF-κB 大家族哺乳动物 NF-κB 家族由五种成员组成&#xff1a;RelA/p65、c-Rel、RelB、p50 (NF-κB1) 和 p52 (NF-κB2)&#xff0c;它们可以形成各种异源二聚体或者同源二聚体 (如常见 p50/RelA 异源二聚体)&#xff0c;并通过与启动子的 κB 位点结合来激活大量基因。所有 N…

Mysql常用函数

Mysql常用函数 字段拼接(concat) CONCAT() 函数用于将多个字符串连接成一个字符串 格式&#xff1a; select CONCAT(str1,str2,…) from table_name; #查询商品表&#xff0c;返回一列&#xff1a;商品名称&#xff08;价格&#xff09;。 SELECT concat(prod_name,(,prod…

【论文阅读】Weakly Supervised Semantic Segmentation using Out-of-Distribution Data

一篇弱监督分割领域的论文&#xff0c;发表在CVPR2022上&#xff1a; 论文标题&#xff1a; Weakly Supervised Semantic Segmentation using Out-of-Distribution Data 作者信息&#xff1a; 代码地址&#xff1a; https://github.com/naver-ai/w-ood Abstract 作者认为…

专精特新小巨人的申报条件

专精特新企业分为市级专精特新、省级专精特新和国家级专精特新。 在2018年&#xff0c;开展了国家第一批专精特新“小巨人” 企业申报工作。为了引导中小企业积极走“专精特新”发展之路&#xff0c;加快新旧动能转 换步伐&#xff0c;提升自主创新能力、加快转型升级&#xf…

软考的网络工程师对就业有用吗?

考证只是一个结果&#xff0c;学会技能才是最重要的。 视工作而言&#xff0c;软考中级网络工程师的性价比还是非常高的&#xff0c;对于从事同类的技术人员&#xff0c;基础扎实一般可以裸考通过。 含金量嘛&#xff0c;计算机专业可以以考代凭&#xff0c;毕竟证书是人社部和…

安装谷歌服务框架2022最新版本22.45.15失败

在这里(谷歌play服务框架下载安装安卓版-谷歌服务框架2022最新版本(Google Play 服务)下载22.45.15官方手机版-蜻蜓手游网 (qt6.com)http://www.qt6.com/XiaZai/155507.html)下载了谷歌服务框架(Google Play 服务)&#xff0c;其应用信息为&#xff1a; 包名&#xff1a;com.g…

Mutated 源代码解析 client (一)

Mutated , a C project https://github.com/scslab/mutated usage Main function in the client directory, mutated_synthetic.cc Line 14 parse the user arguments, such as “-h, -w, -c” parse_synthetic is implemented in client\opts_synthetic.cc Here, use th…

Dive into TensorFlow系列(3)- 揭开Tensor的神秘面纱

TensorFlow计算图是由op和tensor组成&#xff0c;那么tensor一般都用来代表什么呢&#xff1f;显然&#xff0c;像模型的输入数据、网络权重、输入数据经op处理后的输出结果都需要用张量或特殊张量进行表达。既然tensor在TensorFlow体系架构中如此重要&#xff0c;因此本文将带…

Redis通用命令和key的层级结构

目录 1 Redis数据结构介绍 2 Redis 通用命令 3 Redis命令-Key的层级结构 1 Redis数据结构介绍 Redis是一个key-value的数据库&#xff0c;key一般是String类型&#xff0c;不过value的类型多种多样&#xff1a; value的数据类型共有8种&#xff0c;前面5中为基本数据类型&a…

5000立方米球罐设计

目 录 摘 要 I Abstract II 1 文献综述 1 1.1 课题研究的工程背景及理论、实际意义 1 1.2 球罐用钢 1 1.2.1 球罐用钢基本要求分析 1 1.2.2 国内外球罐的常用钢种 2 1.2.3 几种典型球罐用钢的优劣对比 2 1.3 球罐设计 3 1.3.1 球罐设计的执行标准及法规 3 1.3.2 球壳结构 4 1.3…

通过PLC网关如何实现三菱FX3U的远程上下载程序?

FX3U是三菱推出的高性能PLC品牌。基本性能大幅提升&#xff0c;晶体管输出型的基本单元内置了3轴独立最高100kHz的定位功能&#xff0c;并且增加了新的定位指令&#xff0c;从而使得定位控制功能更加强大&#xff0c;使用更为方便&#xff0c;受到企业的青睐。因此&#xff0c;…

PyQt5 QLabel标签

PyQt5 QLabel标签标签显示标签快捷键功能标签显示 QLabel背景色设置&#xff1a; palette QPalette() # 创建调色板 palette.setColor(QPalette.Window, Qt.green) # 设置调色板属性 label.setPalette(palette) # 标签设置Palette label.setAutoFillBackground(True) # 设为T…