【问题标题】:How to setup a non global NLog custom layout renderer not bound to a specific thread?如何设置未绑定到特定线程的非全局 NLog 自定义布局渲染器?
【发布时间】:2013-06-17 19:03:12
【问题描述】:

我有一个作业引擎,它可以并行运行多个作业。这些作业本身可能是多线程的。

我想通过自定义布局渲染器公开一个特定于工作的信息。到目前为止,我看到的解决方案建议使用 GDC、NDC 或 MDC 工具中的任何一个。例如 - http://nlog-forum.1685105.n2.nabble.com/Custom-Layout-Renderers-Runtime-Values-td4065731.html

这种方法不好,因为我要公开的信息是每个作业,既不是全局的也不是线程本地的。作业执行可能涉及来自线程池和/或显式创建的不同线程。

我希望对现有职位代码进行尽可能少的更改。例如,我知道我可能需要更改日志实例的获取方式或生命周期(实例与静态),但我当然不想更改日志消息。

有什么想法吗?

【问题讨论】:

  • 您是否有办法确定当前正在运行的“作业”(在给定的日志调用站点)?你的“工作”是一个对象,还是更抽象的概念?你能举一个包含日志调用站点的“作业运行”代码的例子吗?如果您在日志记录调用站点上知道您正在运行什么作业,那么我认为您有一个简单的解决方案。
  • 作业只是一个带有通过反射调用的入口点方法的 .NET 类。调用反射的代码具有我想作为布局渲染器公开的值,我可以将它公开给作业本身,但我宁愿避免它。
  • 所以您希望从作业中发出的日志记录语句显示(通过 LayoutRenderers 或其他机制)“作业运行器”具有的值?
  • 是的。由于我不控制线程切换,我无法使用线程本地上下文。我也明白所有的记录器都应该是实例而不是静态成员。但我不想更改实际的日志记录代码(例如,除了让它在生命范围更改后编译)
  • 您可以查看 System.Diagnostics.CorrelationManager.ActivityId。它使用 CallContext.LogicalSetData 存储。因此,它的值将从一个线程“流动”到它的任何子线程。然后,您可以将其与您自己的“上下文”对象(如 GDC 和 MDC)结合起来,这实际上是一个字典字典,由 GUID(ActivityId 的类型)键入。作业运行器在作业上设置 GUID,并根据该 guid 将值存储在“上下文”中。编写相应的 LayoutRenderer,您可以从作业运行器中设置值,作业日志语句将记录这些值。

标签: nlog


【解决方案1】:

如果您愿意使用 Logger 的 Log 方法,那么您可以创建一个 LogEventInfo 并将您额外的作业特定值存储在 LogEventInfo 的 Properties 中(类似于 GDC 和 MDC,除了每个 LogEventInfo 都有自己的实例)。

因此,您的代码可能如下所示:

void RunJob(object job)
{
  string name;
  int id;
  DateTime started;

  GetSomeParametersFromJob(job, out name, out id, out started);

  var le = new LogEventInfo(LogLevel.Info, logger.Name, "Hello from RunJob");
  le.Properties.Add("JobName", name);
  le.Properties.Add("JobId", id);
  le.Properties.Add("JobStarted", started);

  logger.Log(le);
}

日志调用可以被清理一些它不那么冗长,但你明白了。只需将所需的参数添加到 LogEventInfo 类的 Properties 字典中,然后您就可以使用 EventContext 布局渲染器将这些值记录下来。你可以像这样配置它:

<targets>
    <target name="file" xsi:type="File" layout="${longdate} | ${level} | ${logger} | JobName = ${event-context:JobName} | JobId = ${event-context:JobId} | JobStarted = ${event-context:JobStarted} | ${message}" fileName="${basedir}/${shortdate}.log" />
</targets>

您可以使用“变量”清理配置:

<variable name="JobName" value = "${event-context:JobName}" />
<variable name="JobId" value = "${event-context:JobId}" />
<variable name="JobStarted" value = "${event-context:JobStarted}" />
<variable name="JobLayout" value="${longdate} | ${level} | ${logger} | ${JobName} | ${JobId} | ${JobStarted} | $ ${message}"/>

<targets>
  <target name="file" 
          xsi:type="File" 
          layout="${JobLayout}" 
          fileName="${basedir}/${shortdate}.log" />
</targets>

更新

这是另一个想法...您可以编写自己的“上下文”对象(类似于 GDC 和 MDC),该对象由可以识别您的工作的东西作为键。您可能希望使用 CallContext 来保存您的额外参数,因为可以将值放入 CallContext 以便它们“流向”子线程。再说一次,如果您将值从作业运行器放入上下文中,您不希望它们流向所有子线程,您只希望它们流向正在运行的作业。所以,也许一开始可以将它们放入全局数据中,但这可能会导致瓶颈......无论如何......这会如何工作?

这一切都很粗略,但我认为它传达了这个想法。这是否是一个好主意?我会让你做法官。从好的方面来说,您的日志记录站点没有改变,您可以设置“上下文”参数,而无需比使用 GDC/MDC 付出更多的努力。不利的一面是,要编写一些代码,访问全局字典可能会遇到瓶颈。

创建您自己的全局字典(类似于此处的 NLog 的 GlobalDiagnosticContext https://github.com/NLog/NLog/blob/master/src/NLog/GlobalDiagnosticsContext.cs)。使 Add API 具有 GUID 类型的额外参数。因此,您在开始作业之前存储参数的代码可能如下所示:

string name;
int id;
DateTime started;

GetSomeParametersFromJob(job, out name, out id, out started);

GUID jobActivity = Guid.NewGuid();
JobRunnerNamespace.JobContext.Add(jobActivity, "JobName", name);
JobRunnerNamespace.JobCotnext.Add(jobActivity, "JobId", id);
JobRunnerNamespace.JobContext.Add(jobActivity, "JobStarted", started);

job.Activity = jobActivity;

job.Run();

在作业内部,当它启动时,它将 System.Diagnostics.CorrelationManager.ActivityId 设置为输入 guid:

public class MyJob
{
  private Guid activityId;

  public Guid
  {
    set 
    {
      activityId = value;
    }
    get
    {
      return activityId;
    }
  }

  public void Run()
  {
    System.Diagnostics.CorrelationManager.ActivityId = activityId;

    //Do whatever the job does.

    logger.Info("Hello from Job.Run.  Hopefully the extra context parameters will get logged!");
  }
}

JobContextLayoutRenderer 看起来像这样:

[LayoutRenderer("JobContext")]
public class JobContextLayoutRenderer : LayoutRenderer
{
    [RequiredParameter]
    [DefaultParameter]
    public string Item { get; set; }

    protected override void Append(StringBuilder builder, LogEventInfo logEvent)
    {
        Guid activity = System.Diagnostics.CorrelationManager.ActivityId;

        string msg = JobContext.Get(activity, this.Item);
        builder.Append(msg);
    }
}

JobContext 看起来像这样(我们现在只考虑 Set 和 Get):

public static class JobContext
{
    private static Dictionary<Guid, Dictionary<string, string>> dict = new Dictionary<Guid, Dictionary<string, string>>();

    public static void Set(Guid activity, string item, string value)
    {
        lock (dict)
        {
            if (!dict.ContainsKey(activity))
            {
              dict[activity] = new Dictionary<string, string>();
            }
            var d = dict[activity];
            lock (d) //Might need to lock this dictionary
            {
              d[activity][item] = value;
            }
        }
    }

    /// <summary>
    /// Gets the Global Diagnostics Context named item.
    /// </summary>
    /// <param name="item">Item name.</param>
    /// <returns>The item value of string.Empty if the value is not present.</returns>
    public static string Get(Guid activity, string item)
    {
        lock (dict)
        {
            string s = string.Empty;

            var d = dict.TryGetValue(activity, d);
            if (d != null)
            {
              lock(d) //Might need to lock this dictionary as well
              {
                if (!d.TryGetValue(item, out s))
                {
                  s = string.Empty;
                }
              }
            }

            return s;
        }
    }
}

【讨论】:

  • +1 用于清理部分,但是,这不是我想要的。更改所有日志记录语句是不可行的。
  • 我会检查你的方法。很有趣。
  • 你觉得blog.stephencleary.com/2013/04/…怎么样?
  • 它看起来很有趣,虽然我不能完全欣赏它,因为我做了很少的异步编程。我也不确定如何适用于您的问题,但我可能会遗漏一些东西。
猜你喜欢
  • 2018-11-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多