Skip to content

Commit 19533ab

Browse files
authored
feat: revamp server logging (#10787)
This PR revamps the server logging mechanism to allow filtering the log output by LSP method.
1 parent 28310a7 commit 19533ab

File tree

10 files changed

+796
-81
lines changed

10 files changed

+796
-81
lines changed

src/Lean/Data/JsonRpc.lean

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ inductive RequestID where
2525
| str (s : String)
2626
| num (n : JsonNumber)
2727
| null
28-
deriving Inhabited, BEq, Ord
28+
deriving Inhabited, BEq, Hashable, Ord
2929

3030
instance : OfNat RequestID n := ⟨RequestID.num n⟩
3131

@@ -307,6 +307,12 @@ inductive MessageMetaData where
307307
| responseError (id : RequestID) (code : ErrorCode) (message : String) (data? : Option Json)
308308
deriving Inhabited
309309

310+
def Message.metaData : Message → MessageMetaData
311+
| .request id method .. => .request id method
312+
| .notification method .. => .notification method
313+
| .response id .. => .response id
314+
| .responseError id code message data? => .responseError id code message data?
315+
310316
def MessageMetaData.toMessage : MessageMetaData → Message
311317
| .request id method => .request id method none
312318
| .notification method => .notification method none
@@ -394,6 +400,24 @@ Namely:
394400
def parseMessageMetaData (input : String) : Except String MessageMetaData :=
395401
messageMetaDataParser input |>.run input
396402

403+
public inductive MessageDirection where
404+
| clientToServer
405+
| serverToClient
406+
deriving Inhabited, FromJson, ToJson
407+
408+
inductive MessageKind where
409+
| request
410+
| notification
411+
| response
412+
| responseError
413+
deriving FromJson, ToJson
414+
415+
def MessageKind.ofMessage : Message → MessageKind
416+
| .request .. => .request
417+
| .notification .. => .notification
418+
| .response .. => .response
419+
| .responseError .. => .responseError
420+
397421
end Lean.JsonRpc
398422

399423
namespace IO.FS.Stream

src/Lean/Data/Lsp/InitShutdown.lean

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,27 @@ instance Trace.hasToJson : ToJson Trace :=
4747
| Trace.messages => "messages"
4848
| Trace.verbose => "verbose"
4949

50+
local instance [BEq α] [Hashable α] [ToJson α] : ToJson (Std.HashSet α) where
51+
toJson s := .arr <| s.toArray.map toJson
52+
53+
local instance [BEq α] [Hashable α] [FromJson α] : FromJson (Std.HashSet α) where
54+
fromJson?
55+
| .arr a => return Std.HashSet.ofArray <| ← a.mapM fromJson?
56+
| _ => throw "Expected array when converting JSON to Std.HashSet"
57+
58+
structure LogConfig where
59+
logDir? : Option System.FilePath
60+
allowedMethods? : Option (Std.HashSet String)
61+
disallowedMethods? : Option (Std.HashSet String)
62+
deriving FromJson, ToJson
63+
5064
/-- Lean-specific initialization options. -/
5165
structure InitializationOptions where
52-
/-- Time (in milliseconds) which must pass since latest edit until elaboration begins. Lower
53-
values may make editors feel faster at the cost of higher CPU usage. Defaults to 200ms. -/
54-
editDelay? : Option Nat
5566
/-- Whether the client supports interactive widgets. When true, in order to improve performance
5667
the server may cease including information which can be retrieved interactively in some standard
5768
LSP messages. Defaults to false. -/
5869
hasWidgets? : Option Bool
70+
logCfg? : Option LogConfig
5971
deriving ToJson, FromJson
6072

6173
structure InitializeParams where
@@ -71,9 +83,6 @@ structure InitializeParams where
7183
workspaceFolders? : Option (Array WorkspaceFolder) := none
7284
deriving ToJson
7385

74-
def InitializeParams.editDelay (params : InitializeParams) : Nat :=
75-
params.initializationOptions? |>.bind (·.editDelay?) |>.getD 200
76-
7786
instance : FromJson InitializeParams where
7887
fromJson? j := do
7988
/- Many of these params can be null instead of not present.

src/Lean/Server.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ public import Lean.Server.FileWorker
1212
public import Lean.Server.Rpc
1313
public import Lean.Server.CodeActions
1414
public import Lean.Server.Test
15+
public import Lean.Server.ProtocolOverview

src/Lean/Server/FileWorker.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,8 +1040,6 @@ where
10401040
return false
10411041

10421042
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO Unit := do
1043-
let i ← maybeTee "fwIn.txt" false i
1044-
let o ← maybeTee "fwOut.txt" true o
10451043
let initParams ← i.readLspRequestAs "initialize" InitializeParams
10461044
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" LeanDidOpenTextDocumentParams
10471045
let doc := param.textDocument
@@ -1090,5 +1088,7 @@ def workerMain (opts : Options) : IO UInt32 := do
10901088
catch err =>
10911089
e.putStrLn err.toString
10921090
IO.Process.forceExit 1 -- Terminate all tasks of this process
1091+
finally
1092+
IO.Process.forceExit (α := UInt32) 1
10931093

10941094
end Lean.Server.FileWorker

src/Lean/Server/Logging.lean

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
5+
Author: Marc Huisinga
6+
-/
7+
module
8+
9+
prelude
10+
public import Lean.Data.JsonRpc
11+
import Std.Time
12+
import Lean.Data.Lsp.Extra
13+
public import Lean.Data.Lsp.InitShutdown
14+
15+
namespace Lean.Server.Logging
16+
17+
public structure LogConfig where
18+
isEnabled : Bool
19+
logFile : System.FilePath
20+
allowedMethods? : Option (Std.HashSet String)
21+
disallowedMethods? : Option (Std.HashSet String)
22+
23+
public def LogConfig.ofLspLogConfig (lspCfg? : Option Lsp.LogConfig) : IO LogConfig := do
24+
let time ← Std.Time.ZonedDateTime.now
25+
let time := time.format "yyyy-MM-dd-HH-mm-ss-SSSSXX"
26+
let logFileName := s!"LSP_{time}.log"
27+
let defaultLogFile : System.FilePath := System.FilePath.join "." logFileName
28+
let some lspCfg := lspCfg?
29+
| return {
30+
isEnabled := false
31+
logFile := defaultLogFile
32+
allowedMethods? := none
33+
disallowedMethods? := none
34+
}
35+
return {
36+
isEnabled := true
37+
logFile := lspCfg.logDir?.map (System.FilePath.join · logFileName) |>.getD defaultLogFile
38+
allowedMethods? := lspCfg.allowedMethods?
39+
disallowedMethods? := lspCfg.disallowedMethods?
40+
}
41+
42+
open Lean
43+
44+
public inductive MessageMethod where
45+
| request (method : String)
46+
| rpcRequest (method : String)
47+
| notification (method : String)
48+
deriving Inhabited
49+
50+
def MessageMethod.all : MessageMethod → Array String
51+
| .request method
52+
| .notification method =>
53+
#[method]
54+
| .rpcRequest method =>
55+
#["$/lean/rpc/call", method]
56+
57+
public def messageMethod? : JsonRpc.Message → Option MessageMethod
58+
| .request _ method params? => do
59+
if method == "$/lean/rpc/call" then
60+
let params := toJson params?
61+
if let .ok (callParams : Lsp.RpcCallParams) := fromJson? params then
62+
return .rpcRequest callParams.method.toString
63+
return .request method
64+
| .notification method _ =>
65+
some <| .notification method
66+
| _ =>
67+
none
68+
69+
def messageId? : JsonRpc.Message → Option JsonRpc.RequestID
70+
| .request id .. => some id
71+
| .response id .. => some id
72+
| .responseError id .. => some id
73+
| _ => none
74+
75+
def isMsgAllowed (cfg : LogConfig)
76+
(pending : Std.HashMap JsonRpc.RequestID MessageMethod)
77+
(msg : JsonRpc.Message) : Bool := Id.run do
78+
if ! cfg.isEnabled then
79+
return false
80+
let some method := method?
81+
| return false
82+
let allMethods := method.all
83+
if let some allowedMethods := cfg.allowedMethods? then
84+
if allMethods.any (! allowedMethods.contains ·) then
85+
return false
86+
if let some disallowedMethods := cfg.disallowedMethods? then
87+
if allMethods.any (disallowedMethods.contains ·) then
88+
return false
89+
return true
90+
where
91+
method? : Option MessageMethod :=
92+
messageMethod? msg <|> (do pending.get? (← messageId? msg))
93+
94+
local instance : ToJson Std.Time.ZonedDateTime where
95+
toJson dt := dt.toISO8601String
96+
97+
local instance : FromJson Std.Time.ZonedDateTime where
98+
fromJson?
99+
| .str s => Std.Time.ZonedDateTime.fromISO8601String s
100+
| _ => throw "Expected string when converting JSON to Std.Time.ZonedDateTime"
101+
102+
structure LogEntry where
103+
time : Std.Time.ZonedDateTime
104+
direction : JsonRpc.MessageDirection
105+
kind : JsonRpc.MessageKind
106+
msg : JsonRpc.Message
107+
deriving FromJson, ToJson
108+
109+
public def writeLogEntry (cfg : LogConfig) (pending : Std.HashMap JsonRpc.RequestID MessageMethod)
110+
(log : IO.FS.Handle) (direction : JsonRpc.MessageDirection) (msg : JsonRpc.Message) : IO Unit := do
111+
if ! isMsgAllowed cfg pending msg then
112+
return
113+
let entry : LogEntry := {
114+
time := ← Std.Time.ZonedDateTime.now
115+
direction
116+
kind := .ofMessage msg
117+
msg
118+
}
119+
let entry := toJson entry |>.compress
120+
log.putStrLn entry
121+
log.flush

0 commit comments

Comments
 (0)