Skip to content

Commit 42a61a5

Browse files
authored
Merge pull request #1 from rainest/feat/body-log
Body logging
2 parents 012849c + 5dde688 commit 42a61a5

File tree

7 files changed

+141
-16
lines changed

7 files changed

+141
-16
lines changed

README.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# kong-plugin-filter-log
22

33
This plugin is similar to the standard Kong HTTP Log plugin, but with the
4-
ability to filter request and response header values.
4+
ability to filter request and response values.
5+
6+
## Header filtering
57

68
Two new configuration options, `response_header_filters` and
7-
`request_header_filters`, are present. These are maps that accept header
9+
`request_header_filters`, add header-filtering capabilities. These are maps that accept header
810
name+filter regular expression pairs. For example:
911

1012
```
@@ -79,3 +81,20 @@ replaced with `XX REDACTED XX`:
7981
```
8082
{"latencies":{"request":218,"kong":43,"proxy":175},"service":{"host":"httpbin.org","created_at":1545243827,"connect_timeout":60000,"id":"537a12a9-3fcd-4a17-a09d-726eb86e860b","protocol":"http","name":"httpbin","read_timeout":60000,"port":80,"path":"\/","updated_at":1545243827,"retries":5,"write_timeout":60000},"request":{"querystring":{"X-Example-Response":"whatever"},"size":"307","uri":"\/logme\/response-headers?X-Example-Response=whatever","url":"http:\/\/localhost:8000\/logme\/response-headers?X-Example-Response=whatever","headers":{"host":"localhost:8000","x-example-request":"this is somXX REDACTED XXgnore it","accept-encoding":"gzip, deflate","user-agent":"HTTPie\/1.0.2","accept":"*\/*","x-example-other-request":"send mail to XX REDACTED XX please","connection":"keep-alive"},"method":"GET"},"client_ip":"10.0.2.2","api":{},"upstream_uri":"\/response-headers?X-Example-Response=whatever","response":{"headers":{"content-type":"application\/json","date":"Sat, 26 Jan 2019 01:14:17 GMT","connection":"close","access-control-allow-credentials":"true","content-length":"106","x-kong-proxy-latency":"42","server":"gunicorn\/19.9.0","x-kong-upstream-latency":"175","via":"kong\/0.34-1-enterprise-edition","access-control-allow-origin":"*","x-example-response":"XX REDACTED XXXX REDACTED XX"},"status":200,"size":"459"},"route":{"created_at":1545938532,"strip_path":true,"hosts":[],"preserve_host":false,"regex_priority":0,"updated_at":1545938532,"paths":["\/logme"],"service":{"id":"537a12a9-3fcd-4a17-a09d-726eb86e860b"},"methods":[],"protocols":["http","https"],"id":"58d48d2a-acf3-40f6-866b-1376a8e79934"},"started_at":1548465257244}
8183
```
84+
85+
## Body filtering
86+
87+
These plugins add the ability to log the request body and apply filters similar to the above. `body_filters` is an array of regular expression strings, which replace any matching content in the logged body with `XX REDACTED XX`.
88+
89+
There are several tuning options to control how and when the body is logged:
90+
91+
* `log_body` defaults to `false`, which will disable body logging altogether. Setting it to `true` will enable body logging.
92+
* `read_full_body` defaults to `false`, which will not attempt to read bodies larger than `client_body_buffer_size` at all. Setting it to `true` will read all request bodies, including those that have been buffered to disk.
93+
* `truncate_body` defaults to `true`, and truncates the body at either `body_size_limit` or `client_body_buffer_size` from kong.conf.
94+
* `body_size_limit` sets the size (in bytes) of body data that will be logged.
95+
96+
It is *highly* recommended that `read_full_body` is always set to `false`. When set to `true`, reading larger files requires reading from disk, which has serious performance implications within Lua code. Users can expect an order of magnitude increase in latency if this is enabled. It should ideally only be used temporarily with a very specific set of matching criteria (e.g. only for a single route/consumer combo).
97+
98+
If it is necessary to log larger bodies consistently, increasing `client_body_buffer_size` is preferable: it will lead to increased RAM usage, but does not incur disk read performance hits.
99+
100+
Note that `truncate_body` and `body_size_limit` do not have a meaningful effect on the performance hit incurred by disk reads. They do have some impact on redaction performance after, but the time needed for that is typically much smaller than disk read time. In general, their main usage is to limit the amount of data sent to a logging service.

kong-plugin-filter-log-0.1.0-1.rockspec

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package = "kong-plugin-filter-log"
2-
version = "0.1.0-1"
2+
version = "0.1.1-1"
33

44
supported_platforms = {"linux", "macosx"}
55
source = {
66
url = "git+https://github.com/rainest/kong-plugin-filter-log.git",
7-
tag = "0.1.0"
7+
tag = "0.1.1"
88
}
99

1010
description = {

kong/plugins/file-log-filtered/handler.lua

+40
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local ffi = require "ffi"
33
local cjson = require "cjson"
44
local system_constants = require "lua_system_constants"
55
local filtered_serializer = require "kong.plugins.log-serializers.filtered"
6+
local singletons = require "kong.singletons"
67
local BasePlugin = require "kong.plugins.base_plugin"
78

89
local ngx_timer = ngx.timer.at
@@ -21,6 +22,40 @@ ffi.cdef[[
2122
int write(int fd, const void * ptr, int numbytes);
2223
]]
2324

25+
-- Convert NGINX-style byte notation to raw byte count
26+
-- @param `bytestring` NGINX-style bytestring
27+
local function calculate_bytes(bytestring)
28+
local conversions = {k = 2^10, m = 2^20, g = 2^30}
29+
local suffix = string.sub(bytestring, -1)
30+
local count = tonumber(string.sub(bytestring, 1, -2))
31+
return count * conversions[suffix]
32+
end
33+
34+
-- Make the request body always available for later
35+
-- @param `conf` plugin configuration table
36+
local function access(conf)
37+
local body
38+
39+
local limit = conf.body_size_limit or calculate_bytes(singletons.configuration.client_body_buffer_size)
40+
41+
if conf.log_body then
42+
ngx.req.read_body()
43+
body = ngx.req.get_body_data()
44+
local body_filepath = ngx.req.get_body_file()
45+
if not body and body_filepath and conf.read_full_body then
46+
local file = io.open(body_filepath, "rb")
47+
if conf.truncate_body then
48+
body = file:read(limit)
49+
else
50+
body = file:read("*all")
51+
end
52+
file:close()
53+
end
54+
end
55+
56+
ngx.ctx.request_body = body
57+
end
58+
2459
-- fd tracking utility functions
2560
local file_descriptors = {}
2661

@@ -67,6 +102,11 @@ function FileLogFilteredHandler:new()
67102
FileLogFilteredHandler.super.new(self, "file-log")
68103
end
69104

105+
function FileLogFilteredHandler:access(conf)
106+
FileLogFilteredHandler.super.access(self)
107+
access(conf)
108+
end
109+
70110
function FileLogFilteredHandler:log(conf)
71111
FileLogFilteredHandler.super.log(self)
72112
local message = filtered_serializer.serialize(ngx, conf)

kong/plugins/file-log-filtered/schema.lua

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ return {
2424
keys = { type = "string" },
2525
values = { type = "string" }
2626
},
27+
body_filters = { type = "array", elements = { type = "string" } },
28+
log_body = { type = "boolean", default = false },
29+
truncate_body = { type = "boolean", default = true },
30+
read_full_body = { type = "boolean", default = false },
31+
body_size_limit = { type = "number" },
2732
reopen = { type = "boolean", default = false },
2833
}
2934
}

kong/plugins/http-log-filtered/handler.lua

+40
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
local filtered_serializer = require "kong.plugins.log-serializers.filtered"
2+
local singletons = require "kong.singletons"
23
local BasePlugin = require "kong.plugins.base_plugin"
34
local cjson = require "cjson"
45
local url = require "socket.url"
@@ -60,6 +61,40 @@ local function parse_url(host_url)
6061
return parsed_url
6162
end
6263

64+
-- Convert NGINX-style byte notation to raw byte count
65+
-- @param `bytestring` NGINX-style bytestring
66+
local function calculate_bytes(bytestring)
67+
local conversions = {k = 2^10, m = 2^20, g = 2^30}
68+
local suffix = string.sub(bytestring, -1)
69+
local count = tonumber(string.sub(bytestring, 1, -2))
70+
return count * conversions[suffix]
71+
end
72+
73+
-- Make the request body always available for later
74+
-- @param `conf` plugin configuration table
75+
local function access(conf)
76+
local body
77+
78+
local limit = conf.body_size_limit or calculate_bytes(singletons.configuration.client_body_buffer_size)
79+
80+
if conf.log_body then
81+
ngx.req.read_body()
82+
body = ngx.req.get_body_data()
83+
local body_filepath = ngx.req.get_body_file()
84+
if not body and body_filepath and conf.read_full_body then
85+
local file = io.open(body_filepath, "rb")
86+
if conf.truncate_body then
87+
body = file:read(limit)
88+
else
89+
body = file:read("*all")
90+
end
91+
file:close()
92+
end
93+
end
94+
95+
ngx.ctx.request_body = body
96+
end
97+
6398
-- Log to a Http end point.
6499
-- This basically is structured as a timer callback.
65100
-- @param `premature` see openresty ngx.timer.at function
@@ -118,6 +153,11 @@ function HttpLogFilteredHandler:serialize(ngx, conf)
118153
return cjson_encode(filtered_serializer.serialize(ngx, conf))
119154
end
120155

156+
function HttpLogFilteredHandler:access(conf)
157+
HttpLogFilteredHandler.super.access(self)
158+
access(conf)
159+
end
160+
121161
function HttpLogFilteredHandler:log(conf)
122162
HttpLogFilteredHandler.super.log(self)
123163

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,3 @@
1-
local function check_for_value(value)
2-
for i, entry in ipairs(value) do
3-
local ok = find(entry, ":")
4-
if not ok then
5-
return false, "key '" .. entry .. "' has no value"
6-
end
7-
end
8-
return true
9-
end
10-
111
return {
122
fields = {
133
http_endpoint = { required = true, type = "url" },
@@ -22,6 +12,11 @@ return {
2212
keys = { type = "string" },
2313
values = { type = "string" }
2414
},
15+
body_filters = { type = "array", elements = { type = "string" } },
16+
log_body = { type = "boolean", default = false },
17+
truncate_body = { type = "boolean", default = true },
18+
read_full_body = { type = "boolean", default = false },
19+
body_size_limit = { type = "number" },
2520
keepalive = { default = 60000, type = "number" }
2621
}
2722
}

kong/plugins/log-serializers/filtered.lua

+28-2
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,40 @@ local EMPTY = tablex.readonly({})
66

77
local function filter_headers(headers, filter_pairs)
88
if filter_pairs then
9+
local n
10+
local err
911
for header, regex in pairs(filter_pairs) do
1012
if headers[header] then
11-
headers[string.lower(header)] = ngx.re.gsub(headers[header], regex, "XX REDACTED XX")
13+
headers[string.lower(header)], n, err = ngx.re.gsub(headers[header], regex, "XX REDACTED XX")
14+
if err then
15+
ngx.log(ngx.ERR, header, " redaction regex \"", regex, "\" failed: ", err)
16+
headers[string.lower(header)] = ""
17+
end
1218
end
1319
end
1420
end
1521

1622
return headers
1723
end
1824

25+
local function filter_body(filters)
26+
local body = ngx.ctx.request_body or ""
27+
if filters and body ~= "" then
28+
local n
29+
local err
30+
for i, regex in pairs(filters) do
31+
body, n, err = ngx.re.gsub(body, regex, "XX REDACTED XX")
32+
if err then
33+
-- regex application failed, which may leave sensitive data unredacted
34+
-- best to simply discard everything
35+
ngx.log(ngx.ERR, "body redaction regex \"", regex, "\" failed: ", err)
36+
return ""
37+
end
38+
end
39+
end
40+
return body
41+
end
42+
1943
function _M.serialize(ngx, conf)
2044
local authenticated_entity
2145
if ngx.ctx.authenticated_credential ~= nil then
@@ -26,6 +50,7 @@ function _M.serialize(ngx, conf)
2650
end
2751

2852
local request_uri = ngx.var.request_uri or ""
53+
local body = filter_body(conf.body_filters)
2954

3055
return {
3156
request = {
@@ -34,7 +59,8 @@ function _M.serialize(ngx, conf)
3459
querystring = ngx.req.get_uri_args(), -- parameters, as a table
3560
method = ngx.req.get_method(), -- http method
3661
headers = filter_headers(ngx.req.get_headers(), conf.request_header_filters),
37-
size = ngx.var.request_length
62+
size = ngx.var.request_length,
63+
body = body
3864
},
3965
upstream_uri = ngx.var.upstream_uri,
4066
response = {

0 commit comments

Comments
 (0)