Skip to content

Commit 1207566

Browse files
authored
Merge pull request #28 from clojure-lsp/improve-traces
Improve tracing
2 parents 8aa121d + 718b834 commit 1207566

File tree

5 files changed

+239
-71
lines changed

5 files changed

+239
-71
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Let language servers pick detail of traces, by setting `:trace-level`. #27
6+
- Let language servers set `:trace-level` on running lsp4clj server. #27
7+
58
## v1.3.1
69

710
## v1.3.0

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,21 @@ This will start listening on the provided port, blocking until a client makes a
111111

112112
### Tracing
113113

114-
As you are implementing, you may want to trace incoming and outgoing messages. Initialize the server with `:trace? true` and then read traces (two element vectors, beginning with the log level `:debug` and ending with a string, the trace itself) off its `:trace-ch`.
114+
As you are implementing, you may want to trace incoming and outgoing messages. Initialize the server with `:trace-level "verbose"` and then read traces (two element vectors, beginning with the log level `:debug` and ending with a string, the trace itself) off its `:trace-ch`.
115115

116116
```clojure
117-
(let [server (lsp4clj.io-server/stdio-server {:trace? true})]
117+
(let [server (lsp4clj.io-server/stdio-server {:trace-level "verbose"})]
118118
(async/go-loop []
119119
(when-let [[level trace] (async/<! (:trace-ch server))]
120120
(logger/log level trace)
121121
(recur)))
122122
(lsp4clj.server/start server context))
123123
```
124124

125+
`:trace-level` can be set to `"off"` (no tracing), `"messages"` (to show just the message time, method, id and direction), or `"verbose"` (to also show details of the message body).
126+
127+
The trace level can be changed during the life of a server by calling, for example, `(ls4clj.server/set-trace-level server "messages")`. This can be used to respect a trace level received at runtime, either in an [initialize](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeParams) request or a [$/setTrace](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#setTrace) notification.
128+
125129
### Testing
126130

127131
A client is in many ways like a serverit also sends and receives requests and notifications and receives responses. That is, LSP uses JSON-RPC as a bi-directional protocol. As such, you may be able to use some of lsp4clj's tools to build a mock client for testing. See `integration.client` in `clojure-lsp` for one such example.

src/lsp4clj/server.clj

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
[lsp4clj.protocols.endpoint :as protocols.endpoint]
1010
[lsp4clj.trace :as trace]
1111
[promesa.core :as p])
12-
(:import (java.util.concurrent CancellationException)))
12+
(:import
13+
(java.util.concurrent CancellationException)))
1314

1415
(set! *warn-on-reflection* true)
1516

@@ -154,15 +155,20 @@
154155
message-details)]
155156
(lsp.responses/error resp error-body)))
156157

158+
(defn trace [{:keys [tracer* trace-ch]} trace-f & params]
159+
(when-let [trace-body (apply trace-f @tracer* params)]
160+
(async/put! trace-ch [:debug trace-body])))
161+
157162
;; TODO: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
158163
;; * receive-request should return error until initialize request is received
159164
;; * receive-notification should drop until initialize request is received, with the exception of exit
160165
;; * send-request should do nothing until initialize response is sent, with the exception of window/showMessageRequest
161166
;; * send-notification should do nothing until initialize response is sent, with the exception of window/showMessage, window/logMessage, telemetry/event, and $/progress
162167
(defrecord ChanServer [input-ch
163168
output-ch
164-
trace-ch
165169
log-ch
170+
trace-ch
171+
tracer*
166172
^java.time.Clock clock
167173
on-close
168174
request-id*
@@ -207,32 +213,32 @@
207213
now (.instant clock)
208214
req (lsp.requests/request id method body)
209215
pending-request (pending-request id method now this)]
210-
(some-> trace-ch (async/put! (trace/sending-request req now)))
216+
(trace this trace/sending-request req now)
211217
;; Important: record request before sending it, so it is sure to be
212218
;; available during receive-response.
213219
(swap! pending-sent-requests* assoc id pending-request)
214220
;; respect back pressure from clients that are slow to read; (go (>!)) will not suffice
215221
(async/>!! output-ch req)
216222
pending-request))
217-
(send-notification [_this method body]
223+
(send-notification [this method body]
218224
(let [now (.instant clock)
219225
notif (lsp.requests/notification method body)]
220-
(some-> trace-ch (async/put! (trace/sending-notification notif now)))
226+
(trace this trace/sending-notification notif now)
221227
;; respect back pressure from clients that are slow to read; (go (>!)) will not suffice
222228
(async/>!! output-ch notif)))
223-
(receive-response [_this {:keys [id error result] :as resp}]
229+
(receive-response [this {:keys [id error result] :as resp}]
224230
(let [now (.instant clock)
225231
[pending-requests _] (swap-vals! pending-sent-requests* dissoc id)]
226232
(if-let [{:keys [p started] :as req} (get pending-requests id)]
227233
(do
228-
(some-> trace-ch (async/put! (trace/received-response req resp started now)))
234+
(trace this trace/received-response req resp started now)
229235
(deliver p (if error resp result)))
230-
(some-> trace-ch (async/put! (trace/received-unmatched-response resp now))))))
236+
(trace this trace/received-unmatched-response resp now))))
231237
(receive-request [this context {:keys [id method params] :as req}]
232238
(let [started (.instant clock)
233239
resp (lsp.responses/response id)]
234240
(try
235-
(some-> trace-ch (async/put! (trace/received-request req started)))
241+
(trace this trace/received-request req started)
236242
;; coerce result/error to promise
237243
(let [result-promise (p/promise (receive-request method context params))]
238244
(swap! pending-received-requests* assoc id result-promise)
@@ -258,34 +264,44 @@
258264
(p/finally
259265
(fn [resp _error]
260266
(swap! pending-received-requests* dissoc id)
261-
(some-> trace-ch (async/put! (trace/sending-response req resp started (.instant clock))))
267+
(trace this trace/sending-response req resp started (.instant clock))
262268
(async/>!! output-ch resp)))))
263269
(catch Throwable e ;; exceptions thrown by receive-request
264270
(log-error-receiving this e req)
265271
(async/>!! output-ch (internal-error-response resp req))))))
266272
(receive-notification [this context {:keys [method params] :as notif}]
267273
(let [now (.instant clock)]
268-
(some-> trace-ch (async/put! (trace/received-notification notif now)))
274+
(trace this trace/received-notification notif now)
269275
(if (= method "$/cancelRequest")
270276
(if-let [result-promise (get @pending-received-requests* (:id params))]
271277
(p/cancel! result-promise)
272-
(some-> trace-ch (async/put! (trace/received-unmatched-cancellation-notification notif now))))
278+
(trace this trace/received-unmatched-cancellation-notification notif now))
273279
(let [result (receive-notification method context params)]
274280
(when (identical? ::method-not-found result)
275281
(protocols.endpoint/log this :warn "received unexpected notification" method)))))))
276282

283+
(defn set-trace-level [server trace-level]
284+
(update server :tracer* reset! (trace/tracer-for-level trace-level)))
285+
277286
(defn chan-server
278-
[{:keys [output-ch input-ch log-ch trace? trace-ch clock on-close]
287+
[{:keys [output-ch input-ch log-ch trace? trace-level trace-ch clock on-close]
279288
:or {clock (java.time.Clock/systemDefaultZone)
280289
on-close (constantly nil)}}]
281-
(map->ChanServer
282-
{:output-ch output-ch
283-
:input-ch input-ch
284-
:trace-ch (or trace-ch (and trace? (async/chan (async/sliding-buffer 20))))
285-
:log-ch (or log-ch (async/chan (async/sliding-buffer 20)))
286-
:clock clock
287-
:on-close on-close
288-
:request-id* (atom 0)
289-
:pending-sent-requests* (atom {})
290-
:pending-received-requests* (atom {})
291-
:join (promise)}))
290+
(let [;; before defaulting trace-ch, so that default is "off"
291+
tracer (trace/tracer-for-level (or trace-level
292+
(when (or trace? trace-ch) "verbose")
293+
"off"))
294+
log-ch (or log-ch (async/chan (async/sliding-buffer 20)))
295+
trace-ch (or trace-ch (async/chan (async/sliding-buffer 20)))]
296+
(map->ChanServer
297+
{:output-ch output-ch
298+
:input-ch input-ch
299+
:log-ch log-ch
300+
:trace-ch trace-ch
301+
:tracer* (atom tracer)
302+
:clock clock
303+
:on-close on-close
304+
:request-id* (atom 0)
305+
:pending-sent-requests* (atom {})
306+
:pending-received-requests* (atom {})
307+
:join (promise)})))

src/lsp4clj/trace.clj

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,110 @@
2525
(format-body "Error data" (:data error))
2626
(format-body "Result" result)))
2727

28-
(defn ^:private format-trace [at direction message-type header-details body]
29-
[:debug
30-
(str (format-tag at) " " direction " " message-type " " header-details "\n"
31-
body "\n\n\n")])
32-
3328
(defn ^:private latency [^java.time.Instant started ^java.time.Instant finished]
3429
(format "%sms" (- (.toEpochMilli finished) (.toEpochMilli started))))
3530

36-
(defn ^:private format-notification [direction notif at]
37-
(format-trace at direction "notification" (format-notification-signature notif)
38-
(format-params notif)))
39-
40-
(defn ^:private format-request [direction req at]
41-
(format-trace at direction "request" (format-request-signature req)
42-
(format-params req)))
43-
44-
(defn ^:private format-response [direction req {:keys [error] :as resp} started finished]
45-
(format-trace finished direction "response"
46-
(format
47-
(str "%s. Request took %s." (when error " Request failed: %s (%s)."))
48-
(format-request-signature req)
49-
(latency started finished)
50-
(:message error) (:code error))
51-
(format-response-body resp)))
52-
53-
(defn received-notification [notif at] (format-notification "Received" notif at))
54-
(defn received-request [req at] (format-request "Received" req at))
55-
(defn received-response [req resp started finished] (format-response "Received" req resp started finished))
56-
57-
(defn received-unmatched-response [resp at]
58-
(format-trace at "Received" "response" "for unmatched request:"
59-
(format-body "Body" resp)))
60-
61-
(defn received-unmatched-cancellation-notification [notif at]
62-
(format-trace at "Received" "cancellation notification" (format "for unmatched request (%s):" (:id (:params notif)))
63-
(format-params notif)))
64-
65-
(defn sending-notification [notif at] (format-notification "Sending" notif at))
66-
(defn sending-request [req at] (format-request "Sending" req at))
67-
(defn sending-response [req resp started finished] (format-response "Sending" req resp started finished))
31+
(defn ^:private format-response-header-details [req {:keys [error]} started finished]
32+
(format
33+
(str "%s. Request took %s." (when error " Request failed: %s (%s)."))
34+
(format-request-signature req)
35+
(latency started finished)
36+
(:message error) (:code error)))
37+
38+
(defn ^:private basic-trace [at direction message-type header-details]
39+
(str (format-tag at) " " direction " " message-type " " header-details))
40+
41+
(defn ^:private verbose-trace [header body]
42+
(str header "\n" body "\n\n\n"))
43+
44+
(defn ^:private basic-notification [direction notif at]
45+
(basic-trace at direction "notification" (format-notification-signature notif)))
46+
47+
(defn ^:private basic-request [direction req at]
48+
(basic-trace at direction "request" (format-request-signature req)))
49+
50+
(defn ^:private basic-response [direction req resp started finished]
51+
(basic-trace finished direction "response" (format-response-header-details req resp started finished)))
52+
53+
(defn ^:private basic-received-unmatched-response [at]
54+
(basic-trace at "Received" "response" "for unmatched request"))
55+
56+
(defn ^:private basic-received-unmatched-cancellation [at notif]
57+
(basic-trace at "Received" "cancellation notification" (format "for unmatched request (%s):" (:id (:params notif)))))
58+
59+
(defn ^:private verbose-notification [direction notif at]
60+
(verbose-trace (basic-notification direction notif at)
61+
(format-params notif)))
62+
63+
(defn ^:private verbose-request [direction req at]
64+
(verbose-trace (basic-request direction req at)
65+
(format-params req)))
66+
67+
(defn ^:private verbose-response [direction req resp started finished]
68+
(verbose-trace (basic-response direction req resp started finished)
69+
(format-response-body resp)))
70+
71+
(defprotocol ITracer
72+
(received-notification [this notif at])
73+
(received-request [this req at])
74+
(received-response [this req resp started finished])
75+
(received-unmatched-response [this resp at])
76+
(received-unmatched-cancellation-notification [this notif at])
77+
(sending-notification [this notif at])
78+
(sending-request [this req at])
79+
(sending-response [this req resp started finished]))
80+
81+
(defrecord VerboseTracer []
82+
ITracer
83+
(received-notification [_this notif at]
84+
(verbose-notification "Received" notif at))
85+
(received-request [_this req at]
86+
(verbose-request "Received" req at))
87+
(received-response [_this req resp started finished]
88+
(verbose-response "Received" req resp started finished))
89+
(received-unmatched-response [_this resp at]
90+
(verbose-trace (basic-received-unmatched-response at) (format-body "Body" resp)))
91+
(received-unmatched-cancellation-notification [_this notif at]
92+
(verbose-trace (basic-received-unmatched-cancellation at notif) (format-params notif)))
93+
(sending-notification [_this notif at]
94+
(verbose-notification "Sending" notif at))
95+
(sending-request [_this req at]
96+
(verbose-request "Sending" req at))
97+
(sending-response [_this req resp started finished]
98+
(verbose-response "Sending" req resp started finished)))
99+
100+
(defrecord MessagesTracer []
101+
ITracer
102+
(received-notification [_this notif at]
103+
(basic-notification "Received" notif at))
104+
(received-request [_this req at]
105+
(basic-request "Received" req at))
106+
(received-response [_this req resp started finished]
107+
(basic-response "Received" req resp started finished))
108+
(received-unmatched-response [_this _resp at]
109+
(basic-received-unmatched-response at))
110+
(received-unmatched-cancellation-notification [_this notif at]
111+
(basic-received-unmatched-cancellation at notif))
112+
(sending-notification [_this notif at]
113+
(basic-notification "Sending" notif at))
114+
(sending-request [_this req at]
115+
(basic-request "Sending" req at))
116+
(sending-response [_this req resp started finished]
117+
(basic-response "Sending" req resp started finished)))
118+
119+
(defrecord SilentTracer []
120+
ITracer
121+
(received-notification [_this _notif _at])
122+
(received-request [_this _req _at])
123+
(received-response [_this _req _resp _started _finished])
124+
(received-unmatched-response [_this _resp _at])
125+
(received-unmatched-cancellation-notification [_this _notif _at])
126+
(sending-notification [_this _notif _at])
127+
(sending-request [_this _req _at])
128+
(sending-response [_this _req _resp _started _finished]))
129+
130+
(defn tracer-for-level [trace-level]
131+
(case trace-level
132+
"verbose" (VerboseTracer.)
133+
"messages" (MessagesTracer.)
134+
(SilentTracer.)))

0 commit comments

Comments
 (0)