阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 日志管理

日志管理

作者:陈川 阅读数:65226人阅读 分类: Node.js

日志管理的重要性

日志是应用程序运行过程中产生的记录,用于追踪系统行为、排查问题和分析性能。良好的日志管理能帮助开发者快速定位问题,提高系统可维护性。在Node.js中,日志管理尤为重要,因为Node.js的单线程特性使得错误追踪更加依赖日志记录。

常见的日志级别

日志通常分为几个级别,每个级别对应不同的重要性:

const levels = {
  error: 0,    // 错误,需要立即处理
  warn: 1,     // 警告,潜在问题
  info: 2,     // 重要信息
  verbose: 3,  // 详细信息
  debug: 4,    // 调试信息
  silly: 5     // 最详细的日志
};

Node.js中的日志模块

console模块

Node.js内置的console模块是最基础的日志工具:

console.error('错误信息');
console.warn('警告信息');
console.info('普通信息');
console.log('等同于info');
console.debug('调试信息');

Winston日志库

Winston是Node.js中最流行的日志库之一:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

Bunyan日志库

另一个流行的选择是Bunyan,它特别适合结构化日志:

const bunyan = require('bunyan');
const log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'
    },
    {
      level: 'info',
      stream: process.stdout
    }
  ]
});

log.info('服务器启动');
log.error({err: new Error('错误示例')}, '发生错误');

日志格式最佳实践

结构化日志

结构化日志比纯文本日志更易于分析和处理:

// 不好的做法
logger.info('用户登录: 张三');

// 好的做法
logger.info({
  event: 'user_login',
  username: '张三',
  ip: '192.168.1.1',
  timestamp: new Date().toISOString()
});

包含上下文信息

日志应包含足够的上下文信息:

function processOrder(order) {
  const logContext = {
    orderId: order.id,
    userId: order.userId,
    amount: order.amount
  };
  
  logger.info(logContext, '开始处理订单');
  try {
    // 处理订单逻辑
    logger.info(logContext, '订单处理成功');
  } catch (err) {
    logger.error({...logContext, error: err.message}, '订单处理失败');
    throw err;
  }
}

日志存储策略

文件存储

最基本的日志存储方式是写入文件:

const fs = require('fs');
const path = require('path');

function writeLog(level, message) {
  const logFile = path.join(__dirname, 'app.log');
  const logEntry = `[${new Date().toISOString()}] [${level}] ${message}\n`;
  
  fs.appendFile(logFile, logEntry, (err) => {
    if (err) console.error('写入日志失败:', err);
  });
}

日志轮转

防止日志文件过大,需要实现日志轮转:

const { createGzip } = require('zlib');
const { pipeline } = require('stream');

function rotateLogs() {
  const currentDate = new Date();
  const oldFile = 'app.log';
  const newFile = `app.${currentDate.toISOString().split('T')[0]}.log.gz`;
  
  pipeline(
    fs.createReadStream(oldFile),
    createGzip(),
    fs.createWriteStream(newFile),
    (err) => {
      if (err) {
        console.error('日志轮转失败:', err);
      } else {
        fs.truncate(oldFile, 0, (err) => {
          if (err) console.error('清空日志文件失败:', err);
        });
      }
    }
  );
}

// 每天午夜执行日志轮转
setInterval(rotateLogs, 24 * 60 * 60 * 1000);

日志分析与监控

ELK Stack

ELK(Elasticsearch, Logstash, Kibana)是流行的日志分析解决方案:

  1. Logstash配置示例:
input {
  file {
    path => "/var/log/node-app/*.log"
    start_position => "beginning"
  }
}

filter {
  grok {
    match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\] \[%{LOGLEVEL:level}\] %{GREEDYDATA:message}" }
  }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
  }
}

实时日志监控

使用Socket.IO实现实时日志监控:

const io = require('socket.io')(3001);
const tail = require('tail').Tail;

const logFile = new tail('app.log');

io.on('connection', (socket) => {
  logFile.on('line', (data) => {
    socket.emit('log', data);
  });
});

性能考虑

异步日志记录

同步日志会阻塞事件循环,应尽可能使用异步:

// 同步方式 - 不推荐
fs.writeFileSync('sync.log', logEntry);

// 异步方式 - 推荐
fs.writeFile('async.log', logEntry, (err) => {
  if (err) console.error('异步写入日志失败:', err);
});

批量写入

高频日志场景应考虑批量写入:

let logBuffer = [];
const BATCH_SIZE = 100;
const BATCH_INTERVAL = 5000; // 5秒

function addToBuffer(logEntry) {
  logBuffer.push(logEntry);
  if (logBuffer.length >= BATCH_SIZE) {
    flushLogs();
  }
}

function flushLogs() {
  if (logBuffer.length === 0) return;
  
  const logsToWrite = logBuffer.join('\n');
  logBuffer = [];
  
  fs.appendFile('batch.log', logsToWrite + '\n', (err) => {
    if (err) console.error('批量写入日志失败:', err);
  });
}

// 定时刷新缓冲区
setInterval(flushLogs, BATCH_INTERVAL);

安全考虑

敏感信息过滤

日志中不应包含敏感信息:

function sanitizeLog(data) {
  const sensitiveFields = ['password', 'creditCard', 'ssn'];
  
  return JSON.parse(JSON.stringify(data, (key, value) => {
    if (sensitiveFields.includes(key)) {
      return '[REDACTED]';
    }
    return value;
  }));
}

logger.info(sanitizeLog({
  username: 'user1',
  password: 'secret123',
  action: 'login'
}));

日志访问控制

确保日志文件有适当的权限:

// 设置日志文件权限为640 (rw-r-----)
fs.chmod('app.log', 0o640, (err) => {
  if (err) console.error('设置文件权限失败:', err);
});

多环境日志配置

不同环境应有不同的日志配置:

function createLogger(env) {
  const commonTransports = [
    new winston.transports.File({ filename: 'errors.log', level: 'error' })
  ];
  
  if (env === 'production') {
    return winston.createLogger({
      level: 'info',
      transports: [
        ...commonTransports,
        new winston.transports.File({ filename: 'combined.log' })
      ]
    });
  } else {
    return winston.createLogger({
      level: 'debug',
      transports: [
        ...commonTransports,
        new winston.transports.Console()
      ]
    });
  }
}

请求追踪

在Web应用中,为每个请求分配唯一ID便于追踪:

const uuid = require('uuid');

app.use((req, res, next) => {
  req.requestId = uuid.v4();
  logger.info({
    requestId: req.requestId,
    method: req.method,
    url: req.url,
    ip: req.ip
  }, '收到请求');
  
  const originalEnd = res.end;
  res.end = function(...args) {
    logger.info({
      requestId: req.requestId,
      statusCode: res.statusCode,
      responseTime: Date.now() - req.startTime
    }, '请求完成');
    originalEnd.apply(res, args);
  };
  
  req.startTime = Date.now();
  next();
});

错误处理与日志

正确处理错误并记录:

process.on('uncaughtException', (err) => {
  logger.error({
    error: err.message,
    stack: err.stack
  }, '未捕获的异常');
  
  // 根据严重程度决定是否退出
  if (err.isFatal) {
    process.exit(1);
  }
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error({
    reason: reason instanceof Error ? reason.stack : reason,
    promise
  }, '未处理的Promise拒绝');
});

日志测试

确保日志系统正常工作:

describe('日志系统', () => {
  let logOutput;
  const originalWrite = process.stdout.write;
  
  beforeEach(() => {
    logOutput = '';
    process.stdout.write = (chunk) => {
      logOutput += chunk;
    };
  });
  
  afterEach(() => {
    process.stdout.write = originalWrite;
  });
  
  it('应正确记录错误', () => {
    logger.error('测试错误');
    expect(logOutput).to.contain('测试错误');
    expect(logOutput).to.contain('error');
  });
});

日志与性能监控集成

将日志与APM工具集成:

const apm = require('elastic-apm-node').start({
  serviceName: 'my-node-app'
});

function trackError(err) {
  apm.captureError(err);
  logger.error({
    error: err.message,
    stack: err.stack,
    transactionId: apm.currentTransaction?.ids['transaction.id']
  }, '应用程序错误');
}

try {
  // 可能出错的代码
} catch (err) {
  trackError(err);
}

自定义日志格式

创建自定义日志格式:

const { format } = require('winston');
const util = require('util');

const customFormat = format.printf(({ level, message, timestamp, ...metadata }) => {
  let msg = `${timestamp} [${level}] ${message}`;
  if (Object.keys(metadata).length > 0) {
    msg += ' ' + util.inspect(metadata, { colors: true, depth: null });
  }
  return msg;
});

const logger = winston.createLogger({
  format: format.combine(
    format.timestamp(),
    format.colorize(),
    customFormat
  ),
  transports: [new winston.transports.Console()]
});

日志采样

高流量环境下可考虑日志采样:

const sampledLogger = winston.createLogger({
  transports: [
    new winston.transports.Console({
      level: 'info',
      sampleRate: 0.1 // 只记录10%的日志
    }),
    new winston.transports.File({
      filename: 'important.log',
      level: 'error' // 错误日志全部记录
    })
  ]
});

分布式系统日志

在微服务架构中,需要集中式日志管理:

const { createLogger } = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');

const esTransport = new ElasticsearchTransport({
  level: 'info',
  clientOpts: { node: 'http://localhost:9200' }
});

const logger = createLogger({
  transports: [esTransport]
});

// 添加服务标识
logger.defaultMeta = { service: 'order-service' };

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:调试工具使用

下一篇:监控告警系统

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌