summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.luacheckrc2
-rw-r--r--modules/http/README.rst25
-rw-r--r--modules/http/http.lua22
-rw-r--r--modules/http/http.mk2
-rw-r--r--modules/http/http_test.lua88
-rw-r--r--modules/http/http_trace.lua71
-rw-r--r--tests/config/test.cfg18
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