918 lines
30 KiB
Lua
918 lines
30 KiB
Lua
local helpers = require "spec.helpers"
|
|
local shell = require "resty.shell"
|
|
local pl_file = require "pl.file"
|
|
local timeout = 10
|
|
local step = 1
|
|
|
|
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)", function()
|
|
local proxy_client
|
|
local admin_client
|
|
local proxy_client_grpc
|
|
local proxy_client_grpcs
|
|
|
|
local tcp_service_port
|
|
local tcp_proxy_port
|
|
|
|
setup(function()
|
|
tcp_service_port = helpers.get_available_port()
|
|
tcp_proxy_port = helpers.get_available_port()
|
|
|
|
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 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",
|
|
stream_listen = "127.0.0.1:" .. tcp_proxy_port,
|
|
})
|
|
proxy_client = helpers.proxy_client()
|
|
admin_client = helpers.admin_client()
|
|
proxy_client_grpc = helpers.proxy_client_grpc()
|
|
proxy_client_grpcs = helpers.proxy_client_grpcs()
|
|
end)
|
|
|
|
teardown(function()
|
|
if proxy_client then
|
|
proxy_client:close()
|
|
end
|
|
if admin_client then
|
|
admin_client:close()
|
|
end
|
|
|
|
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 res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-service",route="http-route",code="200",source="service",workspace="default",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)
|
|
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-service",route="http-route",code="400",source="service",workspace="default",consumer=""} 1', nil, true)
|
|
end)
|
|
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 res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-grpc-service",route="grpc-route",code="200",source="service",workspace="default",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 res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-grpcs-service",route="grpcs-route",code="200",source="service",workspace="default",consumer=""} 1', nil, true)
|
|
end)
|
|
end)
|
|
|
|
it("increments the count for proxied TCP streams", function()
|
|
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()
|
|
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
assert.matches('kong_stream_sessions_total{service="tcp-service",route="tcp-route",code="200",source="service",workspace="default"} 1', body, nil, true)
|
|
assert.matches('kong_session_duration_ms_bucket{service="tcp%-service",route="tcp%-route",workspace="default",le="%+Inf"} %d+', body)
|
|
|
|
return body:find('kong_stream_sessions_total{service="tcp-service",route="tcp-route",code="200",source="service",workspace="default"} 1', nil, true)
|
|
end)
|
|
|
|
thread:join()
|
|
end)
|
|
|
|
it("does not log error if no service was matched", function()
|
|
-- cleanup logs
|
|
shell.run(":> " .. helpers.test_conf.nginx_err_logs, nil, 0)
|
|
|
|
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
|
|
shell.run(":> " .. helpers.test_conf.nginx_err_logs, nil, 0)
|
|
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
|
|
-- make sure no errors
|
|
assert.logfile().has.no.line("[error]", true, 10)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
|
|
it("scrape response has metrics and comments only", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
|
|
for line in body:gmatch("[^\r\n]+") do
|
|
assert.matches("^[#|kong]", line)
|
|
end
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
|
|
it("exposes db reachability metrics", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_datastore_reachable 1', body, nil, true)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
|
|
it("exposes Lua worker VM stats", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_memory_workers_lua_vms_bytes{node_id="' .. UUID_PATTERN .. '",pid="%d+",kong_subsystem="http"} %d+', body)
|
|
assert.matches('kong_memory_workers_lua_vms_bytes{node_id="' .. UUID_PATTERN .. '",pid="%d+",kong_subsystem="stream"} %d+', body)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
|
|
it("exposes lua_shared_dict metrics", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
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
|
|
-- if stream_available then
|
|
-- assert.matches('kong_memory_lua_shared_dict_total_bytes' ..
|
|
-- '{shared_dict="stream_prometheus_metrics",kong_subsystem="stream"} %d+', body)
|
|
-- end
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
|
|
it("does not expose per consumer metrics by default", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.not_match('http_consumer_status', body, nil, true)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
end)
|
|
|
|
describe("Plugin: prometheus (access) no stream listeners", function()
|
|
local admin_client
|
|
|
|
setup(function()
|
|
local bp = helpers.get_db_utils()
|
|
|
|
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 {
|
|
plugins = "bundled, prometheus",
|
|
stream_listen = "off",
|
|
})
|
|
admin_client = helpers.admin_client()
|
|
end)
|
|
|
|
teardown(function()
|
|
if admin_client then
|
|
admin_client:close()
|
|
end
|
|
|
|
helpers.stop_kong()
|
|
end)
|
|
|
|
it("exposes Lua worker VM stats only for http subsystem", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_memory_workers_lua_vms_bytes{node_id="' .. UUID_PATTERN .. '",pid="%d+",kong_subsystem="http"}', body)
|
|
assert.not_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)
|
|
end)
|
|
|
|
it("exposes lua_shared_dict metrics only for http subsystem", function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_memory_lua_shared_dict_total_bytes' ..
|
|
'{node_id="' .. UUID_PATTERN .. '",shared_dict="prometheus_metrics",kong_subsystem="http"} %d+', body)
|
|
|
|
assert.not_matches('kong_memory_lua_shared_dict_bytes' ..
|
|
'{node_id="' .. UUID_PATTERN .. '",shared_dict="stream_prometheus_metric",kong_subsystem="stream"} %d+', body)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
end)
|
|
end)
|
|
|
|
describe("Plugin: prometheus (access) per-consumer metrics", function()
|
|
local proxy_client
|
|
local admin_client
|
|
|
|
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,
|
|
}
|
|
|
|
local route = bp.routes:insert {
|
|
protocols = { "http" },
|
|
name = "http-route",
|
|
paths = { "/" },
|
|
methods = { "GET" },
|
|
service = service,
|
|
}
|
|
|
|
bp.plugins:insert {
|
|
protocols = { "http", "https", "grpc", "grpcs", "tcp", "tls" },
|
|
name = "prometheus",
|
|
config = {
|
|
per_consumer = true,
|
|
status_code_metrics = true,
|
|
latency_metrics = true,
|
|
bandwidth_metrics = true,
|
|
upstream_health_metrics = true,
|
|
},
|
|
}
|
|
|
|
bp.plugins:insert {
|
|
name = "key-auth",
|
|
route = route,
|
|
}
|
|
|
|
local consumer = bp.consumers:insert {
|
|
username = "alice",
|
|
}
|
|
|
|
bp.keyauth_credentials:insert {
|
|
key = "alice-key",
|
|
consumer = consumer,
|
|
}
|
|
|
|
assert(helpers.start_kong {
|
|
nginx_conf = "spec/fixtures/custom_nginx.template",
|
|
plugins = "bundled, prometheus",
|
|
})
|
|
proxy_client = helpers.proxy_client()
|
|
admin_client = helpers.admin_client()
|
|
end)
|
|
|
|
teardown(function()
|
|
if proxy_client then
|
|
proxy_client:close()
|
|
end
|
|
if admin_client then
|
|
admin_client:close()
|
|
end
|
|
|
|
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,
|
|
apikey = 'alice-key',
|
|
}
|
|
})
|
|
assert.res_status(200, res)
|
|
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-service",route="http-route",code="200",source="service",workspace="default",consumer="alice"} 1', nil, true)
|
|
end)
|
|
|
|
res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/400",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
apikey = 'alice-key',
|
|
}
|
|
})
|
|
assert.res_status(400, res)
|
|
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
local body = assert.res_status(200, res)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
return body:find('http_requests_total{service="mock-service",route="http-route",code="400",source="service",workspace="default",consumer="alice"} 1', nil, true)
|
|
end)
|
|
end)
|
|
|
|
it("behave correctly if consumer is not found", function()
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/200",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
}
|
|
})
|
|
assert.res_status(401, res)
|
|
|
|
local body
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return body:find('http_requests_total{service="mock-service",route="http-route",code="200",source="service",workspace="default",consumer="alice"} 1', nil, true)
|
|
end)
|
|
|
|
assert.matches('http_requests_total{service="mock-service",route="http-route",code="401",source="kong",workspace="default",consumer=""} 1', body, nil, true)
|
|
|
|
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 admin_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",
|
|
nginx_worker_processes = 1, -- due to healthcheck state flakyness and local switch of healthcheck export or not
|
|
})
|
|
proxy_client = helpers.proxy_client()
|
|
admin_client = helpers.admin_client()
|
|
end)
|
|
|
|
teardown(function()
|
|
if proxy_client then
|
|
proxy_client:close()
|
|
end
|
|
if admin_client then
|
|
admin_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(admin_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
|
|
|
|
describe("Plugin: prometheus (access) AI metrics", function()
|
|
local proxy_client
|
|
local admin_client
|
|
local prometheus_plugin
|
|
local MOCK_PORT
|
|
|
|
setup(function()
|
|
MOCK_PORT = helpers.get_available_port()
|
|
|
|
local bp = helpers.get_db_utils()
|
|
|
|
local fixtures = {
|
|
http_mock = {},
|
|
}
|
|
|
|
fixtures.http_mock.openai = [[
|
|
server {
|
|
server_name openai;
|
|
listen ]]..MOCK_PORT..[[;
|
|
|
|
default_type 'application/json';
|
|
|
|
|
|
location = "/llm/v1/chat/good" {
|
|
content_by_lua_block {
|
|
local pl_file = require "pl.file"
|
|
local json = require("cjson.safe")
|
|
|
|
ngx.req.read_body()
|
|
local body, err = ngx.req.get_body_data()
|
|
body, err = json.decode(body)
|
|
|
|
local token = ngx.req.get_headers()["authorization"]
|
|
local token_query = ngx.req.get_uri_args()["apikey"]
|
|
|
|
if token == "Bearer openai-key" or token_query == "openai-key" or body.apikey == "openai-key" then
|
|
ngx.req.read_body()
|
|
local body, err = ngx.req.get_body_data()
|
|
body, err = json.decode(body)
|
|
|
|
if err or (body.messages == ngx.null) then
|
|
ngx.status = 400
|
|
ngx.print(pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/responses/bad_request.json"))
|
|
else
|
|
ngx.status = 200
|
|
ngx.print(pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/responses/good.json"))
|
|
end
|
|
else
|
|
ngx.status = 401
|
|
ngx.print(pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/responses/unauthorized.json"))
|
|
end
|
|
}
|
|
}
|
|
}
|
|
]]
|
|
|
|
local empty_service = assert(bp.services:insert {
|
|
name = "empty_service",
|
|
host = "localhost", --helpers.mock_upstream_host,
|
|
port = 8080, --MOCK_PORT,
|
|
path = "/",
|
|
})
|
|
|
|
-- 200 chat good with one option
|
|
local chat_good = assert(bp.routes:insert {
|
|
service = empty_service,
|
|
name = "http-route",
|
|
protocols = { "http" },
|
|
strip_path = true,
|
|
paths = { "/" }
|
|
})
|
|
|
|
bp.plugins:insert {
|
|
name = "ai-proxy",
|
|
route = { id = chat_good.id },
|
|
config = {
|
|
route_type = "llm/v1/chat",
|
|
logging = {
|
|
log_payloads = false,
|
|
log_statistics = true,
|
|
},
|
|
auth = {
|
|
header_name = "Authorization",
|
|
header_value = "Bearer openai-key",
|
|
},
|
|
model = {
|
|
name = "gpt-3.5-turbo",
|
|
provider = "openai",
|
|
options = {
|
|
max_tokens = 256,
|
|
temperature = 1.0,
|
|
upstream_url = "http://"..helpers.mock_upstream_host..":"..MOCK_PORT.."/llm/v1/chat/good",
|
|
input_cost = 10.0,
|
|
output_cost = 10.0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
prometheus_plugin = assert(bp.plugins:insert {
|
|
protocols = { "http", "https", "grpc", "grpcs", "tcp", "tls" },
|
|
name = "prometheus",
|
|
config = {
|
|
-- ai_metrics = true,
|
|
status_code_metrics = true,
|
|
},
|
|
})
|
|
|
|
assert(helpers.start_kong ({
|
|
nginx_conf = "spec/fixtures/custom_nginx.template",
|
|
plugins = "bundled, prometheus",
|
|
}, nil, nil, fixtures))
|
|
proxy_client = helpers.proxy_client()
|
|
admin_client = helpers.admin_client()
|
|
end)
|
|
|
|
teardown(function()
|
|
if proxy_client then
|
|
proxy_client:close()
|
|
end
|
|
if admin_client then
|
|
admin_client:close()
|
|
end
|
|
|
|
helpers.stop_kong()
|
|
end)
|
|
|
|
it("no AI metrics when not enable in Prometheus plugin", function()
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/200",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
authorization = 'Bearer openai-key',
|
|
["content-type"] = 'application/json',
|
|
accept = 'application/json',
|
|
},
|
|
body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"),
|
|
})
|
|
assert.res_status(200, res)
|
|
|
|
local body
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return res.status == 200
|
|
end)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
assert.matches('http_requests_total{service="empty_service",route="http-route",code="200",source="service",workspace="default",consumer=""} 1', body, nil, true)
|
|
|
|
assert.not_match('ai_llm_requests_total', body, nil, true)
|
|
assert.not_match('ai_llm_cost_total', body, nil, true)
|
|
assert.not_match('ai_llm_tokens_total', body, nil, true)
|
|
assert.not_match('ai_llm_provider_latency_ms_bucket', body, nil, true)
|
|
end)
|
|
|
|
it("update prometheus plugin config", function()
|
|
local body
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "PATCH",
|
|
path = "/plugins/" .. prometheus_plugin.id,
|
|
body = {
|
|
name = "prometheus",
|
|
config = {
|
|
status_code_metrics = true,
|
|
ai_metrics = true,
|
|
}
|
|
},
|
|
headers = {
|
|
["Content-Type"] = "application/json"
|
|
}
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return res.status == 200
|
|
end)
|
|
|
|
local cjson = require "cjson"
|
|
local json = cjson.decode(body)
|
|
assert.equal(true, json.config.ai_metrics)
|
|
|
|
ngx.sleep(2)
|
|
end)
|
|
|
|
it("add the count for proxied AI requests", function()
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/200",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
authorization = 'Bearer openai-key',
|
|
["content-type"] = 'application/json',
|
|
accept = 'application/json',
|
|
},
|
|
body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"),
|
|
})
|
|
assert.res_status(200, res)
|
|
|
|
local body
|
|
-- wait until the histogram observe finished and get the correct metrics.
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return body:find('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 1',
|
|
nil, true)
|
|
end, timeout, step)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
assert.matches('http_requests_total{service="empty_service",route="http-route",code="200",source="service",workspace="default",consumer=""} 2', body, nil, true)
|
|
|
|
assert.matches('ai_llm_requests_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 1', body, nil, true)
|
|
|
|
assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00037', body, nil, true)
|
|
|
|
assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 1', body, nil, true)
|
|
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="completion_tokens",workspace="default"} 12', body, nil, true)
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="prompt_tokens",workspace="default"} 25', body, nil, true)
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="total_tokens",workspace="default"} 37', body, nil, true)
|
|
end)
|
|
|
|
it("increments the count for proxied AI requests", function()
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/200",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
authorization = 'Bearer openai-key',
|
|
["content-type"] = 'application/json',
|
|
accept = 'application/json',
|
|
},
|
|
body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"),
|
|
})
|
|
assert.res_status(200, res)
|
|
|
|
local body
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return body:find('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2',
|
|
nil, true)
|
|
end, timeout, step)
|
|
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
assert.matches('http_requests_total{service="empty_service",route="http-route",code="200",source="service",workspace="default",consumer=""} 3', body, nil, true)
|
|
|
|
assert.matches('ai_llm_requests_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 2', body, nil, true)
|
|
|
|
assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00074', body, nil, true)
|
|
|
|
assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2', body, nil, true)
|
|
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="completion_tokens",workspace="default"} 24', body, nil, true)
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="prompt_tokens",workspace="default"} 50', body, nil, true)
|
|
assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="total_tokens",workspace="default"} 74', body, nil, true)
|
|
end)
|
|
|
|
it("behave correctly if AI metrics are not found", function()
|
|
local res = assert(proxy_client:send {
|
|
method = "GET",
|
|
path = "/status/400",
|
|
headers = {
|
|
host = helpers.mock_upstream_host,
|
|
}
|
|
})
|
|
assert.res_status(400, res)
|
|
|
|
local body
|
|
helpers.wait_until(function()
|
|
local res = assert(admin_client:send {
|
|
method = "GET",
|
|
path = "/metrics",
|
|
})
|
|
body = assert.res_status(200, res)
|
|
return body:find('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2',
|
|
nil, true)
|
|
end, timeout, step)
|
|
|
|
assert.matches('http_requests_total{service="empty_service",route="http-route",code="400",source="kong",workspace="default",consumer=""} 1', body, nil, true)
|
|
assert.matches('kong_nginx_metric_errors_total 0', body, nil, true)
|
|
|
|
assert.matches('ai_llm_requests_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 2', body, nil, true)
|
|
assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00074', body, nil, true)
|
|
assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2', body, nil, true)
|
|
end)
|
|
end)
|