I am using Serilog in an Asp dot net core application. I want my my log files to be human readable, but also be able to be parsed easily.
The problem that I have come across is that exceptions are logged with line breaks. Some Microsoft events have messages that include new lines.
I would like to be able to parse the log file with one event per line.
I could write my own implementation of ITextFormatter that replaces new lines with \r\n, but that we mean I would need to duplicate much of the logic in MessageTemplateTextFormatter and other classes.
After digging into this for a while, I was able to come up with an answer. Andy West's answer pointed me in the right direction.
There are two separate issues here: CRLFs in the message and CRLFs in the exception.
I was able to solve the message problem by changing "{Message}" to "{Message:j}" in the outputTemplate.
Changing the exception was a little trickier. I had to add an enricher:
class ExceptionEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (logEvent.Exception == null)
return;
var logEventProperty = propertyFactory.CreateProperty("EscapedException", logEvent.Exception.ToString().Replace("\r\n", "\\r\\n"));
logEvent.AddPropertyIfAbsent(logEventProperty);
}
}
This adds and new property called EscapedException. This has to be added to the configuration with .Enrich.With().
Then I replaced "{Exception}" with "{EscapedException}" in the outputTemplate.
This technique will remove all CRLF. First a new ITextFormatter.
public class RemoveCrLf : ITextFormatter
{
private const int DefaultWriteBuffer = 256;
private readonly ITextFormatter _textFormatter;
/// <summary>
///
/// </summary>
/// <param name="textFormatter"></param>
public RemoveCrLf(ITextFormatter textFormatter)
{
_textFormatter = textFormatter;
}
/// <summary>
///
/// </summary>
/// <param name="logEvent"></param>
/// <param name="output"></param>
public void Format(LogEvent logEvent, TextWriter output)
{
var buffer = new StringWriter(new StringBuilder(DefaultWriteBuffer));
_textFormatter.Format(logEvent, buffer);
var logText = buffer.ToString();
output.WriteLine(logText.Trim().Replace("\n","\\n").Replace("\r","\\r"));
output.Flush();
}
}
Use it like this
configuration.WriteTo.Console(new RemoveCrLf(new MessageTemplateTextFormatter("[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Exception}")));
Of course, customise the output template as needed.
In reference to the solution from #Andrew Radford, that solution was working for me only on windows, but not on linux docker environment as expected. Therefore
I have enhanced the solution for messages as well as exceptions.
I have used Environment.NewLine for case matching in Regex which will pick the case based on the hosted environment.
Add following class for Messages.
public class MessageEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (logEvent.MessageTemplate == null)
return;
var logEventProperty = propertyFactory.CreateProperty("EscapedMessage", Regex.Replace(logEvent.MessageTemplate.ToString(), Environment.NewLine, "[Newline]"));
logEvent.AddPropertyIfAbsent(logEventProperty);
}
}
Use following class for Exceptions.
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
if (logEvent.Exception == null)
return;
var logEventProperty = propertyFactory.CreateProperty("EscapedException", Regex.Replace(logEvent.Exception.ToString(), Environment.NewLine, "[Newline]"));
logEvent.AddPropertyIfAbsent(logEventProperty);
}
Then add these both helper classes into program.cs
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(Configuration)
.Enrich.With(new ExceptionEnricher())
.Enrich.With(new MessageEnricher())
.Enrich.FromLogContext()
.CreateLogger();
Update the appsettings output template
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u4} {SourceContext:l} : {EscapedMessage}{NewLine}{EscapedException}{NewLine}"
You didn't specify how you are logging exceptions, but assuming you are using something like:
Log.Error("An error occurred: {Exception}", exception);
then the exception will be rendered using ToString(). In that case, couldn't you simply do this instead:
Log.Error("An error occurred: {Exception}", exception.ToString().Replace(System.Environment.NewLine, "\\r\\n"));
Of course, you could refactor this into a method (maybe an extension method) if you need to do it more than one place.
Related
I need to set up logs in my asp.net application. It's easy to add output to the console, but I need to configure it in Azure. I don't know how to do it. I need to log all information that occurs with my app into some file and read it.
The ILoggerFactory allows an app to use any implementation of ILogger and ILoggerProvider.
For details on how to implement the interfaces properly, look at the framework's ConsoleLogger and ConsoleLoggerProvider. See also the ASP.NET Core documentation on logging.
Here is a minimal example of a custom ILogger to get started. This is not production code, rather, it demos enough technical depth either to write your own ILogger or to use one from the community.
project.json
"dependencies": {
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging": "1.0.0-rc1-final",
"Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final"
}
MyLoggingProvider.cs
namespace LoggingExample
{
using Microsoft.Extensions.Logging;
public class MyLoggingProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new MyLogger();
}
public void Dispose()
{
// TODO Cleanup
}
}
}
MyLogger.cs
In Azure you will want to write to somewhere other than C:/temp/some-guid.txt. This is enough to get you started, though, with writing your own simple logger.
namespace LoggingExample
{
using System;
using Microsoft.Extensions.Logging;
public class MyLogger : ILogger
{
public void Log(LogLevel logLevel, int eventId, object state,
Exception exception, Func<object, Exception, string> formatter)
{
var builder = new StringBuilder();
if (formatter != null) {
builder.AppendLine(formatter(state, exception));
}
var values = state as ILogValues;
if (values != null) {
foreach (var v in values.GetValues()) {
builder.AppendLine(v.Key + ":" + v.Value);
}
}
var logPath = string.Format("C:/temp/{0}.txt", Guid.NewGuid());
File.WriteAllText(logPath, builder.ToString());
}
public bool IsEnabled(LogLevel logLevel) {
return true;
}
public IDisposable BeginScopeImpl(object state) {
return null;
}
}
}
Startup.cs
Now in startup you can use add your logger via loggerFactory.AddProvider(new MyLoggingProvider()). Every call to the ILogger will now log with your provider.
namespace LoggingExample
{
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.Extensions.Logging;
public class Startup
{
public void Configure(
IApplicationBuilder app,
ILoggerFactory loggerFactory)
{
loggerFactory
.AddConsole(minLevel: LogLevel.Verbose)
.AddProvider(new MyLoggingProvider());
app.Run(async (context) =>
{
var logger = loggerFactory.CreateLogger("CatchAll");
logger.LogInformation("Hello logger!");
await context.Response.WriteAsync("Hello world!");
});
}
}
}
MyController.cs
Anywhere that supports dependency injection can now receive an ILogger that will log to all of the providers that we registered in the Startup.Configure method.
namespace LoggingExample
{
using Microsoft.AspNet.Mvc;
using Microsoft.Extensions.Logging;
public class MyController : Controller
{
public MyController(ILogger logger)
{
logger.LogInformation("Logging from my controller");
}
}
}
Log4Net
Use Log4Net. Its a common framework for logging that everyone who follows up on your code will understand, and it lets you do things like attach a new log "destination" on the fly just by editing your config file. It already covers most of the things you'll want to do (like create a separate file for each "day"), and most of the log mining tools out there will be able to read the files l4n creates.
Setting it Up
There are tutorials online for how to get started, but they basically require a few simple steps:
Download the Log4Net nuget package.
Adjust the log settings in your web.config file
Create a static instance of the logger object
Log Stuff wherever you need to. If you decide you want your logger to write to a file, it will. If you add a database writer, it will write to the db too. Want your log entries to show up in console, just add that logger in your default (debug) config.
Once you get it setup, logging is as simple as this code:
...
} catch(SystemException ex) {
logger.Error("This error was thrown by the XXX routine", ex);
}
Hope that's helpful.
Edit: Config File + Core
As #auga points out in his oh-so-helpful comment, config for ASP.Net 5 may require you to read carefully the link I added under step #2 above (configuring your logger). Instead of re-writing someone else's blog post, I'll just link to the article I used to set this up in our ASP.NET 5 environment. Works really well.
If you're reading this post to learn (instead of skimming it to critique), I'd suggest following the links...
I am in the early stages of developing a new module.
Much of it is laid out in terms of the models etc. Also have the migrations all set up and my database now has the tables for my module.
I am encountering the following error when calling ContentManager.New<myPart> and would like some help please.
Error is this:
An unhandled exception has occurred and the request was terminated. Please refresh the page. If the error persists, go back
Specified cast is not valid.
System.InvalidCastException: Specified cast is not valid.
at Orchard.ContentManagement.ContentCreateExtensions.New[T]
(IContentManager manager, String contentType)
The chunk of code that fires the exception is this:
public static T New<T>(this IContentManager manager, string contentType) where T : class, IContent {
var contentItem = manager.New(contentType);
if (contentItem == null)
return null;
var part = contentItem.Get<T>();
if (part == null)
throw new InvalidCastException();
return part;
}
Here are the various parts to my module that are related to the operation i am struggling with:
ContentPart
public class GoogleMapsSettingsPart : ContentPart<GoogleMapsSettingsPartRecord>
{
public string ApiKey {
get { return Record.ApiKey; }
set { Record.ApiKey = value; }
}
}
ContentPartRecord
public class GoogleMapsSettingsPartRecord : ContentPartRecord
{
public virtual string ApiKey { get; set; }
}
Handler
public GoogleMapsSettingsPartHandler(IRepository<GoogleMapsSettingsPartRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
Migration for this table
// Settings Table
SchemaBuilder.CreateTable("GoogleMapsSettingsPartRecord", table => table
.ContentPartRecord()
.Column("ApiKey", DbType.String, c => c.WithLength(60))
);
Some of the code from the controller for this model etc
public AdminController(IContentManager contentManager, IShapeFactory shapeFactory, IServiceLocatorService serviceLocatorService, INotifier notifier)
{
_contentManager = contentManager;
_serviceLocatorService = serviceLocatorService;
_notifier = notifier;
Shape = shapeFactory;
T = NullLocalizer.Instance;
}
/// <summary>
/// Display Settings
/// </summary>
/// <returns></returns>
public ActionResult Settings()
{
var settings = _serviceLocatorService.GoogleMapsSettings;
var editor = CreateSettingsEditor(settings);
var model = _services.ContentManager.BuildEditor(settings);
return View((object)model);
}
Finally - the Services where my call throws this exception
private GoogleMapsSettingsPart _settings;
public GoogleMapsSettingsPart GoogleMapsSettings
{
get {
if (_settings == null)
{
_settings = _contentManager.Query<GoogleMapsSettingsPart, GoogleMapsSettingsPartRecord>().List().FirstOrDefault();
if (_settings == null)
{
_settings = _contentManager.New<GoogleMapsSettingsPart>("GoogleMapsSettings");
}
}
return _settings;
}
}
The actual line where the exception happens is _settings = _contentManager.New<GoogleMapsSettingsPart>("GoogleMapsSettings");
I have tried all sorts of stuff in place of "GoogleMapsSettings" though nothing is working.
I'm pretty sure at this point it's something simple, though it's avoiding me..My limited knowledge of Orchard is stumping me
Any help would be appreciated :)
The exception is thrown because your content type does not have the part you specified to get.
_contentManager.New<GoogleMapsSettingsPart>("GoogleMapsSettings");
This method call creates a new content item of type GoogleMapSettings and gets the content item as a GoogleMapsSettingsPart. However, it seems that GoogleMapSettings content type does not have a GoogleMapsSettingsPart. That's why the exception gets thrown here.
var part = contentItem.Get<T>();
if (part == null)
throw new InvalidCastException();
You must either attach the part dynamically to your content type or do it in a migration (or manually in the admin, but that's not a good idea). Your migration should look like this.
this.ContentDefinitionManager.AlterTypeDefinition("GoogleMapsSettings",
alt => alt
.WithPart("GoogleMapsSettingsPart");
Ok, so I fixed it...
My understanding of how Orchard works is still very much in the learning stages.
for this particular operation I didn't want to have a content type in the admin - though not sure why after adding the ContentType it still didn't work...
anyway, adding the lines below to my handler took care of the rest. I believe it's actually creating a temporary type so one isn't needed in the system.
public GoogleMapsSettingsPartHandler(IRepository<GoogleMapsSettingsPartRecord> repository)
{
Filters.Add(new ActivatingFilter<GoogleMapsSettingsPart>("GoogleMapsSettings"));
Filters.Add(StorageFilter.For(repository));
Filters.Add(new TemplateFilterForRecord<GoogleMapsSettingsPartRecord>("GoogleMapsSettings", "Parts/GoogleMapsSettings"));
}
I'v got the same error, but in my case it was everything ok with migration class.
The reason was unlucky merge, which deleted my driver class of my part.
Just look at this code of Activating method of ContentPartDriverCoordinator class. In my case there was no partInfo for my content part and resulted part became ContentPart, so casting throws an exception
var partInfos = _drivers.SelectMany(cpp => cpp.GetPartInfo()).ToList();
foreach (var typePartDefinition in contentTypeDefinition.Parts) {
var partName = typePartDefinition.PartDefinition.Name;
var partInfo = partInfos.FirstOrDefault(pi => pi.PartName == partName);
var part = partInfo != null
? partInfo.Factory(typePartDefinition)
: new ContentPart { TypePartDefinition = typePartDefinition };
context.Builder.Weld(part);
}
I've come across a weird problem in my MVC4 (RC) application. (running on .NET 4.0)
I have just setup Elmah for logging exceptions / errors.
I basically installed the Elmah.MVC and elmah.sqlserver NuGet packages. (versions 2.0.0 and 1.2 respectively)
It seemed to work fine out of the box - I can go to the elmah page and view errors:
http://myserver/elmah
for example, if I create some 404 errors, they appear in this log.
What is not working is this: I have a standard MVC controller with a [HttpPost] action. I've set it up so it will always throw an exception:
public class TestController : Controller
{
[HttpPost]
[ValidateInput(false)]
public void Testing()
{
throw new Exception("uh oh");
}
}
I then try to post data to this controller via jQuery:
$.post('/Test/Testing', {test_data: 'This is some test data'});
Ok, this works. The response returns the typical yellow screen of death, and the error is caught and logged in Elmah.
However, if I try to post something like XML/HTML the error is not logged in Elmah. I still get the same response from the server back (yellow screen of death), but nothing in Elmah.
$.post('/Test/Testing', {test_data: '<test><test1>This is some test data</test1></test>'});
Why? It doesn't make sense.
Notice I have already turned off the request validation on the action. If I didn't do that, then posting XML/HTML data would cause this exception:
A potentially dangerous Request.Form value was detected from the client
NuGet would also refuse to log that exception too - which I believe is a bug:
http://code.google.com/p/elmah/issues/detail?id=217
So what is the cause of this problem that I'm experiencing? It it a bug related to the issue I found above?
It just seems quite an unfortunate situation that I can't log exceptions just because the request contained XML/HTML.
Surely there is a way around this?
ELMAH does not catch HttpRequestValidationException by default and if a user sends an invalid request it will be missed in ELMAH's report. so it's necessary to define and use this global filter as well:
public class ElmahRequestValidationErrorFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is HttpRequestValidationException)
ErrorLog.GetDefault(HttpContext.Current).Log(new Error(context.Exception));
}
}
I have a work around for now, which someone suggested on http://code.google.com/p/elmah/issues/detail?id=217
You can force ASP to use the older request validation logic by adding this into the <system.web> section of your Web.config:
<httpRuntime requestValidationMode="2.0" />
This will effect the app globally which is not really that great of a situation.
If anyone has something better, please let me know.
Looks like this was fixed after the current release (as of this writing) 1.2.2. I ended up explicitly logging the error in the log. This won't go through the normal processing as unhandled exceptions (email notifications, etc) but it will log it to the error log.
catch (Exception ex)
{
var newEx = new HttpException(500, "Kaboom!!!", ex);
// Adding this explicit ELMAH logging because of ELMAH bug:
// https://code.google.com/p/elmah/issues/detail?id=217
// Waiting for a new ELMAH release with the fix
var elmahError = new Elmah.Error(newEx);
var elmahLog = Elmah.ErrorLog.GetDefault(HttpContext.ApplicationInstance.Context);
elmahLog.Log(elmahError);
throw newEx;
}
There was a bug in Elmah that was fixed a while back, but I don't believe any of the builds on the google code site are recent enough to include it.
As I mentioned in my comment, the issue is that if you access data through the model (MVC) or control (webforms), no problem; but if you access request.Form["inputId"] (which Elmah does when building one object), it throws an exception every time.
What I did:
Export the google code project to my github account
Clone the new repo to my workstation using VS2013
Select the NETFX 4.5 build configuration
Build the solution, and run the demo
Test my case in the demo by adding the following to default.aspx:
Run some tests
After I was happy with my tests, I added the new dlls to my solution by:
Removing the current Elmah.dll reference
Copy/paste Elmah.dll, Elmah.AspNet.dll, and AntiXssLibrary.dll to a 'lib' folder in my solution
Add references to the three new dlls
Update web.config so that it looks like the demo's web.config (things are very similar, but different (Elmah = Elmah.AspNet))
Remove the Elmah.mvc reference (it caused problems for me)
Remove filters.Add(new HandleErrorAttribute()); from FilterConfig (this allows custom errors to continue to work)
In the Elmah source, the crucial piece is the use of request.Unvalidated in HttpRequestValidation.cs
With thanks to Amarisi for the code, I've reworked it into my Global.asax.cs files Application_Error method to log all the exceptions Elmah fails to log.
It looks like this (edited down but should work):
protected void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
// ELMAH will not log exceptions arising from POST requests
// when the form values contains 'potentially dangerous' chars
// e.g. html tags
// Adding this explicit ELMAH logging because of ELMAH bug:
// https://code.google.com/p/elmah/issues/detail?id=217
// Waiting for a new ELMAH release with the fix
// get form object
System.Collections.Specialized.NameValueCollection RequestForm = Request.Unvalidated.Form;
// get combined text of form values
string formValues = string.Join(",", RequestForm.AllKeys.Select(key => RequestForm[key]));
// is this the type of POST that Elmah fails on?
if (formValues.Contains("<"))
{
// log the exception manually
var elmahError = new Elmah.Error(exception);
var elmahLog = Elmah.ErrorLog.GetDefault(Context);
elmahLog.Log(elmahError);
}
}
}
Contains("<") isn't the best test. I doubt it covers everything that Elmah fails to log.
Would love to see if anyone can improve this further.
You can get Elmah to fire by throwing an HttpException instead of a normal exception.
Our solution was to wrap our controller action code with a try/catch block, and in the catch block wrap the exception that is thrown by your code in an HttpException and throw that instead. Like this:
[HttpPost]
[ValidateInput(false)]
public ActionResult MyAction(FormCollection riskyData)
{
try
{
//... your code here ...
}
catch (Exception ex)
{
throw new HttpException(500, "Internal Server Error", ex);
}
}
Add the following classes to your project:
public class ElmahErrorLogModuleFix : ErrorLogModule
{
protected override void LogException(Exception e, HttpContext context)
{
if (e == null)
throw new ArgumentNullException("e");
ExceptionFilterEventArgs args = new ExceptionFilterEventArgs(e, (object)context);
this.OnFiltering(args);
if (args.Dismissed)
return;
ErrorLogEntry entry = (ErrorLogEntry)null;
try
{
//FIX STARTS
//Error error = new Error(e, context);
Error error = CreateErrorSafe(e, context);
//FIX ENDS
ErrorLog errorLog = this.GetErrorLog(context);
error.ApplicationName = errorLog.ApplicationName;
string id = errorLog.Log(error);
entry = new ErrorLogEntry(errorLog, id, error);
}
catch (Exception ex)
{
Trace.WriteLine((object)ex);
}
if (entry == null)
return;
this.OnLogged(new ErrorLoggedEventArgs(entry));
}
public static Error CreateErrorSafe(Exception e, HttpContext context)
{
try
{
var safeFormCollection = new NameValueCollection();
var form = context.Request.Form;
var additionalMessage = string.Empty;
foreach (var key in form.AllKeys)
{
try
{
safeFormCollection.Add(key, form[key]);
}
catch (Exception)
{
safeFormCollection.Add(key, "_invalid input data_");
additionalMessage += "Form parameter with name=" + key + " has dangerous value. " + Environment.NewLine;
}
}
//if no invalid values in form then do as elmah does
if (string.IsNullOrEmpty(additionalMessage))
{
return new Error(e, context);
}
var exception = new Exception(additionalMessage, e);
var error = new Error(exception);
error.HostName = TryGetMachineName(context, null);
IPrincipal user = context.User;
if (user != null && NullString(user.Identity.Name).Length > 0)
error.User = user.Identity.Name;
HttpRequest request = context.Request;
//this._serverVariables = Error.CopyCollection(request.ServerVariables);
error.ServerVariables.Add(CopyCollection(request.ServerVariables));
if (error.ServerVariables != null && error.ServerVariables["AUTH_PASSWORD"] != null)
error.ServerVariables["AUTH_PASSWORD"] = "*****";
error.QueryString.Add(CopyCollection(request.QueryString));
error.Form.Add(CopyCollection(safeFormCollection));
error.Cookies.Add(CopyCollection(request.Cookies));
return error;
}
catch (Exception logEx)
{
return new Error(new Exception("Error when trying to process error catched by elmah", logEx));
}
}
/// <summary>
/// Elmah dll method in Environment.cs
/// </summary>
/// <param name="context"></param>
/// <param name="unknownName"></param>
/// <returns></returns>
public static string TryGetMachineName(HttpContext context, string unknownName)
{
if (context != null)
{
try
{
return context.Server.MachineName;
}
catch (HttpException ex)
{
}
catch (SecurityException ex)
{
}
}
try
{
return System.Environment.MachineName;
}
catch (SecurityException ex)
{
}
return NullString(unknownName);
}
/// <summary>
/// Elmah method in Mask.cs
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static string NullString(string s)
{
if (s != null)
return s;
else
return string.Empty;
}
/// <summary>
/// Elmah method in Error.cs
/// </summary>
/// <param name="collection"></param>
/// <returns></returns>
private static NameValueCollection CopyCollection(NameValueCollection collection)
{
if (collection == null || collection.Count == 0)
//FIX HERE: cannot allow reutrn null collection as elmah does, because of exception. fix as below
//return (NameValueCollection)null;
return new NameValueCollection();
//FIX ENDS
else
return new NameValueCollection(collection);
}
/// <summary>
/// Elmah method in Error.cs
/// </summary>
/// <param name="cookies"></param>
/// <returns></returns>
private static NameValueCollection CopyCollection(HttpCookieCollection cookies)
{
if (cookies == null || cookies.Count == 0)
//FIX HERE: cannot allow reutrn null collection as elmah does, because of exception. fix as below
//return (NameValueCollection)null;
return new NameValueCollection();
//FIX ENDS
NameValueCollection nameValueCollection = new NameValueCollection(cookies.Count);
for (int index = 0; index < cookies.Count; ++index)
{
HttpCookie httpCookie = cookies[index];
nameValueCollection.Add(httpCookie.Name, httpCookie.Value);
}
return nameValueCollection;
}
}
and
public class ElmahErrorMailModuleFix : ErrorMailModule
{
private bool _reportAsynchronously2;
protected override void OnInit(HttpApplication application)
{
base.OnInit(application);
IDictionary config = (IDictionary)this.GetConfig();
if (config == null)
return;
_reportAsynchronously2 = Convert.ToBoolean(GetSetting(config, "async", bool.TrueString));
}
protected override void OnError(Exception e, HttpContext context)
{
if (e == null)
throw new ArgumentNullException("e");
ExceptionFilterEventArgs args = new ExceptionFilterEventArgs(e, (object)context);
this.OnFiltering(args);
if (args.Dismissed)
return;
//FIX STARTS
//Error error = new Error(e, context);
Error error = ElmahErrorLogModuleFix.CreateErrorSafe(e, context);
//FIX ENDS
if (this._reportAsynchronously2)
this.ReportErrorAsync(error);
else
this.ReportError(error);
}
/// <summary>
/// Elmah method in ErrorMailModule.cs
/// </summary>
/// <param name="config"></param>
/// <param name="name"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
private static string GetSetting(IDictionary config, string name, string defaultValue)
{
string str = ElmahErrorLogModuleFix.NullString((string)config[(object)name]);
if (str.Length == 0)
{
if (defaultValue == null)
throw new global::Elmah.ApplicationException(string.Format("The required configuration setting '{0}' is missing for the error mailing module.", (object)name));
str = defaultValue;
}
return str;
}
}
These classes inherit from ErrorLogModule and ErrorMailModule and rewrite methods where the Error class is created, so that the HttpRequestValidationException exception will not raise.
Then add these to your Web.config:
<add name="ErrorLog" type="YourProject.SomeFolder.ElmahErrorLogModuleFix, YourProject" preCondition="managedHandler" />
<!--and for email module-->
to use these classes instead of the original ones. A bit of a dirty hack, but it works.
Credit goes to the poster of message #17 found here.
the generated code from EF for a property of an entity looks like this:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.DateTime DateCreated
{
get
{
return _DateCreated;
}
set
{
OnDateCreatedChanging(value);
ReportPropertyChanging("DateCreated");
_DateCreated = StructuralObject.SetValidValue(value);
ReportPropertyChanged("DateCreated");
OnDateCreatedChanged();
}
}
private global::System.DateTime _DateCreated;
partial void OnDateCreatedChanging(global::System.DateTime value);
partial void OnDateCreatedChanged();
This code doesn't check if the value has actually changed (in the setter). Therefore the PropertyChanged event is raised even if you set a value that is equal to the current value. But in this case nothing would have changed, so I wouldn't want this event...
For EntityKey properties they do check this:
/// <summary>
/// No Metadata Documentation available.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=true, IsNullable=false)]
[DataMemberAttribute()]
public global::System.Guid Id
{
get
{
return _Id;
}
set
{
if (_Id != value)
{
OnIdChanging(value);
ReportPropertyChanging("Id");
_Id = StructuralObject.SetValidValue(value);
ReportPropertyChanged("Id");
OnIdChanged();
}
}
}
private global::System.Guid _Id;
partial void OnIdChanging(global::System.Guid value);
partial void OnIdChanged();
I would expect this behavior from all properties.
Am I missing a setting in the model designer, or is there another solution?
Thanx!
It is point of T4 templates to allow you modifications you need. It is absolutely wrong approach to say:
But I would rather not use a custom template in my project!
It is like throwing all advantages of T4 templates away and going back to hardcoded custom tools for code generating.
I did, as I knew it was possible and Ladislav also stated, include the T4 template file into the project and made the following changes to the "Write PrimitiveType Properties." part of the template:
if (!Object.Equals(<#=code.FieldName(primitiveProperty)#>, value))
{
<#=ChangingMethodName(primitiveProperty)#>(value);
ReportPropertyChanging("<#=primitiveProperty.Name#>");
<#=code.FieldName(primitiveProperty)#> = StructuralObject.SetValidValue(value<#=OptionalNullableParameterForSetValidValue(primitiveProperty, code)#>);
ReportPropertyChanged("<#=primitiveProperty.Name#>");
<#=ChangedMethodName(primitiveProperty)#>();
}
Hope that will be helpfull to others.
I am wondering what is the best way to use NLog with Managed Extensibility Framework (MEF)?
I have an application that support plugins using MEF architecture (Import and Exports etc)
I want to add logging capability to my application.
As a logging component I want to use NLog.
What would you recommend?
1. Create a wrapper for NLog, i.e. additional plugin that configures NLog and exports functions like void Log(string level, string message) that other plugins importing
2. Every plugin should have it is own instance of NLog configured and used. (They all would write to the same file actually).
This is an interesting approach, however, it seems to suffer from the drawback that all loggers that are injected (or the one singleton that is injected) will be the same instance (or will have the same name, the name being the name of the NLogLoggingService class. That means that you cannot very easily control the granularity of logging (i.e. turn logging to "Info" level in one class and "Warn" in another class). Also, if you opt to use the call site formatting tokens, you will always get the call site of the call the the NLog logger rather than the call site in your application code.
Here is an abbreviated version of the logger that was linked:
[Export(Services.Logging.LoggingService, typeof(ILoggingService))]
class NLogLoggingService : ILoggingService
{
Logger log; public NLogLoggingService()
{
log = LogManager.GetCurrentClassLogger();
}
public void Debug(object message)
{
log.Debug(message);
}
public void DebugWithFormat(string format, params object[] args)
{
if (args.Length == 0)
{
log.Debug(format);
}
else
{
Debug(string.Format(format, args));
}
}
public bool IsDebugEnabled
{
get
{
return log.IsDebugEnabled;
}
}
}
In the constructor LogManager.GetCurrentClassLogger() is used to get the NLog logger. GetCurrentClassLogger will return a NLog logger that is "named" based on the "current" type, which, in this case, is NLogLoggingService. So, to configure NLog in the app.config file, you will configure based on the that the logger is named "SoapBox.Core.NLogLoggingService". Commonly, in code that uses NLog (or log4net) directly, each class gets its own uniquely named logger like this:
namespace MyNamespace
{
public class MyClass1
{
private static readonly Logger logger LogManager.GetCurrentClassLogger();
public void DoSomeWork()
{
logger.Info("Logging from inside MyClass1.DoSomeWork");
}
}
public class MyClass2
{
private static readonly Logger logger LogManager.GetCurrentClassLogger();
public void DoSomeWork()
{
logger.Info("Logging from inside MyClass2.DoSomeWork");
}
}
}
Now the logging for MyClass1 and MyClass2 is individually controllable. You can configure different levels for each class, send them to different targets, or turn one or both off altogether. Alternatively, due to the concept of logger hierarchies in both log4net and NLog, you could control the logging in both class simultaneously by configuring a "logger" for the namespace (MyNamespace in this case), or any "ancestor" namespace. If there is not a logger configured for the fully qualified typename, then the logging framework essentially moves up the hierarchy by considering the name a dot delimited string and removing the last chunk and checking to see if that logger is configured. So, in this case, we are requesting loggers for MyNamespace.MyClass1 and MyNamespace.MyClass2. I could configure the app.config file to have MyNamespace log at the "info" and write to a file target (appender in log4net-speak). If I did that, then both loggers that I requested via their fully qualified names would inherit the MyNamespace configuration.
With the suggested way of injecting NLog via MEF, you will only have one logger instance, so you cannot configure each class to log differently. Also, as I mentioned earlier, if you opt to log call site information, you will always get "SoapBox.Core.NLogLoggingService" for the class and "Debug" (or DebugWithFormat, or Info, or InfoWithFormat, etc) for the method.
This seems to be an issue with successfully injecting loggers from log4net and NLog. You can see the question that I asked about this very issue a couple of months ago.
Ultimately I was able to figure out how some dependency injection frameworks can successfully inject log4net and NLog loggers that are specific to the class being created (i.e. if the DI framework is instantiating MyClass, which in turn depends on an ILogger interface, then MyClass will get a logger that is essentially equivalent to what would have happened if MyClass requested the logger itself via the LogManager.GetCurrentClassLogger api). Generally "resolvers" in DI/IoC frameworks are given the current context (containing, among other information, the type of the object currently being created). With that type available, it becomes a simple matter of having a logging framework-specific resolver receive that type and pass it along to the logging framework to create a logger appropriate for that type.
In order to get the most out of NLog's (and log4net's) capabilities you would really like to be able to tell MEF that your class is dependendent on "ILogger", but also that the instance of "ILogger" that gets injected into your class should depend on the Type of your class.
I don't know how easy it will be to achieve that with MEF. Alternatively, you could wrap NLog's static LogManager in a ILogManager and inject that. That would deviate from the normal "inject ILogger" paradigm.
To summarize: If you inject NLog via MEF this way, you will indeed be able to log with NLog, but you will only ever have one named logger (SoapBox.Core.NLogLoggingService). This means that you will not be able control with any degree of granularity - either for levels/on/off or for output (NLog Target/log4net Appender)
I don't have a good answer for what to do as far as injecting NLog via MEF AND keeping the granularity/flexibility that "raw" NLog gives you.
I can say that we have decided to use Common.Logging for .NET to abstract the logging framework but we decided NOT to inject logging. Instead, we will just use a static LogManager (as provided by Common.Logging) to hand out loggers.
I think Option 1 is better.
You can take a look at how the open source framework SoapBox Core imports a reference to an ILoggingService using MEF. It also provides a default implementation of the logging service based on NLog, but you could easily swap it out for log4Net, for example.
For reference:
the ILoggingService interface
the Logging Service that wraps NLog and exports itself using MEF
SoapBox Core is LGPL'd, so you might be able to use (this part) in your application.
I have been fighting with this problem a while now.
Really improtant was the Callsite (FullyQualified Namespace) within the logfiles.
First, i tryed to get the right logger out of the Stacktrace:
[MethodImpl(MethodImplOptions.NoInlining)]
private static NLog.Logger GetLogger()
{
var stackTrace = new StackTrace(false);
StackFrame[] frames = stackTrace.GetFrames();
if (null == frames) throw new ArgumentException("Stack frame array is null.");
StackFrame stackFrame;
switch (frames.Length)
{
case 0:
throw new ArgumentException("Length of stack frames is 0.");
case 1:
case 2:
stackFrame = frames[frames.Length - 1];
break;
default:
stackFrame = stackTrace.GetFrame(2);
break;
}
Type declaringType = stackFrame.GetMethod()
.DeclaringType;
return declaringType == null ? LogManager.GetCurrentClassLogger() : LogManager.GetLogger(declaringType.FullName);
}
But sadly, the Stacktrace with MEF is very long and i cannot clearly identify the correct caller for the Requester of the ILogger.
So, instead of injecting the ILogger Interface via Constructor Injection, i have created a ILogFactory Interface, that can get injected via Constructor Injection and call then the Create Method on the Factory
public interface ILogFactory
{
#region Public Methods and Operators
/// <summary>
/// Creates a logger with the Callsite of the given Type
/// </summary>
/// <example>
/// factory.Create(GetType());
/// </example>
/// <param name="type">The type.</param>
/// <returns></returns>
ILogger Create(Type type);
#endregion
}
And implemented it:
using System;
using System.ComponentModel.Composition;
[Export(typeof(ILogFactory))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class LogFactory : ILogFactory
{
#region Public Methods and Operators
public ILogger Create(Type type)
{
var logger = new Logger().CreateLogger(type);
return logger;
}
#endregion
}
With the ILogger:
public interface ILogger
{
#region Public Properties
bool IsDebugEnabled { get; }
bool IsErrorEnabled { get; }
bool IsFatalEnabled { get; }
bool IsInfoEnabled { get; }
bool IsTraceEnabled { get; }
bool IsWarnEnabled { get; }
#endregion
#region Public Methods and Operators
void Debug(Exception exception);
void Debug(string format, params object[] args);
void Debug(Exception exception, string format, params object[] args);
void Error(Exception exception);
void Error(string format, params object[] args);
void Error(Exception exception, string format, params object[] args);
void Fatal(Exception exception);
void Fatal(string format, params object[] args);
void Fatal(Exception exception, string format, params object[] args);
void Info(Exception exception);
void Info(string format, params object[] args);
void Info(Exception exception, string format, params object[] args);
void Trace(Exception exception);
void Trace(string format, params object[] args);
void Trace(Exception exception, string format, params object[] args);
void Warn(Exception exception);
void Warn(string format, params object[] args);
void Warn(Exception exception, string format, params object[] args);
#endregion
}
and Implementation of:
using System;
using NLog;
using NLog.Config;
/// <summary>
/// The logging service.
/// </summary>
public class Logger : NLog.Logger, ILogger
{
#region Fields
private string _loggerName;
#endregion
#region Public Methods and Operators
/// <summary>
/// The get logging service.
/// </summary>
/// <returns>
/// The <see cref="ILogger" />.
/// </returns>
public ILogger CreateLogger(Type type)
{
if (type == null) throw new ArgumentNullException("type");
_loggerName = type.FullName;
var logger = (ILogger)LogManager.GetLogger(_loggerName, typeof(Logger));
return logger;
}
To use it... just inject the ILogFactory and calle the Create Method in a Mefed Importing Constructor:
[ImportingConstructor]
public MyConstructor(
ILogFactory logFactory)
{
_logger = logFactory.Create(GetType());
}
hope this helps
If you create a new ExportProvider and cast the ImportDefinition being passed in to a ICompositionElement. You can get the type that the logger is being injected into.
Here is the ExportProvider
public class LoggerExportProvider : ExportProvider
{
private readonly ExportDefinition _loggerExportDefinition;
private readonly Func<string, ILogger> _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="LoggerExportProvider"/> class.
/// </summary>
/// <param name="loggerFactory">The logger factory function.</param>
public LoggerExportProvider(Func<string, ILogger> loggerFactory)
{
_loggerFactory = loggerFactory;
_loggerExportDefinition = new ExportDefinition(typeof (ILogger).FullName, new Dictionary<string, object> {{"ExportTypeIdentity", typeof (ILogger).FullName}});
}
protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
{
IList<Export> exports = new List<Export>();
var compositionElement = definition as ICompositionElement;
if (compositionElement == null || compositionElement.Origin == null)
return exports;
var constraint = definition.Constraint.Compile();
if (constraint(_loggerExportDefinition))
exports.Add(new Export(_loggerExportDefinition, () => _loggerFactory(compositionElement.Origin.DisplayName)));
return exports;
}
}
This is setup in such a way that it will work with any logging framework as you need to pass in a function that will return an ILogger (the Ilogger is our own, you'll have to create your own interface or just make it specific to Nlog). The string being passed to the function is the full class name that the type is being injected too. (compositionElement.Origin.DisplayName)
An example of bootstrapping MEF with this would look like this:
public class Example
{
[Import]
public ILogger Logger { get; set;}
public Example()
{
var aggregatecatalogue = new AggregateCatalog();
aggregatecatalogue.Catalogs.Add(new AssemblyCatalog(typeof (ILogger).Assembly));
aggregatecatalogue.Catalogs.Add(new AssemblyCatalog(GetType().Assembly));
var container = new CompositionContainer(aggregatecatalogue, new LoggerExportProvider(s => new MockLogger(s)));
container.ComposeParts(this);
}
}
The code above was copied from a unit test, so I'm just add specific assemblies instead of parsing a directory. The MockLogger is an implementation of the ILogger interface that takes the logging class name (or injecting type) as a parameter to it's constructor.
This doesn't require parsing any stack traces and pulls the information that is otherwise sitting there directly out of MEF.