diff options
-rw-r--r-- | .luacheckrc | 2 | ||||
-rw-r--r-- | modules/http/README.rst | 25 | ||||
-rw-r--r-- | modules/http/http.lua | 22 | ||||
-rw-r--r-- | modules/http/http.mk | 2 | ||||
-rw-r--r-- | modules/http/http_test.lua | 88 | ||||
-rw-r--r-- | modules/http/http_trace.lua | 71 | ||||
-rw-r--r-- | tests/config/test.cfg | 18 |
7 files changed, 213 insertions, 15 deletions
diff --git a/.luacheckrc b/.luacheckrc index 29e59a28..e45db7d0 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -76,4 +76,4 @@ files['daemon/lua/kres-gen.lua'].ignore = {'631'} -- Allow overly long lines -- Tests and scripts can use global variables files['scripts'].ignore = {'111', '112', '113'} files['tests'].ignore = {'111', '112', '113'} -files['modules/*/*_test.lua'].ignore = {'111', '112', '113', '122'}
\ No newline at end of file +files['modules/*/*_test.lua'].ignore = {'111', '112', '113', '121', '122'}
\ No newline at end of file diff --git a/modules/http/README.rst b/modules/http/README.rst index a3ecfc2c..1fff6cbf 100644 --- a/modules/http/README.rst +++ b/modules/http/README.rst @@ -85,6 +85,7 @@ The HTTP module has several built-in services to use. "``/stats``", "Statistics/metrics", "Exported metrics in JSON." "``/metrics``", "Prometheus metrics", "Exported metrics for Prometheus_" "``/feed``", "Most frequent queries", "List of most frequent queries in JSON." + "``/trace/:name/:type``", "Tracking", "Trace resolution of the query and return the verbose logs." Enabling Prometheus metrics endpoint ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -107,6 +108,30 @@ You can use it out of the box: latency_count 2.000000 latency_sum 11.000000 +Tracing requests +^^^^^^^^^^^^^^^^ + +With the ``/trace`` endpoint you can trace various aspects of the request execution. +The basic mode allows you to resolve a query and trace verbose logs (and messages received): + +.. code-block:: bash + + $ curl http://localhost:8080/trace/e.root-servers.net + iter | 'e.root-servers.net.' type 'A' created outbound query, parent id 0 + rc | => rank: 020, lowest 020, e.root-servers.net. A + rc | => satisfied from cache + iter | <= answer received: + ;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 14771 + ;; Flags: qr aa QUERY: 1; ANSWER: 0; AUTHORITY: 0; ADDITIONAL: 0 + + ;; QUESTION SECTION + e.root-servers.net. A + + ;; ANSWER SECTION + e.root-servers.net. 3599821 A 192.203.230.10 + + iter | <= rcode: NOERROR + resl | finished: 4, queries: 1, mempool: 81952 B How to expose services over HTTP ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/modules/http/http.lua b/modules/http/http.lua index 361fac8e..32a690cf 100644 --- a/modules/http/http.lua +++ b/modules/http/http.lua @@ -104,6 +104,13 @@ for k, v in pairs(prometheus.endpoints) do end M.prometheus = prometheus +-- Export built-in trace interface +local http_trace = require('http_trace') +for k, v in pairs(http_trace.endpoints) do + M.endpoints[k] = v +end +M.trace = http_trace + -- Export HTTP service page snippets M.snippets = {} @@ -138,6 +145,7 @@ local function serve(h, stream) -- Serve content type appropriately 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)) @@ -296,14 +304,18 @@ function M.interface(host, port, endpoints, crtfile, keyfile) local routes = route(endpoints) -- Create TLS context and start listening local s, err = http_server.listen { - cq = cq; + cq = cq, host = host, port = port, client_timeout = 5, ctx = crt and tlscontext(crt, key), - onstream = routes; + onstream = routes, } - if not s then + -- Manually call :listen() so that we are bound before calling :localname() + if s then + err = select(2, s:listen()) + end + if err then panic('failed to listen on %s@%d: %s', host, port, err) end table.insert(M.servers, s) @@ -370,8 +382,8 @@ function M.config(conf) -- Reschedule timeout or create new one local timeout = cq:timeout() if timeout then - -- Throttle web requests - if timeout == 0 then timeout = 0.001 end + -- Throttle web requests (at most 100000 req/s) + if timeout == 0 then timeout = 0.00001 end -- Convert from seconds to duration timeout = timeout * sec if not M.timeout then diff --git a/modules/http/http.mk b/modules/http/http.mk index d4f4bf27..9ce4f0de 100644 --- a/modules/http/http.mk +++ b/modules/http/http.mk @@ -1,3 +1,3 @@ -http_SOURCES := http.lua prometheus.lua +http_SOURCES := http.lua prometheus.lua http_trace.lua http_INSTALL := $(wildcard modules/http/static/*) $(call make_lua_module,http) diff --git a/modules/http/http_test.lua b/modules/http/http_test.lua new file mode 100644 index 00000000..ca61bc54 --- /dev/null +++ b/modules/http/http_test.lua @@ -0,0 +1,88 @@ +-- check prerequisites +local test_utils = require('test_utils') +local supports_http, request = pcall(require, 'http.request') +if not supports_http then + pass('skipping http module test because its not installed') + done() +end + +-- setup resolver +modules = { + http = { + port = 0, -- Select random port + cert = false, + } +} + +local server = http.servers[1] +ok(server ~= nil, 'creates server instance') +local _, host, port = server:localname() +ok(host and port, 'binds to an interface') + +-- constructor for asynchronously executed functions +local cqueues = require('cqueues') +local function asynchronous(cb) + local cq = cqueues.new() + cq:wrap(cb) + event.socket(cq:pollfd(), function (ev) + cq:step(0) + if cq:empty() then + event.cancel(ev) + end + end) + return cq +end + +-- helper for returning useful values to test on +local function http_get(uri) + local headers, stream = assert(request.new_from_uri(uri .. '/'):go()) + local body = assert(stream:get_body_as_string()) + return tonumber(headers:get(':status')), body, headers:get('content-type') +end + +-- test whether http interface responds and binds +local function test_builtin_pages() + local code, body, mime + local uri = string.format('http://%s:%d', host, port) + -- simple static page + code, body, mime = http_get(uri .. '/') + 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') + -- non-existent page + code = http_get(uri .. '/badpage') + same(code, 404, 'non-existent page returns 404') + -- /stats endpoint serves metrics + code, body, mime = http_get(uri .. '/stats') + same(code, 200, '/stats page return 200 OK') + ok(#body > 0, '/stats page has non-empty body') + same(mime, 'application/json', '/stats page has correct content type') + -- /metrics serves metrics + code, body, mime = http_get(uri .. '/metrics') + same(code, 200, '/metrics page return 200 OK') + ok(#body > 0, '/metrics page has non-empty body') + same(mime, 'text/plain; version=0.0.4', '/metrics page has correct content type') + -- /trace serves trace log for requests + code, body, mime = http_get(uri .. '/trace/localhost/A') + same(code, 200, '/trace page return 200 OK') + ok(#body > 0, '/trace page has non-empty body') + same(mime, 'text/plain', '/trace page has correct content type') + -- /trace checks variables + code = http_get(uri .. '/trace/localhost/BADTYPE') + same(code, 400, '/trace checks type') + code = http_get(uri .. '/trace/') + same(code, 400, '/trace requires name') +end + +-- plan tests +local tests = { + test_builtin_pages, +} + +-- run tests asynchronously +asynchronous(function () + for _, t in ipairs(tests) do + test_utils.test(t) + end + done() +end)
\ No newline at end of file diff --git a/modules/http/http_trace.lua b/modules/http/http_trace.lua new file mode 100644 index 00000000..46a65f3d --- /dev/null +++ b/modules/http/http_trace.lua @@ -0,0 +1,71 @@ +local ffi = require('ffi') +local condition = require('cqueues.condition') + +-- Trace execution of DNS queries +local function serve_trace(h, _) + local path = h:get(':path') + local qname, qtype_str = path:match('/trace/([^/]+)/?([^/]*)') + if not qname then + return 400, 'expected /trace/<query name>/<query type>' + end + + -- Parse query type (or default to A) + if not qtype_str or #qtype_str == 0 then + qtype_str = 'A' + end + + local qtype = kres.type[qtype_str] + if not qtype then + return 400, string.format('unexpected query type: %s', qtype_str) + end + + -- Create logging handler callback + local buffer = {} + local buffer_log_cb = ffi.cast('trace_log_f', function (_, source, msg) + local message = string.format('%4s | %s', ffi.string(source), ffi.string(msg)) + table.insert(buffer, message) + end) + + -- Wait for the result of the query + -- Note: We can't do non-blocking write to stream directly from resolve callbacks + -- because they don't run inside cqueue. + local cond = condition.new() + local done = false + + -- Resolve query and buffer logs into table + resolve { + name = qname, + type = qtype, + options = {'TRACE'}, + begin = function (req) + req = kres.request_t(req) + req.trace_log = buffer_log_cb + end, + finish = function () + cond:signal() + done = true + end + } + + -- Wait for asynchronous query and free callbacks + if done then + cond:wait(0) -- Must pick up the signal + else + cond:wait() + end + buffer_log_cb:free() + + -- Return buffered data + local result = table.concat(buffer, '') .. '\n' + if not done then + return 504, result + end + return result +end + +-- Export endpoints +return { + endpoints = { + ['/trace'] = {'text/plain', serve_trace}, + } +}
\ No newline at end of file diff --git a/tests/config/test.cfg b/tests/config/test.cfg index a578aaca..c569dd85 100644 --- a/tests/config/test.cfg +++ b/tests/config/test.cfg @@ -19,14 +19,16 @@ for k, v in pairs(tapered) do end -- load test -local tests = dofile(env.TEST_FILE) or {} +local tests = dofile(env.TEST_FILE) -- run test after processed config file -- default config will be used and we can test it. -local runtest = require('test_utils').test -event.after(0, function () - for _, t in ipairs(tests) do - runtest(t) - end - done() -end) +if tests then + local runtest = require('test_utils').test + event.after(0, function () + for _, t in ipairs(tests) do + runtest(t) + end + done() + end) +end
\ No newline at end of file |