feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides performance metrics tracking for LmxProxy operations
|
||||
/// </summary>
|
||||
public class PerformanceMetrics : IDisposable
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<PerformanceMetrics>();
|
||||
|
||||
private readonly ConcurrentDictionary<string, OperationMetrics> _metrics = new();
|
||||
private readonly Timer _reportingTimer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the PerformanceMetrics class
|
||||
/// </summary>
|
||||
public PerformanceMetrics()
|
||||
{
|
||||
// Report metrics every minute
|
||||
_reportingTimer = new Timer(ReportMetrics, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
_reportingTimer?.Dispose();
|
||||
ReportMetrics(null); // Final report
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the execution time of an operation
|
||||
/// </summary>
|
||||
public void RecordOperation(string operationName, TimeSpan duration, bool success = true)
|
||||
{
|
||||
OperationMetrics? metrics = _metrics.GetOrAdd(operationName, _ => new OperationMetrics());
|
||||
metrics.Record(duration, success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a timing scope for measuring operation duration
|
||||
/// </summary>
|
||||
public ITimingScope BeginOperation(string operationName) => new TimingScope(this, operationName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets current metrics for a specific operation
|
||||
/// </summary>
|
||||
public OperationMetrics? GetMetrics(string operationName) =>
|
||||
_metrics.TryGetValue(operationName, out OperationMetrics? metrics) ? metrics : null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all current metrics
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, OperationMetrics> GetAllMetrics() =>
|
||||
_metrics.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets statistics for all operations
|
||||
/// </summary>
|
||||
public Dictionary<string, MetricsStatistics> GetStatistics() =>
|
||||
_metrics.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.GetStatistics());
|
||||
|
||||
private void ReportMetrics(object? state)
|
||||
{
|
||||
foreach (KeyValuePair<string, OperationMetrics> kvp in _metrics)
|
||||
{
|
||||
MetricsStatistics stats = kvp.Value.GetStatistics();
|
||||
if (stats.TotalCount > 0)
|
||||
{
|
||||
Logger.Information(
|
||||
"Performance Metrics - {Operation}: Count={Count}, Success={SuccessRate:P}, " +
|
||||
"Avg={AverageMs:F2}ms, Min={MinMs:F2}ms, Max={MaxMs:F2}ms, P95={P95Ms:F2}ms",
|
||||
kvp.Key,
|
||||
stats.TotalCount,
|
||||
stats.SuccessRate,
|
||||
stats.AverageMilliseconds,
|
||||
stats.MinMilliseconds,
|
||||
stats.MaxMilliseconds,
|
||||
stats.Percentile95Milliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timing scope for automatic duration measurement
|
||||
/// </summary>
|
||||
public interface ITimingScope : IDisposable
|
||||
{
|
||||
void SetSuccess(bool success);
|
||||
}
|
||||
|
||||
private class TimingScope : ITimingScope
|
||||
{
|
||||
private readonly PerformanceMetrics _metrics;
|
||||
private readonly string _operationName;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
private bool _disposed;
|
||||
private bool _success = true;
|
||||
|
||||
public TimingScope(PerformanceMetrics metrics, string operationName)
|
||||
{
|
||||
_metrics = metrics;
|
||||
_operationName = operationName;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
public void SetSuccess(bool success) => _success = success;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
_stopwatch.Stop();
|
||||
_metrics.RecordOperation(_operationName, _stopwatch.Elapsed, _success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for a specific operation
|
||||
/// </summary>
|
||||
public class OperationMetrics
|
||||
{
|
||||
private readonly List<double> _durations = new();
|
||||
private readonly object _lock = new();
|
||||
private double _maxMilliseconds;
|
||||
private double _minMilliseconds = double.MaxValue;
|
||||
private long _successCount;
|
||||
private long _totalCount;
|
||||
private double _totalMilliseconds;
|
||||
|
||||
public void Record(TimeSpan duration, bool success)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
double ms = duration.TotalMilliseconds;
|
||||
_durations.Add(ms);
|
||||
_totalCount++;
|
||||
if (success)
|
||||
{
|
||||
_successCount++;
|
||||
}
|
||||
|
||||
_totalMilliseconds += ms;
|
||||
_minMilliseconds = Math.Min(_minMilliseconds, ms);
|
||||
_maxMilliseconds = Math.Max(_maxMilliseconds, ms);
|
||||
|
||||
// Keep only last 1000 samples for percentile calculation
|
||||
if (_durations.Count > 1000)
|
||||
{
|
||||
_durations.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MetricsStatistics GetStatistics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_totalCount == 0)
|
||||
{
|
||||
return new MetricsStatistics();
|
||||
}
|
||||
|
||||
var sortedDurations = _durations.OrderBy(d => d).ToList();
|
||||
int p95Index = (int)Math.Ceiling(sortedDurations.Count * 0.95) - 1;
|
||||
|
||||
return new MetricsStatistics
|
||||
{
|
||||
TotalCount = _totalCount,
|
||||
SuccessCount = _successCount,
|
||||
SuccessRate = _successCount / (double)_totalCount,
|
||||
AverageMilliseconds = _totalMilliseconds / _totalCount,
|
||||
MinMilliseconds = _minMilliseconds == double.MaxValue ? 0 : _minMilliseconds,
|
||||
MaxMilliseconds = _maxMilliseconds,
|
||||
Percentile95Milliseconds = sortedDurations.Count > 0 ? sortedDurations[Math.Max(0, p95Index)] : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics for an operation
|
||||
/// </summary>
|
||||
public class MetricsStatistics
|
||||
{
|
||||
public long TotalCount { get; set; }
|
||||
public long SuccessCount { get; set; }
|
||||
public double SuccessRate { get; set; }
|
||||
public double AverageMilliseconds { get; set; }
|
||||
public double MinMilliseconds { get; set; }
|
||||
public double MaxMilliseconds { get; set; }
|
||||
public double Percentile95Milliseconds { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user