save progress
This commit is contained in:
30
src/Signals/StellaOps.Signals.RuntimeAgent/AgentState.cs
Normal file
30
src/Signals/StellaOps.Signals.RuntimeAgent/AgentState.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// <copyright file="AgentState.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime agent state.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create enums
|
||||
/// </summary>
|
||||
public enum AgentState
|
||||
{
|
||||
/// <summary>Agent is stopped.</summary>
|
||||
Stopped = 0,
|
||||
|
||||
/// <summary>Agent is starting up.</summary>
|
||||
Starting = 1,
|
||||
|
||||
/// <summary>Agent is running and collecting.</summary>
|
||||
Running = 2,
|
||||
|
||||
/// <summary>Agent is stopping gracefully.</summary>
|
||||
Stopping = 3,
|
||||
|
||||
/// <summary>Agent encountered an error.</summary>
|
||||
Error = 4,
|
||||
|
||||
/// <summary>Agent is paused but can resume.</summary>
|
||||
Paused = 5
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// <copyright file="AgentStatistics.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Collection statistics for runtime agents.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record AgentStatistics
|
||||
{
|
||||
/// <summary>Agent ID.</summary>
|
||||
public required string AgentId { get; init; }
|
||||
|
||||
/// <summary>Statistics timestamp (UTC).</summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Agent state.</summary>
|
||||
public required AgentState State { get; init; }
|
||||
|
||||
/// <summary>Agent uptime.</summary>
|
||||
public required TimeSpan Uptime { get; init; }
|
||||
|
||||
/// <summary>Total events collected.</summary>
|
||||
public required long TotalEventsCollected { get; init; }
|
||||
|
||||
/// <summary>Events collected in last minute.</summary>
|
||||
public required long EventsLastMinute { get; init; }
|
||||
|
||||
/// <summary>Events dropped due to rate limiting.</summary>
|
||||
public required long EventsDropped { get; init; }
|
||||
|
||||
/// <summary>Unique methods observed.</summary>
|
||||
public required int UniqueMethodsObserved { get; init; }
|
||||
|
||||
/// <summary>Unique types observed.</summary>
|
||||
public required int UniqueTypesObserved { get; init; }
|
||||
|
||||
/// <summary>Unique assemblies observed.</summary>
|
||||
public required int UniqueAssembliesObserved { get; init; }
|
||||
|
||||
/// <summary>Buffer utilization percentage.</summary>
|
||||
public required double BufferUtilizationPercent { get; init; }
|
||||
|
||||
/// <summary>Estimated CPU overhead percentage.</summary>
|
||||
public required double EstimatedCpuOverheadPercent { get; init; }
|
||||
|
||||
/// <summary>Memory usage in bytes.</summary>
|
||||
public required long MemoryUsageBytes { get; init; }
|
||||
|
||||
/// <summary>Last error message if any.</summary>
|
||||
public string? LastError { get; init; }
|
||||
|
||||
/// <summary>Last error timestamp.</summary>
|
||||
public DateTimeOffset? LastErrorTimestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates empty statistics for a new agent.
|
||||
/// </summary>
|
||||
public static AgentStatistics Empty(string agentId, TimeProvider timeProvider) => new()
|
||||
{
|
||||
AgentId = agentId,
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
State = AgentState.Stopped,
|
||||
Uptime = TimeSpan.Zero,
|
||||
TotalEventsCollected = 0,
|
||||
EventsLastMinute = 0,
|
||||
EventsDropped = 0,
|
||||
UniqueMethodsObserved = 0,
|
||||
UniqueTypesObserved = 0,
|
||||
UniqueAssembliesObserved = 0,
|
||||
BufferUtilizationPercent = 0,
|
||||
EstimatedCpuOverheadPercent = 0,
|
||||
MemoryUsageBytes = 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// <copyright file="DotNetEventPipeAgent.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// .NET EventPipe-based runtime agent for CLR method observation.
|
||||
/// This is a framework implementation - actual EventPipe integration
|
||||
/// requires Microsoft.Diagnostics.NETCore.Client package.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Implement .NET agent
|
||||
/// </summary>
|
||||
public sealed class DotNetEventPipeAgent : RuntimeAgentBase
|
||||
{
|
||||
private readonly IGuidGenerator _guidGenerator;
|
||||
private CancellationTokenSource? _processingCts;
|
||||
private Task? _processingTask;
|
||||
private Regex[]? _includePatterns;
|
||||
private Regex[]? _excludePatterns;
|
||||
|
||||
public DotNetEventPipeAgent(
|
||||
string agentId,
|
||||
TimeProvider timeProvider,
|
||||
IGuidGenerator guidGenerator,
|
||||
ILogger<DotNetEventPipeAgent> logger)
|
||||
: base(agentId, RuntimePlatform.DotNet, timeProvider, logger)
|
||||
{
|
||||
_guidGenerator = guidGenerator;
|
||||
}
|
||||
|
||||
protected override Task StartCollectionAsync(RuntimeAgentOptions options, CancellationToken ct)
|
||||
{
|
||||
// Compile filter patterns
|
||||
_includePatterns = options.IncludePatterns
|
||||
.Select(p => new Regex(p, RegexOptions.Compiled | RegexOptions.IgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
_excludePatterns = options.ExcludePatterns
|
||||
.Select(p => new Regex(p, RegexOptions.Compiled | RegexOptions.IgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
_processingCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
|
||||
// Note: Actual EventPipe integration would start here
|
||||
// This requires Microsoft.Diagnostics.NETCore.Client package
|
||||
// For now, this is a framework that can be extended
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task StopCollectionAsync(CancellationToken ct)
|
||||
{
|
||||
_processingCts?.Cancel();
|
||||
|
||||
if (_processingTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _processingTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
_processingCts?.Dispose();
|
||||
_processingTask = null;
|
||||
_processingCts = null;
|
||||
}
|
||||
|
||||
protected override async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
_processingCts?.Dispose();
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit a method observation event (for testing or manual integration).
|
||||
/// </summary>
|
||||
public void EmitMethodObservation(
|
||||
string methodName,
|
||||
string typeName,
|
||||
string assemblyOrModule,
|
||||
RuntimeEventKind kind = RuntimeEventKind.MethodJit)
|
||||
{
|
||||
var fullName = $"{typeName}.{methodName}";
|
||||
|
||||
if (!ShouldInclude(fullName))
|
||||
return;
|
||||
|
||||
var evt = new RuntimeMethodEvent
|
||||
{
|
||||
EventId = _guidGenerator.NewGuid().ToString("N", CultureInfo.InvariantCulture),
|
||||
SymbolId = $"{typeName}.{methodName}",
|
||||
MethodName = methodName,
|
||||
TypeName = typeName,
|
||||
AssemblyOrModule = assemblyOrModule,
|
||||
Timestamp = GetCurrentTime(),
|
||||
Kind = kind,
|
||||
Platform = RuntimePlatform.DotNet,
|
||||
ProcessId = Environment.ProcessId
|
||||
};
|
||||
|
||||
EmitEvent(evt);
|
||||
}
|
||||
|
||||
private bool ShouldInclude(string fullName)
|
||||
{
|
||||
// Check excludes first
|
||||
if (_excludePatterns is not null)
|
||||
{
|
||||
foreach (var pattern in _excludePatterns)
|
||||
{
|
||||
if (pattern.IsMatch(fullName))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If includes are specified, must match one
|
||||
if (_includePatterns is not null && _includePatterns.Length > 0)
|
||||
{
|
||||
foreach (var pattern in _includePatterns)
|
||||
{
|
||||
if (pattern.IsMatch(fullName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for GUID generation (injectable for testing).
|
||||
/// </summary>
|
||||
public interface IGuidGenerator
|
||||
{
|
||||
Guid NewGuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default GUID generator using Guid.NewGuid().
|
||||
/// </summary>
|
||||
public sealed class DefaultGuidGenerator : IGuidGenerator
|
||||
{
|
||||
public static readonly DefaultGuidGenerator Instance = new();
|
||||
|
||||
public Guid NewGuid() => Guid.NewGuid();
|
||||
}
|
||||
68
src/Signals/StellaOps.Signals.RuntimeAgent/IRuntimeAgent.cs
Normal file
68
src/Signals/StellaOps.Signals.RuntimeAgent/IRuntimeAgent.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
// <copyright file="IRuntimeAgent.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime collection agent contract.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create interfaces
|
||||
/// </summary>
|
||||
public interface IRuntimeAgent : IAsyncDisposable
|
||||
{
|
||||
/// <summary>Unique agent identifier.</summary>
|
||||
string AgentId { get; }
|
||||
|
||||
/// <summary>Target platform.</summary>
|
||||
RuntimePlatform Platform { get; }
|
||||
|
||||
/// <summary>Current collection posture.</summary>
|
||||
RuntimePosture Posture { get; }
|
||||
|
||||
/// <summary>Agent state.</summary>
|
||||
AgentState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start collection.
|
||||
/// </summary>
|
||||
/// <param name="options">Agent options.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task StartAsync(RuntimeAgentOptions options, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Stop collection gracefully.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task StopAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Pause collection (can be resumed).
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task PauseAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Resume collection after pause.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task ResumeAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Stream collected events.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Async stream of method events.</returns>
|
||||
IAsyncEnumerable<RuntimeMethodEvent> StreamEventsAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get collection statistics.
|
||||
/// </summary>
|
||||
AgentStatistics GetStatistics();
|
||||
|
||||
/// <summary>
|
||||
/// Update posture at runtime.
|
||||
/// </summary>
|
||||
/// <param name="posture">New posture level.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task SetPostureAsync(RuntimePosture posture, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// <copyright file="IRuntimeFactsIngest.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Service for ingesting runtime facts from agents.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create interfaces
|
||||
/// </summary>
|
||||
public interface IRuntimeFactsIngest
|
||||
{
|
||||
/// <summary>
|
||||
/// Ingest a batch of runtime method events.
|
||||
/// </summary>
|
||||
/// <param name="agentId">Source agent ID.</param>
|
||||
/// <param name="events">Events to ingest.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Number of events ingested.</returns>
|
||||
Task<int> IngestAsync(
|
||||
string agentId,
|
||||
IReadOnlyList<RuntimeMethodEvent> events,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Register an agent for ingestion.
|
||||
/// </summary>
|
||||
/// <param name="registration">Agent registration.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task RegisterAgentAsync(AgentRegistration registration, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Update agent heartbeat.
|
||||
/// </summary>
|
||||
/// <param name="agentId">Agent ID.</param>
|
||||
/// <param name="statistics">Current statistics.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task HeartbeatAsync(string agentId, AgentStatistics statistics, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister an agent.
|
||||
/// </summary>
|
||||
/// <param name="agentId">Agent ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task UnregisterAgentAsync(string agentId, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent registration information.
|
||||
/// </summary>
|
||||
public sealed record AgentRegistration
|
||||
{
|
||||
/// <summary>Unique agent ID.</summary>
|
||||
public required string AgentId { get; init; }
|
||||
|
||||
/// <summary>Target platform.</summary>
|
||||
public required RuntimePlatform Platform { get; init; }
|
||||
|
||||
/// <summary>Agent version.</summary>
|
||||
public required string AgentVersion { get; init; }
|
||||
|
||||
/// <summary>Hostname.</summary>
|
||||
public required string Hostname { get; init; }
|
||||
|
||||
/// <summary>Container ID if applicable.</summary>
|
||||
public string? ContainerId { get; init; }
|
||||
|
||||
/// <summary>Kubernetes pod name if applicable.</summary>
|
||||
public string? PodName { get; init; }
|
||||
|
||||
/// <summary>Kubernetes namespace if applicable.</summary>
|
||||
public string? Namespace { get; init; }
|
||||
|
||||
/// <summary>Target process name.</summary>
|
||||
public string? ProcessName { get; init; }
|
||||
|
||||
/// <summary>Target process ID.</summary>
|
||||
public int? ProcessId { get; init; }
|
||||
|
||||
/// <summary>Registration timestamp.</summary>
|
||||
public required DateTimeOffset RegisteredAt { get; init; }
|
||||
|
||||
/// <summary>Initial posture.</summary>
|
||||
public RuntimePosture Posture { get; init; } = RuntimePosture.Sampled;
|
||||
}
|
||||
314
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimeAgentBase.cs
Normal file
314
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimeAgentBase.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
// <copyright file="RuntimeAgentBase.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Base implementation for runtime agents.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create base implementation
|
||||
/// </summary>
|
||||
public abstract class RuntimeAgentBase : IRuntimeAgent
|
||||
{
|
||||
private readonly Channel<RuntimeMethodEvent> _eventChannel;
|
||||
private readonly ConcurrentDictionary<string, int> _observedMethods = new();
|
||||
private readonly ConcurrentDictionary<string, int> _observedTypes = new();
|
||||
private readonly ConcurrentDictionary<string, int> _observedAssemblies = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private long _totalEventsCollected;
|
||||
private long _eventsDropped;
|
||||
private long _eventsLastMinute;
|
||||
private DateTimeOffset _startTime;
|
||||
private DateTimeOffset _lastMinuteReset;
|
||||
private string? _lastError;
|
||||
private DateTimeOffset? _lastErrorTime;
|
||||
private RuntimeAgentOptions _options = RuntimeAgentOptions.Production;
|
||||
|
||||
protected RuntimeAgentBase(
|
||||
string agentId,
|
||||
RuntimePlatform platform,
|
||||
TimeProvider timeProvider,
|
||||
ILogger logger,
|
||||
int channelCapacity = 100_000)
|
||||
{
|
||||
AgentId = agentId;
|
||||
Platform = platform;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
|
||||
_eventChannel = Channel.CreateBounded<RuntimeMethodEvent>(
|
||||
new BoundedChannelOptions(channelCapacity)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.DropOldest,
|
||||
SingleReader = false,
|
||||
SingleWriter = false
|
||||
});
|
||||
|
||||
State = AgentState.Stopped;
|
||||
Posture = RuntimePosture.None;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string AgentId { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RuntimePlatform Platform { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RuntimePosture Posture { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public AgentState State { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(RuntimeAgentOptions options, CancellationToken ct)
|
||||
{
|
||||
if (State != AgentState.Stopped)
|
||||
throw new InvalidOperationException($"Cannot start agent in state {State}");
|
||||
|
||||
_options = options;
|
||||
State = AgentState.Starting;
|
||||
Posture = options.Posture;
|
||||
_startTime = _timeProvider.GetUtcNow();
|
||||
_lastMinuteReset = _startTime;
|
||||
|
||||
try
|
||||
{
|
||||
await StartCollectionAsync(options, ct).ConfigureAwait(false);
|
||||
State = AgentState.Running;
|
||||
_logger.LogInformation(
|
||||
"Agent {AgentId} started with posture {Posture}",
|
||||
AgentId, Posture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
State = AgentState.Error;
|
||||
_lastError = ex.Message;
|
||||
_lastErrorTime = _timeProvider.GetUtcNow();
|
||||
_logger.LogError(ex, "Failed to start agent {AgentId}", AgentId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken ct)
|
||||
{
|
||||
if (State != AgentState.Running && State != AgentState.Paused)
|
||||
return;
|
||||
|
||||
State = AgentState.Stopping;
|
||||
|
||||
try
|
||||
{
|
||||
await StopCollectionAsync(ct).ConfigureAwait(false);
|
||||
_eventChannel.Writer.Complete();
|
||||
State = AgentState.Stopped;
|
||||
_logger.LogInformation("Agent {AgentId} stopped", AgentId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
State = AgentState.Error;
|
||||
_lastError = ex.Message;
|
||||
_lastErrorTime = _timeProvider.GetUtcNow();
|
||||
_logger.LogError(ex, "Error stopping agent {AgentId}", AgentId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task PauseAsync(CancellationToken ct)
|
||||
{
|
||||
if (State != AgentState.Running)
|
||||
throw new InvalidOperationException($"Cannot pause agent in state {State}");
|
||||
|
||||
await PauseCollectionAsync(ct).ConfigureAwait(false);
|
||||
State = AgentState.Paused;
|
||||
_logger.LogInformation("Agent {AgentId} paused", AgentId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ResumeAsync(CancellationToken ct)
|
||||
{
|
||||
if (State != AgentState.Paused)
|
||||
throw new InvalidOperationException($"Cannot resume agent in state {State}");
|
||||
|
||||
await ResumeCollectionAsync(ct).ConfigureAwait(false);
|
||||
State = AgentState.Running;
|
||||
_logger.LogInformation("Agent {AgentId} resumed", AgentId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<RuntimeMethodEvent> StreamEventsAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
await foreach (var evt in _eventChannel.Reader.ReadAllAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
yield return evt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public AgentStatistics GetStatistics()
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
// Reset per-minute counter if needed
|
||||
if ((now - _lastMinuteReset).TotalMinutes >= 1)
|
||||
{
|
||||
Interlocked.Exchange(ref _eventsLastMinute, 0);
|
||||
_lastMinuteReset = now;
|
||||
}
|
||||
|
||||
return new AgentStatistics
|
||||
{
|
||||
AgentId = AgentId,
|
||||
Timestamp = now,
|
||||
State = State,
|
||||
Uptime = State == AgentState.Running || State == AgentState.Paused
|
||||
? now - _startTime
|
||||
: TimeSpan.Zero,
|
||||
TotalEventsCollected = Interlocked.Read(ref _totalEventsCollected),
|
||||
EventsLastMinute = Interlocked.Read(ref _eventsLastMinute),
|
||||
EventsDropped = Interlocked.Read(ref _eventsDropped),
|
||||
UniqueMethodsObserved = _observedMethods.Count,
|
||||
UniqueTypesObserved = _observedTypes.Count,
|
||||
UniqueAssembliesObserved = _observedAssemblies.Count,
|
||||
BufferUtilizationPercent = GetBufferUtilization(),
|
||||
EstimatedCpuOverheadPercent = EstimateCpuOverhead(),
|
||||
MemoryUsageBytes = GetMemoryUsage(),
|
||||
LastError = _lastError,
|
||||
LastErrorTimestamp = _lastErrorTime
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetPostureAsync(RuntimePosture posture, CancellationToken ct)
|
||||
{
|
||||
if (State != AgentState.Running && State != AgentState.Paused)
|
||||
throw new InvalidOperationException($"Cannot change posture in state {State}");
|
||||
|
||||
var oldPosture = Posture;
|
||||
await UpdatePostureAsync(posture, ct).ConfigureAwait(false);
|
||||
Posture = posture;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Agent {AgentId} posture changed from {OldPosture} to {NewPosture}",
|
||||
AgentId, oldPosture, posture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (State == AgentState.Running || State == AgentState.Paused)
|
||||
{
|
||||
await StopAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit an event to the channel.
|
||||
/// </summary>
|
||||
protected bool EmitEvent(RuntimeMethodEvent evt)
|
||||
{
|
||||
// Track unique symbols
|
||||
_observedMethods.TryAdd(evt.SymbolId, 0);
|
||||
_observedTypes.TryAdd(evt.TypeName, 0);
|
||||
_observedAssemblies.TryAdd(evt.AssemblyOrModule, 0);
|
||||
|
||||
// Try to write to channel
|
||||
if (_eventChannel.Writer.TryWrite(evt))
|
||||
{
|
||||
Interlocked.Increment(ref _totalEventsCollected);
|
||||
Interlocked.Increment(ref _eventsLastMinute);
|
||||
return true;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _eventsDropped);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record an error.
|
||||
/// </summary>
|
||||
protected void RecordError(Exception ex)
|
||||
{
|
||||
_lastError = ex.Message;
|
||||
_lastErrorTime = _timeProvider.GetUtcNow();
|
||||
_logger.LogError(ex, "Agent {AgentId} error", AgentId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current time from injected provider.
|
||||
/// </summary>
|
||||
protected DateTimeOffset GetCurrentTime() => _timeProvider.GetUtcNow();
|
||||
|
||||
/// <summary>
|
||||
/// Get current options.
|
||||
/// </summary>
|
||||
protected RuntimeAgentOptions Options => _options;
|
||||
|
||||
/// <summary>
|
||||
/// Start platform-specific collection.
|
||||
/// </summary>
|
||||
protected abstract Task StartCollectionAsync(RuntimeAgentOptions options, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Stop platform-specific collection.
|
||||
/// </summary>
|
||||
protected abstract Task StopCollectionAsync(CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Pause platform-specific collection.
|
||||
/// </summary>
|
||||
protected virtual Task PauseCollectionAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Resume platform-specific collection.
|
||||
/// </summary>
|
||||
protected virtual Task ResumeCollectionAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Update collection posture.
|
||||
/// </summary>
|
||||
protected virtual Task UpdatePostureAsync(RuntimePosture posture, CancellationToken ct) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Dispose platform-specific resources.
|
||||
/// </summary>
|
||||
protected virtual ValueTask DisposeAsyncCore() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Get buffer utilization percentage.
|
||||
/// </summary>
|
||||
protected virtual double GetBufferUtilization() => 0;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate CPU overhead.
|
||||
/// </summary>
|
||||
protected virtual double EstimateCpuOverhead() => Posture switch
|
||||
{
|
||||
RuntimePosture.None => 0,
|
||||
RuntimePosture.Passive => 0.1,
|
||||
RuntimePosture.Sampled => 1.5,
|
||||
RuntimePosture.ActiveTracing => 3.5,
|
||||
RuntimePosture.Deep => 7.5,
|
||||
RuntimePosture.Full => 25,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get memory usage.
|
||||
/// </summary>
|
||||
protected virtual long GetMemoryUsage() => GC.GetTotalMemory(forceFullCollection: false);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// <copyright file="RuntimeAgentExtensions.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// DI extensions for runtime agent services.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create DI extensions
|
||||
/// </summary>
|
||||
public static class RuntimeAgentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds runtime agent services.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRuntimeAgent(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IGuidGenerator, DefaultGuidGenerator>();
|
||||
services.AddSingleton(TimeProvider.System);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the .NET EventPipe agent.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddDotNetEventPipeAgent(
|
||||
this IServiceCollection services,
|
||||
string agentId)
|
||||
{
|
||||
services.AddRuntimeAgent();
|
||||
services.AddSingleton<IRuntimeAgent>(sp =>
|
||||
{
|
||||
var timeProvider = sp.GetRequiredService<TimeProvider>();
|
||||
var guidGenerator = sp.GetRequiredService<IGuidGenerator>();
|
||||
var logger = sp.GetRequiredService<ILogger<DotNetEventPipeAgent>>();
|
||||
return new DotNetEventPipeAgent(agentId, timeProvider, guidGenerator, logger);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// <copyright file="RuntimeAgentOptions.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for runtime agents.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record RuntimeAgentOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection posture level.
|
||||
/// </summary>
|
||||
public RuntimePosture Posture { get; init; } = RuntimePosture.Sampled;
|
||||
|
||||
/// <summary>
|
||||
/// Target process ID (null for auto-discovery).
|
||||
/// </summary>
|
||||
public int? TargetProcessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target process name pattern (regex).
|
||||
/// </summary>
|
||||
public string? TargetProcessNamePattern { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sampling interval for sampled mode.
|
||||
/// </summary>
|
||||
public TimeSpan SamplingInterval { get; init; } = TimeSpan.FromMilliseconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Event buffer size.
|
||||
/// </summary>
|
||||
public int BufferSizeMb { get; init; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum events per second (rate limiting).
|
||||
/// </summary>
|
||||
public int? MaxEventsPerSecond { get; init; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Namespace/package filter patterns (include only these).
|
||||
/// </summary>
|
||||
public ImmutableArray<string> IncludePatterns { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Namespace/package exclude patterns.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ExcludePatterns { get; init; } =
|
||||
["System.*", "Microsoft.*", "Newtonsoft.*", "NLog.*", "Serilog.*"];
|
||||
|
||||
/// <summary>
|
||||
/// Enable method enter/exit tracking.
|
||||
/// </summary>
|
||||
public bool TrackMethodEnterExit { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Enable JIT compilation tracking.
|
||||
/// </summary>
|
||||
public bool TrackJitCompilation { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable exception tracking.
|
||||
/// </summary>
|
||||
public bool TrackExceptions { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable HTTP request tracking.
|
||||
/// </summary>
|
||||
public bool TrackHttpRequests { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Container ID for correlation.
|
||||
/// </summary>
|
||||
public string? ContainerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Heartbeat interval.
|
||||
/// </summary>
|
||||
public TimeSpan HeartbeatInterval { get; init; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Default options for development.
|
||||
/// </summary>
|
||||
public static RuntimeAgentOptions Development => new()
|
||||
{
|
||||
Posture = RuntimePosture.ActiveTracing,
|
||||
TrackMethodEnterExit = true,
|
||||
MaxEventsPerSecond = 50000,
|
||||
ExcludePatterns = ["System.*", "Microsoft.*"]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Default options for production.
|
||||
/// </summary>
|
||||
public static RuntimeAgentOptions Production => new()
|
||||
{
|
||||
Posture = RuntimePosture.Sampled,
|
||||
TrackMethodEnterExit = false,
|
||||
MaxEventsPerSecond = 5000
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Low overhead options for sensitive environments.
|
||||
/// </summary>
|
||||
public static RuntimeAgentOptions LowOverhead => new()
|
||||
{
|
||||
Posture = RuntimePosture.Passive,
|
||||
TrackMethodEnterExit = false,
|
||||
TrackJitCompilation = false,
|
||||
MaxEventsPerSecond = 1000
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// <copyright file="RuntimeEventKind.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Type of runtime event observed.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create enums
|
||||
/// </summary>
|
||||
public enum RuntimeEventKind
|
||||
{
|
||||
/// <summary>Unknown event type.</summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>Method entry point.</summary>
|
||||
MethodEnter = 1,
|
||||
|
||||
/// <summary>Method exit point.</summary>
|
||||
MethodExit = 2,
|
||||
|
||||
/// <summary>Method observed via sampling.</summary>
|
||||
MethodSample = 3,
|
||||
|
||||
/// <summary>Method JIT compiled.</summary>
|
||||
MethodJit = 4,
|
||||
|
||||
/// <summary>Exception thrown.</summary>
|
||||
ExceptionThrown = 5,
|
||||
|
||||
/// <summary>Exception caught.</summary>
|
||||
ExceptionCaught = 6,
|
||||
|
||||
/// <summary>Type loaded.</summary>
|
||||
TypeLoad = 7,
|
||||
|
||||
/// <summary>Assembly/module loaded.</summary>
|
||||
AssemblyLoad = 8,
|
||||
|
||||
/// <summary>Allocation event.</summary>
|
||||
Allocation = 9,
|
||||
|
||||
/// <summary>GC event.</summary>
|
||||
GarbageCollection = 10,
|
||||
|
||||
/// <summary>Thread started.</summary>
|
||||
ThreadStart = 11,
|
||||
|
||||
/// <summary>Thread ended.</summary>
|
||||
ThreadEnd = 12,
|
||||
|
||||
/// <summary>Lock contention.</summary>
|
||||
Contention = 13,
|
||||
|
||||
/// <summary>I/O operation started.</summary>
|
||||
IoStart = 14,
|
||||
|
||||
/// <summary>I/O operation completed.</summary>
|
||||
IoComplete = 15,
|
||||
|
||||
/// <summary>HTTP request started.</summary>
|
||||
HttpRequestStart = 16,
|
||||
|
||||
/// <summary>HTTP request completed.</summary>
|
||||
HttpRequestComplete = 17
|
||||
}
|
||||
131
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimeMethodEvent.cs
Normal file
131
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimeMethodEvent.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// <copyright file="RuntimeMethodEvent.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// A single method observation event from runtime collection.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create models
|
||||
/// </summary>
|
||||
public sealed record RuntimeMethodEvent
|
||||
{
|
||||
/// <summary>Unique event ID.</summary>
|
||||
public required string EventId { get; init; }
|
||||
|
||||
/// <summary>Symbol identifier (platform-specific until normalized).</summary>
|
||||
public required string SymbolId { get; init; }
|
||||
|
||||
/// <summary>Method name.</summary>
|
||||
public required string MethodName { get; init; }
|
||||
|
||||
/// <summary>Type/class name.</summary>
|
||||
public required string TypeName { get; init; }
|
||||
|
||||
/// <summary>Assembly/module/package.</summary>
|
||||
public required string AssemblyOrModule { get; init; }
|
||||
|
||||
/// <summary>Event timestamp (UTC).</summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>Event kind.</summary>
|
||||
public required RuntimeEventKind Kind { get; init; }
|
||||
|
||||
/// <summary>Target platform.</summary>
|
||||
public RuntimePlatform Platform { get; init; }
|
||||
|
||||
/// <summary>Container ID if running in container.</summary>
|
||||
public string? ContainerId { get; init; }
|
||||
|
||||
/// <summary>Process ID.</summary>
|
||||
public int? ProcessId { get; init; }
|
||||
|
||||
/// <summary>Thread ID.</summary>
|
||||
public string? ThreadId { get; init; }
|
||||
|
||||
/// <summary>Call depth (for enter/exit correlation).</summary>
|
||||
public int? CallDepth { get; init; }
|
||||
|
||||
/// <summary>Duration in microseconds (for exit events).</summary>
|
||||
public long? DurationMicroseconds { get; init; }
|
||||
|
||||
/// <summary>Source file path if available.</summary>
|
||||
public string? SourceFilePath { get; init; }
|
||||
|
||||
/// <summary>Source line number if available.</summary>
|
||||
public int? SourceLineNumber { get; init; }
|
||||
|
||||
/// <summary>Additional context.</summary>
|
||||
public ImmutableDictionary<string, string> Context { get; init; } =
|
||||
ImmutableDictionary<string, string>.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a method enter event.
|
||||
/// </summary>
|
||||
public static RuntimeMethodEvent MethodEnter(
|
||||
string eventId,
|
||||
string symbolId,
|
||||
string methodName,
|
||||
string typeName,
|
||||
string assemblyOrModule,
|
||||
DateTimeOffset timestamp,
|
||||
RuntimePlatform platform) => new()
|
||||
{
|
||||
EventId = eventId,
|
||||
SymbolId = symbolId,
|
||||
MethodName = methodName,
|
||||
TypeName = typeName,
|
||||
AssemblyOrModule = assemblyOrModule,
|
||||
Timestamp = timestamp,
|
||||
Kind = RuntimeEventKind.MethodEnter,
|
||||
Platform = platform
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a method exit event.
|
||||
/// </summary>
|
||||
public static RuntimeMethodEvent MethodExit(
|
||||
string eventId,
|
||||
string symbolId,
|
||||
string methodName,
|
||||
string typeName,
|
||||
string assemblyOrModule,
|
||||
DateTimeOffset timestamp,
|
||||
RuntimePlatform platform,
|
||||
long durationMicroseconds) => new()
|
||||
{
|
||||
EventId = eventId,
|
||||
SymbolId = symbolId,
|
||||
MethodName = methodName,
|
||||
TypeName = typeName,
|
||||
AssemblyOrModule = assemblyOrModule,
|
||||
Timestamp = timestamp,
|
||||
Kind = RuntimeEventKind.MethodExit,
|
||||
Platform = platform,
|
||||
DurationMicroseconds = durationMicroseconds
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a method sample event.
|
||||
/// </summary>
|
||||
public static RuntimeMethodEvent MethodSample(
|
||||
string eventId,
|
||||
string symbolId,
|
||||
string methodName,
|
||||
string typeName,
|
||||
string assemblyOrModule,
|
||||
DateTimeOffset timestamp,
|
||||
RuntimePlatform platform) => new()
|
||||
{
|
||||
EventId = eventId,
|
||||
SymbolId = symbolId,
|
||||
MethodName = methodName,
|
||||
TypeName = typeName,
|
||||
AssemblyOrModule = assemblyOrModule,
|
||||
Timestamp = timestamp,
|
||||
Kind = RuntimeEventKind.MethodSample,
|
||||
Platform = platform
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// <copyright file="RuntimePlatform.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Target runtime platform for collection.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create enums
|
||||
/// </summary>
|
||||
public enum RuntimePlatform
|
||||
{
|
||||
/// <summary>Unknown platform.</summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>.NET runtime (CoreCLR, .NET Framework).</summary>
|
||||
DotNet = 1,
|
||||
|
||||
/// <summary>Java Virtual Machine.</summary>
|
||||
Java = 2,
|
||||
|
||||
/// <summary>Native (C/C++/Rust).</summary>
|
||||
Native = 3,
|
||||
|
||||
/// <summary>Node.js/JavaScript.</summary>
|
||||
NodeJs = 4,
|
||||
|
||||
/// <summary>Python runtime.</summary>
|
||||
Python = 5,
|
||||
|
||||
/// <summary>Go runtime.</summary>
|
||||
Go = 6,
|
||||
|
||||
/// <summary>Ruby runtime.</summary>
|
||||
Ruby = 7
|
||||
}
|
||||
51
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimePosture.cs
Normal file
51
src/Signals/StellaOps.Signals.RuntimeAgent/RuntimePosture.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// <copyright file="RuntimePosture.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Collection intensity level for runtime agents.
|
||||
/// Higher levels provide more data but incur more overhead.
|
||||
/// Sprint: SPRINT_20260109_009_004 Task: Create enums
|
||||
/// </summary>
|
||||
public enum RuntimePosture
|
||||
{
|
||||
/// <summary>No collection.</summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Passive logging only.
|
||||
/// Overhead: ~0%
|
||||
/// Data: Application logs mentioning method names.
|
||||
/// </summary>
|
||||
Passive = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Sampled tracing.
|
||||
/// Overhead: ~1-2%
|
||||
/// Data: Statistical sampling of hot methods.
|
||||
/// </summary>
|
||||
Sampled = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Active tracing with method enter/exit.
|
||||
/// Overhead: ~2-5%
|
||||
/// Data: All method calls (sampled or filtered).
|
||||
/// </summary>
|
||||
ActiveTracing = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Deep instrumentation (eBPF, CLR Profiler).
|
||||
/// Overhead: ~5-10%
|
||||
/// Data: Full call stacks, arguments (limited).
|
||||
/// </summary>
|
||||
Deep = 4,
|
||||
|
||||
/// <summary>
|
||||
/// Full instrumentation (development only).
|
||||
/// Overhead: ~10-50%
|
||||
/// Data: Everything including local variables.
|
||||
/// </summary>
|
||||
Full = 5
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<RootNamespace>StellaOps.Signals.RuntimeAgent</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Reachability.Core\StellaOps.Reachability.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,97 @@
|
||||
// <copyright file="AgentStatisticsTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="AgentStatistics"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class AgentStatisticsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Empty_HasZeroValues()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 9, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var stats = AgentStatistics.Empty("test-agent", timeProvider);
|
||||
|
||||
stats.AgentId.Should().Be("test-agent");
|
||||
stats.State.Should().Be(AgentState.Stopped);
|
||||
stats.TotalEventsCollected.Should().Be(0);
|
||||
stats.EventsLastMinute.Should().Be(0);
|
||||
stats.EventsDropped.Should().Be(0);
|
||||
stats.UniqueMethodsObserved.Should().Be(0);
|
||||
stats.Uptime.Should().Be(TimeSpan.Zero);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_UsesProvidedTimestamp()
|
||||
{
|
||||
var timeProvider = new FakeTimeProvider();
|
||||
var expectedTime = new DateTimeOffset(2026, 1, 9, 12, 0, 0, TimeSpan.Zero);
|
||||
timeProvider.SetUtcNow(expectedTime);
|
||||
|
||||
var stats = AgentStatistics.Empty("test-agent", timeProvider);
|
||||
|
||||
stats.Timestamp.Should().Be(expectedTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Statistics_CanBeCreatedWithValues()
|
||||
{
|
||||
var stats = new AgentStatistics
|
||||
{
|
||||
AgentId = "agent-123",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
State = AgentState.Running,
|
||||
Uptime = TimeSpan.FromHours(2),
|
||||
TotalEventsCollected = 50000,
|
||||
EventsLastMinute = 1234,
|
||||
EventsDropped = 50,
|
||||
UniqueMethodsObserved = 500,
|
||||
UniqueTypesObserved = 100,
|
||||
UniqueAssembliesObserved = 20,
|
||||
BufferUtilizationPercent = 45.5,
|
||||
EstimatedCpuOverheadPercent = 2.3,
|
||||
MemoryUsageBytes = 1024 * 1024 * 50
|
||||
};
|
||||
|
||||
stats.AgentId.Should().Be("agent-123");
|
||||
stats.TotalEventsCollected.Should().Be(50000);
|
||||
stats.BufferUtilizationPercent.Should().BeApproximately(45.5, 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Statistics_CanTrackError()
|
||||
{
|
||||
var stats = new AgentStatistics
|
||||
{
|
||||
AgentId = "agent-123",
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
State = AgentState.Error,
|
||||
Uptime = TimeSpan.FromMinutes(5),
|
||||
TotalEventsCollected = 1000,
|
||||
EventsLastMinute = 0,
|
||||
EventsDropped = 0,
|
||||
UniqueMethodsObserved = 100,
|
||||
UniqueTypesObserved = 50,
|
||||
UniqueAssembliesObserved = 10,
|
||||
BufferUtilizationPercent = 0,
|
||||
EstimatedCpuOverheadPercent = 0,
|
||||
MemoryUsageBytes = 0,
|
||||
LastError = "Connection lost",
|
||||
LastErrorTimestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
stats.State.Should().Be(AgentState.Error);
|
||||
stats.LastError.Should().Be("Connection lost");
|
||||
stats.LastErrorTimestamp.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// <copyright file="DotNetEventPipeAgentTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DotNetEventPipeAgent"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class DotNetEventPipeAgentTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly TestGuidGenerator _guidGenerator = new();
|
||||
|
||||
[Fact]
|
||||
public void Platform_IsDotNet()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
|
||||
agent.Platform.Should().Be(RuntimePlatform.DotNet);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitMethodObservation_WithIncludePattern_EmitsMatchingEvent()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
var options = RuntimeAgentOptions.Development with
|
||||
{
|
||||
IncludePatterns = ["MyApp.*"],
|
||||
ExcludePatterns = []
|
||||
};
|
||||
await agent.StartAsync(options, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("ProcessData", "MyApp.Services.DataService", "MyApp.dll");
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
stats.TotalEventsCollected.Should().Be(1);
|
||||
stats.UniqueMethodsObserved.Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitMethodObservation_WithExcludePattern_FiltersEvent()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
var options = RuntimeAgentOptions.Development with
|
||||
{
|
||||
ExcludePatterns = ["System.*"]
|
||||
};
|
||||
await agent.StartAsync(options, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("ToString", "System.String", "System.Runtime.dll");
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
stats.TotalEventsCollected.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitMethodObservation_TracksUniqueMethods()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development with { ExcludePatterns = [] }, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("Method1", "Type1", "Assembly1");
|
||||
agent.EmitMethodObservation("Method1", "Type1", "Assembly1"); // Same
|
||||
agent.EmitMethodObservation("Method2", "Type1", "Assembly1"); // Different method
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
stats.TotalEventsCollected.Should().Be(3);
|
||||
stats.UniqueMethodsObserved.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitMethodObservation_TracksUniqueTypes()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development with { ExcludePatterns = [] }, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("Method1", "Type1", "Assembly1");
|
||||
agent.EmitMethodObservation("Method2", "Type2", "Assembly1");
|
||||
agent.EmitMethodObservation("Method3", "Type1", "Assembly1"); // Same type
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
stats.UniqueTypesObserved.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmitMethodObservation_TracksUniqueAssemblies()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development with { ExcludePatterns = [] }, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("Method1", "Type1", "Assembly1.dll");
|
||||
agent.EmitMethodObservation("Method2", "Type2", "Assembly2.dll");
|
||||
agent.EmitMethodObservation("Method3", "Type3", "Assembly1.dll"); // Same assembly
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
stats.UniqueAssembliesObserved.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StreamEventsAsync_YieldsEmittedEvents()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development with { ExcludePatterns = [] }, CancellationToken.None);
|
||||
|
||||
agent.EmitMethodObservation("Method1", "Type1", "Assembly1");
|
||||
agent.EmitMethodObservation("Method2", "Type2", "Assembly2");
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
|
||||
var events = new List<RuntimeMethodEvent>();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var evt in agent.StreamEventsAsync(cts.Token))
|
||||
{
|
||||
events.Add(evt);
|
||||
if (events.Count >= 2)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected
|
||||
}
|
||||
|
||||
events.Should().HaveCount(2);
|
||||
events[0].MethodName.Should().Be("Method1");
|
||||
events[1].MethodName.Should().Be("Method2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatistics_TracksUptime()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
_timeProvider.SetUtcNow(new DateTimeOffset(2026, 1, 9, 10, 0, 0, TimeSpan.Zero));
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
_timeProvider.Advance(TimeSpan.FromMinutes(5));
|
||||
var stats = agent.GetStatistics();
|
||||
|
||||
stats.Uptime.Should().Be(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
private DotNetEventPipeAgent CreateAgent()
|
||||
{
|
||||
return new DotNetEventPipeAgent(
|
||||
"test-agent",
|
||||
_timeProvider,
|
||||
_guidGenerator,
|
||||
NullLogger<DotNetEventPipeAgent>.Instance);
|
||||
}
|
||||
|
||||
private sealed class TestGuidGenerator : IGuidGenerator
|
||||
{
|
||||
private int _counter;
|
||||
|
||||
public Guid NewGuid()
|
||||
{
|
||||
var bytes = new byte[16];
|
||||
BitConverter.GetBytes(++_counter).CopyTo(bytes, 0);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// <copyright file="RuntimeAgentBaseTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RuntimeAgentBase"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class RuntimeAgentBaseTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider = new();
|
||||
private readonly TestGuidGenerator _guidGenerator = new();
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesProperties()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
|
||||
agent.AgentId.Should().Be("test-agent");
|
||||
agent.Platform.Should().Be(RuntimePlatform.DotNet);
|
||||
agent.State.Should().Be(AgentState.Stopped);
|
||||
agent.Posture.Should().Be(RuntimePosture.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_TransitionsToRunning()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
var options = RuntimeAgentOptions.Development;
|
||||
|
||||
await agent.StartAsync(options, CancellationToken.None);
|
||||
|
||||
agent.State.Should().Be(AgentState.Running);
|
||||
agent.Posture.Should().Be(RuntimePosture.ActiveTracing);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_WhenAlreadyRunning_Throws()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
var act = () => agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopAsync_TransitionsToStopped()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
await agent.StopAsync(CancellationToken.None);
|
||||
|
||||
agent.State.Should().Be(AgentState.Stopped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PauseAsync_TransitionsToPaused()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
await agent.PauseAsync(CancellationToken.None);
|
||||
|
||||
agent.State.Should().Be(AgentState.Paused);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResumeAsync_TransitionsBackToRunning()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
await agent.PauseAsync(CancellationToken.None);
|
||||
|
||||
await agent.ResumeAsync(CancellationToken.None);
|
||||
|
||||
agent.State.Should().Be(AgentState.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetPostureAsync_ChangesPosture()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
await agent.SetPostureAsync(RuntimePosture.Sampled, CancellationToken.None);
|
||||
|
||||
agent.Posture.Should().Be(RuntimePosture.Sampled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStatistics_ReturnsStatistics()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
var stats = agent.GetStatistics();
|
||||
|
||||
stats.AgentId.Should().Be("test-agent");
|
||||
stats.State.Should().Be(AgentState.Running);
|
||||
stats.TotalEventsCollected.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_StopsAgent()
|
||||
{
|
||||
var agent = CreateAgent();
|
||||
await agent.StartAsync(RuntimeAgentOptions.Development, CancellationToken.None);
|
||||
|
||||
await agent.DisposeAsync();
|
||||
|
||||
agent.State.Should().Be(AgentState.Stopped);
|
||||
}
|
||||
|
||||
private DotNetEventPipeAgent CreateAgent()
|
||||
{
|
||||
return new DotNetEventPipeAgent(
|
||||
"test-agent",
|
||||
_timeProvider,
|
||||
_guidGenerator,
|
||||
NullLogger<DotNetEventPipeAgent>.Instance);
|
||||
}
|
||||
|
||||
private sealed class TestGuidGenerator : IGuidGenerator
|
||||
{
|
||||
private int _counter;
|
||||
|
||||
public Guid NewGuid()
|
||||
{
|
||||
var bytes = new byte[16];
|
||||
BitConverter.GetBytes(++_counter).CopyTo(bytes, 0);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// <copyright file="RuntimeAgentOptionsTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Signals.RuntimeAgent.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RuntimeAgentOptions"/>.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public class RuntimeAgentOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_HasSampledPosture()
|
||||
{
|
||||
var options = new RuntimeAgentOptions();
|
||||
|
||||
options.Posture.Should().Be(RuntimePosture.Sampled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_HasExcludePatterns()
|
||||
{
|
||||
var options = new RuntimeAgentOptions();
|
||||
|
||||
options.ExcludePatterns.Should().NotBeEmpty();
|
||||
options.ExcludePatterns.Should().Contain("System.*");
|
||||
options.ExcludePatterns.Should().Contain("Microsoft.*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Development_HasActiveTracingPosture()
|
||||
{
|
||||
var options = RuntimeAgentOptions.Development;
|
||||
|
||||
options.Posture.Should().Be(RuntimePosture.ActiveTracing);
|
||||
options.TrackMethodEnterExit.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Production_HasSampledPosture()
|
||||
{
|
||||
var options = RuntimeAgentOptions.Production;
|
||||
|
||||
options.Posture.Should().Be(RuntimePosture.Sampled);
|
||||
options.TrackMethodEnterExit.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LowOverhead_HasPassivePosture()
|
||||
{
|
||||
var options = RuntimeAgentOptions.LowOverhead;
|
||||
|
||||
options.Posture.Should().Be(RuntimePosture.Passive);
|
||||
options.TrackJitCompilation.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_HasRateLimiting()
|
||||
{
|
||||
var options = new RuntimeAgentOptions();
|
||||
|
||||
options.MaxEventsPerSecond.Should().Be(10000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_HasHeartbeatInterval()
|
||||
{
|
||||
var options = new RuntimeAgentOptions();
|
||||
|
||||
options.HeartbeatInterval.Should().Be(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithPattern_CanCustomizePatterns()
|
||||
{
|
||||
var options = new RuntimeAgentOptions
|
||||
{
|
||||
IncludePatterns = ["MyApp.*"],
|
||||
ExcludePatterns = []
|
||||
};
|
||||
|
||||
options.IncludePatterns.Should().ContainSingle().Which.Should().Be("MyApp.*");
|
||||
options.ExcludePatterns.Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Signals.RuntimeAgent\StellaOps.Signals.RuntimeAgent.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user