diff --git a/Darling/Darling.Tests/CustomAlertTemplatesTests.cs b/Darling/Darling.Tests/CustomAlertTemplatesTests.cs new file mode 100644 index 000000000..44445748b --- /dev/null +++ b/Darling/Darling.Tests/CustomAlertTemplatesTests.cs @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using PerformanceMonitor.Darling.Service; +using PerformanceMonitor.Darling.Service.Mcp; +using Xunit; + +namespace Darling.Tests; + +/// +/// #3285 (plan Component 7): the starter custom-alert templates' drift-guard, the alert twin of +/// ViewTemplatesTests. A stored rule is validated once at write time and never again, so a template +/// naming a measure that later drifts out of the catalog would ship a rule that can never fire and nothing +/// would grep it. The templates are CODE, so the SAME authority the evaluator and create/update run +/// (, over the live MeasureCatalog) is applied to every +/// one on every build — a drifted measure fails here rather than shipping dead. +/// +public sealed class CustomAlertTemplatesTests +{ + [Fact] + public void EveryTemplate_ValidatesAgainstTheLiveRuleValidatorAndCatalog() + { + Assert.NotEmpty(CustomAlertTemplates.All); + + foreach (var template in CustomAlertTemplates.All) + { + var (def, error) = CustomAlertRuleDefinition.TryParse(template.DefinitionJson); + Assert.True( + error is null && def is not null, + $"template '{template.Key}' no longer validates against the live rule-validator + catalog: {error}"); + } + } + + [Fact] + public void EveryTemplate_HasAUniqueKey_AndSaysWhatItIsFor() + { + var keys = CustomAlertTemplates.All.Select(t => t.Key).ToArray(); + + Assert.True(keys.Length >= 8, "expected the full starter set; found " + keys.Length); + Assert.Equal(keys.Length, keys.Distinct(StringComparer.Ordinal).Count()); + Assert.All(CustomAlertTemplates.All, t => + { + Assert.False(string.IsNullOrWhiteSpace(t.Key)); + Assert.False(string.IsNullOrWhiteSpace(t.Name)); + Assert.False(string.IsNullOrWhiteSpace(t.Description)); + }); + } + + [Fact] + public void EveryTemplate_IsAScalarRuleWithAPredicate_AndBothSeverityTiers() + { + foreach (var template in CustomAlertTemplates.All) + { + var (def, error) = CustomAlertRuleDefinition.TryParse(template.DefinitionJson); + Assert.Null(error); + Assert.NotNull(def); + + // A rule window is real and alert-appropriate; TryParse enforces the Scalar-only + predicate rules, + // and every starter ships a critical tier above warn (a user can drop it when they create the rule). + Assert.True(def!.WindowHours > 0); + Assert.NotNull(def.CriticalThreshold); + } + } + + [Fact] + public async Task ListCustomAlertTemplatesTool_ReturnsEveryTemplate_WithAParsedDefinitionObject() + { + var json = await DarlingMcpCustomAlertTools.ListCustomAlertTemplates(); + var root = (JsonObject)JsonNode.Parse(json)!; + var templates = (JsonArray)root["templates"]!; + + Assert.Equal(CustomAlertTemplates.All.Count, templates.Count); + + var toolKeys = templates.Select(t => (string?)((JsonObject)t!)["key"]).ToHashSet(StringComparer.Ordinal); + Assert.Equal(CustomAlertTemplates.All.Select(t => t.Key).ToHashSet(StringComparer.Ordinal), toolKeys); + + foreach (var entry in templates) + { + var obj = (JsonObject)entry!; + Assert.False(string.IsNullOrWhiteSpace((string?)obj["name"])); + Assert.False(string.IsNullOrWhiteSpace((string?)obj["description"])); + + // The definition is embedded as a JSON OBJECT (not an escaped string), ready to hand to + // create_custom_alert_rule / test_custom_alert_rule, and it still validates. + var definition = obj["definition"] as JsonObject; + Assert.NotNull(definition); + var (parsed, error) = CustomAlertRuleDefinition.TryParse(definition!.ToJsonString()); + Assert.True(error is null && parsed is not null, error); + } + } +} diff --git a/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs b/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs index e010ecda7..8aa786866 100644 --- a/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs +++ b/Darling/Darling.Tests/DarlingMcpCustomAlertToolsTests.cs @@ -22,8 +22,9 @@ namespace Darling.Tests; /// /// Ungated (no-live-store) contract for the #3285 custom-alert-rule MCP tools: the tool surface is EXACTLY the -/// seven management tools (the six CRUD/validate tools plus #3299's test_custom_alert_rule evaluate-now; all -/// static, on a [McpServerToolType] class, returning Task<string>), the advertised tools/list schema is +/// eight management tools (the six CRUD/validate tools, #3299's test_custom_alert_rule evaluate-now, and #3285 +/// Component 7's list_custom_alert_templates; all static, on a [McpServerToolType] class, returning +/// Task<string>), the advertised tools/list schema is /// Gemini-clean (#1074) with the expected required-param set, and validate / create / update run the SAME /// CustomAlertRuleDefinition.TryParse authority (the one the evaluator uses) BEFORE any persistence - an invalid /// definition never reaches the store. The live CRUD round-trip is gated below. @@ -52,6 +53,7 @@ public sealed class DarlingMcpCustomAlertToolsSurfaceTests "delete_custom_alert_rule", "get_custom_alert_rule", "list_custom_alert_rules", + "list_custom_alert_templates", "test_custom_alert_rule", "update_custom_alert_rule", "validate_custom_alert_rule", @@ -63,7 +65,7 @@ private static MethodInfo[] ToolMethods() => typeof(DarlingMcpCustomAlertTools) .ToArray(); [Fact] - public void ToolSurface_IsExactlyTheSevenCustomAlertTools() + public void ToolSurface_IsExactlyTheEightCustomAlertTools() { var toolMethods = ToolMethods(); var names = toolMethods @@ -87,10 +89,10 @@ public void ToolSurface_IsExactlyTheSevenCustomAlertTools() } [Fact] - public void AdvertisedSchema_IsGeminiClean_ForAllSevenTools() + public void AdvertisedSchema_IsGeminiClean_ForAllEightTools() { var tools = BuildToolSchemas(); - Assert.Equal(7, tools.Count); + Assert.Equal(8, tools.Count); var violations = tools.Values.SelectMany(t => DarlingMcpSchemaAssert.Violations(t.Name, t.InputSchema)).ToList(); Assert.True(violations.Count == 0, "Gemini-incompatible schema keywords leaked:\n" + string.Join("\n", violations)); } @@ -104,6 +106,8 @@ public void AdvertisedSchema_IsGeminiClean_ForAllSevenTools() [InlineData("delete_custom_alert_rule", "rule_id")] /* #3299: both inputs are optional (the tool enforces "exactly one" at runtime, not via required-schema). */ [InlineData("test_custom_alert_rule", "")] + /* #3285 Component 7: the template list takes no parameters. */ + [InlineData("list_custom_alert_templates", "")] public void AdvertisedSchema_RequiredParams_MatchTheContract(string toolName, string expectedCsv) { var expected = expectedCsv.Length == 0 ? Array.Empty() : expectedCsv.Split(','); diff --git a/Darling/Darling.Tests/DarlingWebEndpointsTests.cs b/Darling/Darling.Tests/DarlingWebEndpointsTests.cs index fdb5f46fa..741f6b796 100644 --- a/Darling/Darling.Tests/DarlingWebEndpointsTests.cs +++ b/Darling/Darling.Tests/DarlingWebEndpointsTests.cs @@ -84,8 +84,9 @@ public void ExcludedToolNames_AreTheNonReadSurfaceTools() { /* The six original non-read tools (analyze_server, the mute write, the four analyze_*_plan), the eight Custom Views tools (#1599 + describe_custom_view_catalog) served by /api/views + /api/compose/run + - /api/catalog, the seven custom-alert-rule tools (#3285 — create/update/delete write, get/list/validate - read against the compose catalog, and test_custom_alert_rule (#3299) evaluate-now, none a + /api/catalog, the eight custom-alert-rule tools (#3285 — create/update/delete write, get/list/validate + read against the compose catalog, test_custom_alert_rule (#3299) evaluate-now, and + list_custom_alert_templates (#3285 Component 7) starter templates, none a /api/read/{tool} mirror), the three alert-tuning WRITE tools, and the two server-onboarding WRITE tools (add_servers / remove_server) — all with no /api/read/{tool} 1:1 mirror, like mute_analysis_finding. */ Assert.Equal( @@ -94,7 +95,7 @@ Custom Views tools (#1599 + describe_custom_view_catalog) served by /api/views + "add_servers", "analyze_plan_xml", "analyze_procedure_plan", "analyze_query_plan", "analyze_query_store_plan", "analyze_server", "create_custom_alert_rule", "create_custom_view", "create_mute_rule", "delete_custom_alert_rule", "delete_custom_view", "delete_mute_rule", "describe_custom_view_catalog", "get_custom_alert_rule", "get_custom_view", - "list_custom_alert_rules", "list_custom_views", "mute_analysis_finding", "remove_server", "run_custom_view_panel", + "list_custom_alert_rules", "list_custom_alert_templates", "list_custom_views", "mute_analysis_finding", "remove_server", "run_custom_view_panel", "test_custom_alert_rule", "update_alert_settings", "update_custom_alert_rule", "update_custom_view", "validate_custom_alert_rule", "validate_custom_view", }, DarlingWebEndpoints.ExcludedToolNames.OrderBy(n => n, StringComparer.Ordinal).ToArray()); diff --git a/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs b/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs new file mode 100644 index 000000000..a26eea947 --- /dev/null +++ b/Darling/PerformanceMonitor.Darling.Service/CustomAlertTemplates.cs @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System.Collections.Generic; + +namespace PerformanceMonitor.Darling.Service; + +/// One starter custom-alert-rule template (#3285, plan Component 7 / #3282 Q2): a curated +/// {metric + predicate + hysteresis} an operator can browse (list_custom_alert_templates) and create a +/// real rule from (create_custom_alert_rule). is a genuine +/// body: CODE, so the drift-guard re-validates it against the LIVE +/// catalog on every build; a template naming a measure that later drifts out of the catalog fails the build +/// rather than shipping a rule that can never fire. +public sealed record CustomAlertTemplate(string Key, string Name, string Description, string DefinitionJson); + +/// +/// The starter custom-alert templates. #3282's answer to "the collected-but-unalerted signals": templates over +/// new hardcoded evaluators, so the set generalizes to signals nobody has named yet. Each maps to a REAL +/// measure (source + measure|ratio + aggregate + unit), with a deliberately +/// conservative starter threshold + hysteresis the operator tunes; scope defaults to all servers. +/// +/// Thresholds are STARTERS, not tuned bars: they are set where a value is unambiguous trouble on most +/// fleets (a 30s+ block, a 40%+ signal wait), never at a value only a specific fleet's baseline could justify, +/// and are documented per template. Every one fires only after 3 consecutive breaching evaluations (2 for the +/// more urgent blocking signal) so a single spike does not page. +/// +/// Deliberately not shipped: a PostgreSQL "storage growth" template. #3282 names it, but the +/// catalog has no database-size GAUGE for PostgreSQL (pg_database_stats is transaction/block/temp +/// COUNTERS, not size), so there is nothing honest to threshold on. The related disk-fill risk IS covered by +/// the replication-slot-WAL-retention template (retained WAL is the PostgreSQL storage a stuck slot actually +/// grows without bound). SQL Server storage is already covered by the built-in low-disk / file-growth alerts. +/// +public static class CustomAlertTemplates +{ + /// The curated set. Ordered PostgreSQL-first (the signals #3282 flagged as unalerted), then the + /// obvious SQL Server ones. + public static readonly IReadOnlyList All = new[] + { + /* ── PostgreSQL (the #3282 collected-but-unalerted signals) ── */ + + new CustomAlertTemplate( + "pg-autovacuum-dead-tuples", + "PostgreSQL: dead tuples piling up", + "Fires when a table's estimated dead-tuple count stays high, the tell that autovacuum is falling " + + "behind the write rate (bloat and plan drift follow). Starter: warn at 1,000,000 dead tuples, " + + "critical at 10,000,000: table-grain, so it is the worst table in the window. Tune to your " + + "largest hot table; a small database should lower both.", + "{\"metric\":{\"source\":\"pg_autovacuum_stats\",\"measure\":\"pg_av_dead_tuples\",\"aggregate\":\"max\",\"unit\":\"count\",\"hours\":1}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":1000000,\"criticalThreshold\":10000000}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "pg-replication-lag", + "PostgreSQL: replica replay lag high", + "Fires when a standby's replay lag stays high: the replica is falling behind the primary, so a " + + "failover would lose more, and read replicas serve staler data. Starter: warn at 30s of replay " + + "lag, critical at 5 minutes. Lower it if your RPO is tight.", + "{\"metric\":{\"source\":\"pg_replication_stats\",\"measure\":\"pg_repl_replay_lag_ms\",\"aggregate\":\"max\",\"unit\":\"ms\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":30000,\"criticalThreshold\":300000}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "pg-connection-saturation", + "PostgreSQL: connection count high", + "Fires when the total session count stays high: approaching max_connections means new connections " + + "start failing. Starter: warn at 200 sessions, critical at 500 (absolute, since the catalog does " + + "not carry max_connections); set both to a fraction of your configured max_connections.", + "{\"metric\":{\"source\":\"pg_session_states\",\"measure\":\"pg_sess_total_sessions\",\"aggregate\":\"max\",\"unit\":\"count\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":200,\"criticalThreshold\":500}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "pg-table-bloat", + "PostgreSQL: table bloat high", + "Fires when a table's estimated bloat percentage stays high: wasted space and slower scans that a " + + "VACUUM FULL / pg_repack would reclaim. Starter: warn at 40% bloat, critical at 70%. Bloat is " + + "slow-moving, so this evaluates over an hour.", + "{\"metric\":{\"source\":\"pg_table_bloat_stats\",\"measure\":\"pg_tbl_bloat_pct\",\"aggregate\":\"max\",\"unit\":\"percent\",\"hours\":1}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":40,\"criticalThreshold\":70}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "pg-replication-slot-wal", + "PostgreSQL: replication slot retaining WAL", + "Fires when a replication slot's retained WAL stays large: a disconnected or slow consumer holds " + + "WAL that cannot be recycled, and the disk fills for the WHOLE instance. This is the storage risk " + + "#3282 flags (PostgreSQL has no database-size gauge to threshold directly). Starter: warn at ~10 GB " + + "retained, critical at ~50 GB. Set below your free disk headroom.", + "{\"metric\":{\"source\":\"pg_replication_slot_stats\",\"measure\":\"pg_slot_retained_wal_bytes\",\"aggregate\":\"max\",\"unit\":\"mb\",\"hours\":1}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":10240,\"criticalThreshold\":51200}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + /* ── SQL Server (the obvious ones) ── */ + + new CustomAlertTemplate( + "sqlserver-signal-wait-pct", + "SQL Server: high signal wait %", + "Fires when signal wait % stays high: threads are ready but waiting for a scheduler, the classic " + + "sign of CPU pressure / scheduler contention. Starter: warn at 25%, critical at 40% (the " + + "widely-used rule-of-thumb bars).", + "{\"metric\":{\"source\":\"wait_stats\",\"ratio\":\"signal_wait_pct\",\"unit\":\"percent\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":25,\"criticalThreshold\":40}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "sqlserver-sustained-blocking", + "SQL Server: sustained blocking", + "Fires when the longest blocked-process report in the window crosses a duration bar: a session was " + + "blocked that long, not just momentary contention. Starter: warn at 30s blocked, critical at 2 " + + "minutes. Fires after 2 breaching evaluations (blocking is more urgent than the slow-moving " + + "signals). Requires the blocked-process report threshold to be configured on the instance.", + "{\"metric\":{\"source\":\"blocked_process_reports\",\"measure\":\"bpr_wait_time_ms\",\"aggregate\":\"max\",\"unit\":\"ms\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":30000,\"criticalThreshold\":120000}," + + "\"hysteresis\":{\"breachSamples\":2,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "sqlserver-long-running-query", + "SQL Server: long-running query", + "Fires when the average query duration per execution stays high: a query (or plan regression) that " + + "is consistently slow, not one that ran long once. Starter: warn at a 10s average, critical at 60s. " + + "Tune to your workload's normal query time.", + "{\"metric\":{\"source\":\"query_stats\",\"ratio\":\"query_avg_elapsed_us\",\"unit\":\"ms\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":10000,\"criticalThreshold\":60000}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + + new CustomAlertTemplate( + "sqlserver-tempdb-pressure", + "SQL Server: tempdb space growth", + "Fires when total tempdb reserved space stays high: spills, version store, or a runaway object " + + "eating tempdb, which can stall the whole instance if it runs out. Starter: warn at ~50 GB " + + "reserved, critical at ~100 GB (absolute); set both relative to your tempdb file sizes.", + "{\"metric\":{\"source\":\"tempdb_stats\",\"measure\":\"tempdb_total_reserved_mb\",\"aggregate\":\"max\",\"unit\":\"mb\",\"hours\":0.25}," + + "\"predicate\":{\"op\":\"ge\",\"warnThreshold\":51200,\"criticalThreshold\":102400}," + + "\"hysteresis\":{\"breachSamples\":3,\"clearSamples\":2}}"), + }; +} diff --git a/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs b/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs index 1fecdd637..9c489072f 100644 --- a/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs +++ b/Darling/PerformanceMonitor.Darling.Service/DarlingWebEndpoints.cs @@ -94,6 +94,7 @@ public static class DarlingWebEndpoints "delete_custom_alert_rule", "validate_custom_alert_rule", "test_custom_alert_rule", + "list_custom_alert_templates", }; /// The window (hours) the fleet card blocking / deadlock counts default to — the WPF Overview's window. diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs index 34c2317ad..a7fb34892 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpCustomAlertTools.cs @@ -378,6 +378,34 @@ public static async Task TestCustomAlertRule( } } + [McpServerTool(Name = "list_custom_alert_templates"), Description( + "Lists the built-in STARTER custom-alert-rule templates - curated {metric, predicate, hysteresis} " + + "definitions for common signals: PostgreSQL (dead-tuple pile-up / replica replay lag / connection " + + "count / table bloat / replication-slot WAL retention) and SQL Server (high signal-wait % / sustained " + + "blocking / long-running query / tempdb space). Each entry is {key, name, description, definition}, " + + "where definition is a ready-to-use rule body. Browse them, tweak the thresholds and scope to your " + + "fleet (the starters are deliberately conservative, not tuned), then pass the definition to " + + "create_custom_alert_rule to save it - or to test_custom_alert_rule to see what it would do right now. " + + "Read-only: this lists code-defined templates and touches no store.")] + public static Task ListCustomAlertTemplates() + { + var templates = new JsonArray(); + foreach (var template in CustomAlertTemplates.All) + { + templates.Add(new JsonObject + { + ["key"] = template.Key, + ["name"] = template.Name, + ["description"] = template.Description, + // The definition embedded as a JSON object (NOT an escaped string), so a client can hand it + // straight to create_custom_alert_rule / test_custom_alert_rule after editing. + ["definition"] = JsonNode.Parse(template.DefinitionJson), + }); + } + + return Task.FromResult(new JsonObject { ["templates"] = templates }.ToJsonString(McpHelpers.JsonOptions)); + } + /// The full single-rule wire shape (definition embedded as JSON, NOT an escaped string) - mirrors /// , adding the enabled column that alert rules /// carry and views do not. Returned by get / create / update. diff --git a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs index 4a7d3ab36..436635740 100644 --- a/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs +++ b/Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpInstructions.cs @@ -62,7 +62,7 @@ public static string Build(DarlingPeerDirectory.Snapshot peers) ## Tool Reference - This server exposes 146 tools. 86 are the same names Performance Monitor Lite exposes, spanning diagnostic analysis, plan analysis, data reads at core and diagnostic depth, resource contention + jobs, trends, system-health parse-on-read, alerts + health overview, and the Default Trace. The remaining 60 are unique to Darling: thirty-three are the PostgreSQL reads (Aurora/PostgreSQL targets only Darling's central store can hold), eight are the Custom Views tools (seven manage the saved views — the one view-authoring write surface — and `describe_custom_view_catalog` returns the read-only compose vocabulary those authoring tools draw from), seven are the custom-alert-rule tools (`create_custom_alert_rule` / `update_custom_alert_rule` / `delete_custom_alert_rule` manage the user-authored alert rules, the one alert-authoring write surface, while `get_custom_alert_rule` / `list_custom_alert_rules` read them back, `validate_custom_alert_rule` checks a rule definition against the same compose catalog without saving it, and `test_custom_alert_rule` evaluates a rule's metric NOW on each in-scope server and reports whether it would breach, without delivering or persisting anything), three are alert-tuning write tools (`update_alert_settings` tunes the alert engine's thresholds; `create_mute_rule` / `delete_mute_rule` manage the mute rules) that write only the shared alert configuration in the monitoring store, two are server-onboarding write tools (`add_servers` bulk-adds monitored servers; `remove_server` removes one) that add or remove rows in the monitoring store's monitored-server registry, `get_fleet_overview` and `get_ag_health` are the two cross-server reads only a central store can answer, `get_store_metrics` reads the monitoring store's OWN hourly size/compression/growth series for capacity forecasting, `get_store_log` reads what the monitoring store's OWN PostgreSQL server log recorded as a per-class census with its capture denominator (the self-monitoring that shows the store's half of a client-side symptom), `get_collector_cost` reads the tool's OWN per-collector cost on the monitored servers (the self-monitoring that flags a collector regressing into a hog), `get_collector_stall_probes` reads the out-of-band server-wide wait samples this tool takes while one of its own collectors is stalled mid-read — the only surface here that reports what a monitored instance was doing inside the window the sequential sweep records nothing in — and `get_blocking` is Darling's name for the blocked-process-report read that Lite exposes as `get_blocked_process_reports` — a naming difference, not a capability gap. Every data-read tool reads the data the collectors already captured into the store — a stored read, never a live query against the monitored server. + This server exposes 147 tools. 86 are the same names Performance Monitor Lite exposes, spanning diagnostic analysis, plan analysis, data reads at core and diagnostic depth, resource contention + jobs, trends, system-health parse-on-read, alerts + health overview, and the Default Trace. The remaining 61 are unique to Darling: thirty-three are the PostgreSQL reads (Aurora/PostgreSQL targets only Darling's central store can hold), eight are the Custom Views tools (seven manage the saved views — the one view-authoring write surface — and `describe_custom_view_catalog` returns the read-only compose vocabulary those authoring tools draw from), eight are the custom-alert-rule tools (`create_custom_alert_rule` / `update_custom_alert_rule` / `delete_custom_alert_rule` manage the user-authored alert rules, the one alert-authoring write surface, while `get_custom_alert_rule` / `list_custom_alert_rules` read them back, `validate_custom_alert_rule` checks a rule definition against the same compose catalog without saving it, `test_custom_alert_rule` evaluates a rule's metric NOW on each in-scope server and reports whether it would breach without delivering or persisting anything, and `list_custom_alert_templates` lists the built-in starter rule templates to browse and create from), three are alert-tuning write tools (`update_alert_settings` tunes the alert engine's thresholds; `create_mute_rule` / `delete_mute_rule` manage the mute rules) that write only the shared alert configuration in the monitoring store, two are server-onboarding write tools (`add_servers` bulk-adds monitored servers; `remove_server` removes one) that add or remove rows in the monitoring store's monitored-server registry, `get_fleet_overview` and `get_ag_health` are the two cross-server reads only a central store can answer, `get_store_metrics` reads the monitoring store's OWN hourly size/compression/growth series for capacity forecasting, `get_store_log` reads what the monitoring store's OWN PostgreSQL server log recorded as a per-class census with its capture denominator (the self-monitoring that shows the store's half of a client-side symptom), `get_collector_cost` reads the tool's OWN per-collector cost on the monitored servers (the self-monitoring that flags a collector regressing into a hog), `get_collector_stall_probes` reads the out-of-band server-wide wait samples this tool takes while one of its own collectors is stalled mid-read — the only surface here that reports what a monitored instance was doing inside the window the sequential sweep records nothing in — and `get_blocking` is Darling's name for the blocked-process-report read that Lite exposes as `get_blocked_process_reports` — a naming difference, not a capability gap. Every data-read tool reads the data the collectors already captured into the store — a stored read, never a live query against the monitored server. ### Reading an empty result diff --git a/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs b/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs index c44c2c0bd..cbf9e1f76 100644 --- a/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs +++ b/Lite.Tests/CrossAppMcpToolInventoryPinTests.cs @@ -229,7 +229,8 @@ there is no Lite twin to port (same reasoning as the Custom Views + alert-tuning config.custom_alert_rules in the central Postgres store; get_custom_alert_rule / list_custom_alert_rules read them back; validate_custom_alert_rule checks a definition against the same compose catalog the Custom Views tools draw from, without saving; test_custom_alert_rule (#3299) evaluates a rule's metric - now on each in-scope server and reports whether it would breach, without delivering or persisting). + now on each in-scope server and reports whether it would breach, without delivering or persisting; + list_custom_alert_templates (#3285 Component 7) lists the code-defined starter rule templates). Darling-ONLY by architecture, the same kind of entry as the Custom Views + alert-tuning tools above rather than a "not ported yet": custom alert rules are a central-store feature the headless service evaluates on its sweep, and Lite (a single-instance WPF app over local DuckDB with no central, @@ -241,6 +242,7 @@ there is no Lite twin to port (same reasoning as the Custom Views + alert-tuning "delete_custom_alert_rule", "validate_custom_alert_rule", "test_custom_alert_rule", + "list_custom_alert_templates", }; [Fact]