教你巧用webpack在日志中记录文件行号

目录
  • 前言
  • 通过提取 Error 错误栈
  • 通过 webpack 预处理
  • 总结

前言

在做前端项目时,会在各个关键节点打印日志,方便后续数据分析和问题排查。当日志越来越多之后,又会遇到通过日志反查代码所在文件和所在行的场景,于是一个很自然的需求就出来了:

在打印日志的时候,自动注入当前文件名、行号、列号。

举个例子,有个 logger 函数,我们在 index.js 的业务代码某一行添加打印逻辑:

const { logLine } = require('./utils')

function getJuejinArticles() {
  const author = 'keliq'
  const level = 'LV.5'
  // ... 业务代码省略,获取文章列表
  logLine(author, level)
  // ...
}

getJuejinArticles()

正常情况下会输出:

keliq LV.5

但是希望能够输出带文件名和行号,即:

[index.js:7:3] keliq LV.5

表明当前这次打印输出来源于 index.js 文件中的第 7 行第 3 列代码,也就是 logLine 函数所在的具体位置。那如何实现这个需求呢?我的脑海中浮现了两个思路:

通过提取 Error 错误栈

因为 error 错误栈里面天然带有此类信息,可以人工制造了一个 Error,然后捕获它:

exports.logLine = (...args) => {
  try {
    throw new Error()
  } catch (e) {
    console.log(e.stack)
  }
}

仔细观察打印的结果:

Error
    at logLine (/test/src/utils.js:3:11)
    at getJuejinArticles (/test/src/index.js:7:3)
    at Object.<anonymous> (/test/src/index.js:11:1)
    at Module._compile (node:internal/modules/cjs/loader:1105:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47

第三行的内容不正是我们想要的结果吗?只需要把这一行的字符串进行格式化一下,提取出 index.js:7:3 即可:

at getJuejinArticles (/test/src/index.js:7:3)

由于代码结构是这样的:

.
└── src
    ├── index.js
    └── utils.js

只需要改成下面的代码即可:

exports.logLine = (...args) => {
  try {
    throw new Error()
  } catch (e) {
    const lines = e.stack.split('\n')
    const fileLine = lines[2].split('/src/').pop().slice(0, -1)
    console.log(`[${fileLine}]`, ...args)
  }
}

命令行试一试:

$ test node src/index.js
[index.js:7:3] keliq LV.5

问题似乎完美解决,然而还是想的太简单了,上述场景仅限于 node.js 环境,而在 web 环境,所有的产物都会被 webpack 打到一个或多个 js 文件里面,而且做了压缩混淆处理,由于 error 是在运行时被捕获到的 ,所以我没根本无法拿到开发状态下的文件名、行号和列号,如下图所示:

通过 webpack 预处理

那怎么办呢?解铃还须系铃人,既然 webpack 对代码进行了加工处理,那就只能在预处理最开始的阶段介入进来,写一个自定义的 loader 来解析源码文件,拿到文件名、行号和列号。说干就干,创建一个 inject-line.loader.js,写下模板代码:

module.exports = function (content) {
  content = content.toString('utf-8')
  if (this.cacheable) this.cacheable()
  console.log(this.resourcePath) // 打印文件路径
  console.log(content) // 打印文件内容
  return content
}
module.exports.raw = true

然后在 webpack.config.js 中做配置:

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'index.js',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: require.resolve('./loaders/inject-line.loader'),
          },
        ],
      },
    ],
  },
}

一切准备就绪,先运行一下看看输出:

可以看到,index.js 和 utils.js 被自定义的 inject-line.loader.js 给加载到了,通过 this.resourcePath 能够拿到文件名称,行号和列号的话只能通过分析 content 字符串进行提取了,处理的代码如下:

// 拿到文件路径
const fileName = this.resourcePath.split('/src/').pop()
// 文本内容按行处理后再拼接起来
content = content
  .split('\n')
  .map((line, row) => {
    const re = /logLine((.*?))/g
    let result
    let newLine = ''
    let cursor = 0
    while ((result = re.exec(line))) {
      const col = result.index
      newLine += line.slice(cursor, result.index) + `logLine('${fileName}:${row + 1}:${col + 1}', ` + result[1] + ')'
      cursor += col + result[0].length
    }
    newLine += line.slice(cursor)
    return newLine
  })
  .join('\n')

这里面的逻辑,如果光看代码的话可能会云里雾里,其实思路很简单,就是下面这样的:

这样的话,即使代码经过各种压缩转换,也不会改变开发状态下代码所在的文件名、行与列的位置了。打开 webpack 打包后的文件看一下:

到这里,功能就已经开发完了,不过还有一个小小的缺陷就是 logLine 函数名是写死的,能不能让用户自己定义这个函数名呢?当然可以,在 webpack 配置文件中,支持利用 options 属性传递 config 配置参数:

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'index.js',
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: require.resolve('./loaders/inject-line.loader'),
            options: {
              config: {
                name: 'customLogName',
              },
            },
          },
        ],
      },
    ],
  },
}

然后在 inject-line.loader.js 代码中通过 this.query.config 拿到该配置即可,不过正则表达式也要根据这个配置动态创建,字符串替换的时候也要换成该配置变量,最终代码如下:

module.exports = function (content) {
  content = content.toString('utf-8')
  if (this.cacheable) this.cacheable()
  const { name = 'logLine' } = this.query.config || {}
  const fileName = this.resourcePath.split('/src/').pop()
  content = content
    .split('\n')
    .map((line, row) => {
      const re = new RegExp(`${name}\((.*?)\)`, 'g')
      let result
      let newLine = ''
      let cursor = 0
      while ((result = re.exec(line))) {
        const col = result.index
        newLine += line.slice(cursor, result.index) + `${name}('${fileName}:${row + 1}:${col + 1}', ` + result[1] + ')'
        cursor += col + result[0].length
      }
      newLine += line.slice(cursor)
      return newLine
    })
    .join('\n')

  return content
}
module.exports.raw = true

总结

到此这篇关于如何巧用webpack在日志中记录文件行号的文章就介绍到这了,更多相关webpack日志记录文件行号内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2022-11-14

在日志中记录Java异常信息的正确姿势分享

目录 日志中记录Java异常信息 遇到的问题 原因分析 正确的做法 java异常在控制台和日志里面的打印记录 1.e.printStackTrace()打印在哪里 2.e.printStackTrace()打印的内容是什么 3.如果将e.printStackTrace()的信息打印在日志里应该怎么做呢? 日志中记录Java异常信息 遇到的问题 今天遇到一个线上的BUG,在执行表单提交时失败,但是从程序日志中看不到任何异常信息. 在Review源代码时发现,当catch到异常时只是输出了e.get

利用ROW_NUMBER() OVER函数给SQL数据库中每一条记录分配行号的方法

从SQL Server 2005开始,增加了一个新的函数Row_Number(),他的一个很伟大的作用就是可以在数据表中添加一列从1开始的行号,这样大大代替所有多余的代码来产生行号.下面就教大家如何使用Row_Number()函数. 假设数据库中有如下表: 复制代码 代码如下: id    name 1    aa 3    bb 4    cc 执行如下SQL语句后将得到如下结果: SELECT ROW_NUMBER() OVER(ORDER BY id) as rowNum,* FROM 表

MySQL中在查询结果集中得到记录行号的方法

如果需要在查询语句返回的列中包含一列表示该条记录在整个结果集中的行号, ISO SQL:2003 标准提出的方法是提供 ROW_NUMBER() / RANK() 函数. Oracle 中可以使用标准方法(8i版本以上),也可以使用非标准的 ROWNUM : MS SQL Server 则在 2005 版本中提供了 ROW_NUMBER() 函数:但在 MySQL 中似乎还没有这样的系统自带功能.虽然 LIMIT 可以很方便的对返回的结果集数量和位置进行过滤,但过滤出来的记录的行号却没办法被 S

巧用Dictionary实现日志数据批量插入

背景 最近再做一个需求,就是对站点的一些事件进行埋点,说白了就是记录用户的访问行为.那么这些数据怎么保存呢,人家点一下保存一下?显然不合适,肯定是需要批量保存,提高效率. 问题窥探 首先,我想到的是Dictionary,对于C#中的Dictionary类相信大家都不陌生,这是一个Collection(集合)类型,可以通过Key/Value(键值对的形式来存放数据:该类最大的优点就是它查找元素的时间复杂度接近O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率.Dictionary是非线

教你巧用mysql位运算解决多选值存储的问题

目录 一.问题场景 二. 场景分析 1.多字段存储 2.单字段拼接 三.巧用位运算 1.概述 2.sql查询 3.Java解析与计算 4.总结 附MySQL的支持6种位运算 总结 一.问题场景 工作中经常遇到多选值存储问题,例如:用户有多种认证方式,密码认证.短信认证.扫码认证等,一个用户可能只开启了其中某几种认证方式. 二. 场景分析 比较容易理解的两种实现方式,多字段存储.单个字段拼接存储. 1.多字段存储 每种认证方式用一个字段存储,0表示未开启,1表示已开启. 缺点:每增加一种认证方式都

Python统计日志中每个IP出现次数的方法

本文实例讲述了Python统计日志中每个IP出现次数的方法.分享给大家供大家参考.具体如下: 这脚本可用于多种日志类型,本人测试MDaemon的all日志文件大小1.23G左右,分析用时2~3分钟 代码很简单,很适合运维人员,有不足的地方请大家指出哦 #-*- coding:utf-8 -*- import re,time def mail_log(file_path): global count log=open(file_path,'r') C=r'\.'.join([r'\d{1,3}']

使用Python编写提取日志中的中文的脚本的方法

由于工作需要在一大堆日志里面提取相应的一些固定字符,如果单纯靠手工取提取,数据量大,劳心劳力,于是自然而然想到了用Python做一个对应的提取工具,代替手工提取的繁杂,涉及中文字符,正则表达式不好匹配,但不是不可以实现,这个以后优化时再说. 需求描述: 一个父目录中存在多个子文件夹,子文件夹下有多个txt形式化的Log日志,要求从所有地方Log日志中找出CardType=9, CardNo=0时的CardID的值,并将其统计存储到一个文本文件中,要求CardID不能够重复. 需求解析: 首先获取

用vi命令删除日志中的所有内容并对日志进行实时监控

很多时候我们对应用程序的排错需要查看日志文件,然而日志中通常有许多我们以前的应用程序产生的日志,其他的日志过多的时候,有时候看起当前应用程序产生的日志的时候有点费力,这时候我们可能想通过清除先前的日志,使得当前产生的日志看起来清晰: vi test.log :0,$d :wq 注释: :0,$d是删除第0行到最后一行的意思::wq是保存并退出的意思. 然后输入如下命令,就可以实时监控test.log里面的内容了: tail -f test.log 注释:tail命令是查看test.log文件的后

日志中的秘密 Windows登录类型知多少?

不错,Windows为了让你从日志中获得更多有价值的信息,它细分了很多种登录类型,以便让你区分登录者到底是从本地登录,还是从网络登录,以及其它更多的登录方式.因为了解了这些登录方式,将有助于你从事件日志中发现可疑的黑客行为,并能够判断其攻击方式.下面我们就来详细地看看Windows的登录类型. 登录类型2:交互式登录(Interactive) 这应该是你最先想到的登录方式吧,所谓交互式登录就是指用户在计算机的控制台上进行的登录,也就是在本地键盘上进行的登录,但不要忘记通过KVM登录仍然属于交互式

HTTPERR的日志中出现大量Timer_MinBytesPerSecond,Timer_ConnectionIdle错误

在\LogFiles\HTTPERR的日志中发现了大量Timer_MinBytesPerSecond,Timer_ConnectionIdle错误, 根据网上的介绍,做了如下更改: 1) 从 IIS 管理器右键单击 本地计算机 选择 属性.勾选允许直接编辑配置数据库. 2) 在记事本中打开 C:\Windows\system32\inetsrv\MetaBase.xml 文件, 搜索MinFileBytesPerSec,将 MinFileBytesPerSec 设置从 240 更改为 0. 搜索