281 lines
8.6 KiB
C#
281 lines
8.6 KiB
C#
using System.Collections.Generic;
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
namespace Mocha2023.Classes
|
|
{
|
|
|
|
internal static class DeveloperTelemetry
|
|
{
|
|
private const int MaximumRequests = 500;
|
|
private const int MaximumPoints = 300;
|
|
private static readonly object Sync = new();
|
|
private static readonly Queue<RequestEntry> Requests = new();
|
|
private static readonly Queue<TelemetryPoint> Points = new();
|
|
|
|
private static long _currentSecond;
|
|
private static long _currentRequests;
|
|
private static long _currentInboundBytes;
|
|
private static long _currentOutboundBytes;
|
|
private static long _currentErrors;
|
|
private static long _currentLatencyMilliseconds;
|
|
private static long _totalRequests;
|
|
private static long _totalInboundBytes;
|
|
private static long _totalOutboundBytes;
|
|
private static long _totalErrors;
|
|
|
|
public static long EstimateInboundBytes(HttpRequest request)
|
|
{
|
|
long bytes = Math.Max(0, request.ContentLength ?? 0);
|
|
bytes += request.Method.Length +
|
|
(request.Path.Value?.Length ?? 0);
|
|
|
|
foreach (var header in request.Headers)
|
|
{
|
|
bytes += header.Key.Length + 4;
|
|
foreach (string? value in header.Value)
|
|
bytes += value?.Length ?? 0;
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
public static void RecordInboundBytes(long bytes)
|
|
{
|
|
if (bytes <= 0)
|
|
return;
|
|
|
|
lock (Sync)
|
|
{
|
|
RotateNoLock(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
_currentInboundBytes += bytes;
|
|
_totalInboundBytes += bytes;
|
|
}
|
|
}
|
|
|
|
public static void RecordOutboundBytes(long bytes)
|
|
{
|
|
if (bytes <= 0)
|
|
return;
|
|
|
|
lock (Sync)
|
|
{
|
|
RotateNoLock(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
|
|
_currentOutboundBytes += bytes;
|
|
_totalOutboundBytes += bytes;
|
|
}
|
|
}
|
|
|
|
public static void RecordRequest(
|
|
int statusCode,
|
|
string method,
|
|
string path,
|
|
long elapsedMilliseconds,
|
|
long inboundBytes,
|
|
long outboundBytes)
|
|
{
|
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
|
string safePath = string.IsNullOrWhiteSpace(path)
|
|
? "/"
|
|
: path.Length > 300
|
|
? path[..300]
|
|
: path;
|
|
|
|
lock (Sync)
|
|
{
|
|
RotateNoLock(now.ToUnixTimeSeconds());
|
|
|
|
_currentRequests++;
|
|
_currentLatencyMilliseconds += Math.Max(0, elapsedMilliseconds);
|
|
_totalRequests++;
|
|
|
|
if (statusCode >= StatusCodes.Status400BadRequest)
|
|
{
|
|
_currentErrors++;
|
|
_totalErrors++;
|
|
}
|
|
|
|
Requests.Enqueue(new RequestEntry(
|
|
now,
|
|
method,
|
|
safePath,
|
|
statusCode,
|
|
elapsedMilliseconds,
|
|
inboundBytes,
|
|
outboundBytes));
|
|
|
|
while (Requests.Count > MaximumRequests)
|
|
Requests.Dequeue();
|
|
}
|
|
}
|
|
|
|
public static TelemetrySnapshot Snapshot(int requestCount = 100)
|
|
{
|
|
requestCount = Math.Clamp(requestCount, 1, 250);
|
|
|
|
lock (Sync)
|
|
{
|
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
RotateNoLock(now);
|
|
|
|
var points = Points.ToList();
|
|
points.Add(CurrentPointNoLock());
|
|
|
|
return new TelemetrySnapshot(
|
|
_totalRequests,
|
|
_totalInboundBytes,
|
|
_totalOutboundBytes,
|
|
_totalErrors,
|
|
Requests.Reverse().Take(requestCount).ToList(),
|
|
points.TakeLast(120).ToList());
|
|
}
|
|
}
|
|
|
|
private static void RotateNoLock(long second)
|
|
{
|
|
if (_currentSecond == 0)
|
|
{
|
|
_currentSecond = second;
|
|
return;
|
|
}
|
|
|
|
if (second <= _currentSecond)
|
|
return;
|
|
|
|
Points.Enqueue(CurrentPointNoLock());
|
|
while (Points.Count > MaximumPoints)
|
|
Points.Dequeue();
|
|
|
|
long gap = Math.Min(second - _currentSecond - 1, MaximumPoints);
|
|
for (long offset = gap; offset > 0; offset--)
|
|
{
|
|
Points.Enqueue(new TelemetryPoint(
|
|
second - offset,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0));
|
|
|
|
while (Points.Count > MaximumPoints)
|
|
Points.Dequeue();
|
|
}
|
|
|
|
_currentSecond = second;
|
|
_currentRequests = 0;
|
|
_currentInboundBytes = 0;
|
|
_currentOutboundBytes = 0;
|
|
_currentErrors = 0;
|
|
_currentLatencyMilliseconds = 0;
|
|
}
|
|
|
|
private static TelemetryPoint CurrentPointNoLock() =>
|
|
new(
|
|
_currentSecond,
|
|
_currentRequests,
|
|
_currentInboundBytes,
|
|
_currentOutboundBytes,
|
|
_currentErrors,
|
|
_currentRequests == 0
|
|
? 0
|
|
: (double)_currentLatencyMilliseconds / _currentRequests);
|
|
|
|
internal sealed record RequestEntry(
|
|
DateTimeOffset At,
|
|
string Method,
|
|
string Path,
|
|
int StatusCode,
|
|
long ElapsedMilliseconds,
|
|
long InboundBytes,
|
|
long OutboundBytes);
|
|
|
|
internal sealed record TelemetryPoint(
|
|
long UnixSecond,
|
|
long Requests,
|
|
long InboundBytes,
|
|
long OutboundBytes,
|
|
long Errors,
|
|
double AverageLatencyMilliseconds);
|
|
|
|
internal sealed record TelemetrySnapshot(
|
|
long TotalRequests,
|
|
long TotalInboundBytes,
|
|
long TotalOutboundBytes,
|
|
long TotalErrors,
|
|
IReadOnlyList<RequestEntry> Requests,
|
|
IReadOnlyList<TelemetryPoint> Points);
|
|
}
|
|
|
|
internal sealed class CountingResponseStream : Stream
|
|
{
|
|
private readonly Stream _inner;
|
|
private readonly Action<int> _onWrite;
|
|
|
|
public CountingResponseStream(Stream inner, Action<int> onWrite)
|
|
{
|
|
_inner = inner;
|
|
_onWrite = onWrite;
|
|
}
|
|
|
|
public long BytesWritten { get; private set; }
|
|
public override bool CanRead => _inner.CanRead;
|
|
public override bool CanSeek => _inner.CanSeek;
|
|
public override bool CanWrite => _inner.CanWrite;
|
|
public override long Length => _inner.Length;
|
|
public override long Position
|
|
{
|
|
get => _inner.Position;
|
|
set => _inner.Position = value;
|
|
}
|
|
|
|
public override void Flush() => _inner.Flush();
|
|
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
|
_inner.FlushAsync(cancellationToken);
|
|
public override int Read(byte[] buffer, int offset, int count) =>
|
|
_inner.Read(buffer, offset, count);
|
|
public override long Seek(long offset, SeekOrigin origin) =>
|
|
_inner.Seek(offset, origin);
|
|
public override void SetLength(long value) => _inner.SetLength(value);
|
|
|
|
public override void Write(byte[] buffer, int offset, int count)
|
|
{
|
|
_inner.Write(buffer, offset, count);
|
|
Count(count);
|
|
}
|
|
|
|
public override async Task WriteAsync(
|
|
byte[] buffer,
|
|
int offset,
|
|
int count,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await _inner.WriteAsync(
|
|
buffer.AsMemory(offset, count),
|
|
cancellationToken);
|
|
Count(count);
|
|
}
|
|
|
|
public override void Write(ReadOnlySpan<byte> buffer)
|
|
{
|
|
_inner.Write(buffer);
|
|
Count(buffer.Length);
|
|
}
|
|
|
|
public override async ValueTask WriteAsync(
|
|
ReadOnlyMemory<byte> buffer,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await _inner.WriteAsync(buffer, cancellationToken);
|
|
Count(buffer.Length);
|
|
}
|
|
|
|
private void Count(int bytes)
|
|
{
|
|
if (bytes <= 0)
|
|
return;
|
|
|
|
BytesWritten += bytes;
|
|
_onWrite(bytes);
|
|
}
|
|
}
|
|
}
|