我眼里的日志系统:
发生异常---->将异常信息写入日志文件------>退出
Nginx实现的日志系统
专门的日志数据结构
struct ngx_log_s {
ngx_uint_t log_level; // 日志级别,用于控制日志的详细程度(例如,DEBUG、INFO、ERROR 等)
ngx_open_file_t *file; // 指向打开文件的指针,表示日志输出的目标文件
ngx_atomic_uint_t connection; // 记录当前连接数,使用原子类型以保证多线程环境下的安全性
time_t disk_full_time; // 上次磁盘满时间,用于记录磁盘满的时间戳
ngx_log_handler_pt handler; // 自定义日志处理函数指针,用于特殊日志处理
void *data; // 任意类型的指针,可以存放与日志相关的附加数据
ngx_log_writer_pt writer; // 自定义日志写入函数指针,用于特殊日志写入方式
void *wdata; // 任意类型的指针,可以存放与日志写入相关的附加数据
/*
* 我们将 "action" 声明为 "char *" 类型,因为动作通常是静态字符串,
* 如果使用 "u_char *" 类型,我们需要经常覆盖其类型。
*/
char *action; // 描述当前操作的字符串(例如,"reading client request")
ngx_log_t *next; // 指向下一个日志对象的指针,用于支持链式日志记录
};
Nginx的文件描述
struct ngx_open_file_s {
ngx_fd_t fd; // 文件描述符
ngx_str_t name; // 文件名
void (*flush)(ngx_open_file_t *file, ngx_log_t *log); // 刷新文件缓冲区函数指针
void *data; // 指向用户自定义数据的指针
};
将错误日志进行分类—>等级数组
static ngx_str_t err_levels[] = {
ngx_null_string,
ngx_string("emerg"),
ngx_string("alert"),
ngx_string("crit"),
ngx_string("error"),
ngx_string("warn"),
ngx_string("notice"),
ngx_string("info"),
ngx_string("debug")
};
初始化日志文件
ngx_log_t * ngx_log_init(u_char *prefix, u_char *error_log)
{
u_char *p, *name;
size_t nlen, plen;
// 初始化全局的 ngx_log 结构
ngx_log.file = &ngx_log_file;
ngx_log.log_level = NGX_LOG_NOTICE; // 将默认日志级别设置为 NOTICE
// 如果未提供错误日志路径,则使用默认错误日志路径
if (error_log == NULL) {
error_log = (u_char *) NGX_ERROR_LOG_PATH;
}
name = error_log; // 将提供的错误日志路径赋给 name
nlen = ngx_strlen(name); // 获取错误日志路径的长度
// 如果错误日志路径为空,则使用标准错误输出(stderr)
if (nlen == 0) {
ngx_log_file.fd = ngx_stderr;
return &ngx_log;
}
p = NULL;
// 如果错误日志路径不是绝对路径,则需要拼接前缀
#if (NGX_WIN32)
if (name[1] != ':') {
#else
if (name[0] != '/') {
#endif
if (prefix) {
plen = ngx_strlen(prefix); // 计算前缀的长度
} else {
#ifdef NGX_PREFIX
prefix = (u_char *) NGX_PREFIX;
plen = ngx_strlen(prefix);
#else
plen = 0;
#endif
}
if (plen) {
name = malloc(plen + nlen + 2); // 分配存储路径的内存空间
if (name == NULL) {
return NULL;
}
p = ngx_cpymem(name, prefix, plen); // 将前缀拷贝到路径中
if (!ngx_path_separator(*(p - 1))) { // 如果前缀末尾没有路径分隔符,则添加
*p++ = '/';
}
ngx_cpystrn(p, error_log, nlen + 1); // 将错误日志路径拷贝到路径末尾
p = name;
}
}
// 打开错误日志文件
ngx_log_file.fd = ngx_open_file(name, NGX_FILE_APPEND,
NGX_FILE_CREATE_OR_OPEN,
NGX_FILE_DEFAULT_ACCESS);
// 如果打开文件失败,则输出错误日志到标准错误输出(stderr)
if (ngx_log_file.fd == NGX_INVALID_FILE) {
ngx_log_stderr(ngx_errno,
"[alert] could not open error log file: "
ngx_open_file_n " \"%s\" failed", name);
#if (NGX_WIN32)
ngx_event_log(ngx_errno,
"could not open error log file: "
ngx_open_file_n " \"%s\" failed", name);
#endif
ngx_log_file.fd = ngx_stderr;
}
// 释放分配的内存
if (p) {
ngx_free(p);
}
return &ngx_log;
}
设置日志文件配置项
char *
ngx_log_set_log(ngx_conf_t *cf, ngx_log_t **head)
{
ngx_log_t *new_log; // 新日志结构体
ngx_str_t *value, name; // 配置项值和名称
ngx_syslog_peer_t *peer; // syslog 配置结构体
// 如果当前日志链表已存在且日志级别为0,表示已存在一个未指定级别的日志结构
if (*head != NULL && (*head)->log_level == 0) {
new_log = *head; // 直接使用现有的日志结构
} else {
// 否则,创建一个新的日志结构
new_log = ngx_pcalloc(cf->pool, sizeof(ngx_log_t));
if (new_log == NULL) {
return NGX_CONF_ERROR;
}
// 如果当前日志链表为空,则将新日志结构设置为链表的头部
if (*head == NULL) {
*head = new_log;
}
}
// 获取配置项参数值
value = cf->args->elts;
// 如果配置为输出到标准错误输出
if (ngx_strcmp(value[1].data, "stderr") == 0) {
ngx_str_null(&name); // 清空日志文件名
cf->cycle->log_use_stderr = 1; // 标记使用标准错误输出
// 打开标准错误输出文件
new_log->file = ngx_conf_open_file(cf->cycle, &name);
if (new_log->file == NULL) {
return NGX_CONF_ERROR;
}
// 如果配置为输出到内存
} else if (ngx_strncmp(value[1].data, "memory:", 7) == 0) {
#if (NGX_DEBUG)
size_t size, needed; // 缓冲区大小及所需大小
ngx_pool_cleanup_t *cln; // 清理回调结构体
ngx_log_memory_buf_t *buf; // 内存日志缓冲区
// 去除"memory:"前缀
value[1].len -= 7;
value[1].data += 7;
// 计算所需的缓冲区大小
needed = sizeof("MEMLOG :" NGX_LINEFEED)
+ cf->conf_file->file.name.len
+ NGX_SIZE_T_LEN
+ NGX_INT_T_LEN
+ NGX_MAX_ERROR_STR;
// 解析配置的缓冲区大小
size = ngx_parse_size(&value[1]);
// 如果解析失败或缓冲区太小,则返回错误
if (size == (size_t) NGX_ERROR || size < needed) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid buffer size \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
// 分配内存日志缓冲区
buf = ngx_pcalloc(cf->pool, sizeof(ngx_log_memory_buf_t));
if (buf == NULL) {
return NGX_CONF_ERROR;
}
// 分配缓冲区内存
buf->start = ngx_pnalloc(cf->pool, size);
if (buf->start == NULL) {
return NGX_CONF_ERROR;
}
buf->end = buf->start + size;
// 初始化缓冲区,记录日志文件名及行号
buf->pos = ngx_slprintf(buf->start, buf->end, "MEMLOG %uz %V:%ui%N",
size, &cf->conf_file->file.name,
cf->conf_file->line);
// 填充剩余空间为 ' '
ngx_memset(buf->pos, ' ', buf->end - buf->pos);
// 设置清理回调,用于释放内存日志缓冲区
cln = ngx_pool_cleanup_add(cf->pool, 0);
if (cln == NULL) {
return NGX_CONF_ERROR;
}
cln->data = new_log;
cln->handler = ngx_log_memory_cleanup;
// 设置日志写入函数和写入数据
new_log->writer = ngx_log_memory_writer;
new_log->wdata = buf;
#else
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"nginx was built without debug support");
return NGX_CONF_ERROR;
#endif
// 如果配置为输出到syslog
} else if (ngx_strncmp(value[1].data, "syslog:", 7) == 0) {
// 分配syslog配置结构体
peer = ngx_pcalloc(cf->pool, sizeof(ngx_syslog_peer_t));
if (peer == NULL) {
return NGX_CONF_ERROR;
}
// 处理syslog配置
if (ngx_syslog_process_conf(cf, peer) != NGX_CONF_OK) {
return NGX_CONF_ERROR;
}
// 设置日志写入函数和写入数据
new_log->writer = ngx_syslog_writer;
new_log->wdata = peer;
// 其他情况,输出到指定文件
} else {
// 打开指定文件
new_log->file = ngx_conf_open_file(cf->cycle, &value[1]);
if (new_log->file == NULL) {
return NGX_CONF_ERROR;
}
}
// 设置日志级别
if (ngx_log_set_levels(cf, new_log) != NGX_CONF_OK) {
return NGX_CONF_ERROR;
}
// 如果日志链表不为空,则将新日志插入到链表中
if (*head != new_log) {
ngx_log_insert(*head, new_log);
}
return NGX_CONF_OK;
}
格式化错误信息
void ngx_cdecl
ngx_log_abort(ngx_err_t err, const char *fmt, ...)
{
u_char *p;
va_list args;
u_char errstr[NGX_MAX_CONF_ERRSTR];
//使用va_start宏开始对可变参数的处理,并调用ngx_vsnprintf函数将格式化字符串fmt和可变参数args写入errstr数组中,生成格式化的错误信息
va_start(args, fmt);
p = ngx_vsnprintf(errstr, sizeof(errstr) - 1, fmt, args);
va_end(args);
//将生成的错误信息以NGX_LOG_ALERT级别写入到nginx的日志中
ngx_log_error(NGX_LOG_ALERT, ngx_cycle->log, err,
"%*s", p - errstr, errstr);
}
错误码err转换为对应的错误描述
u_char *
ngx_log_errno(u_char *buf, u_char *last, ngx_err_t err)
{
//首先函数进行了边界检查,确保在向缓冲区写入前还有足够的空间。如果缓冲区的剩余空间不足50个字节,就会留出一个空间用于放置错误码,然后在缓冲区的末尾写入三个点号"...",以表示截断了部分错误信息。
if (buf > last - 50) {
/* leave a space for an error code */
buf = last - 50;
*buf++ = '.';
*buf++ = '.';
*buf++ = '.';
}
#if (NGX_WIN32)
buf = ngx_slprintf(buf, last, ((unsigned) err < 0x80000000)
? " (%d: " : " (%Xd: ", err);
#else
buf = ngx_slprintf(buf, last, " (%d: ", err);
#endif
//调用ngx_strerror函数将错误码转换为对应的错误描述,并将描述信息追加到缓冲区中
buf = ngx_strerror(err, buf, last - buf);
if (buf < last) {
*buf++ = ')';
}
return buf;
}
异常信息首先输入到缓冲区
u_char * ngx_cdecl
ngx_slprintf(u_char *buf, u_char *last, const char *fmt, ...)
{
u_char *p;
va_list args;
va_start(args, fmt);
p = ngx_vslprintf(buf, last, fmt, args);
va_end(args);
return p;
}
打开/新建日志文件并插入日志链表
ngx_int_t
ngx_log_open_default(ngx_cycle_t *cycle)
{
ngx_log_t *log;
if (ngx_log_get_file_log(&cycle->new_log) != NULL) {
return NGX_OK;
}
if (cycle->new_log.log_level != 0) {
/* there are some error logs, but no files */
log = ngx_pcalloc(cycle->pool, sizeof(ngx_log_t));
if (log == NULL) {
return NGX_ERROR;
}
} else {
/* no error logs at all */
log = &cycle->new_log;
}
log->log_level = NGX_LOG_ERR;
log->file = ngx_conf_open_file(cycle, &cycle->error_log);
if (log->file == NULL) {
return NGX_ERROR;
}
if (log != &cycle->new_log) {
ngx_log_insert(&cycle->new_log, log);
}
return NGX_OK;
}
获取日志文件
ngx_log_t *
ngx_log_get_file_log(ngx_log_t *head)
{
ngx_log_t *log;
for (log = head; log; log = log->next) {
if (log->file != NULL) {
return log;
}
}
return NULL;
}
刷入磁盘
/*
* 将文本写入标准错误流
*/
static ngx_inline void
ngx_write_stderr(char *text)
{
(void) ngx_write_fd(ngx_stderr, text, ngx_strlen(text));
}
/*
* 将文本写入标准输出流
*/
static ngx_inline void
ngx_write_stdout(char *text)
{
(void) ngx_write_fd(ngx_stdout, text, ngx_strlen(text));
}
/*
* 在指定文件描述符上写入数据
* 参数:
* - fd:文件描述符
* - buf:待写入数据的缓冲区指针
* - n:待写入数据的字节数
* 返回值:
* 成功:返回写入的字节数
* 失败:返回-1,并设置errno为对应的错误码
*/
static ngx_inline ssize_t
ngx_write_fd(ngx_fd_t fd, void *buf, size_t n)
{
return write(fd, buf, n);
}
输入到控制台的功能
void ngx_cdecl
ngx_log_stderr(ngx_err_t err, const char *fmt, ...)
{
u_char *p, *last; // 指向错误信息字符串的指针
va_list args; // 可变参数列表
u_char errstr[NGX_MAX_ERROR_STR]; // 错误信息缓冲区
last = errstr + NGX_MAX_ERROR_STR; // 缓冲区的末尾位置
p = ngx_cpymem(errstr, "nginx: ", 7); // 将固定的前缀拷贝到缓冲区
va_start(args, fmt); // 初始化可变参数列表
p = ngx_vslprintf(p, last, fmt, args); // 将格式化字符串和参数格式化到缓冲区
va_end(args); // 结束可变参数列表
// 如果有错误码,将错误码添加到错误信息中
if (err) {
p = ngx_log_errno(p, last, err);
}
// 如果错误信息超出缓冲区末尾,截断字符串
if (p > last - NGX_LINEFEED_SIZE) {
p = last - NGX_LINEFEED_SIZE;
}
ngx_linefeed(p); // 添加换行符
// 将错误信息输出到标准错误流
(void) ngx_write_console(ngx_stderr, errstr, p - errstr);
}
Nginx有关错误宏定义
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
#endif
Nginx处理错误字符串
选择不同的方法来获取错误信息并将其复制到指定的缓冲区中。
- 第一部分使用了
strerrordesc_np
函数,该函数在glibc 2.32中引入。它是异步信号安全的,可以直接使用它来获取系统错误信息,而无需复制错误消息。如果系统支持这个函数,就直接调用它获取错误信息。如果不支持,则使用预定义的ngx_unknown_error
。 - 第二部分处理了不支持
strerrordesc_np
的情况,主要是为了兼容之前的版本。它首先初始化了一个错误消息列表,存储了系统中可能的错误消息。然后通过strerror
函数获取每个错误码对应的错误消息,并将其存储在动态分配的内存中。这样,在需要获取错误信息时,就可以根据错误码直接在列表中查找对应的错误消息了。
static ngx_str_t ngx_unknown_error = ngx_string("Unknown error");
#if (NGX_HAVE_STRERRORDESC_NP)
/*
* strerrordesc_np()函数是在glibc 2.32中引入的,是异步信号安全的。
* 这使得可以直接使用它,而不需要复制错误消息。
*/
u_char *
ngx_strerror(ngx_err_t err, u_char *errstr, size_t size)
{
size_t len;
const char *msg;
msg = strerrordesc_np(err);
if (msg == NULL) {
msg = (char *) ngx_unknown_error.data;
len = ngx_unknown_error.len;
} else {
len = ngx_strlen(msg);
}
size = ngx_min(size, len);
return ngx_cpymem(errstr, msg, size);
}
ngx_int_t
ngx_strerror_init(void)
{
return NGX_OK;
}
#else
/*
* strerror()消息被复制,原因是:
*
* 1) strerror()和strerror_r()函数不是异步信号安全的,
* 因此不能在信号处理程序中使用;
*
* 2) 可以使用直接的sys_errlist[]数组来替代这些函数,
* 但Linux链接器会警告其使用:
*
* warning: `sys_errlist' is deprecated; use `strerror' or `strerror_r' instead
* warning: `sys_nerr' is deprecated; use `strerror' or `strerror_r' instead
*
* 这会导致错误的bug报告。
*/
static ngx_str_t *ngx_sys_errlist;
static ngx_err_t ngx_first_error;
static ngx_err_t ngx_last_error;
u_char *
ngx_strerror(ngx_err_t err, u_char *errstr, size_t size)
{
ngx_str_t *msg;
if (err >= ngx_first_error && err < ngx_last_error) {
msg = &ngx_sys_errlist[err - ngx_first_error];
} else {
msg = &ngx_unknown_error;
}
size = ngx_min(size, msg->len);
return ngx_cpymem(errstr, msg->data, size);
}
ngx_int_t
ngx_strerror_init(void)
{
char *msg;
u_char *p;
size_t len;
ngx_err_t err;
#if (NGX_SYS_NERR)
ngx_first_error = 0;
ngx_last_error = NGX_SYS_NERR;
#elif (EPERM > 1000 && EPERM < 0x7fffffff - 1000)
/*
* 如果错误数未知,并且EPERM错误代码有很大但合理的值,
* 则根据strerror()返回的错误消息从EPERM开始猜测可能的错误代码。
* 特别是,这包括GNU/Hurd,其错误从0x40000001开始。
*/
for (err = EPERM; err > EPERM - 1000; err--) {
ngx_set_errno(0);
msg = strerror(err);
if (errno == EINVAL
|| msg == NULL
|| strncmp(msg, "Unknown error", 13) == 0)
{
continue;
}
ngx_first_error = err;
}
for (err = EPERM; err < EPERM + 1000; err++) {
ngx_set_errno(0);
msg = strerror(err);
if (errno == EINVAL
|| msg == NULL
|| strncmp(msg, "Unknown error", 13) == 0)
{
continue;
}
ngx_last_error = err + 1;
}
#else
/*
* 如果错误数未知,则根据strerror()返回的错误消息猜测错误数。
*/
ngx_first_error = 0;
for (err = 0; err < 1000; err++) {
ngx_set_errno(0);
msg = strerror(err);
if (errno == EINVAL
|| msg == NULL
|| strncmp(msg, "Unknown error", 13) == 0)
{
continue;
}
ngx_last_error = err + 1;
}
#endif
/*
* ngx_strerror()在此阶段尚不准备工作,因此使用malloc(),
* 并使用strerror()记录可能的错误。
*/
len = (ngx_last_error - ngx_first_error) * sizeof(ngx_str_t);
ngx_sys_errlist = malloc(len);
if (ngx_sys_errlist == NULL) {
goto failed;
}
for (err = ngx_first_error; err < ngx_last_error; err++) {
msg = strerror(err);
if (msg == NULL) {
ngx_sys_errlist[err - ngx_first_error] = ngx_unknown_error;
continue;
}
len = ngx_strlen(msg);
p = malloc(len);
if (p == NULL) {
goto failed;
}
ngx_memcpy(p, msg, len);
ngx_sys_errlist[err - ngx_first_error].len = len;
ngx_sys_errlist[err - ngx_first_error].data = p;
}
return NGX_OK;
failed:
err = errno;
ngx_log_stderr(0, "malloc(%uz) failed (%d: %s)", len, err, strerror(err));
return NGX_ERROR;
}
#endif
总结
-
封装专门的日志和文件数据结构
-
分类异常信息
-
先读入缓冲区后刷入磁盘
-
日志链表设计以及日志配置设计
-
格式化输入输出
-
刷入磁盘
or; err < ngx_last_error; err++) {
msg = strerror(err);if (msg == NULL) { ngx_sys_errlist[err - ngx_first_error] = ngx_unknown_error; continue; } len = ngx_strlen(msg); p = malloc(len); if (p == NULL) { goto failed; } ngx_memcpy(p, msg, len); ngx_sys_errlist[err - ngx_first_error].len = len; ngx_sys_errlist[err - ngx_first_error].data = p;
}
return NGX_OK;
failed:
err = errno;
ngx_log_stderr(0, "malloc(%uz) failed (%d: %s)", len, err, strerror(err));
return NGX_ERROR;
}
#endif
## 总结
1. 封装专门的日志和文件数据结构
2. 分类异常信息
3. 先读入缓冲区后刷入磁盘
4. 日志链表设计以及日志配置设计
5. 格式化输入输出
6. 刷入磁盘