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 }; |