--
-- 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 plugin = require("apisix.plugin")
local get_routes = require("apisix.router").http_routes
local get_services = require("apisix.http.service").services
local upstream_mod = require("apisix.upstream")
local get_upstreams = upstream_mod.upstreams
local collectgarbage = collectgarbage
local ipairs = ipairs
local pcall = pcall
local str_format = string.format
local ngx = ngx
local ngx_var = ngx.var
local events = require("apisix.events")
local _M = {}
_M.RELOAD_EVENT = 'control-api-plugin-reload'
function _M.schema()
local http_plugins, stream_plugins = plugin.get_all({
version = true,
priority = true,
schema = true,
metadata_schema = true,
consumer_schema = true,
type = true,
scope = true,
})
local schema = {
main = {
consumer = core.schema.consumer,
consumer_group = core.schema.consumer_group,
global_rule = core.schema.global_rule,
plugin_config = core.schema.plugin_config,
plugins = core.schema.plugins,
proto = core.schema.proto,
route = core.schema.route,
service = core.schema.service,
ssl = core.schema.ssl,
stream_route = core.schema.stream_route,
upstream = core.schema.upstream,
upstream_hash_header_schema = core.schema.upstream_hash_header_schema,
upstream_hash_vars_schema = core.schema.upstream_hash_vars_schema,
},
plugins = http_plugins,
stream_plugins = stream_plugins,
}
return 200, schema
end
local healthcheck
local function extra_checker_info(value)
if not healthcheck then
healthcheck = require("resty.healthcheck")
end
local name = upstream_mod.get_healthchecker_name(value)
local nodes, err = healthcheck.get_target_list(name, "upstream-healthcheck")
if err then
core.log.error("healthcheck.get_target_list failed: ", err)
end
return {
name = value.key,
nodes = nodes,
}
end
local function get_checker_type(checks)
if checks.active and checks.active.type then
return checks.active.type
elseif checks.passive and checks.passive.type then
return checks.passive.type
end
end
local function iter_and_add_healthcheck_info(infos, values)
if not values then
return
end
for _, value in core.config_util.iterate_values(values) do
local checks = value.value.checks or (value.value.upstream and value.value.upstream.checks)
if checks then
local info = extra_checker_info(value)
info.type = get_checker_type(checks)
core.table.insert(infos, info)
end
end
end
local HTML_TEMPLATE = [[
APISIX upstream check status
APISIX upstream check status
Index |
Upstream |
Check type |
Host |
Status |
Success counts |
TCP Failures |
HTTP Failures |
TIMEOUT Failures |
{% local i = 0 %}
{% for _, stat in ipairs(stats) do %}
{% for _, node in ipairs(stat.nodes) do %}
{% i = i + 1 %}
{% if node.status == "healthy" or node.status == "mostly_healthy" then %}
{% else %}
{% end %}
{* i *} |
{* stat.name *} |
{* stat.type *} |
{* node.ip .. ":" .. node.port *} |
{* node.status *} |
{* node.counter.success *} |
{* node.counter.tcp_failure *} |
{* node.counter.http_failure *} |
{* node.counter.timeout_failure *} |
{% end %}
{% end %}
]]
local html_render
local function try_render_html(data)
if not html_render then
local template = require("resty.template")
html_render = template.compile(HTML_TEMPLATE)
end
local accept = ngx_var.http_accept
if accept and accept:find("text/html") then
local ok, out = pcall(html_render, data)
if not ok then
local err = str_format("HTML template rendering: %s", out)
core.log.error(err)
return nil, err
end
return out
end
end
local function _get_health_checkers()
local infos = {}
local routes = get_routes()
iter_and_add_healthcheck_info(infos, routes)
local services = get_services()
iter_and_add_healthcheck_info(infos, services)
local upstreams = get_upstreams()
iter_and_add_healthcheck_info(infos, upstreams)
return infos
end
function _M.get_health_checkers()
local infos = _get_health_checkers()
local out, err = try_render_html({stats=infos})
if out then
core.response.set_header("Content-Type", "text/html")
return 200, out
end
if err then
return 503, {error_msg = err}
end
return 200, infos
end
local function iter_and_find_healthcheck_info(values, src_type, src_id)
if not values then
return nil, str_format("%s[%s] not found", src_type, src_id)
end
for _, value in core.config_util.iterate_values(values) do
if value.value.id == src_id then
local checks = value.value.checks or
(value.value.upstream and value.value.upstream.checks)
if not checks then
return nil, str_format("no checker for %s[%s]", src_type, src_id)
end
local info = extra_checker_info(value)
info.type = get_checker_type(checks)
return info
end
end
return nil, str_format("%s[%s] not found", src_type, src_id)
end
function _M.get_health_checker()
local uri_segs = core.utils.split_uri(ngx_var.uri)
core.log.info("healthcheck uri: ", core.json.delay_encode(uri_segs))
local src_type, src_id = uri_segs[4], uri_segs[5]
if not src_id then
return 404, {error_msg = str_format("missing src id for src type %s", src_type)}
end
local values
if src_type == "routes" then
values = get_routes()
elseif src_type == "services" then
values = get_services()
elseif src_type == "upstreams" then
values = get_upstreams()
else
return 400, {error_msg = str_format("invalid src type %s", src_type)}
end
local info, err = iter_and_find_healthcheck_info(values, src_type, src_id)
if not info then
return 404, {error_msg = err}
end
local out, err = try_render_html({stats={info}})
if out then
core.response.set_header("Content-Type", "text/html")
return 200, out
end
if err then
return 503, {error_msg = err}
end
return 200, info
end
local function iter_add_get_routes_info(values, route_id)
local infos = {}
for _, route in core.config_util.iterate_values(values) do
local new_route = core.table.deepcopy(route)
if new_route.value.upstream and new_route.value.upstream.parent then
new_route.value.upstream.parent = nil
end
-- remove healthcheck info
new_route.checker = nil
new_route.checker_idx = nil
new_route.checker_upstream = nil
new_route.clean_handlers = nil
core.table.insert(infos, new_route)
-- check the route id
if route_id and route.value.id == route_id then
return new_route
end
end
if not route_id then
return infos
end
return nil
end
function _M.dump_all_routes_info()
local routes = get_routes()
local infos = iter_add_get_routes_info(routes, nil)
return 200, infos
end
function _M.dump_route_info()
local routes = get_routes()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local route_id = uri_segs[4]
local route = iter_add_get_routes_info(routes, route_id)
if not route then
return 404, {error_msg = str_format("route[%s] not found", route_id)}
end
return 200, route
end
local function iter_add_get_upstream_info(values, upstream_id)
if not values then
return nil
end
local infos = {}
for _, upstream in core.config_util.iterate_values(values) do
local new_upstream = core.table.deepcopy(upstream)
core.table.insert(infos, new_upstream)
if new_upstream.value and new_upstream.value.parent then
new_upstream.value.parent = nil
end
-- check the upstream id
if upstream_id and upstream.value.id == upstream_id then
return new_upstream
end
end
if not upstream_id then
return infos
end
return nil
end
function _M.dump_all_upstreams_info()
local upstreams = get_upstreams()
local infos = iter_add_get_upstream_info(upstreams, nil)
return 200, infos
end
function _M.dump_upstream_info()
local upstreams = get_upstreams()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local upstream_id = uri_segs[4]
local upstream = iter_add_get_upstream_info(upstreams, upstream_id)
if not upstream then
return 404, {error_msg = str_format("upstream[%s] not found", upstream_id)}
end
return 200, upstream
end
function _M.trigger_gc()
-- TODO: find a way to trigger GC in the stream subsystem
collectgarbage()
return 200
end
local function iter_add_get_services_info(values, svc_id)
local infos = {}
for _, svc in core.config_util.iterate_values(values) do
local new_svc = core.table.deepcopy(svc)
if new_svc.value.upstream and new_svc.value.upstream.parent then
new_svc.value.upstream.parent = nil
end
-- remove healthcheck info
new_svc.checker = nil
new_svc.checker_idx = nil
new_svc.checker_upstream = nil
new_svc.clean_handlers = nil
core.table.insert(infos, new_svc)
-- check the service id
if svc_id and svc.value.id == svc_id then
return new_svc
end
end
if not svc_id then
return infos
end
return nil
end
function _M.dump_all_services_info()
local services = get_services()
local infos = iter_add_get_services_info(services, nil)
return 200, infos
end
function _M.dump_service_info()
local services = get_services()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local svc_id = uri_segs[4]
local info = iter_add_get_services_info(services, svc_id)
if not info then
return 404, {error_msg = str_format("service[%s] not found", svc_id)}
end
return 200, info
end
function _M.dump_all_plugin_metadata()
local names = core.config.local_conf().plugins
local metadatas = core.table.new(0, #names)
for _, name in ipairs(names) do
local metadata = plugin.plugin_metadata(name)
if metadata then
core.table.insert(metadatas, metadata.value)
end
end
return 200, metadatas
end
function _M.dump_plugin_metadata()
local uri_segs = core.utils.split_uri(ngx_var.uri)
local name = uri_segs[4]
local metadata = plugin.plugin_metadata(name)
if not metadata then
return 404, {error_msg = str_format("plugin metadata[%s] not found", name)}
end
return 200, metadata.value
end
function _M.post_reload_plugins()
local success, err = events:post(_M.RELOAD_EVENT, ngx.req.get_method(), ngx.time())
if not success then
core.response.exit(503, err)
end
core.response.exit(200, "done")
end
return {
-- /v1/schema
{
methods = {"GET"},
uris = {"/schema"},
handler = _M.schema,
},
-- /v1/healthcheck
{
methods = {"GET"},
uris = {"/healthcheck"},
handler = _M.get_health_checkers,
},
-- /v1/healthcheck/{src_type}/{src_id}
{
methods = {"GET"},
uris = {"/healthcheck/*"},
handler = _M.get_health_checker,
},
-- /v1/gc
{
methods = {"POST"},
uris = {"/gc"},
handler = _M.trigger_gc,
},
-- /v1/routes
{
methods = {"GET"},
uris = {"/routes"},
handler = _M.dump_all_routes_info,
},
-- /v1/route/*
{
methods = {"GET"},
uris = {"/route/*"},
handler = _M.dump_route_info,
},
-- /v1/services
{
methods = {"GET"},
uris = {"/services"},
handler = _M.dump_all_services_info
},
-- /v1/service/*
{
methods = {"GET"},
uris = {"/service/*"},
handler = _M.dump_service_info
},
-- /v1/upstreams
{
methods = {"GET"},
uris = {"/upstreams"},
handler = _M.dump_all_upstreams_info,
},
-- /v1/upstream/*
{
methods = {"GET"},
uris = {"/upstream/*"},
handler = _M.dump_upstream_info,
},
-- /v1/plugin_metadatas
{
methods = {"GET"},
uris = {"/plugin_metadatas"},
handler = _M.dump_all_plugin_metadata,
},
-- /v1/plugin_metadata/*
{
methods = {"GET"},
uris = {"/plugin_metadata/*"},
handler = _M.dump_plugin_metadata,
},
-- /v1/plugins/reload
{
methods = {"PUT"},
uris = {"/plugins/reload"},
handler = _M.post_reload_plugins,
},
get_health_checkers = _get_health_checkers,
reload_event = _M.RELOAD_EVENT,
}