[WEB API]如何在上線階段關閉nanoprofiler的監控頁面,如何把數據抓取出來另做紀錄

[WEB API]如何在上線階段關閉nanoprofiler的監控頁面,如何把數據抓取出來另做紀錄

前言

之前有針對nanoprofiler這個監控利器做過較基礎的介紹了,不過還有一些比較進階的議題,可能連github都沒有介紹到,比如說我們該如何在上線階段關掉監控頁面,畢竟我們不可能隨意讓人訪問,而得知我們每個api所執行的時間甚至連sql和參數,還有我們預設是儲存200筆而已,這些數據如果沒保存起來,我們怎麼回溯過往的紀錄呢?這篇就是想要好好來說明這些部份的東西,如果你完全不懂這篇在說明什麼的話,可以參考筆者以前的文章(https://dotblogs.com.tw/kinanson/2017/05/17/073240)

導覽

  1. 實做HttpModule來控制上線階段的訪問權限
  2. 用nlog把nanoprofiler的數據另存進來
  3. 把nanoprofiler的數據解析出來,並存進db
  4. 結論

 

實做HttpModule來控制上線階段的訪問權限

雖然官方並未提供給我們任何可以關閉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才能訪問的方式,請自行發揮想像力囉。

用nlog把nanoprofiler的數據另存起來

nanoprofiler有使用到slf4net這個package,slf4net也有提供了存到nlog或log4net的支援,先打開我們的nuget把相關的package安裝起來吧

安裝完了之後,我們只要在web.config加入下面這段,就會實做存到nlog的部份了

  <slf4net>
    <factory type="slf4net.NLog.NLogLoggerFactory, slf4net.NLog" />
  </slf4net>

但是請注意,你必須得要配置好nlog的info的儲存才能生效哦,至於nlog的config配置部份,筆者就不提供了,再請自行實做囉。

把nanoprofiler的數據解析出來,並存進db

在此部份就比較麻煩點了,因為解析數據的部份其實比較複雜,我目前是隨意設計一個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當然會比本篇的方式更加適合囉,總之只要我們能拿到數據,接著就是自行決定想要怎麼實做囉。