Skip to content

Commit 46f6927

Browse files
committed
Improve tracing
Support tracing at an intermediate level, without message body details. And let language servers change trace level on running servers.
1 parent 8aa121d commit 46f6927

File tree

5 files changed

+238
-67
lines changed

5 files changed

+238
-67
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: 37 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 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,41 @@
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 [trace-level (or trace-level
291+
(when (or trace? trace-ch) "verbose")
292+
"off")]
293+
(map->ChanServer
294+
{:output-ch output-ch
295+
:input-ch input-ch
296+
:log-ch (or log-ch (async/chan (async/sliding-buffer 20)))
297+
:trace-ch (or trace-ch (async/chan (async/sliding-buffer 20)))
298+
:tracer* (atom (trace/tracer-for-level trace-level))
299+
:clock clock
300+
:on-close on-close
301+
:request-id* (atom 0)
302+
:pending-sent-requests* (atom {})
303+
:pending-received-requests* (atom {})
304+
:join (promise)})))

src/lsp4clj/trace.clj

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,116 @@
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]
28+
(defn ^:private format-header [at direction message-type header-details]
29+
(str (format-tag at) " " direction " " message-type " " header-details))
30+
31+
(defn ^:private basic-trace [at direction message-type header-details]
32+
[:debug
33+
(format-header at direction message-type header-details)])
34+
35+
(defn ^:private verbose-trace [at direction message-type header-details body]
2936
[:debug
30-
(str (format-tag at) " " direction " " message-type " " header-details "\n"
37+
(str (format-header at direction message-type header-details) "\n"
3138
body "\n\n\n")])
3239

3340
(defn ^:private latency [^java.time.Instant started ^java.time.Instant finished]
3441
(format "%sms" (- (.toEpochMilli finished) (.toEpochMilli started))))
3542

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

0 commit comments

Comments
 (0)