Skip to content

Commit 820587c

Browse files
committed
feat: revamp server logging
1 parent 80b8e44 commit 820587c

File tree

7 files changed

+281
-80
lines changed

7 files changed

+281
-80
lines changed

src/Lean/Data/JsonRpc.lean

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

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

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

311+
def Message.metaData : Message → MessageMetaData
312+
| .request id method .. => .request id method
313+
| .notification method .. => .notification method
314+
| .response id .. => .response id
315+
| .responseError id code message data? => .responseError id code message data?
316+
311317
def MessageMetaData.toMessage : MessageMetaData → Message
312318
| .request id method => .request id method none
313319
| .notification method => .notification method none

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/FileWorker.lean

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,8 +1050,6 @@ where
10501050
return false
10511051

10521052
def initAndRunWorker (i o e : FS.Stream) (opts : Options) : IO Unit := do
1053-
let i ← maybeTee "fwIn.txt" false i
1054-
let o ← maybeTee "fwOut.txt" true o
10551053
let initParams ← i.readLspRequestAs "initialize" InitializeParams
10561054
let ⟨_, param⟩ ← i.readLspNotificationAs "textDocument/didOpen" LeanDidOpenTextDocumentParams
10571055
let doc := param.textDocument

src/Lean/Server/Logging.lean

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
public inductive MessageDirection where
95+
| clientToServer
96+
| serverToClient
97+
deriving Inhabited, FromJson, ToJson
98+
99+
inductive MessageKind where
100+
| request
101+
| notification
102+
| response
103+
| responseError
104+
deriving FromJson, ToJson
105+
106+
def MessageKind.ofMessage : JsonRpc.Message → MessageKind
107+
| .request .. => .request
108+
| .notification .. => .notification
109+
| .response .. => .response
110+
| .responseError .. => .responseError
111+
112+
local instance : ToJson Std.Time.ZonedDateTime where
113+
toJson dt := dt.toISO8601String
114+
115+
local instance : FromJson Std.Time.ZonedDateTime where
116+
fromJson?
117+
| .str s => Std.Time.ZonedDateTime.fromISO8601String s
118+
| _ => throw "Expected string when converting JSON to Std.Time.ZonedDateTime"
119+
120+
structure LogEntry where
121+
time : Std.Time.ZonedDateTime
122+
direction : MessageDirection
123+
kind : MessageKind
124+
msg : JsonRpc.Message
125+
deriving FromJson, ToJson
126+
127+
public def writeLogEntry (cfg : LogConfig) (pending : Std.HashMap JsonRpc.RequestID MessageMethod)
128+
(log : IO.FS.Handle) (direction : MessageDirection) (msg : JsonRpc.Message) : IO Unit := do
129+
if ! isMsgAllowed cfg pending msg then
130+
return
131+
let entry : LogEntry := {
132+
time := ← Std.Time.ZonedDateTime.now
133+
direction
134+
kind := .ofMessage msg
135+
msg
136+
}
137+
let entry := toJson entry |>.compress
138+
log.putStrLn entry
139+
log.flush

src/Lean/Server/Test/Runner.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,8 @@ partial def main (args : List String) : IO Unit := do
698698
-- We want `dbg_trace` tactics to write directly to stderr instead of being caught in reuse
699699
Ipc.runWith ipcCmd ipcArgs do
700700
let initializationOptions? := some {
701-
editDelay? := none
702701
hasWidgets? := some true
702+
logCfg? := none
703703
}
704704
let capabilities := {
705705
textDocument? := some {

src/Lean/Server/Utils.lean

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,6 @@ def replaceLspRange (text : FileMap) (r : Lsp.Range) (newText : String) : FileMa
120120

121121
open IO
122122

123-
/--
124-
Duplicates an I/O stream to a log file `fName` in LEAN_SERVER_LOG_DIR
125-
if that envvar is set.
126-
-/
127-
def maybeTee (fName : String) (isOut : Bool) (h : FS.Stream) : IO FS.Stream := do
128-
match (← IO.getEnv "LEAN_SERVER_LOG_DIR") with
129-
| none => pure h
130-
| some logDir =>
131-
IO.FS.createDirAll logDir
132-
let hTee ← FS.Handle.mk (System.mkFilePath [logDir, fName]) FS.Mode.write
133-
let hTee := FS.Stream.ofHandle hTee
134-
pure $ if isOut then
135-
hTee.chainLeft h true
136-
else
137-
h.chainRight hTee true
138-
139123
open Lsp
140124

141125
/-- Returns the document contents with the change applied. -/

0 commit comments

Comments
 (0)