原文地址:http://www.dotnetcurry.com/aspnet/1354/elastic-search-kibana-in-docker-dotnet-core-appjavascript
想要輕鬆地經過許多不一樣的方式查詢數據,甚至是從未預料到的方式?想要以多種方式可視化日誌?同時支持基於時間、文本和其餘類型的即時過濾器?
藉助於 Elastic stack 的卓越性能和可擴展方式的優勢,咱們將經過兩個示例輕鬆實現。html
本文由 DNC Magazine for Developers and Architects 發佈。 從這裏下載此雜誌[PDF] 或 免費訂閱本雜誌 下載全部之前和當前的版本版本。java
在這篇文章中,我將介紹流行的搜索引擎 Elasticsearch,其配套的可視化應用 Kibana,並展現如何對.NET核心能夠輕鬆地與 Elastic stack 整合在一塊。git
咱們將開始探索 Elasticsearch 的 REST API ,經過索引和查詢某些數據。接着,咱們將使用Elasticsearch官方的 .Net API 完成相似的練習。一旦熟悉 Elasticsearch 及其 API 後,咱們將使用 .Net Core 建立一個日誌模塊,並將數據發送到 Elasticsearch 。Kibana緊隨其中,以有趣的方式可視化 Elasticsearch 的索引數據。
我迫切但願你會認爲這篇文章十分有趣,而且想要了解更多關於Elastic的強大之處。github
本文假設您已經瞭解 C#和 REST API 的基本知識。使用 Visual Studio,Postman 和 Docker 等工具,但您能夠輕鬆使用 VS Code 和 Fiddler 等替代方案。docker
Elasticsearch 做爲核心的部分,是一個具備強大索引功能的文檔存儲庫,而且能夠經過 REST API 來搜索數據。它使用 Java 編寫,基於 Apache Lucene,儘管這些細節隱藏在 API 中。
經過被索引的字段,能夠用許多不一樣的聚合方式找到任何被存儲(索引)的文檔。
可是,ElasticSearch不只僅只提供對這些被索引文檔的強大搜索功能。
快速、分佈式、水平擴展,支持實時文檔存儲和分析,支持數百臺服務器和 PB 級索引數據。同時做爲 Elastic stack (aka ELK) 的核心,提供了諸如 LogStash、Kibana 和更多的強大應用。
Kibana 是 Elasticsearch 中專門提供強有力的可視化查詢Web應用程序。使用Kibana,能很是簡單地爲 Elasticsearch 中索引的數據建立查詢、圖表和儀表盤。
Elasticsearch開放了一個 REST API,你會發現許多文檔示例是 HTTP 調用,你能夠嘗試使用 curl 或 postman 等工具。固然,這個 API 的客戶端已經用許多不一樣的語言編寫,包括.Net、Java、Python、Ruby和JavaScript等。
若是你想閱讀更多,Elasticsearch 官方網站 多是最好的地方。數據庫
在這篇文章中,咱們須要先鏈接到一個 Elasticsearch (和後面的Kibana)的服務器。若是您已經有一個在本地運行或可使用的服務器,那很好。不然須要先搭建一個服務器。
您能夠選擇在您的本地機器或可使用的 VM 或服務器中下載和安裝 Elasticsearch 和 Kibana 。不過,建議您使用最簡單最純粹的方式,使用Docker 搭建 Elasticsearch 和 Kibana 。
您能夠直接運行如下命令,獲取包含Elasticsearch和Kibana的容器。apache
docker run -it --rm -p 9200:9200 -p 5601:5601 --name esk nshou/elasticsearch-kibana
在同一個容器中運行多個應用程序,就像咱們如今這種作法,很是適用本文,但不是推薦用於生產容器!windows
您應該意識到,一旦你刪除容器,你的數據就會消失(一旦你使用-rm選項就刪除它了)。雖然有利於本地實驗,但在實際環境中,若是您不想丟失數據,請參照 "data container" 模式。api
Docker是一個很棒的工具,我鼓勵你更多地瞭解它,特別是若是你想作更重要的事情,而不只僅是跟隨本文,在本地快速搭建 Elasticsearch 服務器。在以前的文章 Building DockNetFiddle using Docker and .NET Core 中已經對 .NET Core 搭配 Docker 有很好的介紹。
只需打開 http://localhost:9200 和 http://localhost:5600 ,檢查Elasticsearch 和 Kibana 是否均可以使用。(若是您使用docker toolbox,請使用託管Docker的虛擬機ip替換localhost,您能夠在命令行中運行 docker-machine env default )。
在咱們開始編寫任何 .Net 代碼以前,咱們先了解一下一些基本知識。先在 Elasticsearch 索引一些文檔(相似於存到數據庫),以便咱們對它們運行不一樣的查詢。
在這裏,我將使用Postman向咱們的 Elasticsearch 服務器發送 HTTP 請求,但您可使用任何其餘相似的工具,如 Fiddler或 curl 。
咱們要作的第一件事是請求 Elasticsearch 建立一個新的索引 (譯者語:相似建立一個表) 並索引一些文檔 (譯者語:相似於在數據中插入數據) 。這相似於將數據存儲在表/集合中,主要區別(和目的)是讓 Elasticsearch 集羣 (這裏只是一個節點) 能夠分析和搜索文檔數據。
被索引的文檔在 Elasticsearch 中以索引和類型進行組織。以往,被拿來和數據庫表作對比,每每會使人困惑。如這篇文章所述,索引由Lucene處理,在分佈式跨 分片 中,與類型緊密地聯繫在一塊兒。
發送如下兩個請求以建立索引,並在該索引中插入文檔 (請記住 toolbox,若是使用docker ,請使用託管Docker的虛擬機ip而不是localhost) :
PUT localhost:9200/default
PUT localhost:9200/default/product/1 { "name": "Apple MacBook Pro", "description": "Latest MacBook Pro 13", "tags": ["laptops", "mac"] }
在咱們驗證搜索功能和查詢數據以前,再索引幾個 "product"。嘗試使用不一樣的 "tags",如 "laptops"和 "laptops",並記得使用不一樣的ids!
完成後,讓咱們按名稱排序的搜索全部被索引的文檔。您可使用查詢字符串或 GET/POST 一樣的內容,下面兩個請求是等效的:
GET http://localhost:9200/default/_search?q=*&sort=name.keyword:asc
POST http://localhost:9200/default/_search { "query": { "match_all": {} }, "sort": [ { "name.keyword": "asc" } ] }
讓咱們嘗試一些更有趣的東西,例如搜索 "description" 字段中含有 "latest" ,同時 "tags" 字段中含有 "laptops" 的全部文檔:
POST http://localhost:9200/default/_search { "query": { "bool": { "must": [ { "match": {"description": "latest"} }, { "match": { "tags": "laptops" } } ] } }, "sort": [ { "name.keyword": "asc" } ] }
做爲介紹的最後部分,咱們將對 Kibana 的相關知識走馬觀花。
假設您在上一步已經索引了幾個文檔,經過訪問 http://localhost:5601 中打開在 Docker 的 Kibana 服務器。你會注意到,Kibana 要求你提供默認的索引模式,因此必須告訴它使用的 Elasticsearch 索引:
完成後,使用左側菜單打開 " 發現 (Discover) " 頁面,您應該會看到上一節中插入的全部最新文檔。嘗試選擇不一樣的字段,在搜索欄中輸入相關的字段或某個過濾器:
最後,咱們建立一個餅圖,顯示 "laptops" 或 "desktops" 的銷量百分比。利用以前索引的數據,在左側菜單新建一個 "餅圖 (Pie Chart)" 。
您能夠在 餅圖 (Pie Chart)的頁面上配置。將 " Count " 做爲切片的大小,並在 " buckets " 部分中選擇 " split slices " 。將 " filters " 做爲聚合類型,添加兩個過濾器:tags ="laptop" 和 tags ="desktoptops" 。單擊運行,您將看到相似於下圖:
確保在搜索欄中輸入包含已過濾的項目的搜索關鍵詞,並注意到可視化圖形如何變化。
在簡要介紹Elasticsearch和Kibana以後,咱們來看看咱們如何用 .Net 應用程序索引和查詢咱們的文檔。
您可能想知道爲何要這樣作,而不是直接使用 HTTP API 。我能夠提供幾個理由,我相信你能夠本身找幾個:
首先須要注意的是打開 這個文檔 ,有兩個官方提供的 APIs : Elasticsearch.Net 和 NEST ,都支持 .Net Core 項目。
因爲我使用的是 NEST,因此第一步是建立一個新的 ASP .Net Core 應用程序,並使用 Package Manager 安裝NEST。
咱們將在新的 ASP.Net Core 應用程序中完成以前手動發送 HTTP 請求的一些步驟。若是須要,請從新啓Docker 容器,從而清理數據;或經過 HTTP API 和 Postman 手動刪除文檔/索引。
咱們首先爲產品建立一個POCO模型:
public class Product { public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public string[] Tags { get; set; } }
接下來,咱們建立一個新的控制器 ProductController,它具備添加新的 "Product" 的方法和基於單個關鍵詞查找 "Product" 的方法:
[Route("api/[controller]")] public class ProductController : Controller { [HttpPost] public async Task< IActionResult > Create([FromBody]Product product) { } [HttpGet("find")] public async Task< IActionResult > Find(string term) { } }
爲了實現這些方法,咱們須要先鏈接到 Elasticsearch。這裏有一個 ElasticClient 鏈接的正確示範。 因爲該類是線程安全的,因此推薦的方法是在應用程序中使用單例模式,而不是按請求建立新的鏈接。
爲了簡潔起見,我如今將使用帶有硬編碼設置的私有靜態變量。在 .Net Core 中使用依賴注入配置框架,或查看 Github中 的代碼。
能夠想到的是,至少須要提供被鏈接的 Elasticsearch 集羣的URL。固然,還有其餘可選參數,用於與您的羣集進行身份驗證、設置超時、鏈接池等。
private static readonly ConnectionSettings connSettings = new ConnectionSettings(new Uri("http://localhost:9200/")); private static readonly ElasticClient elasticClient = new ElasticClient(connSettings);
創建鏈接後,索引文檔只是簡單地使用 ElasticClient 的Index/IndexAsync 方法:
[Route("api/[controller]")] public class ProductController : Controller { [HttpPost] public async Task<IActionResult> Create([FromBody]Product product) { } [HttpGet("find")] public async Task<IActionResult> Find(string term) { } }
很簡單,對吧?不幸的是,若是您向Postman發送如下請求,您將看到失敗。
POST http://localhost:65113/api/product { "name": "Dell XPS 13", "description": "Latest Dell XPS 13", "tags": ["laptops", "windows"] }
這是由於NEST沒法肯定在索引文檔時使用哪一個索引!若是您想起手動使用 HTTP API 的作法,那麼須要在URL指出文檔的索引、文檔的類型和ID,如 localhost:9200/default/product/1
NEST可以推斷文檔的類型(使用類的名稱),還能夠默認對字段進行索引(基於字段的類型),但須要一些索引名稱的幫助。您能夠指定默認的索引名稱,以及特定類型的特定索引名稱。
connSettings = new ConnectionSettings(new Uri("http://192.168.99.100:9200/")) .DefaultIndex("default") //Optionally override the default index for specific types .MapDefaultTypeIndices(m => m .Add(typeof(Product), "default"));
進行這些更改後再試一次。您將看到 NEST 建立索引(若是還沒有存在),並將文檔編入索引。若是你切換到 Kibana,你也能夠看到該文檔。須要注意的是:
在咱們查詢數據以前,從新考慮建立索引的方式。
如何建立索引?
如今咱們獲得一個事實,即若是這個索引不存在,也會被建立。然而映射字段的索引方式很重要,並直接定義了 Elasticsearch 如何索引和分析這些字段。這對於字符串字段尤爲明顯,由於在 Elasticsearch v5 中提供了兩種不一樣字段類型的 "Text" 和 "Keyword":
您可使用 NEST 索引映射屬性來生成POCO模型:
public class Product { public Guid Id { get; set; } [Text(Name="name")] public string Name { get; set; } [Text(Name = "description")] public string Description { get; set; } [Keyword(Name = "tag")] public string[] Tags { get; set; } }
然而,咱們須要先建立索引,必須使用 ElasticClient API 手動建立和定義索引的映射。這是很是簡單的,特別是若是咱們只是使用屬性:
if (!elasticClient.IndexExists("default").Exists) { elasticClient.CreateIndex("default", i => i .Mappings(m => m .Map<Product>(ms => ms.AutoMap()))); }
直接向Elasticsearch發送請求(GET localhost:92000/default),並注意與咱們想要的映射是否相同。
如今,咱們有一個使用 NEST 對 "products" 進行索引的 ProductController 控制器。是時候,爲這個控制器添加 Find action,用於使用 NEST 向 Elasticsearch 查詢文檔。
咱們只是用到一個字段來實現一個簡單的搜索。您應該觀察全部字段:
NEST 提供了一個查詢 Elasticsearch 的豐富 API,能夠轉換成標準的 HTTP API 。實現上述查詢類型與使用Search/SearchAsync方法同樣簡單,並構建一個 SimpleQueryString 做爲參數。
[HttpGet("find")] public async Task<IActionResult> Find(string term) { var res = await elasticClient.SearchAsync<Product>(x => x .Query( q => q. SimpleQueryString(qs => qs.Query(term)))); if (!res.IsValid) { throw new InvalidOperationException(res.DebugInformation); } return Json(res.Documents); }
使用PostMan測試您的新操做:
正如您可能已經意識到的那樣,咱們的操做行爲與手動發送請求到 Elasticsearch 同樣:
GET http://localhost:9200/default/_search?q=*&
如今咱們瞭解了 NEST 的一些基礎知識,讓咱們嘗試一些更有野心的事情。咱們已經建立了一個 ASP.Net Core 的應用程序,藉助.NET Core的日誌框架,實現咱們的日誌提供程序,並將信息發送到Elasticsearch。
新的日誌 API 在日誌 (logger) 和日誌提供程序 (logger provider) 方面的區別:
該日誌框架內置了一些對事件日誌、Azure 等的日誌提供程序 (provider),但正如您將看到的,建立本身的並不複雜。有關詳細信息,請查閱.NET Core 關於日誌的官方文檔。
在本文的最後部分,咱們將爲Elasticsearch建立一個新的日誌提供程序,在咱們的應用程序中啓用它,並使用Kibana來查看記錄的事件。
首先要作的是定義一個新的POCO對象,咱們將使用它做爲使用NEST進行索引的文檔,相似於以前建立的 "Product" 類。
這將包含有關可能發生的任何異常以及相關請求數據的記錄信息、可選信息。記錄請求數據將會派上用場,由於咱們能夠根據具體請求查詢/可視化咱們記錄的事件。
public class LogEntry { public DateTime DateTime { get; set; } public EventId EventId { get; set; } [Keyword] [JsonConverter(typeof(StringEnumConverter))] public Microsoft.Extensions.Logging.LogLevel Level { get; set; } [Keyword] public string Category { get; set; } public string Message { get; set; } [Keyword] public string TraceIdentifier { get; set; } [Keyword] public string UserName { get; set; } [Keyword] public string ContentType { get; set; } [Keyword] public string Host { get; set; } [Keyword] public string Method { get; set; } [Keyword] public string Protocol { get; set; } [Keyword] public string Scheme { get; set; } public string Path { get; set; } public string PathBase { get; set; } public string QueryString { get; set; } public long? ContentLength { get; set; } public bool IsHttps { get; set; } public IRequestCookieCollection Cookies { get; set; } public IHeaderDictionary Headers { get; set; } [Keyword] public string ExceptionType { get; set; } public string ExceptionMessage { get; set; } public string Exception { get; set; } public bool HasException { get { return Exception != null; } } public string StackTrace { get; set; } }
下一步是在一個新類上實現ILogger接口。如您所想,這將須要記錄的數據映射到一個新的 LogEntry 對象,並使用 ElasticClient 對其進行索引。
在這裏就不寫鏈接到Elasticsearch並建立索引的代碼,這與以前的操做,沒有什麼不一樣。使用不一樣的索引或刪除上一節中索引的 "products" 。
注意: 您可使用依賴注入和配置文檢查 Github 中 的配套代碼。
實現的主要方法是Log <TState>,這是咱們建立一個LogEntry並用NEST進行索引:
public void Log< TState >(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func< TState, Exception, string > formatter) { if (!IsEnabled(logLevel)) return; var message = formatter(state, exception); var entry = new LogEntry { EventId = eventId, DateTime = DateTime.UtcNow, Category = _categoryName, Message = message, Level = logLevel }; var context = _httpContextAccessor.HttpContext; if (context != null) { entry.TraceIdentifier = context.TraceIdentifier; entry.UserName = context.User.Identity.Name; var request = context.Request; entry.ContentLength = request.ContentLength; entry.ContentType = request.ContentType; entry.Host = request.Host.Value; entry.IsHttps = request.IsHttps; entry.Method = request.Method; entry.Path = request.Path; entry.PathBase = request.PathBase; entry.Protocol = request.Protocol; entry.QueryString = request.QueryString.Value; entry.Scheme = request.Scheme; entry.Cookies = request.Cookies; entry.Headers = request.Headers; } if (exception != null) { entry.Exception = exception.ToString(); entry.ExceptionMessage = exception.Message; entry.ExceptionType = exception.GetType().Name; entry.StackTrace = exception.StackTrace; } elasticClient.Client.Index(entry); }
您還須要額外實現 BeginScope 和 IsEnabled 方法。
你可能會問爲何須要分類?這是一個用於標識日誌是哪一種類型的字符串。默認狀況下,每次注入ILogger <T>的實例時,該類別默認分配爲T的分類名稱。例如,獲取ILogger <MyController>並使用它來記錄某些事件,意味着這些事件將具備 "MyController " 名稱。
這可能派上用場,例如爲不一樣的類設置不一樣的日誌級別,以過濾/查詢記錄的事件。我相信您可能還想到更多的用法。
這個類的實現將以下所示:
public class ESLoggerProvider: ILoggerProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly FilterLoggerSettings _filter; public ESLoggerProvider(IServiceProvider serviceProvider, FilterLoggerSettings filter = null) { _httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>(); _filter = filter ?? new FilterLoggerSettings { {"*", LogLevel.Warning} }; } public ILogger CreateLogger(string categoryName) { return new ESLogger(_httpContextAccessor, categoryName, FindLevel(categoryName)); } private LogLevel FindLevel(string categoryName) { var def = LogLevel.Warning; foreach (var s in _filter.Switches) { if (categoryName.Contains(s.Key)) return s.Value; if (s.Key == "*") def = s.Value; } return def; } public void Dispose() { } }
最後,咱們建立一個擴展方法,能夠用於在啓動類中註冊咱們的日誌提供程序:
public static class LoggerExtensions { public static ILoggerFactory AddESLogger(this ILoggerFactory factory, IServiceProvider serviceProvider, FilterLoggerSettings filter = null) { factory.AddProvider(new ESLoggerProvider(serviceProvider, filter)); return factory; } } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")) .AddDebug() .AddESLogger(app.ApplicationServices, new FilterLoggerSettings { {"*", LogLevel.Information} }); … }
請注意我如何覆蓋默認設置並按日誌級別分類記錄。這樣,咱們能夠輕鬆地爲每一個請求索引一些事件。
如今咱們已經在Kibana中記錄了事件,咱們來探索數據可視化吧!
首先,在Kibana中重建索引,此次確保選擇" Index contains time-based events ( Index包含基於時間的事件 )",選擇字段dateTime做爲"Time-field name (時間字段名稱)"。
接下來,啓動您的應用程序,瀏覽一些頁面以獲取一些事件日誌。還能夠在某個端點隨意添加拋出異常的代碼,以便咱們能夠看到被記錄的異常數據。
在這以後,請轉到Kibana的 發現 (Discover) 頁面,您能夠看到由 "dateTime" 字段排序的多個事件(默認狀況下,數據被過濾爲最近15分鐘,但您能夠在右上角更改):
試着在搜索欄中輸入 "exception",並注意任何一個被分析的文本字段中包含 "exception" 的事件。而後嘗試搜索特定的異常類型(記住咱們使用了一個關鍵字字段!)。
您還能夠嘗試搜索特定的URL,如以 " /Home/About "和" /Home/About" 路徑的兩種搜索方式 。您會注意到第一種狀況包括引用者是 "/Home/About" 的事件,而第二種狀況則只能正確返回路徑爲 "/Home/About" 的事件。
一旦你熟悉了數據,以及如何查詢數據,那麼能夠用數據建立一些有趣的圖形。
首先,咱們將建立一個圖表,顯示每分鐘記錄的異常數。
一個很棒的圖表,顯示每分鐘記錄的異常數目:
接下來,顯示每一個 category 隨時間記錄的消息數量,限於前5個 category :
這將繪製相似於如下的圖表:
嘗試在搜索框中添加一些過濾器,並查看它對結果的影響。
最後,咱們添加另外一個圖表,咱們將看到前五個出現最多的消息和前五個 categories 的消息。
花時間觀察下數據(百分比,message/category 顯示在圖表元素上)。例如,您將觀察到由
DeveloperExceptionPageMiddleware類記錄的異常。
Elasticsearch是一個強大的數據索引和查詢平臺。雖然它自己至關使人印象深入,但與其餘應用程序(如Kibana)相結合,能夠很好地分析、報告和可視化數據。只要您開始使用,只是走馬觀花都能的到非凡的結果。
對於 .Net 和 .Net Core,Elasticsearch 官方的 API 已經覆蓋,由於它們支持 .Net Standard 1.3和更高版本(他們仍然在爲1.1提供支持)。
正如咱們已經看到的,在 ASP.Net Core 項目中使用這個 API 是很方便的,咱們能夠輕鬆地將其 REST API 做爲存儲,以及在應用程序中做爲日誌提供程序。
最後但並不是不重要的一點,我但願您使用Docker。嘗試使用 Elasticsearch,同時思考Docker能夠爲您和您的團隊作些什麼。
下載本文的所有源代碼(Github)。