Commit 580a7ed4 authored by Grigorii Demidov's avatar Grigorii Demidov

Merge branch 'http-custom-endpoints' into 'master'

Allow creating custom endpoints in the HTTP module

See merge request !527
parents c4e904e1 c94f79a6
Pipeline #36787 passed with stages
in 5 minutes and 47 seconds
......@@ -1540,6 +1540,7 @@ static int wrk_resolve(lua_State *L)
}
knot_pkt_put_question(pkt, dname, rrclass, rrtype);
knot_wire_set_rd(pkt->wire);
knot_wire_set_ad(pkt->wire);
/* Add OPT RR */
pkt->opt_rr = knot_rrset_copy(worker->engine->resolver.opt_rr, NULL);
......
......@@ -282,6 +282,7 @@ knot_mm_t *kr_resolve_pool(struct kr_request *);
struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
struct kr_query *kr_rplan_resolved(struct kr_rplan *);
struct kr_query *kr_rplan_last(struct kr_rplan *);
int kr_nsrep_set(struct kr_query *, size_t, const struct sockaddr *);
uint32_t kr_rand_uint(uint32_t);
int kr_make_query(struct kr_query *query, knot_pkt_t *pkt);
......
......@@ -141,6 +141,7 @@ EOF
kr_rplan_push
kr_rplan_pop
kr_rplan_resolved
kr_rplan_last
# Nameservers
kr_nsrep_set
# Utils
......
......@@ -702,6 +702,13 @@ ffi.metatype( kr_request_t, {
if req.current_query == nil then return nil end
return req.current_query
end,
-- Return last query on the resolution plan
last = function(req)
assert(ffi.istype(kr_request_t, req))
local query = C.kr_rplan_last(C.kr_resolve_plan(req))
if query == nil then return end
return query
end,
resolved = function(req)
assert(ffi.istype(kr_request_t, req))
local qry = C.kr_rplan_resolved(C.kr_resolve_plan(req))
......
......@@ -31,6 +31,7 @@ for starters?
geoip = 'GeoLite2-City.mmdb' -- Optional, see
-- e.g. https://dev.maxmind.com/geoip/geoip2/geolite2/
-- and install mmdblua library
endpoints = {},
}
}
......@@ -108,6 +109,30 @@ You can use it out of the box:
latency_count 2.000000
latency_sum 11.000000
You can namespace the metrics in configuration, using `http.prometheus.namespace` attribute:
.. code-block:: lua
http = {
host = 'localhost',
}
-- Set Prometheus namespace
http.prometheus.namespace = 'resolver_'
You can also add custom metrics or rewrite existing metrics before they are returned to Prometheus client.
.. code-block:: lua
http = {
host = 'localhost',
}
-- Add an arbitrary metric to Prometheus
http.prometheus.finalize = function (metrics)
table.insert(metrics, 'build_info{version="1.2.3"} 1')
end
Tracing requests
^^^^^^^^^^^^^^^^
......@@ -142,7 +167,7 @@ In order to register a new service, simply add it to the table:
.. code-block:: lua
http.endpoints['/health'] = {'application/json',
local on_health = {'application/json',
function (h, stream)
-- API call, return a JSON table
return {state = 'up', uptime = 0}
......@@ -158,6 +183,12 @@ In order to register a new service, simply add it to the table:
-- Finalize the WebSocket
ws:close()
end}
-- Load module
modules = {
http = {
endpoints = { ['/health'] = on_health }
}
}
Then you can query the API endpoint, or tail the WebSocket using curl.
......@@ -200,7 +231,7 @@ the HTTP response code or send headers and body yourself.
local value = 42
-- Expose the service
http.endpoints['/service'] = {'application/json',
local service = {'application/json',
function (h, stream)
-- Get request method and deal with it properly
local m = h:get(':method')
......@@ -221,6 +252,12 @@ the HTTP response code or send headers and body yourself.
return 405, 'Cannot do that'
end
end}
-- Load the module
modules = {
http = {
endpoints = { ['/service'] = service }
}
}
In some cases you might need to send back your own headers instead of default provided by HTTP handler,
you can do this, but then you have to return ``false`` to notify handler that it shouldn't try to generate
......
......@@ -114,21 +114,27 @@ M.snippets = {}
-- Serve known requests, for methods other than GET
-- the endpoint must be a closure and not a preloaded string
local function serve(h, stream)
local function serve(endpoints, h, stream)
local hsend = http_headers.new()
local path = h:get(':path')
local entry = M.endpoints[path]
local entry = endpoints[path]
if not entry then -- Accept top-level path match
entry = M.endpoints[path:match '^/[^/]*']
entry = endpoints[path:match '^/[^/?]*']
end
-- Unpack MIME and data
local mime, data, err
local data, mime, ttl, err
if entry then
mime, data = unpack(entry)
mime = entry[1]
data = entry[2]
ttl = entry[4]
end
-- Get string data out of service endpoint
if type(data) == 'function' then
data, err = data(h, stream)
local set_mime, set_ttl
data, err, set_mime, set_ttl = data(h, stream)
-- Override default endpoint mime/TTL
if set_mime then mime = set_mime end
if set_ttl then ttl = set_ttl end
-- Handler doesn't provide any data
if data == false then return end
if type(data) == 'number' then return tostring(data), err end
......@@ -144,7 +150,6 @@ local function serve(h, stream)
hsend:append(':status', '200')
hsend:append('content-type', mime)
hsend:append('content-length', tostring(#data))
local ttl = entry and entry[4]
if ttl then
hsend:append('cache-control', string.format('max-age=%d', ttl))
end
......@@ -180,7 +185,7 @@ local function route(endpoints)
ws:close()
return
else
local ok, err, reason = http_util.yieldable_pcall(serve, h, stream)
local ok, err, reason = http_util.yieldable_pcall(serve, endpoints, h, stream)
if not ok or err then
if err ~= '404' and verbose() then
log('[http] %s %s: %s (%s)', m, path, err or '500', reason)
......@@ -278,7 +283,9 @@ function M.interface(host, port, endpoints, crtfile, keyfile)
crtfile = 'self.crt'
keyfile = 'self.key'
ephemeral = true
else error('certificate provided, but missing key') end
elseif not keyfile then
error('certificate provided, but missing key')
end
-- Read or create self-signed x509 certificate
local f = io.open(crtfile, 'r')
if f then
......@@ -357,7 +364,12 @@ function M.config(conf)
error('[http] mmdblua library not found, please remove GeoIP configuration')
end
end
M.interface(conf.host, conf.port, M.endpoints, conf.cert, conf.key)
-- Add endpoints to default endpoints
local endpoints = conf.endpoints or {}
for k, v in pairs(M.endpoints) do
endpoints[k] = v
end
M.interface(conf.host, conf.port, endpoints, conf.cert, conf.key)
end
return M
......@@ -11,6 +11,7 @@ else
http = {
port = 0, -- Select random port
cert = false,
endpoints = { ['/test'] = {'text/custom', function () return 'hello' end} },
}
}
......@@ -35,6 +36,11 @@ else
same(code, 200, 'static page return 200 OK')
ok(#body > 0, 'static page has non-empty body')
same(mime, 'text/html', 'static page has text/html content type')
-- custom endpoint
code, body, mime = http_get(uri .. '/test')
same(code, 200, 'custom page return 200 OK')
same(body, 'hello', 'custom page has non-empty body')
same(mime, 'text/custom', 'custom page has custom content type')
-- non-existent page
code = http_get(uri .. '/badpage')
same(code, 404, 'non-existent page returns 404')
......
-- Module implementation
local M = {
namespace = '',
finalize = function (_ --[[metrics]]) end,
}
local snapshots, snapshots_count = {}, 120
-- Gauge metrics
......@@ -126,33 +132,39 @@ local function serve_prometheus()
elseif k == 'answer_slow' then
table.insert(latency, {'+Inf', v})
-- Counter as a fallback
else table.insert(render, string.format(counter, k, k, v)) end
else
local key = M.namespace .. k
table.insert(render, string.format(counter, key, key, v))
end
end
-- Fill in latency histogram
local function kweight(x) return tonumber(x) or math.huge end
table.sort(latency, function (a,b) return kweight(a[1]) < kweight(b[1]) end)
table.insert(render, '# TYPE latency histogram')
table.insert(render, string.format('# TYPE %slatency histogram', M.namespace))
local count, sum = 0.0, 0.0
for _,e in ipairs(latency) do
-- The information about the %Inf bin is lost, so we treat it
-- as a timeout (3000ms) for metrics purposes
count = count + e[2]
sum = sum + e[2] * (math.min(tonumber(e[1]), 3000.0))
table.insert(render, string.format('latency_bucket{le="%s"} %f', e[1], count))
table.insert(render, string.format('%slatency_bucket{le="%s"} %f', M.namespace, e[1], count))
end
table.insert(render, string.format('%slatency_count %f', M.namespace, count))
table.insert(render, string.format('%slatency_sum %f', M.namespace, sum))
-- Finalize metrics table before rendering
if type(M.finalize) == 'function' then
M.finalize(render)
end
table.insert(render, string.format('latency_count %f', count))
table.insert(render, string.format('latency_sum %f', sum))
return table.concat(render, '\n') .. '\n'
end
-- Export endpoints
return {
init = snapshot_start,
deinit = snapshot_end,
gauges = gauges,
endpoints = {
['/stats'] = {'application/json', getstats, stream_stats},
['/frequent'] = {'application/json', function () return stats.frequent() end},
['/metrics'] = {'text/plain; version=0.0.4', serve_prometheus},
}
}
\ No newline at end of file
-- Export module interface
M.init = snapshot_start
M.deinit = snapshot_end
M.endpoints = {
['/stats'] = {'application/json', getstats, stream_stats},
['/frequent'] = {'application/json', function () return stats.frequent() end},
['/metrics'] = {'text/plain; version=0.0.4', serve_prometheus},
}
return M
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment