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