0

Adding logging to a ASP.NET Core 3.1 Web API.

I got logging working in startup, but it's not getting registered in dependency injection as I wanted.

I implemented a set of log4net classes.

Log4netExtensions.cs

using System;
using log4net;
using Microsoft.Extensions.Logging;

namespace StudentPortal4Api.Utilities
{
    public static class Log4netExtensions // this class lets you set up the log4net in Startup.cs where people expect it, instead of in Program.cs.- EWB
    {
        public static ILoggerFactory AddLog4NetCustom( this ILoggerFactory factory, string log4NetConfigFile )
        {
            factory.AddProvider( new Log4NetProvider( log4NetConfigFile ) );

            return factory;
        }

        public static ILoggerFactory AddLog4NetCustom( this ILoggerFactory factory )
        {
            factory.AddProvider( new Log4NetProvider( "./log4net.config" ) );

            return factory;
        }

        public static void LogTrace( this ILogger log, string message, Exception exception )
        {
            log.Log( LogLevel.Information, "TRACE: " + message, exception );
        }

        public static void LogTrace( this ILogger log, string message )
        {
            log.Log( LogLevel.Information, "TRACE: " + message );
        }

        public static void LogDebug( this ILogger log, string message, Exception exception )
        {
            log.Log( LogLevel.Information, "Debug: " + message, exception );
        }

        public static void LogDebug( this ILogger log, string message )
        {
            log.Log( LogLevel.Information, "Debug: " + message );
        }
    }
}

Log4netlogger.cs

using System;
using System.Reflection;
using System.Xml;
using log4net;
using log4net.Repository;
using Microsoft.Extensions.Logging;

public class Log4NetLogger : ILogger // These class allows Log4net logging set up behind the Microsoft ILogger implementation,
                                     // so code uses ilogger from MS, but log4net does the logging - EWB
                                     // simple Log4net Dontnetcore3.1 https://www.thecodebuzz.com/log4net-file-logging-console-logging-asp-net-core/
                                     // https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/
{
    private readonly string _name;
    private readonly XmlElement _xmlElement;
    private readonly ILog _log;
    private ILoggerRepository _loggerRepository;
    public Log4NetLogger(string name, XmlElement xmlElement)
    {
        _name = name;
        _xmlElement = xmlElement;
        _loggerRepository = log4net.LogManager.CreateRepository(
            Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
        _log = LogManager.GetLogger(_loggerRepository.Name, name);
        log4net.Config.XmlConfigurator.Configure(_loggerRepository, xmlElement);
    }
    public IDisposable BeginScope<TState>(TState state)
    {
        return null;
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        switch (logLevel)
        {
            case LogLevel.Critical:
                return _log.IsFatalEnabled;
            case LogLevel.Debug:
            case LogLevel.Trace:
                return _log.IsDebugEnabled;
            case LogLevel.Error:
                return _log.IsErrorEnabled;
            case LogLevel.Information:
                return _log.IsInfoEnabled;
            case LogLevel.Warning:
                return _log.IsWarnEnabled;
            default:
                throw new ArgumentOutOfRangeException(nameof(logLevel));
        }
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
        Exception exception, Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        if (formatter == null)
        {
            throw new ArgumentNullException(nameof(formatter));
        }
        string message = null;
        if (null != formatter)
        {
            message = formatter(state, exception);
        }
        if (!string.IsNullOrEmpty(message) || exception != null)
        {
            switch (logLevel)
            {
                case LogLevel.Critical:
                    _log.Fatal(message);
                    break;
                case LogLevel.Debug:
                case LogLevel.Trace:
                    _log.Debug(message);
                    break;
                case LogLevel.Error:
                    _log.Error(message);
                    break;
                case LogLevel.Information:
                    _log.Info(message);
                    break;
                case LogLevel.Warning:
                    _log.Warn(message);
                    break;
                default:
                    _log.Warn($"Encountered unknown log level {logLevel}, writing out as Info.");
                    _log.Info(message, exception);
                    break;
            }
        }
    }
}

log4netProvider.cs

using System.Collections.Concurrent;
using System.IO;
using System.Xml;
using Microsoft.Extensions.Logging;

public class Log4NetProvider : ILoggerProvider// these class allows Log4net logging set up behind teh Microsoft ILogger implementation,
                                              // so code uses ilogger from MS, but log4net does the logging - EWB
                                              //// this appears to be petes teams solution https://www.michalbialecki.com/2018/12/21/adding-a-log4net-provider-in-net-core-console-app/ - EWB
/// https://stackify.com/net-core-loggerfactory-use-correctly/
/// https://www.michalbialecki.com/2018/12/21/adding-a-log4net-provider-in-net-core-console-app/
/// https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/
{
    private readonly string _log4NetConfigFile;
    private readonly ConcurrentDictionary<string, Log4NetLogger> _loggers =
        new ConcurrentDictionary<string, Log4NetLogger>();
    public Log4NetProvider(string log4NetConfigFile)
    {
        _log4NetConfigFile = log4NetConfigFile;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return _loggers.GetOrAdd(categoryName, CreateLoggerImplementation);
    }

    public void Dispose()
    {
        _loggers.Clear();
    }
    private Log4NetLogger CreateLoggerImplementation(string name)
    {
        return new Log4NetLogger(name, Parselog4NetConfigFile(_log4NetConfigFile));
    }

    private static XmlElement Parselog4NetConfigFile(string filename)
    {
        XmlDocument log4netConfig = new XmlDocument();
        log4netConfig.Load(File.OpenRead(filename));
        return log4netConfig["log4net"];
    }
}


in Startup.cs, in the configure method, i have this

            loggerFactory.AddLog4NetCustom();
            ILogger log = loggerFactory.CreateLogger( "Startup::Configure(...)" );
            log.LogTrace( "Startup::Configure(...)" );
 

in my controlelrs, I"m addingit for di thusly

 public SpBaseController( ILogger _logger, IConfiguration configuration, IDapperTools _dapperTools )
        {
            log           = _logger;
            Configuration = configuration;
            DapperTools   = _dapperTools;
        }

then in my derived controllers

  public AdStudentController( ILoggerFactory _loggerFactory, IConfiguration _configuration, IDapperTools _dapperTools, IStudentBll passStudBll, IWebHostEnvironment passEnvironment ) : base( _loggerFactory.CreateLogger<AdStudentController>(  ) , _configuration, _dapperTools )
        {
            studBll         = passStudBll;
            hostEnvironment = passEnvironment;
            log.LogTrace( "AdStudentController constructor" );
        }

Which seems like it's a bit redundant..

and then in my classes

 public class BaseBll : IBaseBLL
    {
        IBaseDAL dal;

        public readonly ILogger log;

        public BaseBll(IBaseDAL _dal, ILogger _logger )
        {
            dal = _dal;
            log = _logger;
        }

then in my derived classes

  public class AdBll: BaseBll, IAdBll
    {
        public IConfiguration Configuration { get; }
        public IStudentDal    dal;

        public AdBll( IConfiguration configuration, IStudentDal _dal, ILogger _log ) : base(_dal, _log)
        {
            Configuration = configuration;
            dal           = _dal;

        }

I'm not getting any logging from anywhere in my classes.

I'm assuming I"m not registering it for DI correctly, where and how do i do that?

startup.cs(had to out parts to get the character count down to minium)

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using AdoNetCore.AseClient;
using AutoMapper;
using Dapper.Logging;
using log4net;
using log4net.Config;
using StudentPortal4Api.Dal;
using StudentPortal4Api.Services.StudDistLearnSchedule;
using StudentPortal4Api.Utilities;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Debug;
using StudentPortal4Api.Bll;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using StudentPortal4Api.Dto;
using StudentPortal4Api.Services;
using StudentPortal4Api.Services.StudSchedule;
using VueCliMiddleware;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Newtonsoft.Json;
using StudentPortal4Api.Utility;
using ElmahCore;
using ElmahCore.Mvc;
using ElmahCore.Mvc.Notifiers;


namespace StudentPortal4Api 
{
    public class Startup
    {
        const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
        bool         bJwtOn                 = false;

        private static readonly ILog log = LogManager.GetLogger( MethodBase.GetCurrentMethod().DeclaringType );


        public Startup(IConfiguration configuration )
        {
            Configuration = configuration;

            //DoCheezyTests( );
        }

        static void DoCheezyTests( )
        {
            var logRepository = LogManager.GetRepository( Assembly.GetEntryAssembly( ) );
            XmlConfigurator.Configure( logRepository, new FileInfo( "log4netTest.config" ) );

            Console.WriteLine( "Hello world!" );

            // Log some things
            log.Info( "Hello logging world!" );
            log.Error( "Error!" );
            log.Warn( "Warn!" );

            string filepath = "_file.txt";
            if( File.Exists( filepath ) )
            {
                File.Delete( filepath );
            }            
            
            using ( StreamWriter writer = System.IO.File.CreateText( filepath ) )
            {
                writer.WriteLine( "message" );
            }
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices( IServiceCollection services )
        {
            services.AddMvc( options => options.Filters.Add( typeof( ExceptionFilter ) ) )
                    .SetCompatibilityVersion( CompatibilityVersion.Version_2_2 );



            //new way .NetCore https://stackoverflow.com/questions/40275195/how-to-set-up-automapper-in-asp-net-core
            // Auto Mapper Configurations
            var mapperConfig = new MapperConfiguration(
                                                       mc =>
                                                       {
                                                           mc.AddProfile( new AutoMapProfile( ) ); // TODO : We need to go back and remove auto mapper and use fill me with instead. Automapper was flaky,  I think it was the handling of nulls when copying, forcing me to change the dals pattern of get eo and copy...so I went back to fill me with...  - EWB
                                                       }
                                                      );

            IMapper mapper = mapperConfig.CreateMapper( );
            services.AddSingleton( mapper );

            services.AddSingleton< Microsoft.Extensions.Logging.ILogger >( provider => provider.GetRequiredService< Microsoft.Extensions.Logging.ILogger< BaseDAL > >( ) );

            services.AddMemoryCache( ); // Add this line

            // okta token security for webApi - EWB
            //https://developer.okta.com/blog/2019/04/10/build-rest-api-with-aspnetcore
            // looks to be different version see app.UseAuthentication();
            // .Netcore 2.2 https://developer.okta.com/blog/2019/04/10/build-rest-api-with-aspnetcore
            // another article : https://developer.okta.com/blog/2018/02/01/secure-aspnetcore-webapi-token-auth

            services.Configure< OktaConfig >( Configuration.GetSection( "Okta" ) );
            services.AddSingleton< ITokenService, TokenService >( ); // uses the OktaConfig service above - EWB

            services.AddScoped< ResponseTimeActionFilter >( );

            if ( GetUseOktaSecurityVar( ) )
            {
                services.AddAuthentication(
                                           options => { options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }
                                          )
                        .AddJwtBearer(
                                      options =>
                                      {
                                          options.Authority            = Configuration[ "Okta:Authority" ];
                                          options.Audience             = "api://default";
                                          options.RequireHttpsMetadata = Convert.ToBoolean( Configuration[ "Okta:RequireHttpsMetadata" ] );
                                      }
                                     );


                Trace.WriteLine( "************** JWT SECURITY ON **************" );

                bJwtOn = true;

                services.AddMvc(
                                options => // https://stackoverflow.com/questions/56283860/how-to-check-whether-request-is-going-to-an-method-with-authorize-attribute
                                {
                                    options.Filters.Add( new AuthorizeFilter(   ) );
                                }
                               );
            }
            else
            {
                services.AddAuthorization(
                                          x => // this modifies the default policy such that the Authorized on Base controller is ignored (well, always passes, same same-ish)- EWB (https://stackoverflow.com/questions/41577389/net-core-api-conditional-authentication-attributes-for-development-production)
                                          {
                                              // _env is of type IHostingEnvironment, which you can inject in
                                              // the ctor of Startup
                                              x.DefaultPolicy = new AuthorizationPolicyBuilder( )
                                                                .RequireAssertion( _ => true )
                                                                .Build( );
                                          }
                                         );



                Trace.WriteLine( "**********************************************" );
                Trace.WriteLine( "************** JWT SECURITY OFF **************" );
                Trace.WriteLine( "**********************************************" );

                bJwtOn = false;

                services.AddMvc( );

            }

            //services.AddMvc();

            var whiteList      = new List< string >( );
            var myArraySection = Configuration[ "AllowedOrigin" ];

            if ( ! String.IsNullOrEmpty( myArraySection ) )
            {
                foreach ( var d in myArraySection.Split( ',' ) )
                {
                    whiteList.Add( d.Trim( ) );
                    Trace.WriteLine( "* CorsList + " + d.Trim( ) );
                }
            }

            // CORS
            services.AddCors(
                             options =>
                             {
                                 options.AddPolicy(
                                                   "AllowSpecificOrigin",
                                                   policy => policy.WithOrigins( whiteList.ToArray( ) )
                                                                   .AllowAnyMethod( )
                                                                   .AllowAnyHeader( )
                                                  );
                             }
                            );

            services.AddControllers( );
            services.AddSpaStaticFiles( configuration => { configuration.RootPath = "ClientApp"; } );

            services.AddDbConnectionFactory( prv => new AseConnection( Configuration.GetConnectionString( "SybaseDBDapper" ) ) );

            AddIOCDependencies( services );

            // Register the Swagger generator, defining 1 or more Swagger documents
            services.AddSwaggerGen( c =>{

                                        //The generated Swagger JSON file will have these properties.
                                        //c.SwaggerDoc(
                                        //             "v1", new Info
                                        //                       {
                                        //                           Title   = "Swagger XML Api Demo",
                                        //                           Version = "v1",
                                        //                       }
                                        //            );

            //Locate the XML file being generated by ASP.NET...
            //var xmlFile = $"{Assembly.GetExecutingAssembly( ).GetName( ).Name}.XML";
            //var xmlPath = Path.Combine( AppContext.BaseDirectory, xmlFile );

            //... and tell Swagger to use those XML comments.
            // c.IncludeXmlComments( xmlPath );
            });

            services.AddMediatR( Assembly.GetExecutingAssembly() );

            //  set up a filter for validations https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi/
            services.AddControllers( options => options.Filters.Add( new ApiErrorExceptionFilter() ) ); // Never gets called, commented out source - EWB

            //services.AddControllers( options => options.Filters.Add( new GlobalExceptionFilter() ) );

            EmailOptions emailOptions = new EmailOptions
                                            {
                                                MailRecipient     = Configuration[ "AppSettings:To_ErrorMail_Elmah" ],
                                                MailSender        = Configuration[ "AppSettings:ElmahEmailFrom" ],
                                                SmtpServer        = Configuration[ "AppSettings:SmtpServer" ],
                                                SmtpPort          = Convert.ToInt32( Configuration[ "AppSettings:SmtpPort" ] ),
                                                MailSubjectFormat = Configuration[ "MailSubjectFormat" ],
                                                AuthUserName      = "loginUsername",
                                                AuthPassword      = "loginPassword"
                                            };

            services.AddElmah<XmlFileErrorLog>( options =>
                                                {
                                                    //options.OnPermissionCheck = context => context.User.Identity.IsAuthenticated; // user must be authenticated to see elmah page.- EWB
                                                    options.ApplicationName = Configuration[ "AppSettings:ApplicationName" ];
                                                    options.Notifiers.Add( new ErrorMailNotifier( "Email", emailOptions ) );
                                                    options.LogPath = "~/ELMAH/log";
                                                } );
            ConfigurationHelper.Initialize( Configuration );
        }

        bool GetUseErrorEmailVar()
        {
            string setting = Utils.GetAppSettingStatic( "bUseErrorEmails" );

            return ( setting.ToLower() == "true" );
        }


        bool GetUseOktaSecurityVar()
        {
            string setting = Utils.GetAppSettingStatic( "bUseOktaJwtSecurity" );

            return ( setting.ToLower() == "true" );
        }

        bool GetHttpsSecurityVar()
        {
            string setting = Utils.GetAppSettingStatic( "bUseHttps" );

            return ( setting.ToLower() == "true" );
        }

        
        static void AddIOCDependencies( IServiceCollection services )
        {
            // add IOC dependency, every DAL and BLL, or anything else with an interface that you add should show up here - EWB

            services.AddScoped< IDapperTools, DapperTools >( );
            services.AddScoped< IStudDistLearnSchedWorker, StudDistLearnSchedWorker >( );
            services.AddScoped< IStudSchedWorker, StudSchedWorker >( );

            services.AddScoped< ICenterDAL, CenterDAL >( );
            services.AddScoped< ICenterBLL, CenterBLL >( );

            services.AddScoped< IEvalGoalsDAL, EvalGoalsDAL >( );
            services.AddScoped< IEvalGoalsBLL, EvalGoalsBLL >( );

            services.AddScoped< IEvalStrengthsBarriersDAL, EvalStrengthsBarriersDAL >( );
            services.AddScoped< IEvalStrengthsBarriersBLL, EvalStrengthsBarriersBLL >( );

            services.AddScoped< IEvaluationFormPartialDAL, EvaluationFormPartialDAL >( );
            services.AddScoped< IEvaluationFormPartialBLL, EvaluationFormPartialBLL >( );

            services.AddScoped< IPendingEvaluationsDAL, PendingEvaluationsDAL >( );
            services.AddScoped< IPendingEvaluationsBLL, PendingEvaluationsBLL >( );

            services.AddScoped<IStudentDal, StudentDal>();
            services.AddScoped<IStudentBll, StudentBll>();

            services.AddScoped< IStudentSchedDal, StudentSchedDal >( );
            services.AddScoped< IStudentSchedBll, StudentSchedBll >( );

            services.AddScoped< IStudEvalDal, StudEvalDal >( );
            services.AddScoped< IStudEvalBll, StudEvalBll >( );

            services.AddScoped< IEvalStrengthsBarriersDAL, EvalStrengthsBarriersDAL >( );
            services.AddScoped< IEvalStrengthsBarriersBLL, EvalStrengthsBarriersBLL >( );

            services.AddScoped< IETarDAL, ETarDAL >( );
            services.AddScoped< IETarBLL, ETarBLL >( );

            services.AddScoped< IPreferencesDAL, PreferencesDAL >( );
            services.AddScoped< IPreferencesBLL, PreferencesBLL >( );

            services.AddScoped< IAnnouncementsDAL, AnnouncementsDAL >( );
            services.AddScoped< IAnnouncementsBLL, AnnouncementsBLL >( );

            services.AddScoped<IStudentPortalFacultyDal, StudentPortalFacultyDal>();
            services.AddScoped<IStudentPortalFacultyBll, StudentPortalFacultyBll>();

            services.AddScoped<IUtils, Utils>();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure( IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory )//, ILogger log
        {
            //loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            //loggerFactory.AddDebug();
            
            loggerFactory.AddLog4NetCustom();
            ILogger log = loggerFactory.CreateLogger( "Startup::Configure(...)" );
            log.LogTrace( "Startup::Configure(...)" );
            
app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. app.UseRouting(); if ( GetHttpsSecurityVar() ) { // // refers to "https_port" in appsettings.json to get port - EWB app.UseHttpsRedirection( ); //https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-5.0&tabs=visual-studio log.LogTrace( "************** HTTPS SECURITY ON **************" ); } else { log.LogTrace( "************************************************" ); log.LogTrace( "************** HTTPS SECURITY OFF **************" ); log.LogTrace( "************************************************" ); } log.LogTrace( "" ); if ( bJwtOn )// flag is tied to other code up in up in configureServices { log.LogTrace( "************** JWT SECURITY ON **************" );// NOTE: This will not work unless you uncomment out the [Authorize] tag on SpBaseController - EWB } else { log.LogTrace( "**********************************************" ); log.LogTrace( "************** JWT SECURITY OFF **************" ); log.LogTrace( "**********************************************" ); } app.UseSpaStaticFiles(); // CORS: UseCors with CorsPolicyBuilder. app.UseCors("AllowSpecificOrigin"); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); app.UseSpa(spa => { if (env.IsDevelopment()) spa.Options.SourcePath = "ClientApp"; else spa.Options.SourcePath = "dist"; if (env.IsDevelopment()) { spa.UseVueCli(npmScript: "serve"); } }); //app.UseMiddleware(); //app.ConfigureExceptionHandler( loggerFactory.CreateLogger( "ExceptionHandler" ) );
        }
    }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Eric Brown - Cal
  • 14,135
  • 12
  • 58
  • 97

0 Answers0