しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Event Hubs に構造化されたログを吐き出す TraceListener を作った

普通に TraceListener を作ると、既に文字列に変換されたログを書き出すしか出来ませんが、Trace クラスには Error / Warning / Infomation を出し分けるメソッドが用意されているので、そういった情報を保持したままログを書き出したいと思いました。

TraceListener は Write と WriteLine が abstract になっていますが、それ以外のメソッドも virtual になっているのでオーバーライドが可能です。

なので、Event Hubs に JSON で構造化したログを吐き出す TraceListener を作成してみました。

public class EventHubTraceListener : TraceListener
{
    public EventHubTraceListener()
        : this(ConfigurationManager.AppSettings["Microsoft.ServiceBus.ConnectionString"])
    {
    }

    public EventHubTraceListener(string connectionString)
    {
        _client = EventHubClient.CreateFromConnectionString(connectionString);
    }

    private readonly EventHubClient _client;

    public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
    {
        TraceData(eventCache, source, eventType, id, new[] { data });
    }

    public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data)
    {
        if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, data))
        {
            return;
        }

        SendTraceEvent(eventCache, eventType, string.Join(",", data));
    }

    public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id)
    {
        TraceEvent(eventCache, source, eventType, id, string.Empty);
    }

    public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
    {
        TraceEvent(eventCache, source, eventType, id, message, new object[0]);
    }

    public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
    {
        if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, format, args, null, null))
        {
            return;
        }

        SendTraceEvent(eventCache, eventType, string.Format(format, args));
    }

    public override void Write(string message)
    {
        var obj = new
        {
            Message = message
        };

        var json = JsonConvert.SerializeObject(obj);

        _client.Send(new EventData(Encoding.UTF8.GetBytes(json)));
    }

    public override void WriteLine(string message)
    {
        Write(message);
    }

    private void SendTraceEvent(TraceEventCache eventCache, TraceEventType eventType, string message)
    {
        var traceData = new TraceEventData
        {
            EventType = eventType.ToString(),
            DateTime = eventCache.DateTime,
            ProcessId = eventCache.ProcessId,
            ThreadId = eventCache.ThreadId,
            Timestamp = eventCache.Timestamp,
            Callstack = eventCache.Callstack,
            Message = message
        };

        var json = JsonConvert.SerializeObject(traceData);

        _client.Send(new EventData(Encoding.UTF8.GetBytes(json)));
    }

    public class TraceEventData
    {
        public string EventType { get; set; }
        public DateTime DateTime { get; set; }
        public int ProcessId { get; set; }
        public string ThreadId { get; set; }
        public long Timestamp { get; set; }
        public string Callstack { get; set; }
        public string Message { get; set; }
    }
}

Event Hubs の接続文字列は initializeData で渡してもいい気がしましたが、App Settings から取るようにしました。カスタム属性を使うことも考えましたが、初期化のタイミングがずれるので止めておきました。

使う場合は Web.config や App.config で TraceListener と Service Bus 接続文字列を追加します。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/>
  </startup>
  <appSettings>
    <!-- Service Bus specific app setings for messaging connections -->
    <add key="Microsoft.ServiceBus.ConnectionString"
         value="Endpoint=sb://{namespace}.servicebus.windows.net/;SharedAccessKeyName=Send;SharedAccessKey={key};EntityPath={eventhub name}"/>
  </appSettings>
  <system.diagnostics>
    <trace>
      <listeners>
        <add name="eventhub" type="ConsoleApplication27.EventHubTraceListener, ConsoleApplication27" />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

設定が終われば、あとは Trace.TraceError などのメソッドを呼び出せば、Event Hubs にログの JSON が流れていきます。簡単に確認するために、Azure Functions の Event Hubs Trigger を使ってみました。

コードは単純で、流れてきたメッセージをログに書き出すだけです。

書いたコードを保存すると自動的に実行が開始されるので、設定が上手くいっていれば Logs に流れてきた JSON が表示されていきます。

あとは Json.NET などを使ってデシリアライズすれば、EventType などで処理を分けることも出来そうです。

Azure Functions を使えばデータを DocumentDB や Azure Table に追加できるので、DocumentDB に格納したデータを更に Azure Search に投入して検索といったことも可能になるのではないかと。

思った以上に Azure Functions が便利なので、いろんな場所で使ってみたいと思いました。