diff --git a/src/Persistence/Wolverine.CosmosDb/Internals/Durability/CosmosDbDurabilityAgent.cs b/src/Persistence/Wolverine.CosmosDb/Internals/Durability/CosmosDbDurabilityAgent.cs index 98baf628b..9a4254f31 100644 --- a/src/Persistence/Wolverine.CosmosDb/Internals/Durability/CosmosDbDurabilityAgent.cs +++ b/src/Persistence/Wolverine.CosmosDb/Internals/Durability/CosmosDbDurabilityAgent.cs @@ -157,6 +157,12 @@ public Task StopAsync(CancellationToken cancellationToken) public Uri Uri { get; set; } public AgentStatus Status { get; set; } + /// + /// Human-readable description for monitoring tools — see + /// . + /// + public string Description => $"Wolverine Cosmos DB durability agent for {Uri} — recovers persisted inbox/outbox messages and runs scheduled jobs against the Cosmos DB message store."; + /// /// True once has wired up the recovery and scheduled-job /// background loops. Exposed for diagnostic and test inspection so callers can detect diff --git a/src/Persistence/Wolverine.Postgresql/Transport/StickyPostgresqlQueueListenerAgent.cs b/src/Persistence/Wolverine.Postgresql/Transport/StickyPostgresqlQueueListenerAgent.cs index 1e7c34c97..456f916e6 100644 --- a/src/Persistence/Wolverine.Postgresql/Transport/StickyPostgresqlQueueListenerAgent.cs +++ b/src/Persistence/Wolverine.Postgresql/Transport/StickyPostgresqlQueueListenerAgent.cs @@ -52,4 +52,10 @@ public async Task StopAsync(CancellationToken cancellationToken) } public Uri Uri { get; } + + /// + /// Human-readable description for monitoring tools — see + /// . + /// + public string Description => $"Sticky Postgres queue listener — pinned to the per-tenant database '{_databaseName}' for queue '{_queue}'. Only one node listens to each tenant database to avoid duplicate consumption."; } \ No newline at end of file diff --git a/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs b/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs index ab59253b4..df28f7148 100644 --- a/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs +++ b/src/Persistence/Wolverine.RDBMS/DurabilityAgent.cs @@ -149,6 +149,16 @@ public async Task StopAsync(CancellationToken cancellationToken) public Uri Uri { get; internal set; } + /// + /// Human-readable description for monitoring tools — see + /// . This agent recovers + /// persisted inbox / outbox messages from the relational + /// message store, runs scheduled jobs, and emits persistence + /// metrics. The URI carries the store identity so operators + /// can disambiguate when a service has multiple stores. + /// + public string Description => $"Wolverine durability agent for {Uri} — recovers persisted inbox/outbox messages, runs scheduled jobs, and emits persistence metrics."; + public static Uri SimplifyUri(Uri uri) { return new Uri($"{PersistenceConstants.AgentScheme}://{uri.Host}"); diff --git a/src/Persistence/Wolverine.RavenDb/Internals/Durability/RavenDbDurabilityAgent.cs b/src/Persistence/Wolverine.RavenDb/Internals/Durability/RavenDbDurabilityAgent.cs index 164b43fb7..76facd12e 100644 --- a/src/Persistence/Wolverine.RavenDb/Internals/Durability/RavenDbDurabilityAgent.cs +++ b/src/Persistence/Wolverine.RavenDb/Internals/Durability/RavenDbDurabilityAgent.cs @@ -139,6 +139,12 @@ public Task StopAsync(CancellationToken cancellationToken) public Uri Uri { get; set; } public AgentStatus Status { get; set; } + /// + /// Human-readable description for monitoring tools — see + /// . + /// + public string Description => $"Wolverine RavenDB durability agent for {Uri} — recovers persisted inbox/outbox messages and runs scheduled jobs against the RavenDB message store."; + /// /// True once has wired up the recovery and scheduled-job /// background loops. Exposed for diagnostic and test inspection so callers can detect diff --git a/src/Testing/CoreTests/Runtime/Agents/IAgentDescriptionTests.cs b/src/Testing/CoreTests/Runtime/Agents/IAgentDescriptionTests.cs new file mode 100644 index 000000000..216383da6 --- /dev/null +++ b/src/Testing/CoreTests/Runtime/Agents/IAgentDescriptionTests.cs @@ -0,0 +1,87 @@ +using JasperFx; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Shouldly; +using Wolverine.Runtime.Agents; +using Xunit; + +namespace CoreTests.Runtime.Agents; + +/// +/// Coverage for — the human-readable +/// "what does this agent do" string surfaced to monitoring tools +/// (e.g. CritterWatch). Verifies the default-interface fallback for +/// implementations that don't override it, and verifies an explicit +/// override wins through. +/// +public class IAgentDescriptionTests +{ + [Fact] + public void default_description_includes_uri_scheme_and_full_uri() + { + // Default-interface members are only accessible through the + // interface, not the concrete type — so cast deliberately. + // The default implementation derives a generic description + // from the URI; implementations are expected to override for + // anything more specific, but the fallback should always + // produce a non-empty string identifying scheme and URI so a + // monitoring tool isn't left with a blank tooltip. + IAgent agent = new BareBonesAgent(new Uri("test-agent://default")); + + agent.Description.ShouldNotBeNullOrWhiteSpace(); + agent.Description.ShouldContain("test-agent"); + agent.Description.ShouldContain(agent.Uri.ToString()); + } + + [Fact] + public void overridden_description_wins_over_default() + { + IAgent agent = new DescribedAgent( + new Uri("test-agent://configured"), + "Custom description for this agent"); + + agent.Description.ShouldBe("Custom description for this agent"); + agent.Description.ShouldNotContain("test-agent://configured"); + } + + /// + /// Minimal IAgent that doesn't override Description. The default + /// interface member should provide the fallback — covers the + /// "existing IAgent implementations stay source-compatible" + /// guarantee. + /// + private sealed class BareBonesAgent : IAgent + { + public BareBonesAgent(Uri uri) + { + Uri = uri; + } + + public Uri Uri { get; } + public AgentStatus Status => AgentStatus.Stopped; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + /// + /// Agent that explicitly overrides the description — exercises + /// the implementation-side override path that built-in agents + /// (DurabilityAgent, ExclusiveListenerAgent, etc.) use to give + /// CritterWatch operators agent-specific tooltip text. + /// + private sealed class DescribedAgent : IAgent + { + private readonly string _description; + + public DescribedAgent(Uri uri, string description) + { + Uri = uri; + _description = description; + } + + public Uri Uri { get; } + public AgentStatus Status => AgentStatus.Stopped; + public string Description => _description; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/Wolverine/Runtime/Agents/ExclusiveListenerFamily.cs b/src/Wolverine/Runtime/Agents/ExclusiveListenerFamily.cs index 509e7e8e2..22180377d 100644 --- a/src/Wolverine/Runtime/Agents/ExclusiveListenerFamily.cs +++ b/src/Wolverine/Runtime/Agents/ExclusiveListenerFamily.cs @@ -34,6 +34,12 @@ public async Task StopAsync(CancellationToken cancellationToken) public AgentStatus Status { get; set; } = AgentStatus.Running; + /// + /// Human-readable description for monitoring tools — see + /// . + /// + public string Description => $"Exclusive listener for {_endpoint.Uri} — only one node at a time holds the listening role for this endpoint, so the cluster avoids competing consumers."; + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { diff --git a/src/Wolverine/Runtime/Agents/IAgent.cs b/src/Wolverine/Runtime/Agents/IAgent.cs index 118837331..a769ef536 100644 --- a/src/Wolverine/Runtime/Agents/IAgent.cs +++ b/src/Wolverine/Runtime/Agents/IAgent.cs @@ -23,6 +23,18 @@ public interface IAgent : IHostedService, IHealthCheck /// AgentStatus Status { get; } + /// + /// Human-readable description of what this agent does on a given + /// node. Surfaced in monitoring tools (e.g. CritterWatch) so + /// operators don't have to recognise an agent purely by its URI + /// scheme. The default implementation returns a generic + /// "{scheme} agent: {Uri}" string; override in concrete agent + /// types to provide more specific text. Kept as a default + /// interface member so existing + /// implementations stay source-compatible. + /// + string Description => $"{Uri.Scheme} agent: {Uri}"; + /// /// Default health check implementation based on agent status. /// Override in implementations for more specific health reporting. diff --git a/src/Wolverine/Runtime/Agents/LeaderPinnedAgentFamily.cs b/src/Wolverine/Runtime/Agents/LeaderPinnedAgentFamily.cs index da16d7791..c2c0bcb49 100644 --- a/src/Wolverine/Runtime/Agents/LeaderPinnedAgentFamily.cs +++ b/src/Wolverine/Runtime/Agents/LeaderPinnedAgentFamily.cs @@ -34,6 +34,12 @@ public async Task StopAsync(CancellationToken cancellationToken) public AgentStatus Status { get; set; } = AgentStatus.Running; + /// + /// Human-readable description for monitoring tools — see + /// . + /// + public string Description => $"Leader-pinned listener for {_endpoint.Uri} — runs only on the cluster's elected leader node, so the listening role moves with leader elections."; + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) {