forked from circleci/rollcage
-
Notifications
You must be signed in to change notification settings - Fork 0
/
core.clj
270 lines (237 loc) · 9.51 KB
/
core.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
(ns circleci.rollcage.core
(:require
[clojure.string :as string]
[clojure.tools.logging :as logging]
[schema.core :as s]
[clj-http.client :refer (post)]
[clj-stacktrace.core :refer (parse-trace-elem)]
[clj-stacktrace.repl :refer (method-str)]
[circleci.rollcage.json :as json])
(:import
[java.net InetAddress UnknownHostException]
[java.util UUID]))
(def ^:private endpoint "https://api.rollbar.com/api/1/item/")
(def ^:private http-conn-timeout 3000)
(def ^:private http-socket-timeout 3000)
(def ^:private Client {:access-token (s/maybe String)
:result-fn clojure.lang.IFn
:send-fn clojure.lang.IFn
:data {:environment (s/maybe String)
:platform String
:language String
:framework String
:notifier {:name String}
:server {:host String
:root String
:code_version (s/maybe String)}}})
(defn- deep-merge
"Like merge, but merges maps recursively."
[& maps]
(apply merge-with deep-merge maps))
(def ^:private Item (deep-merge (dissoc Client :result-fn :send-fn)
{:data {:body {:trace_chain s/Any}
:level String
:timestamp s/Int
:uuid UUID
:custom s/Any ;; TODO verify custom
:request {:url (s/maybe String)}}}))
(defn- guess-os []
(System/getProperty "os.name"))
(defn- guess-hostname []
(first (filter (complement string/blank?)
[(System/getenv "HOSTNAME") ;; Unix
(System/getenv "COMPUTERNAME") ;; Windows
(try (.getHostName ^InetAddress (InetAddress/getLocalHost))
(catch UnknownHostException _ nil))])))
(defn- guess-file-root []
(System/getProperty "user.dir"))
(defn- rollbar-frame
"Convert a clj-stacktrace stack frame element to the format that the Rollbar
REST API expects."
[{:keys [file line] :as frame}]
{:filename file
:lineno line
:method (method-str frame)})
(defn- drop-common-head
"Return a vector containing a copy of ys with any common head with xs removed.
(drop-common-head [1 2 3 foo bar baz] [1 2 3 cat hat mat])
=> [cat hat mat]"
[xs ys]
(if (or (empty? xs)
(empty? ys)
(not= (first xs)
(first ys)))
ys
(recur (rest xs)
(rest ys))))
(defn- drop-common-substacks
"Remove the common substacks from trace so that each callstack in a chained
exceptions does not have the same 20 line prelude"
[trace]
(loop [head (first trace)
tail (rest trace)
result [head]]
(if (not-empty tail)
(let [cleaned (drop-common-head (:frames head)
(:frames (first tail)))]
(recur (first tail)
(rest tail)
(conj result (assoc (first tail) :frames cleaned))))
result)))
(defn- build-trace
"Given an Exception, create a sequence of callstacks with one for each
Exception in the cause-chain."
[^Throwable exception]
(drop-common-substacks
(loop [exception exception
result []]
(if (nil? exception) result
(let [elem {:frames (reverse (map (comp rollbar-frame parse-trace-elem)
(.getStackTrace exception)))
:exception {:class (-> exception class str)
:message (.getMessage exception)}}]
(recur (.getCause exception)
(conj result elem)))))))
(defn- ^int timestamp []
(int (/ (System/currentTimeMillis) 1000)))
(defn- ^UUID uuid []
(UUID/randomUUID))
(s/defn ^:private make-rollbar :- Item
"Build a map that matches the Rollbar API"
[client :- Client
level :- String
exception :- Throwable
url :- (s/maybe String)
custom :- (s/maybe {s/Any s/Any})]
;; TODO: Pass request parameters through to here
;; TODO: add person here
(-> client
(dissoc :result-fn :send-fn)
(assoc-in [:data :body :trace_chain] (build-trace exception))
(assoc-in [:data :level] level)
(assoc-in [:data :timestamp] (timestamp))
(assoc-in [:data :uuid] (uuid))
(assoc-in [:data :custom] custom)
(assoc-in [:data :request :url] url)))
(def ^:private rollbar-to-logging
"A look-up table to map from Rollbar severity levels to tools.logging levels"
{"critical" :fatal
"error" :error
"warning" :warn
"info" :info
"debug" :debug})
(defn- send-item-null
[^String endpoint ^Throwable exception {:keys [data] :as item}]
(logging/log (rollbar-to-logging (:level data))
exception
"No Rollbar token configured. Not reporting exception.")
{:err 0
:skipped true
:result {:uuid (str (:uuid data))}})
(defn- send-item-http
"Send a Rollbar item using the HTTP REST API.
Return the result JSON parsed as a Map"
[^String endpoint ^Throwable exception item]
(logging/log (rollbar-to-logging (get-in item [:data :level]))
exception
"Sending exception to Rollbar")
(let [result (post endpoint
{:body (json/encode item)
:conn-timeout http-conn-timeout
:socket-timeout http-socket-timeout
:content-type :json})]
(json/decode (:body result))))
(s/defn ^:private client* :- Client
[access-token :- (s/maybe String)
{:keys [os hostname environment code-version file-root result-fn]
:or {environment "production"}}]
(let [os (or os (guess-os))
hostname (or hostname (guess-hostname))
file-root (or file-root (guess-file-root))
result-fn (or result-fn (constantly nil))]
{:access-token access-token
:result-fn result-fn
:send-fn (if (string/blank? access-token)
send-item-null
send-item-http)
:data {:environment (name environment)
:platform (name os)
:language "Clojure"
:framework "Ring"
:notifier {:name "Rollcage"}
:server {:host hostname
:root file-root
:code_version code-version}}}))
(defn client
"Create a client that can can be passed used to send notifications to Rollbar.
The following options can be set:
:os
The name of the operating system running on the host. Defaults to the value
of the `os.name` system property.
:hostname
The hostname of the host.
:file-root
The path on disk where the filenames in stack traces are relative to. Defaults
the current working directory, as reported by the `user.dir` system property.
:environment
The environment that the app is running is, for example `staging` or `dev`.
Defaults to `production`.
:code-version
A string, up to 40 characters, describing the version of the application code
Rollbar understands these formats:
- semantic version (i.e. '2.1.12')
- integer (i.e. '45')
- git SHA (i.e. '3da541559918a808c2402bba5012f6c60b27661c')
There is no default value.
:result-fn
An function that will be called after each exception is sent to Rollbar.
The function will be passed 2 parameters:
- The Throwable that was being reported
- A map with the result of sending the exception to Rollbar. This map will
have the following keys:
:err - an integer, 1 if there was an error sending the exception to
Rollbar, 0 otherwise.
:message - A human-readable message describing the error.
See https://rollbar.com/docs/api/items_post/
More information on System Properties:
https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html"
([access-token]
(client access-token {}))
([access-token options]
(client* access-token options)))
(defn notify
([^String level client ^Throwable exception]
(notify level client exception {}))
([^String level {:keys [result-fn send-fn] :as client} ^Throwable exception {:keys [url params]}]
(let [log-level (rollbar-to-logging level)
params (merge params (ex-data exception))
item (make-rollbar client level exception url params)
result (try
(send-fn endpoint exception item)
(catch Exception e
;; Return an error that matches the shape of the Rollbar API
;; with an added :exception key
{:err 1
:exception e
:message (.getMessage e)}))]
(result-fn exception result)
result)))
(defn- report-uncaught-exception
[level client exception thread]
(notify level client exception
{:params {:thread-name (.getName thread)}}))
(defn setup-uncaught-exception-handler
"Setup handler to report all uncaught exceptions to rollbar, and optionally
to an additional handler."
([client] (setup-uncaught-exception-handler client (constantly nil)))
([client handler]
(Thread/setDefaultUncaughtExceptionHandler
(reify Thread$UncaughtExceptionHandler
(uncaughtException [_ thread ex]
(report-uncaught-exception "critical" client ex thread)
(handler thread ex))))))
(def critical (partial notify "critical"))
(def error (partial notify "error"))
(def warning (partial notify "warning"))
(def info (partial notify "info"))
(def debug (partial notify "debug"))