Аналитика в реальном времени с использованием .Net и Redis


В этом посте мы рассмотрим модуль RedisTimeSeries и построим пользовательский инструмент веб-аналитики с Redis & .NET 5 с помощью C#.

Обзор архитектуры приложения

Зачем использовать модуль RedisTimeSeries

Redis используется для аналитических приложений с давних пор, и поиск на таких платформах, как github, предоставит множество проектов по аналогичным направлениям.
За последние пару лет Redis был расширен современными моделями данных и инструментами обработки данных (документ, график, поиск и временные ряды).
Эти расширения позволяют разрабатывать определенные виды приложений, в том числе аналитические приложения, работающие практически в реальном времени, намного легче и проще.

RedisTimeSeries — это модуль Redis, который добавляет структуру данных временных рядов в Redis с такими возможностями, как…

  • Вставки большого объема, чтение с низкой задержкой
  • Запросы по времени начала и времени окончания
  • Агрегированные запросы (min, max, avg, sum, range, count, first, last, STD.P, STD.S, Var.P, Var.S, twa) для любого временного блока.
  • Настраиваемый максимальный период хранения
  • Уменьшение выборки / уплотнение для автоматически обновляемых агрегированных временных рядов
  • Вторичное индексирование для записей временных рядов. Каждый временной ряд имеет метки (пары значений полей), что позволяет выполнять запросы по меткам.

Большинство из этих функций вы увидите в разрабатываемом демо-приложении.
Настоятельным советом будет изучение доступных команд, которые можно запустить из Redis CLI. Все эти команды отображаются на методы, доступные в C# через NRedisTimeSeries — .Net клиент для RedisTimeSeries.

На базовом уровне создается новый временной ряд с помощью TS.CREATE и задается период хранения для каждой точки данных, входящей в этот временной ряд.
Возможно, вы захотите удалить ключ перед использованием TS.CREATE, чтобы не получить ошибку в случае, если ключ существует.

Добавьте выборку (точку данных) к временному ряду с помощью TS.ADD.

Получить доступ к последнему значению с помощью TS.GET или выполнить агрегированные запросы, используя различные методы.

Архитектура приложения

В веб-аналитических приложениях, на высоком уровне, есть трекеры/логгеры, либо javascript (в браузере), либо C#/Java/Python/NodeJS (на стороне сервера) или sdks мобильных приложений, которые генерируют данные. Эти данные передаются на уровень обмена сообщениями, такой как kafka или pulsar или даже redis, и потребляются приложениями для сбора, проверки, обогащения и, наконец, хранения в хранилищах данных, где они будут проанализированы.

Если вы разрабатываете аналитическое приложение для IOT, процесс будет аналогичным, но в этом случае данные будут генерироваться различными датчиками.

Эти трекеры генерируют данные из событий пользователя и отправляют множество атрибутов события в коллектор по каждому событию. Для демонстрационного приложения, чтобы не усложнять процесс, имитационные данные генерируются для очень ограниченного числа атрибутов.

Я назвал это приложение Redmetrix.

Для демонстрации эти имитационные данные поступают в консольное приложение .Net — Redmetrix Processor, которое сохраняет имитационные данные в redis (в структуре данных временного ряда).
Файлы журнала имеют формат, в котором каждая строка является правильным json.
Ingestor читает файлы построчно и добавляет данные в Redis.

В реальных приложениях эта логика может быть реализована в azure function / aws lambda, которая будет масштабироваться вверх/вниз по мере необходимости.

Наконец, у нас есть веб-приложение ASP.NET Core 5.0 с использованием razor pages — Redmetrix Webapp, которое считывает эти данные из Redis и отображает графики на приборной панели.

Хотя в этой статье мы получаем данные в файле codebehind страницы, в реальных сценариях у нас есть фоновый рабочий, который вызывает службу через определенные промежутки времени для получения необходимых данных из redis и отправляет данные через signalr клиентам, используя websockets и события на стороне сервера.

Диаграммы в Dashboard

В финальной статье и соответствующем коде в репозитории github будут следующие графики

  • Total PageViews (за последнюю минуту) & сравнение с предыдущей минутой
  • Общее количество транзакций
  • Общая стоимость заказа (в рупиях)
  • Производительность страницы для главной страницы и страницы продукта (за последние минуты)
  • Воронка конверсии (за последнюю минуту)
  • Заказы по способам оплаты (за последние минуты)

Демонстрационная инфраструктура

Создайте базу данных на Redis Cloud

Зарегистрируйтесь на Redis Cloud и / или войдите в систему

Выберите кнопку New DataBase.

Назовите новую базу данных redmetrix и выберите радиокнопку Redis Stack.

На следующей странице, где отображается конфигурация базы данных, пожалуйста, скопируйте public endpoint и password из разделов General и Security и сохраните их.

Генерирование макетов данных

Для создания фиктивных данных можно использовать такие приложения, как Mockaroo.
Посмотрите на https://www.mockaroo.com/7d448f50, чтобы увидеть тот, который будет использоваться здесь.

Используются следующие атрибуты

  1. application.app_id — Id приложения, может быть строкой или даже числом.
  2. application.platform — Обозначение веб-сайта или мобильного приложения.
  3. date_time.collector_tstamp_dt — Время, когда событие достигло уровня обмена сообщениями или было обогащено.
  4. event_detail.event_type — Просмотр страницы, изменение/отправка формы или Ecom Trasaction через событие
  5. event_detail.event_id — Id события — Guid
  6. contexts.page.type — Главная/Категория/Продукт и т.д..
  7. contexts.performance_timing.domContentLoadedEventEnd — Время окончания загрузки страницы
  8. contexts.performance_timing.requestStart — Время начала загрузки страницы
  9. contexts.transaction.amount_to_pay — Сумма транзакции
  10. contexts.transaction.payment_method — Способ оплаты
Для простоты все типы событий — это просмотры страниц, но в реальных приложениях львиная доля из них не будет просмотрами страниц.
Кроме того, Сумма к оплате и Метод оплаты должны быть частью события тогда и только тогда, когда тип страницы — ‘Подтверждение успеха’. Но в фиктивных данных вы обнаружите, что эти поля присутствуют во всех событиях.
Поле Date генерируется случайным образом, но в реальном мире оно будет последовательным. Поскольку мы не собираемся напрямую ничего делать с этим полем, это не создаст никаких проблем.

После загрузки макетных данных несколько раз, сохраните их в папке, переименовав их в файлы .txt, так как хотя каждая строка имеет структуру json, весь файл не имеет структуры (файл не является массивом json объектов).

Разработка Redmetrix

Для разработки в этой статье будет использоваться Visual Studio Code.

Процессор

Создайте базовое консольное приложение в терминале

dotnet new console -n "RedMetrixProcessor"
Войти в полноэкранный режим Выйти из полноэкранного режима

Переместитесь в каталог RedMetrixProcessor

cd RedMetrixProcessor
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Соберите и запустите приложение, чтобы убедиться, что все работает нормально.

dotnet build
dotnet run
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Откройте папку RedMetrixProcessor в VSCode и добавьте Новый файл с именем appsettings.json.

Теперь скопируйте сохраненные публичную конечную точку и пароль из конфигурации базы данных в файл, как показано в блоке кода

{
    "RedisOptions":{
        "EndPoints":"redis-14475.c280.us-central1-2.gce.cloud.redislabs.com:14475",
        "Password":"BVDwMO6Qj1tnDZalcxmfdH2mL1c1G5iA"
    }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Следующим шагом будет установка необходимых пакетов nuget

dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration.Json
Вход в полноэкранный режим Выход из полноэкранного режима

Обычно консольные приложения не являются визуально привлекательными. Поэтому я буду использовать пакет Spectre.Console, чтобы сделать его красочным.

В этой статье я не буду вдаваться в особенности или части кода Spectre. Но код будет доступен на github. Ознакомьтесь с ним.
dotnet add package Spectre.Console
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, пакеты, необходимые для Redis и RedisTimeSeries

dotnet add package StackExchange.Redis
dotnet add package NRedisTimeSeries
Вход в полноэкранный режим Выйти из полноэкранного режима

В результате выполнения описанных выше действий в ваш файл RedMetrixProcessor.csproj будет добавлен следующий блок кода

<ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
    <PackageReference Include="NRedisTimeSeries" Version="1.4.0" />
    <PackageReference Include="Spectre.Console" Version="0.44.0" />
    <PackageReference Include="StackExchange.Redis" Version="2.6.48" />
  </ItemGroup>
Вход в полноэкранный режим Выйти из полноэкранного режима

Добавьте следующий код, чтобы убедиться, что appsettings.json будет скопирован во время сборки/запуска.

  <ItemGroup>
   <None Update="appsettings.json">
     <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
   </None>
  </ItemGroup>
Вход в полноэкранный режим Выход из полноэкранного режима

Обновите код Program.cs, чтобы разрешить доступ к appsettings.json и подключиться к БД Redis.

using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Spectre.Console;
using NRedisTimeSeries;
using NRedisTimeSeries.DataTypes;
using StackExchange.Redis;
namespace RedMetrixProcessor
{
    class Program
    {
        public static IConfigurationRoot configuration;
        static void Main(string[] args)
        {
            ServiceCollection serviceDescriptors = new ServiceCollection();

            ConfigureServices(serviceDescriptors);
            IServiceProvider serviceProvider = serviceDescriptors.BuildServiceProvider();

            var options = new ConfigurationOptions
                {
                    EndPoints = { configuration.GetSection("RedisOptions:EndPoints").Value },
                    Password = configuration.GetSection("RedisOptions:Password").Value,
                    Ssl = false
                };
            // Multiplexer is intended to be reused
            ConnectionMultiplexer redisMultiplexer = ConnectionMultiplexer.Connect(options);

            // The database reference is a lightweight passthrough object intended to be used and discarded
            IDatabase db = redisMultiplexer.GetDatabase();
            Console.WriteLine("Hello World!");
        }
        private static void ConfigureServices(IServiceCollection serviceCollection)
        {
            configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetParent(AppContext.BaseDirectory).FullName)
                .AddJsonFile("appsettings.json")
                .Build();

            serviceCollection.AddSingleton<IConfigurationRoot>(configuration);


        }
    }
}


Войдите в полноэкранный режим Выход из полноэкранного режима

Создайте новый файл ConfigureInitializationServices.cs с двумя методами DeleteKeys, который удаляет все данные из базы данных (если таковые имеются) и InitializeTimeSeriesTotalPageViews, который создает временной ряд для просмотров страниц с периодом хранения 10 минут для каждой точки данных / выборки.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Spectre.Console;
using NRedisTimeSeries;
using NRedisTimeSeries.DataTypes;
using NRedisTimeSeries.Commands;
using StackExchange.Redis;
namespace RedMetrixProcessor
{
    public class ConfigureInitializationServices
    {  
        private  readonly IConfigurationRoot _config;

        public ConfigureInitializationServices(IConfigurationRoot config)
        {
            _config = config;
        }

        public void DeleteKeys(IDatabase db)
        {         


                db.KeyDelete("ts_pv:t"); //TimeSeries-PageView-Total

        } 


        public void InitializeTimeSeriesTotalPageViews(IDatabase db)
        {

                db.TimeSeriesCreate("ts_pv:t", retentionTime: 600000);


        }

    }
}
Вход в полноэкранный режим Выход из полноэкранного режима
Методы в репозитории кода на github гораздо более обширны с try catch blocks и кодом Spectre.Console.

Вернитесь в Program.cs, обновите метод ConfigureServices, чтобы методы в ConfigureInitializationServices были доступны из метода main.

using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Spectre.Console;
using NRedisTimeSeries;
using NRedisTimeSeries.DataTypes;
using StackExchange.Redis;
namespace RedMetrixProcessor
{
    class Program
    {
        public static IConfigurationRoot configuration;
        static void Main(string[] args)
        {
            ServiceCollection serviceDescriptors = new ServiceCollection();

            ConfigureServices(serviceDescriptors);
            IServiceProvider serviceProvider = serviceDescriptors.BuildServiceProvider();

            var options = new ConfigurationOptions
                {
                    EndPoints = { configuration.GetSection("RedisOptions:EndPoints").Value },
                    Password = configuration.GetSection("RedisOptions:Password").Value,
                    Ssl = false
                };
            // Multiplexer is intended to be reused
            ConnectionMultiplexer redisMultiplexer = ConnectionMultiplexer.Connect(options);

            // The database reference is a lightweight passthrough object intended to be used and discarded
            IDatabase db = redisMultiplexer.GetDatabase();

            AnsiConsole.Write(new FigletText("RedMetrix").LeftAligned().Color(Color.Red));
            AnsiConsole.Write(new Markup("[bold red]Copyright(C)[/] [teal]2021 Arnab Choudhuri - Xanadu[/]"));
            Console.WriteLine("");
            var rule = new Rule("[red]Welcome to RedMetrix[/]");
            AnsiConsole.Write(rule);

            var selectedoption = AnsiConsole.Prompt(
                                    new SelectionPrompt<string>()
                                        .Title("[bold yellow]Intitialize Application[/] [red]OR[/] [green]Process Data[/]?")
                                        .PageSize(5)
                                        .AddChoices(new[]
                                        {
                                            "Initialize Application", "Process Data", "Exit"
                                        }));
            if (selectedoption.ToString()=="Exit")
            {
                return;
            }else{
                if (!AnsiConsole.Confirm(selectedoption.ToString()))
                {
                    return;
                }
                else{
                     if (selectedoption.ToString()=="Initialize Application")
                     {
                        serviceProvider.GetService<ConfigureInitializationServices>().DeleteKeys(db);
                        serviceProvider.GetService<ConfigureInitializationServices>().InitializeTimeSeriesTotalPageViews(db);

                     }

                }
            }
            redisMultiplexer.Close();
            AnsiConsole.Write(new Markup("[bold yellow]Press any key to [/] [red]Exit![/]"));
            Console.ReadKey(false);
        }
        private static void ConfigureServices(IServiceCollection serviceCollection)
        {
            configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetParent(AppContext.BaseDirectory).FullName)
                .AddJsonFile("appsettings.json")
                .Build();

            serviceCollection.AddSingleton<IConfigurationRoot>(configuration);
            serviceCollection.AddTransient<ConfigureInitializationServices>();


        }
    }
}


Вход в полноэкранный режим Выход из полноэкранного режима

Приложение должно иметь доступ к файлам макетных данных, загруженных ранее с машины разработки, на которой будет запущено это консольное приложение. Чтобы обеспечить это, пришло время добавить расположение папки для файла макетных данных в appsettings.json.

{
    "RedisOptions":{
        "EndPoints":"redis-14475.c280.us-central1-2.gce.cloud.redislabs.com:14475",
        "Password":"BVDwMO6Qj1tnDZalcxmfdH2mL1c1G5iA"
    },
    "FolderPath":"D:\redis\article\data"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Создайте новый файл DataServices.cs, который будет просматривать папку для файла/файлов макетных данных, читать каждую строку и, если тип события ‘Page View’, добавлять точку данных с текущей меткой времени и значением 1(One). Обратите внимание на метод TSPageViews.

using System;
using System.IO;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Spectre.Console;
using NRedisTimeSeries;
using StackExchange.Redis;
namespace RedMetrixProcessor
{
    public class DataServices
    {
        private  readonly IConfigurationRoot _config;

        public DataServices(IConfigurationRoot config)
        {
            _config = config;
        }

        public void ProcessData(IDatabase db)
        {
            string folderPath = _config.GetSection("FolderPath").Value;
            DirectoryInfo startDir = new DirectoryInfo(folderPath);
            var files = startDir.EnumerateFiles();
            AnsiConsole.Status()
                .AutoRefresh(true)
                .Spinner(Spinner.Known.Default)
                .Start("[yellow]Initializing ...[/]", ctx =>
                {
                    foreach (var file in files)
                    {
                        ctx.Status("[bold blue]Started Processing..[/]");
                        HandleFile(file,db);
                        AnsiConsole.MarkupLine($"[grey]LOG:[/] Done[grey]...[/]");
                    }
                });

            // Done
            AnsiConsole.MarkupLine("[bold green]Finished[/]");
        }

        private static void HandleFile(FileInfo file,IDatabase db)
        {

               Console.WriteLine(file.FullName);

              using var fs = new FileStream(file.FullName, FileMode.Open, FileAccess.Read);
              using var sr = new StreamReader(fs, Encoding.UTF8);
               // int count = 0;
                //int total =0;

                string line = String.Empty;

                while ((line = sr.ReadLine()) != null)
                {
                    try{

                        using JsonDocument doc = JsonDocument.Parse(line);
                        if(doc.RootElement.GetProperty("event_detail").GetProperty("event_type").GetString()=="Page view")
                        {
                                //total++;
                                TSPageViews(db);


                        }


                    }catch(Exception ex){
                       AnsiConsole.WriteException(ex);
                     Console.WriteLine(line); 

                    }finally{

                    }

                }
           // Console.WriteLine($"{total}");
        }   

        private static void TSPageViews( IDatabase db)
        {
            db.TimeSeriesAdd("ts_pv:t", "*", 1);
        }

    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Еще раз в Program.cs, обновите метод ConfigureServices, чтобы методы в DataServices были доступны из метода main.

serviceCollection.AddTransient<ConfigureInitializationServices>();
serviceCollection.AddTransient<DataServices>();
Вход в полноэкранный режим Выход из полноэкранного режима
if (selectedoption.ToString()=="Initialize Application")
    {
        serviceProvider.GetService<ConfigureInitializationServices>().DeleteKeys(db);
        serviceProvider.GetService<ConfigureInitializationServices>().InitializeTimeSeriesTotalPageViews(db);

    }
if (selectedoption.ToString()=="Process Data")
    {
        serviceProvider.GetService<DataServices>().ProcessData(db);
    }           
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь попробуем, что получилось, собрав и запустив приложение.

dotnet build
dotnet run
Войти в полноэкранный режим Выход из полноэкранного режима

Выберите Intitialize Application из предложенных вариантов и подтвердите выбор.

Теперь проверим, сделало ли приложение то, что должно было сделать, используя RedisInsight.

Установите и запустите RedisInsight

Выберите опцию Добавить базу данных вручную.


База данных Redis появится в списке Redis Insight

Выберите значение Database Alias -redmetrix для просмотра только что добавленного ключа.

Запустите приложение еще раз и на этот раз выберите Process Data.

Убедитесь в том, что файлы макетных данных были обработаны, выполнив приведенный ниже запрос для проверки количества точек данных/образцов в базе данных.

TS.RANGE ts_pv:t - + AGGREGATION sum 6000000
Вход в полноэкранный режим Выход из полноэкранного режима

Позже этот же запрос будет запущен через клиента .Net в веб-приложении.

Веб-приложение

Создайте базовое веб-приложение Razor Pages в терминале

dotnet new webapp -o RedMetrixWebApp
Войдите в полноэкранный режим Выйти из полноэкранного режима

Переместитесь в каталог, затем постройте и запустите, чтобы убедиться, что все работает хорошо.

В appsettings.json добавьте раздел RedisOptions, как это сделано в RedMetrixProcessor.

Добавьте необходимые пакеты nuget

dotnet add package StackExchange.Redis.Extensions.AspNetCore
dotnet add package StackExchange.Redis.Extensions.System.Text.Json
dotnet add package NRedisTimeSeries
Войдите в полноэкранный режим Выйти из полноэкранного режима

Обновите код Program.cs, чтобы разрешить доступ к appsettings.json.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace RedMetrixWebApp
{
    public class Program
    {
        private static IConfigurationRoot Configuration { get; set; }
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostContext, config) =>
               {
                   config.AddJsonFile("appsettings.json");
               })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                    webBuilder.UseUrls("http://localhost:5003");
                });


    }
}

Вход в полноэкранный режим Выйдите из полноэкранного режима

В файле Startup.cs закомментируйте строку app.UseHttpsRedirection(); так как https не будет использоваться.

Теперь RedisProvider добавлен в решение в папку Provider.

Обновите Startup.cs — метод ConfigureServices и добавьте строку

services.AddSingleton<RedisProvider>();
Войти в полноэкранный режим Выйти из полноэкранного режима
Не забудьте добавить пространство имен RedisProvider в Startup.cs.

Далее, пришло время получить данные для виджета PageViews.
Он будет показывать Total PageViews (за последнюю минуту) & сравнение с предыдущей минутой, таким образом, два значения.

Поэтому необходимо создать модель для хранения двух значений.
Создайте новую папку Model и в ней создайте файл RealTimeChart.cs.

using System;
using System.Collections.Generic;
namespace RedMetrixWebApp
{
    public class RealtimeChart
    {
        public PageView PageViews { get; set; }

        public DateTime Updated { get; set; }
    }

    public class PageView   
    {
        public PageView(long pv, long prev_pv)
        {
            this.pv=pv;
            this.prev_pv=prev_pv;
        }
        public long pv { get; set; }
        public long prev_pv{ get; set; }
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Далее создайте новую папку Services и в ней создайте файл RealTimeChartService.cs.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Linq;
using dotnetredis.Providers;
using NRedisTimeSeries;
using NRedisTimeSeries.Commands;
using NRedisTimeSeries.DataTypes;
using StackExchange.Redis;
using Microsoft.Extensions.Logging;


namespace RedMetrixWebApp
{
    public class RealtimeChartService
    {
        private readonly ILogger<RealtimeChartService> _logger;
        private readonly RedisProvider _redisProvider;

        public RealtimeChartService(ILogger<RealtimeChartService> logger, RedisProvider redisProvider)
        {
            _logger = logger;
            _redisProvider = redisProvider;
        }

        public RealtimeChart GetChartData()
        {
            return new RealtimeChart()
            {
                PageViews=GetPageViews(),
                Updated= DateTime.Now.ToLocalTime()

            };
        }


        public PageView GetPageViews()
        {
            PageView views = new PageView(0,0);
            try{
                var db = _redisProvider.Database();
                IReadOnlyList<TimeSeriesTuple> results =  db.TimeSeriesRevRange("ts_pv:t", "-", "+", aggregation: TsAggregation.Sum, timeBucket: 60000, count:2); 

              views= new PageView(Convert.ToInt64(results[0].Val), Convert.ToInt64(results[1].Val));

            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }
            return views;
        }

    }    

}
Войдите в полноэкранный режим Выйдите из полноэкранного режима
Просмотрите метод GetPageViews, чтобы понять, как метод .Net Api TimeSeriesRevRange получает данные.

Обновите Startup.cs — метод ConfigureServices и добавьте строку

services.AddTransient<RealtimeChartService>();
Войти в полноэкранный режим Выйти из полноэкранного режима

Наконец, пришло время отобразить данные, полученные из БД redis, на веб-страницах.
Обновите файл Index.cshtml.cs code-behind для получения данных с помощью RealtimeChartService.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using RedMetrixWebApp;

namespace RedMetrixWebApp.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> _logger;
        private readonly RealtimeChartService _service;

        public RealtimeChart chartdata { get; set; }

        public IndexModel(ILogger<IndexModel> logger, RealtimeChartService service)
        {
            _logger = logger;
            _service=service;
        }

        public void OnGet()
        {
           chartdata  =  _service.GetChartData();
            _logger.LogInformation("Worker running at: {Time} ", chartdata.Updated);
        }

    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, пришло время создать представление.
Данные из файла .cshtml.cs могут быть доступны через Model в .cshtml с помощью var chartData = @Html.Raw(Json.Serialize(Model.chartdata));.

Структура приборной панели построена с помощью gridstack.js.

Для дизайна виджетов используется handlebars.js

В гриде, где будет располагаться виджет pageviews, обратите внимание на наличие div с id wpageviews. Именно сюда будут вставляться шаблоны handlebar.
@page
@model IndexModel
@{Layout = null;
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="RedMetrix - RedisTimeSeries ECOM Web analytics Dashboard">
    <title>RedMetrix - A RedisTimeSeries Project</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/gridstack.css" />

    <style>
    body {
    background: #222222;
    color: #465665;
      }
    .grid-stack-item {
        border: 0;
      }
    .grid-stack-item-content {
        background-color: white;
        text-align: center;
      }
    #blue{
        background: #0000ff;
      }
    </style>
</head>
<body>

  <div class="grid-stack" id="simple-grid"></div>
  <script src="~/lib/jquery/dist/jquery.min.js"></script>
  <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
  <script src="~/js/gridstack-jq.js" ></script>
  <script type="text/javascript">
    var simple = [
      {x: 0, y: 0, w: 4, h: 2, noMove: true, noResize: true, locked: true, id:'logo', bgcolor:'#9c4274',content: '<img src="widgets/logo/logo.png" alt="logo RedMetrix" style="width:100%;margin-top: auto;"> <h2 style="color:white;"><strong> -- A RedisTimeSeries Project </strong> </h2>'},
      {x: 4, y: 0, w: 4, h: 6, noMove: true, noResize: true, locked: true, id:'pageperf', bgcolor:'#e83e8c',content: '<h5 style="color:whitesmoke">Page Performance <br/> (in ms) </h5> <div><h6 style="color:white">Home </h6><p/><canvas id ="wpageperfh"></canvas> </div><p/><div><h6 style="color:white">Product </h6></p><canvas id ="wpageperfp"></canvas> </div>'},
      {x: 8, y: 0, w: 2, h: 2,noMove: true, noResize: true, locked: true, id:'pageviews', bgcolor:'#12b0c5', content: '<div id ="wpageviews" style="width: 100%; height: 100%"></div>'},
      {x: 10, y: 0, w: 2, h: 2,noMove: true, noResize: true, locked: true, id:'totalorders', bgcolor:'#ff9618', content: '<div id ="wtotalorders" style="width: 100%; height: 100%"></div>'},
      {x: 0, y: 2, w: 4, h: 4, noMove: true, noResize: true, locked: true, id:'conversions', bgcolor:'#ec663c', content: '<h5 style="color:whitesmoke">Conversions </h5> <div><canvas id ="wconversions"></canvas> </div></p></p> <h5 style="color:whitesmoke">Conversion Rate </h5><div id="wconversionrate" style="font-size: 400%; color:lightgreen;"></div>'},
      {x: 8, y: 2, w: 4, h: 1,noMove: true, noResize: true, locked: true, id:'ordervalue', bgcolor:'#dc5945', content: '<div id ="wordervalue" style="width: 100%; height: 100%"></div>'},
      {x: 8, y: 3, w: 4, h: 3,noMove: true, noResize: true, locked: true, id:'ordbypaymeth', bgcolor:'#47bbb3', content: '<h5 style="color:whitesmoke">Orders By Payment Method </h5> <div><canvas id ="wordbypaymeth"></canvas> </div>'}
    ];

    var simpleGrid = GridStack.init({

    }, '#simple-grid');
    simpleGrid.on('added', function(e, items) {
      items.forEach(function(item) { 
        item.el.firstElementChild.style.backgroundColor=item.bgcolor;
      });
    });
    simpleGrid.load(simple);
  </script>

  <script>
  Handlebars.registerHelper('isdefined', function (value) {
    return value !== undefined;
    });
  Handlebars.registerHelper('isnumber', function (value) {
    return !isNaN(value);
    });
  Handlebars.registerHelper('showchange', function (value) {
    return value !== undefined || !isNaN(value) || value !==0;
    });

  </script>
  <script id="pageview_template" type="text/x-handlebars-template">
    <div class="entry">
      <h5 style="color:whitesmoke">{{pageview.title}}</h5>
      <div class="pageview" style="width: 100%; height: 83%">
        <div class= "views" style="font-size: 400%; color:#ff9618;">{{pageview.views}}</div>
        <div class= "views"><h5 style="color:#ff9618">Last value : {{pageview.last}}</h5></div>
        <div class="change" style="color:white"><h3>{{#if (showchange pageview.diff)}} <span>({{pageview.change}}) {{pageview.diff}}%</span>{{/if}} </h3></div>
      </div>
    </div>
  </script>
  <script>
  var pageview_source = document.getElementById("pageview_template").innerHTML;
  var pageviewTemplate = Handlebars.compile(pageview_source);

  </script> 
  <script>
    function percentage(n, total) {
    return Math.round(n * 100 / total);
  }
  function shortnumber(num) {
    if (isNaN(num)) {
      return num;
    }
    if (num >= 1000000000) {
      return (num / 1000000000).toFixed(1) + 'B';
    } else if (num >= 1000000) {
      return (num / 1000000).toFixed(1) + 'M';
    } else if (num >= 1000) {
      return (num / 1000).toFixed(1) + 'K';
    } else {
      return num;
    }
  };
  $(document).ready(function () {
  var chartData = @Html.Raw(Json.Serialize(Model.chartdata));
  console.log(chartData);
  var pageviews=0;
  var lastpv=0;
  var pageviewdiff=0;
  var pageviewchange="";
      if (!isNaN(chartData.pageViews.pv)){
            pageviews=chartData.pageViews.pv;
            if (!isNaN(chartData.pageViews.prev_pv)){
              lastpv=chartData.pageViews.prev_pv;
                var diff=0;
                if(chartData.pageViews.prev_pv>chartData.pageViews.pv){
                    diff=chartData.pageViews.prev_pv-chartData.pageViews.pv;
                    pageviewchange="-"
                }else{
                    diff=chartData.pageViews.pv-chartData.pageViews.prev_pv;
                    pageviewchange="+";
                }
                pageviewdiff=percentage(diff,pageviews);

              }
          }

        var context={
            pageview: {
            title:"Page Views",  
            views: shortnumber(pageviews),
            last:shortnumber(lastpv),
            change: pageviewchange,
            diff:pageviewdiff
            }
        };
        document.getElementById("wpageviews").innerHTML = pageviewTemplate(context);

  });      

  </script>  
</body>
</html>
Вход в полноэкранный режим Выход из полноэкранного режима

Результат

Остальные виджеты

Общее количество заказов / транзакций и общая стоимость заказа (в рупиях)

В отличие от случая использования Total page views, где показывались просмотры страниц за последние минуты, в данном случае могут быть случаи, когда необходимо показать общую стоимость с начала бизнеса или даже текущего финансового года.

Процессор

Добавьте этот раздел в файл Processor appsettings.json. Здесь будут указаны значения указанных данных на момент запуска приложения.

"InitializationOptions":{
        "TotalOrders":0,
        "TotalOrderValue":0.00
    }    
Войти в полноэкранный режим Выход из полноэкранного режима
В реальных сценариях вы бы сделали Api вызов к вашим корпоративным системам, чтобы получить значения.

В ConfigureInitializationServices.cs создайте метод, который будет создавать необходимые ключи и добавлять значения из appsettings.json.

public void InitializeTimeSeriesTotalOrderNValue(IDatabase db)
        {
                db.TimeSeriesCreate("ts_o:t", retentionTime: 600000);
                ulong totalorders=0;
                // TODO: Get Data from Rest Api for total orders.
                totalorders=Convert.ToUInt64(_config.GetSection("InitializationOptions:TotalOrders").Value);
                db.TimeSeriesAdd("ts_o:t", "*", Convert.ToDouble(totalorders));  
                db.TimeSeriesCreate("ts_o:v", retentionTime: 600000);
                double totalordervalue=0;
                // TODO: Get Data from Rest Api for total order value
                totalordervalue=Convert.ToDouble(_config.GetSection("InitializationOptions:TotalOrderValue").Value);
                db.TimeSeriesAdd("ts_o:v", "*", totalordervalue);


        }
Вход в полноэкранный режим Выход из полноэкранного режима
Не забудьте также обновить метод DeleteKeys.

Аналогично в DataServices.cs добавьте следующий метод и вызовите его в методе HandleFile со значением orderamount, когда тип страницы Success Confirmation.

Метод HandleFile здесь не показан. При необходимости проверьте репозиторий кода.
private static void TSOrders( double orderamount, IDatabase db)
        {
           db.TimeSeriesIncrBy("ts_o:t", 1, timestamp: "*");
           db.TimeSeriesIncrBy("ts_o:v", orderamount, timestamp: "*");
        }
Вход в полноэкранный режим Выйти из полноэкранного режима
Соответствующей командой для CLI будет TS.INCRBY ts_o:t 1 TIMESTAMP *.

Webapp

Обновите RealtimeChartService с методом, который использует TS.GET для получения последних данных в выборках временного ряда.

public double GetOrderValue()
        {
            double orderamount = 0;
            try{
                var db = _redisProvider.Database();
                TimeSeriesTuple value = db.TimeSeriesGet("ts_o:v");
                orderamount = value.Val;
            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }
            return orderamount;
        }

        public long GetOrders()
        {
            long orders = 0;
            try{
                var db = _redisProvider.Database();
                TimeSeriesTuple value = db.TimeSeriesGet("ts_o:t");
                orders=Convert.ToInt64(value.Val);
            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }
            return orders;
        }
Вход в полноэкранный режим Выход из полноэкранного режима

Производительность страницы для главной страницы и страницы продукта (за последние минуты)

Процессор

Инициализация

db.TimeSeriesCreate("ts_pv:pp:h", retentionTime: 600000);
db.TimeSeriesCreate("ts_pv:pp:p", retentionTime: 600000);
Вход в полноэкранный режим Выход из полноэкранного режима

и затем…

private static ulong GetStartTime( JsonElement context)
        {
            string s_starttime=context.GetProperty("performance_timing").GetProperty("requestStart").GetString();
            ulong starttime=Convert.ToUInt64(s_starttime);
            return starttime;                           
        }
        private static ulong GetEndTime( JsonElement context)
        {
            string s_endtime=context.GetProperty("performance_timing").GetProperty("domContentLoadedEventEnd").GetString();
            ulong endtime=Convert.ToUInt64(s_endtime);
            return endtime;    
        }

        private static void TSPagePerformance( string pagetype,long pageperf, IDatabase db)
        {
            if (pagetype=="Home"){
                db.TimeSeriesAdd("ts_pv:pp:h", "*", pageperf);
            }else if(pagetype=="Product"){
                db.TimeSeriesAdd("ts_pv:pp:p", "*", pageperf);
            }else{}
        }
Войти в полноэкранный режим Выйти из полноэкранного режима
В исходном коде в исходном репозитории есть два дополнительных закомментированных метода для получения времени начала и времени окончания с семью атрибутами производительности вместо двух.

Webapp

Здесь мы покажем производительность страницы с плавающими полосами, обозначающими максимальное и минимальное значения, в то время как avg будет линейным графиком.
В модели RealtimeChart

public class PagePerf
    {
        public List<string> time {get;set;}
        public List<List<int>> maxmin {get;set;}
        public List<int> avg{get;set;}
    }
Войдите в полноэкранный режим Выйдите из полноэкранного режима

и в RealtimeChartService

public PagePerf GetPagePerformance(string pagetype){
            string key="";
            if (pagetype=="Home")
            {
                key="ts_pv:pp:h";
            }else if(pagetype=="Product")
            {
                key="ts_pv:pp:p";
            }else{}
            List<int> avgdata=new List<int>();
            List<string> timedata= new List<string>();
            List<List<int>> maxmindata= new List<List<int>>();
            try{
                var db = _redisProvider.Database();

                IReadOnlyList<TimeSeriesTuple> resultsmax =  db.TimeSeriesRange(key, "-", "+", aggregation: TsAggregation.Max, timeBucket: 60000, count:10);
                IReadOnlyList<TimeSeriesTuple> resultsmin =  db.TimeSeriesRange(key, "-", "+", aggregation: TsAggregation.Min, timeBucket: 60000, count:10);
                IReadOnlyList<TimeSeriesTuple> resultsavg =  db.TimeSeriesRange(key, "-", "+", aggregation: TsAggregation.Avg, timeBucket: 60000, count:10);
                maxmindata=GetMaxMinList(resultsmax,resultsmin);
                foreach (var result in resultsavg)
                {
                  avgdata.Add(Convert.ToInt32(result.Val));
                }
                timedata=GetStringTimeList(resultsavg);

            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }
            return new PagePerf
            {
                time=timedata,
                maxmin=maxmindata,
                avg=avgdata
            };
        }

        private List<List<int>> GetMaxMinList(IReadOnlyList<TimeSeriesTuple> resultsmax,IReadOnlyList<TimeSeriesTuple> resultsmin)
        {
            return resultsmax.Concat(resultsmin)
                                .GroupBy(o => o.Time)
                                .Select(g => g.Select(s => (int)s.Val).ToList())
                                .ToList();
        }

        private List<string> GetStringTimeList(IReadOnlyList<TimeSeriesTuple> resultsavg)
        {
             List<string> timedata= new List<string>();
             foreach (var result in resultsavg)
                {
                   TimeStamp ts = result.Time;
                   System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
                   dtDateTime = dtDateTime.AddMilliseconds((long)ts);
                   String hourMinute = dtDateTime.ToString("HH:mm");
                   timedata.Add(hourMinute);
                }
                return timedata;
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Воронка конверсии (в течение последних минут)

Процессор

Инициализировать

var label = new TimeSeriesLabel("chart", "Funnel");
                var labels = new List<TimeSeriesLabel> { label };
                db.TimeSeriesCreate("ts_fnl:pl", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:pl", "*", 0);
                db.TimeSeriesCreate("ts_fnl:pd", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:pd", "*", 0);
                db.TimeSeriesCreate("ts_fnl:ac", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:ac", "*", 0);
                db.TimeSeriesCreate("ts_fnl:vc", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:vc", "*", 0);
                db.TimeSeriesCreate("ts_fnl:co", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:co", "*", 0);
                db.TimeSeriesCreate("ts_fnl:sc", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_fnl:sc", "*", 0);
Вход в полноэкранный режим Выйти из полноэкранного режима
Соответствующей командой будет TS.CREATE ts_fnl:pl RETENTION 600000 LABELS chart Funnel.

И затем

private static void TSFunnel( string funneltype,IDatabase db)
        {
            switch (funneltype)
                {
                    case "Success":

                       db.TimeSeriesAdd("ts_fnl:sc", "*", 1);
                        break;
                    case "Checkout":

                        db.TimeSeriesAdd("ts_fnl:co", "*", 1);
                        break;
                    case "ViewCart":

                        db.TimeSeriesAdd("ts_fnl:vc", "*", 1);
                        break;
                    case "AddToCart":

                        db.TimeSeriesAdd("ts_fnl:ac", "*", 1);
                        break;
                    case "ProductDetail":

                        db.TimeSeriesAdd("ts_fnl:pd", "*", 1);
                        break;
                    case "ProductList":

                        db.TimeSeriesAdd("ts_fnl:pl", "*", 1);
                        break;
                    default:
                         AnsiConsole.MarkupLine($"[red]{funneltype}[/]");
                        break;
                }
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Webapp

Это будет показано в виде горизонтальной гистограммы.
В модели RealtimeChart

public class Conversion
    {
        public List<FunnelItem> FunnelItems{get;set;}
        public long TotalFunnelValue{get;set;}
       // public double ConversionRate{get;set;}
    }
    public class FunnelItem
    {
        public FunnelItem(int Order, string Item, long Value)
    {
        this.Order=Order;
        this.Item = Item;
        this.Value = Value;        
    }
       public int Order { get; set; }
       public string Item { get; set; }
       public long Value { get; set; }

    }
Войти в полноэкранный режим Выйти из полноэкранного режима

И в RealtimeChartService

public Conversion GetConversions(){
            List<FunnelItem> funnelItems = new List<FunnelItem>();
            long totalFunnelValue =0;
            try{
                var db = _redisProvider.Database();
                var filter = new List<string> { "chart=Funnel" };

               var results= db.TimeSeriesMRevRange("-", "+", filter, aggregation:TsAggregation.Sum, timeBucket:600000, count: 1);

                foreach (var result in results)
                {
                    string key = result.key;
                    IReadOnlyList<TimeSeriesTuple> values = result.values;
                    funnelItems.Add(new FunnelItem(GetFunnelOrder(key),PrettyFunnelItem(key),Convert.ToInt64(values[0].Val)));
                    totalFunnelValue=totalFunnelValue+Convert.ToInt64(values[0].Val);
                }
            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }
            return new Conversion
            {
                FunnelItems=funnelItems,
                TotalFunnelValue=totalFunnelValue
            };
        }

        private int GetFunnelOrder(string key){
           switch (key)
                {
                    case "ts_fnl:sc":
                        return 6;
                    case "ts_fnl:co":
                        return 5;
                    case "ts_fnl:vc":
                        return 4;
                    case "ts_fnl:ac":
                        return 3;
                    case "ts_fnl:pd":
                        return 2;
                    case "ts_fnl:pl":
                        return 1;
                    default:
                        _logger.LogInformation(key);
                    break;
                } 
            return 0;
        }
        private string PrettyFunnelItem(string key){
           switch (key)
                {
                    case "ts_fnl:sc":
                        return "Transaction Success";
                    case "ts_fnl:co":
                        return "Checkout";
                    case "ts_fnl:vc":
                        return "View Cart";
                    case "ts_fnl:ac":
                        return "Add To Cart";
                    case "ts_fnl:pd":
                        return "Product Detail";
                    case "ts_fnl:pl":
                        return "Product Listings";
                    default:
                        _logger.LogInformation(key);
                    break;
                } 
            return "";
        }
Войти в полноэкранный режим Выйти из полноэкранного режима
Используется команда TS.MREVRANGE - + AGGREGATION sum 600000 FILTER chart=Funnel.

Заказы по способу оплаты (за последнюю минуту)

Процессор

Инициализировать

var label = new TimeSeriesLabel("chart", "Ordersbypaymenttype");
                var labels = new List<TimeSeriesLabel> { label };
                db.TimeSeriesCreate("ts_o:t:cod", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:cod", "*", 0);
                db.TimeSeriesCreate("ts_o:t:dc", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:dc", "*", 0);
                db.TimeSeriesCreate("ts_o:t:cc", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:cc", "*", 0);
                db.TimeSeriesCreate("ts_o:t:nb", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:nb", "*", 0);
                db.TimeSeriesCreate("ts_o:t:ap", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:ap", "*", 0);
                db.TimeSeriesCreate("ts_o:t:gp", retentionTime: 600000, labels: labels);
                db.TimeSeriesAdd("ts_o:t:gp", "*", 0);
Вход в полноэкранный режим Выход из полноэкранного режима

Затем

private static void TSOrdersbypaymenttype( string paymentmethod, IDatabase db)
        {
           switch (paymentmethod)
                {
                    case "Cash On Delivery":

                       db.TimeSeriesAdd("ts_o:t:cod", "*", 1);
                        break;
                    case "Debit Card":

                        db.TimeSeriesAdd("ts_o:t:dc", "*", 1);
                        break;
                    case "Credit Card":

                        db.TimeSeriesAdd("ts_o:t:cc", "*", 1);
                        break;
                    case "Netbanking":

                        db.TimeSeriesAdd("ts_o:t:nb", "*", 1);
                        break;
                    case "Amazon Pay":

                        db.TimeSeriesAdd("ts_o:t:ap", "*", 1);
                        break;
                    case "Google Pay":

                        db.TimeSeriesAdd("ts_o:t:gp", "*", 1);
                        break;
                    default:
                         AnsiConsole.MarkupLine($"[red]{paymentmethod}[/]");
                        break;
                }
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Webapp

В модели RealtimeChart

public class PaymentMethodOrders
    {
        public PaymentMethodOrders(string PaymentMethod, long Orders)
        {
        this.PaymentMethod = PaymentMethod;
        this.Orders = Orders;
        }
       public string PaymentMethod { get; set; }
       public long Orders { get; set; }
    } 
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем в RealtimeChartService

public List<PaymentMethodOrders> GetOrdersByPaymentMethod(){
            List<PaymentMethodOrders> OrdersByPaymentMethod = new List<PaymentMethodOrders>();
            try{
                var db = _redisProvider.Database();
                var filter = new List<string> { "chart=Ordersbypaymenttype" };

               var results= db.TimeSeriesMRevRange("-", "+", filter, aggregation:TsAggregation.Sum, timeBucket:600000, count: 1);
            //      string jsonString = JsonSerializer.Serialize(results);
            //   _logger.LogInformation(jsonString);
                foreach (var result in results)
                {
                    string key = result.key;
                   IReadOnlyList<TimeSeriesTuple> values = result.values;
                    OrdersByPaymentMethod.Add(new PaymentMethodOrders(PrettyPaymentMethod(key),Convert.ToInt64(values[0].Val)));
                }
            }catch(Exception ex){
                _logger.LogError(ex.Message);
            }

            return OrdersByPaymentMethod;
        }

        private string PrettyPaymentMethod(string key){
           switch (key)
                {
                    case "ts_o:t:cod":
                        return "Cash On Delivery";
                    case "ts_o:t:dc":
                        return "Debit Card";
                    case "ts_o:t:cc":
                        return "Credit Card";
                    case "ts_o:t:nb":
                        return "Net Banking";
                    case "ts_o:t:ap":
                        return "Amazon Pay";
                    case "ts_o:t:gp":
                        return "Google Pay";
                    default:
                        _logger.LogInformation(key);
                    break;
                } 
            return "";
        }
Войти в полноэкранный режим Выйти из полноэкранного режима

Окончательный вид

Для построения графиков используется Chart.js.
Приборная панель обновляется с помощью meta http-equiv="refresh".

@page
@model IndexModel
@{Layout = null;
}
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="refresh" content="60" />
    <meta name="description" content="RedMetrix - RedisTimeSeries ECOM Web analytics Dashboard">
    <title>RedMetrix - A RedisTimeSeries Project</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/gridstack.css" />

    <style>
    body {
    background: #222222;
    color: #465665;
      }
    .grid-stack-item {
        border: 0;
      }
    .grid-stack-item-content {
        background-color: white;
        text-align: center;
      }
    #blue{
        background: #0000ff;
      }
    </style>
</head>
<body>

  <div class="grid-stack" id="simple-grid"></div>
  <script src="~/lib/jquery/dist/jquery.min.js"></script>
  <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.js"></script>
  <script src="//cdn.jsdelivr.net/npm/chart.js"></script>
  <script src="~/js/gridstack-jq.js" ></script>
  <script type="text/javascript">
    var simple = [
      {x: 0, y: 0, w: 4, h: 2, noMove: true, noResize: true, locked: true, id:'logo', bgcolor:'#9c4274',content: '<img src="widgets/logo/logo.png" alt="logo RedMetrix" style="width:100%;margin-top: auto;"> <h2 style="color:white;"><strong> -- A RedisTimeSeries Project </strong> </h2>'},
      {x: 4, y: 0, w: 4, h: 6, noMove: true, noResize: true, locked: true, id:'pageperf', bgcolor:'#e83e8c',content: '<h5 style="color:whitesmoke">Page Performance <br/> (in ms) </h5> <div><h6 style="color:white">Home </h6><p/><canvas id ="wpageperfh"></canvas> </div><p/><div><h6 style="color:white">Product </h6></p><canvas id ="wpageperfp"></canvas> </div>'},
      {x: 8, y: 0, w: 2, h: 2,noMove: true, noResize: true, locked: true, id:'pageviews', bgcolor:'#12b0c5', content: '<div id ="wpageviews" style="width: 100%; height: 100%"></div>'},
      {x: 10, y: 0, w: 2, h: 2,noMove: true, noResize: true, locked: true, id:'totalorders', bgcolor:'#ff9618', content: '<div id ="wtotalorders" style="width: 100%; height: 100%"></div>'},
      {x: 0, y: 2, w: 4, h: 4, noMove: true, noResize: true, locked: true, id:'conversions', bgcolor:'#ec663c', content: '<h5 style="color:whitesmoke">Conversions </h5> <div><canvas id ="wconversions"></canvas> </div></p></p> <h5 style="color:whitesmoke">Conversion Rate </h5><div id="wconversionrate" style="font-size: 400%; color:lightgreen;"></div>'},
      {x: 8, y: 2, w: 4, h: 1,noMove: true, noResize: true, locked: true, id:'ordervalue', bgcolor:'#dc5945', content: '<div id ="wordervalue" style="width: 100%; height: 100%"></div>'},
      {x: 8, y: 3, w: 4, h: 3,noMove: true, noResize: true, locked: true, id:'ordbypaymeth', bgcolor:'#47bbb3', content: '<h5 style="color:whitesmoke">Orders By Payment Method </h5> <div><canvas id ="wordbypaymeth"></canvas> </div>'}
    ];

    var simpleGrid = GridStack.init({

    }, '#simple-grid');
    simpleGrid.on('added', function(e, items) {
      items.forEach(function(item) { 
        item.el.firstElementChild.style.backgroundColor=item.bgcolor;
      });
    });
    simpleGrid.load(simple);
  </script>

  <script>
  Handlebars.registerHelper('isdefined', function (value) {
    return value !== undefined;
    });
  Handlebars.registerHelper('isnumber', function (value) {
    return !isNaN(value);
    });
  Handlebars.registerHelper('showchange', function (value) {
    return value !== undefined || !isNaN(value) || value !==0;
    });

  </script>
  <script id="pageview_template" type="text/x-handlebars-template">
    <div class="entry">
      <h5 style="color:whitesmoke">{{pageview.title}}</h5>
      <div class="pageview" style="width: 100%; height: 83%">
        <div class= "views" style="font-size: 400%; color:#ff9618;">{{pageview.views}}</div>
        <div class= "views"><h5 style="color:#ff9618">Last value : {{pageview.last}}</h5></div>
        <div class="change" style="color:white"><h3>{{#if (showchange pageview.diff)}} <span>({{pageview.change}}) {{pageview.diff}}%</span>{{/if}} </h3></div>
      </div>
    </div>
  </script>
  <script id="totalorders_template" type="text/x-handlebars-template">
    <div class="entry">
      <h5 style="color:whitesmoke">{{totalorders.title}}</h5>
      <div class="pageview" style="width: 100%; height: 83%">
        <div class= "views" style="font-size: 500%; color:#12b0c5">{{totalorders.orders}}</div>
      </div>
    </div>
  </script>
  <script id="totalsales_template" type="text/x-handlebars-template">
    <div class="entry">
      <h5 style="color:whitesmoke">{{totalordervalue.title}}</h5>
      <div class="pageview" style="width: 100%; height: 83%">
        <div class= "views"><h1 style="color:#ff9618";  font-size: 250%;>&#8377; {{totalordervalue.orderValue}}</h1></div>
      </div>
    </div>
  </script>
  <script>
  var pageview_source = document.getElementById("pageview_template").innerHTML;
  var pageviewTemplate = Handlebars.compile(pageview_source);
  var totalorders_source = document.getElementById("totalorders_template").innerHTML;
  var totalordersTemplate = Handlebars.compile(totalorders_source);
  var totalsales_source = document.getElementById("totalsales_template").innerHTML;
  var totalsalesTemplate = Handlebars.compile(totalsales_source);
  </script> 
  <script>
    function percentage(n, total) {
    return Math.round(n * 100 / total);
  }
  function shortnumber(num) {
    if (isNaN(num)) {
      return num;
    }
    if (num >= 1000000000) {
      return (num / 1000000000).toFixed(1) + 'B';
    } else if (num >= 1000000) {
      return (num / 1000000).toFixed(1) + 'M';
    } else if (num >= 1000) {
      return (num / 1000).toFixed(1) + 'K';
    } else {
      return num;
    }
  };
  var orderbypaychartdata= {
    labels: [],
    datasets: [{
      label: 'Orders',
      data: [],
      borderColor: 'rgb(255, 99, 132)',
      backgroundColor: 'rgb(255, 99, 132)',
      borderWidth: 2,
      borderRadius: 5,
      borderSkipped: false,
      order: 1
    }]
   };
var orderbypaychartconfig = {
    type: 'bar',
    data: orderbypaychartdata,
    options: {
      responsive: true,
      plugins: {
        legend: {
            display: false,
        }
      }
    }
  };

var orderbypayChart = new Chart(
  document.getElementById('wordbypaymeth'),
  orderbypaychartconfig
);

const pageperfhomechartdata = {
  labels: [],
  datasets: [
    {
      label: 'Max-Min',
      data: [],
      borderColor: 'rgb(54, 162, 235)',
      backgroundColor: 'rgb(54, 162, 235)',
      borderWidth: 2,
      borderRadius: 5,
      borderSkipped: false,
      order: 1
    },
    {
      label: 'Avg',
      data: [],
      borderColor: 'rgb(255, 205, 86)',
      backgroundColor: 'rgb(255, 205, 86)',
      type: 'line',
      order: 0
    }]
};

const pageperfhomechartconfig = {
  type: 'bar',
  data: pageperfhomechartdata,
  options: {
    responsive: true,
    plugins: {
      legend: {
        display: false,
      },
      title: {
        display: false,
        text: 'Home Page Performance'
      }
    }
  }
};

var pageperfhomeChart = new Chart(
  document.getElementById('wpageperfh'),
  pageperfhomechartconfig
);

const pageperfproductchartdata = {
  labels: [],
  datasets: [
    {
      label: 'Max-Min',
      data: [],
      borderColor: 'rgb(255, 205, 86)',
      backgroundColor: 'lightgreen',
      borderWidth: 2,
      borderRadius: 5,
      borderSkipped: false,
      order: 1
    },
    {
      label: 'Avg',
      data: [],
      borderColor: 'rgb(54, 162, 235)',
      backgroundColor: 'rgb(54, 162, 235)',
      type: 'line',
      order: 0
    }]
};

const pageperfproductchartconfig = {
  type: 'bar',
  data: pageperfproductchartdata,
  options: {
    responsive: true,
    plugins: {
      legend: {
        display: false,
      },
      title: {
        display: false,
        text: 'Product Page Performance'
      }
    }
  }
};

var pageperfproductChart = new Chart(
  document.getElementById('wpageperfp'),
  pageperfproductchartconfig
);

var conversionchartdata= {
  axis: 'y',
  labels: [],
  datasets: [{
    label: 'Value',
    data: [],
    borderColor: 'rgb(54, 162, 235)',
    backgroundColor: 'rgb(54, 162, 235)',
    borderWidth: 2,
    borderRadius: 5,
    borderSkipped: false,
    order: 1
  }]
 };
var conversionchartconfig = {
  type: 'bar',
  data: conversionchartdata,
  options: {
    indexAxis: 'y',
    responsive: true,
    plugins: {
      legend: {
          display: false,
      }
    }
  }
};

var conversionChart = new Chart(
document.getElementById('wconversions'),
conversionchartconfig
);
  $(document).ready(function () {
  var chartData = @Html.Raw(Json.Serialize(Model.chartdata));
  console.log(chartData);
  var pageviews=0;
  var lastpv=0;
  var pageviewdiff=0;
  var pageviewchange="";
  var totalorders=0;
  var totalsales=0;
      if (!isNaN(chartData.pageViews.pv)){
            pageviews=chartData.pageViews.pv;
            if (!isNaN(chartData.pageViews.prev_pv)){
              lastpv=chartData.pageViews.prev_pv;
                var diff=0;
                if(chartData.pageViews.prev_pv>chartData.pageViews.pv){
                    diff=chartData.pageViews.prev_pv-chartData.pageViews.pv;
                    pageviewchange="-"
                }else{
                    diff=chartData.pageViews.pv-chartData.pageViews.prev_pv;
                    pageviewchange="+";
                }
                pageviewdiff=percentage(diff,pageviews);

              }
          }
        if (!isNaN(chartData.orders)){
          totalorders =chartData.orders
        }
        if (!isNaN(chartData.orderValue)){
          totalsales =chartData.orderValue
        }        
        var context={
            pageview: {
            title:"Page Views",  
            views: shortnumber(pageviews),
            last:shortnumber(lastpv),
            change: pageviewchange,
            diff:pageviewdiff
            },
            totalorders: {
            title:"Transactions",  
            orders: shortnumber(totalorders)
            },
            totalordervalue: {
            title:"Total Sales",  
            orderValue: shortnumber(totalsales)
            },
        };
        document.getElementById("wpageviews").innerHTML = pageviewTemplate(context);
        document.getElementById("wtotalorders").innerHTML =totalordersTemplate(context);
        document.getElementById("wordervalue").innerHTML=totalsalesTemplate(context);
        const updatedorderbypaychart = Chart.getChart("wordbypaymeth");
        updatedorderbypaychart.data.labels=chartData.ordersByPaymentMethod.map((item) =>item.paymentMethod);
        updatedorderbypaychart.data.datasets[0].data=chartData.ordersByPaymentMethod.map((item)=>item.orders);
        updatedorderbypaychart.update();

        const updatedpageperfhomechart = Chart.getChart("wpageperfh");
        updatedpageperfhomechart.data.labels=chartData.pagePerformanceHome.time;
        updatedpageperfhomechart.data.datasets[0].data=chartData.pagePerformanceHome.maxmin;
        updatedpageperfhomechart.data.datasets[1].data=chartData.pagePerformanceHome.avg;
        updatedpageperfhomechart.update();

        const updatedpageperfproductchart = Chart.getChart("wpageperfp");
        updatedpageperfproductchart.data.labels=chartData.pagePerformanceProduct.time;
        updatedpageperfproductchart.data.datasets[0].data=chartData.pagePerformanceProduct.maxmin;
        updatedpageperfproductchart.data.datasets[1].data=chartData.pagePerformanceProduct.avg;
        updatedpageperfproductchart.update();

        var funneldata =chartData.conversions.funnelItems.sort(function (a, b) {
          return a.order - b.order;
        });
        var funneldataVal=funneldata.map((item)=>item.value);
        const updatedconversionschart = Chart.getChart("wconversions");
        updatedconversionschart.data.labels=funneldata.map((item) =>item.item);
        updatedconversionschart.data.datasets[0].data=funneldata.map((item)=>item.value);
        updatedconversionschart.update();
        document.getElementById("wconversionrate").innerHTML = percentage(funneldataVal[funneldataVal.length-1],funneldataVal[0]) + "%";
  });      

  </script>  
</body>
</html>
Вход в полноэкранный режим Выйти из полноэкранного режима

И в результате

RedMetrixWebApp также содержит файл docker, который должен помочь в легком запуске приложения.

Заключительные заметки

RedisTimeSeries : https://redis.io/docs/stack/timeseries/
Команды RedisTimeSeries : https://redis.io/commands/?group=timeseries
NRedisTimeSeries — .Net клиент для RedisTimeSeries : https://github.com/RedisTimeSeries/NRedisTimeSeries/
Redis Cloud : https://redis.com/try-free/
Mockaroo — Для имитации данных : https://www.mockaroo.com/7d448f50
RedisProvider : https://github.com/redis-developer/nredi2read-preview/blob/main/Providers/RedisProvider.cs
gridstack.js — Для структуры приборной панели : https://github.com/gridstack/gridstack.js
handlebars.js — Для дизайна виджетов : https://github.com/handlebars-lang/handlebars.js
Chart.js — Библиотека для построения графиков : https://github.com/chartjs/Chart.js
Github Code Repository — Redmetrix : https://github.com/c-arnab/Redmetrix

Этот пост написан в сотрудничестве с Redis

Оцените статью
devanswers.ru
Добавить комментарий