I want to write to a custom serilog sink, which logs errors into EMails.
I wrote a custom serilog sink based on Serilog sample code and tested the sink with a .NET 5.0 console app. It works nicely. I then added the sink package and exactly the same configuration into the appsettings.json file of my umbraco project. The sink is simply ignored.
I use Umbraco 9.1.2.
Setting a breakpoint shows, that the Extension method is never called.
The debug log shows, that the assembly has been loaded.
Entering a wrong assembly name into the Using line leads to an exception, which proves, that the Serilog configuration is read.
EDIT:
In the Umbraco Sources in SerilogLogger.CreateWithDefaultConfiguration we see the following lines:
var loggerConfig = new LoggerConfiguration()
.MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration)
.ReadFrom.Configuration(configuration);
I can see there, that the name of the ConfigurationMethod is not "LaykitEmailPickup", but "Async". The parameters are the right ones. Changing the name in the debugger from "Async" to the correct name "LaykitEmailPickup" makes the Sink working. Looks like a bug.
WTF? I set a breakpoint in Startup.cs, and watched at _config. This variable should represent the configuration as entered in the appsettings.json file. If I put the following expression in the Watch window
_config.GetSection("Serilog:WriteTo:0:Name")
the result is "Key"="Name" and "Value"="Async". The value should definitely be "LaykitEmailPickup" (see my appsettings.json above). Changing the value at this point results in a working condition.
Whoever stumbles upon this problem, and other problems, like "My smtp configuration always returns 'localhost', even if I declared the host to be 'xx.yy.com'": Umbraco constructs a development version of appsettings.json with a lot of unnecessary defaults. This is the reason, why it returns "Async" as sink name.
I don't think the additional stuff in appsetings.development.json is useless: for instance, I find it really useful to get the logs to the console when debugging.
However, I am grateful for your reminder of the fact that "WriteTo" essentially is overwritten like this.
Any change to see the code of your custom email sink? I am looking for a simple email sink which will use the email settings of Umbraco.CMS.Global.Smtp
Hi Mikael,
these are actually two questions, one about the log sink and one about the SmtpSettings, because my custom mail sink doesn't make use of the Smtp settings in the appsettings.json file. If you want to use them, write a class SmtpSettings like that:
public class SmtpSettings
{
public string From { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public string SecureSocketOptions { get; set; }
public string DeliveryMethod { get; set; }
public string PickupDirectoryLocation { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool OmitRevocationTest { get; set; }
public bool IsInErrorLog { get; set; }
}
You can define whatever properties you like in the class and in the config section. You are not bound to the usual properties. I use a mail delivery package of my own, which delivers mail from the PickupDirectoryLocation. This is a directory used as cache for mail delivery. If the mail transfer fails due to network problems it can be repeated later.
Regarding to Serilog I took an example sink from their sources and rewrote it such that it logs into mails, which are copied to the PickupDirectoryLocation. This is the code:
using System;
using System.Collections.Generic;
using System.IO;
using Formfakten.Sinks.EmailPickup.Interfaces;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Sinks.PeriodicBatching;
namespace Formfakten.Sinks.EmailPickup
{
public class LaykitEmailPickupSink : PeriodicBatchingSink
{
private readonly IFormatProvider _formatProvider;
private string _pickupDirectory;
private readonly string _toEmail;
private readonly string _subject;
private readonly string _fileExtension;
private readonly string _fromEmail;
private bool _directoryExists;
/// <summary>
/// Maximale Anzahl Events, die in einem Mail versandt werden soll
/// </summary>
public const int DefaultBatchPostingLimit = 100;
/// <summary>
/// Wartezeit zwischen zwei Versandperioden
/// </summary>
public static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(30);
public static ILogEnvironment LogEnvironment { get; set; }
public LaykitEmailPickupSink(string pickupDirectory, string toEmail, string fromEmail, string subject,
string fileExtension, IFormatProvider formatProvider, TimeSpan defaultPeriod, int defaultBatchPostingLimit) : base(defaultBatchPostingLimit, defaultPeriod)
{
_formatProvider = formatProvider;
_pickupDirectory = pickupDirectory ?? throw new ArgumentNullException(nameof(pickupDirectory));
SelfLog.WriteLine( $"LaykitEmailPickupSink Konstruktor: LogEnvironment: {LogEnvironment != null}, PickupDir: {_pickupDirectory}" );
_toEmail = toEmail ?? throw new ArgumentNullException(nameof(toEmail));
_fromEmail = fromEmail ?? throw new ArgumentNullException(nameof(fromEmail));
_subject = subject ?? throw new ArgumentNullException(nameof(subject));
_fileExtension = fileExtension ?? throw new ArgumentNullException(nameof(fileExtension));
}
protected override void EmitBatch(IEnumerable<LogEvent> logEvents)
{
if (logEvents == null) throw new ArgumentNullException(nameof(logEvents));
EnsurePickupDirExists();
bool isFirst = true;
var filePath = "";
try
{
filePath = Path.Combine(_pickupDirectory, Guid.NewGuid().ToString("N") + _fileExtension);
using (var writer = File.CreateText(filePath))
{
writer.WriteLine($"To: {_toEmail}");
writer.WriteLine($"From: {_fromEmail}");
writer.WriteLine($"Subject: {_subject}");
writer.WriteLine($"Date: {DateTime.UtcNow:R}");
writer.WriteLine();
foreach (var e in logEvents)
{
if (!isFirst)
writer.WriteLine("____________________");
writer.WriteLine();
writer.WriteLine($"Level: {e.Level}");
e.RenderMessage(writer, _formatProvider);
writer.WriteLine();
WriteProperties(writer, e.Properties);
writer.WriteLine();
WriteException(writer, e.Exception);
writer.WriteLine();
isFirst = false;
}
}
if (LogEnvironment != null)
LogEnvironment.Deliver();
}
catch (Exception e)
{
SelfLog.WriteLine($"Failure when writing writing event to {filePath}: {e.Message}");
}
}
private void EnsurePickupDirExists()
{
if (_directoryExists) return;
if ((_pickupDirectory[0] == '~' || _pickupDirectory[0] == '/') && LogEnvironment != null)
this._pickupDirectory = LogEnvironment.MapPath( _pickupDirectory );
if (Path.IsPathRooted( _pickupDirectory ))
{
if (!Directory.Exists( _pickupDirectory ))
Directory.CreateDirectory( _pickupDirectory );
_directoryExists = true;
}
else
{
SelfLog.WriteLine( "Error: Path is relative. Provide a LogEnvironment." );
throw new Exception( "Path is relative. Provide a LogEnvironment." );
}
}
private void WriteProperties(TextWriter writer, IReadOnlyDictionary<string, LogEventPropertyValue> properties)
{
writer.WriteLine();
writer.WriteLine("Properties:");
foreach (var prop in properties.Keys)
{
writer.Write($"{prop}: ");
properties[prop].Render(writer, null, _formatProvider);
writer.WriteLine();
}
}
private void WriteException(TextWriter writer, Exception e)
{
if (e == null)
{
writer.WriteLine("No exception was logged");
return;
}
var exText = e.ToString().Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in exText)
{
writer.WriteLine(line);
}
}
}
}
The sink needs to provide a static extension class:
using System;
using Serilog;
using Serilog.Configuration;
using Serilog.Debugging;
using Serilog.Events;
namespace Formfakten.Sinks.EmailPickup
{
public static class LaykitEmailPickupSinksExtensions
{
public static LoggerConfiguration LaykitEmailPickup( this LoggerSinkConfiguration loggerConfiguration,
string pickupDirectory,
string toEmail,
string fromEmail,
string subject,
string fileExtension = ".eml",
LogEventLevel restrictedToMinimumLevel = LogEventLevel.Error,
IFormatProvider formatProvider = null,
TimeSpan? period = null,
int batchPostingLimit = LaykitEmailPickupSink.DefaultBatchPostingLimit)
{
try
{
return loggerConfiguration.Sink( new LaykitEmailPickupSink( pickupDirectory, toEmail, fromEmail, subject,
fileExtension, formatProvider, period ?? LaykitEmailPickupSink.DefaultPeriod, batchPostingLimit ),
restrictedToMinimumLevel );
}
catch (Exception ex)
{
SelfLog.WriteLine( $"{nameof( LaykitEmailPickupSinksExtensions )}: {ex}" );
return null;
}
}
}
}
thank you for your input! It gave me a much better understanding of how the entire thing works.
However, my logger never is called. I built a static extension method, which just instantiates the Serilog.Sinks.Email logger, and I included some logger calls, but as far as I can see, the method never gets called.
This is my static method:
using Serilog.Configuration;
using Serilog.Debugging;
using Serilog.Events;
using Serilog;
using System.Net;
namespace KleinwortEffective.Libs.Umb
{
public static class SerilogSinkExtensions
{
public static LoggerConfiguration KleinwortEffectiveEmailSink(this LoggerSinkConfiguration loggerConfiguration,
string toEmail,
string fromEmail,
string host,
string subject,
int port,
string userName,
string password,
LogEventLevel restrictedToMinimumLevel = LogEventLevel.Warning,
IFormatProvider formatProvider = null)
{
Serilog.Log.Warning("Entering KleinwortEffectiveEmailSink");
SelfLog.WriteLine("SelfLog: Entering KleinwortEffectiveEmailSink");
try
{
return new LoggerConfiguration().WriteTo.Email(
fromEmail,
toEmail,
host,
port,
credentials: new NetworkCredential(userName, password),
subject: subject,
restrictedToMinimumLevel: restrictedToMinimumLevel,
formatProvider: formatProvider);
}
catch (Exception ex)
{
SelfLog.WriteLine($"{nameof(KleinwortEffectiveEmailSink)}: {ex}");
return null;
}
}
}
}
Umbraco 9: Can't write to custom serilog sink
Hi everyone,
I want to write to a custom serilog sink, which logs errors into EMails.
I wrote a custom serilog sink based on Serilog sample code and tested the sink with a .NET 5.0 console app. It works nicely. I then added the sink package and exactly the same configuration into the appsettings.json file of my umbraco project. The sink is simply ignored.
I use Umbraco 9.1.2.
Setting a breakpoint shows, that the Extension method is never called.
The debug log shows, that the assembly has been loaded.
Entering a wrong assembly name into the Using line leads to an exception, which proves, that the Serilog configuration is read.
I followed the instructions given here: https://our.umbraco.com/documentation/reference/V9-Config/Serilog/
This is my config:
EDIT: In the Umbraco Sources in SerilogLogger.CreateWithDefaultConfiguration we see the following lines:
This ends in a Serilog function
I can see there, that the name of the ConfigurationMethod is not "LaykitEmailPickup", but "Async". The parameters are the right ones. Changing the name in the debugger from "Async" to the correct name "LaykitEmailPickup" makes the Sink working. Looks like a bug.
WTF? I set a breakpoint in Startup.cs, and watched at _config. This variable should represent the configuration as entered in the appsettings.json file. If I put the following expression in the Watch window
the result is "Key"="Name" and "Value"="Async". The value should definitely be "LaykitEmailPickup" (see my appsettings.json above). Changing the value at this point results in a working condition.
But that's not the way ist should be... Any Ideas?
Mirko
I reported this as a bug. See
https://github.com/umbraco/Umbraco-CMS/issues/11941
Whoever stumbles upon this problem, and other problems, like "My smtp configuration always returns 'localhost', even if I declared the host to be 'xx.yy.com'": Umbraco constructs a development version of appsettings.json with a lot of unnecessary defaults. This is the reason, why it returns "Async" as sink name.
The debug version overrides the standard version.
Hey Mmaty,
I don't think the additional stuff in
appsetings.development.json
is useless: for instance, I find it really useful to get the logs to the console when debugging.However, I am grateful for your reminder of the fact that "WriteTo" essentially is overwritten like this.
Any change to see the code of your custom email sink? I am looking for a simple email sink which will use the email settings of
Umbraco.CMS.Global.Smtp
Kind regards! Mikael
Hi Mikael, these are actually two questions, one about the log sink and one about the SmtpSettings, because my custom mail sink doesn't make use of the Smtp settings in the appsettings.json file. If you want to use them, write a class SmtpSettings like that:
Register the class like that:
Then you can use it as DI constructor parameter:
You can define whatever properties you like in the class and in the config section. You are not bound to the usual properties. I use a mail delivery package of my own, which delivers mail from the PickupDirectoryLocation. This is a directory used as cache for mail delivery. If the mail transfer fails due to network problems it can be repeated later.
Regarding to Serilog I took an example sink from their sources and rewrote it such that it logs into mails, which are copied to the PickupDirectoryLocation. This is the code:
The sink needs to provide a static extension class:
I hope the code is helpful for you.
Hello Mmaty,
thank you for your input! It gave me a much better understanding of how the entire thing works.
However, my logger never is called. I built a static extension method, which just instantiates the Serilog.Sinks.Email logger, and I included some logger calls, but as far as I can see, the method never gets called.
This is my static method:
And this is the appsettings.config:
The console logger works, but not the email logger. I guess I am doing something really basic wrong, but I can't spot it...
May be another pair of eyes can see it :-)
Kind regards, Mikael
is working on a reply...