はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】「messagetemplates-csharp」を用いてC#でMessageTemplateを扱う

はじめに

今回はmessagetemplates-csharpというC#用のMessageTemplateライブラリを紹介したいと思います。

github.com

概要

MessageTemplateとは

Unity公式のLoggingパッケージであるUnity Loggingなんかでも採用されているのですが、構造化ロギングを実現する際にMessageTemplateが利用されることがあります。

www.hanachiru-blog.com

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翻訳
メッセージ・テンプレートは、イベント・データのための名前付きホールを持つフォーマット指定子です。

Message Templates

以下の画像が分かりやすいですが、値を埋め込みたい箇所に{}で囲ってプロパティ名を記載します。

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"
  //    }
https://messagetemplates.org/

一見String.Formatっぽいなと思った人はかなり勘が鋭いです。実はUser {1} logged in from {0}のように書いたとしても、MessageTemplateString.Formatと同じ挙動をします。MessageTemplateString.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

概要

MessageTemplateのC#用ライブラリです。メンテ自体は最近全然されていないライブラリではありますが、一応使えます。

github.com

インストール方法

Nugetから取得します。以下はRiderでのサンプル画像です。

NuGetから取得する

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するという以下の画像のような流れのサンプルになっています。

https://messagetemplates.org/

詳細な仕様

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