File: //opt/alt/luajit/share/lua/raven/init.lua
-- vim: st=4 sts=4 sw=4 et:
--- Main Sentry reporting module.
-- This module contains the core of the reporting logic, it still depends on a
-- network layer to actually send the data to the Sentry server.
--
-- @module raven
-- @copyright 2014-2017 CloudFlare, Inc.
-- @license BSD 3-clause (see LICENSE file)
local util = require 'raven.util'
local cjson = require 'cjson'
local _M = {}
_M._VERSION = util._VERSION
local debug_getinfo = debug.getinfo
local table_insert = table.insert
local unpack = unpack or table.unpack -- luacheck: ignore
local generate_event_id = util.generate_event_id
local iso8601 = util.iso8601
local json_encode = cjson.encode
local catcher_trace_level = 4
--- Table describing main Sentry client settings.
-- @field sender Object used to send message, see `rave.senders.*` modules to
-- find a sender suitable with your application
-- @field level Set the message level (string), defaults to `"error"`
-- @field logger Sets the message logger (string), defaults to `"root"`
-- @field tags Defaults tags for sent messages, defaults to `{}`. Example:
-- `{ "foo"="bar", ... }`
-- @field extra Default extra data sent with messages, defaults to `{}`
-- @table sentry_conf
local raven_mt = { }
raven_mt.__index = raven_mt
-- utility function to deal errors, xpcall and stack traces
-- This metatable is associated with returned error object so the original
-- error message is returned when tostring is invoked on it. This allows to use
-- the error objects for logging purposes as well. This is mostly a workaround
-- of xpcall error handlers having a single result.
local err_mt = {
__tostring = function(self)
return self.message
end,
}
-- return a string detailing the function running at a stack level
local function get_culprit(level)
local culprit
level = level + 1
local info = debug_getinfo(level, "Snl")
if info.name then
culprit = info.name
else
culprit = info.short_src .. ":" .. info.linedefined
end
return culprit
end
local function backtrace(level)
local frames = {}
level = level + 1
while true do
local info = debug_getinfo(level, "Snl")
if not info then
break
end
table_insert(frames, 1, {
filename = info.short_src,
["function"] = info.name,
lineno = info.currentline,
})
level = level + 1
end
return { frames = frames }
end
-- error_catcher: used to catch an error from xpcall and return a correct
-- error message
local function error_catcher(err)
return {
message = err,
culprit = get_culprit(catcher_trace_level),
exception = { {
value = err,
stacktrace = backtrace(catcher_trace_level),
} },
}
end
-- a wrapper around error_catcher that will return something even if
-- error_catcher itself crashes
local function capture_error_handler(err)
local ok, json_exception = pcall(error_catcher, err)
if not ok then
-- when failed, json_exception is error message
util.errlog('failed to run exception catcher: ' .. tostring(json_exception))
-- try to return something anyway (error message with no culprit and
-- no stacktrace
json_exception = {
message = err,
culprit = '???',
exception = { { value=err } },
}
end
return setmetatable(json_exception, err_mt)
end
_M.capture_error_handler = capture_error_handler
--- Create a new Sentry client.
-- It takes a @{sentry_conf} table tune its behavior.
-- @param conf client configuration.
-- @return a new raven instance
-- @usage
-- local raven = require "raven"
-- local rvn = raven.new {
-- sender = require("raven.senders.luasocket").new {
-- dsn = "http://pub:secret@127.0.0.1:8080/sentry/proj-id",
-- },
-- tags = { foo = "bar", abc = "def" },
-- logger = "foo",
-- }
function _M.new(conf)
local obj = {
sender = assert(conf.sender, "sender is required"),
level = conf.level or "error",
logger = conf.logger or "root",
tags = conf.tags or nil,
extra = conf.extra or nil,
}
return setmetatable(obj, raven_mt)
end
--- This method is reponsible to return the `server_name` field.
-- The default implementation just returns `"undefined"`, users are encouraged
-- to override this to something more sensible.
function _M.get_server_name()
return "undefined"
end
--- This table can be used to tune the message reporting.
-- @field tags Tags for the message, they will be coalesced with the ones
-- provided in the @{sentry_conf} table used in the constructor if any. In
-- case of conflict, the message tags have precedence.
--
-- @field extra Extra data for the message. Like tags, data is merged with
-- the extra data passed to the constructor.
--
-- @field trace_level Starting stack level for the report (can be used to skip
-- useless frames. The internal raven frames are automatically skipped, so a
-- level of `1` is means that the direct caller will be reported as culprit.
--
-- @table report_conf
--- A Raven client instance is responsible to collect events and send them
-- using the associated sender object.
-- @type Raven
--- Send an exception to Sentry.
-- See [reference](https://docs.sentry.io/clientdev/interfaces/exception/).
-- Note that the stack trace will be filled automatically.
--
-- Note that the `conf` table will be modified by the function, therefore it
-- is not safe to reuse conf table across calls. Consider passing common
-- attributes to the client constructor instead.
--
-- @function Raven:captureException
-- @param exception a table describing the exception conforming to the Sentry
-- format described in the reference docs
-- @param conf capture configuration table, see @{report_conf}
-- @return On success, return event id. If not success, return nil and
-- an error string.
-- @usage
-- local rvn = ravennew(...)
-- local exception = { { -- beware, exceptions are arrays
-- type = "SyntaxError",
-- value = "Wattttt!",
-- module = "mymodule",
-- -- stacktrace is populated automatically
-- } }
-- local id, err = rvn:captureException(exception,
-- { tags = { foo = "bar", abc = "def" }})
function raven_mt:captureException(exception, conf)
local trace_level
if not conf then
conf = { trace_level = 2 }
elseif not conf.trace_level then
conf.trace_level = 2
else
conf.trace_level = conf.trace_level + 1
end
trace_level = conf.trace_level
exception[1].stacktrace = backtrace(trace_level)
local payload = {
exception = exception,
message = exception[1].value,
culprit = get_culprit(trace_level),
}
-- because whether tail call will or will not appear in the stack back trace
-- is different between PUC-lua or LuaJIT, so just avoid tail call
local id, err = self:send_report(payload, conf)
return id, err
end
--- Send a message to Sentry.
-- See [reference](https://docs.sentry.io/clientdev/interfaces/message/).
--
-- Note that the `conf` table will be modified by the function, therefore it
-- is not safe to reuse conf table across calls. Consider passing common
-- attributes to the client constructor instead.
--
-- @function Raven:captureMessage
-- @param message the message, usually a raw string
-- @param conf capture configuration table, see @{report_conf}
-- @return On success, return event id. If not success, return nil and
-- an error string.
-- @usage
-- local rvn = ravennew(...)
-- local id, err = rvn:captureMessage("simple message",
-- { tags = { foo = "bar", abc = "def" }})
function raven_mt:captureMessage(message, conf)
if not conf then
conf = { trace_level = 2 }
elseif not conf.trace_level then
conf.trace_level = 2
else
conf.trace_level = conf.trace_level + 1
end
local payload = {
message = message,
culprit = get_culprit(conf.trace_level),
}
local id, err = self:send_report(payload, conf)
return id, err
end
--- Alias of @{Raven:captureException}.
-- @function Raven:capture_exception
raven_mt.capture_exception = raven_mt.captureException
--- Ailas of @{Raven:captureMessage}.
-- @function Raven:capture_message
raven_mt.capture_message = raven_mt.captureMessage
local function merge_tables(msg, root)
if not root then
return msg
elseif not msg then
return root
end
-- both table exist, merge root into msg
for k, v in pairs(root) do
msg[k] = msg[k] or v
end
return msg
end
--- Send directly a report to Sentry.
-- This is an internal function, you should not call it directly, use
-- @{Raven:captureException} or @{Raven:captureMessage} instead.
--
-- Note that the `conf` table will be modified by the function, therefore it
-- is not safe to reuse conf table across calls. Consider passing common
-- attributes to the client constructor instead.
--
-- @function Raven:send_report
-- @param json table to be sent. Don't need to fill `event_id`, `timestamp`,
-- `tags` and `level`.
-- @param conf capture configuration table, see @{report_conf}
-- @return On success, return event id. If not success, return nil and an
-- error string.
function raven_mt:send_report(json, conf)
local event_id = generate_event_id()
if not json then
json = self.json
if not json then
return
end
end
json.event_id = event_id
json.timestamp = iso8601()
json.level = self.level
json.platform = "lua"
json.logger = self.logger
if conf then
json.tags = merge_tables(conf.tags, self.tags)
json.extra = merge_tables(conf.extra, self.extra)
if conf.level then
json.level = conf.level
end
else
json.tags = self.tags
json.extra = self.extra
end
json.server_name = _M.get_server_name()
local json_str = json_encode(json)
local ok, err = self.sender:send(json_str)
if not ok then
util.errlog("Failed to send to Sentry: ", err, " ", json_str)
return nil, err
end
return json.event_id
end
-- the above two function used to be exposed as method, but there is no reason
-- to do so as they don't need self at the end.
-- Get culprit using given level
function raven_mt:get_culprit(level) -- luacheck: ignore self
return get_culprit(level)
end
-- catcher: used to catch an error from xpcall.
function raven_mt:catcher(err) -- luacheck: ignore self
return error_catcher(err)
end
--- Call given function and report any errors to Sentry.
-- @function Raven:call
-- @param f function to be called
-- @param ... function's arguments
-- @return the same as @{xpcall}
-- @usage
-- function func(a, b, c)
-- return a * b + c
-- end
-- return rvn:call(func, 1, 'foo', true)
function raven_mt:call(f, ...)
-- When used with ngx_lua, connecting a tcp socket in xpcall error handler
-- will cause a "yield across C-call boundary" error. To avoid this, we
-- move all the network operations outside of the xpcall error handler.
local res = { xpcall(f, capture_error_handler, ...) }
if not res[1] then
self:send_report(res[2])
res[2] = res[2].message -- turn the error object back to its initial form
end
return unpack(res)
end
--- Return a error handler function to be used with @{xpcall}
-- This is a high performance version of the @{call} method, but it demands
-- some more work to be used. It return a function that will turn a Lua error
-- into a Sentry exception table. It is meant to be used as a @{xpcall} error
-- hander.
--
-- The error handler returns that table, the should then be sent with the
-- @{send_report} method when appropriate. This cannot be easily done in the
-- error handler itself as Lua disallow yielding form an error handler.
--
-- It has better performance than @{call} because it avoid creating temporary
-- objects at each invocation.
--
-- Note that the error table generated by the handler has a `__tostring`
-- metamethod returning the original error message for logging purposes.
--
-- @function Raven:gen_capture_err
-- @usage
-- local handler = rvn:gen_capture_err()
-- local ok, err = xpcall(function() error("boom") end, handler)
-- if not ok then
-- rvn:send_report(err, { tags = { "foo"="bar" } })
-- end
function raven_mt:gen_capture_err() -- luacheck: ignore self
return capture_error_handler
end
return _M