2017年5月

这是调试器工作原理系列文章的第二部分,阅读本文前,请确保你已经读过第一部分

关于本文

我将会演示如何在调试器中实现断点。断点是调试的两大利器之一,另一个是可以在被调试进程的内存中检查变量值。我们在系列的第一部分已经了解过值检查,但是断点对我们来说依然神秘。不过本文过后,它们就不再如此了。

软件中断

为了在 x86 架构机器上实现断点,软件中断(也被称作“陷阱”)被会派上用场。在我们深入细节之前,我想先大致解释一下中断和陷阱的概念。

CPU 有一条单独的执行流,一条指令接一条的执行(在更高的层面看是这样的,但是在底层的细节上来说,现在的许多 CPU 都会并行执行多个指令,这其中的一些指令就不是按照原本的顺序执行的)。为了能够处理异步的事件,如 IO 和 硬件定时器,CPU 使用了中断。硬件中断通常是一个特定的电子信号,并附加了一个特别的”响应电路”。该电路通知中断激活,并让 CPU 停止当前执行,保存状态,然后跳转到一个预定义的地址,也就是中断处理程序的位置。当处理程序完成其工作后,CPU 又从之前停止的地方重新恢复运行。

软件中断在规则上与硬件相似,但实际操作中有些不同。CPU 支持一些特殊的指令,来允许软件模拟出一个中断。当这样的一个指令被执行时,CPU 像对待一个硬件中断那样 —— 停止正常的执行流,保存状态,然后跳转到一个处理程序。这种“中断”使得许多现代 OS 的惊叹设计得以高效地实现(如任务调度,虚拟内存,内存保护,调试)。

许多编程错误(如被 0 除)也被 CPU 当做中断对待,常常也叫做“异常”, 这时候硬件和软件中断之间的界限就模糊了,很难说这种异常到底是硬件中断还是软件中断。但我已经偏离今天主题太远了,所以现在让我们回到断点上来。

int 3 理论

前面说了很多,现在简单来说断点就是一个部署在 CPU 上的特殊中断,叫 int 3int 是一个 “中断指令”的 x86 术语,该指令是对一个预定义中断处理的调用。x86 支持 8 位的 int 指令操作数,这决定了中断的数量,所以理论上可以支持 256 个中断。前 32 个中断为 CPU 自己保留,而 int 3 就是本文关注的 —— 它被叫做 “调试器专用中断”。

避免更深的解释,我将引用“圣经”里一段话(这里说的“圣经”,当然指的是英特尔的体系结构软件开发者手册, 卷 2A)。

INT 3 指令生成一个以字节操作码(CC),用于调用该调试异常处理程序。(这个一字节格式是非常有用的,因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码)

上述引用非常重要,但是目前去解释它还是为时过早。本文后面我们会回过头再看。

int 3 实践

没错,知道事物背后的理论非常不错,不过,这些理论到底意思是啥?我们怎样使用 int 3 部署断点?或者怎么翻译成通用的编程术语 —— 请给我看代码!

实际上,实现非常简单。一旦你的程序执行了 int 3 指令, OS 就会停止程序( OS 是怎么做到像这样停止进程的? OS 注册其 int 3 的控制程序到 CPU 即可,就这么简单)。在 Linux(这也是本文比较关心的地方) 上, OS 会发送给进程一个信号 —— SIGTRAP

就是这样,真的。现在回想一下本系列的第一部分, 追踪进程(调试程序) 会得到其子进程(或它所连接的被调试进程)所得到的所有信号的通知,接下来你就知道了。

就这样, 没有更多的电脑架构基础术语了。该是例子和代码的时候了。

手动设置断点

现在我要演示在程序里设置断点的代码。我要使用的程序如下:

section    .text
    ; The _start symbol must be declared for the linker (ld)
    global _start

_start:

    ; Prepare arguments for the sys_write system call:
    ;   - eax: system call number (sys_write)
    ;   - ebx: file descriptor (stdout)
    ;   - ecx: pointer to string
    ;   - edx: string length
    mov     edx, len1
    mov     ecx, msg1
    mov     ebx, 1
    mov     eax, 4

    ; Execute the sys_write system call
    int     0x80

    ; Now print the other message
    mov     edx, len2
    mov     ecx, msg2
    mov     ebx, 1
    mov     eax, 4
    int     0x80

    ; Execute sys_exit
    mov     eax, 1
    int     0x80

section    .data

msg1    db      'Hello,', 0xa
len1    equ     $ - msg1
msg2    db      'world!', 0xa
len2    equ     $ - msg2

我现在在使用汇编语言,是为了当我们面对 C 代码的时候,能清楚一些编译细节。上面代码做的事情非常简单,就是在一行打印出 “hello,”,然后在下一行打印出 “world!”。这与之前文章中的程序非常类似。

现在我想在第一次打印和第二次打印之间设置一个断点。我们看到在第一条 int 0x80 ,其后指令是 mov edx, len2。(等等,再次 int?是的,Linux 使用 int 0x80 来实现用户进程到系统内核的系统调用。用户将系统调用的号码及其参数放到寄存器,并执行 int 0x80。然后 CPU 会跳到相应的中断处理程序,其中, OS 注册了一个过程,该过程查看寄存器并决定要执行的系统调用。)首先,我们需要知道该指令所映射的地址。运行 objdump -d:

traced_printer2:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000033  08048080  08048080  00000080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         0000000e  080490b4  080490b4  000000b4  2**2
                  CONTENTS, ALLOC, LOAD, DATA

Disassembly of section .text:

08048080 <.text>:
 8048080:     ba 07 00 00 00          mov    $0x7,%edx
 8048085:     b9 b4 90 04 08          mov    $0x80490b4,%ecx
 804808a:     bb 01 00 00 00          mov    $0x1,%ebx
 804808f:     b8 04 00 00 00          mov    $0x4,%eax
 8048094:     cd 80                   int    $0x80
 8048096:     ba 07 00 00 00          mov    $0x7,%edx
 804809b:     b9 bb 90 04 08          mov    $0x80490bb,%ecx
 80480a0:     bb 01 00 00 00          mov    $0x1,%ebx
 80480a5:     b8 04 00 00 00          mov    $0x4,%eax
 80480aa:     cd 80                   int    $0x80
 80480ac:     b8 01 00 00 00          mov    $0x1,%eax
 80480b1:     cd 80                   int    $0x80

所以,我们要设置断点的地址是 0x8048096。等等,这不是调试器工作的真实姿势,对吧?真正的调试器是在代码行和函数上设置断点,而不是赤裸裸的内存地址?完全正确,但是目前我们仍然还没到那一步,为了更像真正的调试器一样设置断点,我们仍不得不首先理解一些符号和调试信息。所以现在,我们就得面对内存地址。

在这点上,我真想又偏离一下主题。所以现在你有两个选择,如果你真的感兴趣想知道为什么那个地址应该是 0x8048096,它代表着什么,那就看下面的部分。否则你只是想了解断点,你可以跳过这部分。

题外话 —— 程序地址和入口

坦白说,0x8048096 本身没多大意义,仅仅是可执行程序的 text 部分开端偏移的一些字节。如果你看上面导出来的列表,你会看到 text 部分从地址 0x08048080 开始。这告诉 OS 在分配给进程的虚拟地址空间里,将该地址映射到 text 部分开始的地方。在 Linux 上面,这些地址可以是绝对地址(例如,当可执行程序加载到内存中时它不做重定位),因为通过虚拟地址系统,每个进程获得自己的一块内存,并且将整个 32 位地址空间看做自己的(称为 “线性” 地址)。

如果我们使用 readelf 命令检查 ELF 文件头部(ELF,可执行和可链接格式,是 Linux 上用于对象文件、共享库和可执行程序的文件格式),我们会看到:

$ readelf -h traced_printer2
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048080
  Start of program headers:          52 (bytes into file)
  Start of section headers:          220 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         4
  Section header string table index: 3

注意头部里的 Entry point address,它同样指向 0x8048080。所以我们在系统层面解释该 elf 文件的编码信息,它意思是:

  1. 映射 text 部分(包含所给的内容)到地址 0x8048080
  2. 从入口 —— 地址 0x8048080 处开始执行

但是,为什么是 0x8048080 呢?事实证明是一些历史原因。一些 Google 的结果把我引向源头,宣传每个进程的地址空间的前 128M 是保留在栈里的。128M 对应为 0x8000000,该地址是可执行程序其他部分可以开始的地方。而 0x8048080,比较特别,是 Linux ld 链接器使用的默认入口地址。该入口可以通过给 ld 传递 -Ttext 参数改变。

总结一下,这地址没啥特别的,我们可以随意修改它。只要 ELF 可执行文件被合理的组织,并且头部里的入口地址与真正的程序代码(text 部分)开始的地址匹配,一切都没问题。

用 int 3 在调试器中设置断点

为了在被追踪进程的某些目标地址设置一个断点,调试器会做如下工作:

  1. 记住存储在目标地址的数据
  2. 用 int 指令替换掉目标地址的第一个字节

然后,当调试器要求 OS 运行该进程的时候(通过上一篇文章中提过的 PTRACE_CONT),进程就会运行起来直到遇到 int 3,此处进程会停止运行,并且 OS 会发送一个信号给调试器。调试器会收到一个信号表明其子进程(或者说被追踪进程)停止了。调试器可以做以下工作:

  1. 在目标地址,用原来的正常执行指令替换掉 int 3 指令
  2. 将被追踪进程的指令指针回退一步。这是因为现在指令指针位于刚刚执行过的 int 3 之后。
  3. 允许用户以某些方式与进程交互,因为该进程仍然停止在特定的目标地址。这里你的调试器可以让你取得变量值,调用栈等等。
  4. 当用户想继续运行,调试器会小心地把断点放回目标地址去(因为它在第 1 步时被移走了),除非用户要求取消该断点。

让我们来看看,这些步骤是如何翻译成具体代码的。我们会用到第一篇里的调试器 “模板”(fork 一个子进程并追踪它)。无论如何,文末会有一个完整样例源代码的链接

/* Obtain and show child's instruction pointer */
ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
procmsg("Child started. EIP = 0x%08x\n", regs.eip);

/* Look at the word at the address we're interested in */
unsigned addr = 0x8048096;
unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);

这里调试器从被追踪的进程中取回了指令指针,也检查了在 0x8048096 的字。当开始追踪运行文章开头的汇编代码,将会打印出:

[13028] Child started. EIP = 0x08048080
[13028] Original data at 0x08048096: 0x000007ba

目前为止都看起来不错。接下来:

/* Write the trap instruction 'int 3' into the address */
unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);

/* See what's there again... */
unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);

注意到 int 3 是如何被插入到目标地址的。此处打印:

[13028] After trap, data at 0x08048096: 0x000007cc

正如预料的那样 —— 0xba0xcc 替换掉了。现在调试器运行子进程并等待它在断点处停止:

/* Let the child run to the breakpoint and wait for it to
** reach it
*/
ptrace(PTRACE_CONT, child_pid, 0, 0);

wait(&wait_status);
if (WIFSTOPPED(wait_status)) {
    procmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));
}
else {
    perror("wait");
    return;
}

/* See where the child is now */
ptrace(PTRACE_GETREGS, child_pid, 0, &regs);
procmsg("Child stopped at EIP = 0x%08x\n", regs.eip);

这里打印出:

Hello,
[13028] Child got a signal: Trace/breakpoint trap
[13028] Child stopped at EIP = 0x08048097

注意到 “Hello,” 在断点前打印出来了 —— 完全如我们计划的那样。同时注意到子进程停止的地方 —— 刚好就是单字节中断指令后面。

最后,如早先诠释的那样,为了让子进程继续运行,我们得做一些工作。我们用原来的指令替换掉中断指令,并且让进程从这里继续之前的运行。

/* Remove the breakpoint by restoring the previous data
** at the target address, and unwind the EIP back by 1 to
** let the CPU execute the original instruction that was
** there.
*/
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data);
regs.eip -= 1;
ptrace(PTRACE_SETREGS, child_pid, 0, &regs);

/* The child can continue running now */
ptrace(PTRACE_CONT, child_pid, 0, 0);

这会使子进程继续打印出 “world!”,然后退出。

注意,我们在这里没有恢复断点。通过在单步调试模式下,运行原来的指令,然后将中断放回去,并且只在运行 PTRACE\_CONT 时做到恢复断点。文章稍后会展示 debuglib 如何做到这点。

更多关于 int 3

现在可以回过头去看看 int 3 和因特尔手册里那个神秘的说明,原文如下:

这个一字节格式是非常有用的,因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码

int 指令在 x86 机器上占两个字节 —— 0xcd 紧跟着中断数(细心的读者可以在上面列出的转储中发现 int 0x80 翻译成了 cd 80)。int 3 被编码为 cd 03,但是为其还保留了一个单字节指令 —— 0xcc

为什么这样呢?因为这可以允许我们插入一个断点,而不需要重写多余的指令。这非常重要,考虑下面的代码:

    .. some code ..
    jz    foo
    dec   eax
foo:
    call  bar
    .. some code ..

假设你想在 dec eax 这里放置一个断点。这对应一个单字节指令(操作码为 0x48)。由于替换断点的指令长于一个字节,我们不得不强制覆盖掉下个指令(call)的一部分,这就会篡改 call 指令,并很可能导致一些完全不合理的事情发生。这样一来跳转到 foo 分支的 jz foo 指令会导致什么?就会不在 dec eax 这里停止,CPU 径直去执行后面一些无效的指令了。

而有了单字节的 int 3 指令,这个问题就解决了。 1 字节是在 x86 上面所能找到的最短指令,这样我们可以保证仅改变我们想中断的指令。

封装一些晦涩的细节

很多上述章节样例代码的底层细节,都可以很容易封装在方便使用的 API 里。我已经做了很多封装的工作,将它们都放在一个叫做 debuglib 的通用库里 —— 文末可以去下载。这里我仅仅是想展示它的用法示例,但是绕了一圈。下面我们将追踪一个用 C 写的程序。

追踪一个 C 程序地址和入口

目前为止,为了简单,我把注意力放在了目标汇编代码。现在是时候往上一个层次,去看看我们如何追踪一个 C 程序。

事实证明并不是非常难 —— 找到放置断点位置有一点难罢了。考虑下面样例程序:

#include <stdio.h>

void do_stuff()
{
    printf("Hello, ");
}

int main()
{
    for (int i = 0; i < 4; ++i)
        do_stuff();
    printf("world!\n");
    return 0;
}

假设我想在 do_stuff 入口处放置一个断点。我会先使用 objdump 反汇编一下可执行文件,但是打印出的东西太多。尤其看到很多无用,也不感兴趣的 C 程序运行时的初始化代码。所以我们仅看一下 do_stuff 部分:

080483e4 <do_stuff>:
 80483e4:     55                      push   %ebp
 80483e5:     89 e5                   mov    %esp,%ebp
 80483e7:     83 ec 18                sub    $0x18,%esp
 80483ea:     c7 04 24 f0 84 04 08    movl   $0x80484f0,(%esp)
 80483f1:     e8 22 ff ff ff          call   8048318 <puts@plt>
 80483f6:     c9                      leave
 80483f7:     c3                      ret

那么,我们将会把断点放在 0x080483e4,这是 do_stuff 第一条指令执行的地方。而且,该函数是在循环里面调用的,我们想要在断点处一直停止执行直到循环结束。我们将会使用 debuglib 来简化该流程,下面是完整的调试函数:

void run_debugger(pid_t child_pid)
{
    procmsg("debugger started\n");

    /* Wait for child to stop on its first instruction */
    wait(0);
    procmsg("child now at EIP = 0x%08x\n", get_child_eip(child_pid));

    /* Create breakpoint and run to it*/
    debug_breakpoint* bp = create_breakpoint(child_pid, (void*)0x080483e4);
    procmsg("breakpoint created\n");
    ptrace(PTRACE_CONT, child_pid, 0, 0);
    wait(0);

    /* Loop as long as the child didn't exit */
    while (1) {
        /* The child is stopped at a breakpoint here. Resume its
        ** execution until it either exits or hits the
        ** breakpoint again.
        */
        procmsg("child stopped at breakpoint. EIP = 0x%08X\n", get_child_eip(child_pid));
        procmsg("resuming\n");
        int rc = resume_from_breakpoint(child_pid, bp);

        if (rc == 0) {
            procmsg("child exited\n");
            break;
        }
        else if (rc == 1) {
            continue;
        }
        else {
            procmsg("unexpected: %d\n", rc);
            break;
        }
    }

    cleanup_breakpoint(bp);
}

为了避免修改 EIP 标志位和目的进程的内存空间的麻烦,我们仅需要调用 create_breakpointresume_from_breakpointcleanup_breakpoint。让我们来看看追踪上面的 C 代码样例会输出什么:

$ bp_use_lib traced_c_loop
[13363] debugger started
[13364] target started. will run 'traced_c_loop'
[13363] child now at EIP = 0x00a37850
[13363] breakpoint created
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
[13363] child stopped at breakpoint. EIP = 0x080483E5
[13363] resuming
Hello,
world!
[13363] child exited

如预期一样!

样例代码

这里是本文用到的完整源代码文件。在归档中你可以找到:

  • debuglib.h 和 debuglib.c - 封装了调试器的一些内部工作的示例库
  • bp\_manual.c - 这篇文章开始部分介绍的“手动”设置断点的方法。一些样板代码使用了 debuglib 库。
  • bpuselib.c - 大部分代码使用了 debuglib 库,用于在第二个代码范例中演示在 C 程序的循环中追踪。

引文

在准备本文的时候,我搜集了如下的资源和文章:


via: http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints

作者:Eli Bendersky 译者:wi-cuckoo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

WordPress 是一个免费开源、可高度自定义的内容管理系统(CMS),它被全世界数以百万计的人来运行博客和完整的网站。因为它是被用的最多的 CMS,因此有许多潜在的 WordPress 安全问题/漏洞需要考虑。

然而,如果我们遵循通常的 WordPress 最佳实践,这些安全问题可以避免。在本篇中,我们会向你展示如何使用 WPSeku,一个 Linux 中的 WordPress 漏洞扫描器,它可以被用来找出你安装的 WordPress 的安全漏洞,并阻止潜在的威胁。

WPSeku 是一个用 Python 写的简单的 WordPress 漏洞扫描器,它可以被用来扫描本地以及远程安装的 WordPress 来找出安全问题。

如何安装 WPSeku - Linux 中的 WordPress 漏洞扫描器

要在 Linux 中安装 WPSeku,你需要如下从 Github clone 最新版本的 WPSeku。

$ cd ~
$ git clone https://github.com/m4ll0k/WPSeku

完成之后,进入 WPSeku 目录,并如下运行。

$ cd WPSeku

使用 -u 选项指定 WordPress 的安装 URL,如下运行 WPSeku:

$ ./wpseku.py -u http://yourdomain.com 

WordPress Vulnerability Scanner

WordPress 漏洞扫描器

以下命令使用 -p 选项搜索 WordPress 插件中的跨站脚本(x)、本地文件夹嵌入(l)和 SQL 注入(s)漏洞,你需要在 URL 中指定插件的位置:

$ ./wpseku.py -u http://yourdomain.com/wp-content/plugins/wp/wp.php?id= -p [x,l,s]

以下命令将使用 -b 选项通过 XML-RPC 执行暴力密码登录。另外,你可以使用 --user--wordlist 选项分别设置用户名和单词列表,如下所示。

$ ./wpseku.py -u http://yourdomian.com --user username --wordlist wordlist.txt -b [l,x]   

要浏览所有 WPSeku 使用选项,输入:

$ ./wpseku.py --help

WPSeku WordPress Vulnerability Scanner Help

WPSeku WordPress 漏洞扫描帮助

WPSeku Github 仓库:https://github.com/m4ll0k/WPSeku

就是这样了!在本篇中,我们向你展示了如何在 Linux 中获取并使用 WPSeku 用于 WordPress 漏洞扫描。WordPress 是安全的,但需要我们遵循 WordPress 安全最佳实践才行。你有要分享的想法么?如果有,请在评论区留言。


作者简介:

Aaron Kili 是一个 Linux 及 F.O.S.S 热衷者,即将成为 Linux 系统管理员、web 开发者,目前是 TecMint 的内容创作者,他喜欢用电脑工作,并坚信分享知识。


via: https://www.tecmint.com/wpseku-wordpress-vulnerability-security-scanner/

作者:Aaron Kili 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Vim 是一种流行的、功能丰富的和高度可扩展的 Linux 文本编辑器,它的一个特殊功能便是支持用带密码各种的加密方法来加密文本文件。

本文中,我们将向你介绍一种简单的 Vim 使用技巧:在 Linux 中使用 Vim 对文件进行密码保护。我们将向你展示如何让一个文件在它创建的时侯以及为了修改目的而被打开了之后获得安全防护。

建议阅读: 你应该在 Linux 中使用 Vim 编辑器的 7 个原因

要安装 Vim 完整版,只需运行这些命令:

$ sudo apt install vim          #Debian/Ubuntu 系统
$ sudo yum install vim          #RHEL/CentOS 系统 
$ sudo dnf install vim      #Fedora 22+

参阅: 十年后 Vim 8.0 发布了在 Linux 上安装

怎样在 Linux 中用 Vim 对文件进行密码保护

Vim 有个 -x 选项,这个选项能让你在创建文件时用它来加密。一旦你运行下面的 vim 命令,你会被提示输入一个密钥:

$ vim -x file.txt
警告:正在使用弱加密方法;参见 :help 'cm'
输入加密密钥:*******
再次输入相同密钥:*******

如果第二次输入的密钥无误,你就能可以修改此文件了。

Vim File Password Protected

被密码保护的 Vim 文件

等你修改好之后,摁 Esc 和键入 :wq 来保存及关闭文件。下次你想打开它编辑一下,你就必须像这样去输入密钥:

$ vim file.txt
需要 "file.txt" 的加密密钥
警告:正在使用弱加密方法;参见 :help 'cm'
输入密钥:*******

假设你输了一个错误的密码(或者没输密码),你会看到一些垃圾字符。

Vim Content Encrypted

Vim 中的加密内容

在 Vim 中设置一种强加密方法

注意:警告信息暗示保护文件的是弱加密方法。那么接下来,我们来看看怎么在 Vim 中设置一种强加密方法。

Weak Encryption on Vim File

Vim 中文件弱加密

为了查看加密方式(cm)集,键入如下:

:help 'cm'

输出样例:

                                                *'cryptmethod'* *'cm'*
'cryptmethod'       string  (默认 "zip")
                            全局或本地到缓冲区 |global-local|
                            {not in Vi}
        当缓冲区写进文件中所用的加密方式:
                                                *pkzip*
            zip             PkZip 兼容方式。  一种弱加密方法。
                            与 Vim 7.2 及更老版本后向兼容。
                                                *blowfish*
            blowfish        河豚加密方式。  中级强度加密方法但有实现上
                            的瑕疵。需要 Vim 7.3 及以上版本,用它加密的文件不
                            能被 Vim 7.2 及更老版本读取。它会添加一个 “种子”,
                            每次你当你写入文件时,这个加密字节都不同。

你可以像如下所示的那样给一个 Vim 文件设置个新的加密方法(本例中我们用 blowfish2 加密方法)

:setlocal cm=blowfish2

然后键入回车和 :wq 保存文件。

Set Strong Encryption on Vim File

对 Vim 文件设置强加密

现在你再打开下面的文件时应该就看不到那条警告信息了。

$ vim file.txt
需要 "file.txt" 的加密密钥
输入加密密钥:*******

你也可以在打开 Vim 文件之后来设置密码,用 :X 命令就能像上面所示的那样去设置一个加密密码。

可以看看我们其他的关于 Vim 编辑器的有用的文章。

  1. 在 Linux 中学习有用的 Vim 编辑器的技巧
  2. 给每个 Linux 用户的 8 种有用的 Vim 编辑器技巧
  3. spf13-vim – Vim 编辑器的顶级分发版
  4. 怎样在 Linux 种把 Vim 编辑当作 Bash IDE 来用

本文到这里就结束了!文章中我们介绍了怎么通过 Linux 下的 Vim 文本编辑器来给一个文件做加密防护。

永远记住要用强加密方式及密码来适当的保护那些可能包含了诸如用户名及密码、财务账户信息等机密信息的文本文件。


作者简介:

Aaron Kili 是一个 Linux 和 F.O.S.S(Free and Open-Source Software,自由及开放源代码软件)爱好者,未来的 Linux 系统管理员、Web 开发人员,目前是 TecMint 的内容创作者,他喜欢用电脑工作,且崇尚分享知识。


via: https://www.tecmint.com/password-protect-vim-file-in-linux/

作者:Aaron Kili 译者:ch-cn 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linfo 是一个自由开源的跨平台的服务器统计 UI 或库,它可以显示大量的系统信息。Linfo 是可扩展的,通过 composer,很容易使用 PHP5 库以程序化方式获取来自 PHP 应用的丰富的系统统计数据。它有 Web UI 及其Ncurses CLI 视图,在 Linux、Windows、BSD、Darwin/Mac OSX、Solaris 和 Minix 系统上均可用。

Linfo 显示的系统信息包括 CPU 类型/速度、服务器的体系结构、挂载点用量、硬盘/光纤/Flash 驱动器、硬件设备、网络设备和统计信息、运行时间/启动日期、主机名、内存使用量(RAM 和 swap)、温度/电压/风扇速度和 RAID 阵列等。

环境要求:

  • PHP 5.3
  • pcre 扩展
  • Linux – /proc/sys 已挂载且可对 PHP 可读,已经在 2.6.x/3.x 内核中测试过

如何在 Linux 中安装 Linfo 服务器统计 UI及库

首先,在 Apache 或 Nginx 的 Web 根目录下创建 Linfo 的目录,然后,使用下面展示的 rsync 命令 克隆仓库文件并将其移动到目录 /var/www/html/linfo 下:

$ sudo mkdir -p /var/www/html/linfo 
$ git clone git://github.com/jrgp/linfo.git 
$ sudo rsync -av linfo/ /var/www/html/linfo/

接下来,将 sample.config.inc.php 重命名为 config.inc.php。这是 Linfo 的配置文件,你可以在里面定义你想要的值:

$ sudo mv sample.config.inc.php config.inc.php 

现在,在 Web 浏览器中打开链接 http://SERVER_IP/linfo 来查看这个 Web UI,正如下面的截图所展示的。

从截图中可以看到, Linfo 显示了系统内核信息、硬件组成、RAM 统计、网络设备、驱动器以及文件系统挂载点。

Linux Server Health Information

Linux 服务器运行信息

你可以将下面一行内容加入配置文件 config.inc.php 中,以便进行故障排查时看到错误信息。

$settings['show_errors'] = true;

以 Ncurses 模式运行 Linfo

Linfo 有一个基于 ncurses 的简单界面,它依赖于 phpncurses 扩展。

# yum install php-pecl-ncurses                    [在 CentOS/RHEL 上]
# dnf install php-pecl-ncurses                    [在 Fedora 上]
$ sudo apt-get install php5-dev libncurses5-dev   [在 Debian/Ubuntu 上] 

现在,像下面这样编译这个 php 扩展:

$ wget http://pecl.php.net/get/ncurses-1.0.2.tgz
$ tar xzvf ncurses-1.0.2.tgz
$ cd ncurses-1.0.2
$ phpize # generate configure script
$ ./configure
$ make
$ sudo make install

接下来,如果编译成功并安装好了该 php 扩展,运行下面的命令:

$ sudo echo extension=ncurses.so > /etc/php5/cli/conf.d/ncurses.ini

验证 ncurse:

$ php -m | grep ncurses

现在,运行 Info:

$ cd /var/www/html/linfo/
$ ./linfo-curses

Linux Server Information

Linux 服务器信息

Info 中尚欠缺下面这些功能:

  1. 支持更多 Unix 操作系统(比如 Hurd、IRIX、AIX 和 HP UX 等)
  2. 支持不太出名的操作系统 Haiku/BeOS
  3. 额外功能/扩展
  4. 在 ncurses 模式中支持 htop 类 特性

如果想了解更多信息,请访问 Linfo 的 GitHub 仓库: https://github.com/jrgp/linfo

这就是本文的全部内容了。从现在起,你可以使用 Linfo 在 Web 浏览器中查看 Linux 系统的信息。尝试一下,并在评论中和我们分享你的想法。另外,你是否还知道与之类似的有用工具/库?如果有,请给我们提供一些相关信息。


作者简介:

Aaron Kili 是 Linux 和 F.O.S.S 爱好者,将来的 Linux 系统管理员和网络开发人员,目前是 TecMint 的内容创作者,他喜欢用电脑工作,并坚信分享知识


via: https://www.tecmint.com/linfo-shows-linux-server-health-status-in-real-time/

作者:Aaron Kili 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

昨天,微博上的朋友 @马甲与小号 告诉我发现了一个奇怪的东西,本着好奇心使人进步(害死猫?)的目的,我去观摩了一番,于是就有了今天这篇文章。

这是一个 bash shell 脚本,其中有若干可以整蛊(结仇)你的同事的小技巧——或者说恶作剧。看完之后,感觉不寒而栗,要是谁敢这样整我,我一定和他绝交!

警告,切勿在生产环境体验,一切后果脚本作者和本文作者均不不承担!

警告,切勿在生产环境体验,一切后果脚本作者和本文作者均不不承担!

警告,切勿在生产环境体验,一切后果脚本作者和本文作者均不不承担!

这个世界怎么了?

这个脚本主要由一些别名、函数、环境变量定义组织而成,执行该脚本后,你的 shell 环境就变成了一个光怪陆离的世界。

好了,那么我们来看看都发生了什么。

注:本脚本适用于 bash 环境,其它 shell 环境有些不支持。

可怕的默认编辑器

当系统调用默认编辑器来编辑比如 crontab 时,biu 的一下,文件没了!

export EDITOR=/bin/rm;

这是将 EDITOR 环境变量定义为 rm,而它原本应该是 viemacsnano 的,体会一下,是不是很酸爽?

学习课堂:

EDITOR 环境变量用于定于系统的默认编辑器,在一些系统内置功能里面,比如编辑 crontab 时,会根据该变量调用默认编辑器。

猥琐的制表符(tab)

当你想用制表符来个自动补全时,你会它非但不干,而且还删除了一个字母,不信邪的你使劲多砸了几个制表符,这下好了,更多的字符被删除了。

tset -Qe $'\t';

原来是将制表符定义为退格键了。

学习课堂:

tset 用于设置终端特征;-e 参数设置擦除字符,缺省为退格字符;-Q 表示不显示设置信息(静默)。

莫名退出

有时候,执行一个命令就会莫名其妙地退出 shell,只不过是命令有个非 0 的返回状态嘛,为什么会这样?

((RANDOM % 10)) || set -o errexit;

学习课堂:

set -o errexit 等价于 set -e,表示有任何错误(命令的返回状态非 0 )时即退出。

啥都看(cat)不了

当我想看(cat)一下文件时,它居然就当没听见,到底文件里面有啥啊?

alias cat=true;

原来是把 cat 定义成 true 命令的别名了, true 命令啥都不干,不管你给它什么参数和什么输入,它只是静静地返回一个 0 的状态码。

学习课堂

true 命令和 false 命令常用于 shell 脚本中。

到底是按什么排列的啊?

好吧,我想看看目录里面有啥文件,于是我输入了 ls,咦?这是什么顺序?我再次输入一遍,怎么回事,列出的文件和目录又是一种顺序,难道它的输出看心情吗?

function ls { command ls -$(opts="frStu"; echo ${opts:$((RANDOM % ${#opts})):1}) "$@"; }

原来它用一个函数重新定义了 ls,所以,真是看心情,你永远不知道它会以什么顺序返回结果。

学习课堂:

ls 的 f 选项表示不排序输出(即只按照磁盘存储顺序输出);r 表示反向排序;S 表示按文件大小排序;t 表示按修改时间排序;u 表示按最后访问时间排序。

再也不要试着进入目录了

当我想进入目录看看时,惊奇的是居然没进去,难道没有自动补全我就输入错了?用前面那个奇奇怪怪的 ls 再次看看时,令人惊恐的是,那个目录!它没有了!!!不信邪的我又重复了这个过程,然后,我就一个子目录也没有了!

alias cd='rm -rfv';

这该死的,连输入 cd 这么无害的命令都这么可怕!

学习课堂:

rm 命令的 -r 表示可删除(非空)目录;-f 表示不需要确认删除;-v 表示删除后显示被删除的文件/目录名称——这里是用来嘲讽我删除了某个目录的吗?

还敢用 sudo 权限吗?

我很遵守安全守则,从来不用 root 直接登录,凡是管理任务,都用 sudo 来执行。然而,现在无论我用 sudo 执行什么命令,都会马上关机,并将我输入的命令广而告之!这是我被系统讨厌了么?

alias sudo='sudo shutdown -P now';

学习课堂:

shutdown 命令用来关闭系统,-P 参数表示连同电源一起关闭; now 表示马上关机。这之后的参数(在此例中,是原本希望 sudo 执行的命令)会作为关闭前的通知信息,广播给系统上所有在线的用户。

我原本想静静,结果世界都静了

杂乱的屏幕输出让你厌憎,所以,一个 clear 命令就可以静静了——等等,为什么我的终端崩溃了?然后系统也死机了。

alias clear=':(){ :|:& };:';

这是将 clear 命令别名为一个 fork 炸弹了,据说这个是最精简、最难懂的 fork 炸弹了。至于炸弹的效果,嗯,世界都安静了

学习课堂:

Fork 炸弹带来的后果就是耗尽服务器资源,使服务器不能正常的对外提供服务,也就是常说的 DoS(Denial of Service)。

今夕是何年?

这光怪陆离的世界啊,让我疑似梦中,那么,现在是什么时候?当然,我肯定不会去翻日历的,输入 date 命令才是我们命令行极客该做的事情。看着返回的日期,我不禁怀疑我的记忆,难道我穿越了么?

alias date='date -d "now + $RANDOM days"';

学习课堂:

date 命令可以显示相对偏移的日期,上述命令中 $RANDOM 的结果是一个随机的整数,也就是说这里的 date 命令会返回若干天之后的日期。

如果你有一个鬼马的 CD 驱动器

现在 CD 驱动器用的不多了,但是很多机器上还残留着这个“咖啡杯托”,如果你有幸还有这个东西的话,或许今天它就被鬼怪附体了,一会弹出,一会又收回去,有时候你按下弹出键却毫无反应——当你真的将咖啡杯放上面时,小心,你的咖啡杯会掉下来!

将 CD 盘托当成咖啡杯托是一个笑话,据说某人曾经给电脑厂家打电话:

“您好,我想说你们的机器上的咖啡杯托以前挺好用的,可是现在它不动了。”

“‘咖啡杯托’?那是什么?”

“就是那个一按按钮就会弹出的托盘啊,放咖啡杯正好,还有合适的凹槽,设计的不错!以前都好好的,现在它不会弹出了。”

“……”

    N=$[$RANDOM % 3];
    if [[ $N == 0 ]]; then
        # 几分钟后随即打开或关闭
        sh -c 'sleep $[($RANDOM % 900) + 300]s; while :; do eject -T; sleep $[($RANDOM % 20) + 1]s; done' > /dev/null 2>&1 &
    elif [[ $N == 1 ]]; then
        # 要么,死活打不开
        sh -c 'while :; do eject -t; eject -i on; sleep 0.1s; done' > /dev/null 2>&1 &
    else
        # 要么,读取变得极慢(1 倍速),需要循环的原因是弹出后就需要重新设定。
        sh -c 'set +o errexit; while :; do eject -x 1; sleep 1s; done' > /dev/null 2>&1 &
    fi;

学习课堂:

eject 是操作 CD 驱动器的命令行,记得当年有位第一次接触 SUN Solaris 的同事问我,这 CD 怎么打开啊?我默默地输入了 eject, 在同事愕然的眼光中不带走一丝云彩轻轻地离开。

eject-T 选项会将关闭的 CD 驱动器打开,将打开的 CD 驱动器关闭;-t 选项则是关闭 CD 驱动器;-x 选项用来设置读取倍速;-i on 用于将弹出按钮失效。

冰川时代

突然地,某个你已经打开的程序冻结了,也许是你的浏览器、也许是你正写了一半的文档,所以,随时保存文档是个好习惯吗?

sleep $[ ( $RANDOM % 100 )  + 1 ]s && kill -STOP $(ps x -o pid|sed 1d|sort -R|head -1) &

学习课题:

sleep 就不用解释了,这代表暂停若干秒。

通过上述 ps 命令会会随机选出(sort 命令的 -R 选项)一个你的进程号,然后由 kill 命令发送 STOP 信号给它。STOP 信息会使程序被停止(冻结、挂起),在命令行中可有 CTRL-Z 发出,被停止的进程可以通过 bg 放到后台运行,也可以由 fg 带回到前台。

一个还是两个?

当我想复制一个文件到另外一个地方时,咦?原来的那个哪里去了?

alias cp='mv';

还好,还好,你总是还有一个副本的,这总算是不幸中的大幸了。

学习课堂:

cpmvmv 还是 mv

永不停止的工作

打完收工,你总是要退出(exit)你的 shell 的,但是一直退不出是什么意思?

alias exit='sh';

学习课堂:

exit 命令别名为 sh ,这样输入 exit 命令后不是退出当前 shell,而是有进入了一个新的子 shell,想退出不干?没门!

到底是哪行?

会用 grep 的你,应该知道 -n 参数可以告诉你所匹配的行的行号,但是随机乱变的行号是什么鬼?我讨厌随机!

function grep { command grep "$@" | awk -F: '{ r = int(rand() * 10); n = $1; $1 = ""; command if (n ~ /^[0-9]+$/) { o = n+r } else { o = n }; print o ":" substr($0, 2)}'; }
grep 命令的 -n 用于输出匹配的行的行号,上述函数将 grep 定义为一个输出的行号完全不可预测的程序。

世界是反着的

你脚本也总是出各种匪夷所思的问题,而且你还不知道什么地方出了问题。这一切都要怪你进入了一个“是”即是“非”的世界。

alias if='if !' for='for !' while='while !';

ifforwhile 所检测的条件定义为反,我不知道这个世界可以疯狂到这个地步!

学习课堂:

ifforwhile 是用于 shell 脚本中做逻辑判断和循环的语句,! 表示对表达式逻辑取反。

想执行命令?没门!

当你输入了一个命令之后,用小指轻轻地、优雅地,按下右侧的那个小小的回车键,满心以为会爆发出绝世高手的风范。然而……并没有,非但没有,你输入的命令还被删除了一个字符!懵逼的你以为用力太轻了,再次敲击后发现又被删除了一个!!!

记得有一个电影,危急情况下,当别人把键盘递给一位即将闭眼的黑客时,他只是轻轻按下了那个“回车”!
bind '"\C-J":"\C-?"';
bind '"\C-M":"\C-?"';

学习课堂:

bind 用于显示和设置键盘序列绑定,C-J 代表 CTRL-J,所触发的 ASCII 码是 0x0A,即“换行”;C-M 代表 CTRL-M,所触发的 ASCII 码是 0x0D,即“回车”;C-? 代表 CTRL-?,所触发的 ASCII 码是 0x7F,即“退格”。也就是说,你按下的回车键,会被映射为退格键。关于 ASCII 控制字符,可参见: http://ascii-table.com/control-chars.php 。也可以使用 showkey -a 命令来检验你按下的键的键值(CTRL-D 退出)。

好的,但是我不干

你说要,但是你的身体却说不要。明明应该应答 yes,但是却实际上拒绝了。

alias yes="yes n";

学习课堂:

yes 命令常用于脚本中应答 y,但是这里重定义了 yes 的结果。这是身口不一么?

我要编辑文件

当我用 vim 打开一个文件时,为什么什么都没发生?

alias vim="vim +q";

学习课堂:

vim 可以用 + 来跟上要在 vim 里面执行的命令,这里 +q 表示退出 vim

最后,别想回到正常的世界

好吧,我明白了,都是 alias 捣的鬼,我要取消它们。什么?取消也无效了?

alias unalias=false;
alias alias=false;

学习课堂:

aliasunalias 别名为 false,那你就不能执行 alias 的功能了。

让我回到真实的世界吧!

好了,我已经受够了这个疯狂是世界了。其实,上面这些别名,都是可以通过输入命令的全路径来绕开别名的——只是一般人不会这样输入。

想要整蛊你的同事,那就将这个脚本(https://github.com/mathiasbynens/evil.sh/blob/master/evil.sh )放到他的机器上,并在他的 .bash_profile 的末尾加入 source ~/evil.sh 即可。当然,你要这么做之前,要有友尽的心理准备。

感谢这个邪恶的脚本的贡献者: Mathias Bynens 和 Jan Moesen 等人 ;-D

你是否厌烦了那些使用复杂语言编写的、难以部署的、总是在不停构建的解决方案?Golang 是解决这些问题的好方法,它和 C 语言一样快,又和 Python 一样简单。

但是你是如何使用 Golang 日志监控你的应用程序的呢?Golang 没有异常,只有错误。因此你的第一印象可能就是开发 Golang 日志策略并不是一件简单的事情。不支持异常事实上并不是什么问题,异常在很多编程语言中已经失去了其异常性:它们过于被滥用以至于它们的作用都被忽视了。

在进一步深入之前,我们首先会介绍 Golang 日志的基础,并讨论 Golang 日志标准、元数据意义、以及最小化 Golang 日志对性能的影响。通过日志,你可以追踪用户在你应用中的活动,快速识别你项目中失效的组件,并监控总体性能以及用户体验。

I. Golang 日志基础

1) 使用 Golang “log” 库

Golang 给你提供了一个称为 “log” 的原生日志库 。它的日志器完美适用于追踪简单的活动,例如通过使用可用的选项在错误信息之前添加一个时间戳。

下面是一个 Golang 中如何记录错误日志的简单例子:

package main

import (
    "log"
    "errors"
    "fmt"
    )

func main() {
   /* 定义局部变量 */
  ...

   /* 除法函数,除以 0 的时候会返回错误 */
   ret,err = div(a, b)
if err != nil {
 log.Fatal(err)
    }
    fmt.Println(ret)
}

如果你尝试除以 0,你就会得到类似下面的结果:

为了快速测试一个 Golang 函数,你可以使用 go playground

为了确保你的日志总是能轻易访问,我们建议你把它们写到一个文件:

package main
import (
        "log"
        "os"
)
func main() {
        // 按照所需读写权限创建文件
        f, err := os.OpenFile("filename", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
        if err != nil {
                log.Fatal(err)
        }   
        // 完成后延迟关闭,而不是习惯!
        defer f.Close()
        //设置日志输出到 f
        log.SetOutput(f)
        //测试用例
        log.Println("check to make sure it works")
}

你可以在这里找到 Golang 日志的完整指南,以及 “log” 内可用函数的完整列表。

现在你就可以记录它们的错误以及根本原因啦。

另外,日志也可以帮你将活动流拼接在一起,查找需要修复的错误上下文,或者调查在你的系统中单个请求如何影响其它应用层和 API。

为了获得更好的日志效果,你首先需要在你的项目中使用尽可能多的上下文丰富你的 Golang 日志,并标准化你使用的格式。这就是 Golang 原生库能达到的极限。使用最广泛的库是 gloglogrus。必须承认还有很多好的库可以使用。如果你已经在使用支持 JSON 格式的库,你就不需要再换其它库了,后面我们会解释。

II. 为你 Golang 日志统一格式

1) JSON 格式的结构优势

在一个项目或者多个微服务中结构化你的 Golang 日志可能是最困难的事情,但一旦完成就很轻松了。结构化你的日志能使机器可读(参考我们 收集日志的最佳实践博文)。灵活性和层级是 JSON 格式的核心,因此信息能够轻易被人类和机器解析以及处理。

下面是一个使用 Logrus/Logmatic.io 如何用 JSON 格式记录日志的例子:

package main
import (
  log "github.com/Sirupsen/logrus"
  "github.com/logmatic/logmatic-go"
)
func main() {
    // 使用 JSONFormatter
    log.SetFormatter(&logmatic.JSONFormatter{})
        // 使用 logrus 像往常那样记录事件
    log.WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1 }).Info("My first ssl event from golang")
}

会输出结果:

{   
    "date":"2016-05-09T10:56:00+02:00",
    "float":1.1,
    "int":1,
    "level":"info",
    "message":"My first ssl event from golang",
    "String":"foo"
}

2) 标准化 Golang 日志

同一个错误出现在你代码的不同部分,却以不同形式被记录下来是一件可耻的事情。下面是一个由于一个变量错误导致无法确定 web 页面加载状态的例子。一个开发者日志格式是:

message: 'unknown error: cannot determine loading status from unknown error: missing or invalid arg value client'</span>

另一个人的格式却是:

unknown error: cannot determine loading status - invalid client</span>

强制日志标准化的一个好的解决办法是在你的代码和日志库之间创建一个接口。这个标准化接口会包括所有你想添加到你日志中的可能行为的预定义日志消息。这么做可以防止出现不符合你想要的标准格式的自定义日志信息。这么做也便于日志调查。

由于日志格式都被统一处理,使它们保持更新也变得更加简单。如果出现了一种新的错误类型,它只需要被添加到一个接口,这样每个组员都会使用完全相同的信息。

最常使用的简单例子就是在 Golang 日志信息前面添加日志器名称和 id。你的代码然后就会发送 “事件” 到你的标准化接口,它会继续讲它们转化为 Golang 日志消息。

// 主要部分,我们会在这里定义所有消息。
// Event 结构体很简单。为了当所有信息都被记录时能检索它们,
// 我们维护了一个 Id
var (
    invalidArgMessage = Event{1, "Invalid arg: %s"}
    invalidArgValueMessage = Event{2, "Invalid arg value: %s => %v"}
    missingArgMessage = Event{3, "Missing arg: %s"}
)

// 在我们应用程序中可以使用的所有日志事件
func (l *Logger)InvalidArg(name string) {
    l.entry.Errorf(invalidArgMessage.toString(), name)
}
func (l *Logger)InvalidArgValue(name string, value interface{}) {
    l.entry.WithField("arg." + name, value).Errorf(invalidArgValueMessage.toString(), name, value)
}
func (l *Logger)MissingArg(name string) {
    l.entry.Errorf(missingArgMessage.toString(), name)
}

因此如果我们使用前面例子中无效的参数值,我们就会得到相似的日志信息:

time="2017-02-24T23:12:31+01:00" level=error msg="LoadPageLogger00003 - Missing arg: client - cannot determine loading status" arg.client=<nil> logger.name=LoadPageLogger

JSON 格式如下:

{"arg.client":null,"level":"error","logger.name":"LoadPageLogger","msg":"LoadPageLogger00003 - Missing arg: client - cannot determine loading status", "time":"2017-02-24T23:14:28+01:00"}

III. Golang 日志上下文的力量

现在 Golang 日志已经按照特定结构和标准格式记录,时间会决定需要添加哪些上下文以及相关信息。为了能从你的日志中抽取信息,例如追踪一个用户活动或者工作流,上下文和元数据的顺序非常重要。

例如在 logrus 库中可以按照下面这样使用 JSON 格式添加 hostnameappnamesession 参数:

// 对于元数据,通常做法是通过复用来重用日志语句中的字段。
  contextualizedLog := log.WithFields(log.Fields{
    "hostname": "staging-1",
    "appname": "foo-app",
    "session": "1ce3f6v"
  })
contextualizedLog.Info("Simple event with global metadata")

元数据可以视为 javascript 片段。为了更好地说明它们有多么重要,让我们看看几个 Golang 微服务中元数据的使用。你会清楚地看到是怎么在你的应用程序中跟踪用户的。这是因为你不仅需要知道一个错误发生了,还要知道是哪个实例以及什么模式导致了错误。假设我们有两个按顺序调用的微服务。上下文信息保存在头部(header)中传输:

func helloMicroService1(w http.ResponseWriter, r *http.Request) {
client := &http.Client{}
// 该服务负责接收所有到来的用户请求
// 我们会检查是否是一个新的会话还是已有会话的另一次调用
session := r.Header.Get("x-session")
if ( session == "") {
session = generateSessionId()
// 为新会话记录日志
}
// 每个请求的 Track Id 都是唯一的,因此我们会为每个会话生成一个
track := generateTrackId()
// 调用你的第二个微服务,添加 session/track
reqService2, _ := http.NewRequest("GET", "http://localhost:8082/", nil)
reqService2.Header.Add("x-session", session)
reqService2.Header.Add("x-track", track)
resService2, _ := client.Do(reqService2)
….

当调用第二个服务时:

func helloMicroService2(w http.ResponseWriter, r *http.Request) {
// 类似之前的微服务,我们检查会话并生成新的 track
session := r.Header.Get("x-session")
track := generateTrackId()
// 这一次,我们检查请求中是否已经设置了一个 track id,
// 如果是,它变为父 track
parent := r.Header.Get("x-track")
if (session == "") {
w.Header().Set("x-parent", parent)
}
// 为响应添加 meta 信息
w.Header().Set("x-session", session)
w.Header().Set("x-track", track)
if (parent == "") {
w.Header().Set("x-parent", track)
}
// 填充响应
w.WriteHeader(http.StatusOK)
io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, session, track, parent))
}

现在第二个微服务中已经有和初始查询相关的上下文和信息,一个 JSON 格式的日志消息看起来类似如下。

在第一个微服务:

{"appname":"go-logging","level":"debug","msg":"hello from ms 1","session":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","track":"UzWHRihF"}

在第二个微服务:

{"appname":"go-logging","level":"debug","msg":"hello from ms 2","parent":"UzWHRihF","session":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","track":"DPRHBMuE"}

如果在第二个微服务中出现了错误,多亏了 Golang 日志中保存的上下文信息,现在我们就可以确定它是怎样被调用的以及什么模式导致了这个错误。

如果你想进一步深挖 Golang 的追踪能力,这里还有一些库提供了追踪功能,例如 Opentracing。这个库提供了一种简单的方式在或复杂或简单的架构中添加追踪的实现。它通过不同步骤允许你追踪用户的查询,就像下面这样:

IV. Golang 日志对性能的影响

1) 不要在 Goroutine 中使用日志

在每个 goroutine 中创建一个新的日志器看起来很诱人。但最好别这么做。Goroutine 是一个轻量级线程管理器,它用于完成一个 “简单的” 任务。因此它不应该负责日志。它可能导致并发问题,因为在每个 goroutine 中使用 log.New() 会重复接口,所有日志器会并发尝试访问同一个 io.Writer。

为了限制对性能的影响以及避免并发调用 io.Writer,库通常使用一个特定的 goroutine 用于日志输出。

2) 使用异步库

尽管有很多可用的 Golang 日志库,要注意它们中的大部分都是同步的(事实上是伪异步)。原因很可能是到现在为止它们中没有一个会由于日志严重影响性能。

但正如 Kjell Hedström 在他的实验中展示的,使用多个线程创建成千上万日志,即便是在最坏情况下,异步 Golang 日志也会有 40% 的性能提升。因此日志是有开销的,也会对你的应用程序性能产生影响。如果你并不需要处理大量的日志,使用伪异步 Golang 日志库可能就足够了。但如果你需要处理大量的日志,或者很关注性能,Kjell Hedström 的异步解决方案就很有趣(尽管事实上你可能需要进一步开发,因为它只包括了最小的功能需求)。

3)使用严重等级管理 Golang 日志

一些日志库允许你启用或停用特定的日志器,这可能会派上用场。例如在生产环境中你可能不需要一些特定等级的日志。下面是一个如何在 glog 库中停用日志器的例子,其中日志器被定义为布尔值:

type Log bool
func (l Log) Println(args ...interface{}) {
    fmt.Println(args...)
}
var debug Log = false
if debug {
    debug.Println("DEBUGGING")
}

然后你就可以在配置文件中定义这些布尔参数来启用或者停用日志器。

没有一个好的 Golang 日志策略,Golang 日志可能开销很大。开发人员应该抵制记录几乎所有事情的诱惑 - 尽管它非常有趣!如果日志的目的是为了获取尽可能多的信息,为了避免包含无用元素的日志的白噪音,必须正确使用日志。

V. 集中化 Golang 日志

如果你的应用程序是部署在多台服务器上的,这样可以避免为了调查一个现象需要连接到每一台服务器的麻烦。日志集中确实有用。

使用日志装箱工具,例如 windows 中的 Nxlog,linux 中的 Rsyslog(默认安装了的)、Logstash 和 FluentD 是最好的实现方式。日志装箱工具的唯一目的就是发送日志,因此它们能够处理连接失效以及其它你很可能会遇到的问题。

这里甚至有一个 Golang syslog 软件包 帮你将 Golang 日志发送到 syslog 守护进程。

希望你享受你的 Golang 日志之旅

在你项目一开始就考虑你的 Golang 日志策略非常重要。如果在你代码的任意地方都可以获得所有的上下文,追踪用户就会变得很简单。从不同服务中阅读没有标准化的日志是已经很痛苦的事情。一开始就计划在多个微服务中扩展相同用户或请求 id,后面就会允许你比较容易地过滤信息并在你的系统中跟踪活动。

你是在构架一个很大的 Golang 项目还是几个微服务也会影响你的日志策略。一个大项目的主要组件应该有按照它们功能命名的特定 Golang 日志器。这使你可以立即判断出日志来自你的哪一部分代码。然而对于微服务或者小的 Golang 项目,只有较少的核心组件需要它们自己的日志器。但在每种情形中,日志器的数目都应该保持低于核心功能的数目。

你现在已经可以使用 Golang 日志量化决定你的性能或者用户满意度啦!

如果你有想阅读的特定编程语言,在 Twitter @logmatic 上告诉我们吧。


via: https://logmatic.io/blog/our-guide-to-a-golang-logs-world/

作者:Nils 译者:ictlyh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出