Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

在本地开发环境和GCP Stackdriver Logging中更好的输出日志 #75

Open
mrdulin opened this issue May 7, 2019 · 0 comments
Open

Comments

@mrdulin
Copy link
Owner

mrdulin commented May 7, 2019

前言

日志组件是开发调试过程必不可少的一个组件,对于开发测试等技术人员快速定位问题很有帮助,好的日志可读性强,容易理解,甚至对于BA,产品经理,老板等非技术人员,理解起来也没什么难度。持久化的日志对于回溯,复盘问题,数据统计,数据分析也有帮助。

这篇文章很浅显易懂,不会讲Logstash, ElasticSearch, fluentd等等类似的东西,只简单讲一下如何在本地开发阶段,以及部署到GCP上的运行阶段,如何在本地和GCP Stackdriver Logging服务中更好的输出日志,作为抛砖引玉。需要熟悉以下云服务和技术栈:

  • GCP Cloud Function
  • GCP Stackdriver Logging
  • Express.js/Node.js

不友好的日志

飞先来看一个使用JavaScript语言原生API打印日志的例子:

const faker = require('faker');

const aLog = {
  context: 'UserService.findById',
  arguments: {
    id: faker.random.uuid(),
    extra: {
      campaignId: faker.random.uuid(),
      location: {
        id: faker.random.uuid(),
        latitude: faker.address.latitude(),
        longitude: faker.address.longitude()
      }
    }
  },
  labels: ['UserService', 'bad logging', 'info']
};

console.log(aLog);

输出:

☁  nodejs-gcp [master] ⚡  node /Users/ldu020/workspace/github.com/mrdulin/nodejs-gcp/src/stackdriver/better-logging/bad-logger.spec.js
{ context: 'UserService.findById',
  arguments: 
   { id: '99f37477-2608-4312-a8e0-623baa05113a',
     extra: 
      { campaignId: '1a93cd81-6b12-4676-a8b6-6973475fe6cc',
        location: [Object] } },
  labels: [ 'UserService', 'bad logging', 'info' ] }

这样的日志显而易见有三个问题:

  • 没有时间戳
  • 没有日志级别
  • 没有好的格式

更友好的日志

现在来改进这三个问题,使用winston模块,改造后本地开发环境日志输出如下:

Debug日志:

local-dev-better-logging-debug

Error日志:

local-dev-better-error-logging

改进后的日志包含如下信息:

  • 时间戳(timestamp): 根据时间戳定位日志范围
  • 日志级别(log level): debug, info, error 等等
  • 当前服务(应用程序)名称(service name):我用了package.jsonname字段作为当前服务(应用程序)名称
  • 上下文信息(context): UserService.findById
  • 元数据信息(metadata): 这里是arguments字段,包含一些调试用的id,入参等信息
  • 标签(labels): 用于筛选日志,本地开发,最简单的,最没有成本的筛选操作就是终端通过搜索label定位日志
  • 日志主体(log body): 这里是message字段
  • 错误堆栈(error stack)

以上信息对于日常开发调试基本够用了。

在GCP Stackdriver Logging中打印日志

部署到GCP上,本例的应用程序运行在cloud function环境。GCP官方提供了Stackdriver Logging Winston 插件@google-cloud/logging-winston,使用该插件来优化日志组件。不过在优化之前,先来看一个使用console打印日志的例子:

首先构造一个user对象,是个有三层嵌套的对象,作为我们的日志主体。

const faker = require('faker');

const user = {
  id: faker.random.uuid(),
  name: faker.name.findName(),
  email: faker.internet.email(),
  address: {
    city: faker.address.city(),
    country: faker.address.country(),
    street: {
      streetPrefix: faker.address.streetPrefix(),
      streetSuffix: faker.address.streetSuffix()
    }
  }
};

module.exports = { user };

然后我们使用console.log,传入各种形式的入参来打印该user对象,测试哪种方式打印的日志可读性更强,格式更好。Cloud Function如下:

const { user } = require('../data');

exports.consoleLogging = async (req, res) => {
  console.log('[log 1] user: ', user);
  console.log(`[log 2] user: ${user}`);
  console.log('[log 3] user: %o', user);
  console.log(`[log 4] user: ${JSON.stringify(user)}`);
  console.log('[log 5] user');
  console.log(user);
  console.log('[log 6] user: \n ', user);
  console.log('[log 7] user: %s', JSON.stringify(user));
  console.log(`[log 8] user: ${JSON.stringify(user, null, 2)}`);
  console.log(`[log 9] user:\n${JSON.stringify(user, null, 2)}`);
  console.log('[log 10] user');
  console.log(JSON.stringify(user, null, 2));

  try {
    await findById();
    res.sendStatus(200);
  } catch (error) {
    console.error(error);
    res.sendStatus(500);
  }
};

function findById() {
  return new Promise((resolve, reject) => {
    process.nextTick(() => {
      reject(new Error('something bad happened'));
    });
  });
}

我们来看上下文是[log 1]的日志在GCP Stackdriver Logging中打印出来长什么样,点击Expand all,展开日志:

图片

看到这个日志,有这么几个问题:

  • 没有格式化(prettify)
  • 无法添加标签(labels)
  • 无法使用Stackdriver Logging提供的日志过滤和筛选功能(filter)
  • 没有上下文(context),本例没有加入上下文信息,如果加入,为了区分日志主体和上下文,我们可能会构造这样的日志数据:{user, context}
  • 整个日志主体和上下文都以字符串的形式存在textpayload字段中,没有区分,不够“结构化”,并且嵌套越深可读性越差。

另外9个测试打印的日志都不尽人意,感兴趣的同学可以部署源码到Cloud Function查看各个测试用例的输出。

改造后的日志输出如下:

Info日志:

图片

Error日志:

图片

日志筛选和过滤:

图片

可用的日志筛选条件:

图片

可以看到改造后的日志更“结构化”(JSON-structured log messages),这种结构化带来了更好的日志筛选和过滤功能,可以根据日志的相关字段进行筛选过滤。这是textpayload这种字符串形式的日志做不到的。封装后的日志组件可以根据需要进行配置,还可以更方便的开启与关闭,比如生产环境(process.env.NODE_ENV === 'production'),我们日志级别设置为error。最好避免在代码各处直接使用原生console,一方面无法配置,另一方面违反tslint,jslint等代码检查工具的规则,当然,我们可以使用grunt, gulp,webpack等这样的资源依赖打包工具来移除console

日志组件关键配置如下:

const winston = require('winston');
const { LoggingWinston } = require('@google-cloud/logging-winston');
const { format } = require('winston');
const pkg = require('./package.json');

function jsonFormat(obj) {
  return JSON.stringify(obj, null, 2);
}

let transports = [];
let level = 'debug';
if (process.env.NODE_ENV === 'production') {
  const loggingWinston = new LoggingWinston({
    serviceContext: {
      service: pkg.name,
      version: pkg.version
    }
  });
  transports = [new winston.transports.Console(), loggingWinston];
  level = 'error';
} else {
  const printf = format.printf((info) => {
    const { level, ...rest } = info;
    let log;
    if (rest.stack) {
      const { stack, ...others } = rest;
      log =
        `[${info.timestamp}][${info.level}]: ${jsonFormat(others)}\n\n` +
        `[${info.timestamp}][${info.level}]: ${stack}\n\n`;
    } else {
      log = `[${info.timestamp}][${info.level}]: ${jsonFormat(rest)}\n\n`;
    }
    return log;
  });
  transports = [
    new winston.transports.Console({
      format: format.combine(format.colorize(), format.timestamp(), format.errors({ stack: true }), printf)
    })
  ];
}

const logger = winston.createLogger({
  level,
  defaultMeta: { service: pkg.name },
  transports
});

logger.debug(`process.env.NODE_ENV: ${process.env.NODE_ENV}`);

module.exports = { logger };

好的日志不仅能提升开发测试效率,看起来也赏心悦目,让生活少点痛苦。兼容各个云平台,容器环境,系统环境,本地和线上环境的日志组件需要不断进化,一个日志组件最开始在一个项目中使用 ,随着时间推移,更好的封装,更方便的使用,更多场景的覆盖,更强的稳定性,那么就可以作为一个通用的模块在多个项目中使用,然后是产品线,接着是公司的所有产品线,最后就可以按照心情决定是否开源,推广到全世界。

源码地址

参考


Flag Counter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant