Go项目实现日志按时间及文件大小切割并压缩

news2024/9/24 5:24:31

关于日志的一些问题:

单个文件过大会影响写入效率,所以会做拆分,但是到多大拆分? 最多保留几个日志文件?最多保留多少天,要不要做压缩处理?

一般都使用 lumberjack[1]这个库完成上述这些操作


lumberjack


 //info文件writeSyncer
 infoFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
  Filename:   "./log/info.log"//日志文件存放目录,如果文件夹不存在会自动创建
  MaxSize:    2,                //文件大小限制,单位MB
  MaxBackups: 100,              //最大保留日志文件数量
  MaxAge:     30,               //日志文件保留天数
  Compress:   false,            //是否压缩处理
 })
 infoFileCore := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(infoFileWriteSyncer, zapcore.AddSync(os.Stdout)), lowPriority) //第三个及之后的参数为写入文件的日志级别,ErrorLevel模式只记录error级别的日志
 //error文件writeSyncer
 errorFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
  Filename:   "./log/error.log"//日志文件存放目录
  MaxSize:    1,                 //文件大小限制,单位MB
  MaxBackups: 5,                 //最大保留日志文件数量
  MaxAge:     30,                //日志文件保留天数
  Compress:   false,             //是否压缩处理
 })

测试日志到达指定大小后自动会切分


alt

例如,当info级别的日志文件到达2M时,会根据当时的时间戳,切分出一个info-2023-04-13T05-27-18.296.log。 后续新写入的info级别的日志将写入到info.log,直到又到达2M,继续会切分。


测试日志到达指定最大保留日志文件数量后,将作何操作


清掉log文件夹,修改error日志配置:

 errorFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
  Filename:   "./log/error.log"//日志文件存放目录
  MaxSize:    1,                 //文件大小限制,单位MB
  MaxBackups: 3,                 //最大保留日志文件数量
  MaxAge:     30,                //日志文件保留天数
  Compress:   false,             //是否压缩处理
 })

代码中只打印error日志,执行代码 进行观察

alt

继续执行

alt

继续执行

alt
alt

可见最早拆分出的那个error-2023-04-13T05-40-48.715.log文件不见了~

继续执行,切分出来的文件数量,也会始终保持3个


完整变化图:

alt

测试压缩处理的效果


清掉log文件夹,修改error日志配置:

 errorFileWriteSyncer := zapcore.AddSync(&lumberjack.Logger{
  Filename:   "./log/error.log"//日志文件存放目录
  MaxSize:    5,                 //文件大小限制,单位MB
  MaxBackups: 10,                 //最大保留日志文件数量
  MaxAge:     30,                //日志文件保留天数
  Compress:   false,             //是否压缩处理
 })

代码中只打印error日志,执行代码,循环10000000次, 进行观察

alt

不压缩共占用814M存储空间


清掉log文件夹,修改Compress字段为true,执行代码:

alt

启用压缩后,仅占用了30M磁盘空间!

不太好的地方就是不方便直接查看了,需要解压后查看。但大大省了所占用的空间


golang zap日志库使用[2]


lumberjack这个库目前只支持按文件大小切割(按时间切割效率低且不能保证日志数据不被破坏,详情见https://github.com/natefinch/lumberjack/issues/54)

想按日期切割可以使用github.com/lestrrat-go/file-rotatelogs[3]这个库(目前不维护了)




file-rotatelogs实现按时间的切割


注意:

github.com/lestrrat-go/file-rotatelogs[4](2021年后不更新了) 和 github.com/lestrrat/go-file-rotatelogs[5](2018年以后就不更新了) 两个不一样。。前面那个是更新的,作者是一个人...

(有一个linux系统上的日志工具,也叫logrotate)

logrotate 是一个用于日志文件轮换的 Go 语言库,支持按时间轮换、按文件大小轮换和按行数轮换。还支持在轮换时压缩文件、删除旧文件、给文件添加时间戳等功能

用zap和go-file-rotatelogs实现日志的记录和日志按时间分割[6]


WithRotationCount和WithMaxAge两个选项不能共存,只能设置一个(都设置编译时不会出错,但运行时会报错。也是为了防止影响切分的处理逻辑):

panic: options MaxAge and RotationCount cannot be both set

package main

import (
 "fmt"
 "io"
 "net/http"
 "time"

 rotatelogs "github.com/lestrrat-go/file-rotatelogs"
 "go.uber.org/zap"
 "go.uber.org/zap/zapcore"
)

// 使用file-rotatelogs做切分

var sugarLogger *zap.SugaredLogger

func main() {
 fmt.Println("shuang提示:begin main")
 InitLogger()
 defer sugarLogger.Sync()

 for i := 0; i < 100000; i++ {
  simpleHttpGet("www.cnblogs.com")
  simpleHttpGet("https://www.baidu.com")
 }

}

// 例子,http访问url,返回状态
func simpleHttpGet(url string) {
 fmt.Println("begin simpleHttpGet:" + url)
 sugarLogger.Debugf("Trying to hit GET request for %s", url)
 resp, err := http.Get(url)
 if err != nil {
  sugarLogger.Errorf("Error fetching URL %s : Error = %s", url, err)
 } else {
  sugarLogger.Infof("Success! statusCode = %s for URL %s", resp.Status, url)
  resp.Body.Close()
 }
}

func InitLogger() {
 encoder := getEncoder()

 //两个interface,判断日志等级
 //warnlevel以下归到info日志
 infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  return lvl < zapcore.WarnLevel
 })
 //warnlevel及以上归到warn日志
 warnLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  return lvl >= zapcore.WarnLevel
 })

 infoWriter := getLogWriter("/Users/fliter/zap-demo/demo2-log/info")
 warnWriter := getLogWriter("/Users/fliter/zap-demo/demo2-log/warn")

 //创建zap.Core,for logger
 core := zapcore.NewTee(
  zapcore.NewCore(encoder, infoWriter, infoLevel),
  zapcore.NewCore(encoder, warnWriter, warnLevel),
 )
 //生成Logger
 logger := zap.New(core, zap.AddCaller())
 sugarLogger = logger.Sugar()
}

// getEncoder
func getEncoder() zapcore.Encoder {
 encoderConfig := zap.NewProductionEncoderConfig()
 encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
 return zapcore.NewConsoleEncoder(encoderConfig)
}

// 得到LogWriter
func getLogWriter(filePath string) zapcore.WriteSyncer {
 warnIoWriter := getWriter(filePath)
 return zapcore.AddSync(warnIoWriter)
}

// 日志文件切割
func getWriter(filename string) io.Writer {

 //保存日志30天,每1分钟分割一次日志
 hook, err := rotatelogs.New(
  filename+"_%Y-%m-%d %H:%M:%S.log",
  // 为最新的日志建立软连接,指向最新日志文件
  rotatelogs.WithLinkName(filename),

  // 清理条件: 将已切割的日志文件按条件(数量or时间)直接删除
  //--- MaxAge and RotationCount cannot be both set  两者不能同时设置
  //--- RotationCount用来设置最多切割的文件数(超过的会被 从旧到新 清理)
  //--- MaxAge 是设置文件清理前的最长保存时间 最小分钟为单位
  //--- if both are 0, give maxAge a default 7 * 24 * time.Hour
  // WithRotationCount和WithMaxAge两个选项不能共存,只能设置一个(都设置编译时不会出错,但运行时会报错。也是为了防止影响切分的处理逻辑)
  //rotatelogs.WithRotationCount(10),       // 超过这个数的文件会被清掉
  rotatelogs.WithMaxAge(time.Hour*24*30), // 保存多久(设置文件清理前的最长保存时间 最小分钟为单位)

  // 切分条件(将日志文件做切割;WithRotationTime and WithRotationSize ~~两者任意一个条件达到都会切割~~)
  // 经过亲测后发现,如果日志没有持续增加,WithRotationTime设置较小(如10s),并不会按WithRotationTime频次切分文件。当日志不停增加时,会按照WithRotationTime设置来切分(即便WithRotationTime设置的很小)
  rotatelogs.WithRotationTime(time.Second*10),     // 10秒分割一次(设置日志切割时间间隔,默认 24 * time.Hour)
  rotatelogs.WithRotationSize(int64(1*1024*1024*1024)), // 文件达到多大则进行切割,单位为 bytes;
 )
 if err != nil {
  panic(err)
 }
 return hook
}

验证其切分功能:


将触发切分的文件大小设置得很大(110241024*1024 Byte即1 GB),切分时间设置得较小(10秒分割一次),执行代码,可以观察到日志文件的变化:

alt
alt

再将触发切分的文件大小设置得很小(1102450 Byte即50 KB),切分时间设置得较大(24h分割一次),执行代码,清掉之前的日志,再观察到日志文件的变化:

alt
alt

将触发切分的文件大小设置得很小(1102435 Byte即35 KB),同时切分时间也设置得很小(10s分割一次),执行代码,清掉之前的日志,再观察到日志文件的变化:

alt

当前日志容量大于配置的容量时,会生成新的日志文件,如果时间一样,在时间后缀后面会自动加上一个数字后缀,以此区分同一时间的不同日志文件,如果时间不一样,则生成新的时间后缀文件 (golang实现分割日志[7])

日志文件中是会出现有的命中时间规则,有的命中文件大小规则的情况,两者命名格式不同,参考上图


切分之后执行压缩命令


alt

默认是没有的,不像lumberjack那样提供Compress选项

前面所提的还支持在轮换时压缩文件、删除旧文件、给文件添加时间戳等功能需要自己实现。 提供了一个WithHandler回调函数,发生切分后会触发该函数,可以在其中进项压缩等操作

alt

改一下代码(不再请求网站因为速度太慢,直接在for里面写日志)

不启用压缩:

alt

启用压缩,效果显著:

alt

相关代码:

package main

import (
 "archive/zip"
 "fmt"
 "io"
 "net/http"
 "os"
 "path/filepath"
 "reflect"
 "time"

 "github.com/davecgh/go-spew/spew"
 rotatelogs "github.com/lestrrat-go/file-rotatelogs"
 "go.uber.org/zap"
 "go.uber.org/zap/zapcore"
)

// 使用file-rotatelogs做切分

var sugarLogger *zap.SugaredLogger

func main() {
 fmt.Println("shuang提示:begin main")
 InitLogger()
 defer sugarLogger.Sync()

 for i := 0; i < 10000000; i++ {

  sugarLogger.Infof("测试压缩后少占用的空间,这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本这是填充文本,i is %d", i)

  //simpleHttpGet("www.cnblogs.com", i)
  //simpleHttpGet("https://www.baidu.com", i)
 }

 time.Sleep(10000e9)

}

// 例子,http访问url,返回状态
func simpleHttpGet(url string, i int) {
 //fmt.Println("begin simpleHttpGet:" + url)
 sugarLogger.Debugf("Trying to hit GET request for %s, i is %d", url, i)
 resp, err := http.Get(url)
 if err != nil {
  sugarLogger.Errorf("Error fetching URL %s : Error = %s, i is %d", url, err, i)
 } else {
  sugarLogger.Infof("Success! statusCode = %s for URL %s,i is %d", resp.Status, url, i)
  resp.Body.Close()
 }
}

func InitLogger() {
 encoder := getEncoder()

 //两个interface,判断日志等级
 //warnlevel以下归到info日志
 infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  return lvl < zapcore.WarnLevel
 })
 //warnlevel及以上归到warn日志
 warnLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
  return lvl >= zapcore.WarnLevel
 })

 infoWriter := getLogWriter("/Users/fliter/zap-demo/demo2-log/info")
 warnWriter := getLogWriter("/Users/fliter/zap-demo/demo2-log/warn")

 //创建zap.Core,for logger
 core := zapcore.NewTee(
  zapcore.NewCore(encoder, infoWriter, infoLevel),
  zapcore.NewCore(encoder, warnWriter, warnLevel),
 )
 //生成Logger
 logger := zap.New(core, zap.AddCaller())
 sugarLogger = logger.Sugar()
}

// getEncoder
func getEncoder() zapcore.Encoder {
 encoderConfig := zap.NewProductionEncoderConfig()
 encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
 return zapcore.NewConsoleEncoder(encoderConfig)
}

// 得到LogWriter
func getLogWriter(filePath string) zapcore.WriteSyncer {
 warnIoWriter := getWriter(filePath)
 return zapcore.AddSync(warnIoWriter)
}

// 日志文件切割
func getWriter(filename string) io.Writer {

 //保存日志30天,每1分钟分割一次日志
 hook, err := rotatelogs.New(
  filename+"_%Y-%m-%d %H:%M:%S.log",
  // 为最新的日志建立软连接,指向最新日志文件
  rotatelogs.WithLinkName(filename),

  // 清理条件: 将已切割的日志文件按条件(数量or时间)直接删除
  //--- MaxAge and RotationCount cannot be both set  两者不能同时设置
  //--- RotationCount用来设置最多切割的文件数(超过的会被 从旧到新 清理)
  //--- MaxAge 是设置文件清理前的最长保存时间 最小分钟为单位
  //--- if both are 0, give maxAge a default 7 * 24 * time.Hour
  // WithRotationCount和WithMaxAge两个选项不能共存,只能设置一个(都设置编译时不会出错,但运行时会报错。也是为了防止影响切分的处理逻辑)
  //rotatelogs.WithRotationCount(10),       // 超过这个数的文件会被清掉
  rotatelogs.WithMaxAge(time.Hour*24*30), // 保存多久(设置文件清理前的最长保存时间 最小分钟为单位)

  // 切分条件(将日志文件做切割;WithRotationTime and WithRotationSize ~~两者任意一个条件达到都会切割~~)
  // 经过亲测后发现,如果日志没有持续增加,WithRotationTime设置较小(如10s),并不会按WithRotationTime频次切分文件。当日志不停增加时,会按照WithRotationTime设置来切分(即便WithRotationTime设置的很小)
  rotatelogs.WithRotationTime(time.Second*10),           // 10秒分割一次(设置日志切割时间间隔,默认 24 * time.Hour)
  rotatelogs.WithRotationSize(int64(1*1024*35000*1024)), // 文件达到多大则进行切割,单位为 bytes;

  // 其他可选配置
  //default: rotatelogs.Local ,you can set rotatelogs.UTC
  //rotatelogs.WithClock(rotatelogs.UTC),
  //rotatelogs.WithLocation(time.Local),
  //--- 当rotatelogs.New()创建的文件存在时,强制创建新的文件 命名为原文件的名称+序号,如a.log存在,则创建创建 a.log.1
  //rotatelogs.ForceNewFile(),

  rotatelogs.WithHandler(rotatelogs.Handler(rotatelogs.HandlerFunc(func(e rotatelogs.Event) {
   if e.Type() != rotatelogs.FileRotatedEventType {
    return
   }

   fmt.Println("切割完成,进行打包压缩操作")

   spew.Dump("e is:", e)

   prevFile := e.(*rotatelogs.FileRotatedEvent).PreviousFile()

   if prevFile != "" {
    // 进行压缩
    paths, fileName := filepath.Split(prevFile)
    //_ = paths
    //err := Zip("archive.zip", paths, prevFile)
    err := ZipFiles(paths+fileName+".zip", []string{prevFile})
    fmt.Println("err is", err)

    if err == nil {
     os.RemoveAll(prevFile)
    }

   }

   fmt.Println("e的类型为:", reflect.TypeOf(e))

   fmt.Println("------------------")
   fmt.Println()
   fmt.Println()
   fmt.Println()

   //ctx := CleanContext{
   // Dir:         LogsConfig.LogOutputDir,
   // DirMaxSizeG: LogsConfig.LogDirMaxSizeG,
   // DirMaxCount: LogsConfig.LogDirMaxFileCount,
   //}
   //strategyOne := CleanStrategyOne{}
   //result, err := NewCleanStrategy(&ctx, &strategyOne).
   // Clean().
   // Result()
   //Warn("文件切割,清理文件策略one已经执行完毕; 结果:%v; 错误:%v", result, err)
  }))),
 )

 if err != nil {
  panic(err)
 }
 return hook
}

// ZipFiles compresses one or many files into a single zip archive file.
// Param 1: filename is the output zip file's name.
// Param 2: files is a list of files to add to the zip.
func ZipFiles(filename string, files []string) error {

 newZipFile, err := os.Create(filename)
 if err != nil {
  return err
 }
 defer newZipFile.Close()

 zipWriter := zip.NewWriter(newZipFile)
 defer zipWriter.Close()

 // Add files to zip
 for _, file := range files {
  if err = AddFileToZip(zipWriter, file); err != nil {
   return err
  }
 }
 return nil
}

func AddFileToZip(zipWriter *zip.Writer, filename string) error {

 fileToZip, err := os.Open(filename)
 if err != nil {
  return err
 }
 defer fileToZip.Close()

 // Get the file information
 info, err := fileToZip.Stat()
 if err != nil {
  return err
 }

 header, err := zip.FileInfoHeader(info)
 if err != nil {
  return err
 }

 // Using FileInfoHeader() above only uses the basename of the file. If we want
 // to preserve the folder structure we can overwrite this with the full path.
 header.Name = filename

 // Change to deflate to gain better compression
 // see http://golang.org/pkg/archive/zip/#pkg-constants
 header.Method = zip.Deflate

 writer, err := zipWriter.CreateHeader(header)
 if err != nil {
  return err
 }
 _, err = io.Copy(writer, fileToZip)
 return err
}

//
 Zip compresses the specified files or dirs to zip archive.
 If a path is a dir don't need to specify the trailing path separator.
 For example calling Zip("archive.zip", "dir", "csv/baz.csv") will get archive.zip and the content of which is
 baz.csv
 dir
 ├── bar.txt
 └── foo.txt
 Note that if a file is a symbolic link it will be skipped.
//
 https://blog.csdn.net/K346K346/article/details/122441250
//func Zip(zipPath string, paths ...string) error {
// // Create zip file and it's parent dir.
// if err := os.MkdirAll(filepath.Dir(zipPath), os.ModePerm); err != nil {
//  return err
// }
// archive, err := os.Create(zipPath)
// if err != nil {
//  return err
// }
// defer archive.Close()
//
// // New zip writer.
// zipWriter := zip.NewWriter(archive)
// defer zipWriter.Close()
//
// // Traverse the file or directory.
// for _, rootPath := range paths {
//  // Remove the trailing path separator if path is a directory.
//  rootPath = strings.TrimSuffix(rootPath, string(os.PathSeparator))
//
//  // Visit all the files or directories in the tree.
//  err = filepath.Walk(rootPath, walkFunc(rootPath, zipWriter))
//  if err != nil {
//   return err
//  }
// }
// return nil
//}
//
//func walkFunc(rootPath string, zipWriter *zip.Writer) filepath.WalkFunc {
// return func(path string, info fs.FileInfo, err error) error {
//  if err != nil {
//   return err
//  }
//
//  // If a file is a symbolic link it will be skipped.
//  if info.Mode()&os.ModeSymlink != 0 {
//   return nil
//  }
//
//  // Create a local file header.
//  header, err := zip.FileInfoHeader(info)
//  if err != nil {
//   return err
//  }
//
//  // Set compression method.
//  header.Method = zip.Deflate
//
//  // Set relative path of a file as the header name.
//  header.Name, err = filepath.Rel(filepath.Dir(rootPath), path)
//  if err != nil {
//   return err
//  }
//  if info.IsDir() {
//   header.Name += string(os.PathSeparator)
//  }
//
//  // Create writer for the file header and save content of the file.
//  headerWriter, err := zipWriter.CreateHeader(header)
//  if err != nil {
//   return err
//  }
//  if info.IsDir() {
//   return nil
//  }
//  f, err := os.Open(path)
//  if err != nil {
//   return err
//  }
//  defer f.Close()
//  _, err = io.Copy(headerWriter, f)
//  return err
// }
//}


完整demo项目代码 以zap为例,展示如何切割日志文件。 使用Go生态两个使用最高的切分库[8]


关于压缩:

压缩解压文件[9]

Golang 学习笔记(五)- archive/zip 实现压缩及解压[10]

Golang zip 压缩与解压[11]




更多参考:

zap日志切割,同时支持按日期拆分,也支持按日志固定大小拆分,支持定时清理[12]

go-logrus 日志框架封装使用[13]

Go zap日志[14]

设计自用的golang日志模块[15]

golang log rotate file[16]

golang高性能日志库zap的使用[17]

参考资料

[1]

lumberjack: https://github.com/natefinch/lumberjack

[2]

golang zap日志库使用: https://segmentfault.com/a/1190000040443996

[3]

github.com/lestrrat-go/file-rotatelogs: https://github.com/lestrrat/go-file-rotatelogs

[4]

github.com/lestrrat-go/file-rotatelogs: https://github.com/lestrrat-go/file-rotatelogs

[5]

github.com/lestrrat/go-file-rotatelogs: https://github.com/lestrrat/go-file-rotatelogs

[6]

用zap和go-file-rotatelogs实现日志的记录和日志按时间分割: https://blog.csdn.net/weixin_43881017/article/details/110200176

[7]

golang实现分割日志: https://blog.csdn.net/qq_42119514/article/details/121372416

[8]

以zap为例,展示如何切割日志文件。 使用Go生态两个使用最高的切分库: https://github.com/cuishuang/zap-demo/tree/main

[9]

压缩解压文件: https://www.topgoer.com/%E5%85%B6%E4%BB%96/%E5%8E%8B%E7%BC%A9%E8%A7%A3%E5%8E%8B%E6%96%87%E4%BB%B6.html

[10]

Golang 学习笔记(五)- archive/zip 实现压缩及解压: https://learnku.com/articles/23434/golang-learning-notes-five-archivezip-to-achieve-compression-and-decompression

[11]

Golang zip 压缩与解压: https://blog.csdn.net/K346K346/article/details/122441250

[12]

zap日志切割,同时支持按日期拆分,也支持按日志固定大小拆分,支持定时清理: https://blog.csdn.net/qq_22186119/article/details/122003691

[13]

go-logrus 日志框架封装使用: https://www.jianshu.com/p/722250f0b609

[14]

Go zap日志: https://blog.csdn.net/qq_41004932/article/details/119760061

[15]

设计自用的golang日志模块: https://studygolang.com/articles/12537

[16]

golang log rotate file: https://juejin.cn/s/golang%20log%20rotate%20file

[17]

golang高性能日志库zap的使用: https://www.jianshu.com/p/910b626f67d9

本文由 mdnice 多平台发布

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

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

相关文章

linux用户密码存放在哪个文件

linux用户密码存放在“/etc/shadow”文件中。“/etc/shadow”文件又称为“影子文件”&#xff0c;用于存储Linux系统中用户的密码信息&#xff1b;该文件只有root用户拥有读权限&#xff0c;其他用户没有任何权限&#xff0c;这样就保证了用户密码的安全性。 本教程操作环境&am…

IOS看书最终选择|源阅读转换|开源阅读|IOS自签

环境&#xff1a;IOS想使用 换源阅读 问题&#xff1a;换新手机&#xff0c;源阅读下架后&#xff0c;没有好的APP阅读小说 解决办法&#xff1a;自签APP 转换源仓库书源 最终预览 &#xff1a;https://rc.real9.cn/ 背景&#xff1a;自从我换了新iPhone手机&#xff0c;就无法…

iOS 搭建组件化私有库

一、创建私有库索引 步骤1是在没有索引库的情况下或者是新增索引的时候才需要用到&#xff08;创建基础组件库&#xff09; 首先在码云上建立一个私有库索引&#xff0c;起名为SYComponentSpec 二、本地添加私有库索引 添加私有库索引 pod repo add SYComponentSpec https:/…

fiddler 手机抓包(含https) 完整流程

第一部分&#xff1a;下载并安装fiddler 一.使用任一浏览器搜索【fiddler下载安装】&#xff0c;并下载fiddler 安装包。 二.fiddler安装包下载成功后&#xff0c;将下载的fiddler压缩包解压到自定义文件夹【fiddler】或者解压到当前文件夹下&#xff0c;双击文件夹中的【fidd…

工作记录------单元测试(持续更新)

工作记录------单元测试 之前的工作中从来没有写过单元测试&#xff0c;新入职公司要求写单元测试&#xff0c; 个人觉得&#xff0c;作为程序员单元测试还是必须会写的 于此记录一下首次编写单元测试的过程。 首先引入单元测试相关的依赖 <dependency><groupId>…

二、数据结构7:KMP 模板题+算法模板(KMP字符串)

文章目录 算法模板KMP题目模板 模板题KMP字符串原题链接题目思路题解 算法模板 KMP题目模板 // s[]是长文本&#xff0c;p[]是模式串&#xff0c;n是s的长度&#xff0c;m是p的长度 求模式串的Next数组&#xff1a; for (int i 2, j 0; i < m; i ) {while (j &&…

Kafka3.0.0版本——Broker(Zookeeper服务端存储的Kafka相关信息)

目录 一、启动zookeeper集群及kafka集群服务启动1.1、先启动三台zookeeper集群服务&#xff0c;再启动三台kafka集群服务1.2、使用PrettyZoo连接zookeeper客户端工具 二、在zookeeper服务端存储的Kafka相关信息 一、启动zookeeper集群及kafka集群服务启动 1.1、先启动三台zook…

一篇文章带你彻底搞懂方法重写(override)和重载(overload)—菜鸟教程学习记录

一篇文章带你彻底搞懂方法重写(override)和重载(overload) ​ 这个其实我们在之前的java方法那一章节里面就提到了&#xff0c;这里更加深入一下&#xff0c;并且提供具体的例子。 重写(Override)基本概念&#xff1a; ​ Java方法**重写(Override)**指的是&#xff0c;子类…

02 笔记本电脑m.2硬盘更换

1 工具展示 SN570的2T硬盘。够用了。 对于这台华为&#xff0c;使用的螺丝刀批头是4或5毫米的六边形批头。如果出现打滑的情况&#xff0c;请不要用蛮力哦。 2 更换过程 使用螺丝刀拧走后盖的螺丝&#xff08;为了避免会出问题要再次打开&#xff0c;我到现在还没有把螺丝拧回…

groovy.lang.GroovyRuntimeException:Ambiguous method

目录 问题 根因及解决 问题 今天线上的计算脚本报了个问题&#xff0c;报警如下是 groovy 运行时异常。调用方法 org.springframework.util.CollectionUtils#isEmpty 出现了异常。异常原因是由于方法重载造成了歧义。无法确定如何调用方法 [null]&#xff0c;因为它同时匹配了…

如何隐藏开源流媒体EasyPlayer.js视频H.265播放器的实时录像按钮?

目前我们TSINGSEE青犀视频所有的视频监控平台&#xff0c;集成的都是EasyPlayer.js版播放器&#xff0c;它属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;包括WebSocket-FLV、HTTP-FLV&#xff0c;HLS&#xff08;m3u8&#x…

Stable diffusion 三大基础脚本 提示词矩阵,载入提示词,XYZ图表讲解

目录 0.本章讲解 1.提示词矩阵(prompt matrix) 1.2.提示词矩阵功能选项 1.2.1.把可变部分放在提示词文本的开头 1.2.2.为每张图片使用不同随机种子 1.2.3.选择提示词 1.2.4.选择分割符 1.2.5.宫格图边框&#xff08;像素&#xff09; 2.从文本框或文件载入提示词(Pro…

0基础学习VR全景平台篇 第75篇:多现场

多现场是指将多台设备的直播画面整合到一个直播活动链接里面&#xff0c;让用户自行选择切换要看哪个直播画面的功能。既可以是同一个活动的不同角度直播&#xff0c;也可以是异地的直播。多现场不需要导播台&#xff0c;并且可以同时支持平面直播和VR直播的混合切换。多现场仅…

excel中使地址按十六进制进行数值递增的函数

这里是尼德兰的喵工具相关文章&#xff0c;欢迎您的访问&#xff01; 如果文章对您有所帮助&#xff0c;期待您的点赞收藏&#xff01; 让我们一起为成为芯片前端全栈工程师而努力&#xff01; 在进行寄存器编写时很多时候会涉及到算地址的问题&#xff0c;通常32bit位宽的寄存…

Gin框架使用

Gin Gin框架安装与使用 下载安装Gin&#xff1a; go get -u github.com/gin-gonic/gin使用示例&#xff1a; package mainimport "github.com/gin-gonic/gin"func main() {//创建一个默认的路由引擎r : gin.Default()//GET:请求方式&#xff0c;/hello:请求路径/…

Vivado进行自定义IP封装

一. 简介 本篇文章将介绍如何使用Vivado来对上篇文章(FPGA驱动SPI屏幕)中的代码进行一个IP封装&#xff0c;Vivado自带的IP核应该都使用过&#xff0c;非常方便。 这里将其封装成IP核的目的主要是为了后续项目的调用&#xff0c;否则当我新建一个项目的时候&#xff0c;我需要将…

实现 rollup 实现多模块打包

rollup 是一个 JavaScript 模块打包器&#xff0c;可以将许多 JavaScript 库和应用程序打包成少量的捆绑包&#xff0c;从而提高了应用程序的性能。本文详细描述如何通过 rollup 实现多模块打包。 前提 项目的目录结构 先看下项目的 package.json 文件夹&#xff1a; {&qu…

LLaMA系列 | LLaMA和LLaMA-2精简总结

文章目录 1、LLaMA1.1、模型结构1.2、训练方式1.3、结论 2、LLaMA-22.1、相比LLaMA1的升级2.3、模型结构2.3.1、MHA, MQA, GQA区别与联系 2.4、训练方式 1、LLaMA &#x1f525; 纯基座语言模型 《LLaMA: Open and Efficient Foundation Language Models》&#xff1a;https:/…

CS5801国产HDMI转DP/edp(4k60)转换器方案芯片 可替代LT6711

CS5801是HDMI2.0b到DP1.4a转换器方案IC。CS5801 有一个HDMI2.0b .输入&#xff0c;带宽高达18Gbps.它支持辨别率是4k60Hz。对于DP1.4输出&#xff0c;由4条数据通道组成&#xff0c;支持1.62Gbps、 2.7Gbps、 5.4Gbps链路速率。内置可选SSC功能可降低EMI影响。嵌入式MCU基于32位…

3d虚拟vr汽车实景展厅吸引更多潜在消费者

随着人们对生活品质的追求&#xff0c;越来越多的消费者开始关注汽车的外观设计、内饰配置等方面。传统的展示方式已经不能满足消费者的需求&#xff0c;车辆VR虚拟漫游展示应运而生。借助VR虚拟现实和web3d开发建模技术&#xff0c;对汽车的外观、造型及信息数据进行数字化处理…