innominate227
innominate227

Reputation: 11703

NLog in Azure Function Log Streams using nlog.config

My question is very similar to this one: How can I get NLog output to appear in the streaming logs for an Azure Function?. In that question the accepted answer shows how you can configure nlog at the start of the function call. I would like to configure via an nlog.config file or at the very least configure just once at setup and not on every call to the function.

With the code below I see NLog messages in the Application Insight logs, but not in the Log Stream. I would like to get the messages logged from NLog to show in Log Streams as well.

Function code

    private static readonly Logger _logger = NLog.LogManager.GetCurrentClassLogger();

    [FunctionName("TestLog")]
    public static async Task<IActionResult> TestLog([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
                                                HttpRequest req, ILogger log)
    {
        //shows in both Application Insights and Log Stream.
        log.LogInformation("Log From ILogger");

        //shows in Application Insights but not in Log Stream.
        _logger.Info("Log From NLog");

        return new OkObjectResult("OK");
    }

FunctionStartUp

    [assembly: FunctionsStartup(typeof(MyNamespace.TestFunctionApp.Startup))]
    
    namespace MyNamespace.TestFunctionApp
    {            
        public class Startup : FunctionsStartup
        {            
            public override void Configure(IFunctionsHostBuilder builder)
            {
                //nLog file ends 1 directory up from bins when deployed
                var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                var rootDirectory = Path.GetFullPath(Path.Combine(binDirectory, ".."));
                var nLogConfigPath = Path.Combine(rootDirectory, "NLog.config");
        
                //create MicrosoftILoggerTarget (I think this has to be done from code since it needs a ref to an ILogger instance).  But this does not seem to work.
                var loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory();
                var azureILogger = loggerFactory.CreateLogger("NLog");
                var loggerTarget = new NLog.Extensions.Logging.MicrosoftILoggerTarget(azureILogger);
        
                //setup NLog
                LogManager.Setup()
                          .SetupExtensions(e => e.AutoLoadAssemblies(false))
                          .LoadConfigurationFromFile(nLogConfigPath, optional: false)
                          .LoadConfiguration(builder => builder.Configuration.AddRuleForAllLevels(loggerTarget));            
             }
        }
   }

NLog.config

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <extensions>
    <add assembly="Microsoft.ApplicationInsights.NLogTarget" />
  </extensions>
  <targets>
    <target name="a" xsi:type="ApplicationInsightsTarget"/>
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="a" />
  </rules>
</nlog>

Upvotes: 0

Views: 2392

Answers (4)

Rolf Kristensen
Rolf Kristensen

Reputation: 19867

You can activate log stream from log-files, so it will monitor any files ending in .txt or .log, that are stored in the HOME-folder. Then you can use a File-target:

<?xml version="1.0" encoding="utf-8"?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <extensions>
    <add assembly="Microsoft.ApplicationInsights.NLogTarget" />
  </extensions>
  <targets>
    <target name="a" xsi:type="ApplicationInsightsTarget"/>
    <target name="logfile" xsi:type="File" filename="${environment:HOME:cached=true}/logfiles/application/app-${shortdate}-${processid}.txt" />
  </targets>
  <rules>
    <logger name="*" minlevel="Info" writeTo="logfile, a" />
  </rules>
</nlog>

See also: https://github.com/NLog/NLog.Extensions.Logging/wiki/NLog-cloud-logging-with-Azure-function-or-AWS-lambda#writing-to-azure-diagnostics-log-stream

Upvotes: 0

Rolf Kristensen
Rolf Kristensen

Reputation: 19867

Time for my 3rd answer. With NLog.Extensions.Logging ver. 1.7.1 then MicrosoftILoggerTarget can take ILoggerFactory as input-parameter, and one can override the LoggerName.

So you can setup like this:

    var msLoggerFactory = serviceProvider.GetService<ILoggerFactory>();
    var nLogToMsLogTarget = new MicrosoftILoggerTarget(msLoggerFactory);
    nLogToMsLogTarget.LoggerName = "${mdlc:FunctionName}";

And use it like this:

    [FunctionName("TestLog")]
    public async Task<IActionResult> TestLog([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
                                                HttpRequest req, Microsoft.Extensions.Logging.ILogger log)
    {
        using (NLog.MappedDiagnosticsLogicalContext.SetScoped("FunctionName", log.ToString())
        {
             log.LogInformation("Log From ILogger");

             LogManager.GetCurrentClassLogger().Info("Log From NLog");

             var other = new SomeOtherClassWithNLog();
             other.SomeMethod();

             return new OkObjectResult("OK");
        }
    }

No longer restricted with a single static Logger-Name for MicrosoftILoggerTarget

Upvotes: 1

innominate227
innominate227

Reputation: 11703

I managed to get something that works starting from Rolf's answers. But it is hacky, and only makes sense if you have just 1 Function in your function app.

FunctionStartUp code:

    public class Startup : FunctionsStartup
    {

        public override void Configure(IFunctionsHostBuilder builder)
        {
            builder.Services.AddSingleton<NLog.Logger>(SetupNLog);
        }
        
        private NLog.Logger SetupNLog(IServiceProvider serviceProvider)
        {
            //find NLog.config
            var executionContextOptions = serviceProvider.GetService<IOptions<ExecutionContextOptions>>().Value;
            var appDirectory = executionContextOptions.AppDirectory;
            var nLogConfigPath = Path.Combine(appDirectory, "NLog.config");

            //setup target to forward NLog logs to ILogger
            //NOTE: string passed to CreateLogger must match "function.<function name>" or logs will not show in Log Stream
            var msLoggerFactory = serviceProvider.GetService<ILoggerFactory>();
            var msLogger = msLoggerFactory.CreateLogger("Function.TestLog");
            var nLogToMsLogTarget = new MicrosoftILoggerTarget(msLogger);
            
            //setup NLog
            LogManager.Setup()
                      .SetupExtensions(e => e.AutoLoadAssemblies(false))
                      .LoadConfigurationFromFile(nLogConfigPath, optional: false)
                      .LoadConfiguration(c => c.Configuration.AddRuleForAllLevels(nLogToMsLogTarget));

            //return an NLog logger
            return LogManager.GetLogger("NLog");
        }
   }

Function code:

    public class Function1
    {

        private readonly NLog.Logger _logger;

        public Function1(NLog.Logger logger)
        {
            _logger = logger;
        }

        [FunctionName("TestLog")]
        public async Task<IActionResult> TestLog([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
                                                    HttpRequest req, Microsoft.Extensions.Logging.ILogger log)
        {
            log.LogInformation("Log From ILogger");
    
            _logger.Info("Log From NLog");

            var other = new SomeOtherClassWithNLog();
            other.SomeMethod();
    
            return new OkObjectResult("OK");
        }
    }


        public class SomeOtherClassWithNLog
        {
           private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

           public void SomeMethod()
           {
               _logger.Info("Logs From Another Class"); 
           }

        }

I have also found that Azure seems to filter logs shown in the log stream to be only those with "Host.*" or "Function.<FunctionName>.*" in the category name. Its filtered even further when looking at the monitor page for a single Function in that case it filters to just "Function.<FunctionName>.*".

So you are stuck either viewing in the Function App level log stream where lots of "not my app" logs also get written. Or viewing in just one of the function level Log streams even if that's not the function your debugging.

I think in the end I will just use the default ILogger, and be ok with the fact that code logging direct to an NLog logger isn't visible in the Log Stream viewer.

Upvotes: 1

Rolf Kristensen
Rolf Kristensen

Reputation: 19867

Usually the goal is to have the output from Microsoft ILogger to reach NLog Targets. This is done by calling UseNLog() or AddNLog(), that will add NLog as LoggingProvider.

Alternative one have an existing library that uses NLog already, but wants the output to be redirected to the Microsoft ILoggerFactory. But one have to be careful if also wanting to add NLog as LoggingProvider because of cyclic/recursive nightmare.

But I guess you can do the following to redirect all output from NLog to Microsoft ILoggerFactory:

    public class Startup : FunctionsStartup
    {            
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var serviceProvider = builder.Services.BuildServiceProvider();
            var executionContextOptions = serviceProvider.GetService<IOptions<ExecutionContextOptions>>().Value;
            var appDirectory = executionContextOptions.AppDirectory;

            var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
                          
            // Setup NLog redirect all logevents to Microsoft ILoggerFactory
            var nlogLoggerName = "NLog";
            var nlogLogger = loggerFactory.CreateLogger(nlogLoggerName);
            var nlogTarget = new NLog.Extensions.Logging.MicrosoftILoggerTarget(nlogLogger);
            var nLogConfigPath = Path.Combine(appDirectory, "NLog.config");
    
            //setup NLog
            LogManager.Setup()
                      .SetupExtensions(e => e.AutoLoadAssemblies(false))
                      .LoadConfigurationFromFile(nLogConfigPath, optional: false)
                      .LoadConfiguration(builder => {
                          // Ignore output from logger named "NLog" to avoid recursion
                          builder.Configuration.Rules.Add(new LoggingRule() { LoggerNamePattern = nlogLoggerName, MaxLevel = LogLevel.Off, Final = true });
                          builder.Configuration.AddRuleForAllLevels(loggerTarget));
                      });
         }
    }

Upvotes: 1

Related Questions