azure functions から serilogを経由してログを出力する方法です。
他のサイトにある方法だとログ出力によってパフォーマンスが悪化したり、Storageへのアクセスがうまくいかなかったりするため、散々調査しました。
なお、NLog等の別の部品を使ってもログ出力は実装できますが、ログが一部抜け落ちる、最後まで出力されないといった課題がありましたので、Serilogを使っています。
前提
- azure functions v4
- c#(dotnet6)
- ログ出力は、ローカルファイルに行う(BlobStorageやTableStorageへの変更も可能)
パッケージの追加
Serilog経由でログを出力するには、以下のパッケージを追加します。
- Serilog
Serilog経由でのログ出力をするための、メインのパッケージです。 - Serilog.AspNetCore
Microsoft.Extensions.DependencyInjection のバージョンが新しすぎるとFunctions起動用の他のパッケージと競合するため、v3.4.0あたりをインストールした方が無難です(2023/03/31現在)。 - Serilog.Sinks.Async
ログ出力を非同期にして、関数のパフォーマンスを上げます。
(これを追加しないと、ログ出力1件当たり0.1秒ほどの処理時間が増加します。) - Serilog.Exceptions
エラー時の詳細情報を取得するために追加します。
以下は、任意のパッケージです。ほかにもSerilogのパッケージはあるので、必要に応じて追加してください。
- Serilog.Enrichers.Environment
Functionsをスケールする際に、どのサーバーから関数が呼び出されているのかを監視するために追加します(サーバー情報が不要であれば、入れる必要はありません)。
ロガーの定義
Serilogの定義
LogConfig.cs
using Serilog.Events;
using Serilog;
using System;
using Serilog.Exceptions;
namespace SerilogSample
{
public class LogConfig
{
/// <summary>
/// ロガーの定義
/// </summary>
/// <returns></returns>
public static Serilog.Core.Logger LogConditionBuilder(string functionName)
{
//Serilogの構成
LoggerConfiguration config = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithExceptionDetails()
.Enrich.WithProperty("FunctionName", functionName)
.MinimumLevel.Information()
;
config.WriteTo.Async(x => x.Conditional(
DefaultLoggingCondition
, LocalFile.LogCondition.ConfigureSink
));
return config.CreateLogger();
}
/// <summary>
/// Functionsが独自に出力するログのうち、不要なものを対象外とする
/// </summary>
public static readonly Func<LogEvent, bool> DefaultLoggingCondition = (condition
=>
{
bool result = true;
result = (condition.Properties.ContainsKey("MS_FunctionInvocationId") && condition.Level >= LogEventLevel.Information)
|| (!condition.Properties.ContainsKey("MS_FunctionInvocationId") && condition.Level >= LogEventLevel.Warning);
if (!result) { return result; }
result = condition.Level >= LogEventLevel.Warning;
if (!result) { return result; }
result = condition.Properties.ContainsKey("SourceContext");
if (!result) { return result; }
condition.Properties.TryGetValue("SourceContext", out LogEventPropertyValue value);
result = value.ToString().StartsWith("\"Function.");
return result;
});
}
}
LogConditionBuilderで、Serilogの出力内容やログレベルなどの構成を定義しています。
今回は、ThreadIdやEnvironmentNameも出力対象として追加し、ログに出力する項目を増やしています。また、関数名を引数として受け取るようにし、Functionsが複数ある環境下でもログの区別をできるようにしています(このように、Serilogには任意のプロパティを追加することもできます)。
DefaultLoggingConditionで、ログの出力条件を細かく定義しています。
Functionsから出力されるログには、自身で出力するように記述したログのほかに、Functions独自に出力されるログもあります。そのままの形ですべてを出力対象とした場合、ログの量が膨大となってしまうため、不要なログは出力対象外、もしくはエラーのときのみ出力対象とします。
(もしかしたら、もっと良い方法があるかもしれません。)
今回は、自身で出力するように記述したログ(Information以上)と、Functions独自のログのうち開始や終了などの関数の処理に関するものとWarning以上のものを出力対象としています。
ローカルファイルへのログ出力
LogConditionToLocalFile.cs
namespace SerilogSample.LocalFile
{
public class LogCondition
{
public static Action<LoggerSinkConfiguration> ConfigureSink
{
get
{
return new Action<LoggerSinkConfiguration>(write => write.File(
new CompactJsonFormatter() //JSON形式でログ出力する
, $"logs\\log.json"
, restrictedToMinimumLevel: LogEventLevel.Information
, rollingInterval: RollingInterval.Hour
));
}
}
}
}
今回は、ローカルファイルにJSON形式でのログ出力としています。rollingIntervalを指定することで、ログのローテーションも可能です。
DI機能の追加
Functionsの関数が呼び出された際の引数には、Microsoft.Extensions.Logging.ILoggerが渡されています。このロガーを用いてログを出力する際の出力をSerilogに変更するためには、DIを実装します。
azure functionsでロガーのDIを実装するには、Startup.csファイルをプロジェクト直下に作成し、Serilogの定義をServiceCollectionに追加します。
Startup.cs
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System;
[assembly: FunctionsStartup(typeof(SerilogSample.Startup))]
namespace SerilogSample
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
if (builder == null)
{
throw new ApplicationException("Builder is not defined");
}
ConfigureLogging(builder.Services);
}
private void ConfigureLogging(IServiceCollection services)
{
services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddSerilog(LogConfig.LogConditionBuilder(nameof(SerilogSample)));
});
}
}
}
各Functionsでの利用方法
各Functionsでは、デフォルトで以下のようにILoggerの引数を受け取るようになっています。
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
このlogを使って、log.LogInformationなどのログ出力を呼び出すことで、ローカルファイルにログが出力されるようになります。
(コンストラクタの引数にILoggerを指定しているサンプルコードも散見されますが、どちらでも構いません。)
まとめ
ログ出力をSerilogとDIを用いて変更することで、エラー時にメールを送信する、ログをBlobStorageに出力してコストを削減する、BlobStorageのローテーションを活用することでログの退役を自動化するといったことも可能になります。
また、ログファイルの出力単位も時系列だけでなく関数ごとやinvocationIdごとにもできるため、ログを用いた調査をよりスムーズにする助けにもなります。
目的に合った形にログを出力し、プロジェクトの作業効率を高めていきましょう。