We分析收费了怎么办?自己撸一套小程序日志系统

话说最近微信小程序的We分析不是开始收费了吗,免费版本一天日志额度只有5000条,对于一个高峰期间日活2万用户的小程序来说这点日志额度肯定是不够的,如果你的日志量很少完全可以用We分析的不需要自己折腾。

We分析日志怎么用

直接看小程序的 开发文档 ,对着操作步骤来很简单。想要查看的话去 We分析 网站下的实时日志菜单查看。

看上面截图很明显我的小程序日志已经丢失了,每天都超额上报几万条,这还不是高峰期呢。

想着怎么方便怎么来,大不了加钱买就是的了,不看不知道一看吓一跳,想要满足日常使用需求的话,一年要4288,这不是纯纯的大冤种吗?"打着给公司省钱的旗号"自己搞一套日志系统岂不美滋滋。

说干就干

由于之前搭建过内网的Web日志系统也写过文章,没看过的同学可以移步这里 从零开始搭建一个前端日志框架 ,那么小程序的日志系统还不是手到擒来嘛,日志采集模块直接copy不就好了,等等好像哪里不对,小程序没有 indexDB 没办法持久化存储日志,那用 localStorage 可以吗,可以是可以但是不建议,因为 localStorage 存储容量太小了,存储上限是10MB很容易爆仓。

那我们换个思路,日志不存储了上传失败了大不了不上传总行了吧,毕竟日志只是个辅助工具,不能因为日志搞的太复杂把小程序给搞崩溃了。我的思路就是日志先放在内存中,比如定义一个 logQueue 队列来存储日志,每生成一条日志就push到队列中,然后每隔几秒钟把日志上传到服务器完事,至于日志丢失,丢了就丢了嘛,有舍才有得嘛。

直接上小程序代码,有不懂的可以看注释或者移步上一篇文章 从零开始搭建一个前端日志框架

ts 复制代码
import dayjs from "dayjs"

export default class Logger {
  /** 日志队列 */
  logQueue!: string[]
  /** 单条日志最大长度,超出截取 */
  logMaxLength!: number

  constructor() {
    this.logQueue = []
    this.logMaxLength = 5000 
    this.interceptConsole()
    this.loopUpload()
  }

  /**
   * 重写 console 函数
   */
  interceptConsole() {
    const consoleLog = console.log
    console.log = (...args: any[]) => {
      const now = dayjs().format("YYYY-MM-DD HH:mm:ss:SSS")
      consoleLog.apply(console, [`[${now}] [info]`, ...args])
      this.writeMessage(`[${now}] [info]`, ...args)
    }

    const consoleWarn = console.warn
    console.warn = (...args: any[]) => {
      const now = dayjs().format("YYYY-MM-DD HH:mm:ss:SSS")
      consoleWarn.apply(console, [`[${now}] [warn]`, ...args])
      this.writeMessage(`[${now}] [warn]`, ...args)
    }

    const consoleError = console.error
    console.error = (...args: any[]) => {
      const now = dayjs().format("YYYY-MM-DD HH:mm:ss:SSS")
      consoleError.apply(console, [`[${now}] [error]`, ...args])
      this.writeMessage(`[${now}] [error]`, ...args)
    }
  }

  /**
   * 日志格式化并添加到日志队列中
   * @param prefix 日志前缀,时间戳和日志等级
   * @param args 日志内容,可选参数
   */
  writeMessage(prefix: string, ...args: any[]) {
    let message = prefix

    // 遍历参数,如果是字符串直接相加,如果是其他类型则要JSON转义后相加
    args.forEach((arg) => {
      if (typeof arg === "string") {
        message += " " + arg
      } else {
        try {
          message += " " + JSON.stringify(arg)
        } catch {}
      }
    })

    // 日志内容为空则不上传
    if (message.trim() === prefix) {
      return
    }

    // 日志内容超过最大长度截取,小于0不截断
    if (this.logMaxLength > 0 && message.trim().length > this.logMaxLength) {
      message = message.slice(0, this.logMaxLength)
    }

    this.logQueue.push(message)
  }

  /**
   * 上传日志
   */
  uploadLog() {
    // 日志拷贝一份然后清空
    const logQueue = Object.assign([], this.logQueue)
    this.logQueue = []

    wx.request({
      // 这里地址填写你们实际部署的后端服务接口
      url: "http://127.0.0.1:8080/logger",
      method: "POST",
      data: {
        // 用户id,为了方便在日志系统中找到这个用户,作为搜索条件
        userid: wx.getStorageSync("userid"),
        // 未登录的时候可能没有用户id,但一定有openId
        openid: wx.getStorageSync("openid"),
        logs: logQueue
      },
    })
  }

  /**
   * 循环上传日志
   */
  loopUpload() {
    setInterval(() => {
      this.uploadLog()
    }, 5 * 1000)
  }
}

怎么使用呢?在小程序 app.js 中引入并实例化。后续你在小程序中任何地方调用 console.log 命令都会自动采集日志并上传了,当然了在本地控制台下也会打印出来。

js 复制代码
import Logger from "./logger"

new Logger()

后端服务

在上一篇文章 从零开始搭建一个前端日志框架 里后端服务是用 node.js 写的,考虑到现网一万以上的并发请求,不知道 node.js 能不能扛得住。所以这次打算用 golang 写后端服务,用 lumberjack 对日志进行滚动切片,每个日志最大设置为100MB。然后用 Filebeat 采集日志最终在 Kibana 上查看。

其实我之前也没接触过 golang,不过没关系有 ChatGPT,把我们的需求告诉 ChatGPT,他会帮我们写代码,考虑到很多前端同学都没接触过 golang ,这里我把步骤写的尽量详细点,把踩过的坑都列下。

第一步:下载安装 Go,地址 golang.google.cn/dl/

第二步:新建一个 logger 文件夹,新建一个 main.go 文件,把下面代码复制过去。

go 复制代码
package main  
  
import (  
  "log"  
  "net/http"  
  "os"  
  "github.com/gin-gonic/gin"  
  lumberjack "github.com/natefinch/lumberjack"  
)  
  
// LogRequest 表示请求体的结构体  
type LogRequest struct {  
  OpenId string     `json:"openid"`  
  UserID string     `json:"userid"`  
  Logs   []string   `json:"logs"`  
}  
  
// 全局的日志写入器  
var logWriter *os.File  
  
func main() {  
  // 初始化日志文件的滚动配置  
  logPath := "./logs/web.log"  
  err := os.MkdirAll("./logs", 0755) // 确保logs目录存在  
  if err != nil {  
    log.Fatalf("Failed to create logs directory: %v", err)  
  }  
  
  // 使用lumberjack封装我们的logWriter  
  wrappedWriter := &lumberjack.Logger{  
    Filename:   logPath,  
    MaxSize:    100, // 每个日志文件最大大小,单位MB   
    MaxAge:     28,  // 存储的最大天数
    Compress:   false,  // 过期的日志文件是否压缩
  }  
  wrappedWriter.Rotate() // 立即执行一次滚动,确保从新的文件开始写入  
  
  // 将wrappedWriter的Write方法包装为gin.HandlerFunc以便在路由中使用  
  logMiddleware := func(c *gin.Context) {  
    // 实际的中间件逻辑可以在这里执行,例如记录请求信息等  
    c.Next() // 继续处理请求  
  }  
  
  // 创建Gin路由器实例  
  r := gin.Default()  
  
  // 使用自定义的中间件,可以在这里添加更多中间件逻辑  
  r.Use(logMiddleware)  
  
  // 提供POST请求的/logger接口  
  r.POST("/logger", func(c *gin.Context) {  
    var logReq LogRequest  
    if err := c.ShouldBindJSON(&logReq); err != nil {  
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})  
      return  
    }  
  
    // 将logs数组中的每个日志条目写入文件  
    for _, message := range logReq.Logs {  
      if _, err := wrappedWriter.Write([]byte("[" + logReq.OpenId + "] " + "[" + logReq.UserID + "] " + message + "\n")); err != nil {  
        log.Println(err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write to log file"})  
        return  
      }  
    }  
  
    c.JSON(http.StatusOK, gin.H{"message": "Logs written successfully"})  
  })  
  
  // 启动服务,监听在8080端口  
  if err := r.Run(":8080"); err != nil {  
    log.Fatal("Failed to start server:", err)  
  }  
}

第三步: 在 logger 目录下执行命令 go mod init logger 初始化。

第四步:设置代理地址,go env -w GOPROXY=https://goproxy.cn

第五步:下载依赖,执行命令 go mod tidy

第六步:启动服务,执行命令 go run .\main.go

如果顺利的话且你的小程序也开着的,那么此时在服务器上已经能看到小程序上传的日志了,不顺利的话就告辞了🤪。

ELK

日志目前还是存储在文件中,日志一旦多了就不好查找了,我建议配合ELK去采集日志并查看,这个不会的可以找运维帮忙,最终效果大家看下

如果公司没有运维或者搞不定ELK的话,只想在服务器上看日志怎么办呢?我的建议是按日期每天新建一个文件夹,然后每个 openid 一个文件,这样查找起来也很方便,只需要把 main.go 稍微改造下就可以了。

go 复制代码
package main  
  
import (  
  "log"  
  "net/http"  
  "os"  
  "path/filepath"  
  "time"  
  "github.com/gin-gonic/gin"  
  lumberjack "github.com/natefinch/lumberjack"  
)  
  
// LogRequest 表示请求体的结构体  
type LogRequest struct {  
  OpenID string     `json:"openid"`  
  UserID string     `json:"userid"`  
  Logs   []string   `json:"logs"`  
}  
  
func main() {  
  // 创建Gin路由器实例  
  r := gin.Default()  
  
  // 定义路由处理函数  
  r.POST("/logger", func(c *gin.Context) {  
    var logReq LogRequest  
    if err := c.ShouldBindJSON(&logReq); err != nil {  
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})  
      return  
    }  
  
    // 创建或获取openid对应的文件夹  
    logDir := filepath.Join("./logs", logReq.OpenID)  
    err := os.MkdirAll(logDir, 0755)  
    if err != nil {  
      log.Fatalf("Failed to create logs directory for openid %s: %v", logReq.OpenID, err)  
    }  
  
    // 获取当前日期,并格式化用于文件名  
    today := time.Now().Format("2006-01-02")  
    logFile := filepath.Join(logDir, today+".log")  
  
    // 创建或获取lumberjack Logger实例  
    wrappedWriter := &lumberjack.Logger{  
      Filename:   logFile,  
      MaxSize:    10, // 每个日志文件最大大小,单位MB 
      MaxAge:     28, // 最大保存时间,单位天  
      Compress:   false, // 是否压缩/归档旧文件  
    }  
  
    // 将logs数组中的每个日志条目写入文件  
    for _, message := range logReq.Logs {  
      logEntry := "[" + logReq.OpenID + "] " + "[" + logReq.UserID + "] " + message + "\n"  
      if _, err := wrappedWriter.Write([]byte(logEntry)); err != nil {  
        log.Println(err)  
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write to log file"})  
        return  
      }  
    }  
  
    c.JSON(http.StatusOK, gin.H{"message": "Logs written successfully"})  
  })  
  
  // 启动服务,监听在8080端口  
  if err := r.Run(":8080"); err != nil {  
    log.Fatal("Failed to start server:", err)  
  }  
}

看看最终效果,可以看到日志是以日期维度存储的。

请求鉴权

可以看到目前接口是不需要登录验证的,所有人都可以访问,虽然这个接口是辅助接口不是业务接口,不涉及到业务逻辑,但是如果被人利用一直调用也会把我们服务器写爆的,且会有很多无用的日志内容。但是这个接口加登录token又太麻烦了,我的想法是对Http Referer做检验,小程序接口都会带上 Referer,格式为 servicewechat.com/{appid}/{ve... ,那么对 Referer 中的 appId 做校验就可以满足目前的需求了,当然了如果别人知道了你的小程序 appId,且知道了你是这么校验的,那还是可以伪造的。老规则还是贴完整代码。

go 复制代码
package main  
  
import (  
  "log"  
  "net/http"  
  "os"
	"regexp"
  "github.com/gin-gonic/gin"  
  lumberjack "github.com/natefinch/lumberjack"  
)  
  
// LogRequest 表示请求体的结构体  
type LogRequest struct {  
  OpenId string     `json:"openid"`  
  UserID string     `json:"userid"`  
  Logs   []string   `json:"logs"`  
}

// 假设我们有一个配置文件或环境变量来定义小程序ID  
var appID = "wxf195a50feba1dfc4" // 这里替换为你的小程序ID  
  
// 全局的日志写入器  
var logWriter *os.File  
  
func main() {  
  // 初始化日志文件的滚动配置  
  logPath := "./logs/web.log"  
  err := os.MkdirAll("./logs", 0755) // 确保logs目录存在  
  if err != nil {  
    log.Fatalf("Failed to create logs directory: %v", err)  
  }  
  
  // 使用lumberjack封装我们的logWriter  
  wrappedWriter := &lumberjack.Logger{  
    Filename:   logPath,  
    MaxSize:    100, // 每个日志文件最大大小,单位MB   
    MaxAge:     28,  // 存储的最大天数
    Compress:   false,  // 过期的日志文件是否压缩
  }  
  wrappedWriter.Rotate() // 立即执行一次滚动,确保从新的文件开始写入  
  
  // 将wrappedWriter的Write方法包装为gin.HandlerFunc以便在路由中使用  
  logMiddleware := func(c *gin.Context) {  
    // 实际的中间件逻辑可以在这里执行,例如记录请求信息等  
    c.Next() // 继续处理请求  
  }  
  
  // 创建Gin路由器实例  
  r := gin.Default()  
  
  // 使用自定义的中间件,可以在这里添加更多中间件逻辑  
  r.Use(logMiddleware)  
  
  // 提供POST请求的/logger接口  
  r.POST("/logger", func(c *gin.Context) {  
    referer := c.Request.Header.Get("Referer")  
    if referer == "" {  
      c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Referer header is missing"})  
      return  
    }  
  
    // 正则表达式匹配规则  
    pattern := `^https://servicewechat\.com/` + regexp.QuoteMeta(appID) + `/.*/page-frame\.html$`  
    matched, err := regexp.MatchString(pattern, referer)  
    if err != nil {  
      log.Printf("Error matching referer: %v", err)  
      c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})  
      return  
    }  
    if !matched {  
      c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid referer"})  
      return  
    }
    
    var logReq LogRequest  
    if err := c.ShouldBindJSON(&logReq); err != nil {  
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})  
      return  
    }  
  
    // 将logs数组中的每个日志条目写入文件  
    for _, message := range logReq.Logs {  
      if _, err := wrappedWriter.Write([]byte("[" + logReq.OpenId + "] " + "[" + logReq.UserID + "] " + message + "\n")); err != nil {  
        log.Println(err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write to log file"})  
        return  
      }  
    }  
  
    c.JSON(http.StatusOK, gin.H{"message": "Logs written successfully"})  
  })  
  
  // 启动服务,监听在8080端口  
  if err := r.Run(":8080"); err != nil {  
    log.Fatal("Failed to start server:", err)  
  }  
}

当在外部调用接口时就会报错。

相关推荐
计算机徐师兄36 分钟前
Java基于SSM框架的无中介租房系统小程序【附源码、文档】
java·微信小程序·小程序·无中介租房系统小程序·java无中介租房系统小程序·无中介租房微信小程序
源码哥_博纳软云37 分钟前
JAVA智慧养老养老护理帮忙代办陪诊陪护小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
渊渟岳1 小时前
掌握设计模式--装饰模式
设计模式
zh路西法3 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
夏旭泽4 小时前
设计模式-备忘录模式
设计模式·备忘录模式
蓝染-惣右介4 小时前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式
美美的海顿7 小时前
springboot基于Java的校园导航微信小程序的设计与实现
java·数据库·spring boot·后端·spring·微信小程序·毕业设计
捕鲸叉8 小时前
C++软件设计模式之类型模式和对象型模式
开发语言·c++·设计模式
诸葛悠闲9 小时前
设计模式——组合模式
设计模式·组合模式