Skip to content

Commit

Permalink
feat(APIAnalytics) ALF serializer skeleton + tests
Browse files Browse the repository at this point in the history
- almost complete ALF serialization
- basic data push to apianalytics
- entries can be queued in the serializer, and flushed anytime
- serializer can output itself in JSON, being a valid ALF object
- more tests, with fixtures
- introduce serializers for logging plugins

Only a temporary solution before each serializer to be a module of its
own (maybe?) and each logging plugin to be independent from Kong, and
require serializers as dependencies. Following the discussion in #86
  • Loading branch information
thibaultcha committed Jun 2, 2015
1 parent f77bd4c commit ddd82ea
Show file tree
Hide file tree
Showing 18 changed files with 413 additions and 56 deletions.
7 changes: 6 additions & 1 deletion kong-0.3.0-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ build = {
["kong.plugins.keyauth.schema"] = "kong/plugins/keyauth/schema.lua",
["kong.plugins.keyauth.api"] = "kong/plugins/keyauth/api.lua",

["kong.plugins.log_serializers.basic"] = "kong/plugins/log_serializers/basic.lua",
["kong.plugins.log_serializers.alf"] = "kong/plugins/log_serializers/alf.lua",

["kong.plugins.tcplog.handler"] = "kong/plugins/tcplog/handler.lua",
["kong.plugins.tcplog.log"] = "kong/plugins/tcplog/log.lua",
["kong.plugins.tcplog.schema"] = "kong/plugins/tcplog/schema.lua",
Expand All @@ -112,10 +115,12 @@ build = {
["kong.plugins.httplog.schema"] = "kong/plugins/httplog/schema.lua",

["kong.plugins.filelog.handler"] = "kong/plugins/filelog/handler.lua",
["kong.plugins.filelog.log"] = "kong/plugins/filelog/log.lua",
["kong.plugins.filelog.schema"] = "kong/plugins/filelog/schema.lua",
["kong.plugins.filelog.fd_util"] = "kong/plugins/filelog/fd_util.lua",

["kong.plugins.apianalytics.handler"] = "kong/plugins/apianalytics/handler.lua",
["kong.plugins.apianalytics.schema"] = "kong/plugins/apianalytics/schema.lua",

["kong.plugins.ratelimiting.handler"] = "kong/plugins/ratelimiting/handler.lua",
["kong.plugins.ratelimiting.access"] = "kong/plugins/ratelimiting/access.lua",
["kong.plugins.ratelimiting.schema"] = "kong/plugins/ratelimiting/schema.lua",
Expand Down
1 change: 1 addition & 0 deletions kong.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins_available:
- httplog
- cors
- request_transformer
- apianalytics

## The Kong working directory
## (Make sure you have read and write permissions)
Expand Down
47 changes: 12 additions & 35 deletions kong/kong.lua
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ function _M.exec_plugins_access()
end
end

local conf = ngx.ctx.plugin_conf[plugin.name]
if not ngx.ctx.stop_phases and (plugin.resolver or conf) then
plugin.handler:access(conf and conf.value or nil)
local plugin_conf = ngx.ctx.plugin_conf[plugin.name]
if not ngx.ctx.stop_phases and (plugin.resolver or plugin_conf) then
plugin.handler:access(plugin_conf and plugin_conf.value or nil)
end
end

Expand All @@ -211,9 +211,9 @@ function _M.exec_plugins_header_filter()
ngx.header["Via"] = constants.NAME.."/"..constants.VERSION

for _, plugin in ipairs(plugins) do
local conf = ngx.ctx.plugin_conf[plugin.name]
if conf then
plugin.handler:header_filter(conf.value)
local plugin_conf = ngx.ctx.plugin_conf[plugin.name]
if plugin_conf then
plugin.handler:header_filter(plugin_conf.value)
end
end
end
Expand All @@ -223,9 +223,9 @@ end
function _M.exec_plugins_body_filter()
if not ngx.ctx.stop_phases then
for _, plugin in ipairs(plugins) do
local conf = ngx.ctx.plugin_conf[plugin.name]
if conf then
plugin.handler:body_filter(conf.value)
local plugin_conf = ngx.ctx.plugin_conf[plugin.name]
if plugin_conf then
plugin.handler:body_filter(plugin_conf.value)
end
end
end
Expand All @@ -234,33 +234,10 @@ end
-- Calls log() on every loaded plugin
function _M.exec_plugins_log()
if not ngx.ctx.stop_phases then
-- Creating the log variable that will be serialized
local message = {
request = {
uri = ngx.var.request_uri,
request_uri = ngx.var.scheme.."://"..ngx.var.host..":"..ngx.var.server_port..ngx.var.request_uri,
querystring = ngx.req.get_uri_args(), -- parameters, as a table
method = ngx.req.get_method(), -- http method
headers = ngx.req.get_headers(),
size = ngx.var.request_length
},
response = {
status = ngx.status,
headers = ngx.resp.get_headers(),
size = ngx.var.bytes_sent
},
authenticated_entity = ngx.ctx.authenticated_entity,
api = ngx.ctx.api,
client_ip = ngx.var.remote_addr,
started_at = ngx.req.start_time() * 1000
}

ngx.ctx.log_message = message

for _, plugin in ipairs(plugins) do
local conf = ngx.ctx.plugin_conf[plugin.name]
if conf then
plugin.handler:log(conf.value)
local plugin_conf = ngx.ctx.plugin_conf[plugin.name]
if plugin_conf then
plugin.handler:log(plugin_conf.value)
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions kong/plugins/apianalytics/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
local http = require "socket.http"
local ltn12 = require "ltn12"
local BasePlugin = require "kong.plugins.base_plugin"
local ALFSerializer = require "kong.plugins.log_serializers.alf"

local http_client = require "kong.tools.http_client"

--local SERVER_URL = "http://socket.apianalytics.com/"
local SERVER_URL = "http://localhost:58000/alf_1.0.0"

local APIAnalyticsHandler = BasePlugin:extend()

function APIAnalyticsHandler:new()
APIAnalyticsHandler.super.new(self, "apianalytics")
end

function APIAnalyticsHandler:access(conf)
APIAnalyticsHandler.super.access(self)

ngx.req.read_body()
ngx.ctx.req_body = ngx.req.get_body_data()
ngx.ctx.res_body = ""
end

function APIAnalyticsHandler:body_filter(conf)
APIAnalyticsHandler.super.body_filter(self)

-- concatenate response chunks for response.content.text
local chunk = ngx.arg[1]
ngx.ctx.res_body = ngx.ctx.res_body..chunk
end

function APIAnalyticsHandler:log(conf)
APIAnalyticsHandler.super.log(self)

ALFSerializer:add_entry(ngx)

-- if queue is full
local message = ALFSerializer:to_json_string("54d2b98ee0d5076065fd6f93")
print("MESSAGE: "..message)

-- TODO: use the cosocket API
local response, status, headers = http_client.post(SERVER_URL, message,
{
["content-length"] = string.len(message),
["content-type"] = "application/json"
})

print("STATUS: "..status)
if status ~= 200 then
ngx.log(ngx.ERR, "Could not send entry to "..SERVER_URL)
print("RESPONSE IS: "..response)
end

-- todo: flush
end

return APIAnalyticsHandler
1 change: 1 addition & 0 deletions kong/plugins/apianalytics/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return {}
7 changes: 5 additions & 2 deletions kong/plugins/filelog/handler.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
-- Copyright (C) Mashape, Inc.

local basic_serializer = require "kong.plugins.log_serializers.basic"
local BasePlugin = require "kong.plugins.base_plugin"
local log = require "kong.plugins.filelog.log"
local cjson = require "cjson"

local FileLogHandler = BasePlugin:extend()

Expand All @@ -11,7 +12,9 @@ end

function FileLogHandler:log(conf)
FileLogHandler.super.log(self)
log.execute(conf)

local message = basic_serializer.serialize(ngx)
ngx.log(ngx.INFO, cjson.encode(message))
end

return FileLogHandler
10 changes: 7 additions & 3 deletions kong/plugins/filelog/log.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
-- Copyright (C) Mashape, Inc.
local cjson = require "cjson"

local ffi = require "ffi"
local cjson = require "cjson"
local fd_util = require "kong.plugins.filelog.fd_util"
local basic_serializer = require "kong.plugins.log_serializers.basic"

ffi.cdef[[
typedef struct {
Expand All @@ -24,7 +26,7 @@ int fprintf(FILE *stream, const char *format, ...);
-- @param `conf` Configuration table, holds http endpoint details
-- @param `message` Message to be logged
local function log(premature, conf, message)
local message = cjson.encode(message).."\n"
message = cjson.encode(message).."\n"

local f = fd_util.get_fd()
if not f then
Expand All @@ -39,7 +41,9 @@ end
local _M = {}

function _M.execute(conf)
local ok, err = ngx.timer.at(0, log, conf, ngx.ctx.log_message)
local message = basic_serializer.serialize(ngx)

local ok, err = ngx.timer.at(0, log, conf, message)
if not ok then
ngx.log(ngx.ERR, "[filelog] failed to create timer: ", err)
end
Expand Down
136 changes: 136 additions & 0 deletions kong/plugins/log_serializers/alf.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
-- ALF serializer module.
-- ALF is the format supported by API Analytics (http://apianalytics.com)
--
-- - ALF specifications: https://github.com/Mashape/api-log-format
-- - Nginx lua module documentation: http://wiki.nginx.org/HttpLuaModule
-- - ngx_http_core_module: http://wiki.nginx.org/HttpCoreModule#.24http_HEADER

local EMPTY_ARRAY_PLACEHOLDER = "__empty_array_placeholder__"

local alf_mt = {}
alf_mt.__index = alf_mt

local ALF = {
version = "1.0.0",
serviceToken = "",
har = {
log = {
version = "1.2",
creator = {
name = "kong-api-analytics-plugin",
version = "0.1"
},
entries = {}
}
}
}

-- Transform a key/value lua table into an array of elements with `name`, `value`
-- Since Lua won't recognize {} as an empty array but an empty object, we need to force it
-- to be an array, hence we will do "[__empty_array_placeholder__]".
-- Then once the ALF will be stringified, we will remove the placeholder so the only left element will be "[]".
-- @param hash key/value dictionary to serialize
-- @return an array, or nil
local function dic_to_array(hash)
local arr = {}
for k, v in pairs(hash) do
table.insert(arr, { name = k, value = v })
end

if #arr > 0 then
return arr
else
return {EMPTY_ARRAY_PLACEHOLDER}
end
end

-- Serialize into one ALF entry
-- For performance reasons, it tries to use the NGINX Lua API
-- instead of ngx_http_core_module when possible.
function alf_mt:serialize_entry(ngx)
-- Extracted data
local req_headers = ngx.req.get_headers()
local res_headers = ngx.resp.get_headers()

local req_body = ngx.ctx.req_body
local res_body = ngx.ctx.res_body

-- ALF format
local alf_req_mimeType = req_headers["Content-Type"] and req_headers["Content-Type"] or "application/octet-stream"
local alf_res_mimeType = res_headers["Content-Type"] and res_headers["Content-Type"] or "application/octet-stream"
local alf_req_bodySize = req_headers["Content-Length"] and req_headers["Content-Length"] or 0
local alf_res_bodySize = res_headers["Content-Length"] and res_headers["Content-Length"] or 0

return {
startedDateTime = os.date("!%Y-%m-%dT%TZ", ngx.req.start_time()),
clientIPAddress = ngx.var.remote_addr,
time = 3,
-- REQUEST
request = {
method = ngx.req.get_method(),
url = ngx.var.scheme.."://"..ngx.var.host..ngx.var.uri,
httpVersion = "HTTP/"..ngx.req.http_version(),
queryString = dic_to_array(ngx.req.get_uri_args()),
headers = dic_to_array(req_headers),
headersSize = 10,
cookies = {EMPTY_ARRAY_PLACEHOLDER},
bodySize = tonumber(alf_req_bodySize),
content = {
size = tonumber(ngx.var.request_length),
mimeType = alf_req_mimeType,
text = req_body and req_body or ""
}
},
-- RESPONSE
response = {
status = ngx.status,
statusText = "",
httpVersion = "",
headers = dic_to_array(res_headers),
headersSize = 10,
cookies = {EMPTY_ARRAY_PLACEHOLDER},
bodySize = tonumber(alf_res_bodySize),
redirectURL = "",
content = {
size = tonumber(ngx.var.bytes_sent),
mimeType = alf_res_mimeType,
text = res_body and res_body or ""
}
},
cache = {},
-- TIMINGS
timings = {
send = 1,
wait = 1,
receive = 1,
blocked = 0,
connect = 0,
dns = 0,
ssl = 0
}
} -- end of entry
end

function alf_mt:add_entry(ngx)
table.insert(self.har.log.entries, self:serialize_entry(ngx))
end

function alf_mt:to_json_string(token)
if not token then
error("API Analytics serviceToken required", 2)
end

local cjson = require "cjson"

-- inject token
self.serviceToken = token

local str = cjson.encode(self)
return str:gsub("\""..EMPTY_ARRAY_PLACEHOLDER.."\"", ""):gsub("\\/", "/")
end

function alf_mt:flush_entries()
self.har.log.entries = {}
end

return setmetatable(ALF, alf_mt)
24 changes: 24 additions & 0 deletions kong/plugins/log_serializers/basic.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
local _M = {}

function _M.serialize(ngx)
return {
uri = ngx.var.request_uri,
request_uri = ngx.var.scheme.."://"..ngx.var.host..":"..ngx.var.server_port..ngx.var.request_uri,
querystring = ngx.req.get_uri_args(), -- parameters, as a table
method = ngx.req.get_method(),
headers = ngx.req.get_headers(),
size = ngx.var.request_length
},
response = {
status = ngx.status,
headers = ngx.resp.get_headers(),
size = ngx.var.bytes_sent
},
authenticated_entity = ngx.ctx.authenticated_entity,
api = ngx.ctx.api,
client_ip = ngx.var.remote_addr,
started_at = ngx.req.start_time() * 1000
}
end

return _M
Loading

0 comments on commit ddd82ea

Please sign in to comment.