diff --git a/kong-0.6.1-0.rockspec b/kong-0.6.1-0.rockspec index 849a73c7487..f12f5988a93 100644 --- a/kong-0.6.1-0.rockspec +++ b/kong-0.6.1-0.rockspec @@ -143,6 +143,7 @@ build = { ["kong.plugins.log-serializers.basic"] = "kong/plugins/log-serializers/basic.lua", ["kong.plugins.log-serializers.alf"] = "kong/plugins/log-serializers/alf.lua", + ["kong.plugins.log-serializers.runscope"] = "kong/plugins/log-serializers/runscope.lua", ["kong.plugins.tcp-log.handler"] = "kong/plugins/tcp-log/handler.lua", ["kong.plugins.tcp-log.schema"] = "kong/plugins/tcp-log/schema.lua", @@ -156,6 +157,10 @@ build = { ["kong.plugins.file-log.handler"] = "kong/plugins/file-log/handler.lua", ["kong.plugins.file-log.schema"] = "kong/plugins/file-log/schema.lua", + ["kong.plugins.runscope.handler"] = "kong/plugins/runscope/handler.lua", + ["kong.plugins.runscope.schema"] = "kong/plugins/runscope/schema.lua", + ["kong.plugins.runscope.buffer"] = "kong/plugins/runscope/log.lua", + ["kong.plugins.mashape-analytics.schema.migrations"] = "kong/plugins/mashape-analytics/schema/migrations.lua", ["kong.plugins.mashape-analytics.handler"] = "kong/plugins/mashape-analytics/handler.lua", ["kong.plugins.mashape-analytics.schema"] = "kong/plugins/mashape-analytics/schema.lua", diff --git a/kong/plugins/log-serializers/runscope.lua b/kong/plugins/log-serializers/runscope.lua new file mode 100644 index 00000000000..4f4fdfe06fc --- /dev/null +++ b/kong/plugins/log-serializers/runscope.lua @@ -0,0 +1,32 @@ +local ngx_now = ngx.now +local req_get_method = ngx.req.get_method +local req_start_time = ngx.req.start_time +local req_get_headers = ngx.req.get_headers +local res_get_headers = ngx.resp.get_headers + +local _M = {} + +function _M.serialize(ngx) + local runscope_ctx = ngx.ctx.runscope or {} + + return { + request = { + url = ngx.var.scheme.."://"..ngx.var.host..":"..ngx.var.server_port..ngx.var.request_uri, + method = req_get_method(), + headers = req_get_headers(), + body = runscope_ctx.req_body, + timestamp = req_start_time(), + form = runscope_ctx.req_post_args + }, + response = { + status = ngx.status, + headers = res_get_headers(), + size_bytes = ngx.var.body_bytes_sent, + body = runscope_ctx.res_body, + timestamp = ngx_now(), + response_time = ngx.var.request_time * 1 + } + } +end + +return _M diff --git a/kong/plugins/runscope/handler.lua b/kong/plugins/runscope/handler.lua new file mode 100644 index 00000000000..0b6f7ce156d --- /dev/null +++ b/kong/plugins/runscope/handler.lua @@ -0,0 +1,74 @@ +local runscope_serializer = require "kong.plugins.log-serializers.runscope" +local BasePlugin = require "kong.plugins.base_plugin" +local log = require "kong.plugins.runscope.log" + +local ngx_log = ngx.log +local ngx_log_ERR = ngx.ERR +local string_find = string.find +local req_read_body = ngx.req.read_body +local req_get_headers = ngx.req.get_headers +local req_get_body_data = ngx.req.get_body_data +local req_get_post_args = ngx.req.get_post_args +local pcall = pcall + +local RunscopeLogHandler = BasePlugin:extend() + +function RunscopeLogHandler:new() + RunscopeLogHandler.super.new(self, "runscope") +end + +function RunscopeLogHandler:access(conf) + RunscopeLogHandler.super.access(self) + + local req_body, res_body = "", "" + local req_post_args = {} + + if conf.log_body then + req_read_body() + req_body = req_get_body_data() + + local headers = req_get_headers() + local content_type = headers["content-type"] + if content_type and string_find(content_type:lower(), "application/x-www-form-urlencoded", nil, true) then + local status, res = pcall(req_get_post_args) + if not status then + if res == "requesty body in temp file not supported" then + ngx_log(ngx_log_ERR, "[runscope] cannot read request body from temporary file. Try increasing the client_body_buffer_size directive.") + else + ngx_log(ngx_log_ERR, res) + end + else + req_post_args = res + end + end + end + + -- keep in memory the bodies for this request + ngx.ctx.runscope = { + req_body = req_body, + res_body = res_body, + req_post_args = req_post_args + } +end + +function RunscopeLogHandler:body_filter(conf) + RunscopeLogHandler.super.body_filter(self) + + if conf.log_body then + local chunk = ngx.arg[1] + local runscope_data = ngx.ctx.runscope or {res_body = ""} -- minimize the number of calls to ngx.ctx while fallbacking on default value + runscope_data.res_body = runscope_data.res_body..chunk + ngx.ctx.runscope = runscope_data + end +end + +function RunscopeLogHandler:log(conf) + RunscopeLogHandler.super.log(self) + + local message = runscope_serializer.serialize(ngx) + log.execute(conf, message) +end + +RunscopeLogHandler.PRIORITY = 1 + +return RunscopeLogHandler diff --git a/kong/plugins/runscope/log.lua b/kong/plugins/runscope/log.lua new file mode 100644 index 00000000000..6fd13dca59d --- /dev/null +++ b/kong/plugins/runscope/log.lua @@ -0,0 +1,94 @@ +local cjson = require "cjson" +local url = require "socket.url" + +local _M = {} + +local HTTPS = "https" +local ngx_log = ngx.log +local ngx_log_ERR = ngx.ERR +local ngx_timer_at = ngx.timer.at +local string_format = string.format +local string_len = string.len + +-- Generates http payload . +-- @param `method` http method to be used to send data +-- @param `parsed_url` contains the host details +-- @param `message` Message to be logged +-- @return `payload` http payload +local function generate_post_payload(parsed_url, access_token, message) + local body = cjson.encode(message) + local payload = string_format( + "%s %s HTTP/1.1\r\nHost: %s\r\nConnection: Keep-Alive\r\nAuthorization: Bearer %s\r\nContent-Type: application/json\r\nContent-Length: %s\r\n\r\n%s", + "POST", parsed_url.path, parsed_url.host, access_token, string_len(body), body) + return payload +end + +-- Parse host url +-- @param `url` host url +-- @return `parsed_url` a table with host details like domain name, port, path etc +local function parse_url(host_url) + local parsed_url = url.parse(host_url) + if not parsed_url.port then + if parsed_url.scheme == "http" then + parsed_url.port = 80 + elseif parsed_url.scheme == HTTPS then + parsed_url.port = 443 + end + end + if not parsed_url.path then + parsed_url.path = "/" + end + return parsed_url +end + +-- Log to a Http end point. +-- @param `premature` +-- @param `conf` Configuration table, holds http endpoint details +-- @param `message` Message to be logged +local function log(premature, conf, message) + if premature then + return + end + + local ok, err + local parsed_url = parse_url(conf.api_endpoint.."/buckets/"..conf.bucket_key.."/messages") + local access_token = conf.access_token + local host = parsed_url.host + local port = tonumber(parsed_url.port) + + local sock = ngx.socket.tcp() + sock:settimeout(conf.timeout) + + ok, err = sock:connect(host, port) + if not ok then + ngx_log(ngx_log_ERR, "[runscope] failed to connect to "..host..":"..tostring(port)..": ", err) + return + end + + if parsed_url.scheme == HTTPS then + local _, err = sock:sslhandshake(true, host, false) + if err then + ngx_log(ngx_log_ERR, "[runscope] failed to do SSL handshake with "..host..":"..tostring(port)..": ", err) + end + end + + ok, err = sock:send(generate_post_payload(parsed_url, access_token, message).."\r\n") + if not ok then + ngx_log(ngx_log_ERR, "[runscope] failed to send data to "..host..":"..tostring(port)..": ", err) + end + + ok, err = sock:setkeepalive(conf.keepalive) + if not ok then + ngx_log(ngx_log_ERR, "[runscope] failed to keepalive to "..host..":"..tostring(port)..": ", err) + return + end +end + +function _M.execute(conf, message) + local ok, err = ngx_timer_at(0, log, conf, message) + if not ok then + ngx_log(ngx_log_ERR, "[runscope] failed to create timer: ", err) + end +end + +return _M diff --git a/kong/plugins/runscope/schema.lua b/kong/plugins/runscope/schema.lua new file mode 100644 index 00000000000..87064372363 --- /dev/null +++ b/kong/plugins/runscope/schema.lua @@ -0,0 +1,10 @@ +return { + fields = { + api_endpoint = {required = true, type = "url", default = "https://api.runscope.com"}, + bucket_key = {required = true, type = "string"}, + access_token = {required = true, default = "", type = "string"}, + timeout = {default = 10000, type = "number"}, + keepalive = {default = 30, type = "number"}, + log_body = {default = false, type = "boolean"} + } +}