Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bye bye Log4Net, Hello Serilog #2845

Merged
merged 86 commits into from
Aug 28, 2018
Merged

Bye bye Log4Net, Hello Serilog #2845

merged 86 commits into from
Aug 28, 2018

Conversation

warrenbuckley
Copy link
Contributor

@warrenbuckley warrenbuckley commented Aug 10, 2018

This PR contains the initial configuration work of getting rid of Log4Net and swapping it out to use Serilog so we have better control of logging output with structured logs eventually; that's to come in a seperate PR

Things to look for

Try and check some of the following things

  • Ensure/verify all Log4Net files are gone & nothing left behind
  • Proof read the code changes here & shout if nothing is not clear
  • Install Umbraco from nupkg builkd outputs (Test Nupkg's install Serilog dependencies)

Testing Notes

The C# configured logged by default will generate two rolling log files (one for each day)

  • Same format of text file ouput
  • A JSON log file that includes all th extra meta-data/properties (Can be used to be imported to local copy of SEQ for querying)

Two config files have been added to configure the logger

  • config/serilog.config
  • config/serilog.user.config

See if you can experiment with the two configuration files with different values (See config & filtering sections below) to output the logs you require.

Importing the JSON log file into SEQ

Install SEQ locally on your machine for free
https://getseq.net

Use the CLEF (SEQ log import tool)
https://github.com/datalust/clef-tool/releases

Details on usage here
https://docs.getseq.net/docs/importing-log-files

Using a 3rd party Serilog Sink

There are many sinks available
https://github.com/serilog/serilog/wiki/Provided-Sinks

I recommend adding/installing PM> Install-Package Serilog.Sinks.Seq from Nuget then add the following to the serilog.user.config

<add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
<add key="serilog:write-to:Seq.serverUrl" value="http://localhost:5341" />

Config files

The config files uses serilog.Settings.AppSettings NuGet package & the project readme details some usage, however I have tried to put in some examples in the serilog.user.config file. That hopefuly gives some examples or usages on what could be put in here.

https://github.com/serilog/serilog-settings-appsettings

Filtering

Its possible for the user definied configuration to include or exclude logs based on expressions, but in the example I have shown how you would configure it so that all sinks definied in the user config will only log out messages that start with my namespace.

This is optional and could be removed alltogether. If the user wants to configure their sink to capture all of Umbraco logs AND any logs they are writing to the logger too.

https://github.com/serilog/serilog-filters-expressions

Configure/Overide logging

After a suggestion from @Shazwazza its now possible to configure Serilog in code if that is easier than trying to configure a complex Serilog pipeline only via the serilog.user.config file.

Example that developers coulld implement (instead of serilog.user.config)

using Serilog;
using Serilog.Filters;
using Umbraco.Core.Logging;
using Umbraco.Core.Logging.SerilogExtensions;
using Umbraco.Web;
using ILogger = Umbraco.Core.Logging.ILogger;

namespace MyNamespace
{
    public class WarrensLogging : UmbracoApplication
    {
        protected override ILogger GetLogger()
        {
            //Example of building my own logger configuration
            var loggerConfig = new LoggerConfiguration();
            loggerConfig.MinimalConfiguration()
                .OutputDefaultJsonFile()
                .Enrich.WithProperty("Environment", "Warrens-Test-UPDATED")
                .WriteTo.Logger(lc =>
                    lc.OutputDefaultTextFile()
                        .Filter.ByExcluding(Matching.FromSource("Umbraco.Web"))
                );
            
            return new Logger(loggerConfig);
        }
    }
}

Update Global.asax

Then they will need to update the Global.asax file like so
<%@ Application Inherits="MyNamespace.WarrensLogging" Language="C#" %>

Any Questions?

Anything doesn't make sense or is not clear leave a comment & I will try my best to describe what is going on.

Warren added 30 commits July 27, 2018 13:48
Removes references to Log4Net from Web.Config's
…ckages

Removes AppDomainTokenConvertor - as we can add to SeriLog via a custom Enrich Property
…h Serilog CloseAndFlush- not 100% sure we need to do this as think LightInject tears it down nicely
…ckage & a custom one for the AppDomainAppId
…o Log4Net

Unsure about the methods that take in args - have commented these out for now (Need to talk with Seb or Stephan about this)
…only diff currently is the log level type in the log file)

Ensures we use the calling type in the Umbraco ILogger and passes it onto Serilog
# Conflicts:
#	src/Umbraco.Web.UI/web.Template.Debug.config
Configured to read from a custom .config file location as opposed to root web.config appsettings
Enable Serilog Debugging aka Selflog
…LEF format so that it can be imported into a local copy of SEQ to help with filtering/finding log items
…ig can use it to save files with a relative path
Warren added 3 commits August 17, 2018 11:38
…template so its more in sync with Serilog signature & was an easy way to find some more logs I missed too
Copy link
Contributor

@Shazwazza Shazwazza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow! epic PR, i've left some comments/questions... mostly minor stuff

@@ -33,7 +33,6 @@ if ($project) {
robocopy $umbracoBinFolder $umbracoBinBackupPath /e /LOG:$copyLogsPath\UmbracoBinBackup.log

# Delete files Umbraco ships with
if(Test-Path $umbracoBinFolder\log4net.dll) { Remove-Item $umbracoBinFolder\log4net.dll -Force -Confirm:$false }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure what this does and if we need it in v8 but if we do doesn't all the serilog DLLs need to be in here too? Might need to ask seb about that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was related to when we were shipping a custom Log4Net DLL as Serilog will be a nupkg dependancy reference.

Any thoughts @nul800sebastiaan

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK after a quick slack chat with Seb this is fine & was a specific hack to Log4Net. So should all be good @Shazwazza

@@ -27,7 +27,7 @@ public void Log<T>(string text, [CallerMemberName] string callerName = null, [Ca
{
// Using LogHelper since the ImageProcessor logger expects a parameterless constructor.
var message = $"{callerName} {lineNumber} : {text}";
Current.Logger.Error<T>(string.Empty, new ImageProcessingException(message));
Current.Logger.Error<T>(new ImageProcessingException(message), string.Empty);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make an extension method for Error to be able to pass in just an exception which in turn will pass in an empty string to the real method.

return true;
case TraceEventType.Error:
_logger.Error(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state), exception ?? new Exception("Error"));
_logger.Error(_type.Value, exception ?? new Exception("Error"), "[{EventType}] Event Id: {EventId}, State: {State}", eventType, eventId, state);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe have an extension method for error that allows us to just pass in a message and it will create the underlying exception?

/// This is used to create a new property in Logs called 'Log4NetLevel'
/// So that we can map Serilog levels to Log4Net levels - so log files stay consistent
/// </summary>
public class Log4NetLevelMapperEnricher : ILogEventEnricher
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep just a bad habit of mine that I make everything public - will fix up

+ $" but the {logPropertyAlias} property does not exist on the member type so a default value is returned. Ensure that you have a property type with alias: "
+ logPropertyAlias + $" configured on your member type in order to use the '{logPropertyName}' property on the model correctly.");
{
//WB: TODO Check that we do not need to specify properties again if used later on again in message template
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this TODO still need a follow up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to test this out - will check & verify

var e = new Exception(m);
Logger.Error<ContentTypeRepository>(m, e);
var e = new Exception($"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.");
Logger.Error<ContentTypeRepository>(e, "ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where we could have an extension method to just pass in a message

var m = $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.";
var e = new InvalidOperationException(m);
Logger.Error<ContentTypeRepositoryBase<TEntity>>(m, e);
var e = new InvalidOperationException($"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where we could have an extension method to just pass in a message ? this ones is a little more tricky with the typed exception and the message formatting, but might work somehow?


//Not sure if we need to do this - as my POC approach I never had to deal with this
//As the LightInject container when tearing down will dispose of Serilog AFAIK
Log.CloseAndFlush();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it doesn't crash anything then might be better to just have it. Is it also IDisposable? i.e. would calling .Dispose() do the same thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the logger itself is IDisposable AFAIK and hence my comment/note to say that I am not 100% sure if we need to keep in this explicit call to Log.CloseAndFlush()

@@ -356,7 +356,6 @@ internal static IEnumerable<Assembly> GetBinAssemblies()
"Dynamic,",
"HtmlDiff,",
"Iesi.Collections,",
"log4net,",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serilog should be added here, you'd want to put Serilog. with the dot as i think that means anything that starts with serilog will be ignored

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -29,7 +29,10 @@ public override void Log(ExceptionLoggerContext context)
&& context.ExceptionContext.ActionContext.ControllerContext.Controller != null
&& context.Exception != null)
{
_logger.Error(context.ExceptionContext.ActionContext.ControllerContext.Controller.GetType(), "Unhandled controller exception occurred", context.Exception);
var requestUrl = context.ExceptionContext.ControllerContext.Request.RequestUri.AbsoluteUri;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prob need to do some null checks on this (can use ?.) since i think there might be cases where it's null

@warrenbuckley warrenbuckley removed the request for review from clausjensen August 23, 2018 11:58
@ghost ghost assigned Shazwazza Aug 27, 2018
@Shazwazza
Copy link
Contributor

@warrenbuckley everything looks good from my tests, only a couple more comments about the ILogger changes:

  • For Error on the ILogger interface, maybe we shouldn't have either of these: void Error(Type reporting, Exception exception) + void Error(Type reporting, string message) and instead just have those signatures as extension methods ... then it would be required by the ILogger method void Error(Type reporting, Exception exception, string message) to deal with null values for either exception and message ?
  • Update the Fatal implementation in the same manner, we'd just leave the ILogger method void Fatal(Type reporting, Exception exception, string message) but it can deal with having null passed for either exception or message and we have extension methods to allow the user to pass in one or the other

What do you think?

Copy link
Contributor

@Shazwazza Shazwazza left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna go ahead and merge this, all looks good, i've fixed up some small things.

@@ -38,16 +38,17 @@ internal enum LogType
_endMessage = endMessage;
_failMessage = failMessage;
_thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds;
_timingId = Guid.NewGuid();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the timing ID should not include spaces so that when you are viewing the log and you double click on one of these IDs, it will highlight the entire thing which means it will also highlight the other affiliated log entries. So should be Guid.NewGuid.ToString("N") and change _timingId to a string

if (logger == null || logger.IsWarnEnabled == false) return;
logger.Warn(format);
var logger = Log.Logger;
logger?.Warning(format);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect as it doesn't take into account the type, I've fixed up in the latest commit

@Shazwazza
Copy link
Contributor

@warrenbuckley We could simplify the ILogger interface to just:

void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues);

void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues);

void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues);

void Info(Type reporting, string messageTemplate, params object[] propertyValues);

void Debug(Type reporting, string messageTemplate, params object[] propertyValues);

void Verbose(Type reporting, string messageTemplate, params object[] propertyValues);

For Fatal, Error, and Warn it can be expected that the implementer deals with null values for either Message or Exception which is what you implementation is already doing.

... But then I realized that this is the meaning of "Leaky abstraction" in that now the implementer and consumer need to be aware of the implementation details.

So i think the only changes needed would be to add additional ILogger methods for Fatal to allow for the same logic as Error.

I've pushed a commit with those changes.

I found another issue that i pushed a change for since Warn wasn't taking into account the Type in some cases, example:

2018-08-28 15:25:31,993 [P21164/D2/T12] WARN    - Umbraco has no content

Another thing I've noticed is that a new log file is created when a new AppDomain starts up. This is a known issue: serilog/serilog#380

So i'm not going to merge this in just yet! ... i think we need to solve this issue first.

Warren added 2 commits August 28, 2018 09:46
…p restarts we don't get _001 or _002 files etc
…nd examples on what can be setup & configured
@warrenbuckley
Copy link
Contributor Author

@Shazwazza updated the PR File Sink with the Shared property set to true and I can't reproduce the issue with the _01 and _02 etc appended files

@Shazwazza
Copy link
Contributor

:shipit:

@Shazwazza Shazwazza merged commit a151829 into temp8 Aug 28, 2018
@ghost ghost removed the review label Aug 28, 2018
@warrenbuckley warrenbuckley deleted the temp8-serilog branch August 28, 2018 12:11
@jaddie
Copy link
Contributor

jaddie commented Aug 29, 2018

I was going to join in the fun on this one until I realised that it was a branch I couldn't contribute to easily haha. Also I agree @Shazwazza I was thinking the same thing about the implementation etc having to care about null values. Although it could also be excused in that any decent logging platform (as Serilog says on their wiki) should handle invalid arguments being sent through as there is always unforseen situations with run-time which is the whole point of having logging in the first place to track down unexpected circumstances.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants