save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View 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
}

View File

@@ -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
};
}

View File

@@ -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();
}

View 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);
}

View File

@@ -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;
}

View 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);
}

View File

@@ -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;
}
}

View File

@@ -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
};
}

View File

@@ -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
}

View 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
};
}

View File

@@ -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
}

View 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
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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>