简体   繁体   English

NLog - 数据库目标的运行时参数

[英]NLog - Run-Time Parameters for Database Target

I am attempting to add some customization to my database log target.我正在尝试向我的数据库日志目标添加一些自定义。 In my NLog.config, I have this:在我的 NLog.config 中,我有这个:

<target name="DatabaseExample1" xsi:type="Database"
 dbProvider="System.Data.SqlClient"
 dbDatabase="${event-context:item=dbDatabase}"
 dbUserName="${event-context:item=dbUserName}"
 dbPassword="${event-context:item=dbPassword}"
 dbHost="${event-context:item=dbHost}"
 commandText="${event-context:item=commandText}">
</target>

And in my C# code, I have this:在我的 C# 代码中,我有这个:

protected override void updateBeforeLog(LogEventInfo info)
{
    info.Properties["dbDatabase"] = "TempDB";
    info.Properties["dbUserName"] = "username";
    info.Properties["dbPassword"] = "password";
    info.Properties["dbHost"] = "SERVER\\SQLSERVER";
    info.Properties["commandText"] = "exec InsertLog @LogDate, @LogLevel, @Location, @Message";
    
    info.Parameters = new DatabaseParameterInfo[] {
        new DatabaseParameterInfo("@LogDate", Layout.FromString("${date:format=yyyy\\-MM\\-dd HH\\:mm\\:ss.fff}")), 
        new DatabaseParameterInfo("@LogLevel", Layout.FromString("${level}")),
        new DatabaseParameterInfo("@Location", Layout.FromString("${event-context:item=location}")),
        new DatabaseParameterInfo("@Message", Layout.FromString("${event-context:item=shortmessage}"))
    };
    
    log.Log(info);
}

But I'm getting a SQL error that reads "Must declare the scalar variable "@LogDate"".但是我收到一个 SQL 错误,内容为“必须声明标量变量“@LogDate””。

The properties are working because the connection succeeds.由于连接成功,这些属性正在工作。 But for some reason the parameters are not "binding" to the scalar variables in the command.但是由于某种原因,参数没有“绑定”到命令中的标量变量。

If I create the parameters manually in the NLog.config file, it works perfectly:如果我在 NLog.config 文件中手动创建参数,它可以完美运行:

<target name="DatabaseExample1" xsi:type="Database"
 dbProvider="System.Data.SqlClient"
 dbDatabase="${event-context:item=dbDatabase}"
 dbUserName="${event-context:item=dbUserName}"
 dbPassword="${event-context:item=dbPassword}"
 dbHost="${event-context:item=dbHost}"
 commandText="${event-context:item=commandText}">
   <parameter name="@LogDate" layout="${date:format=yyyy\-MM\-dd HH\:mm\:ss.fff}" />
   <parameter name="@LogLevel" layout="${level}" />
   <parameter name="@Location" layout="${event-context:item=location}" />
   <parameter name="@Message" layout="${event-context:item=shortmessage}" />
</target>

But that defeats the whole purpose of being able to customize the commandText and parameter values at run-time.但这违背了能够在运行时自定义 commandText 和参数值的全部目的。

What do I need to do to cause the target to correctly pick up the values for the parameters?我需要做什么才能使目标正确获取参数值?

update更新

Overall, I want to have a way to customize the targets via C# code.总的来说,我想要一种通过 C# 代码自定义目标的方法。 I want to rely on the NLog.config file only has much as needed.我想依靠 NLog.config 文件只需要多少。 But it seems like NLog is pretty tied to the config file settings, and I'm trying to work within that constraint but be as flexible as possible.但似乎 NLog 与配置文件设置非常相关,我正在尝试在该约束范围内工作,但要尽可能灵活。 With this database target, if I can figure out how to update the parameters programmatically, then I can have a fairly generic target in the config file, then update the LogEventInfo properties and parameters to suit the needs of whatever database to connect to, or stored procedure to execute.有了这个数据库目标,如果我能弄清楚如何以编程方式更新参数,那么我就可以在配置文件中有一个相当通用的目标,然后更新 LogEventInfo 属性和参数以满足要连接或存储的任何数据库的需要执行的程序。

update更新

It turns out that the LogEventInfo.Parameters collection is used for the LogEventInfo.FormattedMessage property.事实证明, LogEventInfo.Parameters集合用于LogEventInfo.FormattedMessage属性。 If you want to use the LogEventInfo.FormatProvider or even set LogEventInfo.Message equal to a string.format string, then the Parameters object[] array is used to provide the substitutions in the string.如果要使用LogEventInfo.FormatProvider或者甚至设置LogEventInfo.Message等于A string.format字符串,则Parameters对象[]数组被用于提供该串中的取代。 See here for the code . 代码见这里

Despite the similar naming, LogEventInfo.Parameters does not correspond to <target ><parameter /></target> in the NLog.config file.尽管命名相似,但LogEventInfo.Parameters与 NLog.config 文件中的<target ><parameter /></target>并不对应。 And there does not appear to be a way to get to the database parameters via the LogEventInfo object.而且似乎没有办法通过LogEventInfo对象获取数据库参数。 (Thanks to Kim Christensen over at NLog project forum for that link) (感谢 Kim Christensen 在 NLog 项目论坛上提供的链接)


I was able to get this working using a custom target.我能够使用自定义目标来完成这项工作。 But I am still questioning why my previous approach did not work.但我仍然质疑为什么我以前的方法不起作用。 It really seems like that if the Parameters array is accessible, that NLog should honor the parameters that are assigned to it.看起来,如果Parameters数组是可访问的,那么 NLog 应该尊重分配给它的参数。

That said, here is the code that I ended up using:也就是说,这是我最终使用的代码:

First I had to create the custom target and set it up to send data to the database:首先,我必须创建自定义目标并将其设置为将数据发送到数据库:

[Target("DatabaseLog")]
public sealed class DatabaseLogTarget : TargetWithLayout
{
  public DatabaseLogTarget()
  {
  }

  protected override void Write(AsyncLogEventInfo logEvent)
  {
    //base.Write(logEvent);
    this.SaveToDatabase(logEvent.LogEvent);
  }

  protected override void Write(AsyncLogEventInfo[] logEvents)
  {
    //base.Write(logEvents);
    foreach (AsyncLogEventInfo info in logEvents)
    {
      this.SaveToDatabase(info.LogEvent);
    }
  }

  protected override void Write(LogEventInfo logEvent)
  {
    //string logMessage = this.Layout.Render(logEvent);
    this.SaveToDatabase(logEvent);
  }

  private void SaveToDatabase(LogEventInfo logInfo)
  {
    if (logInfo.Properties.ContainsKey("commandText") &&
      logInfo.Properties["commandText"] != null)
    {
      //Build the new connection
      SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();

      //use the connection string if it's present
      if (logInfo.Properties.ContainsKey("connectionString") && 
        logInfo.Properties["connectionString"] != null)
        builder.ConnectionString = logInfo.Properties["connectionString"].ToString();

      //set the host
      if (logInfo.Properties.ContainsKey("dbHost") &&
        logInfo.Properties["dbHost"] != null)
        builder.DataSource = logInfo.Properties["dbHost"].ToString();

      //set the database to use
      if (logInfo.Properties.ContainsKey("dbDatabase") &&
        logInfo.Properties["dbDatabase"] != null)
        builder.InitialCatalog = logInfo.Properties["dbDatabase"].ToString();

      //if a user name and password are present, then we're not using integrated security
      if (logInfo.Properties.ContainsKey("dbUserName") && logInfo.Properties["dbUserName"] != null &&
        logInfo.Properties.ContainsKey("dbPassword") && logInfo.Properties["dbPassword"] != null)
      {
        builder.IntegratedSecurity = false;
        builder.UserID = logInfo.Properties["dbUserName"].ToString();
        builder.Password = logInfo.Properties["dbPassword"].ToString();
      }
      else
      {
        builder.IntegratedSecurity = true;
      }

      //Create the connection
      using (SqlConnection conn = new SqlConnection(builder.ToString()))
      {
        //Create the command
        using (SqlCommand com = new SqlCommand(logInfo.Properties["commandText"].ToString(), conn))
        {
          foreach (DatabaseParameterInfo dbi in logInfo.Parameters)
          {
            //Add the parameter info, using Layout.Render() to get the actual value
            com.Parameters.AddWithValue(dbi.Name, dbi.Layout.Render(logInfo));
          }

          //open the connection
          com.Connection.Open();

          //Execute the sql command
          com.ExecuteNonQuery();
        }
      }
    }
  }
}

Next, I updated my NLog.config file to include a rule for the new target:接下来,我更新了我的 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">
  <targets async="true">
    <target name="DatabaseLog1" xsi:type="DatabaseLog" />
  </targets>
  <rules>
    <logger name="LogDB"  minlevel="Trace" writeTo="DatabaseLog1" />
  </rules>
</nlog>

Then I created a class to wrap my database logging calls.然后我创建了一个类来包装我的数据库日志记录调用。 It also provides a function to convert an Exception into an NLog LogEventInfo object:它还提供了将Exception转换为 NLog LogEventInfo对象的函数:

public class DatabaseLogger
{
  public Logger log = null;

  public DatabaseLogger()
  {
    //Make sure the custom target is registered for use BEFORE using it
    ConfigurationItemFactory.Default.Targets.RegisterDefinition("DatabaseLog", typeof(DatabaseLogTarget));

    //initialize the log
    this.log = NLog.LogManager.GetLogger("LogDB");
  }
  
  /// <summary>
  /// Logs a trace level NLog message</summary>
  public void T(LogEventInfo info)
  {
    info.Level = LogLevel.Trace;
    this.Log(info);
  }
  
  /// <summary>
  /// Allows for logging a trace exception message to multiple log sources.
  /// </summary>
  public void T(Exception e)
  {
    this.T(FormatError(e));
  }
  
  //I also have overloads for all of the other log levels...
  
  /// <summary>
  /// Attaches the database connection information and parameter names and layouts
  /// to the outgoing LogEventInfo object. The custom database target uses
  /// this to log the data.
  /// </summary>
  /// <param name="info"></param>
  /// <returns></returns>
  public virtual void Log(LogEventInfo info)
  {
    info.Properties["dbHost"] = "SQLServer";
    info.Properties["dbDatabase"] = "TempLogDB";
    info.Properties["dbUserName"] = "username";
    info.Properties["dbPassword"] = "password";
    info.Properties["commandText"] = "exec InsertLog @LogDate, @LogLevel, @Location, @Message";
    
    info.Parameters = new DatabaseParameterInfo[] {
      new DatabaseParameterInfo("@LogDate", Layout.FromString("${date:format=yyyy\\-MM\\-dd HH\\:mm\\:ss.fff}")), 
      new DatabaseParameterInfo("@LogLevel", Layout.FromString("${level}")),
      new DatabaseParameterInfo("@Location", Layout.FromString("${event-context:item=location}")),
      new DatabaseParameterInfo("@Message", Layout.FromString("${event-context:item=shortmessage}"))
    };

    this.log.Log(info);
  }


  /// <summary>
  /// Creates a LogEventInfo object with a formatted message and 
  /// the location of the error.
  /// </summary>
  protected LogEventInfo FormatError(Exception e)
  {
    LogEventInfo info = new LogEventInfo();

    try
    {
      info.TimeStamp = DateTime.Now;

      //Create the message
      string message = e.Message;
      string location = "Unknown";

      if (e.TargetSite != null)
        location = string.Format("[{0}] {1}", e.TargetSite.DeclaringType, e.TargetSite);
      else if (e.Source != null && e.Source.Length > 0)
        location = e.Source;

      if (e.InnerException != null && e.InnerException.Message.Length > 0)
        message += "\nInnerException: " + e.InnerException.Message;

      info.Properties["location"] = location;

      info.Properties["shortmessage"] = message;

      info.Message = string.Format("{0} | {1}", location, message);
    }
    catch (Exception exp)
    {
      info.Properties["location"] = "SystemLogger.FormatError(Exception e)";
      info.Properties["shortmessage"] = "Error creating error message";
      info.Message = string.Format("{0} | {1}", "SystemLogger.FormatError(Exception e)", "Error creating error message");
    }

    return info;
  }
}

So when I start my application, I can start logging easily:所以当我启动我的应用程序时,我可以轻松地开始记录:

DatabaseLogger dblog = new DatabaseLogger();
dblog.T(new Exception("Error message", new Exception("Inner message")));

With a slight bit of effort, I can inherit from the DatabaseLogger class and override the Log method to create any number of different database logs.只需稍加努力,我就可以从DatabaseLogger类继承并覆盖Log方法来创建任意数量的不同数据库日志。 I can update the connection info dynamically if I want.如果需要,我可以动态更新连接信息。 I can change the commandText and Parameters to suit each database call.我可以更改commandTextParameters以适应每个数据库调用。 And I just have to have the one target.我只需要有一个目标。

If I want to provide functionality for multiple database types, I can add a info.Properties["dbProvider"] property which gets read in the SaveToDatabase method, which can then spawn off a different connection type.如果我想为多种数据库类型提供功能,我可以添加一个info.Properties["dbProvider"]属性,该属性在SaveToDatabase方法中读取,然后可以产生不同的连接类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM