Skip to content

Commit

Permalink
Overhauls the stats server.
Browse files Browse the repository at this point in the history
  • Loading branch information
alanedwardes committed Aug 5, 2023
1 parent 6c2c83d commit 3277444
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 53 deletions.
179 changes: 130 additions & 49 deletions misc/Ae.Dns.Console/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Net;
using System.Runtime.Caching;
using System.Threading.Tasks;
using Ae.Dns.Client;
using Ae.Dns.Protocol;
using Ae.Dns.Protocol.Enums;
using Ae.Dns.Server.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Ae.Dns.Console
{
Expand Down Expand Up @@ -44,6 +48,48 @@ async Task WriteHeader(string header)
await context.Response.WriteAsync(Environment.NewLine);
}
async Task WriteTable(DataTable table)
{
await context.Response.WriteAsync("<table>");
await context.Response.WriteAsync("<thead>");
await context.Response.WriteAsync("<tr>");
foreach (DataColumn heading in table.Columns)
{
await context.Response.WriteAsync($"<th>{heading.ColumnName}</th>");
}
await context.Response.WriteAsync("</tr>");
await context.Response.WriteAsync("</thead>");
await context.Response.WriteAsync("<tbody>");
foreach (DataRow row in table.Rows)
{
await context.Response.WriteAsync("<tr>");
foreach (var item in row.ItemArray)
{
await context.Response.WriteAsync($"<td>{item}</td>");
}
await context.Response.WriteAsync("</tr>");
}
await context.Response.WriteAsync("</tbody>");
await context.Response.WriteAsync("</table>");
}
async Task GroupToTable(IEnumerable<IGrouping<string, DnsQuery>> groups, params string[] headings)
{
var table = new DataTable();
foreach (var heading in headings)
{
table.Columns.Add(heading);
}
foreach (var group in groups.OrderByDescending(x => x.Count()).Take(20))
{
table.Rows.Add(group.Key, group.Count());
}
await WriteTable(table);
}
var resolverCache = context.RequestServices.GetRequiredService<ObjectCache>();
if (context.Request.Path.StartsWithSegments("/cache/remove"))
Expand All @@ -55,6 +101,14 @@ async Task WriteHeader(string header)
return;
}
if (context.Request.Path.StartsWithSegments("/reset"))
{
Reset();
context.Response.StatusCode = StatusCodes.Status307TemporaryRedirect;
context.Response.Headers.Location = "/";
return;
}
if (context.Request.Path.StartsWithSegments("/cache"))
{
var cacheEntries = resolverCache.Where(x => x.Value is DnsCachingClient.DnsCacheEntry)
Expand Down Expand Up @@ -102,49 +156,74 @@ async Task WriteHeader(string header)
if (context.Request.Path == "/")
{
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "text/plain; charset=utf-8";
context.Response.ContentType = "text/html; charset=utf-8";
var statsSets = new Dictionary<string, IDictionary<string, int>>
{
{ "Top Blocked Domains", _topBlockedDomains },
{ "Top Permitted Domains", _topPermittedDomains },
{ "Top Missing Domains", _topMissingDomains },
{ "Top Other Error Domains", _topOtherErrorDomains },
{ "Top Exception Error Domains", _topExceptionDomains }
};
await WriteHeader(resolverCache.Name);
await context.Response.WriteAsync($"Cached Objects = {resolverCache.Count()}");
await context.Response.WriteAsync(Environment.NewLine);
await context.Response.WriteAsync(Environment.NewLine);
var answeredQueries = _queries.Where(x => x.Answer != null);
var missingQueries = answeredQueries.Where(x => x.Answer.Header.ResponseCode == DnsResponseCode.NXDomain).ToArray();
var successfulQueries = answeredQueries.Where(x => x.Answer.Header.ResponseCode == DnsResponseCode.NoError).ToArray();
var refusedQueries = answeredQueries.Where(x => x.Answer.Header.ResponseCode == DnsResponseCode.Refused).ToArray();
var notAnsweredQueries = _queries.Where(x => x.Answer == null).ToArray();
foreach (var statsSet in statsSets)
{
await WriteHeader(statsSet.Key);
await context.Response.WriteAsync($"<h1>Metrics Server</h1>");
await context.Response.WriteAsync($"<p>" +
$"Metrics since {_lastReset} (current time is {DateTime.UtcNow}). " +
$"There were {_queries.Count} total queries, and there are {resolverCache.Count()} cache entries. " +
$"Of those queries, {successfulQueries.Length} were successful, {missingQueries.Length} were missing, " +
$"{refusedQueries.Length} were refused, and {notAnsweredQueries.Length} were not answered." +
$"</p>");
await context.Response.WriteAsync($"<p><a href=\"/reset\">Reset statistics</a></p>");
if (!statsSet.Value.Any())
{
await context.Response.WriteAsync("None");
await context.Response.WriteAsync(Environment.NewLine);
}
await context.Response.WriteAsync($"<h2>Top Top Level Domains</h2>");
await context.Response.WriteAsync($"<p>Top level domains (permitted and refused).</p>");
await GroupToTable(_queries.GroupBy(x => string.Join('.', x.Query.Header.Host.Split('.').Reverse().First())), "Top Level Domain", "Hits");
foreach (var statistic in statsSet.Key == "Statistics" ? statsSet.Value.OrderBy(x => x.Key) : statsSet.Value.OrderByDescending(x => x.Value).Take(50))
{
await context.Response.WriteAsync($"{statistic.Key} = {statistic.Value}");
await context.Response.WriteAsync(Environment.NewLine);
}
await context.Response.WriteAsync($"<h2>Top Refused Root Domains</h2>");
await context.Response.WriteAsync($"<p>Top domain names which were refused.</p>");
await GroupToTable(refusedQueries.GroupBy(x => string.Join('.', x.Query.Header.Host.Split('.').Reverse().Take(2).Reverse())), "Root Domain", "Hits");
await context.Response.WriteAsync(Environment.NewLine);
}
await context.Response.WriteAsync($"<h2>Top Permitted Root Domains</h2>");
await context.Response.WriteAsync($"<p>Top domain names which were permitted.</p>");
await GroupToTable(successfulQueries.GroupBy(x => string.Join('.', x.Query.Header.Host.Split('.').Reverse().Take(2).Reverse())), "Root Domain", "Hits");
await context.Response.WriteAsync($"<h2>Top Missing Root Domains</h2>");
await context.Response.WriteAsync($"<p>Root domain names which were missing (NXDomain).</p>");
await GroupToTable(missingQueries.GroupBy(x => string.Join('.', x.Query.Header.Host.Split('.').Reverse().Take(2).Reverse())), "Root Domain", "Hits");
await context.Response.WriteAsync($"<h2>Top Clients</h2>");
await context.Response.WriteAsync($"<p>Top DNS clients.</p>");
await GroupToTable(_queries.GroupBy(x => x.Sender.Address.ToString()), "Client Address", "Hits");
await context.Response.WriteAsync($"<h2>Top Responses</h2>");
await context.Response.WriteAsync($"<p>Top response codes for all queries.</p>");
await GroupToTable(_queries.GroupBy(x => x.Answer?.Header.ResponseCode.ToString()), "Response Code", "Hits");
await context.Response.WriteAsync($"<h2>Top Query Types</h2>");
await context.Response.WriteAsync($"<p>Top query types across all queries.</p>");
await GroupToTable(_queries.GroupBy(x => x.Query.Header.QueryType.ToString()), "Query Type", "Hits");
await context.Response.WriteAsync($"<h2>Top Answer Sources</h2>");
await context.Response.WriteAsync($"<p>Top sources of query responses in terms of the code or upstream which generated them.</p>");
await GroupToTable(answeredQueries.GroupBy(x => (x.Answer.Header.Tags.ContainsKey("Resolver") ? x.Answer.Header.Tags["Resolver"] : "<none>").ToString()), "Answer Source", "Hits");
}
});
}

private readonly ConcurrentDictionary<string, int> _topPermittedDomains = new();
private readonly ConcurrentDictionary<string, int> _topBlockedDomains = new();
private readonly ConcurrentDictionary<string, int> _topMissingDomains = new();
private readonly ConcurrentDictionary<string, int> _topOtherErrorDomains = new();
private readonly ConcurrentDictionary<string, int> _topExceptionDomains = new();
private sealed class DnsQuery
{
public DnsMessage Query { get; set; }
public DnsMessage? Answer { get; set; }
public IPEndPoint Sender { get; set; }
public TimeSpan? Elapsed { get; set; }
}

private readonly ConcurrentBag<DnsQuery> _queries = new ConcurrentBag<DnsQuery>();
private DateTime _lastReset = DateTime.UtcNow;

private void Reset()
{
_queries.Clear();
_lastReset = DateTime.UtcNow;
}

private void OnMeasurementRecorded(Instrument instrument, int measurement, ReadOnlySpan<KeyValuePair<string, object>> tags, object state)
{
Expand All @@ -160,27 +239,29 @@ static TObject GetObjectFromTags<TObject>(ReadOnlySpan<KeyValuePair<string, obje
}
}

throw new InvalidOperationException();
return default(TObject);
}

if (instrument.Meter.Name == DnsMetricsClient.MeterName)
{
var meterMap = new Dictionary<string, ConcurrentDictionary<string, int>>
{
{ DnsMetricsClient.SuccessCounterName, _topPermittedDomains },
{ DnsMetricsClient.OtherErrorCounterName, _topOtherErrorDomains },
{ DnsMetricsClient.MissingErrorCounterName, _topMissingDomains },
{ DnsMetricsClient.RefusedErrorCounterName, _topBlockedDomains },
{ DnsMetricsClient.ExceptionErrorCounterName, _topExceptionDomains }
};

if (meterMap.TryGetValue(instrument.Name, out var domainCounts))
{
var query = GetObjectFromTags<DnsMessage>(tags, "Query");
var sourceEndpoint = query.Header.Tags.TryGetValue("Sender", out var rawEndpoint) && rawEndpoint is IPEndPoint endpoint ? endpoint : throw new Exception();
var query = GetObjectFromTags<DnsMessage>(tags, "Query");
var answer = GetObjectFromTags<DnsMessage>(tags, "Answer");
var elapsed = GetObjectFromTags<TimeSpan?>(tags, "Elapsed");
var sender = query.Header.Tags.TryGetValue("Sender", out var rawEndpoint) && rawEndpoint is IPEndPoint endpoint ? endpoint : throw new Exception();

domainCounts.AddOrUpdate(sourceEndpoint.Address.ToString() + ' ' + query.Header.QueryType.ToString() + ' ' + query.Header.Host, 1, (id, count) => count + 1);
// Ensure we don't run out of memory
if (_queries.Count > 1_000_000)
{
Reset();
}

_queries.Add(new DnsQuery
{
Query = query,
Answer = answer,
Sender = sender,
Elapsed = elapsed
});
}
}
}
Expand Down
11 changes: 7 additions & 4 deletions src/Ae.Dns.Client/DnsMetricsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,22 @@ public async Task<DnsMessage> Query(DnsMessage query, CancellationToken token =
sw.Stop();
}

var answerTag = new KeyValuePair<string, object>("Answer", answer);
var elapsedTag = new KeyValuePair<string, object>("Elapsed", sw.Elapsed);

switch (answer.Header.ResponseCode)
{
case DnsResponseCode.NoError:
_successCounter.Add(1, queryTag);
_successCounter.Add(1, queryTag, answerTag, elapsedTag);
break;
case DnsResponseCode.NXDomain:
_missingCounter.Add(1, queryTag);
_missingCounter.Add(1, queryTag, answerTag, elapsedTag);
break;
case DnsResponseCode.Refused:
_refusedCounter.Add(1, queryTag);
_refusedCounter.Add(1, queryTag, answerTag, elapsedTag);
break;
default:
_otherErrorCounter.Add(1, queryTag);
_otherErrorCounter.Add(1, queryTag, answerTag, elapsedTag);
break;
}

Expand Down

0 comments on commit 3277444

Please sign in to comment.