Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • mmaty 114 posts 288 karma points
    Feb 04, 2022 @ 11:18
    mmaty
    0

    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:

        "Serilog": {
        "Using": [ "Formfakten.Serilog.EmailPickup" ],
        "MinimumLevel": {
            "Default": "Debug",
            "Override": {
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information",
                "System": "Warning"
            }
        },
        "WriteTo": [
            {
                "Name": "LaykitEmailPickup",
                "Args": {
                    "fromEmail": "[email protected]",
                    "toEmail": "[email protected]",
                    "pickupDirectory": "/App_Data/MailCache",
                    "subject": "Laykit-Fehler",
                    "restrictedToMinimumLevel": "Error"
                }
            }
        ]
    }
    

    EDIT: In the Umbraco Sources in SerilogLogger.CreateWithDefaultConfiguration we see the following lines:

    var loggerConfig = new LoggerConfiguration()
        .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration)
        .ReadFrom.Configuration(configuration);
    

    This ends in a Serilog function

    Serilog.Settings.Configuration.dll ! Serilog.Settings.Configuration.ConfigurationReader.SelectConfigurationMethod()
    

    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.

    _config["Serilog:WriteTo:0:Name"] = "LaykitEmailPickup";
    

    But that's not the way ist should be... Any Ideas?

    Mirko

  • mmaty 114 posts 288 karma points
    Feb 04, 2022 @ 14:34
    mmaty
    0
  • mmaty 114 posts 288 karma points
    Feb 04, 2022 @ 15:31
    mmaty
    1

    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.

  • Mikael Axel Kleinwort 154 posts 499 karma points c-trib
    Dec 05, 2024 @ 08:50
    Mikael Axel Kleinwort
    0

    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

  • mmaty 114 posts 288 karma points
    Dec 05, 2024 @ 13:31
    mmaty
    0

    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; }
    }
    

    Register the class like that:

    services.Configure<SmtpSettings>( config.GetSection( "Umbraco:CMS:Global:Smtp" ) );
    

    Then you can use it as DI constructor parameter:

    IOptionsMonitor<SmtpSettings> smtpSettingsMonitor
    

    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;
                }
            }
        }
    }
    

    I hope the code is helpful for you.

  • Mikael Axel Kleinwort 154 posts 499 karma points c-trib
    Dec 09, 2024 @ 14:48
    Mikael Axel Kleinwort
    0

    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:

    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;
                }
            }
        }
    }
    

    And this is the appsettings.config:

      "Serilog": {
        "Using": [ "KleinwortEffective.Libs.Umb" ],
        "WriteTo": [
          {
            "Name": "KleinwortEffectiveEmailSink",
            "Args": {
              "fromEmail": "myFromEmail",
              "toEmail": "myToEmail",
              "host": "myhost",
              "subject": "Error Log",
              "userName": "myUserName",
              "password": "myPassword",
              "Port": 587,
              "restrictedToMinimumLevel": "Warning"
            }
          },
          {
            "Name": "Async",
            "Args": {
              "configure": [
                {
                  "Name": "Console"
                }
              ]
            }
          }
        ]
      }
    

    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

Please Sign in or register to post replies

Write your reply to:

Draft