太仓市娄城高新建设有限公司网站,网页制作收费吗,微信开放平台是干什么的,你认为什么对网络营销至关重要回顾在《监控系统简介#xff1a;使用 Prometheus 与 Grafana》一文中#xff0c;我们了解了什么是监控系统#xff0c;Prometheus 这一监控工具及它提供的数据类型、PromQL 以及 Grafana 可视化工具的基本用法。今天这一篇我们将在 ASP.NET Web API 项目中进行实战#xf… 回顾在《监控系统简介使用 Prometheus 与 Grafana》一文中我们了解了什么是监控系统Prometheus 这一监控工具及它提供的数据类型、PromQL 以及 Grafana 可视化工具的基本用法。今天这一篇我们将在 ASP.NET Web API 项目中进行实战将 Web API 接口的请求次数、响应耗时、错误率等指标记录下来并提供给 Prometheus 和 Grafana用于分析和呈现。我们主要采用一个名为 App Metrics 的类库记录指标。App Metrics 是以 Apache v2 协议开源的一款类库支持 .NET Framework 4.5.2 以上以及 .NET Core 的应用程序。除了记录各种程序生成的指标它还提供健康检查的功能但这不在本文的范围内。为什么没有使用 Prometheus 推荐的 .NET 类库主要是因为 App Metrics 在 GitHub 的 star 比较多另外 API 用起来比较顺手而已……本文示例代码已提交至 Github https://github.com/huhubun/AppMetricsPrometheusSample 欢迎一同讨论。在 ASP.NET Web API 中记录指标因为还有一些项目在 .NET Framework 下所以先以 .NET Framework 的 ASP.NET Web API 开始通过 Visual Studio 创建“ASP.NET Web 应用程序.NET Framework”框架版本高于或等于 .NET Framework 4.5.2 即可然后选择 “Web API”。首先通过 nuget将 App Metrics 添加至项目中Install-Package App.Metrics
Install-Package App.Metrics.Formatters.Prometheus
App Metrics 支持各种各样的监控系统或时序数据库。因为我们最终要将数据提供给 Prometheus所以除了 App Metrics 的包外还需要安装一个用于格式化数据的包 App.Metrics.Formatters.Prometheus。由于这是一个新建的项目简单起见这里创建一个名为 ApiMetrics 的类保证 Web API 整个生命周期中只初始化一次 App Metrics。如果项目中有依赖注入容器例如 AutoFac则直接将 IMetricsRoot 注册为单例即可通过 InitAppMetrics() 的代码来创建。public class ApiMetrics
{private static IMetricsRoot _metrics;public static IMetricsRoot GetMetrics(){if (_metrics null){_metrics InitAppMetrics();}return _metrics;}private static IMetricsRoot InitAppMetrics(){var metrics new MetricsBuilder().Configuration.Configure(options {options.DefaultContextLabel API;options.AddAppTag(Assembly.GetExecutingAssembly().GetName().Name);options.AddServerTag(Environment.MachineName);#if DEBUGoptions.AddEnvTag(Dev);
#elseoptions.AddEnvTag(Release);
#endifoptions.GlobalTags.Add(my_custom_tag, MyCustomValue);}).Build();return metrics;}
}
DefaultContextLabel 的值会成为指标的前缀这里设置成 API则默认所有指标都为 api_ 开头AddAppTag() 会为所有指标添加一个名为 app 的 tag内容为当前程序的名称AddServerTag() 会为所有指标添加一个名为 server 的 tag内容是运行程序的机器名称AddEnvTag() 会为所有指标添加一个名为 env 的 tag用于区分运行程序的环境也可以通过 GlobalTags 属性来添加自定义的 tag因为没有依赖注入容器还需要在 Global.asax 的 Application_Start() 中手动调用一下 GetMetrics() 方法以完成初始化。protected void Application_Start()
{// 省略其他内容ApiMetrics.GetMetrics();
}
记录程序启动时间我们把程序启动的时间作为一项指标在 Grafana 中就能显示出程序已经运行了多长时间。Prometheus 通过 time() 能得到当前时间的 unix 时间戳所以我们只需要将程序启动时的时间以 unix 时间戳的方式记录下来即可。在 Application_Start() 中当一切准备就绪后通过 App Metrics 创建一个 Gauge var metrics ApiMetrics.GetMetrics(); // 如果有依赖注入容器请替换为注入 IMetricsRoot 的代码var unixTimestamp DateTimeOffset.UtcNow.ToUnixTimeSeconds();metrics.Measure.Gauge.SetValue(new GaugeOptions{Name Boot Time Seconds}, unixTimestamp);
通过 App Metrics 的 Measure 属性可以找到 Gauge 属性然后通过 SetValue() 方法即可记录指标。指标的各种设置例如名称通过参数传入。指标名称 Name 我习惯按可读性高的方式来写因为 App Metrics 的 Prometheus 格式化器会自动帮我们处理它后文会说明。另外虽然我们创建的是 Gauge但对于启动时间而言除了这时的赋值外这个指标的值是不会改变的。添加 /metrics 终结点现在我们已经有一个内容为程序启动时间的指标了还缺少一个能让 Prometheus 抓取指标数据的地方。因为这是一个 Web API 项目很简单来创建一个 Web API 控制器 MetricsController [RoutePrefix(metrics)]public class MetricsController : ApiController{[HttpGet][Route()]public async TaskHttpResponseMessage GetMetricsAsync(){var formatter new App.Metrics.Formatters.Prometheus.MetricsPrometheusTextOutputFormatter();var snapshot ApiMetrics.GetMetrics().Snapshot.Get();using (var ms new MemoryStream()){await formatter.WriteAsync(ms, snapshot);var result Encoding.UTF8.GetString(ms.ToArray());var response Request.CreateResponse(HttpStatusCode.OK);response.Content new StringContent(result, Encoding.UTF8, formatter.MediaType.ContentType);return response;}}}
现在启动程序访问 localhost:端口/metrics 就能看到类似这样的效果# HELP api_boot_time_seconds
# TYPE api_boot_time_seconds gauge
api_boot_time_seconds{appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustonValue} 1580913792
App Metrics 的指标类型及转换由于 App Metrics 的指标类型与 Prometheus 的并不是一一对应的我们先看看 App Metrics 中提供的类型有哪些Apdex 应用性能指数评分它的含义可以参考 《应用性能指标apdex》 https://www.cnblogs.com/tetu/p/4968666.htmlCounter 计数器Gauge gaugeHistogram 直方图Meter 一个可增减的计数器一般用于统计次数和速率Timer 计时器根据统计的时间自动进行分组可以看到Apdex、Meter 和 Timer 是 Prometheus 中没有的。通过 App.Metrics.Formatters.Prometheus 可以转换成 Prometheus 的指标Apdex - GaugeCounter - CounterGauge - GaugeHistogram - HistogramMeter - Counter用起来和 Counter 好像也没什么区别…Timer - Summary会自动帮我们计算好 0.5、0.75、0.95、0.99 的分位数还需要提到的是通过 App Metrics Prometheus 格式化器指标的名称也会发生变化指标名称 Boot Time Seconds 会被转换为 api_boot_time_seconds空格会自动变为下划线大写也会被转为小写。所以代码中可以按习惯的方式编写只要统一即可。App Metrics 的 API在 IMetricsRoot 下我们常用的有这两个属性MeasureProvider通过 Measure 和 Provider 属性都可以访问到所有的指标类型仔细观察可以发现 通过 Measure 操作指标方法返回的都是 XXXContext 或者 void而 Provider 返回的都是 IXXX来看看方法的定义void IMetricsRoot.Measure.Counter.Increment(CounterOptions options, long amount)只能通过参数列表直接传入值ICounter IMetricsRoot.Provider.Counter.Instance(CounterOptions options)可以对该计数器执行 Increment() 增加值、Decrement() 减少值、Reset() 重置等操作当然Prometheus 的计数器应该是只增不减的但因为 App Metrics 并不是专为 Prometheus 设计所以它的 API 可以这样操作也是可以理解的总的来说区别在于 Measure 中的 API 相当于去测量某些指标而 Provider 的 API 可以直接为指标赋值。通过 Timer 来看更为明显void IMetricsRoot.Measure.Timer.Time(TimerOptions options, Action action) 要求将要统计时间的操作直接在 Action 中执行这个 API 会自动开始计时当 Action 执行完毕后停止计时TimerContext IMetricsRoot.Measure.Timer.Time(TimerOptions options) 当创建 TimerContext 后开始计时通过 TimerContext 提供的 Dispose() 方法来停止计时ITimer IMetricsRoot.Provider.Timer.Instance(TimerOptions options) 通过 Record() 直接设置时间另外也有 StartRecording()、EndRecording() 等方法手动开始和停止计时记录 API 响应耗时和请求次数在 Web API 中可以通过消息处理程序在请求进入控制器之前以及响应被生成后执行一些操作。我们可以通过一个计时器在收到请求时计时处理完请求后停止计时的方式统计一次 HTTP 请求所需要的时间。确定计时的方案后需要确定维度。对于 API 的响应耗时我们应该关注 API 的请求方式GET、POST、PUT、DELETE等、API 的路由/api/values、/api/values/{id}等、响应状态码这些信息。所以需要在指标中体现出这几个标签。最后确认使用何种数据类型。App Metrics 提供了 Timer 类型能自动生成 0.5、0.99 等分位数并且转换为 Prometheus 后它是 summary 类型意味着还会产生 XXX_sum 和 XXX_count 两个指标。通过 XXX_count 我们顺便还能把请求次数给计算出来。新建一个 MetricsHandler 类代码如下 public class MetricsHandler : DelegatingHandler{private const string API_METRICS_RESPONSE_TIME_KEY __ApiMetrics.ResponseTime__;private const string API_METRICS_ROUTE metrics;protected async override TaskHttpResponseMessage SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){var routeTemplate GetRouteTemplate(request);// 如果访问的是 /metrics 则不计入统计中if (routeTemplate API_METRICS_ROUTE){return await base.SendAsync(request, cancellationToken);}StartRecordingResponseTime(request);var response await base.SendAsync(request, cancellationToken);EndRecordingResponseTime(routeTemplate, request, response);return response;}private string GetRouteTemplate(HttpRequestMessage request){// MS_SubRoutes 适用于 Route Attribute 的情况request.GetRouteData().Values.TryGetValue(MS_SubRoutes, out var routes);return (routes as System.Web.Http.Routing.IHttpRouteData[])?.FirstOrDefault()?.Route?.RouteTemplate ?? unknown;}#region Response Time/// summary/// 开始记录响应时间/// /summary/// param namerequest/param/// param namerouteTemplate/paramprivate void StartRecordingResponseTime(HttpRequestMessage request){var stopwatch new Stopwatch();stopwatch.Start();request.Properties.Add(API_METRICS_RESPONSE_TIME_KEY, stopwatch);}/// summary/// 停止记录响应时间/// /summary/// param nameresponse/paramprivate void EndRecordingResponseTime(string routeTemplate, HttpRequestMessage request, HttpResponseMessage response){var stopwatch response.RequestMessage.Properties[API_METRICS_RESPONSE_TIME_KEY] as Stopwatch;ApiMetrics.GetMetrics().Provider.Timer.Instance(new TimerOptions{Name Response Time,Tags new MetricTags(new string[] { method, route, status },new string[] { request.Method.Method, routeTemplate, ((int)response.StatusCode).ToString() }),DurationUnit TimeUnit.Milliseconds,RateUnit TimeUnit.Milliseconds,MeasurementUnit Unit.Requests}).Record(stopwatch.ElapsedMilliseconds, TimeUnit.Milliseconds);response.RequestMessage.Properties.Remove(API_METRICS_RESPONSE_TIME_KEY);}#endregion}
MetricsHandler 的原理是请求进入后首先触发 StartRecordingResponseTime() 方法该方法创建了一个 Stopwatch 并开始计时同时将 Stopwatch 储存在当前请求的缓存中等待 await base.SendAsync() 完成这会执行其它的 Handler、Filter 以及 Action 中的内容这里执行完成意味着所有的操作都已经完成并且响应体也已经生成触发 EndRecordingResponseTime() 停止计时并将记录的时间直接储存到 App Metrics 的 Timer 类型的 Response Time 指标中需要注意的是GetRouteTemplate() 方法通过 MS_SubRoutes 获取路由的方式仅适用于使用特性路由的方式根据需要可以使用不同的获取路由的方式。为了使 MetricsHandler 能正常工作首先修改默认生成的 ValuesController将其修改为使用特性路由的方式注册路由 [RoutePrefix(api/values)]public class ValuesController : ApiController{// GET api/values[HttpGet, Route()]public IEnumerablestring Get(){return new string[] { value1, value2 };}// GET api/values/5[HttpGet, Route({id:int})]public string Get([FromUri]int id){return value id;}// POST api/values[HttpPost, Route()]public void Post([FromBody]string value){}// PUT api/values/5[HttpPut, Route({id:int})]public void Put([FromUri]int id, [FromBody]string value){}// DELETE api/values/5[HttpDelete, Route({id:int})]public void Delete([FromUri]int id){}}
接着修改 WebApiConfig 的 Register() 将 config.Routes.MapHttpRoute() 路由模板注释掉然后注册 MetricsHandler。现在 Register() 看起来类似这样 public static void Register(HttpConfiguration config){config.MapHttpAttributeRoutes();// 注释掉这部分代码//config.Routes.MapHttpRoute(// name: DefaultApi,// routeTemplate: api/{controller}/{id},// defaults: new { id RouteParameter.Optional }//);// Metrics Handlerconfig.MessageHandlers.Add(new MetricsHandler());}
完成后我们启动程序先通过浏览器或者 Postman 随意访问几个接口例如 localhost:端口/api/values 之后再访问 /metrics就能看到我们新增的 api_response_time 指标了# HELP api_response_time
# TYPE api_response_time summary
api_response_time_sum{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue} 0.158
api_response_time_count{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue} 1
api_response_time{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue,quantile0.5} 0.158
api_response_time{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue,quantile0.75} 0.158
api_response_time{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue,quantile0.95} 0.158
api_response_time{methodGET,routeapi/values,status200,appWebAPISample,serverBUNPC,envDev,my_custom_tagMyCustomValue,quantile0.99} 0.158
虽然我们的例子是基于 .NET Framework 的但其实对于 .NET Core 而言也是类似。App Metrics 的 API 是一致的 MetricsHandler 由 Middleware 实现即可这里就不展开说了。通过 Prometheus 分析Prometheus 的配置参考上一篇文章这里直接通过 PromQL 来查询默认地址为 http://localhost:9090/ 打开 Graph 页面。计算每个接口总请求数量因为 api_response_time_count 中包含响应状态同一个 method 和 route 有时可能返回 200有时可能返回 400所以我们需要根据 method 和 route 进行分组再求和sum by (method, route)(api_response_time_count)
还可以统计1分钟内的错误率我们对“错误”的定义为所有非 2XX 的响应所以非 2 开头的 status 都属于错误sum(rate(api_response_time_count{status!~2.*}[1m]))
请注意一定要先 rate() 再 sum()参考文章 Rate then sum, never sum then rate https://www.robustperception.io/rate-then-sum-never-sum-then-rate统计每个接口 95% 情况下的响应时间api_response_time{quantile0.95}
与 Grafana 图表结合的例子可以参考本文 demo 的 https://github.com/huhubun/AppMetricsPrometheusSample链接App Metrics 官方网站 https://www.app-metrics.io/