/- Copyright (c) 2025 Lean FRO. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Author: Marc Huisinga -/ module prelude public import Lean.Data.JsonRpc import Std.Time import Lean.Data.Lsp.Extra public import Lean.Data.Lsp.InitShutdown namespace Lean.Server.Logging public structure LogConfig where isEnabled : Bool logFile : System.FilePath allowedMethods? : Option (Std.HashSet String) disallowedMethods? : Option (Std.HashSet String) public def LogConfig.ofLspLogConfig (lspCfg? : Option Lsp.LogConfig) : IO LogConfig := do let time ← Std.Time.ZonedDateTime.now let time := time.format "yyyy-MM-dd-HH-mm-ss-SSSSXX" let logFileName := s!"LSP_{time}.log" let defaultLogFile : System.FilePath := System.FilePath.join "." logFileName let some lspCfg := lspCfg? | return { isEnabled := false logFile := defaultLogFile allowedMethods? := none disallowedMethods? := none } return { isEnabled := true logFile := lspCfg.logDir?.map (System.FilePath.join · logFileName) |>.getD defaultLogFile allowedMethods? := lspCfg.allowedMethods? disallowedMethods? := lspCfg.disallowedMethods? } open Lean public inductive MessageMethod where | request (method : String) | rpcRequest (method : String) | notification (method : String) deriving Inhabited def MessageMethod.all : MessageMethod → Array String | .request method | .notification method => #[method] | .rpcRequest method => #["$/lean/rpc/call", method] public def messageMethod? : JsonRpc.Message → Option MessageMethod | .request _ method params? => do if method == "$/lean/rpc/call" then let params := toJson params? if let .ok (callParams : Lsp.RpcCallParams) := fromJson? params then return .rpcRequest callParams.method.toString return .request method | .notification method _ => some <| .notification method | _ => none def messageId? : JsonRpc.Message → Option JsonRpc.RequestID | .request id .. => some id | .response id .. => some id | .responseError id .. => some id | _ => none def isMsgAllowed (cfg : LogConfig) (pending : Std.HashMap JsonRpc.RequestID MessageMethod) (msg : JsonRpc.Message) : Bool := Id.run do if ! cfg.isEnabled then return false let some method := method? | return false let allMethods := method.all if let some allowedMethods := cfg.allowedMethods? then if allMethods.any (! allowedMethods.contains ·) then return false if let some disallowedMethods := cfg.disallowedMethods? then if allMethods.any (disallowedMethods.contains ·) then return false return true where method? : Option MessageMethod := messageMethod? msg <|> (do pending.get? (← messageId? msg)) local instance : ToJson Std.Time.ZonedDateTime where toJson dt := dt.toISO8601String local instance : FromJson Std.Time.ZonedDateTime where fromJson? | .str s => Std.Time.ZonedDateTime.fromISO8601String s | _ => throw "Expected string when converting JSON to Std.Time.ZonedDateTime" structure LogEntry where time : Std.Time.ZonedDateTime direction : JsonRpc.MessageDirection kind : JsonRpc.MessageKind msg : JsonRpc.Message deriving FromJson, ToJson public def writeLogEntry (cfg : LogConfig) (pending : Std.HashMap JsonRpc.RequestID MessageMethod) (log : IO.FS.Handle) (direction : JsonRpc.MessageDirection) (msg : JsonRpc.Message) : IO Unit := do if ! isMsgAllowed cfg pending msg then return let entry : LogEntry := { time := ← Std.Time.ZonedDateTime.now direction kind := .ofMessage msg msg } let entry := toJson entry |>.compress log.putStrLn entry log.flush