Mercurial > prosody-modules
comparison mod_sentry/sentry.lib.lua @ 4283:2ae71126e379
mod_sentry: New module to forward errors to a Sentry server
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Tue, 08 Dec 2020 15:34:53 +0000 |
| parents | |
| children | b7045af1e5b7 |
comparison
equal
deleted
inserted
replaced
| 4282:281a864e7472 | 4283:2ae71126e379 |
|---|---|
| 1 local hex = require "util.hex"; | |
| 2 local random = require "util.random"; | |
| 3 local url = require "socket.url"; | |
| 4 local datetime = require "util.datetime".datetime; | |
| 5 local http = require 'net.http' | |
| 6 local json = require "util.json"; | |
| 7 local errors = require "util.error"; | |
| 8 local promise = require "util.promise"; | |
| 9 | |
| 10 local unpack = unpack or table.unpack -- luacheck: ignore | |
| 11 | |
| 12 local user_agent = ("prosody-mod-%s/%s"):format((module.name:gsub("%W", "-")), (prosody.version:gsub("[^%w.-]", "-"))); | |
| 13 | |
| 14 local function generate_event_id() | |
| 15 return hex.to(random.bytes(16)); | |
| 16 end | |
| 17 | |
| 18 local function get_endpoint(server, name) | |
| 19 return ("%s/api/%d/%s/"):format(server.base_uri, server.project_id, name); | |
| 20 end | |
| 21 | |
| 22 -- Parse a DSN string | |
| 23 -- https://develop.sentry.dev/sdk/overview/#parsing-the-dsn | |
| 24 local function parse_dsn(dsn_string) | |
| 25 local parsed = url.parse(dsn_string); | |
| 26 if not parsed then | |
| 27 return nil, "unable to parse dsn (url)"; | |
| 28 end | |
| 29 local path, project_id = parsed.path:match("^(.*)/(%d+)$"); | |
| 30 if not path then | |
| 31 return nil, "unable to parse dsn (path)"; | |
| 32 end | |
| 33 local base_uri = url.build({ | |
| 34 scheme = parsed.scheme; | |
| 35 host = parsed.host; | |
| 36 port = parsed.port; | |
| 37 path = path; | |
| 38 }); | |
| 39 return { | |
| 40 base_uri = base_uri; | |
| 41 public_key = parsed.user; | |
| 42 project_id = project_id; | |
| 43 }; | |
| 44 end | |
| 45 | |
| 46 local function get_error_data(instance_id, context) | |
| 47 local data = { | |
| 48 instance_id = instance_id; | |
| 49 }; | |
| 50 for k, v in pairs(context) do | |
| 51 data[k] = tostring(v); | |
| 52 end | |
| 53 return data; | |
| 54 end | |
| 55 | |
| 56 local function error_to_sentry_exception(e) | |
| 57 return { | |
| 58 type = e.condition or (e.code and tostring(e.code)) or nil; | |
| 59 value = e.text or tostring(e); | |
| 60 context = e.source; | |
| 61 mechanism = { | |
| 62 type = "generic"; | |
| 63 description = "Prosody error object"; | |
| 64 synthetic = not not e.context.wrapped_error; | |
| 65 data = get_error_data(e.instance_id, e.context); | |
| 66 }; | |
| 67 }; | |
| 68 end | |
| 69 | |
| 70 local sentry_event_methods = {}; | |
| 71 local sentry_event_mt = { __index = sentry_event_methods }; | |
| 72 | |
| 73 function sentry_event_methods:set(key, value) | |
| 74 self.event[key] = value; | |
| 75 return self; | |
| 76 end | |
| 77 | |
| 78 function sentry_event_methods:tag(tag_name, tag_value) | |
| 79 local tags = self.event.tags; | |
| 80 if not tags then | |
| 81 tags = {}; | |
| 82 self.event.tags = tags; | |
| 83 end | |
| 84 if type(tag_name) == "string" then | |
| 85 tags[tag_name] = tag_value; | |
| 86 else | |
| 87 for k, v in pairs(tag_name) do | |
| 88 tags[k] = v; | |
| 89 end | |
| 90 end | |
| 91 return self; | |
| 92 end | |
| 93 | |
| 94 function sentry_event_methods:extra(key, value) | |
| 95 local extra = self.event.extra; | |
| 96 if not extra then | |
| 97 extra = {}; | |
| 98 self.event.extra = extra; | |
| 99 end | |
| 100 if type(key) == "string" then | |
| 101 extra[key] = tostring(value); | |
| 102 else | |
| 103 for k, v in pairs(key) do | |
| 104 extra[k] = tostring(v); | |
| 105 end | |
| 106 end | |
| 107 return self; | |
| 108 end | |
| 109 | |
| 110 function sentry_event_methods:message(text) | |
| 111 return self:set("message", { formatted = text }); | |
| 112 end | |
| 113 | |
| 114 function sentry_event_methods:add_exception(e) | |
| 115 if errors.is_error(e) then | |
| 116 e = error_to_sentry_exception(e); | |
| 117 elseif type(e) ~= "table" or not (e.type and e.value) then | |
| 118 e = error_to_sentry_exception(errors.coerce(nil, e)); | |
| 119 end | |
| 120 | |
| 121 local exception = self.event.exception; | |
| 122 if not exception or not exception.values then | |
| 123 exception = { values = {} }; | |
| 124 self.event.exception = exception; | |
| 125 end | |
| 126 | |
| 127 table.insert(exception.values, e); | |
| 128 | |
| 129 return self; | |
| 130 end | |
| 131 | |
| 132 function sentry_event_methods:add_breadcrumb(crumb_timestamp, crumb_type, crumb_category, message, data) | |
| 133 local crumbs = self.event.breadcrumbs; | |
| 134 if not crumbs then | |
| 135 crumbs = { values = {} }; | |
| 136 self.event.breadcrumbs = crumbs; | |
| 137 end | |
| 138 | |
| 139 local crumb = { | |
| 140 timestamp = crumb_timestamp and datetime(crumb_timestamp) or self.timestamp; | |
| 141 type = crumb_type; | |
| 142 category = crumb_category; | |
| 143 message = message; | |
| 144 data = data; | |
| 145 }; | |
| 146 table.insert(crumbs.values, crumb); | |
| 147 return self; | |
| 148 end | |
| 149 | |
| 150 function sentry_event_methods:add_http_request_breadcrumb(http_request, message) | |
| 151 local request_id_message = ("[Request %s]"):format(http_request.id); | |
| 152 message = message and (request_id_message.." "..message) or request_id_message; | |
| 153 return self:add_breadcrumb(http_request.time, "http", "net.http", message, { | |
| 154 url = http_request.url; | |
| 155 method = http_request.method or "GET"; | |
| 156 status_code = http_request.response and http_request.response.code or nil; | |
| 157 }); | |
| 158 end | |
| 159 | |
| 160 function sentry_event_methods:set_request(http_request) | |
| 161 return self:set("request", { | |
| 162 method = http_request.method; | |
| 163 url = url.build(http_request.url); | |
| 164 headers = http_request.headers; | |
| 165 env = { | |
| 166 REMOTE_ADDR = http_request.ip; | |
| 167 }; | |
| 168 }); | |
| 169 end | |
| 170 | |
| 171 function sentry_event_methods:send() | |
| 172 return self.server:send(self.event); | |
| 173 end | |
| 174 | |
| 175 local sentry_mt = { } | |
| 176 sentry_mt.__index = sentry_mt | |
| 177 | |
| 178 local function new(conf) | |
| 179 local server = assert(parse_dsn(conf.dsn)); | |
| 180 return setmetatable({ | |
| 181 server = server; | |
| 182 endpoints = { | |
| 183 store = get_endpoint(server, "store"); | |
| 184 }; | |
| 185 insecure = conf.insecure; | |
| 186 tags = conf.tags or nil, | |
| 187 extra = conf.extra or nil, | |
| 188 server_name = conf.server_name or "undefined"; | |
| 189 logger = conf.logger; | |
| 190 }, sentry_mt); | |
| 191 end | |
| 192 | |
| 193 local function resolve_sentry_response(response) | |
| 194 if response.code == 200 and response.body then | |
| 195 local data = json.decode(response.body); | |
| 196 return data; | |
| 197 end | |
| 198 return promise.reject(response); | |
| 199 end | |
| 200 | |
| 201 function sentry_mt:send(event) | |
| 202 local json_payload = json.encode(event); | |
| 203 local response_promise, err = self:_request(self.endpoints.store, "application/json", json_payload); | |
| 204 | |
| 205 if not response_promise then | |
| 206 module:log("warn", "Failed to submit to Sentry: %s %s", err, json); | |
| 207 return nil, err; | |
| 208 end | |
| 209 | |
| 210 return response_promise:next(resolve_sentry_response), event.event_id; | |
| 211 end | |
| 212 | |
| 213 function sentry_mt:_request(endpoint_url, body_type, body) | |
| 214 local auth_header = ("Sentry sentry_version=7, sentry_client=%s, sentry_timestamp=%s, sentry_key=%s") | |
| 215 :format(user_agent, datetime(), self.server.public_key); | |
| 216 | |
| 217 return http.request(endpoint_url, { | |
| 218 headers = { | |
| 219 ["X-Sentry-Auth"] = auth_header; | |
| 220 ["Content-Type"] = body_type; | |
| 221 ["User-Agent"] = user_agent; | |
| 222 }; | |
| 223 insecure = self.insecure; | |
| 224 body = body; | |
| 225 }); | |
| 226 end | |
| 227 | |
| 228 function sentry_mt:event(level, source) | |
| 229 local event = setmetatable({ | |
| 230 server = self; | |
| 231 event = { | |
| 232 event_id = generate_event_id(); | |
| 233 timestamp = datetime(); | |
| 234 platform = "lua"; | |
| 235 server_name = self.server_name; | |
| 236 logger = source or self.logger; | |
| 237 level = level; | |
| 238 }; | |
| 239 }, sentry_event_mt); | |
| 240 if self.tags then | |
| 241 event:tag(self.tags); | |
| 242 end | |
| 243 if self.extra then | |
| 244 event:extra(self.extra); | |
| 245 end | |
| 246 return event; | |
| 247 end | |
| 248 | |
| 249 return { | |
| 250 new = new; | |
| 251 }; |
