You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by me...@apache.org on 2020/12/09 02:08:05 UTC

[apisix] branch master updated: feat: implement new plugin `server-info` (#2926)

This is an automated email from the ASF dual-hosted git repository.

membphis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 7855d9e  feat: implement new plugin `server-info` (#2926)
7855d9e is described below

commit 7855d9eed252f579ad43027912cb1a513ed7ef7e
Author: Alex Zhang <zc...@gmail.com>
AuthorDate: Wed Dec 9 10:07:57 2020 +0800

    feat: implement new plugin `server-info` (#2926)
    
    close #2821
---
 apisix/cli/ngx_tpl.lua           |   1 +
 apisix/core/utils.lua            |  30 ++++++
 apisix/plugins/server-info.lua   | 210 +++++++++++++++++++++++++++++++++++++++
 apisix/timers.lua                |  11 +-
 conf/config-default.yaml         |   4 +
 doc/plugins/server-info.md       | 104 +++++++++++++++++++
 doc/zh-cn/plugins/server-info.md | 108 ++++++++++++++++++++
 t/APISIX.pm                      |   1 +
 t/plugin/server-info.t           | 137 +++++++++++++++++++++++++
 9 files changed, 605 insertions(+), 1 deletion(-)

diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index a962784..ed693d0 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -123,6 +123,7 @@ http {
                       .. [=[$prefix/deps/lib/lua/5.1/?.so;;]=]
                       .. [=[{*lua_cpath*};";
 
+    lua_shared_dict internal_status      10m;
     lua_shared_dict plugin-limit-req     10m;
     lua_shared_dict plugin-limit-count   10m;
     lua_shared_dict prometheus-metrics   10m;
diff --git a/apisix/core/utils.lua b/apisix/core/utils.lua
index 041bd7e..ab9ba0a 100644
--- a/apisix/core/utils.lua
+++ b/apisix/core/utils.lua
@@ -16,6 +16,8 @@
 --
 local core_str = require("apisix.core.string")
 local table    = require("apisix.core.table")
+local log      = require("apisix.core.log")
+local string   = require("apisix.core.string")
 local ngx_re   = require("ngx.re")
 local resolver = require("resty.dns.resolver")
 local ipmatcher= require("resty.ipmatcher")
@@ -29,11 +31,14 @@ local tonumber = tonumber
 local tostring = tostring
 local re_gsub  = ngx.re.gsub
 local type     = type
+local io_popen = io.popen
 local C        = ffi.C
 local ffi_string = ffi.string
 local get_string_buf = base.get_string_buf
 local exiting = ngx.worker.exiting
 local ngx_sleep    = ngx.sleep
+
+local hostname
 local max_sleep_interval = 1
 
 ffi.cdef[[
@@ -206,6 +211,31 @@ function _M.validate_header_value(value)
 end
 
 
+-- only use this method in init/init_worker phase.
+function _M.gethostname()
+    if hostname then
+        return hostname
+    end
+
+    local hd = io_popen("/bin/hostname")
+    local data, err = hd:read("*a")
+    if err == nil then
+        hostname = data
+        if string.has_suffix(hostname, "\r\n") then
+            hostname = sub_str(hostname, 1, -3)
+        elseif string.has_suffix(hostname, "\n") then
+            hostname = sub_str(hostname, 1, -2)
+        end
+
+    else
+        hostname = "unknown"
+        log.error("failed to read output of \"/bin/hostname\": ", err)
+    end
+
+    return hostname
+end
+
+
 local function sleep(sec)
     if sec <= max_sleep_interval then
         return ngx_sleep(sec)
diff --git a/apisix/plugins/server-info.lua b/apisix/plugins/server-info.lua
new file mode 100644
index 0000000..f5dccb8
--- /dev/null
+++ b/apisix/plugins/server-info.lua
@@ -0,0 +1,210 @@
+--
+-- 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.
+--
+local require = require
+local core = require("apisix.core")
+local timers = require("apisix.timers")
+
+local ngx_time = ngx.time
+local ngx_timer_at = ngx.timer.at
+local ngx_worker_id = ngx.worker.id
+local type = type
+
+local boot_time = os.time()
+local plugin_name = "server-info"
+local default_report_interval = 60
+local default_report_ttl = 7200
+
+local schema = {
+    type = "object",
+    additionalProperties = false,
+}
+local attr_schema = {
+    type = "object",
+    properties = {
+        report_interval = {
+            type = "integer",
+            description = "server info reporting interval (unit: second)",
+            default = default_report_interval,
+            minimum = 60,
+            maximum = 3600,
+        },
+        report_ttl = {
+            type = "integer",
+            description = "live time for server info in etcd",
+            default = default_report_ttl,
+            minimum = 3600,
+            maximum = 86400,
+        }
+    }
+}
+
+local internal_status = ngx.shared.internal_status
+if not internal_status then
+    error("lua_shared_dict \"internal_status\" not configured")
+end
+
+
+local _M = {
+    version = 0.1,
+    priority = 990,
+    name = plugin_name,
+    schema = schema,
+}
+
+
+local function uninitialized_server_info()
+    return {
+        etcd_version     = "unknown",
+        hostname         = core.utils.gethostname(),
+        id               = core.id.get(),
+        version          = core.version.VERSION,
+        up_time          = ngx_time() - boot_time,
+        boot_time        = boot_time,
+        last_report_time = -1,
+    }
+end
+
+
+local function get()
+    local data, err = internal_status:get("server_info")
+    if err ~= nil then
+        return nil, err
+    end
+
+    if not data then
+        return uninitialized_server_info()
+    end
+
+    local server_info, err = core.json.decode(data)
+    if not server_info then
+        return nil, err
+    end
+
+    server_info.up_time = ngx_time() - server_info.boot_time
+    return server_info
+end
+
+
+local function report(premature, report_ttl)
+    if premature then
+        return
+    end
+
+    local server_info, err = get()
+    if not server_info then
+        core.log.error("failed to get server_info: ", err)
+        return
+    end
+
+    if server_info.etcd_version == "unknown" then
+        local res, err = core.etcd.server_version()
+        if not res then
+            core.log.error("failed to fetch etcd version: ", err)
+            return
+
+        elseif type(res.body) ~= "table" then
+            core.log.error("failed to fetch etcd version: bad version info")
+            return
+
+        else
+            server_info.etcd_version = res.body.etcdcluster
+        end
+    end
+
+    server_info.last_report_time = ngx_time()
+
+    local data, err = core.json.encode(server_info)
+    if not data then
+        core.log.error("failed to encode server_info: ", err)
+        return
+    end
+
+    local key = "/data_plane/server_info/" .. server_info.id
+    local ok, err = core.etcd.set(key, data, report_ttl)
+    if not ok then
+        core.log.error("failed to report server info to etcd: ", err)
+        return
+    end
+
+    local ok, err = internal_status:set("server_info", data)
+    if not ok then
+        core.log.error("failed to encode and save server info: ", err)
+        return
+    end
+end
+
+
+function _M.check_schema(conf)
+    local ok, err = core.schema.check(schema, conf)
+    if not ok then
+        return false, err
+    end
+
+    return true
+end
+
+
+function _M.init()
+    core.log.info("server info: ", core.json.delay_encode(get()))
+
+    if core.config ~= require("apisix.core.config_etcd") then
+        -- we don't need to report server info if etcd is not in use.
+        return
+    end
+
+    local local_conf = core.config.local_conf()
+    local attr = core.table.try_read_attr(local_conf, "plugin_attr",
+                                          plugin_name)
+    local ok, err = core.schema.check(attr_schema, attr)
+    if not ok then
+        core.log.error("failed to check plugin_attr: ", err)
+        return
+    end
+
+    local report_ttl = attr and attr.report_ttl or default_report_ttl
+    local report_interval = attr and attr.report_interval or default_report_interval
+    local start_at = ngx_time()
+
+    local fn = function()
+        local now = ngx_time()
+        if now - start_at >= report_interval then
+            start_at = now
+            report(nil, report_ttl)
+        end
+    end
+
+    if ngx_worker_id() == 0 then
+        local ok, err = ngx_timer_at(0, report, report_ttl)
+        if not ok then
+            core.log.error("failed to create initial timer to report server info: ", err)
+            return
+        end
+    end
+
+    timers.register_timer("plugin#server-info", fn, true)
+
+    core.log.info("timer created to report server info, interval: ",
+                  report_interval)
+end
+
+
+function _M.destory()
+    timers.unregister_timer("plugin#server-info", true)
+end
+
+
+return _M
diff --git a/apisix/timers.lua b/apisix/timers.lua
index 51b0271..a08f651 100644
--- a/apisix/timers.lua
+++ b/apisix/timers.lua
@@ -21,6 +21,7 @@ local unpack = unpack
 local thread_spawn = ngx.thread.spawn
 local thread_wait = ngx.thread.wait
 
+local check_interval = 0.5
 
 local timers = {}
 
@@ -57,7 +58,10 @@ end
 
 
 function _M.init_worker()
-    local timer, err = core.timer.new("background", background_timer, {check_interval = 0.5})
+    local opts = {
+        check_interval = check_interval,
+    }
+    local timer, err = core.timer.new("background", background_timer, opts)
     if not timer then
         core.log.error("failed to create background timer: ", err)
         return
@@ -85,4 +89,9 @@ function _M.unregister_timer(name, privileged)
 end
 
 
+function _M.check_interval()
+    return check_interval
+end
+
+
 return _M
diff --git a/conf/config-default.yaml b/conf/config-default.yaml
index fed43f4..91c08a3 100644
--- a/conf/config-default.yaml
+++ b/conf/config-default.yaml
@@ -239,6 +239,7 @@ plugins:                          # plugin list (sorted in alphabetical order)
   - uri-blocker
   - wolf-rbac
   - zipkin
+  #- server-info
 
 stream_plugins:
   - mqtt-proxy
@@ -253,3 +254,6 @@ plugin_attr:
     endpoint_addr: http://127.0.0.1:12800
   prometheus:
     export_uri: /apisix/prometheus/metrics
+  server-info:
+    report_interval: 60 # server info report interval (unit: second)
+    report_ttl: 3600    # live time for server info in etcd (unit: second)
diff --git a/doc/plugins/server-info.md b/doc/plugins/server-info.md
new file mode 100644
index 0000000..f9744d3
--- /dev/null
+++ b/doc/plugins/server-info.md
@@ -0,0 +1,104 @@
+<!--
+#
+# 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.
+#
+-->
+
+- [中文](../zh-cn/plugins/server-info.md)
+
+# Summary
+
+- [**Name**](#name)
+- [**Attributes**](#attributes)
+- [API](#api)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Name
+
+`server-info` is a plugin that reports basic server information to etcd periodically.
+
+The meaning of each item in server information is following:
+
+| Name    | Type | Description |
+|---------|------|-------------|
+| up_time | integer | Elapsed time (in seconds) since APISIX instance was launched, value will be reset when you hot updating APISIX but is kept for intact if you just reloading APISIX. |
+| boot_time | integer | Bootstrap time (UNIX timestamp) of the APISIX instance, value will be reset when you hot updating APISIX but is kept for intact if you just reloading APISIX. |
+| last_report_time | integer | Last reporting time (UNIX timestamp). |
+| id | string | APISIX instance id. |
+| etcd_version | string | The etcd cluster version that APISIX is using, value will be `"unknown"` if the network (to etcd) is partitioned. |
+| version | string | APISIX version. |
+| hostname | string | Hostname of the machine/pod that APISIX is deployed. |
+
+## Attributes
+
+None
+
+## API
+
+None
+
+## How to Enable
+
+Just configure `server-info` in the plugin list of the configuration file `conf/config.yaml`.
+
+```
+plugins:                          # plugin list
+  - example-plugin
+  - limit-req
+  - node-status
+  - server-info
+  - jwt-auth
+  - zipkin
+  ......
+```
+
+## How to customize the server info report configurations
+
+We can change the report configurations in the `plugin_attr` section of `conf/config.yaml`.
+
+| Name         | Type   | Default  | Description                                                          |
+| ------------ | ------ | -------- | -------------------------------------------------------------------- |
+| report_interval | integer | 60 | the interval to report server info to etcd (unit: second, maximum: 3600, minimum: 60). |
+| report_ttl | integer | 7200 | the live time for server info in etcd (unit: second, maximum: 86400, minimum: 3600). |
+
+Here is an example, which modifies the `report_interval` to 10 minutes and sets the `report_ttl` to one hour.
+
+```yaml
+plugin_attr:
+  server-info:
+    report_interval: 600,
+    report_ttl: 3600
+```
+
+## Test Plugin
+
+The APISIX Dashboard will collects server info in etcd, after enabling this plugin, you may try to check them through Dashboard.
+
+## Disable Plugin
+
+Remove `server-info` in the plugin list of configure file `conf/config.yaml`.
+
+```
+plugins:                          # plugin list
+  - example-plugin
+  - limit-req
+  - node-status
+  - jwt-auth
+  - zipkin
+  ......
+```
diff --git a/doc/zh-cn/plugins/server-info.md b/doc/zh-cn/plugins/server-info.md
new file mode 100644
index 0000000..9642da5
--- /dev/null
+++ b/doc/zh-cn/plugins/server-info.md
@@ -0,0 +1,108 @@
+<!--
+#
+# 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.
+#
+-->
+
+- [English](../../plugins/server-info.md)
+
+# Summary
+
+- [插件简介](#插件简介)
+- [插件属性](#插件属性)
+- [插件接口](#插件接口)
+- [启用插件](#启用插件)
+- [如何自定义服务信息上报间隔](#如何自定义服务信息上报间隔)
+- [测试插件](#测试插件)
+- [禁用插件](#禁用插件)
+
+## 插件简介
+
+`server-info` 是一款能够定期将服务基本信息上报至 etcd 的插件。
+
+服务信息中每一项的含义如下:
+
+| 名称    | 类型 | 描述 |
+|---------|------|-------------|
+| up_time | integer | APISIX 服务实例当前的运行时间(单位:秒), 如果对 APISIX 进行热更新操作,该值将被重置;普通的 reload 操作不会影响该值。 |
+| boot_time | integer | APISIX 服务实例的启动时间(UNIX 时间戳),如果对 APIISIX 进行热更新操作,该值将被重置;普通的 reload 操作不会影响该值。|
+| last_report_time | integer | 最近一次服务信息上报的时间 (UNIX 时间戳)。|
+| id | string | APISIX 服务实例 id 。|
+| etcd_version | string | etcd 集群的版本信息,如果 APISIX 和 etcd 集群之间存在网络分区,该值将设置为 `"unknown"`。|
+| version | string | APISIX 版本信息。 |
+| hostname | string | APISIX 所部署的机器或 pod 的主机名信息。|
+
+## 插件属性
+
+无
+
+## 插件接口
+
+无
+
+## 启用插件
+
+在配置文件 `apisix/conf/config.yaml` 的插件列表中添加 `server-info`, 即可启用该插件。
+
+```
+plugins:                          # plugin list
+  - example-plugin
+  - limit-req
+  - node-status
+  - server-info
+  - jwt-auth
+  - zipkin
+  ......
+```
+
+## 如何自定义服务信息上报配置
+
+我们可以在 `conf/config.yaml` 文件的 `plugin_attr` 一节中修改上报配置。
+
+| 名称         | 类型   | 默认值  | 描述                                                          |
+| ------------ | ------ | -------- | -------------------------------------------------------------------- |
+| report_interval | integer | 60 | 上报服务信息至 etcd 的间隔(单位:秒,最大值:3600,最小值:60)|
+| report_ttl | integer | 7200 | etcd 中服务信息保存的 TTL(单位:秒,最大值:86400,最小值:3600)|
+
+下面的例子将 `report_interval` 修改成了 10 秒,并将 `report_ttl` 修改成了 1
+小时:
+
+```yaml
+plugin_attr:
+  server-info:
+    report_interval: 10
+    report_ttl: 3600
+```
+
+
+## 测试插件
+
+Apache APISIX Dashboard 会收集上报到 etcd 中的服务信息,在启用这个插件后,你可以通过 APISIX Dashboard 来查看这些数据。
+
+
+## 禁用插件
+
+通过移除配置文件 `apisix/conf/config.yaml` 插件列表中的 `server-info`,即可方便地禁用该插件。
+
+```
+plugins:                          # plugin list
+  - example-plugin
+  - limit-req
+  - node-status
+  - jwt-auth
+  - zipkin
+  ......
+```
diff --git a/t/APISIX.pm b/t/APISIX.pm
index b3b7e60..f9d5e4c 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -239,6 +239,7 @@ _EOC_
     lua_shared_dict plugin-limit-count   10m;
     lua_shared_dict plugin-limit-conn    10m;
     lua_shared_dict prometheus-metrics   10m;
+    lua_shared_dict internal_status      10m;
     lua_shared_dict upstream-healthcheck 32m;
     lua_shared_dict worker-events        10m;
     lua_shared_dict lrucache-lock        10m;
diff --git a/t/plugin/server-info.t b/t/plugin/server-info.t
new file mode 100644
index 0000000..c61a4eb
--- /dev/null
+++ b/t/plugin/server-info.t
@@ -0,0 +1,137 @@
+#
+# 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.
+#
+
+our $SkipReason;
+
+BEGIN {
+    if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+        $SkipReason = "unavailable for the hup tests";
+
+    } else {
+        $ENV{TEST_NGINX_USE_HUP} = 1;
+        undef $ENV{TEST_NGINX_USE_STAP};
+    }
+}
+use Test::Nginx::Socket::Lua $SkipReason ? (skip_all => $SkipReason) : ();
+use t::APISIX 'no_plan';
+
+master_on();
+repeat_each(1);
+no_long_string();
+no_root_location();
+no_shuffle();
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: sanity check
+--- yaml_config
+apisix:
+    id: 123456
+plugins:
+    - server-info
+plugin_attr:
+    server-info:
+        report_interval: 60
+--- config
+location /t {
+    content_by_lua_block {
+        ngx.sleep(2)
+        local json_decode = require("cjson.safe").decode
+        local core = require("apisix.core")
+        local key = "/data_plane/server_info/" .. core.id.get()
+        local res, err = core.etcd.get(key)
+        if err ~= nil then
+            ngx.status = 500
+            ngx.say(err)
+            return
+        end
+
+        local keys = {}
+        local body = json_decode(res.body.node.value)
+        for k in pairs(body) do
+            keys[#keys + 1] = k
+        end
+
+        table.sort(keys)
+        for i = 1, #keys do
+            ngx.say(keys[i], ": ", body[keys[i]])
+        end
+    }
+}
+--- request
+GET /t
+--- response_body eval
+qr{^boot_time: \d+
+etcd_version: [\d\.]+
+hostname: [a-zA-Z\-0-9\.]+
+id: [a-zA-Z\-0-9]+
+last_report_time: \d+
+up_time: \d+
+version: [\d\.]+
+$}
+--- no_error_log
+[error]
+--- error_log
+timer created to report server info, interval: 60
+
+
+
+=== TEST 2: verify the data integrity after reloading
+--- yaml_config
+apisix:
+    id: 123456
+plugins:
+    - server-info
+plugin_attr:
+    server-info:
+        report_interval: 60
+--- config
+location /t {
+    content_by_lua_block {
+        local json_decode = require("cjson.safe").decode
+        local core = require("apisix.core")
+        local key = "/data_plane/server_info/" .. core.id.get()
+        local res, err = core.etcd.get(key)
+        if err ~= nil then
+            ngx.status = 500
+            ngx.say(err)
+            return
+        end
+
+        local keys = {}
+        local body = json_decode(res.body.node.value)
+        for k in pairs(body) do
+            keys[#keys + 1] = k
+        end
+
+        if body.up_time >= 2 then
+            ngx.say("integral")
+        else
+            ngx.say("reset")
+        end
+    }
+}
+--- request
+GET /t
+--- response_body
+integral
+--- no_error_log
+[error]
+--- error_log
+timer created to report server info, interval: 60