kong/spec/03-plugins/26-prometheus/04-status_api_spec.lua (445 lines of code) (raw):

local helpers = require "spec.helpers" local tcp_service_port = helpers.get_available_port() local tcp_proxy_port = helpers.get_available_port() local tcp_status_port = helpers.get_available_port() local UUID_PATTERN = "%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x" describe("Plugin: prometheus (access via status API)", function() local proxy_client local status_client local proxy_client_grpc local proxy_client_grpcs local function get_metrics(reopened) if not status_client then status_client = helpers.http_client("127.0.0.1", tcp_status_port, 20000) end local res, err = status_client:send({ method = "GET", path = "/metrics", }) if err and err:find("closed", nil, true) and not reopened then status_client = nil return get_metrics(true) end assert.is_nil(err, "failed GET /metrics: " .. tostring(err)) return assert.res_status(200, res) end setup(function() local bp = helpers.get_db_utils() local upstream_hc_off = bp.upstreams:insert({ name = "mock-upstream-healthchecksoff", }) bp.targets:insert { target = helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port, weight = 1000, upstream = { id = upstream_hc_off.id }, } local upstream = bp.upstreams:insert({ name = "mock-upstream", }) upstream.healthchecks = { active = { concurrency = 10, healthy = { http_statuses = { 200, 302 }, interval = 0.1, successes = 2 }, http_path = "/status/200", https_verify_certificate = true, timeout = 1, type = "http", unhealthy = { http_failures = 1, http_statuses = { 429, 404, 500, 501, 502, 503, 504, 505 }, interval = 0.1, tcp_failures = 1, timeouts = 1 } }, passive = { healthy = { http_statuses = { 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308 }, successes = 1 }, type = "http", unhealthy = { http_failures = 1, http_statuses = { 429, 500, 503 }, tcp_failures = 1, timeouts = 1 } } } upstream = bp.upstreams:update({ id = upstream.id }, { healthchecks = upstream.healthchecks }) bp.targets:insert { target = helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port, weight = 1000, upstream = { id = upstream.id }, } bp.targets:insert { target = helpers.mock_upstream_host .. ':8001', weight = 1, upstream = { id = upstream.id }, } bp.targets:insert { target = 'some-random-dns:80', weight = 1, upstream = { id = upstream.id }, } local service = bp.services:insert { name = "mock-service", host = upstream.name, port = helpers.mock_upstream_port, protocol = helpers.mock_upstream_protocol, } bp.routes:insert { protocols = { "http" }, name = "http-route", paths = { "/" }, methods = { "GET" }, service = service, } local grpc_service = bp.services:insert { name = "mock-grpc-service", url = helpers.grpcbin_url, } bp.routes:insert { protocols = { "grpc" }, name = "grpc-route", hosts = { "grpc" }, service = grpc_service, } local grpcs_service = bp.services:insert { name = "mock-grpcs-service", url = helpers.grpcbin_ssl_url, } bp.routes:insert { protocols = { "grpcs" }, name = "grpcs-route", hosts = { "grpcs" }, service = grpcs_service, } local tcp_service = bp.services:insert { name = "tcp-service", url = "tcp://127.0.0.1:" .. tcp_service_port, } bp.routes:insert { protocols = { "tcp" }, name = "tcp-route", service = tcp_service, destinations = { { port = tcp_proxy_port } }, } bp.plugins:insert { protocols = { "http", "https", "grpc", "grpcs", "tcp", "tls" }, name = "prometheus", config = { status_code_metrics = true, latency_metrics = true, bandwidth_metrics = true, upstream_health_metrics = true, }, } assert(helpers.start_kong { nginx_conf = "spec/fixtures/custom_nginx.template", plugins = "bundled", status_listen = "0.0.0.0:" .. tcp_status_port, stream_listen = "127.0.0.1:" .. tcp_proxy_port, nginx_worker_processes = 1, -- due to healthcheck state flakyness and local switch of healthcheck export or not }) proxy_client_grpc = helpers.proxy_client_grpc() proxy_client_grpcs = helpers.proxy_client_grpcs() require("socket").sleep(1) -- wait 1 second until healthchecks run end) before_each(function() proxy_client = helpers.proxy_client() end) after_each(function() if status_client then status_client:close() end if proxy_client then proxy_client:close() end end) teardown(function() helpers.stop_kong() end) it("increments the count for proxied requests", function() local res = assert(proxy_client:send { method = "GET", path = "/status/200", headers = { host = helpers.mock_upstream_host, } }) assert.res_status(200, res) helpers.wait_until(function() local body = get_metrics() return body:find('http_requests_total{service="mock-service",route="http-route",code="200",source="service",consumer=""} 1', nil, true) end) res = assert(proxy_client:send { method = "GET", path = "/status/400", headers = { host = helpers.mock_upstream_host, } }) assert.res_status(400, res) local body = get_metrics() assert.matches('kong_kong_latency_ms_bucket{service="mock%-service",route="http%-route",le="%+Inf"} +%d', body) assert.matches('kong_upstream_latency_ms_bucket{service="mock%-service",route="http%-route",le="%+Inf"} +%d', body) assert.matches('kong_request_latency_ms_bucket{service="mock%-service",route="http%-route",le="%+Inf"} +%d', body) assert.matches('http_requests_total{service="mock-service",route="http-route",code="400",source="service",consumer=""} 1', body, nil, true) assert.matches('kong_bandwidth_bytes{service="mock%-service",route="http%-route",direction="ingress",consumer=""} %d+', body) assert.matches('kong_bandwidth_bytes{service="mock%-service",route="http%-route",direction="egress",consumer=""} %d+', body) end) it("increments the count for proxied grpc requests", function() local ok, resp = proxy_client_grpc({ service = "hello.HelloService.SayHello", body = { greeting = "world!" }, opts = { ["-authority"] = "grpc", } }) assert(ok, resp) assert.truthy(resp) helpers.wait_until(function() local body = get_metrics() return body:find('http_requests_total{service="mock-grpc-service",route="grpc-route",code="200",source="service",consumer=""} 1', nil, true) end) ok, resp = proxy_client_grpcs({ service = "hello.HelloService.SayHello", body = { greeting = "world!" }, opts = { ["-authority"] = "grpcs", } }) assert(ok, resp) assert.truthy(resp) helpers.wait_until(function() local body = get_metrics() return body:find('http_requests_total{service="mock-grpcs-service",route="grpcs-route",code="200",source="service",consumer=""} 1', nil, true) end) end) it("does not log error if no service was matched", function() -- cleanup logs os.execute(":> " .. helpers.test_conf.nginx_err_logs) local res = assert(proxy_client:send { method = "POST", path = "/no-route-match-in-kong", }) assert.res_status(404, res) -- make sure no errors assert.logfile().has.no.line("[error]", true, 10) end) it("does not log error during a scrape", function() -- cleanup logs os.execute(":> " .. helpers.test_conf.nginx_err_logs) get_metrics() -- make sure no errors assert.logfile().has.no.line("[error]", true, 10) end) it("scrape response has metrics and comments only", function() local body = get_metrics() for line in body:gmatch("[^\r\n]+") do assert.matches("^[#|kong]", line) end end) it("exposes db reachability metrics", function() local body = get_metrics() assert.matches('kong_datastore_reachable 1', body, nil, true) end) it("exposes nginx timer metrics", function() local body = get_metrics() assert.matches('kong_nginx_timers{state="running"} %d+', body) assert.matches('kong_nginx_timers{state="pending"} %d+', body) end) it("exposes upstream's target health metrics - healthchecks-off", function() local body helpers.wait_until(function() body = get_metrics() return body:find('kong_upstream_target_health{upstream="mock-upstream-healthchecksoff",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="healthchecks_off",subsystem="http"} 1', nil, true) end) assert.matches('kong_upstream_target_health{upstream="mock-upstream-healthchecksoff",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="healthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream-healthchecksoff",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="unhealthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream-healthchecksoff",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="dns_error",subsystem="http"} 0', body, nil, true) end) it("exposes upstream's target health metrics - healthy", function() local body helpers.wait_until(function() body = get_metrics() return body:find('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="healthy",subsystem="http"} 1', nil, true) end) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="healthchecks_off",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="unhealthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",address="' .. helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port .. '",state="dns_error",subsystem="http"} 0', body, nil, true) end) it("exposes upstream's target health metrics - unhealthy", function() local body helpers.wait_until(function() body = get_metrics() return body:find('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':8001",address="' .. helpers.mock_upstream_host .. ':8001",state="unhealthy",subsystem="http"} 1', nil, true) end) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':8001",address="' .. helpers.mock_upstream_host .. ':8001",state="healthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':8001",address="' .. helpers.mock_upstream_host .. ':8001",state="healthchecks_off",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="' .. helpers.mock_upstream_host .. ':8001",address="' .. helpers.mock_upstream_host .. ':8001",state="dns_error",subsystem="http"} 0', body, nil, true) end) it("exposes upstream's target health metrics - dns_error", function() local body helpers.wait_until(function() body = get_metrics() return body:find('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="dns_error",subsystem="http"} 1', nil, true) end) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="unhealthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthchecks_off",subsystem="http"} 0', body, nil, true) end) it("adds subsystem label to upstream's target health metrics", function() -- need to send at least TCP request to start exposing target health metrics local thread = helpers.tcp_server(tcp_service_port, { requests = 1 }) local conn = assert(ngx.socket.connect("127.0.0.1", tcp_proxy_port)) assert(conn:send("hi there!\n")) local gotback = assert(conn:receive("*a")) assert.equal("hi there!\n", gotback) conn:close() local body helpers.wait_until(function() body = get_metrics() return body:find('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="dns_error",subsystem="stream"} 1', nil, true) end) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthy",subsystem="stream"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="unhealthy",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="unhealthy",subsystem="stream"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthchecks_off",subsystem="http"} 0', body, nil, true) assert.matches('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80",address="",state="healthchecks_off",subsystem="stream"} 0', body, nil, true) thread:join() end) it("remove metrics from deleted upstreams", function() local admin_client = helpers.admin_client() assert(admin_client:send { method = "DELETE", path = "/upstreams/mock-upstream-healthchecksoff", }) admin_client:close() local body helpers.wait_until(function() body = get_metrics() return not body:find('kong_upstream_target_health{upstream="mock-upstream-healthchecksoff"', nil, true) end, 15) end) it("remove metrics from deleted targets", function() local admin_client = helpers.admin_client() assert(admin_client:send { method = "DELETE", path = "/upstreams/mock-upstream/targets/some-random-dns:80", }) admin_client:close() local body helpers.wait_until(function() body = get_metrics() return not body:find('kong_upstream_target_health{upstream="mock-upstream",target="some-random-dns:80"', nil, true) end, 15) end) it("exposes Lua worker VM stats", function() local body = get_metrics() assert.matches('kong_memory_workers_lua_vms_bytes{node_id="' .. UUID_PATTERN .. '",pid="%d+",kong_subsystem="http"}', body) assert.matches('kong_memory_workers_lua_vms_bytes{node_id="' .. UUID_PATTERN .. '",pid="%d+",kong_subsystem="stream"}', body) assert.matches('kong_nginx_metric_errors_total 0', body, nil, true) assert.matches('kong_node_info{node_id="' .. UUID_PATTERN .. '",version="%S+"} 1', body) end) it("exposes lua_shared_dict metrics", function() local body = get_metrics() assert.matches('kong_memory_lua_shared_dict_total_bytes' .. '{node_id="' .. UUID_PATTERN .. '",shared_dict="prometheus_metrics",kong_subsystem="http"} %d+', body) -- TODO: uncomment below once the ngx.shared iterrator in stream is fixed -- assert.matches('kong_memory_lua_shared_dict_total_bytes' .. -- '{shared_dict="prometheus_metrics",kong_subsystem="stream"} %d+', body) assert.matches('kong_nginx_metric_errors_total 0', body, nil, true) end) end) local granular_metrics_set = { status_code_metrics = "http_requests_total", latency_metrics = "kong_latency_ms", bandwidth_metrics = "bandwidth_bytes", upstream_health_metrics = "upstream_target_health", } for switch, expected_pattern in pairs(granular_metrics_set) do describe("Plugin: prometheus (access) granular metrics switch", function() local proxy_client local status_client local success_scrape = "" setup(function() local bp = helpers.get_db_utils() local service = bp.services:insert { name = "mock-service", host = helpers.mock_upstream_host, port = helpers.mock_upstream_port, protocol = helpers.mock_upstream_protocol, } bp.routes:insert { protocols = { "http" }, name = "http-route", paths = { "/" }, methods = { "GET" }, service = service, } local upstream_hc_off = bp.upstreams:insert({ name = "mock-upstream-healthchecksoff", }) bp.targets:insert { target = helpers.mock_upstream_host .. ':' .. helpers.mock_upstream_port, weight = 1000, upstream = { id = upstream_hc_off.id }, } bp.plugins:insert { protocols = { "http", "https", "grpc", "grpcs", "tcp", "tls" }, name = "prometheus", config = { [switch] = true, }, } assert(helpers.start_kong { nginx_conf = "spec/fixtures/custom_nginx.template", plugins = "bundled, prometheus", status_listen = "0.0.0.0:" .. tcp_status_port, nginx_worker_processes = 1, -- due to healthcheck state flakyness and local switch of healthcheck export or not }) proxy_client = helpers.proxy_client() status_client = helpers.http_client("127.0.0.1", tcp_status_port, 20000) end) teardown(function() if proxy_client then proxy_client:close() end if status_client then status_client:close() end helpers.stop_kong() end) it("expected metrics " .. expected_pattern .. " is found", function() local res = assert(proxy_client:send { method = "GET", path = "/status/200", headers = { host = helpers.mock_upstream_host, apikey = 'alice-key', } }) assert.res_status(200, res) helpers.wait_until(function() local res = assert(status_client:send { method = "GET", path = "/metrics", }) local body = assert.res_status(200, res) assert.matches('kong_nginx_metric_errors_total 0', body, nil, true) success_scrape = body return body:find(expected_pattern, nil, true) end) end) it("unexpected metrics is not found", function() for test_switch, test_expected_pattern in pairs(granular_metrics_set) do if test_switch ~= switch then assert.not_match(test_expected_pattern, success_scrape, nil, true) end end end) end) end