Files
DnsServer/DnsServerCore.HttpApi/Models/DashboardStats.cs

360 lines
13 KiB
C#

/*
Technitium DNS Server
Copyright (C) 2025 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using System.Linq;
namespace DnsServerCore.HttpApi.Models
{
public enum DashboardStatsType
{
Unknown = 0,
LastHour = 1,
LastDay = 2,
LastWeek = 3,
LastMonth = 4,
LastYear = 5,
Custom = 6
}
public enum DashboardTopStatsType
{
Unknown = 0,
TopClients = 1,
TopDomains = 2,
TopBlockedDomains = 3
}
public class DashboardStats
{
public StatsData? Stats { get; set; }
public ChartData? MainChartData { get; set; }
public ChartData? QueryResponseChartData { get; set; }
public ChartData? QueryTypeChartData { get; set; }
public ChartData? ProtocolTypeChartData { get; set; }
public TopClientStats[]? TopClients { get; set; }
public TopStats[]? TopDomains { get; set; }
public TopStats[]? TopBlockedDomains { get; set; }
public void Merge(DashboardStats other, int limit)
{
if ((Stats is not null) && (other.Stats is not null))
Stats.Merge(other.Stats);
if ((MainChartData is not null) && (other.MainChartData is not null))
MainChartData = ChartData.Merge(MainChartData, other.MainChartData, false);
if ((QueryResponseChartData is not null) && (other.QueryResponseChartData is not null))
QueryResponseChartData = ChartData.Merge(QueryResponseChartData, other.QueryResponseChartData, false);
if ((QueryTypeChartData is not null) && (other.QueryTypeChartData is not null))
QueryTypeChartData = ChartData.Merge(QueryTypeChartData, other.QueryTypeChartData, true);
if ((ProtocolTypeChartData is not null) && (other.ProtocolTypeChartData is not null))
ProtocolTypeChartData = ChartData.Merge(ProtocolTypeChartData, other.ProtocolTypeChartData, true);
if ((TopClients is not null) && (other.TopClients is not null))
TopClients = TopStats.Merge(TopClients, other.TopClients, limit);
if ((TopDomains is not null) && (other.TopDomains is not null))
TopDomains = TopStats.Merge(TopDomains, other.TopDomains, limit);
if ((TopBlockedDomains is not null) && (other.TopBlockedDomains is not null))
TopBlockedDomains = TopStats.Merge(TopBlockedDomains, other.TopBlockedDomains, limit);
}
public class StatsData
{
public long TotalQueries { get; set; }
public long TotalNoError { get; set; }
public long TotalServerFailure { get; set; }
public long TotalNxDomain { get; set; }
public long TotalRefused { get; set; }
public long TotalAuthoritative { get; set; }
public long TotalRecursive { get; set; }
public long TotalCached { get; set; }
public long TotalBlocked { get; set; }
public long TotalDropped { get; set; }
public long TotalClients { get; set; }
public int Zones { get; set; }
public long CachedEntries { get; set; }
public int AllowedZones { get; set; }
public int BlockedZones { get; set; }
public int AllowListZones { get; set; }
public int BlockListZones { get; set; }
public void Merge(StatsData statsData)
{
TotalQueries += statsData.TotalQueries;
TotalNoError += statsData.TotalNoError;
TotalServerFailure += statsData.TotalServerFailure;
TotalNxDomain += statsData.TotalNxDomain;
TotalRefused += statsData.TotalRefused;
TotalAuthoritative += statsData.TotalAuthoritative;
TotalRecursive += statsData.TotalRecursive;
TotalCached += statsData.TotalCached;
TotalBlocked += statsData.TotalBlocked;
TotalDropped += statsData.TotalDropped;
if (statsData.TotalClients > TotalClients)
TotalClients = statsData.TotalClients;
if (statsData.Zones > Zones)
Zones = statsData.Zones;
if (statsData.CachedEntries > CachedEntries)
CachedEntries = statsData.CachedEntries;
if (statsData.AllowedZones > AllowedZones)
AllowedZones = statsData.AllowedZones;
if (statsData.BlockedZones > BlockedZones)
BlockedZones = statsData.BlockedZones;
if (statsData.AllowListZones > AllowListZones)
AllowListZones = statsData.AllowListZones;
if (statsData.BlockListZones > BlockListZones)
BlockListZones = statsData.BlockListZones;
}
}
public class ChartData
{
public required string[] Labels { get; set; }
public required DataSet[] DataSets { get; set; }
internal static ChartData Merge(ChartData x, ChartData y, bool sortByData)
{
Dictionary<string, Dictionary<string, long>> aggregateDataSet = new Dictionary<string, Dictionary<string, long>>(x.Labels.Length + y.Labels.Length);
foreach (DataSet dataSet in x.DataSets)
{
Dictionary<string, long> data = new Dictionary<string, long>(dataSet.Data.Length);
for (int i = 0; i < dataSet.Data.Length; i++)
data[x.Labels[i]] = dataSet.Data[i];
aggregateDataSet[dataSet.Label ?? ""] = data;
}
foreach (DataSet dataSet in y.DataSets)
{
if (!aggregateDataSet.TryGetValue(dataSet.Label ?? "", out Dictionary<string, long>? data))
{
data = new Dictionary<string, long>(dataSet.Data.Length);
aggregateDataSet[dataSet.Label ?? ""] = data;
}
for (int i = 0; i < dataSet.Data.Length; i++)
{
string label = y.Labels[i];
if (data.TryGetValue(label, out long value))
data[label] = value + dataSet.Data[i];
else
data[label] = dataSet.Data[i];
}
}
if (sortByData && (aggregateDataSet.Count == 1))
{
//prepare single dataset with sorted data
KeyValuePair<string, Dictionary<string, long>> firstDataSet = aggregateDataSet.First();
Dictionary<string, long> dataSet = firstDataSet.Value;
List<KeyValuePair<string, long>> sortedData = [.. dataSet];
sortedData.Sort(delegate (KeyValuePair<string, long> item1, KeyValuePair<string, long> item2)
{
return item2.Value.CompareTo(item1.Value);
});
string[] labels = new string[sortedData.Count];
long[] data = new long[sortedData.Count];
for (int i = 0; i < sortedData.Count; i++)
{
labels[i] = sortedData[i].Key;
data[i] = sortedData[i].Value;
}
return new ChartData
{
Labels = labels,
DataSets =
[
new DataSet
{
Label = firstDataSet.Key == "" ? null : aggregateDataSet.First().Key,
Data = data
}
]
};
}
else
{
//prepare merged labels
List<string> mergedLabels = new List<string>(x.Labels.Length + y.Labels.Length);
mergedLabels.AddRange(x.Labels);
foreach (string label in y.Labels)
{
if (!mergedLabels.Contains(label))
mergedLabels.Add(label);
}
//prepare merged datasets with ordered data
List<DataSet> mergedDataSets = new List<DataSet>(aggregateDataSet.Count);
foreach (KeyValuePair<string, Dictionary<string, long>> dataSetEntry in aggregateDataSet)
{
long[] data = new long[mergedLabels.Count];
for (int i = 0; i < mergedLabels.Count; i++)
{
string label = mergedLabels[i];
if (dataSetEntry.Value.TryGetValue(label, out long value))
data[i] = value;
}
mergedDataSets.Add(new DataSet
{
Label = dataSetEntry.Key == "" ? null : dataSetEntry.Key,
Data = data
});
}
return new ChartData
{
Labels = [.. mergedLabels],
DataSets = [.. mergedDataSets]
};
}
}
public void Trim(int limit)
{
if (Labels.Length > limit)
{
string[] newLabels = new string[limit];
for (int i = 0; i < limit - 1; i++)
newLabels[i] = Labels[i];
newLabels[limit - 1] = "Others";
Labels = newLabels;
foreach (DataSet dataSet in DataSets)
dataSet.Trim(limit);
}
}
}
public class DataSet
{
public string? Label { get; set; }
public required long[] Data { get; set; }
public void Trim(int limit)
{
if (Data.Length > limit)
{
long[] newData = new long[limit];
for (int i = 0; i < newData.Length - 1; i++)
newData[i] = Data[i];
long othersCount = 0;
for (int i = limit; i < Data.Length; i++)
othersCount += Data[i];
newData[limit - 1] = othersCount;
Data = newData;
}
}
}
public class TopStats
{
public required string Name { get; set; }
public required long Hits { get; set; }
private static List<KeyValuePair<string, T>> GetTopList<T>(List<KeyValuePair<string, T>> list, int limit) where T : TopStats
{
list.Sort(delegate (KeyValuePair<string, T> item1, KeyValuePair<string, T> item2)
{
return item2.Value.Hits.CompareTo(item1.Value.Hits);
});
if (list.Count > limit)
list.RemoveRange(limit, list.Count - limit);
return list;
}
internal static T[] Merge<T>(T[] x, T[] y, int limit) where T : TopStats
{
Dictionary<string, T> aggregateData = new Dictionary<string, T>(x.Length + y.Length);
foreach (T item in x)
aggregateData[item.Name] = item;
foreach (T item in y)
{
if (aggregateData.TryGetValue(item.Name, out T? entry))
{
entry.Hits += item.Hits;
if ((entry is TopClientStats topClientEntry) && (item is TopClientStats topClientItem))
{
topClientEntry.Domain ??= topClientItem.Domain;
topClientEntry.RateLimited |= topClientItem.RateLimited;
}
}
else
{
aggregateData[item.Name] = item;
}
}
List<KeyValuePair<string, T>> topList = GetTopList([.. aggregateData], limit);
T[] z = new T[topList.Count];
for (int i = 0; i < topList.Count; i++)
z[i] = topList[i].Value;
return z;
}
}
public class TopClientStats : TopStats
{
public string? Domain { get; set; }
public bool RateLimited { get; set; }
}
}
}