代码改变世界

利用AppMetrics对Web进行监控教程

2020-02-10 17:31  y-z-f  阅读(1317)  评论(0编辑  收藏  举报

利用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. 环境准备

这里需要使用到PrometheusGrafana,为了避免版本导致的区别这里提供了对应百度云的下载地址,大家可以自行进行下载。

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中的HELPTYPE进行了小写而已,对应文件如下。

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直接导入,并选择正确的数据源就可以看到最终的效果了。