SAFE Stack のアプリケーションからログを出力するためのきちんとした方法をそろそろ覚えたい。 これまで printf や HTML の要素にどこかに表示していた。

調べたら Zaid Ajaj さん (GitHub) が書いた Fable.Remoting のドキュメント (link) に含まれる “Logging” のページ (link) に辿り着いた。 Serilog (main site, GitHub, nuget) が推奨されている。 同じく Zaid Ajaj さん作の SAFE Stack のアプリケーションの tabula-rasa (GitHub) でも Serilog を使っているので、これを参考にさせてもらって SAFE Stack でも利用できそう。

なお、”Logging” ページに “Using logger from remoting functions” や “To use Serilog from the remote functions” といった記述がある。 これは単に「サーバの関数」くらいの意図で書いてあるっぽい。 (クライアントの関数からも透過的にサーバにあるロガーを呼べる、とかではなかった。)

“Logging” ページの内容に沿って、あれこれやっていったらサーバでロガーを使えるようになった。 このページには “Basic use case” と書かれているから、基本的な使い方だけなのだと思う。 より詳しいことは Suave.SerilogExtensions (GitHub) や Serilog のページに書いてある。

サーバでロガーを使えるようになるまでの手順を以下のまとめた。 「”Logging” ページの内容に沿って」と手間で書いたけど、現在の SAFE Stack のバージョン (v3.0.1) に合わせるため、実際のコマンドやコードは細かなところで異なっている。

手順

手順1: パッケージのインストール

“Basic use case” の “1 - Install from Nuget:” をおこなう。

コマンド:

% dotnet paket add Serilog
Paket version 6.0.0-rc002
Adding package 'Serilog'
Resolving dependency graph...
Created dependency graph (70 packages in total)
Resolved package 'Serilog' to version 2.10
Total time taken: 22 seconds
% 
% dotnet paket add Giraffe.SerilogExtensions
Paket version 6.0.0-rc002
Adding package 'Giraffe.SerilogExtensions'
Resolving dependency graph...
Created dependency graph (83 packages in total)
Downloading Microsoft.Extensions.DependencyInjection.Abstractions 5.0.0
Downloading FSharp.Core 5.0.2
Downloading System.IO.Pipelines 5.0.1
Download of Microsoft.Extensions.DependencyInjection.Abstractions 5.0.0 done in 72 milliseconds. (9754 kbit/s, 0 MB)
Downloading Microsoft.Extensions.ObjectPool 5.0.8
Download of Microsoft.Extensions.ObjectPool 5.0.8 done in 26 milliseconds. (21881 kbit/s, 0 MB)
Downloading Microsoft.Extensions.Options 5.0.0
Download of System.IO.Pipelines 5.0.1 done in 175 milliseconds. (8853 kbit/s, 0 MB)
Downloading System.Text.Encodings.Web 5.0.1
Download of Microsoft.Extensions.Options 5.0.0 done in 147 milliseconds. (6883 kbit/s, 0 MB)
Downloading Microsoft.Extensions.Primitives 5.0.1
Download of System.Text.Encodings.Web 5.0.1 done in 94 milliseconds. (19902 kbit/s, 0 MB)
Download of Microsoft.Extensions.Primitives 5.0.1 done in 106 milliseconds. (8810 kbit/s, 0 MB)
Downloading Microsoft.Net.Http.Headers 2.2.8
Download of Microsoft.Net.Http.Headers 2.2.8 done in 27 milliseconds. (18031 kbit/s, 0 MB)
Download of FSharp.Core 5.0.2 done in 1 second. (9618 kbit/s, 1 MB)
Downloading Microsoft.AspNetCore.Http.Features 5.0.8
Download of Microsoft.AspNetCore.Http.Features 5.0.8 done in 791 milliseconds. (1060 kbit/s, 0 MB)
Resolved package 'Giraffe.SerilogExtensions' to version 2.0
Total time taken: 41 seconds
%
% dotnet paket add Serilog.Sinks.Console
Paket version 6.0.0-rc002
Adding package 'Serilog.Sinks.Console'
Resolving dependency graph...
Created dependency graph (84 packages in total)
Downloading Serilog.Sinks.Console 4.0.0
Download of Serilog.Sinks.Console 4.0.0 done in 167 milliseconds. (5069 kbit/s, 0 MB)
Resolved package 'Serilog.Sinks.Console' to version 4.0
Total time taken: 30 seconds
% 

手順2: HttpHandler を Remoting.buildHttpHandler でラップ

“Basic use case” の “2 - Wrap your WebPart given by Remoting with SerilogAdaper.Enable function:” をおこなう。

src/Server/Server.fs を変更した。

--- src/Server/Server.fs
+++ src/Server/Server.fs
@@ -6,6 +6,9 @@
 open Fable.Remoting.Giraffe
 open Saturn

+open Serilog
+open Giraffe.SerilogExtensions
+
 open Shared
 open Storage

@@ -64,6 +67,7 @@
     |> Remoting.withRouteBuilder Route.builder
     |> Remoting.fromValue todosApi
     |> Remoting.buildHttpHandler
+    |> SerilogAdapter.Enable

 let app =
     application {

手順3: ロガーの設定と作成

“Basic use case” の “3 - Create the logger and configure the sinks:” をおこなう。

更に src/Server/Server.fs を変更した。

--- src/Server/Server.fs
+++ src/Server/Server.fs
@@ -27,6 +27,11 @@

 let storage = dataDir () |> Storage

+Log.Logger <- LoggerConfiguration()
+              .Destructure.FSharpTypes()
+              .WriteTo.Console()
+              .CreateLogger()
+
 storage.AddTodo(Todo.create "Create new SAFE project")
 |> ignore

手順4: web サーバの起動

“Basic use case” の “4 - Start the web server” なのだけど、Fable.Remoting なり SAFE Stack なりの アプリケーションが既に動いているなら、とくにすることは無いはず。

SAFE Stack のサンプル (ToDo アプリ) でいえば src/Server/Server.fs の末尾の run app の行でおこなわれている。

手順5: ロガーを取得して API の関数に渡す

ここからは “Logging” ページ (link) にある “Using logger from remoting functions” の手順になる。

ロガーを HttpContext から取得して API の処理をする関数へ渡すようにする。 HttpContextMicrosoft.AspNetCore.Http で定義されている。

--- src/Server/Server.fs
+++ src/Server/Server.fs
@@ -41,7 +41,7 @@
 storage.AddTodo(Todo.create "Ship it !!!")
 |> ignore
 
-let todosApi =
+let todosApi (logger: ILogger) : ITodosApi =
     {
         getTodos = fun () -> async { return storage.GetTodos () }
         addTodo =
@@ -67,10 +67,12 @@
                 }
     }
 
+open Microsoft.AspNetCore.Http
+
 let webApp =
     Remoting.createApi ()
     |> Remoting.withRouteBuilder Route.builder
-    |> Remoting.fromValue todosApi
+    |> Remoting.fromContext (fun (ctx: HttpContext) -> todosApi (ctx.Logger()))
     |> Remoting.buildHttpHandler
     |> SerilogAdapter.Enable
 

ここまでくれば、渡されたロガーを利用していくだけでよさそう。 Serilog の使い方だけ意識すればよいのかな?

以下のように変更したら API の開始 / 終了を示すログが出力された。

--- src/Server/Server.fs
+++ src/Server/Server.fs
@@ -43,27 +43,45 @@
 
 let todosApi (logger: ILogger) : ITodosApi =
     {
-        getTodos = fun () -> async { return storage.GetTodos () }
+        getTodos =
+            fun () ->
+                async {
+                    logger.Information "getTodos: begin"
+                    let todos = storage.GetTodos ()
+                    logger.Information "getTodos: end"
+                    return todos
+                }
         addTodo =
             fun todo ->
                 async {
+                    logger.Information "addTodo: begin"
-                    match storage.AddTodo todo with
-                    | Ok () -> return todo
-                    | Error e -> return failwith e
+                    match storage.AddTodo todo with
+                    | Ok ()   -> ()
+                    | Error e -> failwith e
+                    logger.Information "addTodo: end"
+                    return todo
                 }
         moveTodo =
             fun (srcIdx, dstPos) ->
                 async {
+                    logger.Information "moveTodo: begin"
-                    match storage.MoveTodo srcIdx dstPos with
-                    | Ok newTodoList -> return newTodoList
-                    | Error e -> return failwith e
+                    let newTodoList =
+                        match storage.MoveTodo srcIdx dstPos with
+                        | Ok newTodoList -> newTodoList
+                        | Error e        -> failwith e
+                    logger.Information "moveTodo: end"
+                    return newTodoList
                 }
         saveTodos =
             fun () ->
                 async {
+                    logger.Information "saveTodos: begin"
-                    match storage.SaveTodos () with
-                    | Ok newTodoList -> return newTodoList
-                    | Error e -> return failwith e
+                    let newTodoList =
+                        match storage.SaveTodos () with
+                        | Ok newTodoList -> newTodoList
+                        | Error e        -> failwith e
+                    logger.Information "saveTodos: end"
+                    return newTodoList
                 }
     }