利用AppMetrics對Web進行監控教程
一、基礎准備
1. 安裝依賴
這里可以通過nuget或使用命令行進行安裝,具體需要安裝的類庫如下(注意版本):
Install-Package App.Metrics.AspNetCore.Mvc -Version 2.0.0
由於我們需要兼容Prometheus進行監控,所以我們還需要安裝對應的格式化庫,具體如下:
Install-Package App.Metrics.Formatters.Prometheus -Version 2.0.0
以上就是需要的類庫,接下來我們開始進行其他初始化部分。
2. 初始配置
為了保證其能夠正常工作,我們需要根據不同的環境設定對應的appsettings.json
文件從而讓度量指標可以根據不同的環境進行輸出,這里考慮到實際情況尚未存在不同的配置可能性故統一配置即可,打開appsettings.json
輸入下配置項:
{
"MetricsOptions": {
"DefaultContextLabel": "MetricsApplication",
"Enabled": true
},
"MetricsWebTrackingOptions": {
"ApdexTrackingEnabled": true,
"ApdexTSeconds": 0.3,
"IgnoredHttpStatusCodes": [ 404 ],
"IgnoreRoutesRegexPatterns": [],
"OAuth2TrackingEnabled": false
},
"MetricEndpointsOptions": {
"MetricsEndpointEnabled": true,
"MetricsTextEndpointEnabled": true,
"EnvironmentInfoEndpointEnabled": true
}
}
參數DefaultContextLabel
可以設定為我們期望其他名稱,這里建議采用項目的簡寫名稱,保證項目之間不存在沖突即可。參數ApdexTSeconds
用於設定應用的響應能力標准,其采用了當前流行的Apdex標准,這里使用者可以根據自身應用的實際情況調整對應的參數,其他相關參數建議默認即可。
3. 啟用度量指標
因為我們的數據需要符合Promethues格式,所以后續教程我們會替換默認的格式采用符合的格式。首先我們需要Program.cs
里輸入以下內容:
public static IWebHost BuildWebHost(string[] args)
{
Metrics = AppMetrics.CreateDefaultBuilder()
.OutputMetrics.AsPrometheusPlainText()
.OutputMetrics.AsPrometheusProtobuf()
.Build();
return WebHost.CreateDefaultBuilder(args)
.ConfigureMetrics(Metrics)
.UseMetrics(options =>
{
options.EndpointOptions = endpointsOptions =>
{
endpointsOptions.MetricsTextEndpointOutputFormatter = Metrics.OutputMetricsFormatters.OfType<MetricsPrometheusTextOutputFormatter>().First();
endpointsOptions.MetricsEndpointOutputFormatter = Metrics.OutputMetricsFormatters.OfType<MetricsPrometheusProtobufOutputFormatter>().First();
};
})
.UseStartup<Startup>()
.Build();
}
其中為了能夠支持其他格式,我們需要手動實例化Metrics
對象完成相關初始化然后將其注入到asp.net core中,其中相關格式的代碼主要是由以下這幾部分組成:
OutputMetrics.AsPrometheusPlainText()
OutputMetrics.AsPrometheusProtobuf()
endpointsOptions.MetricsTextEndpointOutputFormatter = Metrics.OutputMetricsFormatters.OfType<MetricsPrometheusTextOutputFormatter>().First();
endpointsOptions.MetricsEndpointOutputFormatter = Metrics.OutputMetricsFormatters.OfType<MetricsPrometheusProtobufOutputFormatter>().First();
完成以上操作后,我們最后還需要進行其他配置,打開Startup.cs
文件增加如下內容:
services.AddMvc().AddMetrics();
至此我們就完成了基本的初始化了,通過啟動程序並訪問localhost:5000/metrics-text
即可查看最終的輸出內容。
二、自定義指標
由於其內部已經默認提供了若干的指標,但是並不能符合實際業務的需求故以下將對常用的度量指標類型以及用法進行介紹,這里這里大家通過注入IMetrics
接口對象即可訪問,所以下面這部分代碼不在闡述。
1. 儀表盤(Gauge)
最常見的類型,主要是用於直接反應當前的指標情況,比如我們常見的CPU和內存基本都是使用這種方式進行顯示的,可以直觀的看到當前的實際的狀態情況。對於所有的指標我們都需要定義對應的Options,當然這可以完成攜程靜態變量供應用程序全局使用。
比如下面我們定義一個表示當前發生錯誤次數的指標:
GaugeOptions Errors = new GaugeOptions()
{
Name = "Errors"
};
完成指標的定義后,我們就可以在需要使用的地方進行指標數據的修改,比如下面我們將錯誤數量設置為10:
metrics.Measure.Gauge.SetValue(MyMetricsRegistry.Errors, 10);
這樣我們就完成了指標的設定,但是有時候我們還想卻分具體的Error是那個層面發起的,這個時候我們需要使用到Tag了,下面我們在設定值的同時設定指標,當然也可以在新建指標的時候通過Tags
變量,並且通用於其他所有指標:
var tags = new MetricTags("fromt", "db");
metrics.Measure.Gauge.SetValue(MyMetricsRegistry.Errors, tags, 10);
至此我們就完成了一個基本的指標,下面我們繼續其他類型指標。
2. 計數值(Counter)
對於HTTP類型的網站來說,存在非常多的數量需要統計記錄,所以計數值此時就特別適合這類情況,比如我們需要統計請求數量就可以利用這類指標類型,下面我們就以請求數來定義這個指標:
var requestCounter = new CounterOptions()
{
Name = "httpRequest",
MeasurementUnit = Unit.Calls
};
以上我們定義了一個計數指標,其中我們可以看到我們這里使用了一個新變量MeasurementUnit
主要是用於定義指標單位的,當然這個只是輔助信息會一同輸出到結果,下面我們需要進行增加和減少,考慮到大多數情況都是減1和增1的情況:
metrics.Measure.Counter.Increment(requestCounter);
實際情況可能我們都是統計請求但是期望還能單獨統計特定接口的請求,這個時候我們在原本調用方式基礎上增加額外的參數:
metrics.Measure.Counter.Increment(requestCounter, "api");
如果嫌每次增長1比較慢,我們通過其函數的重載形式填寫我們希望增長的具體值。
3. 計量值(Meter)
有點類似於計數值但是相比來說,它可以提供更加豐富的信息,比如每1、5、15分鍾的增長率等,所以對於一些需要通過增長率觀察的數據特別時候,這里我們以請求的反應狀態碼進行記錄來體現其用途:
var httpStatusMeter = new MeterOptions()
{
Name = "Http Status",
MeasurementUnit = Unit.Calls
};
以上我們完成了一個指標的定義,下面我們開始使用其並且定義不同的狀態的碼的發生情況,具體如下:
metrics.Measure.Meter.Mark(httpStatusMeter, "200");
metrics.Measure.Meter.Mark(httpStatusMeter, "500");
metrics.Measure.Meter.Mark(httpStatusMeter, "401");
當然如果希望增加的數量自定控制也可以使用其提供的重載形式進行。
4. 柱狀圖(Histogram)
顧名思義,主要反應數據的分布情況,所以這里不在重復闡述,大家對於這種數據表現形式還是比較了解的,所以下面就直接以實際代碼的實列進行介紹,便於大家的理解:
var postAndPutRequestSize = new HistogramOptions()
{
Name = "Web Request Post & Put Size",
MeasurementUnit = Unit.Bytes
};
以上我們定義一個體現Post和Put請求的數據尺寸的指標,下面我們利用隨機數來進行數據的模擬對其進行數據填充,便於顯示數據:
var rnd = new Random();
foreach (var i in Enumerable.Range(0, 50))
{
var t = rnd.Next(0, 10);
metrics.Measure.Histogram.Update(postAndPutRequestSize, t);
}
5. 時間線(Timer)
對應指標的監控閉然少不了對於時間的記錄,特別對於HTTP來說,直接影響到用戶的體驗就是響應時間,素以我們也需要時刻關於這類指標的變化情況及時做出反應,下面我們就以數據庫的響應時間的情況作為指標進行監控:
TimerOptions DatabaseTimer = new TimerOptions()
{
Name = "Database Timer",
MeasurementUnit = Unit.Items,
DurationUnit = TimeUnit.Milliseconds,
RateUnit = TimeUnit.Milliseconds
};
上面我們通過特別的屬性指定了改指標記錄時間的單位,下面我們使用其指標進行數據的記錄:
using(metrics.Measure.Timer.Time(DatabaseTimer))
{
//to do sonmething
}
我們可以看到為了方便的記錄請求的時間,我們使用using
進行涵括,並將需要記錄耗時的請求操作放入其中,在請求完成操作后就可以正確的記錄其需要的時間。
6. apdex
采用了一種標准的性能指標計算方式,用法類似與上述,這里僅僅列舉用法:
ApdexOptions SampleApdex = new ApdexOptions
{
Name = "Sample Apdex"
};
using(metrics.Measure.Apdex.Track(SampleApdex))
{
Thread.Sleep(100);
}
三、高級指標
1. 平均響應
很多時候我們僅僅依靠一個指標很難完成一個實際的需求,是所以我們就需要將多個指標進行組合進行,比如我們期望得到請求次數,同時還有請求的總時間和平均響應時間等,為此我們可以特殊的指標將多個指標進行組合,具體操作如下:
var cacheHitRatioGauge = new GaugeOptions
{
Name = "Cache Gauge",
MeasurementUnit = Unit.Calls
};
var cacheHitsMeter = new MeterOptions
{
Name = "Cache Hits Meter",
MeasurementUnit = Unit.Calls
};
var databaseQueryTimer = new TimerOptions
{
Name = "Database Query Timer",
MeasurementUnit = Unit.Calls,
DurationUnit = TimeUnit.Milliseconds,
RateUnit = TimeUnit.Milliseconds
};
var cacheHits = metrics.Provider.Meter.Instance(cacheHitsMeter);
var calls = metrics.Provider.Timer.Instance(databaseQueryTimer);
var cacheHit = new Random().Next(0, 2) == 0;
using(calls.NewContext())
{
if (cacheHit)
{
cacheHits.Mark(5);
}
Thread.Sleep(cacheHit ? 10 : 100);
}
metrics.Measure.Gauge.SetValue(cacheHitRatioGauge, () => new HitRatioGauge(cacheHits, calls, m => m.OneMinuteRate));
四、利用Promethues和Grafana進行監控
1. 環境准備
這里需要使用到Prometheus
和Grafana
,為了避免版本導致的區別這里提供了對應百度雲的下載地址,大家可以自行進行下載。
Prometheus對應提取碼為2b1r
Grafana對應提取碼為mjym
完成以上下載后需要解壓到對應文件夾下即可。
2. 配置服務
首先我們需要針對Prometheus
進行配置,我們打開prometheus.yml
文件新增基於AppMetrics
的監控指標。
- job_name: 'appweb'
scrape_interval: 5s
metrics_path: '/metrics-text'
static_configs:
- targets: ['localhost:5000']
完成之后我們可以先打開采集讓其在后台持續采集,后面我們需要針對AppMetrics
暴露的數據進行調整。
3. 應用指標輸出
通過實際的測試發現基於2.0.0
版本的Prometheus存在問題,因為指標類型被大寫了,導致Prometheus
無法正確讀取,所以我們需要將源碼復制出來進行操作,這里直接給出了對應的源碼文件,
主要的工作就是將AsciiFormatter.cs
中的HELP
和TYPE
進行了小寫而已,對應文件如下。
PS:考慮到很多基於2.0的所以這里保留了基於HTTP的文本實現方式發布了一個對應的版本庫:
Install-Package Sino.Metrics.Formatters.Prometheus -Version 0.1.2
- AsciiFormatter.cs
internal static class AsciiFormatter
{
public static void Format(Stream destination, IEnumerable<MetricFamily> metrics)
{
var metricFamilys = metrics.ToArray();
using (var streamWriter = new StreamWriter(destination, Encoding.UTF8))
{
streamWriter.NewLine = "\n";
foreach (var metricFamily in metricFamilys)
{
WriteFamily(streamWriter, metricFamily);
}
}
}
internal static string Format(IEnumerable<MetricFamily> metrics, NewLineFormat newLine)
{
var newLineChar = GetNewLineChar(newLine);
var metricFamilys = metrics.ToArray();
var s = new StringBuilder();
foreach (var metricFamily in metricFamilys)
{
s.Append(WriteFamily(metricFamily, newLineChar));
}
return s.ToString();
}
private static void WriteFamily(StreamWriter streamWriter, MetricFamily metricFamily)
{
streamWriter.WriteLine("# HELP {0} {1}", metricFamily.name, metricFamily.help.ToLower());
streamWriter.WriteLine("# TYPE {0} {1}", metricFamily.name, metricFamily.type.ToString().ToLower());
foreach (var metric in metricFamily.metric)
{
WriteMetric(streamWriter, metricFamily, metric);
}
}
private static string WriteFamily(MetricFamily metricFamily, string newLine)
{
var s = new StringBuilder();
s.Append(string.Format("# HELP {0} {1}", metricFamily.name, metricFamily.help.ToLower()), newLine);
s.Append(string.Format("# TYPE {0} {1}", metricFamily.name, metricFamily.type.ToString().ToLower()), newLine);
foreach (var metric in metricFamily.metric)
{
s.Append(WriteMetric(metricFamily, metric, newLine), newLine);
}
return s.ToString();
}
private static void WriteMetric(StreamWriter streamWriter, MetricFamily family, Metric metric)
{
var familyName = family.name;
if (metric.gauge != null)
{
streamWriter.WriteLine(SimpleValue(familyName, metric.gauge.value, metric.label));
}
else if (metric.counter != null)
{
streamWriter.WriteLine(SimpleValue(familyName, metric.counter.value, metric.label));
}
else if (metric.summary != null)
{
streamWriter.WriteLine(SimpleValue(familyName, metric.summary.sample_sum, metric.label, "_sum"));
streamWriter.WriteLine(SimpleValue(familyName, metric.summary.sample_count, metric.label, "_count"));
foreach (var quantileValuePair in metric.summary.quantile)
{
var quantile = double.IsPositiveInfinity(quantileValuePair.quantile)
? "+Inf"
: quantileValuePair.quantile.ToString(CultureInfo.InvariantCulture);
streamWriter.WriteLine(
SimpleValue(
familyName,
quantileValuePair.value,
metric.label.Concat(new[] { new LabelPair { name = "quantile", value = quantile } })));
}
}
else if (metric.histogram != null)
{
streamWriter.WriteLine(SimpleValue(familyName, metric.histogram.sample_sum, metric.label, "_sum"));
streamWriter.WriteLine(SimpleValue(familyName, metric.histogram.sample_count, metric.label, "_count"));
foreach (var bucket in metric.histogram.bucket)
{
var value = double.IsPositiveInfinity(bucket.upper_bound) ? "+Inf" : bucket.upper_bound.ToString(CultureInfo.InvariantCulture);
streamWriter.WriteLine(
SimpleValue(
familyName,
bucket.cumulative_count,
metric.label.Concat(new[] { new LabelPair { name = "le", value = value } }),
"_bucket"));
}
}
else
{
// not supported
}
}
private static string WriteMetric(MetricFamily family, Metric metric, string newLine)
{
var s = new StringBuilder();
var familyName = family.name;
if (metric.gauge != null)
{
s.Append(SimpleValue(familyName, metric.gauge.value, metric.label), newLine);
}
else if (metric.counter != null)
{
s.Append(SimpleValue(familyName, metric.counter.value, metric.label), newLine);
}
else if (metric.summary != null)
{
s.Append(SimpleValue(familyName, metric.summary.sample_sum, metric.label, "_sum"), newLine);
s.Append(SimpleValue(familyName, metric.summary.sample_count, metric.label, "_count"), newLine);
foreach (var quantileValuePair in metric.summary.quantile)
{
var quantile = double.IsPositiveInfinity(quantileValuePair.quantile)
? "+Inf"
: quantileValuePair.quantile.ToString(CultureInfo.InvariantCulture);
s.Append(
SimpleValue(
familyName,
quantileValuePair.value,
metric.label.Concat(new[] { new LabelPair { name = "quantile", value = quantile } })), newLine);
}
}
else if (metric.histogram != null)
{
s.Append(SimpleValue(familyName, metric.histogram.sample_sum, metric.label, "_sum"), newLine);
s.Append(SimpleValue(familyName, metric.histogram.sample_count, metric.label, "_count"), newLine);
foreach (var bucket in metric.histogram.bucket)
{
var value = double.IsPositiveInfinity(bucket.upper_bound) ? "+Inf" : bucket.upper_bound.ToString(CultureInfo.InvariantCulture);
s.Append(
SimpleValue(
familyName,
bucket.cumulative_count,
metric.label.Concat(new[] { new LabelPair { name = "le", value = value } }),
"_bucket"), newLine);
}
}
else
{
// not supported
}
return s.ToString();
}
private static string WithLabels(string familyName, IEnumerable<LabelPair> labels)
{
var labelPairs = labels as LabelPair[] ?? labels.ToArray();
if (labelPairs.Length == 0)
{
return familyName;
}
return string.Format("{0}{{{1}}}", familyName, string.Join(",", labelPairs.Select(l => string.Format("{0}=\"{1}\"", l.name, l.value))));
}
private static string SimpleValue(string family, double value, IEnumerable<LabelPair> labels, string namePostfix = null)
{
return string.Format("{0} {1}", WithLabels(family + (namePostfix ?? string.Empty), labels), value.ToString(CultureInfo.InvariantCulture));
}
private static string GetNewLineChar(NewLineFormat newLine)
{
switch (newLine)
{
case NewLineFormat.Auto:
return Environment.NewLine;
case NewLineFormat.Windows:
return "\r\n";
case NewLineFormat.Unix:
case NewLineFormat.Default:
return "\n";
default:
throw new ArgumentOutOfRangeException(nameof(newLine), newLine, null);
}
}
private static void Append(this StringBuilder sb, string line, string newLineChar)
{
sb.Append(line + newLineChar);
}
}
- MetricsPrometheusTextOutputFormatter.cs
public class MetricsPrometheusTextOutputFormatter : IMetricsOutputFormatter
{
private readonly MetricsPrometheusOptions _options;
public MetricsPrometheusTextOutputFormatter()
{
_options = new MetricsPrometheusOptions();
}
public MetricsPrometheusTextOutputFormatter(MetricsPrometheusOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); }
/// <inheritdoc/>
public MetricsMediaTypeValue MediaType => new MetricsMediaTypeValue("text", "vnd.appmetrics.metrics.prometheus", "v1", "plain");
/// <inheritdoc/>
public async Task WriteAsync(
Stream output,
MetricsDataValueSource metricsData,
CancellationToken cancellationToken = default(CancellationToken))
{
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
using (var streamWriter = new StreamWriter(output))
{
await streamWriter.WriteAsync(AsciiFormatter.Format(metricsData.GetPrometheusMetricsSnapshot(_options.MetricNameFormatter), _options.NewLineFormat));
}
}
}
新建好以上兩個文件后我們接着需要修改Program.cs
文件,具體內容如下:
public static IWebHost BuildWebHost(string[] args)
{
Metrics = AppMetrics.CreateDefaultBuilder()
.OutputMetrics.AsPrometheusPlainText()
.Build();
return WebHost.CreateDefaultBuilder(args)
.ConfigureMetrics(Metrics)
.UseMetrics(options =>
{
options.EndpointOptions = endpointsOptions =>
{
endpointsOptions.MetricsTextEndpointOutputFormatter = new MetricsPrometheusTextOutputFormatter();
};
})
.UseStartup<Startup>()
.Build();
}
完成以上操作后我們可以啟用應用,此時可以看到不斷用請求到/metrics-text
表示已經開始采集指標了。
4. 指標可視化
此時我們打開Grafana
文件夾,通過其中的bin
目錄下的grafana-server.exe
啟動服務,然后訪問localhost:3000
利用初始賬戶密碼進行登錄(admin/admin)。
進入后添加Prometheus
數據源。由於AppMetrics已經提供了對應的看板所以我們可以通過ID2204
直接導入,並選擇正確的數據源就可以看到最終的效果了。