MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Context/Monitoring/ContextMetrics.cs

383 lines
11 KiB
C#
Executable File

using System.Diagnostics.Metrics;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using MarketAlly.AIPlugin.Context.Configuration;
namespace MarketAlly.AIPlugin.Context.Monitoring
{
/// <summary>
/// Provides comprehensive metrics and monitoring for context operations
/// </summary>
public class ContextMetrics : IDisposable
{
private readonly Meter _meter;
private readonly ILogger<ContextMetrics> _logger;
private readonly ContextConfiguration _configuration;
// Counters
private readonly Counter<long> _operationCounter;
private readonly Counter<long> _errorCounter;
private readonly Counter<long> _cacheHitCounter;
private readonly Counter<long> _cacheMissCounter;
// Histograms
private readonly Histogram<double> _operationDuration;
private readonly Histogram<double> _fileSize;
private readonly Histogram<long> _searchResultCount;
// Gauges (using UpDownCounters as approximation)
private readonly UpDownCounter<long> _activeConnections;
private readonly UpDownCounter<long> _contextEntriesCount;
private readonly UpDownCounter<long> _fileCacheSize;
// Activity sources for distributed tracing
private readonly ActivitySource _activitySource;
public ContextMetrics(ILogger<ContextMetrics> logger, ContextConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
_meter = new Meter("MarketAlly.Context", "1.0.0");
_activitySource = new ActivitySource("MarketAlly.Context.Operations");
// Initialize counters
_operationCounter = _meter.CreateCounter<long>(
"context_operations_total",
"Total number of context operations performed",
"operations");
_errorCounter = _meter.CreateCounter<long>(
"context_errors_total",
"Total number of context operation errors",
"errors");
_cacheHitCounter = _meter.CreateCounter<long>(
"context_cache_hits_total",
"Total number of cache hits",
"hits");
_cacheMissCounter = _meter.CreateCounter<long>(
"context_cache_misses_total",
"Total number of cache misses",
"misses");
// Initialize histograms
_operationDuration = _meter.CreateHistogram<double>(
"context_operation_duration_ms",
"Duration of context operations in milliseconds",
"ms");
_fileSize = _meter.CreateHistogram<double>(
"context_file_size_bytes",
"Size of context files in bytes",
"bytes");
_searchResultCount = _meter.CreateHistogram<long>(
"context_search_results_count",
"Number of results returned by search operations",
"results");
// Initialize gauges (using UpDownCounters)
_activeConnections = _meter.CreateUpDownCounter<long>(
"context_active_connections",
"Number of active context connections",
"connections");
_contextEntriesCount = _meter.CreateUpDownCounter<long>(
"context_entries_total",
"Total number of context entries stored",
"entries");
_fileCacheSize = _meter.CreateUpDownCounter<long>(
"context_file_cache_size_bytes",
"Current size of file cache in bytes",
"bytes");
}
/// <summary>
/// Records the start of an operation and returns a disposable tracker
/// </summary>
public OperationTracker StartOperation(string operationType, Dictionary<string, object>? tags = null)
{
var activity = _configuration.Monitoring.EnableTracing ?
_activitySource.StartActivity($"context.{operationType}") : null;
if (activity != null && tags != null)
{
foreach (var tag in tags)
{
activity.SetTag(tag.Key, tag.Value);
}
}
_activeConnections.Add(1);
return new OperationTracker(
operationType,
activity,
this,
_logger,
Stopwatch.StartNew());
}
/// <summary>
/// Records a completed operation
/// </summary>
internal void RecordOperation(string operationType, TimeSpan duration, bool success, Dictionary<string, object>? tags = null)
{
var operationTags = CreateTags(operationType, success ? "success" : "error", tags);
_operationCounter.Add(1, operationTags);
_operationDuration.Record(duration.TotalMilliseconds, operationTags);
if (!success)
{
_errorCounter.Add(1, operationTags);
}
_activeConnections.Add(-1);
if (_configuration.Monitoring.EnableDetailedLogging)
{
var logLevel = success ? LogLevel.Debug : LogLevel.Warning;
_logger.Log(logLevel,
"Operation {OperationType} completed in {Duration}ms with status {Status}",
operationType, duration.TotalMilliseconds, success ? "Success" : "Error");
}
}
/// <summary>
/// Records cache hit metrics
/// </summary>
public void RecordCacheHit(string cacheType, Dictionary<string, object>? tags = null)
{
var cacheTags = CreateTags("cache", "hit", tags);
cacheTags = cacheTags.Append(new KeyValuePair<string, object?>("cache_type", cacheType)).ToArray();
_cacheHitCounter.Add(1, cacheTags);
}
/// <summary>
/// Records cache miss metrics
/// </summary>
public void RecordCacheMiss(string cacheType, Dictionary<string, object>? tags = null)
{
var cacheTags = CreateTags("cache", "miss", tags);
cacheTags = cacheTags.Append(new KeyValuePair<string, object?>("cache_type", cacheType)).ToArray();
_cacheMissCounter.Add(1, cacheTags);
}
/// <summary>
/// Records file operation metrics
/// </summary>
public void RecordFileOperation(string operation, long fileSizeBytes, TimeSpan duration, bool success)
{
var tags = CreateTags("file", success ? "success" : "error", null);
tags = tags.Append(new KeyValuePair<string, object?>("file_operation", operation)).ToArray();
_fileSize.Record(fileSizeBytes, tags);
_operationDuration.Record(duration.TotalMilliseconds, tags);
}
/// <summary>
/// Records search operation metrics
/// </summary>
public void RecordSearchOperation(string searchType, int resultCount, TimeSpan duration, bool usedCache = false)
{
var tags = CreateTags("search", "success", null);
tags = tags.Append(new KeyValuePair<string, object?>("search_type", searchType)).ToArray();
tags = tags.Append(new KeyValuePair<string, object?>("used_cache", usedCache)).ToArray();
_searchResultCount.Record(resultCount, tags);
_operationDuration.Record(duration.TotalMilliseconds, tags);
}
/// <summary>
/// Updates the total number of context entries
/// </summary>
public void UpdateContextEntriesCount(long newCount)
{
// Since we can't set absolute values with UpDownCounter, we need to track the delta
// This is a simplified approach - in production, you might want to use a different gauge implementation
var tags = CreateTags("storage", "update", null);
_contextEntriesCount.Add(newCount, tags);
}
/// <summary>
/// Updates the file cache size
/// </summary>
public void UpdateFileCacheSize(long newSizeBytes)
{
var tags = CreateTags("cache", "update", null);
_fileCacheSize.Add(newSizeBytes, tags);
}
/// <summary>
/// Records an error with detailed information
/// </summary>
public void RecordError(string operationType, Exception exception, Dictionary<string, object>? tags = null)
{
var errorTags = CreateTags(operationType, "error", tags);
errorTags = errorTags.Append(new KeyValuePair<string, object?>("error_type", exception.GetType().Name)).ToArray();
_errorCounter.Add(1, errorTags);
_logger.LogError(exception,
"Error in {OperationType}: {ErrorMessage}",
operationType, exception.Message);
}
/// <summary>
/// Gets current performance metrics
/// </summary>
public PerformanceMetrics GetCurrentMetrics()
{
// Note: In a real implementation, you'd want to collect these values from the actual meters
// This is a simplified representation
return new PerformanceMetrics
{
Timestamp = DateTime.UtcNow,
ActiveConnections = 0, // Would need to track this separately
TotalOperations = 0, // Would need to track this separately
TotalErrors = 0, // Would need to track this separately
CacheHitRatio = 0.0, // Would calculate from hit/miss counters
AverageOperationDuration = 0.0, // Would calculate from histogram
MemoryUsage = GC.GetTotalMemory(false),
IsHealthy = true
};
}
/// <summary>
/// Creates standardized tags for metrics
/// </summary>
private KeyValuePair<string, object?>[] CreateTags(string operationType, string status, Dictionary<string, object>? additionalTags)
{
var tags = new List<KeyValuePair<string, object?>>
{
new("operation_type", operationType),
new("status", status),
new("version", "1.0.0")
};
if (additionalTags != null)
{
foreach (var tag in additionalTags)
{
tags.Add(new KeyValuePair<string, object?>(tag.Key, tag.Value));
}
}
return tags.ToArray();
}
public void Dispose()
{
_meter?.Dispose();
_activitySource?.Dispose();
}
}
/// <summary>
/// Tracks the duration and outcome of an operation
/// </summary>
public class OperationTracker : IDisposable
{
private readonly string _operationType;
private readonly Activity? _activity;
private readonly ContextMetrics _metrics;
private readonly ILogger _logger;
private readonly Stopwatch _stopwatch;
private readonly Dictionary<string, object> _tags;
private bool _disposed;
internal OperationTracker(
string operationType,
Activity? activity,
ContextMetrics metrics,
ILogger logger,
Stopwatch stopwatch)
{
_operationType = operationType;
_activity = activity;
_metrics = metrics;
_logger = logger;
_stopwatch = stopwatch;
_tags = new Dictionary<string, object>();
}
/// <summary>
/// Adds a tag to the operation
/// </summary>
public OperationTracker AddTag(string key, object value)
{
_tags[key] = value;
_activity?.SetTag(key, value);
return this;
}
/// <summary>
/// Records the operation as successful
/// </summary>
public void RecordSuccess()
{
if (!_disposed)
{
_stopwatch.Stop();
_metrics.RecordOperation(_operationType, _stopwatch.Elapsed, true, _tags);
_activity?.SetStatus(ActivityStatusCode.Ok);
_disposed = true;
}
}
/// <summary>
/// Records the operation as failed
/// </summary>
public void RecordError(Exception? exception = null)
{
if (!_disposed)
{
_stopwatch.Stop();
_metrics.RecordOperation(_operationType, _stopwatch.Elapsed, false, _tags);
if (exception != null)
{
_activity?.SetStatus(ActivityStatusCode.Error, exception.Message);
_metrics.RecordError(_operationType, exception, _tags);
}
else
{
_activity?.SetStatus(ActivityStatusCode.Error);
}
_disposed = true;
}
}
public void Dispose()
{
if (!_disposed)
{
// If not explicitly marked as success or error, assume success
RecordSuccess();
}
_activity?.Dispose();
}
}
/// <summary>
/// Current performance metrics snapshot
/// </summary>
public class PerformanceMetrics
{
public DateTime Timestamp { get; set; }
public long ActiveConnections { get; set; }
public long TotalOperations { get; set; }
public long TotalErrors { get; set; }
public double CacheHitRatio { get; set; }
public double AverageOperationDuration { get; set; }
public long MemoryUsage { get; set; }
public bool IsHealthy { get; set; }
}
}