Serilog输出到Hangfire上下文控制台

6
我希望能够通过Hangfires上下文控制台使用我的日志来发出事件,以便我不需要使用context.writeline将日志事件输出到我的hangfire仪表板。
我已经尝试实现一个特定的Serilog sink以达到此目的。但是,由于我需要来自hangfire的PerformContext(在运行时注入到任务方法中),因此我无法在应用程序启动时配置日志池。我尝试在任务方法内创建一个新的记录器,只是为了看看是否可以工作,但它并没有起作用 - 有人能够看到为什么它不起作用,或者可能建议另一种方法吗?
下面是该汇流处:
 class HangfireContextSink : ILogEventSink {
        private readonly IFormatProvider formatProvider;
        private readonly PerformContext context;
        public HangfireContextSink(IFormatProvider formatProvider, PerformContext context) {
            this.formatProvider = formatProvider;
            this.context = context;
        }
        public void Emit(LogEvent logEvent) {
            var message = logEvent.RenderMessage(formatProvider);
            context.WriteLine(ConsoleTextColor.Blue, DateTimeOffset.Now.ToString() + " " + message);
        }

水槽配置:

 public static class SinkExtensions {
        public static LoggerConfiguration HangfireContextSink(this LoggerSinkConfiguration loggerSinkConfiguration, PerformContext context, IFormatProvider formatProvider = null) {
            return loggerSinkConfiguration.Sink(new HangfireContextSink(formatProvider, context));
        }
    }

任务方法:
 public static bool TestJob(PerformContext context) {
            using (LogContext.PushProperty("Hangfirejob", "TestJob")) {
                try {
                    using (var hangfireLog = new LoggerConfiguration().WriteTo.HangfireContextSink(context).CreateLogger()) {
                        var progress = context.WriteProgressBar("Progress");
                        for (int i = 0; i < 10; i++) {
                            context.WriteLine("Working with {0}", i);
                            progress.SetValue((i + 1) * 10);
                            Log.Debug("Test serilog");
                            hangfireLog.Debug("Test from hangfirelog");
                            Thread.Sleep(5000);
                        }
                    }
                    Log.Debug("Done testjob");
                    return true;
                } catch (Exception ex) {
                    Log.Error(ex, "Error!");
                    return false;
                }
            }
        }
1个回答

9

因为您使用Debug日志级别记录消息,而默认的Serilog级别为Information,所以消息不会记录到Hangfire控制台中。您可以通过在LoggerConfiguration上调用.MinimumLevel.Debug()来更改记录级别。此外,如果要通过Serilog.Log静态类记录消息,您应该设置它的Logger属性。

这是一个修复后的代码,它将记录日志到Hangfire控制台:

using (LogContext.PushProperty("Hangfirejob", "TestJob"))
{
    try
    {
        using (var hangfireLog = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.HangfireContextSink(context).CreateLogger())
        {
            //  This is a dirty code that will be removed in final solution
            var prevLogger = Log.Logger;
            Log.Logger = hangfireLog;

            var progress = context.WriteProgressBar("Progress");
            for (int i = 0; i < 10; i++)
            {
                context.WriteLine("Working with {0}", i);
                progress.SetValue((i + 1) * 10);
                Log.Debug("Test serilog");
                hangfireLog.Debug("Test from hangfirelog");
                Thread.Sleep(5000);
            }
            Log.Debug("Done testjob");
            Log.Logger = prevLogger;
        }
        return true;
    }
    catch (Exception ex)
    {
        Log.Error(ex, "Error!");
        return false;
    }
}

这个代码可以运行,但是为每个作业创建新日志的总体方法非常不好。你可以通过在LogEvent中的属性中传递PerformContext实例来避免这种情况。您应该定义一个自定义属性类,该属性类从抽象的LogEventPropertyValue派生,并公开PerformContext实例。

以下是最终代码:

PerformContextProperty.cs:

public class PerformContextProperty : LogEventPropertyValue
{
    public PerformContext PerformContext { get; }

    public PerformContextProperty(PerformContext performContext)
    {
        PerformContext = performContext;
    }

    public override void Render(TextWriter output, string format = null, IFormatProvider formatProvider = null)
    {
    }
}

PerformContextEnricher.cs:

public class PerformContextEnricher : ILogEventEnricher
{
    private readonly PerformContext performContext;

    public PerformContextEnricher(PerformContext performContext)
    {
        this.performContext = performContext;
    }

    public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
    {
        logEvent.AddPropertyIfAbsent(new LogEventProperty(HangfireContextSink.PerformContextProperty, new PerformContextProperty(performContext)));
    }
}

TestJob.cs:

public class TestJob
{
    public static bool Execute(PerformContext context)
    {
        using (LogContext.PushProperty("Hangfirejob", "TestJob"))
        using (LogContext.Push(new PerformContextEnricher(context)))
        {
            try
            {
                var progress = context.WriteProgressBar("Progress");
                for (int i = 0; i < 10; i++)
                {
                    context.WriteLine("Working with {0}", i);
                    progress.SetValue((i + 1) * 10);
                    Log.Debug("Test serilog", context);
                    Log.Debug("Test from hangfirelog");
                    Thread.Sleep(5000);
                }
                Log.Debug("Done testjob");
                return true;
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Error!");
                return false;
            }
        }
    }
}

HangfireContextSink.cs:

class HangfireContextSink : ILogEventSink
{
    public const string PerformContextProperty = "PerformContext";

    private readonly IFormatProvider formatProvider;

    public HangfireContextSink(IFormatProvider formatProvider)
    {
        this.formatProvider = formatProvider;
    }

    public void Emit(LogEvent logEvent)
    {
        var message = logEvent.RenderMessage(formatProvider);

        LogEventPropertyValue propertyValue;
        if (logEvent.Properties.TryGetValue(PerformContextProperty, out propertyValue))
        {
            var context = (propertyValue as PerformContextProperty)?.PerformContext;
            context?.WriteLine(ConsoleTextColor.Green, DateTimeOffset.Now + " " + message);
        }
    }
}

SinkExtensions.cs:

public static class SinkExtensions
{
    public static LoggerConfiguration HangfireContextSink(this LoggerSinkConfiguration loggerSinkConfiguration, IFormatProvider formatProvider = null)
    {
        return loggerSinkConfiguration.Sink(new HangfireContextSink(formatProvider));
    }
}

Serilog配置:

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .MinimumLevel.Debug()
    .WriteTo.HangfireContextSink()
    .CreateLogger();

在Serilog配置中,不要忘记调用.Enrich.FromLogContext()以便从LogContext中获取属性丰富日志事件。
以上代码非常简单,所以我没有详细评论它。如果您对代码有疑问,请提出并我会尽量解释不清楚的部分。

我现在正在浏览它,下班了,看起来正是我想要的 - 我知道每个Hangfire作业的一次性日志非常混乱(而且我简直不敢相信我忘记了最小级别!)。Enricher方法看起来正是我需要的“漂亮”方法 - 非常感谢! - Dynde
快速跟进 - 我有一个问题,使用这个sink会阻止其他sink的输出。我似乎无法弄清楚阻塞发生在哪里,而且无论loggerconfiguration中哪个sink首先链接都没有关系。Hangfire上的Context接收到消息,但其他的sink却没有 :/ - Dynde
1
我似乎已经缩小了范围——它并没有阻止所有的sinks,只有SEQ sink,并且看起来是一个格式问题,因为SEQ sink需要json可格式化的属性值。我正在研究一个简单的解决方案。 - Dynde
我将PerformContextProperty从抽象基类更改为继承自ScalarValue。也许应该弄清楚如何正确渲染属性以支持JSON格式,但目前我只需要快速修复它。 - Dynde
你可以通过在PerformContextProperty类的Render()方法中删除NotImplementedException的抛出来解决此问题。我已经在我的回答中进行了编辑 - 只需将此方法保留为空即可。这样做是可以的,因为PerformContextProperty.Render()仅用于PerformContext属性,并且不会带来任何有用的有效负载。但是,这种方法将在结果JSON中输出一个值为空字符串的PerformContext属性。我希望这对您来说不是问题,因为没有简单的方法可以抑制从输出中添加的属性。 - CodeFuller
我确实尝试了空的渲染方法(并在渲染方法中尝试生成我认为可以转换为JSON格式的内容),但是我在所有尝试中都没有成功。继承ScalarValue是一个简单的解决方法,也没有引起任何副作用,所以我对这个解决方案感到满意 - 感谢您的帮助! - Dynde

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接