NLog自定義Target之MQTT

NLog是.Net中最流行的日誌記錄開源項目(之一),它靈活免費開源

官方支援文件網路(Tcp、Udp)、資料庫控制台等輸出

社區支援ElasticSeq等日誌平台輸出

實時日誌需求

在工業物聯網等特定場景下需要實時獲取日誌資訊

工業物聯網領域常用的是mqtt協議

那我們就使用NLog 自定義一個Target,將日誌輸出到MqttServer

Web通過Mqtt(websocket)實時獲取日誌,而不是傳統的通過WebApi輪詢日誌

NLog自定義Target

  1. 官方文檔介紹了如何自定義Target,可以獲取到一串日誌消息,無法獲取結構化消息
  2. 需要時用使用自定義Field來完成這部分工作
/// <summary>
/// Additional field details
/// </summary>
[NLogConfigurationItem]
public class Field
{
    /// <summary>
    /// Name of additional field
    /// </summary>
    [RequiredParameter]
    public string Name { get; set; }

    /// <summary>
    /// Value with NLog <see cref="NLog.Layouts.Layout"/> rendering support
    /// </summary>
    [RequiredParameter]
    public Layout Layout { get; set; }

    /// <summary>
    /// Custom type conversion from default string to other type
    /// </summary>
    /// <remarks>
    /// <see cref="System.Object"/> can be used if the <see cref="Layout"/> renders JSON
    /// </remarks>
    public Type LayoutType { get; set; } = typeof(string);

    /// <inheritdoc />
    public override string ToString()
    {
        return $"Name: {Name}, LayoutType: {LayoutType}, Layout: {Layout}";
    }
}
  1. 重寫Write方法
protected override void Write(LogEventInfo logEvent)
{
    //default fields
    Dictionary<string, object> logDictionary = new()
    {
        { "timestamp", logEvent.TimeStamp },
        { "level", logEvent.Level.Name },
        { "message", RenderLogEvent(Layout, logEvent) }
    };

    //customer fields
    //這裡都處理為字元串了,有優化空間
    foreach (var field in Fields)
    {
        var renderedField = RenderLogEvent(field.Layout, logEvent);

        if (string.IsNullOrWhiteSpace(renderedField))
            continue;

        logDictionary[field.Name] = renderedField;
    }

    SendTheMessage2MqttBroker(JsonConvert.SerializeObject(logDictionary));
}

使用

下面將使用Nlog.Target.MQTT,演示通過web實時查看應用程式的日誌

  1. 創建WebApi項目
  2. 引用NLog.Target.MQTT

  1. 配置文件
<extensions>
    <add assembly="NLog.Web.AspNetCore"/>
    <!--<add assembly="NLog.Targets.MQTT"/>-->
    <add assembly="NLog.Targets.MQTT"/>
</extensions>

<!-- the targets to write to -->
<targets>
    <!-- MQTT Target  -->
    <target xsi:type="MQTT" name="mqtt" host="localhost" port="1883" username="UserName"  password="Password" topic="log"
            layout="${longdate}|${event-properties:item=EventId_Id:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}|${callsite}" >
        <field name="machine" layout="${machinename}" />
        <field name="processid" layout="${processid}" />
        <field name="threadname" layout="${threadname}" />
        <field name="logger" layout="${logger}" />
        <field name="callsite" layout="${callsite-linenumber}" />
        <field name="url" layout="${aspnet-request-url}" />
        <field name="action" layout="${aspnet-mvc-action}" />
        <field name="level" layout="${level:uppercase=true}" />
        <field name="message" layout="${message}" />
        <field name="exception" layout="${exception:format=toString}" />
    </target>
</targets>

<!-- rules to map from logger name to target -->
<rules>
    <logger name="*" minlevel="Trace" writeTo="mqtt" />
</rules>
  1. 配置MQTTServer和NLog
// ...
// NLog: Setup NLog for Dependency injection
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
builder.Host.UseNLog();

//AddHostedMqttServer
builder.Services.AddHostedMqttServer(mqttServer =>
    {
        mqttServer.WithoutDefaultEndpoint();
    })
    .AddMqttConnectionHandler()
    .AddConnections();

//Config Port
builder.WebHost.UseKestrel(option =>
{
    option.ListenAnyIP(1883, l => l.UseMqtt());
    option.ListenAnyIP(80);
});
var app = builder.Build();

// ...
//UseStaticFiles html js etc.
app.UseStaticFiles();
app.UseRouting();

//Websocket Mqtt
app.UseEndpoints(endpoints =>
{
    //MqttServerWebSocket
    endpoints.MapConnectionHandler<MqttConnectionHandler>("/mqtt", options =>
    {
        options.WebSockets.SubProtocolSelector = MqttSubProtocolSelector.SelectSubProtocol;
    });
});
// ...
  1. Web連接MqttServer
// ...    
<script src="./jquery.min.js"></script>
<script src="./mqtt.min.js"></script>
<script src="./vue.js"></script>
// ...

var client = mqtt.connect('ws://' + window.location.host + '/mqtt', options);
client.on('connect',
    function() {
        client.subscribe('log',
            function(err) {
                if (!err) {
                    console.log("subed!");
                } else {
                    alert("subed error!");
                }
            });
    });
client.on('message',
    function(topic, message) {
        if (topic === 'log') {
            if (app.logs.length > 50)
                app.logs.length = 0;
            app.logs.unshift($.parseJSON(message.toString()));
        }
    });
// ...
  1. 輸出日誌
// ...  
_logger.LogDebug("LogDebug!");
_logger.LogError(new Exception("Exception Message!"), "LogError!");

//new thread output log after 500ms
Thread thread = new Thread(ThreadProc);
thread.Name = "My Thread";
thread.Start();
// ...
  1. 實時查看日誌
    訪問index.html

  2. 通過Mqtt客戶端訂閱日誌

源碼

在這裡NLog.Targets.MQTT://github.com/iioter/NLog.Targets.MQTT

相關鏈接

[1] NLog.Targets.MQTT://github.com/iioter/NLog.Targets.MQTT

[2] IoTGateway-Doc://iotgateway.net/blog/NLog

[3] NLog自定義Target://github.com/NLog/NLog/wiki/How-to-write-a-custom-target

交流

公眾號:工業物聯網網關 QQ群:712105424