Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Policy: logging add custom access log options (JSON, Service vars, NGX vars) #1089

Merged
merged 11 commits into from
Jul 24, 2019
8 changes: 8 additions & 0 deletions gateway/http.d/apicast.conf.liquid
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
log_format time '[$time_local] $host:$server_port $remote_addr:$remote_port "$request" $status $body_bytes_sent ($request_time) $post_action_impact';

map $status $extended_access_log {
eloycoto marked this conversation as resolved.
Show resolved Hide resolved
default '';
}

log_format extended escape=none '$extended_access_log';

server {
listen {{ port.management | default: 8090 }};
server_name {{ server_name.management | default: 'management _' }};
Expand Down Expand Up @@ -45,6 +51,8 @@ server {

set $access_logs_enabled '1';
access_log {{ access_log_file | default: "/dev/stdout" }} time if=$access_logs_enabled;
set $extended_access_logs_enabled '0';
access_log {{ access_log_file | default: "/dev/stdout" }} extended if=$extended_access_logs_enabled;

{%- assign http_port = port.apicast | default: 8080 %}
{%- assign https_port = env.APICAST_HTTPS_PORT %}
Expand Down
103 changes: 102 additions & 1 deletion gateway/src/apicast/policy/logging/apicast-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,116 @@
"summary": "Controls logging.",
"description": [
"Controls logging. It allows to enable and disable access logs per ",
"service."
"service. Also it allows to have a custom access logs format per service"
],
"version": "builtin",
"configuration": {
"definitions": {
"value_type": {
"type": "string",
"oneOf": [
{
"enum": [
"plain"
],
"title": "Evaluate as plain text."
},
{
"enum": [
"liquid"
],
"title": "Evaluate as liquid."
}
]
}
},
"type": "object",
"properties": {
"enable_access_logs": {
"description": "Whether to enable access logs for the service",
"type": "boolean"
},
"custom_logging": {
"title": "Custom logging format",
"description": "A string variable that uses liquid templating to render a custom access log entry. All Nginx variables can be used plus per service entries",
"type": "string"
},
"enable_json_logs": {
"description": "To enable logs in json format. Custom logging format will be disabled",
"type": "boolean"
},
"json_object_config": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"description": "Key for the the json object",
"type": "string"
},
"value": {
"description": "String to get request information",
"type": "string"
},
"value_type": {
"description": "How to evaluate 'value' field",
"$ref": "#/definitions/value_type"
}
}
}
},
"condition": {
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"op": {
"description": "Match operation to compare match field with the provided value",
"type": "string",
"enum": [
"==",
"!=",
"matches"
]
},
"match": {
"description": "String to get request information to match",
"type": "string"
},
"match_type": {
"description": "How to evaluate 'match' value",
"$ref": "#/definitions/value_type"
},
"value": {
"description": "Value to compare the retrieved match",
"type": "string"
},
"value_type": {
"description": "How to evaluate 'value' field",
"$ref": "#/definitions/value_type"
}
},
"required": [
"op",
"match",
"match_type",
"value",
"value_type"
]
}
},
"combine_op": {
"type": "string",
"enum": [
"and",
"or"
],
"default": "and"
}
}
}
}
}
Expand Down
118 changes: 115 additions & 3 deletions gateway/src/apicast/policy/logging/logging.lua
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
--- Logging policy

local _M = require('apicast.policy').new('Logging Policy', 'builtin')

local new = _M.new

local default_enable_access_logs = true
local Condition = require('apicast.conditions.condition')
local LinkedList = require('apicast.linked_list')
local Operation = require('apicast.conditions.operation')
local TemplateString = require('apicast.template_string')
local cjson = require('cjson')

-- Defined in ngx.conf.liquid and used in the 'access_logs' directive.
local ngx_var_access_logs_enabled = 'access_logs_enabled'
local ngx_var_extended_access_logs_enabled = 'extended_access_logs_enabled'
local ngx_var_extended_access_log = 'extended_access_log'

local default_enable_access_logs = true
local default_template_type = 'plain'
local default_combine_op = "and"

-- Returns the value for the ngx var above from a boolean that indicates
-- whether access logs are enabled or not.
Expand All @@ -29,12 +38,115 @@ function _M.new(config)
end

self.enable_access_logs_val = val_for_ngx_var[enable_access_logs]
self.custom_logging = config.custom_logging
self.enable_json_logs = config.enable_json_logs
self.json_object_config = config.json_object_config or {}

self:load_condition(config)

return self
end

function _M:log()
function _M:load_condition(config)
if not config.condition then
return
end

ngx.log(ngx.DEBUG, 'Enabling extended log with conditions')
local operations = {}
for _, operation in ipairs(config.condition.operations) do
table.insert( operations,
Operation.new(
operation.match, operation.match_type,
operation.op,
operation.value, operation.value_type or default_template_type))
end
self.condition = Condition.new( operations, config.condition.combine_op or default_combine_op)
end

local function get_request_context(context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to define these headers here instead of reusing ngx_variable ?
I think everything on ngx_variable should be available. We should define there the response headers, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just because it is easy to provide to users information about the request and response in an easy way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this limits us to the information in the service plus the headers.
I guess it's fine for now but in the future, users might ask for other info that we have in ngx_variable like it happened for other policies.

local ctx = { }
ctx.req = {
headers=ngx.req.get_headers(),
}

ctx.resp = {
headers=ngx.resp.get_headers(),
}

ctx.service = context.service or {}
return LinkedList.readonly(ctx, ngx.var)
end

local function enable_extended_access_log()
ngx.var[ngx_var_extended_access_logs_enabled] = 1
end

local function disable_extended_access_log()
ngx.var[ngx_var_extended_access_logs_enabled] = 0
end

local function disable_default_access_logs()
ngx.var[ngx_var_access_logs_enabled] = 0
end

--- log_dump_json: returns an string with the json output.
local function log_dump_json(self, extended_context)
local result = {}
for _, value in ipairs(self.json_object_config) do
result[value.key] = TemplateString.new(value.value, value.value_type or default_template_type):render(extended_context)
end

local status, data = pcall(cjson.encode, result)
if not status then
ngx.log(ngx.WARN, "cannot serialize json on logging, err:", data)
-- Disable access log due to no valid information can be returned
disable_extended_access_log()
return ""
end

return data
end

-- log_dump_line: render the liquid custom_logging value and return it.
local function log_dump_line(self, extended_context)
local tmpl = TemplateString.new(self.custom_logging, "liquid")
return tmpl:render(extended_context)
end

-- get_log_line return the log line based on the kind of log defined in the
-- service, if Json is enabled will dump a json object, if not will render the
-- simple log line.
function _M:get_log_line(extended_context)
if self.enable_json_logs then
return log_dump_json(self, extended_context)
end
return log_dump_line(self, extended_context)
end


function _M:use_default_access_logs()
return not (self.custom_logging or self.enable_json_logs)
end

function _M:log(context)
ngx.var[ngx_var_access_logs_enabled] = self.enable_access_logs_val
if self:use_default_access_logs() then
return
end
-- Extended log is now enaled, disable the default access_log
disable_default_access_logs()

local extended_context = get_request_context(context or {})
if self.condition and not self.condition:evaluate(extended_context) then
-- Access log is disabled here, request does not match, so log is disabled
-- for this request
disable_extended_access_log()
return
end

enable_extended_access_log()
ngx.var[ngx_var_extended_access_log] = self:get_log_line(extended_context)
end

return _M
Loading