[WEB API]如何在上線階段關閉nanoprofiler的監控頁面,如何把數據抓取出來另做紀錄
前言
之前有針對nanoprofiler這個監控利器做過較基礎的介紹了,不過還有一些比較進階的議題,可能連github都沒有介紹到,比如說我們該如何在上線階段關掉監控頁面,畢竟我們不可能隨意讓人訪問,而得知我們每個api所執行的時間甚至連sql和參數,還有我們預設是儲存200筆而已,這些數據如果沒保存起來,我們怎麼回溯過往的紀錄呢?這篇就是想要好好來說明這些部份的東西,如果你完全不懂這篇在說明什麼的話,可以參考筆者以前的文章(https://dotblogs.com.tw/kinanson/2017/05/17/073240)
導覽
雖然官方並未提供給我們任何可以關閉nanoprofiler/view的方法,但是我們當然可以利用自己的機制來處理,HttpModule也就是代表了web server的頁面每次只要有訪問的時候,一定都會先經過這關,但不包含web api哦,因為nanoprofiler是走傳統mvc的路由,而不是走web api的路由,先新增一支NanoProfilerModule.cs
public class NanoProfilerModule : IHttpModule
{
public NanoProfilerModule()
{
}
public void Init(HttpApplication application)
{
application.BeginRequest +=
(new EventHandler(this.Application_BeginRequest));
application.EndRequest +=
(new EventHandler(this.Application_EndRequest));
}
private void Application_BeginRequest(Object source,
EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
}
private void Application_EndRequest(Object source, EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
var IsDisableNanoProfiler =Convert.ToBoolean(System.Configuration.ConfigurationManager.AppSettings["IsDisableNanoProfiler"]); //從web.config讀取是否要開啟nanoprofiler的頁面
if (context.Request.FilePath == "/nanoprofiler/view" && IsDisableNanoProfiler)
{
throw new HttpException(404, String.Format("The file {0} does not exist", context.Request.PhysicalPath));
}
}
public void Dispose() { }
}
web.config下的我就自行加入了IsDisableNanoProfiler。
還有HttpModule的註冊
<add name="NanoProfilerModule" type="BookStore.Api.Filter.NanoProfilerModule"/>
這樣子我們只要在上線階段切換config就可以去關閉掉nanprofiler了,當然聰明的讀者一定了解,即然我們能判斷到頁面,想用什麼方式來關閉就取決於你了,比如說你可以判斷只有localhost的頁面可以訪問nanoprofiler或者用只有Debug Model才能訪問的方式,請自行發揮想像力囉。
nanoprofiler有使用到slf4net這個package,slf4net也有提供了存到nlog或log4net的支援,先打開我們的nuget把相關的package安裝起來吧
安裝完了之後,我們只要在web.config加入下面這段,就會實做存到nlog的部份了
<slf4net>
<factory type="slf4net.NLog.NLogLoggerFactory, slf4net.NLog" />
</slf4net>
但是請注意,你必須得要配置好nlog的info的儲存才能生效哦,至於nlog的config配置部份,筆者就不提供了,再請自行實做囉。
在此部份就比較麻煩點了,因為解析數據的部份其實比較複雜,我目前是隨意設計一個table來存放數據的,而依照團隊的狀況不一樣,可能想保存數據的目標也不一樣,以筆者為例是存放在oracle裡面,想另外保存數據首先要先改掉web.config的實做部份
<!--下面storage的部份,筆者已改成自己實作的類別還有專案名稱了,首先是命名空間和程式類別,第二個則是專案名稱-->
<nanoprofiler circularBufferSize="200" storage="BookStore.Api.Infrastructure.ProfilingStorageOracleDb, BookStore.Api">
<filters>
<add key="_tools" value="_tools/" type="Contain" />
<add key="exts" value="ico,jpg,js,css" type="EF.Diagnostics.Profiling.Web.ProfilingFilters.FileExtensionProfilingFilter, NanoProfiler.Web" />
<add key="ViewProfilingLogsHandler" value="ViewProfilingLogsHandler.*" type="regex" />
</filters>
</nanoprofiler>
接著則是新增相對應的類別ProfilingStorageOracleDb.cs
public class ProfilingStorageOracleDb : ProfilingStorageBase
{
protected override void Save(ITimingSession session)
{
NanoProfilerForDb nanoProfilerForDb = new NanoProfilerForDb();
nanoProfilerForDb.Save(session); //射後不理
}
}
最後則是實做NanoProfilerForDb.cs這支檔案,所有詳細的說明都寫在註解裡面,我相信對工程師來說,看程式碼會比看一堆囉嗦的文字還要容易理解
public class NanoProfilerForDb
{
private IDbConnection GetConnection //回傳OracleConnection
{
get
{
string connString = "Data source=localhost/book;User id=C##ANSON;Password=7154;";
var conn = new OracleConnection(connString);
return conn;
}
}
public async Task Save(ITimingSession session)
{
try
{
using (TransactionScope scope = new TransactionScope())
{
using (var con = GetConnection)
{
var saveMainSession = SaveMainSession(session, con); //請見圖示
var saveRootSession = SaveRootSession(session, con); //請見圖示
var saveDbSession = SaveDbSession(session, con); //請見圖示
await saveMainSession;
await saveRootSession;
await saveDbSession;
scope.Complete();
}
}
}
catch (Exception ex)
{
throw ex;
}
}
public async Task SaveMainSession(ITimingSession session, IDbConnection con)
{
await con.ExecuteAsync(@"INSERT INTO nanoprofiler (mainid,sessionid,machine,type,name,
druation,started,dbdruation,requesttype,clientip,dbcount) VALUES
(:mainid,:sessionid,:machine,:type,:name,:druation,:started,:dbdruation,:requesttype,
:clientip,:dbcount)", new
{
mainid = session.Id.ToString(),//主id,不重覆
sessionId = session.Id.ToString(),//每次api共同的id
machine = session.MachineName,//呼叫的電腦名稱
type = session.Type,//總共分session和setp和db,這個是session
name = session.Name,//api的名字
started = session.Started,//開始時間
dbdruation = session.Data.FirstOrDefault(x => x.Key == "dbDruation").Value,//db耗時
druation = session.DurationMilliseconds,//api的總耗時
requesttype = session.Data.FirstOrDefault(x => x.Key == "requestType").Value,//request的方式,以我的例子是web
clientip = session.Data.FirstOrDefault(x => x.Key == "clientIp").Value,
dbcount = session.Data.FirstOrDefault(x => x.Key == "dbCount").Value//這次api呼叫了多少個連線,如果有兩個sp就會有兩個連線
});
}
public async Task SaveRootSession(ITimingSession session, IDbConnection con)
{
var rootSession = session.Timings.FirstOrDefault(x => x.Type == "step");
await con.ExecuteAsync(@"INSERT INTO nanoprofiler (mainid,sessionid,parentid,machine,type,
name,druation) VALUES (:mainid,:sessionid,:parentid,:machine,:type,:name,:druation)", new
{
mainid = rootSession.Id.ToString(),
sessionid = session.Id.ToString(),
machine = session.MachineName,
type = rootSession.Type, //總共分session和setp和db,這個是setp
parentid = rootSession.ParentId.ToString(), //對應SaveMainSession的mainid
name = rootSession.Name, //紀錄root
druation = rootSession.DurationMilliseconds //耗時
});
}
public async Task SaveDbSession(ITimingSession session, IDbConnection con)
{
var dbSession = session.Timings.Where(x => x.Type == "db");
foreach (var item in dbSession)
{
await con.ExecuteAsync(@"INSERT INTO nanoprofiler (mainid,sessionid,parentId,machine,
type,name,started,druation,executetype,parameters) VALUES
(:mainid,:sessionid,:parentId,:machine,:type,:name,:started,:druation,
:executetype,:parameters)", new
{
mainid = item.Id.ToString(),
sessionid = session.Id.ToString(),
parentId = item.ParentId.ToString(), //對應SaveMainSession的mainid
machine = session.MachineName,
type = item.Type, //總共分session和setp和db,這個是db
name = item.Name, //紀錄執行的sql或sp名稱
started = item.Started,
druation = item.DurationMilliseconds,
executetype = item.Data.FirstOrDefault(x => x.Key == "executeType").Value, //查詢或非查詢
parameters = item.Data.FirstOrDefault(x => x.Key == "parameters").Value //參數包含型別和丟進去的數值
});
}
}
}
最後可以看到db存進來的值
但是請注意一下哦,因為我們把實做替換掉了,所以原本官方儲存nlog的部份,就消失而替換成我們實做的ProfilingStorageOracleDb了,當然即然我們有數據了,我們當然也能自行實做nlog的存取部份囉,但是如果你想要保留原本官方寫的話,那我們就把官方的code跟我們存取db的code,全部結合在一起吧。
public class ProfilingStorageOracleDb : ProfilingStorageBase
{
private static readonly Lazy<ILogger> Logger = new Lazy<ILogger>(() => LoggerFactory.GetLogger(typeof(ProfilingStorageOracleDb)));
/// <summary>
/// Data filed names which should be treated as integer fields.
/// </summary>
public static string[] IntegerDataFieldNames { get; set; }
public ProfilingStorageOracleDb()
{
IntegerDataFieldNames = new[] { "Count", "Size", "econds" };
}
/// <summary>
/// Saves an <see cref="ITimingSession"/>.
/// </summary>
/// <param name="session"></param>
protected override void Save(ITimingSession session)
{
NanoProfilerForDb nanoProfilerForDb = new Infrastructure.NanoProfilerForDb();
nanoProfilerForDb.Save(session); //射後不理
if (!Logger.Value.IsInfoEnabled) //如果有加上log功能的話,才會觸發
{
return;
}
if (session == null)
{
return;
}
SaveSessionJson(session); //儲存主明細
if (session.Timings == null) return;
foreach (var timing in session.Timings) //儲存type為root和db的
{
if (timing == null) continue;
SaveTimingJson(session, timing);
}
}
/// <summary>
/// Whether or not a field is an integer field.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
protected virtual bool IsIntFieldName(string key)
{
if (IntegerDataFieldNames == null || !IntegerDataFieldNames.Any()) return false;
return IntegerDataFieldNames.Any(key.EndsWith);
}
private void SaveSessionJson(ITimingSession session)
{
var sb = new StringBuilder();
sb.Append("{");
AppendSessionSharedFields(sb, session);
AppendTimingFields(sb, session);
sb.Append("}");
Logger.Value.Info(sb.ToString());
}
private void SaveTimingJson(ITimingSession session, ITiming timing)
{
var sb = new StringBuilder();
sb.Append("{");
AppendSessionSharedFields(sb, session);
AppendTimingFields(sb, timing);
sb.Append("}");
Logger.Value.Info(sb.ToString());
}
private static void AppendSessionSharedFields(StringBuilder sb, ITimingSession session)
{
AppendField(sb, "sessionId", session.Id.ToString("N"), null);
AppendField(sb, "machine", session.MachineName);
}
private void AppendTimingFields(StringBuilder sb, ITiming timing)
{
AppendField(sb, "type", timing.Type);
AppendField(sb, "id", timing.Id.ToString("N"));
if (timing.ParentId.HasValue)
AppendField(sb, "parentId", timing.ParentId.Value.ToString("N"));
AppendField(sb, "name", timing.Name);
AppendField(sb, "started", timing.Started);
AppendField(sb, "start", timing.StartMilliseconds);
AppendField(sb, "duration", timing.DurationMilliseconds);
AppendField(sb, "tags", timing.Tags);
AppendField(sb, "sort", timing.Sort);
AppendDataFields(sb, timing.Data);
}
private static void EncodeAndAppendJsString(StringBuilder sb, string s)
{
foreach (var c in s)
{
switch (c)
{
case '\'':
sb.Append("\\\'");
break;
case '\"':
sb.Append("\\\"");
break;
case '\\':
sb.Append("\\\\");
break;
case '\b':
sb.Append("\\b");
break;
case '\f':
sb.Append("\\f");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
default:
var i = (int)c;
if (i < 32 || i > 127)
{
sb.AppendFormat("\\u{0:X04}", i);
}
else
{
sb.Append(c);
}
break;
}
}
}
private void AppendDataFields(StringBuilder sb, Dictionary<string, string> data)
{
if (data == null) return;
foreach (var keyValue in data)
{
if (keyValue.Value == null) continue;
if (IsIntFieldName(keyValue.Key))
{
AppendField(sb, keyValue.Key, long.Parse(keyValue.Value));
}
else
{
AppendField(sb, keyValue.Key, keyValue.Value);
}
}
}
private static void AppendField(StringBuilder sb, string key, string value, string separator = ",")
{
if (separator != null)
sb.Append(separator);
sb.Append("\"");
sb.Append(key);
sb.Append("\":\"");
EncodeAndAppendJsString(sb, value);
sb.Append("\"");
}
private static void AppendField(StringBuilder sb, string key, long value, string separator = ",")
{
if (separator != null)
sb.Append(separator);
sb.Append("\"");
sb.Append(key);
sb.Append("\":");
sb.Append(value);
}
private static void AppendField(StringBuilder sb, string key, DateTime value, string separator = ",")
{
if (separator != null)
sb.Append(separator);
sb.Append("\"");
sb.Append(key);
sb.Append("\":\"");
sb.Append(value.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFZ")); //ISO8601
sb.Append("\"");
}
private static void AppendField(StringBuilder sb, string key, TagCollection value, string separator = ",")
{
if (value == null || !value.Any()) return;
if (separator != null)
sb.Append(separator);
sb.Append("\"");
sb.Append(key);
sb.Append("\":\"");
var separator2 = "";
foreach (var tag in value)
{
sb.Append(separator2);
EncodeAndAppendJsString(sb, tag);
separator2 = ",";
}
sb.Append("\"");
}
}
因為原作者並未針對本篇的議題做任何講解,所以這邊的部份都是筆者自己研究出來的,有些是去看他的原始碼而來的,所以如果有任何更好的做法,或者是覺得筆者有任何有誤的論點,再請提醒和告知哦,而存取數據的部份,就請自行決定要怎麼實做囉,存取數據的部份elasticsearch當然會比本篇的方式更加適合囉,總之只要我們能拿到數據,接著就是自行決定想要怎麼實做囉。