Skip to content
New issue

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

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

Already on GitHub? # to your account

feat(http-logger): support for specified the log formats via admin API #2294

Merged
merged 12 commits into from
Sep 24, 2020
16 changes: 16 additions & 0 deletions apisix/plugin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ local type = type
local local_plugins = core.table.new(32, 0)
local ngx = ngx
local tostring = tostring
local error = error
local local_plugins_hash = core.table.new(0, 32)
local stream_local_plugins = core.table.new(32, 0)
local stream_local_plugins_hash = core.table.new(0, 32)
Expand Down Expand Up @@ -391,6 +392,21 @@ end

function _M.init_worker()
_M.load()

local plugin_metadatas, err = core.config.new("/plugin_metadata",
{automatic = true}
)
if not plugin_metadatas then
error("failed to create etcd instance for fetching /plugin_metadatas : "
.. err)
end

_M.plugin_metadatas = plugin_metadatas
end


function _M.plugin_metadata(name)
return _M.plugin_metadatas:get(name)
end


Expand Down
74 changes: 65 additions & 9 deletions apisix/plugins/http-logger.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,26 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
local core = require("apisix.core")
local log_util = require("apisix.utils.log-util")

local batch_processor = require("apisix.utils.batch-processor")
local plugin_name = "http-logger"
local ngx = ngx
local log_util = require("apisix.utils.log-util")
local core = require("apisix.core")
local http = require("resty.http")
local url = require("net.url")
local plugin = require("apisix.plugin")
local ngx = ngx
local tostring = tostring
local http = require "resty.http"
local url = require "net.url"
local buffers = {}
local pairs = pairs
local ipairs = ipairs


local plugin_name = "http-logger"
local buffers = {}
local lru_log_format = core.lrucache.new({
ttl = 300, count = 512
})


local schema = {
type = "object",
properties = {
Expand All @@ -45,11 +54,24 @@ local schema = {
}


local metadata_schema = {
type = "object",
properties = {
log_format = {
type = "object",
default = {},
},
},
additionalProperties = false,
}


local _M = {
version = 0.1,
priority = 410,
name = plugin_name,
schema = schema,
metadata_schema = metadata_schema,
}


Expand Down Expand Up @@ -117,8 +139,42 @@ local function send_http_data(conf, log_message)
end


function _M.log(conf)
local entry = log_util.get_full_log(ngx, conf)
local function gen_log_format(metadata)
local log_format = {}
for k, var_name in pairs(metadata.value.log_format) do
if var_name:sub(1, 1) == "$" then
log_format[k] = {true, var_name:sub(2)}
else
log_format[k] = {false, var_name}
end
end
core.log.info("log_format: ", core.json.delay_encode(log_format))
return log_format
end


function _M.log(conf, ctx)
local metadata = plugin.plugin_metadata(plugin_name)
core.log.info("metadata: ", core.json.delay_encode(metadata))

local entry
local log_format = lru_log_format(metadata, nil, gen_log_format, metadata)
if log_format then
entry = core.table.new(0, core.table.nkeys(log_format))
for k, var_attr in pairs(log_format) do
if var_attr[1] then
entry[k] = ctx.var[var_attr[2]]
else
entry[k] = var_attr[2]
end
end
local matched_route = ctx.matched_route and ctx.matched_route.value

entry.service_id = matched_route.service_id
entry.route_id = matched_route.id
else
entry = log_util.get_full_log(ngx, conf)
end

if not entry.route_id then
core.log.error("failed to obtain the route id for http logger")
Expand Down
2 changes: 1 addition & 1 deletion bin/apisix
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ local function init_etcd(show_output)
for _, dir_name in ipairs({"/routes", "/upstreams", "/services",
"/plugins", "/consumers", "/node_status",
"/ssl", "/global_rules", "/stream_routes",
"/proto"}) do
"/proto", "/plugin_metadata"}) do
Copy link
Member

Choose a reason for hiding this comment

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

What is the difference between metadata and the properties of plugin itself? I didn't see why we need it.

Copy link
Member Author

Choose a reason for hiding this comment

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

get plugin schema:
/apisix/admin/plugins/{plugin-name}, eg: /apisix/admin/plugins/limit-req

get plugin metadata:
/apisix/admin/plugin_metadata/{plugin-name}, eg: /apisix/admin/plugin_metadata/example-plugin

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

why we need this PR?

Copy link
Member

@juzhiyuan juzhiyuan Sep 24, 2020

Choose a reason for hiding this comment

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

I would recommend using /plugins/{name}/metadata in a RESTful way if this PR is needed..

Copy link
Contributor

Choose a reason for hiding this comment

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

I am also curious about what scenarios need metadata instead of plugin's option.

Copy link
Member Author

Choose a reason for hiding this comment

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

this PR has been merged. we can talk in here: #2307 (comment)

local key = (etcd_conf.prefix or "") .. dir_name .. "/"

local base64_encode = require("base64").encode
Expand Down
28 changes: 27 additions & 1 deletion doc/zh-cn/plugins/http-logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

## 如何开启

1. 这是有关如何为特定路由启用 http-logger 插件的示例。
这是有关如何为特定路由启用 http-logger 插件的示例。

```shell
curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
Expand Down Expand Up @@ -82,6 +82,32 @@ HTTP/1.1 200 OK
hello, world
```

## 插件元数据设置

| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
| ---------------- | ------- | ------ | ------------- | ------- | ------------------------------------------------ |
| log_format | object | 可选 | | | 以 Hash 对象方式声明日志格式。对 value 部分,仅支持字符串。如果是以`$`开头,则表明是要获取 [Nginx 内置变量](http://nginx.org/en/docs/varindex.html)。特别的,该设置是全局生效的,意味着指定 log_format 后,将对所有绑定 http-logger 的 Route 或 Service 生效。 |

### 设置日志格式示例

```shell
curl http://127.0.0.1:9080/apisix/admin/plugin_metadata/http-logger -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"log_format": {
"host": "$host",
"@timestamp": "$time_iso8601",
"client_ip": "$remote_addr"
}
}'
```

在日志收集处,将得到类似下面的日志:

```shell
{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"}
{"host":"localhost","@timestamp":"2020-09-23T19:05:05-04:00","client_ip":"127.0.0.1","route_id":"1"}
```

## 禁用插件

在插件配置中删除相应的 json 配置以禁用 http-logger。APISIX 插件是热重载的,因此无需重新启动 APISIX:
Expand Down
147 changes: 147 additions & 0 deletions t/plugin/http-logger-log-format.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
use t::APISIX 'no_plan';

log_level('info');
repeat_each(1);
no_long_string();
no_root_location();

run_tests;

__DATA__

=== TEST 1: add plugin metadata
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/http-logger',
ngx.HTTP_PUT,
[[{
"log_format": {
"host": "$host",
"@timestamp": "$time_iso8601",
"client_ip": "$remote_addr"
}
}]],
[[{
"node": {
"value": {
"log_format": {
"host": "$host",
"@timestamp": "$time_iso8601",
"client_ip": "$remote_addr"
}
}
},
"action": "set"
}]]
)

ngx.status = code
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]



=== TEST 2: sanity, batch_max_size=1
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/routes/1',
ngx.HTTP_PUT,
[[{
"plugins": {
"http-logger": {
"uri": "http://127.0.0.1:1980/log",
"batch_max_size": 1,
"max_retry_count": 1,
"retry_delay": 2,
"buffer_duration": 2,
"inactive_timeout": 2,
"concat_method": "new_line"
}
},
"upstream": {
"nodes": {
"127.0.0.1:1982": 1
},
"type": "roundrobin"
},
"uri": "/hello"
}]]
)

if code >= 300 then
ngx.status = code
end
ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]



=== TEST 3: hit route and report http logger
--- request
GET /hello
--- response_body
hello world
--- wait: 0.5
--- no_error_log
[error]
--- error_log
request log: {"host":"localhost"



=== TEST 4: remove plugin metadata
--- config
location /t {
content_by_lua_block {
local t = require("lib.test_admin").test
local code, body = t('/apisix/admin/plugin_metadata/http-logger',
ngx.HTTP_DELETE
)

if code >= 300 then
ngx.status = code
end

ngx.say(body)
}
}
--- request
GET /t
--- response_body
passed
--- no_error_log
[error]