https://a.storyblok.com/f/270183/56919/50e711090f/send-notifications_app-proformance_1200x600.png

Enviar notificações por SMS sobre o desempenho do aplicativo usando C#

Publicado em August 2, 2021

Tempo de leitura: 6 minutos

Ao trabalhar com applications, muitas vezes você vai querer registrar dados relevantes, como onde o aplicativo vem apresentando falhas, o uso da CPU e o uso da memória. Isso lhe proporcionaria várias informações essenciais que impulsionariam seus negócios e ajudariam a detectar bugs rapidamente. Para ajudá-lo a responder rapidamente, você pode usar notificações que contenham um resumo das estatísticas de desempenho ou detalhes sobre um erro no ambiente de produção.

Este artigo explica como você pode obter estatísticas de desempenho de um aplicativo web ASP.NET Core 5 usando C# e, em seguida, aproveitar a SMS API da Vonage para enviar notificações por mensagem de texto em intervalos de tempo pré-configurados.

Pré-requisitos

  • Visual Studio 2019.

  • Sistema operacional Windows

Você também precisará instalar os seguintes pacotes NuGet no seu projeto:

  • NLog

  • Quartzo

  • Vonage

Você pode instalá-los por meio do Gerenciador de Pacotes NuGet enquanto estiver no Visual Studio.

O Projeto Inicial

Você pode encontrar um projeto inicial no GitHub na pasta “starter”. O projeto inicial contém um projeto ASP.NET Core MVC 5 com recursos para ler informações sobre o uso da CPU e da memória. Neste tutorial, você adicionará a funcionalidade de notificação por SMS.

Utilizando o Quartz.NET para tarefas em segundo plano

O Quartz.NET é gratuito e uma versão .NET de código aberto do popular framework de agendamento de tarefas em Java, o Quartz. Ele oferece excelente suporte para lidar com expressões Cron e é amplamente recomendado para a criação de Applications que necessitem de funcionalidades de agendamento de tarefas.

Os principais componentes do Quartz.NET

O Quartz.NET é composto por três Concepts fundamentais:

Um Job – Representa as tarefas em segundo plano que você deseja executar enquanto realiza outras atividades. Uma classe de job estende a interface IJob e implementa seus membros.

Um gatilho — Um gatilho determina quando uma tarefa é executada e, geralmente, é configurado para ser executado de acordo com uma programação pré-definida. Em outras palavras, você pode usar um gatilho para especificar a programação de um trabalho.

Um Agendador — Trata-se de um componente responsável por coordenar as tarefas e os gatilhos, bem como por executar as tarefas com base em agendamentos predefinidos.

O blog abordará esses componentes com mais detalhes em breve.

Os componentes do aplicativo “Enviar Notificação”

No aplicativo de exemplo, você utilizará a SMS API da Vonage para enviar notificações assim que o uso da CPU ou da memória exceder um limite predefinido. Portanto, nosso aplicativo ASP.NET Core MVC 5 será composto pelos seguintes componentes:

  • Agendador personalizado — Você utilizará o Quartz para criar um agendador personalizado que monitore o consumo de memória e da CPU no sistema do computador. Ele verificará esses valores em intervalos de tempo predefinidos em segundo plano, enquanto você trabalha com o aplicativo na interface do usuário.

  • Registrador personalizado - Você também utilizará o NLog para registrar dados sobre o desempenho do aplicativo nesta aplicação. Embora as notificações sejam enviadas apenas se o consumo de CPU ou memória ultrapassar o limite predefinido, o registro conterá dados de consumo de CPU e memória coletados a cada 5 ou 10 segundos (conforme o intervalo predefinido) e armazenados em um arquivo de texto.

Para este projeto, estas são as classes que você criará no aplicativo:

  • PerformanceHelper – Essa classe é usada para obter informações sobre o uso da CPU e da memória.

  • NotificationManager – essa classe encapsula a funcionalidade para enviar notificações.

  • NotificationJob – Esta é a classe de tarefa que utiliza a NotificationManager classe para enviar notificações, conforme discutido anteriormente.

  • CustomJobFactory – Essa turma é responsável pela criação de empregos.

  • CustomJobScheduler – Essa classe é responsável pelo agendamento de tarefas.

  • QuartzExtensions - Essa classe contém um método de extensão chamado AddQuartz para simplificar e organizar o código.

project architecture diagramproject architecture diagram

Use o WMI para obter informações sobre o uso da CPU e da memória

O Windows Management Instrumentation (WMI) é uma estrutura orientada a objetos, extensível e baseada em padrões que amplia o Modelo de Driver do Windows e oferece uma interface do sistema operacional por meio da qual os componentes instrumentados fornecem informações e notificações. O objetivo principal do WMI é criar um conjunto proprietário de especificações independentes do ambiente que permita a troca de informações de gerenciamento entre Applications.

Para obter os índices de uso da CPU e da memória em porcentagem, você utilizará a ManagementObjectSearcher classe do namespace System Management. Essa classe pode enumerar todas as unidades de disco, adaptadores de rede, processos e outros objetos de gerenciamento em um sistema, além de recuperar uma coleção de objetos de gerenciamento com base na consulta especificada. Ao instanciar essa classe, você precisará passar uma consulta WMI representada em um ObjectQuery e, opcionalmente, um ManagementScope representando o namespace WMI para executar a consulta.

Crie uma classe chamada PerformanceHelper.cs no projeto ASP.NET Core MVC 5 que você clonou anteriormente e escreva o seguinte código:

public static class PerformanceHelper
{
  public static double GetCPUUsageInPercentage()
  {
    ObjectQuery objQuery = new ObjectQuery("SELECT * FROM Win32_PerfFormattedData_PerfOS_Processor WHERE Name=\"_Total\"");
    ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher(objQuery);
    ManagementObjectCollection managementObjectCollection = managementObjectSearcher.Get();
    double cpu_usage = 0;

    if (managementObjectCollection.Count > 0)
    {
      foreach (ManagementObject managementObject in managementObjectCollection)
      {
        try
        {
          cpu_usage = 100 - Convert.ToUInt32(managementObject["PercentIdleTime"]);
          break;
        }
        catch (Exception ex)
        {
          break;
        }
      }
    }

    return cpu_usage;
  }

  public static Dictionary<string, double> GetMemoryUsageInfo()
  {
    Dictionary<string, double> keyValuePairs = new Dictionary<string, double>();

    ObjectQuery objQuery = new ObjectQuery("SELECT * FROM Win32_OperatingSystem");
    ManagementObjectSearcher searcher = new ManagementObjectSearcher(objQuery);
    ManagementObjectCollection results = searcher.Get();
    Dictionary<string, double> resourceConsumptionResults = new Dictionary<string, double>();

    foreach (ManagementObject result in results)
    {
      var totalVisibleMemory = Math.Round(double.Parse(result["TotalVisibleMemorySize"].ToString()), 2);
      var totalFreeMemory = Math.Round(double.Parse(result["FreePhysicalMemory"].ToString()), 2);

      resourceConsumptionResults.Add("Total_Visible_Memory", totalVisibleMemory);
      resourceConsumptionResults.Add("Free_Physical_Memory", totalFreeMemory);
    }

    double totalVisibleMemorySize = Math.Round(resourceConsumptionResults["Total_Visible_Memory"] / (1024 * 1024), 2);
    double freePhysicalMemory = Math.Round(resourceConsumptionResults["Free_Physical_Memory"] / (1024 * 1024), 2);
    double totalUsedMemory = totalVisibleMemorySize - freePhysicalMemory;
    double memory_usage = 0;

    try
    {
      memory_usage = ((totalVisibleMemorySize - freePhysicalMemory) / totalVisibleMemorySize) * 100;
      keyValuePairs.Add("Total_Visible_Memory", totalVisibleMemorySize);
      keyValuePairs.Add("Total_Free_Memory", freePhysicalMemory);
      keyValuePairs.Add("Total_Used_Memory", totalUsedMemory);
      keyValuePairs.Add("Total_Used_Memory_Percentage", memory_usage);
    }
    catch
    {
      throw;
    }

    return keyValuePairs;
  }
}

Criar uma classe de trabalho

Em seguida, você criará a classe de tarefa. Uma classe de tarefa no Quartz estende a interface IJob e implementa seus membros. Crie uma classe chamada NotificationJob em um arquivo chamado NotificationJob.cs com o seguinte conteúdo:

public class NotificationJob: IJob 
{
  private readonly NotificationManager _notificationManager;
  public NotificationJob(NotificationManager notificationManager)
  {
    _notificationManager = notificationManager;
  }
  public async Task Execute(IJobExecutionContext context)
  {
    await _notificationManager.SendNotification();
  }
}

O método Execute da NotificationJob classe chama o SendNotification método da NotificationManager classe. Crie uma nova classe chamada NotificationManager em um arquivo chamado NotificationManager.cs com o seguinte código:

public class NotificationManager
{
  private ILoggerFactory LoggerFactory { get; }
  private readonly ILogger Logger;
  public IConfiguration Configuration { get; set; }

  public NotificationManager(IConfiguration config, ILoggerFactory loggerFactory)
  {
    Configuration = config;
    LoggerFactory = loggerFactory;

    if (loggerFactory != null) {
      Logger = loggerFactory.CreateLogger("NotificationManager");
    }
  }
    
  public async Task SendNotification()
  {
    await SendResourceUsageInfo();
    await Task.CompletedTask;
  }

  private async Task SendResourceUsageInfo()
  {
    bool isMemoryUsageHigh = false;
    bool isCPUUsageHigh = false;

    var keyValuePairs = PerformanceHelper.GetMemoryUsageInfo();

    var memoryUsage = Math.Round(keyValuePairs["Total_Used_Memory_Percentage"], 2);
    var cpuUsage = Math.Round(PerformanceHelper.GetCPUUsageInPercentage(), 2);

    var memoryThreshold = Configuration["Memory_Threshold"];
    var cpuThreshold = Configuration["CPU_Threshold"];

    if (memoryUsage > double.Parse(memoryThreshold))
    {
      isMemoryUsageHigh = true;
    }
    if (cpuUsage > double.Parse(cpuThreshold))
    {
      isCPUUsageHigh = true;
    }

    if (isMemoryUsageHigh)
    {
      string memoryConsumptionAlertMessage = string.Format($"Alert!!! Memory Usage: " +
      $ "{keyValuePairs["
      Total_Used_Memory "]} GB " +
      $ "/ {keyValuePairs["
      Total_Visible_Memory "]} GB");
      SendTextMessage(memoryConsumptionAlertMessage);
    }

    if (isCPUUsageHigh)
    {
      string cpuConsumptionAlertMessage = string.Format("Alert!!! CPU Usage: {0}", cpuUsage);
      SendTextMessage(cpuConsumptionAlertMessage);
    }

    await Task.CompletedTask;
  }

    private void SendTextMessage(string message)
    {
      try
      {
        var apiKey = Configuration["API_KEY"];
        var apiSecret = Configuration["API_SECRET"];
        var credentials = Credentials.FromApiKeyAndSecret(apiKey, apiSecret);
        var client = new SmsClient(credentials);

        var request = new SendSmsRequest
        {
          To = Configuration["TO"],
          From = Configuration["FROM"],
          Text = message
        };

        var response = client.SendAnSms(request);
        Logger?.LogInformation(response.MessageCount);
      }
      catch (VonageSmsResponseException ex)
      {
        Logger?.LogError(ex.Message);
        throw;
      }
  }
}

A SendTextMessage função usa o pacote da Vonage para enviar um SMS. SendTextMessage é chamada SendResourceUsageInfo se o uso da CPU ou da memória ultrapassar o limite. Você definirá os limites e as variáveis de configuração posteriormente.

Criar uma classe JobFactory personalizada

Em seguida, você precisa criar nossa classe personalizada JobFactory . Essa classe é necessária para criar novas instâncias de tarefa e descartá-las quando não forem mais necessárias. Você precisa de uma implementação personalizada do Quartz IJobFactory , já que a implementação padrão não permite o uso de Injeção de Dependências (DI) ao criar tarefas. Crie uma classe chamada CustomJobFactory em um arquivo com o mesmo nome, contendo o seguinte código:

public class CustomJobFactory : IJobFactory
{
    private readonly IServiceProvider _serviceProvider;
    public CustomJobFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
    {
        var jobDetail = bundle.JobDetail;
        return (IJob)_serviceProvider.GetService(jobDetail.JobType);
    }
    public void ReturnJob(IJob job)
    {
        var disposableJobInstance = job as IDisposable;

        if (disposableJobInstance != null)
        {
            disposableJobInstance.Dispose();
        }
    }
}

Criar a classe CustomJobScheduler

Agora, crie uma nova classe chamada CustomJobScheduler em um arquivo chamado CustomJobScheduler.cs na pasta raiz do projeto e substitua o código-fonte padrão da CustomJobScheduler que você acabou de criar pelo seguinte código:

public class CustomJobScheduler
{
    public static void ScheduleJob(IScheduler scheduler)
    {
      var jobName = "NotificationJob";

      var job = JobBuilder.Create<NotificationJob>()
      .WithIdentity(jobName)
      .Build();

      var trigger = TriggerBuilder.Create()
      .WithIdentity($"{jobName}.trigger")
      .StartNow()
      .WithSimpleSchedule(scheduleBuilder =>
          scheduleBuilder
              .WithInterval(TimeSpan.FromSeconds(5))
              .RepeatForever())
      .Build();

      scheduler.ScheduleJob(job, trigger);
    }
}

O ScheduleJob método da CustomJobScheduler classe aceita uma instância do tipo IScheduler e a utiliza para agendar uma tarefa com base em um gatilho determinado, que, neste caso, é um TimeSpan.

Configurando as dependências

Agora, crie uma classe chamada QuartzExtensions com o seguinte código:

public static class QuartzExtensions
{
  public static void AddQuartz(this IServiceCollection services, params Type[] jobs)
  {
    services.AddSingleton<IJobFactory, CustomJobFactory>();
    services.AddSingleton<NotificationJob>();

    services.AddSingleton(provider =>
    {
      var schedulerFactory = new StdSchedulerFactory();
      var scheduler = schedulerFactory.GetScheduler().Result;
      scheduler.JobFactory = provider.GetService<IJobFactory>();
      scheduler.Start();
      return scheduler;
    });
  }
}

Conforme mostrado no diagrama de componentes apresentado anteriormente, a QuartzExtensions classe contém um método chamado AddQuartz. O método cria instâncias de JobFactory eNotificationJob, que possuem, ambos, um ciclo de vida de singleton. AddQuartz adiciona ambas ao contêiner. Ele também atribui nossa JobFactory classe à instância do agendador.

Configurar o aplicativo

Adicione o Memory_Threshold, CPU_Threshold, TO, FROM, API_KEY, e API_SECRET chaves no appsettings.json arquivo. Substituindo FROM, API_KEY, e API_SECRET pelo seu número da Vonage, chave de API e segredo de API. TO deve ser o número para o qual você deseja enviar as notificações por SMS:

{

"AllowedHosts": "*",

"Memory_Threshold": 80,

"CPU_Threshold": 80,

"TO": "TO",

"FROM": "FROM",

"API_KEY": "API_KEY",

"API_SECRET": "API_SECRET"

}

Você também deve configurar NLog.config e especificar o nome e o caminho do arquivo de log nesse local.

<?xml version="1.0" encoding="utf-8" ?>
<nlog
  xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Trace"
      internalLogFile="C:\Logs\InternalLog.txt">
  <targets>
    <target name="logfile" xsi:type="File"
            fileName="C:/Logs/${shortdate}_AppLog.txt"
            layout="${longdate} ${level:uppercase=true} ${message}"/>
  </targets>
  <rules>
    <logger name="*" minlevel="Debug" writeTo="logfile">
      <filters defaultAction='Log'>
        <when condition="contains('${message}','Quartz scheduler')" action="Ignore" />
      </filters>
    </logger>
  </rules>
</nlog>

Adicione a seguinte linha no ConfigureServices método da Startup classe para adicionar uma instância da NotificationManager classe como um serviço transitório ao contêiner de serviços integrado:

services.AddTransient<NotificationManager>();

Agendar a tarefa

Por fim, você deve recuperar uma instância do agendador do contêiner de serviços do aplicativo e usá-la para agendar uma tarefa. Para isso, é necessário utilizar o Configure método da Startup classe. O ConfigureServices e Configure da Startup são gerados por padrão ao criar um projeto web no ASP.NET Core ou no ASP.NET Core MVC. O ConfigureServices método é usado para adicionar serviços ao contêiner integrado, e o Configure é usado para configurar o pipeline de solicitações HTTP.

Escreva o código a seguir no Configure método da classe Startup:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())

    {
      app.UseDeveloperExceptionPage();
    }

    app.UseRouting();
    app.UseAuthorization();

    var scheduler = app.ApplicationServices.GetService<IScheduler>();
    CustomJobScheduler.ScheduleJob(scheduler);

    app.UseEndpoints(endpoints =>
    {
      endpoints.MapControllerRoute(
          name: "default",
          pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

Executar o aplicativo

Para executar este aplicativo:

  1. Localize o arquivo .sln e clique duas vezes nele para abri-lo no Visual Studio 2019

  2. Pressione F5 para executar o aplicativo

Assim que o aplicativo estiver em execução, aguarde até que ele gere os registros. Você poderá ver mensagens de registro sendo criadas a cada 5 segundos na C:\\Logs pasta. Você pode alterar essa duração conforme necessário, tornando-a configurável no appsettings.json arquivo. Você também verá notificações enviadas para o número de celular pré-configurado por meio da SMS API da Vonage assim que o consumo de CPU ou memória ultrapassar os valores limite.

a text message generated by the application when the memory usage exceeds 80%a text message generated by the application when the memory usage exceeds 80%

E agora?

Você pode conferir uma versão finalizada do projeto no GitHub na pasta “completed”.

Compartilhar:

https://a.storyblok.com/f/270183/320x319/b39aa07782/joydip-kanjilal.png
Joydip Kanjilal

Joydip is a Microsoft Most Valuable Professional in ASP.NET, Speaker, and Author of several books and articles. He has more than 25 years of experience in IT with more than 18 years in Microsoft .NET and its related technologies. Several times, he was selected as MSDN Featured Developer of the Fortnight (MSDN) and was a Community Credit Winner at http://www.community-credit.com. He has authored more than 500 articles in some of the most reputed sites worldwide, including MSDN, Info World, CodeMag, Tech Beacon, Tech Target, etc.