- В этом посте мы рассмотрим модуль RedisTimeSeries и построим пользовательский инструмент веб-аналитики с Redis & .NET 5 с помощью C#.
- Обзор архитектуры приложения
- Зачем использовать модуль RedisTimeSeries
- Архитектура приложения
- Диаграммы в Dashboard
- Демонстрационная инфраструктура
- Создайте базу данных на Redis Cloud
- Генерирование макетов данных
- Разработка Redmetrix
- Процессор
- Веб-приложение
- Остальные виджеты
- Общее количество заказов / транзакций и общая стоимость заказа (в рупиях)
- Процессор
- Webapp
- Производительность страницы для главной страницы и страницы продукта (за последние минуты)
- Процессор
- Webapp
- Воронка конверсии (в течение последних минут)
- Процессор
- Webapp
- Заказы по способу оплаты (за последнюю минуту)
- Процессор
- Webapp
- Окончательный вид
- Заключительные заметки
В этом посте мы рассмотрим модуль 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, чтобы увидеть тот, который будет использоваться здесь.
Используются следующие атрибуты
- application.app_id — Id приложения, может быть строкой или даже числом.
- application.platform — Обозначение веб-сайта или мобильного приложения.
- date_time.collector_tstamp_dt — Время, когда событие достигло уровня обмена сообщениями или было обогащено.
- event_detail.event_type — Просмотр страницы, изменение/отправка формы или Ecom Trasaction через событие
- event_detail.event_id — Id события — Guid
- contexts.page.type — Главная/Категория/Продукт и т.д..
- contexts.performance_timing.domContentLoadedEventEnd — Время окончания загрузки страницы
- contexts.performance_timing.requestStart — Время начала загрузки страницы
- contexts.transaction.amount_to_pay — Сумма транзакции
- 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%;>₹ {{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