Using log4net with ASP.NET to track Session variables - asp.net

Our web app captures a user's login and stores it in a session variable, similar to Session("User_Id"). I'd like to use log4net to capture the User in the log.
I see a few references to using the MDC (Mapped Diagnostic Context) has been replaced with ThreadContext properties.
Has anyone implemented this ThreadContext approach? Any suggestions?

In the code...
log4net.ThreadContext.Properties["Log_User"] = userName;
in the web.config
<appender name="ADONetAppender" type="log4net.Appender.ADONetAppender">
<bufferSize value="1" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="set in global.asax" />
<commandText value="INSERT INTO Log4Net ([Log_Date], [Severity],[Application],[Message], [Source], [Log_User]) VALUES (#log_date, #severity, #application, #message, #source, #currentUser)" />
<parameter>
<parameterName value="#log_date" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawTimeStampLayout" />
</parameter>
...
<parameter>
<parameterName value="#currentUser" />
<dbType value="String" />
<size value="100" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{Log_User}" />
</layout>
</parameter>
</appender>

I always encapsulate access to all Session variables in a class. This controls access and let's me use strong typing. I do any logging in this class. Here's an example:
public static class SessionInfo
{
private static readonly ILog log = LogManager.GetLogger(typeof(SessionInfo));
private const string AUDITOR_ID_KEY = "AuditorId";
static SessionInfo()
{
log.Info("SessionInfo created");
}
#region Generic methods to store and retrieve in session state
private static T GetSessionObject<T>(string key)
{
object obj = HttpContext.Current.Session[key];
if (obj == null)
{
return default(T);
}
return (T)obj;
}
private static void SetSessionObject<T>(string key, T value)
{
if (Equals(value, default(T)))
{
HttpContext.Current.Session.Remove(key);
}
{
HttpContext.Current.Session[key] = value;
}
}
#endregion
public static int AuditorId
{
get { return GetSessionObject<int>(AUDITOR_ID_KEY); }
set { SetSessionObject<int>(AUDITOR_ID_KEY, value); }
}
}

Related

Connect NLog To Database Using Azure KeyVault Connection String In NetCore 3.1

My nlogconfig file is writing fine to a text file. It is also writing to a database when I include the connection string in appsettings.json. Now that we are ready to move to production, I am not going to be housing the connection string in appsettings.json.
However, the problem is that I do not know how to connect mynlogconfig file to a connection string that is located in Azure KeyVault.
How do I take this line of code in nlogconfig
connectionString="${configsetting:item=ConnectionStrings.DefaultConnection}"
and reference the connection string in Azure KeyVault?
My nlogconfig file:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true">
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
<add assembly="NLog.Appsettings.Standard"/>
</extensions>
<targets>
<!-- local file target -->
<target name="fileTarget"
xsi:type="File"
fileName="C:\Nlog\logs\meLog.txt"
layout="
-------------- ${level} (${longdate}) --------------${newline}
${newline}
Call Site: ${callsite}${newline}
Exception Type: ${exception:format=Type}${newline}
Exception Message: ${exception:format=Message}${newline}
Stack Trace: ${exception:format=StackTrace}${newline}
Additional Info: ${message}${newline}" />
<target xsi:type="Database"
name="dbTarget"
connectionString="${configsetting:item=ConnectionStrings.DefaultConnection}"
commandText="INSERT INTO Logs(CreatedOn,Message,Level,Exception,StackTrace,Logger,Url) VALUES (#datetime,#msg,#level,#exception,#trace,#logger,#url)">
<parameter name="#datetime" layout="${date}" />
<parameter name="#msg" layout="${message}" />
<parameter name="#level" layout="${level}" />
<parameter name="#exception" layout="${exception:format=#}" />
<parameter name="#trace" layout="${stacktrace}" />
<parameter name="#logger" layout="${logger}" />
<parameter name="#url" layout="${aspnet-request-url}" />
</target>
</targets>
<rules>
<logger name="*" minlevel="Error" writeTo="dbTarget" />
<logger name="*" minlevel="Error" writeTo="fileTarget"/>
</rules>
</nlog>
My Program.cs file which is getting the database connection string from Azure KeyVault:
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using WebApplication6.Models;
namespace WebApplication6
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
string MyClientID = "1MyClientID";
string MyTenantID = "1MyTenantID";
string MyClientSecretID = "1MyClientSecretID";
ClientSecretCredential credential =
new ClientSecretCredential(MyTenantID, MyClientID, MyClientSecretID);
var secretClient = new SecretClient(new Uri("https://somerandomurivault.vault.azure.net/"),
credential);
config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
config.Build();
})
.ConfigureServices((hostContext, services)=>
{
var databaseConnectionString = hostContext.Configuration.GetValue<string>("databaseConnectionString");
services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(databaseConnectionString);
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
I tried to set the connection string to a global variable, but I had no way of referencing this global variable in the nlogconfig file.

Xamarin forms : Open PDF file in external App

I am facing below error when trying to launch a PDF file stored in assets folder of android project in xamarin forms.
"this file could not be accessed. check your connection or make the filename shorter in xamarin forms"
Giving shorter file name didn't help.
Below is my code:
public interface IDocumentView
{
void ShowPDFTXTFromLocal(string filename);
}
[assembly: Xamarin.Forms.Dependency(typeof(DocumentView))]
namespace Portfolio_Pdf.Droid.Platform
{
public class DocumentView : IDocumentView
{
[Obsolete]
public void ShowPDFTXTFromLocal(string filename)
{
string reportSavedPath = "/data/user/0/com.companyname.portfolio_pdf/files/test.pdf";
Java.IO.File file = new Java.IO.File(reportSavedPath);
Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
{
var fileUri = Android.Net.Uri.FromFile(new Java.IO.File(reportSavedPath));
Intent intent = new Intent(Intent.ActionView);
var mimetype = MimeTypeMap.Singleton.GetMimeTypeFromExtension(MimeTypeMap.GetFileExtensionFromUrl((string)fileUri).ToLower());
Android.Net.Uri apkURI = FileProvider.GetUriForFile(
Xamarin.Forms.Forms.Context.ApplicationContext,
Xamarin.Forms.Forms.Context.ApplicationContext.PackageName + ".provider", file);
intent.SetDataAndType(apkURI, mimetype);
intent.SetFlags(ActivityFlags.ClearWhenTaskReset | ActivityFlags.NewTask);
intent.SetFlags(ActivityFlags.GrantReadUriPermission | ActivityFlags.ClearTop);
intent.AddFlags(ActivityFlags.GrantReadUriPermission | ActivityFlags.ClearTop);
try
{
Xamarin.Forms.Forms.Context.StartActivity(intent);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
});
}
}
}
Android.Manifest.xml:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths"/>
</provider>
provider_paths:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external"
path="." />
<external-files-path
name="external_files"
path="." />
<cache-path
name="cache"
path="." />
<external-cache-path
name="external_cache"
path="." />
<files-path
name="files"
path="." />
</paths>

SignalR Javascript client can't connect, timeout occurs

I'm trying to connect to a signalR server with Javascript client. I can't get the JS client to connect to the server. My debug output shows that the client first tries to subscribe to Websocket, then SSE and makes Long Polling request, all of them get timeouts. The server is client to another signalR server and there is no problem with that connection. But the JS client cannot connect.
I have the below packages:
<package id="Microsoft.AspNet.SignalR" version="2.1.0" targetFramework="net451" />
<package id="Microsoft.AspNet.SignalR.Client" version="2.1.0" targetFramework="net451" />
<package id="Microsoft.AspNet.SignalR.Core" version="2.1.0" targetFramework="net451" />
<package id="Microsoft.AspNet.SignalR.JS" version="2.1.0" targetFramework="net451" />
<package id="Microsoft.AspNet.SignalR.Owin" version="1.2.1" targetFramework="net451" />
<package id="Microsoft.AspNet.SignalR.SystemWeb" version="2.1.0" targetFramework="net451" />
My code is as below:
[assembly: OwinStartup(typeof(Startup))]
namespace CompanyPage
{
public class Startup
{
[ExceptionAspect]
public void Configuration(IAppBuilder app)
{
app.MapSignalR("/signalr", new HubConfiguration { EnableDetailedErrors = true });
}
}
}
.
protected void Session_Start(object sender, EventArgs e)
{
var context = GlobalHost.ConnectionManager.GetHubContext<ExchangeRate>();
context.Clients.All.insertOrder("TEST");
}
.
$(function () {
$.connection.hub.logging = true;
var chat = $.connection.exchangeRate;
chat.client.insertOrder = function(message) {
alert(message);
};
$.connection.hub.start().done(function() {
console.log("Connection Started");
});
});

How do I get log4net to decrypt an encrypted connection string from the web.config?

The web application I'm working on uses log4net for logging. A requirement of the project is that the connections strings should be encrypted. How do I tell log4net to use the decrypted value?
For example:
<log4net>
<root>
<level value="Debug"/>
<appender-ref ref="AdoNetAppender"/>
</root>
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="1"/>
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
<connectionString value="encryptedconnectionstringhere=="/>
Is there a way to accomplish this?
When implementing drumboog's answer, I ran into stackoverflow exceptions due to an infinitely recursive method call. This is essentially what I ended up using.
public class CustomAdoNetAppender : AdoNetAppender
{
private string _connectionString;
protected override string ResolveConnectionString(out string connectionStringContext)
{
if(string.IsNullOrEmpty(_connectionString))
{
var decrypt = new MyDecyptionLib();
_connectionString = decrypt.MyDecryptionFunction(ConfigurationManager.AppSettings["Connection"]);
}
connectionStringContext = _connectionString;
return connectionStringContext;
}
}
...and in the log4net config section
<appender name="AdoNetAppender" type="My.Name.Space.To.CustomAdoNetAppender">
Aside from writing a custom appender, you could encrypt the entire configuration section:
http://msdn.microsoft.com/en-us/library/zhhddkxy.aspx
Programmatically encrypting a config-file in .NET
Edit:
log4net is open source, so you can also try looking through their code and customizing their appender to fit your needs... maybe something like this:
public class DecryptConnectionStringAdoNetAppender : AdoNetAppender
{
protected override string ResolveConnectionString(out string connectionStringContext)
{
string result = base.ResolveConnectionString(out connectionStringContext);
if (String.IsNullOrEmpty(result))
{
return result;
}
else
{
Decrypt(result);
}
}
private string Decrypt(string encryptedValue)
{
// Your code goes here.
}
}
Then update the type attribute of the appender element in the config file:
<appender name="AdoNetAppender" type="Your.Namespace.DecryptConnectionStringAdoNetAppender">

Request is not available in this context

I'm running IIS 7 Integrated mode and I'm getting
Request is not available in this context
when I try to access it in a Log4Net related function that is called from Application_Start. This is the line of code I've
if (HttpContext.Current != null && HttpContext.Current.Request != null)
and an exception is being thrown for second comparison.
What else can I check other than checking HttpContext.Current.Request for null??
A similar question is posted #
Request is not available in this context exception when runnig mvc on iis7.5
but no relevant answer there either.
Please see IIS7 Integrated mode: Request is not available in this context exception in Application_Start:
The “Request is not available in this
context” exception is one of the more
common errors you may receive on when
moving ASP.NET applications to
Integrated mode on IIS 7.0. This
exception happens in your
implementation of the
Application_Start method in the
global.asax file if you attempt to
access the HttpContext of the request
that started the application.
When you have custom logging logic, it is rather annoying to be forced either not to log application_start or to have to let an exception occurs in the logger (even if handled).
It appears that rather than testing for Request availability, you can test for Handler availability: when there is no Request, it would be strange to still have a request handler. And testing for Handler does not raise that dreaded Request is not available in this context exception.
So you may change your code to:
var currContext = HttpContext.Current;
if (currContext != null && currContext.Handler != null)
Beware, in the context of a http module, Handler may not be defined though Request and Response are defined (I have seen that in BeginRequest event). So if you need request/response logging in a custom http module, my answer may not be suitable.
This is very classic case: If you end up having to check for any data provided by the http instance then consider moving that code under the BeginRequest event.
void Application_BeginRequest(Object source, EventArgs e)
This is the right place to check for http headers, query string and etc...
Application_Start is for the settings that apply for the application entire run time, such as routing, filters, logging and so on.
Please, don't apply any workarounds such as static .ctor or switching to the Classic mode unless there's no way to move the code from the Start to BeginRequest. that should be doable for the vast majority of your cases.
Since there's no Request context in the pipeline during app start anymore, I can't imagine there's any way to guess what server/port the next actual request might come in on. You have to so it on Begin_Session.
Here's what I'm using when not in Classic Mode. The overhead is negligible.
/// <summary>
/// Class is called only on the first request
/// </summary>
private class AppStart
{
static bool _init = false;
private static Object _lock = new Object();
/// <summary>
/// Does nothing after first request
/// </summary>
/// <param name="context"></param>
public static void Start(HttpContext context)
{
if (_init)
{
return;
}
//create class level lock in case multiple sessions start simultaneously
lock (_lock)
{
if (!_init)
{
string server = context.Request.ServerVariables["SERVER_NAME"];
string port = context.Request.ServerVariables["SERVER_PORT"];
HttpRuntime.Cache.Insert("basePath", "http://" + server + ":" + port + "/");
_init = true;
}
}
}
}
protected void Session_Start(object sender, EventArgs e)
{
//initializes Cache on first request
AppStart.Start(HttpContext.Current);
}
Based on OP detailed needs explained in comments, a more appropriate solution exists.
The OP states he wishes to add custom data in its logs with log4net, data related to requests.
Rather than wrapping each log4net call into a custom centralized log call which handles retrieving request related data (on each log call), log4net features context dictionaries for setting up custom additional data to log. Using those dictionnaries allows to position your request log data for current request at BeginRequest event, then to dismiss it at EndRequest event. Any log in between will benefit from these custom data.
And things that do not happen in a request context will not try to log request related data, eliminating the need to test for request availability. This solution matches the principle Arman McHitaryan was suggesting in his answer.
For this solution to work, you will also need some additional configuration on your log4net appenders in order for them to log your custom data.
This solution can be easily implemented as a custom log enhancement module. Here is some sample code for it:
using System;
using System.Web;
using log4net;
using log4net.Core;
namespace YourNameSpace
{
public class LogHttpModule : IHttpModule
{
public void Dispose()
{
// nothing to free
}
private const string _ipKey = "IP";
private const string _urlKey = "URL";
private const string _refererKey = "Referer";
private const string _userAgentKey = "UserAgent";
private const string _userNameKey = "userName";
public void Init(HttpApplication context)
{
context.BeginRequest += WebAppli_BeginRequest;
context.PostAuthenticateRequest += WebAppli_PostAuthenticateRequest;
// All custom properties must be initialized, otherwise log4net will not get
// them from HttpContext.
InitValueProviders(_ipKey, _urlKey, _refererKey, _userAgentKey,
_userNameKey);
}
private void InitValueProviders(params string[] valueKeys)
{
if (valueKeys == null)
return;
foreach(var key in valueKeys)
{
GlobalContext.Properties[key] = new HttpContextValueProvider(key);
}
}
private void WebAppli_BeginRequest(object sender, EventArgs e)
{
var currContext = HttpContext.Current;
currContext.Items[_ipKey] = currContext.Request.UserHostAddress;
currContext.Items[_urlKey] = currContext.Request.Url.AbsoluteUri;
currContext.Items[_refererKey] = currContext.Request.UrlReferrer != null ?
currContext.Request.UrlReferrer.AbsoluteUri : null;
currContext.Items[_userAgentKey] = currContext.Request.UserAgent;
}
private void WebAppli_PostAuthenticateRequest(object sender, EventArgs e)
{
var currContext = HttpContext.Current;
// log4net doc states that %identity is "extremely slow":
// http://logging.apache.org/log4net/release/sdk/log4net.Layout.PatternLayout.html
// So here is some custom retrieval logic for it, so bad, especialy since I
// tend to think this is a missed copy/paste in that documentation.
// Indeed, we can find by inspection in default properties fetch by log4net a
// log4net:Identity property with the data, but it looks undocumented...
currContext.Items[_userNameKey] = currContext.User.Identity.Name;
}
}
// General idea coming from
// http://piers7.blogspot.fr/2005/12/log4net-context-problems-with-aspnet.html
// We can not use log4net ThreadContext or LogicalThreadContext with asp.net, since
// asp.net may switch thread while serving a request, and reset the call context
// in the process.
public class HttpContextValueProvider : IFixingRequired
{
private string _contextKey;
public HttpContextValueProvider(string contextKey)
{
_contextKey = contextKey;
}
public override string ToString()
{
var currContext = HttpContext.Current;
if (currContext == null)
return null;
var value = currContext.Items[_contextKey];
if (value == null)
return null;
return value.ToString();
}
object IFixingRequired.GetFixedObject()
{
return ToString();
}
}
}
Add it to your site, IIS 7+ configuration sample:
<system.webServer>
<!-- other stuff removed ... -->
<modules>
<!-- other stuff removed ... -->
<add name="LogEnhancer" type="YourNameSpace.LogHttpModule, YourAssemblyName" preCondition="managedHandler" />
<!-- other stuff removed ... -->
</modules>
<!-- other stuff removed ... -->
</system.webServer>
And set up appenders to log those additional properties, sample config:
<log4net>
<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
<!-- other stuff removed ... -->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message - %property%newline%exception" />
</layout>
</appender>
<appender name="SqlAppender" type="log4net.Appender.AdoNetAppender">
<!-- other stuff removed ... -->
<commandText value="INSERT INTO YourLogTable ([Date],[Thread],[Level],[Logger],[UserName],[Message],[Exception],[Ip],[Url],[Referer],[UserAgent]) VALUES (#log_date, #thread, #log_level, #logger, #userName, #message, #exception, #Ip, #Url, #Referer, #UserAgent)" />
<!-- other parameters removed ... -->
<parameter>
<parameterName value="#userName" />
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{userName}" />
</layout>
</parameter>
<parameter>
<parameterName value="#Ip"/>
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{Ip}" />
</layout>
</parameter>
<parameter>
<parameterName value="#Url"/>
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{Url}" />
</layout>
</parameter>
<parameter>
<parameterName value="#Referer"/>
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{Referer}" />
</layout>
</parameter>
<parameter>
<parameterName value="#UserAgent"/>
<dbType value="String" />
<size value="255" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{UserAgent}" />
</layout>
</parameter>
</appender>
<!-- other stuff removed ... -->
</log4net>
You can get around the problem without switching to classic mode and still use Application_Start
public class Global : HttpApplication
{
private static HttpRequest initialRequest;
static Global()
{
initialRequest = HttpContext.Current.Request;
}
void Application_Start(object sender, EventArgs e)
{
//access the initial request here
}
For some reason, the static type is created with a request in its HTTPContext, allowing you to store it and reuse it immediately in the Application_Start event
This worked for me - if you have to log in Application_Start, do it before you modify the context. You will get a log entry, just with no source, like:
2019-03-12 09:35:43,659 INFO (null) - Application Started
I generally log both the Application_Start and Session_Start, so I see more detail in the next message
2019-03-12 09:35:45,064 INFO ~/Leads/Leads.aspx - Session Started (Local)
protected void Application_Start(object sender, EventArgs e)
{
log4net.Config.XmlConfigurator.Configure();
log.Info("Application Started");
GlobalContext.Properties["page"] = new GetCurrentPage();
}
protected void Session_Start(object sender, EventArgs e)
{
Globals._Environment = WebAppConfig.getEnvironment(Request.Url.AbsoluteUri, Properties.Settings.Default.LocalOverride);
log.Info(string.Format("Session Started ({0})", Globals._Environment));
}
I was able to workaround/hack this problem by moving in to "Classic" mode from "integrated" mode.
In visual studio 2012, When I published the solution mistakenly with 'debug' option I got this exception. With 'release' option it never occurred. Hope it helps.
Set application pool to .NET v4.5 Classic
public bool StartVideo(byte channel)
{
try
{
CommandObject command = new CommandObject(Commands.START_VIDEO, new byte[] {channel}, channel);
m_ResponseEvent.Reset();
lock (m_Commands)
{
m_Commands.Enqueue(command);
}
if (m_ResponseEvent.WaitOne(5000, true))
{
return m_Response == null ? false : true;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
return false;
}
You can use following:
protected void Application_Start(object sender, EventArgs e)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(StartMySystem));
}
private void StartMySystem(object state)
{
Log(HttpContext.Current.Request.ToString());
}
do this in global.asax.cs:
protected void Application_Start()
{
//string ServerSoftware = Context.Request.ServerVariables["SERVER_SOFTWARE"];
string server = Context.Request.ServerVariables["SERVER_NAME"];
string port = Context.Request.ServerVariables["SERVER_PORT"];
HttpRuntime.Cache.Insert("basePath", "http://" + server + ":" + port + "/");
// ...
}
works like a charm. this.Context.Request is there...
this.Request throws exception intentionally based on a flag

Resources