1

I have a job engine, which runs several jobs in parallel. The jobs may be multithreaded themselves.

There is a job specific information I would like to expose via custom layout renderers. The solutions I have seen so far suggest using either of GDC, NDC or MDC facilities. For example - http://nlog-forum.1685105.n2.nabble.com/Custom-Layout-Renderers-Runtime-Values-td4065731.html

This approach is not good, because the information I want to expose is per job, which is neither global nor thread local. A job execution may involve different threads, from the thread pool and/or explicitly created.

I want to make as less as possible changes to the existing job code. For instance, I understand I might need to change how the log instances are obtained or life scoped (instance vs static), but I certainly do not want to change the log messages.

Any ideas?

mark
  • 59,016
  • 79
  • 296
  • 580
  • Do you have a way of determining what "job" is currently running (at a given logging call site)? Is your "job" an object, or is it more abstract concept? Can you give an example of some "job running" code that contains a logging call site? If you know at the logging call site what job you are running, then I think that there is an easy solution for you. – wageoghe Jun 17 '13 at 19:10
  • A job is just a .NET class with an entry point method invoked through reflection. The code that invokes the reflection has the values I want to expose as layout renderers, I can expose it to the job itself, but I'd rather avoid it. – mark Jun 17 '13 at 19:23
  • So you want the logging statements issued from within the jobs to display (through LayoutRenderers or some other mechanism) the values that the "job runner" has? – wageoghe Jun 18 '13 at 13:40
  • Yes. Since I do not control the thread switching I cannot use the thread local contexts. I also understand all the loggers should be instance rather than static members. But I do not want to change the actual logging code (except making it compile following the life scope change, for instance) – mark Jun 18 '13 at 13:54
  • You could look at System.Diagnostics.CorrelationManager.ActivityId. It is stored using CallContext.LogicalSetData. As such, its value will be "flowed" from a thread to any of its child threads. You could then combine that with your own "context" object (like GDC and MDC), which would actually be a dictionary of dictionaries, keyed by GUID (which is the type of ActivityId). Job runner sets GUID on job and stores values in "context" based on that guid. Write a corresponding LayoutRenderer, and you could set values from job runner and job logging statements would log those values. – wageoghe Jun 18 '13 at 14:44
  • If this sounds like it might interesting to you, I could work up some sample code. I have a picture in my mind of how it would work and I think that it might be a reasonable approach, given your requirements. – wageoghe Jun 18 '13 at 14:45
  • I did not know about this thing. Will it work with the thread pool threads? They are not children of the main thread. The way I see it, I need to create a Logger instance in the job base class prior to invocation of the job entry point and change the code to use that logger all along. In addition, the logger factory method must be exposed in the job base class, so that new instances could be created. Of course, this is not optimal at all. – mark Jun 18 '13 at 15:13
  • I've added a section to my answer that shows another approach that might be useful. – wageoghe Jun 18 '13 at 15:56
  • System.Diagnostics.CorrelationManager.ActivityId is "flowed" to child threads, including thread pool threads. Here is a question about using ActivityId in the context of threads and tasks. http://stackoverflow.com/questions/4340948/how-do-tasks-in-the-task-parallel-library-affect-activityid – wageoghe Jun 18 '13 at 16:02

1 Answers1

2

If you are willing to use the Logger's Log method, then you can create a LogEventInfo and store your extra job-specific values in the LogEventInfo's Properties (similar to GDC and MDC, except that each LogEventInfo has its own instance).

So, your code might look something like this:

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);
}

The logging call could be cleaned up some that it is less verbose, but you get the idea. Simply add your desired parameters to the Properties dictionary on the LogEventInfo class, then you can log those values out using the EventContext layout renderer. You would configure it something like this:

<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>

You could clean up the configuration by using "variables":

<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>

UPDATE

Here is another idea... You could write your own "context" object (similar to GDC and MDC) that is keyed by something that would identify your job. You might want to use the CallContext to hold your extra parameters, as it is possible to put values into the CallContext such that they will be "flowed" to child threads. Then again, if you are putting the values in the context from the job runner, you don't want them flowed to all child threads, you only want them flowed to the job that is being run. So, maybe to start they could be put into global data, but that might lead to a bottleneck... At any rate... How might this work?

This is all pretty rough, but I think it conveys the idea. Whether or not it is a good idea? I'll let you be the judge. On the plus side, your logging sites are unchanged, you can set the "context" parameters without much more effort than it would have taken if you had used GDC/MDC. On the minus side, there is some code to write, there could be a bottleneck in accessing the global dictionary.

Create your own global dictionary (similar to NLog's GlobalDiagnosticContext here https://github.com/NLog/NLog/blob/master/src/NLog/GlobalDiagnosticsContext.cs). Make the Add API have an extra parameter of type GUID. So, your parameter storing code before starting a job might look like this:

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();

Inside the job, when it starts, it sets System.Diagnostics.CorrelationManager.ActivityId to the input 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!");
  }
}

The JobContextLayoutRenderer would look something like this:

[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 would look something like this (let's just consider Set and Get for now):

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;
        }
    }
}
wageoghe
  • 27,390
  • 13
  • 88
  • 116
  • +1 for cleanup section, however, this is not what I am looking for. It is not feasible to change all the logging statements. – mark Jun 17 '13 at 21:44
  • I will check your approach. It is interesting. – mark Jun 18 '13 at 20:56
  • What do you think of http://blog.stephencleary.com/2013/04/cancellationtoken-behavior-with-asyncex.html?m=1 ? – mark Jun 19 '13 at 11:48
  • It looks interesting enough, although I can't fully appreciate it as I have done very little async programming. I'm also not sure how applies to your problem, but I could be missing something. – wageoghe Jun 19 '13 at 15:44
  • My bad, it is http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html – mark Jun 19 '13 at 18:29
  • That is a good blog post. System.Diagnostics.CorrelationManager.ActivityId that I suggested above is stored using CallContext.LogicalSetData. Here is an article about CallContext by Jeffrey Richter: http://www.wintellect.com/blogs/jeffreyr/logical-call-context-flowing-data-across-threads-appdomains-and-processes You can also search for "CallContext" and/or "ActivityId" and/or "CorrelationManager" and my username here on SO and find more about context, threading, etc. – wageoghe Jun 19 '13 at 18:58
  • I think I am convinced. I have written a unit test to try and ascertain that things work as expected in 4.0 despite what Stephen claims in his blog, you can find the unit test here - http://pastebin.com/tAgRfjrg. – mark Jun 19 '13 at 19:27
  • Good luck! If you are strictly interested in the ActivityId, you could use S.D.CM.ActivityId. It is stored using CallContext.LogicalSetData. – wageoghe Jun 20 '13 at 19:37
  • I have opted in favour of using `CallContext` directly. – mark Jun 20 '13 at 20:13