



using System; using App.Metrics; using App.Metrics.AspNetCore; using App.Metrics.AspNetCore.Health; using App.Metrics.Formatters.Prometheus; using Microsoft.AspNetCore.Hosting;namespace Common.Metrics {public static class Extensions{public static IWebHostBuilder UseAppMetrics(this IWebHostBuilder webHostBuilder)=> webHostBuilder.ConfigureMetricsWithDefaults((context, builder) =>{var metricsOptions = context.Configuration.GetOptions<MetricsOptions>("metrics");if (!metricsOptions.Enabled){return;}builder.Configuration.Configure(cfg =>{var tags = metricsOptions.Tags;if (tags == null){return;}tags.TryGetValue("app", out var app);tags.TryGetValue("env", out var env);tags.TryGetValue("server", out var server);cfg.AddAppTag(string.IsNullOrWhiteSpace(app) ? null : app);cfg.AddEnvTag(string.IsNullOrWhiteSpace(env) ? null : env);cfg.AddServerTag(string.IsNullOrWhiteSpace(server) ? null : server);foreach (var tag in tags){if (!cfg.GlobalTags.ContainsKey(tag.Key)){cfg.GlobalTags.Add(tag.Key, tag.Value);}}});if (metricsOptions.InfluxEnabled){builder.Report.ToInfluxDb(o =>{o.InfluxDb.Database = metricsOptions.Database;o.InfluxDb.BaseUri = new Uri(metricsOptions.InfluxUrl);o.InfluxDb.CreateDataBaseIfNotExists = true;o.FlushInterval = TimeSpan.FromSeconds(metricsOptions.Interval);});}}).UseHealth().UseHealthEndpoints().UseMetricsWebTracking().UseMetrics((context, options) =>{var metricsOptions = context.Configuration.GetOptions<MetricsOptions>("metrics");if (!metricsOptions.Enabled){return;}if (!metricsOptions.PrometheusEnabled){return;}options.EndpointOptions = endpointOptions =>{switch (metricsOptions.PrometheusFormatter?.ToLowerInvariant() ?? string.Empty){case "protobuf":endpointOptions.MetricsEndpointOutputFormatter =new MetricsPrometheusProtobufOutputFormatter();break;default:endpointOptions.MetricsEndpointOutputFormatter =new MetricsPrometheusTextOutputFormatter();break;}};});} }
User AOP approach (Attribute / Interceptor base) to send hit count to InfluxDB and poll by Gafana
Benefit
Using attribute to encapsulate metric related operation so the team can have a uniform and elegant approach to achieve this.
Controller level

using Microsoft.AspNetCore.Mvc.Filters; using System.Threading.Tasks;namespace Common.Metrics {public class AppMetricCountAttribute : ActionFilterAttribute{private readonly string metricname;public IMetricRegistry metricRegistry { get; set; }public AppMetricCountAttribute(string MetricName) => metricname = MetricName; public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next){metricRegistry = (IMetricRegistry)context.HttpContext.RequestServices.GetService(typeof(IMetricRegistry));metricRegistry.IncrementCount(metricname);return base.OnActionExecutionAsync(context, next);}} }
Start up level

Add Memory metric to Grafana
Goal is to allow mimimum coding for subsystem to utilize the common feature to send system momory metric to INFLUX db and Displayed by Grafana
Demo



Extension
public static IServiceCollection AddGaugeMetric(this IServiceCollection service){GaugeOption option;using (var serviceprovider = service.BuildServiceProvider()){var configuration = serviceprovider.GetService<IConfiguration>();service.Configure<GaugeOption>(configuration.GetSection("gaugemetric"));option = configuration.GetOptions<GaugeOption>("gaugemetric");}//IF THESE TYPE ARE NOT FROM EXECUTING ASSEMBLY BUT FROM REFERENCED ASEMBLY//THEN ADDING TO SERVICE DI ONE BY ONE IS NECESSARY service.AddSingleton(option.GetType(), option); service.AddTransient<GaugeMetric>();service.AddSingleton<IHostedService, GaugeHostedService>();return service;}
GaugeMetric (Will be Inject to GaugeHostedService super class)
using App.Metrics; using App.Metrics.Gauge; using System.Diagnostics;namespace Common.Metrics {public class GaugeMetric{private readonly IMetricsRoot root;private readonly GaugeOption option;public GaugeMetric(IMetricsRoot root, GaugeOption option){this.root = root;this.option = option;}public void SendGaugeData(){var processPhysicalMemoryGauge = new GaugeOptions{Name = option.name,MeasurementUnit = Unit.MegaBytes};var process = Process.GetCurrentProcess();root.Measure.Gauge.SetValue(processPhysicalMemoryGauge, process.WorkingSet64 / 1024.0 / 1024.0);}} }
Utilized hosting service to trigger a contineous task is web hosted that run in configured interval
using Microsoft.Extensions.Logging; using System;namespace Common.Metrics {public class GaugeHostedService : HostedServiceTemplate{ILogger<GaugeHostedService> log;GaugeMetric gaugeMetric;GaugeOption option;public GaugeHostedService(ILogger<GaugeHostedService> log, GaugeMetric gaugeMetric, GaugeOption option) : base(option){this.log = log;this.gaugeMetric = gaugeMetric;this.option = option;}protected override void ExecuteAsync(object state){log.LogInformation(DateTime.Now.ToLongTimeString());gaugeMetric.SendGaugeData();}} }
Super class - template design pattern
using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks;namespace Common.Metrics {public abstract class HostedServiceTemplate : IHostedService{private readonly CancellationTokenSource source;private int interval = 10;private Timer timer;private GaugeOption option;public HostedServiceTemplate(){ }protected HostedServiceTemplate(GaugeOption option){this.option = option;interval = option.delay;}public Task StartAsync(CancellationToken cancellationToken){//Create new cancel tokentimer = new Timer(ExecuteAsync, null, TimeSpan.Zero, TimeSpan.FromSeconds(interval));return Task.CompletedTask;}protected abstract void ExecuteAsync(object state);public Task StopAsync(CancellationToken cancellationToken){// Trigger source cancel to stop the on going timer?.Change(Timeout.Infinite, 0);return Task.CompletedTask;}} }