はじめに
今回はmessagetemplates-csharp
というC#用のMessageTemplateライブラリを紹介したいと思います。
概要
MessageTemplateとは
Unity公式のLoggingパッケージであるUnity Logging
なんかでも採用されているのですが、構造化ロギングを実現する際にMessageTemplate
が利用されることがあります。
A language-neutral specification for 1) capturing, and 2) rendering, structured log events in a format that’s both human-friendly and machine-readable.
// DeepL翻訳
構造化されたログイベントを、1) 取り込み、2) 人間にやさしく機械が読めるフォーマットでレンダリングするための、言語にとらわれない仕様。
A message template is a format specifier with named holes for event data
// DeepL翻訳
メッセージ・テンプレートは、イベント・データのための名前付きホールを持つフォーマット指定子です。
以下の画像が分かりやすいですが、値を埋め込みたい箇所に{
と}
で囲ってプロパティ名を記載します。
User {username} logged in from {ip_address}
ロギングAPIでは引数として値を渡すと、引数の順番でプロパティ名が対応することになります。({0}
, {1}
のようにプロパティ名にインデックスを指定した場合は特殊な挙動になるので注意)
log("User {username} logged in from {ip_address}","alice", "123.45.67.89") // -> { // "time": "2016-05-27T13:02:11.888", // "template": "User {username} logged in from {ip_address}", // "username": "alice", // "ip_address": "123.45.67.89" // }
一見String.Format
っぽいなと思った人はかなり勘が鋭いです。実はUser {1} logged in from {0}
のように書いたとしても、MessageTemplate
はString.Format
と同じ挙動をします。MessageTemplate
はString.Format
を内包しているより表現力の高いものになっています。
より詳細の設定
詳細は公式のSyntax ~ Rendering semantics
の箇所を見て欲しいですが、上記は基本仕様で一部特殊なものがあります。
- Property names are written between { and } brackets
- Brackets can be escaped by doubling them, e.g. {{ will be rendered as {
- Property names may be prefixed with an optional operator, @ or $, to control how a property is captured
- Property names may be suffixed with an optional format, e.g. :000, to control how the property is rendered; the formatting semantics are application-dependent, and thus require the formatted value to be captured alongside the raw property if rendering is to take place in a different environment
// DeepL翻訳
- プロパティ名は
{
と}
の大括弧の間に記述します。- 括弧は二重にしてエスケープすることができ、例えば
{{
は{
として表示されます。- プロパティ名の前にオプションの演算子、
@
または$
を付けることができます。- プロパティ名には、プロパティがどのようにレンダリングされるかを制御するために、オプションのフォーマット、例えば
:000
を接尾辞として付けることができます。フォーマットのセマンティクスはアプリケーション依存であり、したがって、レンダリングが異なる環境で行われる場合には、生のプロパティと一緒にフォーマットされた値をキャプチャする必要があります。
// EBNF(Extended Backus–Naur Form)での定義 Text ::= ([^\{] | '{{' | '}}')+ Name ::= [0-9a-zA-Z_]+ Index::= [0-9]+ Format ::= [^\}]+ Template ::= (Text | Hole)* Hole ::= '{' ('@' | '$')? (Name | Index) (',' Alignment)? (':' Format)? '}' Alignment ::= '-'?[0-9]+
grammar/message-template.ebnf at master · messagetemplates/grammar · GitHub
messagetemplates-csharp
インストール方法
Nuget
から取得します。以下はRider
でのサンプル画像です。
nuget.org : NuGet Gallery | MessageTemplates 1.0.0-rc-00275
利用方法
MessageTemplate
の説明でしたことをこのライブラリを用いて実装すると以下のようになります。
string template = "User {username} logged in from {ip_address}"; // -------------------------------------------------------------------------------------------------- // MessageTemplate型の変数を作成するパターン // -------------------------------------------------------------------------------------------------- // templateからMessageTemplateを作成(プロパティを抽出して保持しておく) MessageTemplate? parsed = MessageTemplate.Parse(template); // プロパティ名と値の対応 TemplatePropertyList? list = MessageTemplate.Capture(template, "alice", "123.45.67.89"); foreach (TemplateProperty? templateProperty in list) { // username, "alice" // ip_address, "123.45.67.89" Console.WriteLine(templateProperty.Name + ", " + templateProperty.Value); } // User "alice" logged in from "123.45.67.89" Console.WriteLine(parsed.Render(new TemplatePropertyValueDictionary(list))); // -------------------------------------------------------------------------------------------------- // MessageTemplate.Formatを使うパターン(内部実装的にはMessageTemplateを生成してからCaptureしてRenderを実行している(つまり↑の操作と同じ)) // -------------------------------------------------------------------------------------------------- // User "alice" logged in from "123.45.67.89" Console.WriteLine(MessageTemplate.Format("User {username} logged in from {ip_address}", "alice", "123.45.67.89"));
MessageTemplate
を作成しておき、Capture
してRender
するという以下の画像のような流れのサンプルになっています。
詳細な仕様
MessageTemplate
の細かい仕様は実装によるものが多々あるのですが、messagetemplates-csharp
では以下のようになっています。
- Property names are written between { and } brackets
- Brackets can be escaped by doubling them, e.g. {{ will be rendered as {
- Formats that use numeric property names, like {0} and {1} exclusively, will be matched with the Format method's parameters by treating the property names as indexes; this is identical to string.Format()'s behaviour
- If any of the property names are non-numeric, then all property names will be matched from left-to-right with the Format method's parameters
- Property names may be prefixed with an optional operator, @ or $, to control how the property is serialised
- The destructuring operator (@) in front of will serialize the object passed in, rather than convert it using ToString().
the stringification operator ($) will convert the property value to a string before any other processing takes place, regardless of its type or implemented interfaces.
- Property names may be suffixed with an optional format, e.g. :000, to control how the property is rendered; these format strings behave exactly as their counterparts within the string.Format() syntax
GitHub - messagetemplates/messagetemplates-csharp: A C# implementation of Message Templates
具体的には以下の通りです。
public static class Program { public static void Main(string[] args) { // 出力 : 10 , 0 , 20 // String.Formatの機能も内包している Console.WriteLine(MessageTemplate.Format("{1} , {0} , {2}", 0, 10, 20)); // 出力 : "I sat at "a chair" // NOTE: オブジェクトの場合はToStringして埋め込まれる Console.WriteLine(MessageTemplate.Format("I sat at {Chair}", new Chair())); // 出力 : "I sat at Chair { Back: \"straight\", Legs: [1, 2, 3, 4] }" // NOTE: プロパティ名の前に「@」をつけると、ToStringではなくシリアライズされる Console.WriteLine(MessageTemplate.Format("I sat at {@Chair}", new Chair())); // 出力 : "I sat at "a chair" // NOTE: プロパティ名の前に「$」をつけると、他の処理が行われる前にプロパティ値を文字列に変換する Console.WriteLine(MessageTemplate.Format("I sat at {$Chair}", new Chair())); // 出力 : "I'm 20.00" // NOTE: プロパティ名の後に「:000」のようなオプション書式をつけることもできる Console.WriteLine(MessageTemplate.Format("I'm {Age:00.00}", 20)); } } class Chair { public string Back => "straight"; public int[] Legs => new[] {1, 2, 3, 4}; public override string ToString() => "a chair"; }
さいごに
ここまで書いておいてなんですが、最後のコミットが6年前だったり、最新でもnetstandard1.3対応だったりとなかなかに古いライブラリです。
www.nuget.org
今はMicrosoft.Extensions.Loggingが主流だと思います。MS製ですしね。
github.com