diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 7fb57a2758c7..d7b9e0a7635b 100644 --- a/apisix/cli/config.lua +++ b/apisix/cli/config.lua @@ -236,6 +236,7 @@ local _M = { "limit-count", "limit-req", "gzip", + "server-info", "traffic-split", "redirect", "response-rewrite", @@ -321,6 +322,9 @@ local _M = { port = 9091 } }, + ["server-info"] = { + report_ttl = 60 + }, ["dubbo-proxy"] = { upstream_multiplex_count = 32 }, diff --git a/apisix/plugins/server-info.lua b/apisix/plugins/server-info.lua new file mode 100644 index 000000000000..9ced84cd4042 --- /dev/null +++ b/apisix/plugins/server-info.lua @@ -0,0 +1,315 @@ +-- +-- 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 plugin = require("apisix.plugin") + +local ngx_time = ngx.time +local ngx_timer_at = ngx.timer.at +local ngx_worker_id = ngx.worker.id +local type = type + +local load_time = os.time() +local plugin_name = "server-info" +local default_report_ttl = 60 +local lease_id + +local schema = { + type = "object", +} +local attr_schema = { + type = "object", + properties = { + report_ttl = { + type = "integer", + description = "live time for server info in etcd", + default = default_report_ttl, + minimum = 3, + 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, + scope = "global", +} + + +local function get_boot_time() + local time, err = internal_status:get("server_info:boot_time") + if err ~= nil then + core.log.error("failed to get boot_time from shdict: ", err) + return load_time + end + + if time ~= nil then + return time + end + + local _, err = internal_status:set("server_info:boot_time", load_time) + if err ~= nil then + core.log.error("failed to save boot_time to shdict: ", err) + end + + return load_time +end + + +local function uninitialized_server_info() + local boot_time = get_boot_time() + return { + etcd_version = "unknown", + hostname = core.utils.gethostname(), + id = core.id.get(), + version = core.version.VERSION, + boot_time = boot_time, + } +end + + +local function get() + local data, err = internal_status:get("server_info") + if err ~= nil then + core.log.error("get error: ", err) + 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 + core.log.error("failed to decode server_info: ", err) + return nil, err + end + + return server_info +end + + +local function get_server_info() + local info, err = get() + if not info then + core.log.error("failed to get server_info: ", err) + return 500 + end + + return 200, info +end + + +local function set(key, value, ttl) + local res_new, err = core.etcd.set(key, value, ttl) + if not res_new then + core.log.error("failed to set server_info: ", err) + return nil, err + end + + if not res_new.body.lease_id then + core.log.error("failed to get lease_id: ", err) + return nil, err + end + + lease_id = res_new.body.lease_id + + -- set or update lease_id + local ok, err = internal_status:set("lease_id", lease_id) + if not ok then + core.log.error("failed to set lease_id to shdict: ", err) + return nil, err + end + + return true +end + + +local function report(premature, report_ttl) + if premature then + return + end + + -- get apisix node info + 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 + if res.body.etcdcluster == "" then + server_info.etcd_version = res.body.etcdserver + else + server_info.etcd_version = res.body.etcdcluster + end + end + end + + -- get inside etcd data, if not exist, create it + local key = "/data_plane/server_info/" .. server_info.id + local res, err = core.etcd.get(key) + if not res or (res.status ~= 200 and res.status ~= 404) then + core.log.error("failed to get server_info from etcd: ", err) + return + end + + if not res.body.node then + local ok, err = set(key, server_info, report_ttl) + if not ok then + core.log.error("failed to set server_info to etcd: ", err) + return + end + + return + end + + local ok = core.table.deep_eq(server_info, res.body.node.value) + -- not equal, update it + if not ok then + local ok, err = set(key, server_info, report_ttl) + if not ok then + core.log.error("failed to set server_info to etcd: ", err) + return + end + + return + end + + -- get lease_id from ngx dict + lease_id, err = internal_status:get("lease_id") + if not lease_id then + core.log.error("failed to get lease_id from shdict: ", err) + return + end + + -- call keepalive + local res, err = core.etcd.keepalive(lease_id) + if not res then + core.log.error("send heartbeat failed: ", err) + return + end + + local data, err = core.json.encode(server_info) + if not data then + core.log.error("failed to encode server_info: ", 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.control_api() + return { + { + methods = {"GET"}, + uris ={"/v1/server_info"}, + handler = get_server_info, + } + } +end + + +function _M.init() + 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 deployment_role = core.table.try_read_attr( + local_conf, "deployment", "role") + if deployment_role == "data_plane" then + -- data_plane should not write to etcd + return + end + + local attr = plugin.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 start_at = ngx_time() + + local fn = function() + local now = ngx_time() + -- If ttl remaining time is less than half, then flush the ttl + if now - start_at >= (report_ttl / 2) 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 update the server info ttl, current ttl: ", report_ttl) +end + + +function _M.destroy() + timers.unregister_timer("plugin#server-info", true) +end + + +return _M diff --git a/conf/config.yaml.example b/conf/config.yaml.example index 283d68b7db6b..176f0f510bd2 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -497,6 +497,7 @@ plugins: # plugin list (sorted by priority) - ai-proxy-multi # priority: 998 #- brotli # priority: 996 - gzip # priority: 995 + - server-info # priority: 990 - traffic-split # priority: 966 - redirect # priority: 900 - response-rewrite # priority: 899 diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 34bef0bf8f92..bdbdf778596d 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -91,6 +91,7 @@ "plugins/gzip", "plugins/brotli", "plugins/real-ip", + "plugins/server-info", "plugins/ext-plugin-pre-req", "plugins/ext-plugin-post-req", "plugins/ext-plugin-post-resp", diff --git a/docs/en/latest/plugins/server-info.md b/docs/en/latest/plugins/server-info.md new file mode 100644 index 000000000000..9e9005639d22 --- /dev/null +++ b/docs/en/latest/plugins/server-info.md @@ -0,0 +1,112 @@ +--- +title: server-info +keywords: + - Apache APISIX + - API Gateway + - Plugin + - Server info + - server-info +description: This document contains information about the Apache APISIX server-info Plugin. +--- + + + +## Description + +The `server-info` Plugin periodically reports basic server information to etcd. + +The information reported by the Plugin is explained below: + +| Name | Type | Description | +|--------------|---------|------------------------------------------------------------------------------------------------------------------------| +| boot_time | integer | Bootstrap time (UNIX timestamp) of the APISIX instance. Resets when hot updating but not when APISIX is just reloaded. | +| id | string | APISIX instance ID. | +| etcd_version | string | Version of the etcd cluster used by APISIX. Will be `unknown` if the network to etcd is partitioned. | +| version | string | Version of APISIX instance. | +| hostname | string | Hostname of the machine/pod APISIX is deployed to. | + +## Attributes + +None. + +## API + +This Plugin exposes the endpoint `/v1/server_info` to the [Control API](../control-api.md) + +## Enable Plugin + +Add `server-info` to the Plugin list in your configuration file (`conf/config.yaml`): + +```yaml title="conf/config.yaml" +plugins: + - ... + - server-info +``` + +## Customizing server info report configuration + +We can change the report configurations in the `plugin_attr` section of `conf/config.yaml`. + +The following configurations of the server info report can be customized: + +| Name | Type | Default | Description | +| ------------ | ------ | -------- | -------------------------------------------------------------------- | +| report_ttl | integer | 36 | Time in seconds after which the report is deleted from etcd (maximum: 86400, minimum: 3). | + +To customize, you can modify the `plugin_attr` attribute in your configuration file (`conf/config.yaml`): + +```yaml title="conf/config.yaml" +plugin_attr: + server-info: + report_ttl: 60 +``` + +## Example usage + +After you enable the Plugin as mentioned above, you can access the server info report through the Control API: + +```shell +curl http://127.0.0.1:9090/v1/server_info -s | jq . +``` + +```json +{ + "etcd_version": "3.5.0", + "id": "b7ce1c5c-b1aa-4df7-888a-cbe403f3e948", + "hostname": "fedora32", + "version": "2.1", + "boot_time": 1608522102 +} +``` + +:::tip + +You can also view the server info report through the [APISIX Dashboard](/docs/dashboard/USER_GUIDE). + +::: + +## Delete Plugin + +To remove the Plugin, you can remove `server-info` from the list of Plugins in your configuration file: + +```yaml title="conf/config.yaml" +plugins: + - ... +``` diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 8ae176f42b13..b463a59fd6eb 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -65,6 +65,7 @@ "plugins/gzip", "plugins/brotli", "plugins/real-ip", + "plugins/server-info", "plugins/ext-plugin-pre-req", "plugins/ext-plugin-post-req", "plugins/ext-plugin-post-resp", diff --git a/docs/zh/latest/plugins/server-info.md b/docs/zh/latest/plugins/server-info.md new file mode 100644 index 000000000000..9fa5aac8468b --- /dev/null +++ b/docs/zh/latest/plugins/server-info.md @@ -0,0 +1,112 @@ +--- +title: server-info +keywords: + - Apache APISIX + - API 网关 + - Plugin + - Server info + - server-info +description: 本文介绍了关于 Apache APISIX `server-info` 插件的基本信息及使用方法。 +--- + + + +## 描述 + +`server-info` 插件可以定期将服务基本信息上报至 etcd。 + +服务信息中每一项的含义如下: + +| 名称 | 类型 | 描述 | +| ---------------- | ------- | --------------------------------------------------------------------------------------------------------------------- | +| boot_time | integer | APISIX 服务实例的启动时间(UNIX 时间戳),如果对 APISIX 进行热更新操作,该值将被重置。普通的 reload 操作不会影响该值。 | +| id | string | APISIX 服务实例 id。 | +| etcd_version | string | etcd 集群的版本信息,如果 APISIX 和 etcd 集群之间存在网络分区,该值将设置为 `"unknown"`。 | +| version | string | APISIX 版本信息。 | +| hostname | string | 部署 APISIX 的主机或 Pod 的主机名信息。 | + +## 属性 + +无。 + +## 插件接口 + +该插件在 [Control API](../control-api.md) 下暴露了一个 API 接口 `/v1/server_info`。 + +## 启用插件 + +该插件默认是禁用状态,你可以在配置文件(`./conf/config.yaml`)中添加如下配置启用 `server-info` 插件。 + +```yaml title="conf/config.yaml" +plugins: # plugin list + - ... + - server-info +``` + +## 自定义服务信息上报配置 + +我们可以在 `./conf/config.yaml` 文件的 `plugin_attr` 部分修改上报配置。 + +下表是可以自定义配置的参数: + +| 名称 | 类型 | 默认值 | 描述 | +| --------------- | ------- | ------ | --------------------------------------------------------------- | +| report_ttl | integer | 36 | etcd 中服务信息保存的 TTL(单位:秒,最大值:86400,最小值:3)。| + +以下是示例是通过修改配置文件(`conf/config.yaml`)中的 `plugin_attr` 部分将 `report_ttl` 设置为 1 分钟: + +```yaml title="conf/config.yaml" +plugin_attr: + server-info: + report_ttl: 60 +``` + +## 测试插件 + +在启用 `server-info` 插件后,可以通过插件的 Control API 来访问到这些数据: + +```shell +curl http://127.0.0.1:9090/v1/server_info -s | jq . +``` + +```JSON +{ + "etcd_version": "3.5.0", + "id": "b7ce1c5c-b1aa-4df7-888a-cbe403f3e948", + "hostname": "fedora32", + "version": "2.1", + "boot_time": 1608522102 +} +``` + +:::tip + +你可以通过 [APISIX Dashboard](/docs/dashboard/USER_GUIDE) 查看服务信息报告。 + +::: + +## 删除插件 + +如果你想禁用插件,可以将 `server-info` 从配置文件中的插件列表删除,重新加载 APISIX 后即可生效。 + +```yaml title="conf/config.yaml" +plugins: # plugin list + - ... +``` diff --git a/t/admin/plugins.t b/t/admin/plugins.t index da322610447a..bbacad6ab3a1 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -111,6 +111,7 @@ limit-conn limit-count limit-req gzip +server-info traffic-split redirect response-rewrite @@ -395,6 +396,7 @@ qr/\{"encrypt_fields":\["password"\],"properties":\{"password":\{"type":"string" plugins: - batch-requests - error-log-logger + - server-info - example-plugin - node-status --- config @@ -424,7 +426,7 @@ plugins: } } --- response_body -{"batch-requests":"global","error-log-logger":"global","node-status":"global"} +{"batch-requests":"global","error-log-logger":"global","node-status":"global","server-info":"global"} diff --git a/t/control/schema.t b/t/control/schema.t index 9c43604e4efd..349193831a9d 100644 --- a/t/control/schema.t +++ b/t/control/schema.t @@ -115,6 +115,7 @@ passed plugins: - batch-requests - error-log-logger + - server-info - example-plugin - node-status --- config @@ -145,4 +146,4 @@ plugins: } } --- response_body -{"batch-requests":"global","error-log-logger":"global","node-status":"global"} +{"batch-requests":"global","error-log-logger":"global","node-status":"global","server-info":"global"} diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index a41dacb77e03..f2bdbb2c9a75 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -70,6 +70,7 @@ loaded plugin and sort by priority: 1003 name: limit-conn loaded plugin and sort by priority: 1002 name: limit-count loaded plugin and sort by priority: 1001 name: limit-req loaded plugin and sort by priority: 995 name: gzip +loaded plugin and sort by priority: 990 name: server-info loaded plugin and sort by priority: 966 name: traffic-split loaded plugin and sort by priority: 900 name: redirect loaded plugin and sort by priority: 899 name: response-rewrite diff --git a/t/plugin/server-info.t b/t/plugin/server-info.t new file mode 100644 index 000000000000..13c44a92f759 --- /dev/null +++ b/t/plugin/server-info.t @@ -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. +# + +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'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + $block; +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity check +--- yaml_config +apisix: + id: 123456 +plugins: + - server-info +plugin_attr: + server-info: + report_ttl: 60 +--- config +location /t { + content_by_lua_block { + ngx.sleep(2) + 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 value = res.body.node.value + local json = require("toolkit.json") + ngx.say(json.encode(value)) + } +} +--- response_body eval +qr/^{"boot_time":\d+,"etcd_version":"[\d\.]+","hostname":"[a-zA-Z\-0-9\.]+","id":[a-zA-Z\-0-9]+,"version":"[\d\.]+"}$/ + + + +=== TEST 2: get server_info from plugin control API +--- yaml_config +apisix: + id: 123456 +plugins: + - server-info +--- config +location /t { + content_by_lua_block { + local json = require("toolkit.json") + local t = require("lib.test_admin").test + local code, _, body = t("/v1/server_info") + if code >= 300 then + ngx.status = code + end + + body = json.decode(body) + ngx.say(json.encode(body)) + } +} +--- response_body eval +qr/^{"boot_time":\d+,"etcd_version":"[\d\.]+","hostname":"[a-zA-Z\-0-9\.]+","id":[a-zA-Z\-0-9]+,"version":"[\d\.]+"}$/