Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions doc/acid/category_log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Table of Content

- [Name](#name)
- [Status](#status)
- [Description](#description)
- [Synopsis](#synopsis)
- [Methods](#methods)
- [new](#new)
- [write_log](#write_log)
- [Author](#author)
- [Copyright and License](#copyright-and-license)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Name

acid.category_log

# Status

This library is considered production ready.

# Description

This lua module is used to write error log of different requst to
different log file. Error logs can be separated by response status
or something else.

It is implementd by wrapping the ngx.log function,
besides logging to the original log file normally, it also save logs
in memory, and at nginx log phase, it finally write to a proper log file.

# Synopsis

```lua
# nginx.conf

http {
init_worker_by_lua_block {
local category_log = require('acid.category_log')

local get_category_file = function()
return string.format('front_%d_error.log', ngx.status)
end

local opts = {
get_category_file = get_category_file,
max_repeat_n = 64,
max_entry_n = 256,
}

category_log.wrap_log(opts)
}

server {
...

log_by_lua_block {
local category_log = require('acid.category_log')
category_log.write_log()
}

location = /t {
rewrite_by_lua_block {
...
ngx.log(ngx.ERR, 'test_error_log')
ngx.status = 500
ngx.exit(ngx.HTTP_OK)
}
}
}
}
```

# Methods

## new

**syntax**:
`category_log.wrap_log(opts)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

简单说明下这个函数干啥的吧..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


**arguments**:

- `opts`:
The options table should contain the following fields:

- `get_category_file`: a callback function, have no argument,
should return the log file to which the logs of this request
should write, return nil indicate not write to any log file.

- `max_repeat_n`: set how many logs on same source file and same
line number will be saved.

- `max_entry_n`: set the max total number of logs to save.

- `log_level`: set level of logging, the default is `ngx.INFO`.

**return**:
do not have return value

## write_log

**syntax**:
`category_log:write_log()`

write logs saved in memory to proper log file.

**return**:
do not have return value

# Author

Renzhi (任稚) <zhi.ren@baishancloud.com>

# Copyright and License

The MIT License (MIT)

Copyright (c) 2015 Renzhi (任稚) <zhi.ren@baishancloud.com>
199 changes: 199 additions & 0 deletions lib/acid/category_log.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
local strutil = require('acid.strutil')
local time = require('acid.time')


local to_str = strutil.to_str


local _M = {}


local level_to_str = {
[ngx.DEBUG] = '[debug]',
[ngx.INFO] = '[info]',
[ngx.NOTICE] = '[notice]',
[ngx.WARN] = '[warn]',
[ngx.ERR] = '[error]',
[ngx.CRIT] = '[crit]',
[ngx.ALERT] = '[alert]',
[ngx.EMERG] = '[emerg]',
}


local function write_to_file(log_path, log_ctx)
if log_ctx == nil then
return
end

table.insert(log_ctx.log_entry, to_str(log_ctx.counter) .. '\n')

local data = table.concat(log_ctx.log_entry, '')

local f = io.open(log_path, 'a+')
f:write(data)
f:close()
end


local function feed_log_entry(level, repeat_n, prefix, args)
local log_ctx = ngx.ctx.category_log
local log_entry = log_ctx.log_entry

if #log_entry >= _M.max_entry_n then
return
end

if repeat_n > _M.max_repeat_n then
return
end

local time_str = time.format(ngx.time(), 'nginxerrorlog')
local level_str = level_to_str[level]

local parts = {time_str, level_str, ngx.worker.pid(), prefix}

for _, v in ipairs(args) do
table.insert(parts, tostring(v))
end

local log_line = table.concat(parts, ', ') .. '\n'

table.insert(log_entry, log_line)

return
end


local function _get_request_id()
local request_id = ngx.var.requestid
return request_id
end


local function get_request_id()
local ok, request_id = pcall(_get_request_id)
if not ok then
return
end

return request_id
end


local function log_by_category(level, src_file_name, line_number, prefix, args)
local log_ctx = ngx.ctx.category_log

local counter = log_ctx.counter

local ident = string.format('%s_%d', src_file_name, line_number)
counter[ident] = (counter[ident] or 0) + 1

local repeat_n = counter[ident]

feed_log_entry(level, repeat_n, prefix, args)

return
end


local function _category_log(level, ...)
if ngx.ctx.category_log == nil then
ngx.ctx.category_log = {
-- contain log entries.
log_entry = {},
-- save how many times a log on same source file
-- and same line nubmer have repeated.
counter = {},
}
end

local log_ctx = ngx.ctx.category_log

if log_ctx.request_id == nil then
log_ctx.request_id = get_request_id()
end

-- get info of function at level 4 of the call stack, which is the
-- function called ngx.log
local debug_info = debug.getinfo(4)
local path_parts = strutil.rsplit(debug_info.short_src, '/',
{plain=true, maxsplit=1})
local src_file_name = path_parts[#path_parts]
local line_number = debug_info.currentline
local func_name = debug_info.name

local prefix = string.format('%s %s:%d %s() ', log_ctx.request_id,
src_file_name, line_number, func_name)

_M.origin_log(level, prefix, ...)

if level > _M.log_level then
return
end

log_by_category(level, src_file_name, line_number, prefix, {...})

return
end


local function category_log(level, ...)
local ok, err = pcall(_category_log, level, ...)
if not ok then
_M.origin_log(ngx.ERR, 'failed to do category log: %s' .. err)
end
end


function _M.wrap_log(opts)
if _M.origin_log ~= nil then
ngx.log(ngx.ERR, 'ngx.log can be wrapped only once')
return
end

_M.origin_log = ngx.log

if opts == nil then
opts = {}
end

_M.get_category_file = opts.get_category_file
_M.max_repeat_n = opts.max_repeat_n
_M.max_entry_n = opts.max_entry_n
_M.log_level = opts.log_level or ngx.INFO

ngx.log = category_log
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有个情况是timer里的日志怎么打印出来...我看timer里也可以使用ngx.ctx

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timer里的日志好像不能按状态码区分,timer里的ctx和request中的ctx好像不是同一个,在timer里应该得不到返回状态码,不过所有的日志包括timer中的日志都会正常写到front.error.log,只有指定状态码的日志(不包括timer中的)会被写到另外的文件中,这些日志在两个文件中都有的。


return
end


local function write_log_timer(premature, log_path, log_ctx)
if premature then
_M.origin_log(ngx.WARN, 'write log timer premature')
return
end

write_to_file(log_path, log_ctx)
end


function _M.write_log()
local log_ctx = ngx.ctx.category_log
if log_ctx == nil then
return
end

local log_path = _M.get_category_file()
if log_path == nil then
return
end

local ok, err = ngx.timer.at(0, write_log_timer, log_path, log_ctx)
if not ok then
_M.origin_log(ngx.ERR, 'failed to add write log timer: ' .. err)
end
end


return _M
Loading