我掌握了少数人才知道持续集成系统的日志密码

安全 应用安全
前段时间在使用 Travis CI 的时候发现它的部署日志包含了很多带色彩的日志。

[[429062]]

前言

前段时间在使用 Travis CI 的时候发现它的部署日志包含了很多带色彩的日志。

并且我们知道,在使用命令行终端的时候也会出现这些可爱的色彩。

当然我不是为了吹它而吹它,它是有实际的作用的,能够帮助我们快速定位问题!

对此我就产生了好奇,Travis CI 是怎么把这些彩色日志搬到浏览器的?

我猜想肯定不是通过对关键字词特征识别来做的,因为那样太 low 了。

进行了查询后,查到了一个终于查到了关键词,它就是 ANSI escape sequences。

ANSI转义序列是带内信令的标准,用于控制终端和终端仿真器上的光标位置,颜色和一些其他选项。--维基百科

通俗地讲,就是那些在终端输出彩色的文字中包含了一些转义序列字符,只不过我们看不到,被终端进行了解析。然后终端将这些字符解析成了我们现在看到的形形色色多彩的日志(包括一些颜色、下划线、粗体等)。

例如,我们在终端进行npm 的安装,git 分支的切换,包括运行报错的时候都能看到。

正是有了这些色彩,让我们的调试工作效率大大提高,一眼便能看到哪些命令出错了,以及如何解决的方案。

现在我们要做的就是如何将这些色彩日志输出到浏览器端。而进行这个步骤之前,我们得先知道,这些ANSI转义序列的形态是什么样子的?

根据wiki我们可以知道 ANSI 转义序列可以操作很多功能,例如光标位置、颜色、下划线和其他选项。下面我们就 颜色部分 来进行讲解。

ANSI 转义序列

ANSI 转义序列 也是跟随着终端的发展而发展,颜色的规范也是随着设备的不同有所区别。例如在早期的设备只支持 3 / 4 Bit ,支持的颜色分别为 8 / 16 种。

ANSI 转义序列大多数以 ESC 和'['开头嵌入到文本中,终端会查找并解释为命令,而不是字符串。

ESC 的 ANSI 值为 27 ,8进制表示为 \033 ,16进制表示为 \u001B。

3/4 bit

原始规格只有 8/16 种颜色。

比如ESC[30;47m 它是以 ESC[ 开头 m 结束,中间为code码,以分号进行分割。

color 取值为30-37,background 取值为 40-47。例如 :

  1. echo -e "\u001B[31m hello" 

(如果想要清除颜色就需要使用 ESC [39;49m(某些终端不支持) 或者ESC[0m )

后来的终端增加了直接指定 90-97 和 100-107 的“明亮”颜色的能力。

效果如下:

以下是其色彩对照表:

8-bit

后来由于256色在显卡上很常见,因此添加了转义序列以从预定义的256种颜色中进行选择,也就是说在原来的书写方式上增加了新的一位来代表更多的颜色。

  1. ESC[ 38;5;<n> m // 设置字体颜色 
  2. ESC[ 48;5;<n> m // 设置背景颜色 
  3.     0-7:  standard colors (as in ESC [ 30–37 m) 
  4.     8-15:  high intensity colors (as in ESC [ 90–97 m) 
  5.     16-231:  6 × 6 × 6 cube (216 colors): 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) 
  6.    232-255:  grayscale from black to white in 24 steps 

在支持更多色彩的终端中,例如:

  1. echo -e "\u001B[38;5;11m hello" 

代表输出黄色字体。

  1. echo -e "\u001B[48;5;14;38;5;13m hello" 

代表输出蓝色背景,粉红色字体。

以下是其色彩对照表:

24-bit

再往后发展就是支持 24 位真彩的显卡,Xterm, KDE 的Konsole,以及所有基于 libvte 的终端(包括GNOME终端)支持24位前景和背景颜色设置。

  1. ESC[ 38;2;<r>;<g>;<b>m // 前景色 
  2. ESC[ 48;2;<r>;<g>;<b>m // 背景色 

例如:

  1. echo -e "\u001B[38;2;100;228;75m hello" 

输出绿色的字体代表 rgb(100,228,75)。

解析工具

我们知道了转义的规范后,那么我们需要将 ANSI 字符进行解析。

由于规范比较多,因此我们先调研一下在 js 中常用的色彩库,来进行一个小小的探索。

由于 3 / 4bit 的兼容性更好,大多数工具(如chalk)会采用这 8 / 16 色来做高亮,因此我们先实现一个 8 / 16 色的解析。

这里参考了 ansiparse 这个解析库:

核心思路为:

  1. const ansiparse = require('ansiparse'
  2.  
  3. const ansiStr = "\u001B[34mHello \u001B[39m World \u001B[31m! \u001B[39m" 
  4.  
  5. const json = ansiparse(ansiStr) 
  6. console.log(json) 
  7.  
  8. // json输出如下: 
  9.   { foreground: 'blue', text: 'Hello ' }, 
  10.   { text: ' World ' }, 
  11.   { foreground: 'red', text: '! ' } 

然后我们可以写一个函数来遍历上面解析得到的 JSON数组,输出 HTML。

  1. function createHtml(ansiList, wrap = '') { 
  2.     let html = ''
  3.     for (let i = 0; i < ansiList.length; i++) { 
  4.         const htmlFrame = ansiList[i]; 
  5.  
  6.         const {background = '', text, foreground = ''} = htmlFrame; 
  7.         if(background && foreground) { 
  8.             if(text.includes('\n')) { 
  9.                 html += wrap; 
  10.                 continue
  11.             } 
  12.             html += fontBgCode(text, foreground, background); 
  13.             continue
  14.         } 
  15.         if (background || foreground) { 
  16.             const color = background ? `bg-${background}` : foreground; 
  17.             let textColor = bgCode(text, color); 
  18.  
  19.             textColor = textColor.replace(/\n/g, wrap); 
  20.              
  21.             html += textColor; 
  22.             continue
  23.         } 
  24.         if (text.includes('\n')) { 
  25.             const textColor = text.replace(/\n/g, wrap); 
  26.             html += textColor; 
  27.             continue
  28.         } 
  29.         html += singleCode(text); 
  30.     } 
  31.     html += '' 
  32.     return html; 
  33.  
  34. function fontBgCode(value, color, bgColor) { 
  35.     return `<span class="${color} bg-${bgColor}">${value}</span>` 
  36.  
  37. function bgCode(value, color) { 
  38.     return `<span class="${color}">${value}</span>` 
  39.  
  40. function singleCode(value) { 
  41.     return `<span>${value}</span>` 

使用示例如下:

  1. const str = "\u001B[34mHello \u001B[39m World \u001B[31m! \u001B[39m"
  2.  
  3. console.log(createHtml(parseAnsi(str))); 
  4.  
  5. // <span class="blue">Hello</span><span> World</span><span class="red">!</span> 

部署实战

有了上面的部分我们就来用一个简单的demo实际演示一下部署日志吧!

  1. // 项目目录结构 
  2. demo 
  3.  |- package.json 
  4.  |- index.html 
  5.  |- webpack.config.js 
  6.  |- /src 
  7.    |- index.js 
  8. index.js 
  9. build.sh 

我们在 index.js 中启动一个 build 脚本,来模拟一下我们真实的部署场景。

  1. const { spawn } = require('child_process'); 
  2. const cmd = spawn('sh', ['build.sh']); 
  3.  
  4. cmd.stdout.on('data', (data) => { 
  5.   console.log(`stdout: ${data}`); 
  6. }); 
  7.  
  8. cmd.stderr.on('data', (data) => { 
  9.   console.log(`stderr: ${data}`); 
  10. }); 
  11.  
  12. cmd.on('close', (code) => { 
  13.   console.log(`child process exited with code ${code}`); 
  14. }); 
  15. // build.sh 
  16.  
  17. cd demo 
  18.  
  19. npx webpack 

我们在终端尝试一下,控制台输入 node index.js

发现在输出的日志中,并没有看到对应的色彩。

为什么从 child_process 为什么无法输出色彩,而我们如果在终端中直接打包项目却能够输出色彩呢?

Why?

第一反应就是去查找根源,也就是使用频率最高的几个色彩输出的库。

以简单的方式给控制台的输出标记颜色。

https://github.com/Marak/colors.js

https://github.com/chalk/chalk

在看了webpack-cli的源码后,查到它是用了colorette作为色彩输出库的。

那么我们就来查看一下colorette的源码一探究竟。

在入口文件的开头就看到一个变量isColorSupported来判断是否支持色彩输出。

https://github.com/jorgebucaran/colorette/blob/main/index.js#L17

  1. // colorette/index.js 
  2. import * as tty from "tty" 
  3.  
  4. const env = process.env || {} 
  5. const argv = process.argv || [] 
  6.  
  7. const isDisabled = "NO_COLOR" in env || argv.includes("--no-color"
  8.  
  9. const isForced = "FORCE_COLOR" in env || argv.includes("--color"
  10. const isWindows = process.platform === "win32" 
  11. const isCompatibleTerminal = tty && tty.isatty && tty.isatty(1) && env.TERM && env.TERM !== "dumb" 
  12. const isCI = "CI" in env && ("GITHUB_ACTIONS" in env || "GITLAB_CI" in env || "CIRCLECI" in env) 
  13.  
  14. export const isColorSupported = !isDisabled && (isForced || isWindows || isCompatibleTerminal || isCI) 

可以看到这种工具判断了很多条件,来对我们的输出流进行处理。

在以上条件成立下,才会输出 ANSI 日志。在不满足以上情况的条件下,就会切换输出更容易解析的方式。

const isWindows = process.platform === "win32"

参考:https://stackoverflow.com/questions/8683895/how-do-i-determine-the-current-operating-system-with-node-js

dumb: "哑终端"

哑终端指不能执行诸如“删行”、“清屏”或“控制光标位置”的一些特殊ANSI转义序列的计算机终端

参考:https://zh.wikipedia.org/wiki/%E5%93%91%E7%BB%88%E7%AB%AF

也就是说我们的 child_process 的输出流关闭了终端模式(TTY),上面的四种情况都不满足。所以我们得不到带有 ANSI 的色彩日志。

How?

我们可以显示传入环境变量 FORCE_COLOR=1 或者命令带上参数 --color 强制启动颜色来解决这个问题。

这样我们就拿到了带有 ANSI 颜色信息的输出文本,最终解析得到 HTML。

  1. <div>asset <span class="green">main.js</span><span> 132 bytes </span><span class="yellow">[compared for emit]</span><span> </span><span class="green">[minimized]</span> (name: main)</div><div><span>./src/index.js</span><span> 289 bytes </span><span class="yellow">[built]</span><span> </span><span class="yellow">[code generated]</span></div><div></div><div><span class="yellow">WARNING</span><span> in </span>configuration</div><div>The <span class="red">'mode' option has not been set</span>, webpack will fallback to 'production' for this value.</div><div><span class="green">Set 'mode' option to 'development' or 'production'</span> to enable defaults for each environment.</div><div>You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/</div><div></div><div>webpack 5.53.0 compiled with <span class="yellow">1 warning</span> in 201 ms</div><div></div> 

然后就可以在浏览器中展示我们彩色的输出日志了,与在终端里输出的一致。

参考

https://www.twilio.com/blog/guide-node-js-logging

https://github.com/jorgebucaran/colorette/blob/main/index.js#L17

https://en.wikipedia.org/wiki/ANSI_escape_code#Colors

https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences

https://stackoverflow.com/questions/15011478/ansi-questions-x1b25h-and-x1be

https://bluesock.org/~willg/dev/ansi.html

https://www.cnblogs.com/gamesky/archive/2012/07/28/2613264.html

https://github.com/mmalecki/ansiparse

 

责任编辑:武晓燕 来源: 秋风的笔记
相关推荐

2012-11-20 09:57:14

2017-02-27 18:35:23

集成交付部署

2016-08-05 17:19:37

持续集成持续交付系统运维

2017-10-19 09:47:55

容器化微服务集成

2023-03-19 11:47:57

Taro小程序持续集

2019-04-18 10:35:30

持续集成工具Buddy

2021-03-31 09:00:00

管道集成工具

2015-07-22 14:59:30

OpenStac持续集成持续交付

2015-09-29 10:08:26

DockerJava持续集成

2009-06-14 18:05:58

ibmdwWebSphere

2015-07-27 11:32:24

Docker持续集成Docker部署

2023-09-15 09:27:35

英伟达

2015-09-24 09:43:08

阮一峰持续集成

2021-09-03 11:33:38

Jenkins 微服务集成

2021-06-18 09:00:00

云计算开发存储库

2011-09-15 09:21:46

持续集成

2012-02-23 10:22:03

JavaTeamCity

2018-01-08 14:18:14

代码互联网持续集成

2017-03-01 08:56:28

VSTSTFSiOS

2015-12-02 14:56:07

Docker开发模式持续集成
点赞
收藏

51CTO技术栈公众号