using System.Globalization;
using Microsoft.Extensions.Options;
using SQLInsight.Core.Data;
using SQLInsight.Core.Findings;
using SQLInsight.Reports;
using SQLInsight.Repository;
namespace SQLInsight.Web.Api;
///
/// The read-only JSON API for findings, rules, reports and the pages of the web UI: the same data, windows and filters
/// as the pages, built from the same models. Customer data (statement texts, logins, names) is sensitive and returned
/// as "[udeladt]" unless SQLInsight:Web:ApiIncludesSensitiveEvidence is on.
///
public static class ApiV1Endpoints
{
public const string Redacted = "[udeladt]";
private static readonly string[] ReportFormats = ["json", "pdf"];
/// Report types under /api/v1/reports/{type}.
public static readonly IReadOnlyList<(string Type, string Title, string Parameters)> ReportTypes =
[
("driftsrapport", "Driftsrapport til ledelse og compliance", "days (1–366, standard 30)"),
("analyse", "Analyse af tidsrum for én instans", "instance (påkrævet), from, to (ISO 8601; standard seneste time), sort (cpu|varighed|laesninger|koersler)"),
("rettigheder", "Rettighedsrevision", "days (1–366, standard 90), instance (valgfri; standard alle)"),
];
public static void Map(IEndpointRouteBuilder app)
{
ArgumentNullException.ThrowIfNull(app);
var api = app.MapGroup("/api/v1").DisableAntiforgery().WithTags("SQLInsight Pro API v1").RequireRead();
api.MapGet("/findings", SearchFindingsAsync)
.WithSummary("Fund: aktive, løste eller alle, med filtre og sider / Findings with filters and paging")
.WithDescription("Returns an array. More pages: the response header X-Continuation holds the value for ?continuation=.");
api.MapGet("/findings/{id:long}", FindingAsync).WithSummary("Ét fund med forklaring, anbefaling, målinger, fix-script og historik / One finding");
api.MapGet("/rules", RulesAsync).WithSummary("Regler: indbyggede, community og lokale / Rules");
api.MapGet("/instances/{name}/rule-states", RuleStatesAsync).WithSummary("Reglernes udfald på en instans / Rule outcomes on an instance");
api.MapGet("/reports", () => ReportTypes.Select(r => new { r.Type, r.Title, r.Parameters, Formats = ReportFormats }))
.WithSummary("Rapporttyper og deres parametre / Report types");
api.MapGet("/reports/{type}", ReportAsync).WithSummary("En rapport som JSON eller PDF (format=json|pdf) / A report as JSON or PDF");
api.MapGet("/instances/{name}/analysis", AnalysisAsync).WithSummary("Analyse af tidsrum / Time window analysis");
api.MapGet("/instances/{name}/top-sql", TopSqlAsync).WithSummary("Tungeste sætninger i et tidsrum / Top SQL in a window");
api.MapGet("/instances/{name}/disks", DisksAsync).WithSummary("Diskhastighed pr. drev med vurdering og tidsserier / Disk speed per drive");
api.MapGet("/instances/{name}/configuration", ConfigurationAsync).WithSummary("Konfiguration og ændringslog / Configuration and change log");
api.MapGet("/instances/{name}/privileges", InstancePrivilegesAsync).WithSummary("Rettigheder og ændringer på en instans / Privileges on an instance");
api.MapGet("/privileges", AllPrivilegesAsync).WithSummary("Rettigheder og ændringer på alle instanser / Privileges on all instances");
api.MapGet("/instances/{name}/activity", ActivityAsync).WithSummary("Åbne transaktioner og lange sætninger lige nu / Open transactions and long-running statements");
api.MapGet("/instances/{name}/tempdb", TempdbAsync).WithSummary("tempdb og version store lige nu / tempdb and the version store");
api.MapGet("/instances/{name}/error-log", ErrorLogAsync).WithSummary("Vigtige hændelser i fejlloggen / Important error log events (days)");
api.MapGet("/instances/{name}/wait-types/{waitType}", WaitTypeAsync).WithSummary("En ventetype: forklaring, udvikling og sætninger / One wait type");
}
private sealed class Redactor(bool include)
{
public bool Include => include;
public string? Value(string? value, bool sensitive = true) => sensitive && !include && value is not null ? Redacted : value;
}
private static Redactor RedactorFor(IOptions options) => new(options.Value.ApiIncludesSensitiveEvidence);
private static object FindingSummary(StoredFinding f, Redactor redact) => new
{
Id = f.FindingId,
Instance = f.InstanceName,
f.RuleId,
Category = Category(f.RuleId),
f.Subject,
Severity = f.Severity.ToString(),
f.Title,
f.Explanation,
f.Recommendation,
Evidence = f.Evidence.Select(e => new { e.Name, Value = redact.Value(e.Value, e.Sensitive) }),
f.FirstSeenAt,
f.LastSeenAt,
f.ResolvedAt,
};
/// SEC in SQLI-SEC-001, FT in COM-FT-002.
public static string Category(string ruleId)
{
ArgumentNullException.ThrowIfNull(ruleId);
var parts = ruleId.Split('-');
return parts.Length >= 3 ? parts[1] : ruleId;
}
private static async Task SearchFindingsAsync(HttpContext http, InsightRepository repository, IOptions options, CancellationToken ct,
string? status = null, DateTimeOffset? from = null, DateTimeOffset? to = null, string? instance = null, string? database = null,
string? ruleId = null, Severity? minSeverity = null, string? category = null, string? sort = null, int? limit = null, string? continuation = null)
{
var findingStatus = status?.ToLowerInvariant() switch
{
null or "" or "active" => (FindingStatus?)FindingStatus.Active,
"resolved" => FindingStatus.Resolved,
"all" => FindingStatus.All,
_ => null,
};
var findingSort = sort?.ToLowerInvariant() switch
{
null or "" or "severity" => (FindingSort?)FindingSort.Severity,
"firstseen" => FindingSort.FirstSeen,
"lastseen" => FindingSort.LastSeen,
"resolved" => FindingSort.Resolved,
_ => null,
};
if (findingStatus is null || findingSort is null)
{
return Results.BadRequest("status skal være active, resolved eller all; sort skal være severity, firstSeen, lastSeen eller resolved.");
}
var offset = 0;
if (continuation is { Length: > 0 } && !int.TryParse(continuation, NumberStyles.None, CultureInfo.InvariantCulture, out offset))
{
return Results.BadRequest("Ugyldig continuation.");
}
var size = Math.Clamp(limit ?? 100, 1, InsightRepository.MaxFindingPage);
var (findings, more) = await repository.SearchFindingsAsync(new FindingQuery
{
Status = findingStatus.Value,
From = from,
To = to,
Instance = instance,
Database = database,
RuleId = ruleId,
MinSeverity = minSeverity,
Category = category,
Sort = findingSort.Value,
Offset = offset,
Limit = size,
}, ct).ConfigureAwait(false);
if (more)
{
http.Response.Headers["X-Continuation"] = (offset + size).ToString(CultureInfo.InvariantCulture);
}
var redact = RedactorFor(options);
return Results.Ok(findings.Select(f => FindingSummary(f, redact)));
}
private static async Task FindingAsync(long id, InsightRepository repository, IOptions options, CancellationToken ct)
{
if (await repository.GetFindingAsync(id, ct).ConfigureAwait(false) is not { } f)
{
return Results.NotFound();
}
var redact = RedactorFor(options);
return Results.Ok(new
{
Id = f.FindingId,
Instance = f.InstanceName,
f.RuleId,
Category = Category(f.RuleId),
f.Subject,
Severity = f.Severity.ToString(),
f.Title,
f.Explanation,
f.Recommendation,
Evidence = f.Evidence.Select(e => new { e.Name, Value = redact.Value(e.Value, e.Sensitive), e.Sensitive }),
f.FixScript,
References = f.References.Select(r => new { r.Title, r.Url }),
History = new { f.FirstSeenAt, f.LastSeenAt, f.ResolvedAt, Status = f.ResolvedAt is null ? "active" : "resolved" },
});
}
private static async Task RulesAsync(InsightRepository repository, CancellationToken ct)
{
var builtIn = Rules.BuiltInRules.Create().Select(r => (r.Id, r.Title, Source: "indbygget"));
var community = Rules.Lifecycle.RulePack.Embedded().CommunityRules.Select(r => (r.Id, r.Title, Source: "community"));
var seen = (await repository.GetAllRuleStatesAsync(ct).ConfigureAwait(false)).Select(s => s.RuleId);
var known = builtIn.Concat(community).ToList();
var titles = (await repository.GetRuleTitlesAsync(ct).ConfigureAwait(false)).ToDictionary(t => t.RuleId, t => t.Title, StringComparer.OrdinalIgnoreCase);
var local = seen.Distinct(StringComparer.OrdinalIgnoreCase).Where(id => known.All(k => !string.Equals(k.Id, id, StringComparison.OrdinalIgnoreCase)))
.Select(id => (Id: id, Title: titles.GetValueOrDefault(id) ?? id, Source: id.StartsWith("COM-", StringComparison.Ordinal) ? "community" : "lokal"));
return Results.Ok(known.Concat(local).OrderBy(r => r.Id, StringComparer.Ordinal)
.Select(r => new { r.Id, r.Title, Category = Category(r.Id), r.Source }));
}
private static async Task RuleStatesAsync(string name, InsightRepository repository, CancellationToken ct)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
return Results.Ok((await repository.GetRuleStatesAsync(instance.InstanceId, ct).ConfigureAwait(false)).Select(s => new
{
s.RuleId,
s.RuleTitle,
Outcome = s.Outcome switch
{
Rules.Engine.RuleOutcome.Passed => "passed",
Rules.Engine.RuleOutcome.Findings => "findings",
Rules.Engine.RuleOutcome.NotMeasurable => "notMeasurable",
Rules.Engine.RuleOutcome.NotApplicable => "notApplicable",
Rules.Engine.RuleOutcome.ManualCheck => "manualCheck",
_ => "error",
},
s.Reason,
s.EvaluatedAt,
}));
}
private static async Task ReportAsync(string type, InsightRepository repository, Display display, IOptions options, CancellationToken ct,
string? format = null, int? days = null, string? instance = null, DateTimeOffset? from = null, DateTimeOffset? to = null, string? sort = null)
{
var pdf = string.Equals(format, "pdf", StringComparison.OrdinalIgnoreCase);
if (format is { Length: > 0 } && !pdf && !string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
return Results.BadRequest("format skal være json eller pdf.");
}
var now = DateTimeOffset.UtcNow;
var redact = RedactorFor(options);
switch (type.ToLowerInvariant())
{
case "driftsrapport":
{
var span = Math.Clamp(days ?? 30, 1, 366);
var data = await ReportData.LoadAsync(repository, options.Value.Organization, now.AddDays(-span), now, display.Zone, TimeProvider.System, ct).ConfigureAwait(false);
return pdf
? Results.File(ManagementReport.Render(data), "application/pdf", $"driftsrapport-{now:yyyy-MM-dd}.pdf")
: Results.Ok(ManagementJson(data, redact));
}
case "analyse":
{
if (instance is null || await repository.GetInstanceAsync(instance, ct).ConfigureAwait(false) is not { } target)
{
return Results.BadRequest("instance er påkrævet og skal være en kendt instans.");
}
var (start, end) = (from ?? now.AddHours(-1), to ?? now);
if (end <= start || end - start > TimeWindow.MaxLength)
{
return Results.BadRequest("to skal ligge efter from, og tidsrummet må højst være 31 dage.");
}
var analysis = await TimeWindowAnalysis.LoadAsync(repository, target, start, end, now, new RetentionOptions().RawDays, AnalysisOrder.Parse(sort), ct).ConfigureAwait(false);
return pdf
? Results.File(TimeWindowReport.Render(analysis, options.Value.Organization, display.Zone), "application/pdf", $"analyse-{now:yyyy-MM-dd}.pdf")
: Results.Ok(AnalysisJson(analysis, redact));
}
case "rettigheder":
{
InstanceOverview? target = null;
if (instance is { Length: > 0 } && (target = await repository.GetInstanceAsync(instance, ct).ConfigureAwait(false)) is null)
{
return Results.NotFound();
}
var span = Math.Clamp(days ?? 90, 1, 366);
var audit = await PrivilegeAudit.LoadAsync(repository, target, now.AddDays(-span), now, ct).ConfigureAwait(false);
return pdf
? Results.File(audit.Render(options.Value.Organization, display.Zone), "application/pdf", $"rettigheder-{now:yyyy-MM-dd}.pdf")
: Results.Ok(AuditJson(audit, redact));
}
default:
return Results.NotFound();
}
}
private static object ManagementJson(ReportData data, Redactor redact) => new
{
data.Organization,
data.From,
data.To,
data.GeneratedAt,
TimeZone = data.TimeZone.Id,
data.Version,
data.RulePack,
Summary = ManagementReport.SummarySentences(data),
Period = data.Period,
Controls = ComplianceControl.All.Select(c => new
{
c.Name,
c.Description,
c.Rules,
Instances = data.Instances.Select(i => new { Instance = i.Name, Status = c.StatusOn(i.Name, data.RuleStates).ToString() }),
}),
Instances = data.Instances.Select(i => new
{
i.Name, i.ProductVersion, i.ProductLevel, i.ProductUpdateLevel, i.Edition, i.LastSeenAt, i.IsFailing,
Findings = new { i.Critical, i.High, i.Medium, i.Low, i.Info },
}),
Findings = data.Active.Select(f => FindingSummary(f, redact)),
ConfigurationChanges = data.ConfigurationChanges.Select(c => ConfigurationChangeJson(c, redact)),
};
private static object ConfigurationChangeJson(StoredConfigurationChange c, Redactor redact)
{
// Service accounts are logins: sensitive, as in the configuration rule's evidence.
var sensitive = c.Area == ConfigurationFingerprint.ServiceAccount;
return new { Instance = c.InstanceName, c.DetectedAt, c.Area, c.Name, OldValue = redact.Value(c.OldValue, sensitive), NewValue = redact.Value(c.NewValue, sensitive) };
}
private static object AnalysisJson(TimeWindowAnalysis a, Redactor redact) => new
{
Instance = a.Instance.Name,
a.From,
a.To,
Resolution = a.Resolution.ToString(),
a.HasData,
Metrics = a.Metrics.Select(m => new
{
m.Metric, m.Label, m.Unit, m.Average, m.Peak, m.NormalAverage, m.IsUnusual,
Assessment = AssessmentJson(a.Assess(m)),
Explanation = MetricAssessment.Explanation(m.Metric),
Points = m.Points,
}),
Waits = a.Waits.Select(w => new { w.WaitType, Category = w.Category.ToString(), w.WaitTimeMs, w.WaitingTasks, w.PerMinute, w.NormalPerMinute, w.Share, w.IsUnusual }),
TopQueries = a.TopQueries.Select(q => TopQueryJson(q, a.ActivityPercent(q), redact)),
Disks = a.Disks.Select(d => DiskJson(d.Window, d.Normal)),
Files = a.Files,
Blocking = a.Blocking.Select(b => new
{
b.SampledAt, b.HeadSessionId, b.BlockedCount, b.MaxWaitMs,
Sessions = b.Sessions.Select(s => new
{
s.SessionId, s.BlockingSessionId, s.Status, s.WaitType, s.WaitTimeMs, s.DatabaseName,
LoginName = redact.Value(s.LoginName), HostName = redact.Value(s.HostName), ProgramName = redact.Value(s.ProgramName), SqlText = redact.Value(s.SqlText),
}),
}),
Deadlocks = a.Deadlocks.Select(d => new { d.DeadlockId, d.OccurredAt, Processes = d.Processes.Count, Graph = $"/deadlocks/{d.DeadlockId}.xdl" }),
LongOperations = a.LongOperations.Select(o => new { o.Command, o.DatabaseName, LoginName = redact.Value(o.LoginName), o.StartedAt, o.LastSeenAt, o.PercentComplete, o.EstimatedEnd }),
LongTransactions = a.LongTransactions.Select(t => new
{
t.SessionId, t.BeganAt, t.LastSeenAt, t.DatabaseName, LogMegabytes = t.MaxLogBytes / 1024.0 / 1024.0,
LoginName = redact.Value(t.LoginName), HostName = redact.Value(t.HostName), ProgramName = redact.Value(t.ProgramName), Statement = redact.Value(t.Statement),
}),
Findings = a.Findings.Select(f => FindingSummary(f, redact)),
};
private static object AssessmentJson(Assessment a) => new { Level = a.Level.ToString(), Text = a.LevelText, a.Summary, a.NextStep, a.Link };
private static object TopQueryJson(TopQuery q, double? activity, Redactor redact) => new
{
q.QueryKey, q.DatabaseName, Text = redact.Value(q.Text), q.Executions, q.WorkerTimeUs, q.ElapsedTimeUs, q.LogicalReads, q.PhysicalReads, q.LogicalWrites,
ActivityPercent = activity,
};
private static object DiskJson(DiskActivity w, DiskActivity? normal) => new
{
w.Volume, w.DataFiles, w.LogFiles, w.ReadIops, w.WriteIops, w.ReadMbPerSecond, w.WriteMbPerSecond, w.AverageReadMs, w.AverageWriteMs,
Normal = normal is null ? null : new { normal.AverageReadMs, normal.AverageWriteMs, normal.ReadIops, normal.WriteIops },
Assessment = AssessmentJson(MetricAssessment.AssessDisk(w)),
};
private static object AuditJson(PrivilegeAudit audit, Redactor redact) => new
{
audit.From,
audit.To,
Method = PrivilegeAudit.MethodText,
Instances = audit.Instances.Select(e => InstanceAuditJson(e, redact)),
};
private static object InstanceAuditJson(PrivilegeAuditInstance e, Redactor redact) => new
{
Instance = e.Instance.Name,
Measured = e.Snapshot?.CapturedAt,
SysadminCount = e.Snapshot is null ? (int?)null : e.Sysadmins.Count,
MaxSysadminAccounts = e.Snapshot?.MaxSysadminAccounts,
Sysadmins = e.Sysadmins.Select(a => new { Account = redact.Value(a.Account), DisplayName = redact.Value(a.DisplayName), a.Type, Source = a.Source == PrivilegeAnalysis.Direct ? a.Source : redact.Value(a.Source) }),
Principals = (e.Snapshot?.Principals ?? []).Select(p => new
{
Name = redact.Value(p.Name), p.Type, p.Privilege, p.IsDisabled, p.ExpansionError,
Members = p.Members?.Select(m => new { Account = redact.Value(m.Account), DisplayName = redact.Value(m.DisplayName), m.Enabled }),
}),
Sa = e.Snapshot is null ? null : PrivilegeAudit.SaText(e.Sa),
LoginsWithoutPolicy = e.LoginsWithoutPolicy.Select(l => redact.Value(l.Name)),
DefaultTrace = e.Snapshot is null ? null : PrivilegeAudit.TraceText(e.Snapshot),
Changes = e.Changes.Select(c => new
{
c.DetectedAt, c.Kind, c.Privilege, Principal = redact.Value(c.Principal), c.PrincipalType, ViaGroup = redact.Value(c.ViaGroup),
ChangedBy = redact.Value(c.ChangedBy), c.ChangedAt,
}),
};
private static async Task<(InstanceOverview? Instance, DateTimeOffset From, DateTimeOffset To, IResult? Error)> WindowAsync(
string name, InsightRepository repository, DateTimeOffset? from, DateTimeOffset? to, TimeSpan defaultLength, CancellationToken ct)
{
var instance = await repository.GetInstanceAsync(name, ct).ConfigureAwait(false);
var now = DateTimeOffset.UtcNow;
var (start, end) = (from ?? (to ?? now) - defaultLength, to ?? now);
IResult? error = instance is null ? Results.NotFound()
: end <= start || end - start > TimeWindow.MaxLength ? Results.BadRequest("to skal ligge efter from, og tidsrummet må højst være 31 dage.")
: null;
return (instance, start, end, error);
}
private static async Task AnalysisAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct,
DateTimeOffset? from = null, DateTimeOffset? to = null, string? sort = null)
{
var (instance, start, end, error) = await WindowAsync(name, repository, from, to, TimeSpan.FromHours(1), ct).ConfigureAwait(false);
if (error is not null)
{
return error;
}
var analysis = await TimeWindowAnalysis.LoadAsync(repository, instance!, start, end, DateTimeOffset.UtcNow, new RetentionOptions().RawDays, AnalysisOrder.Parse(sort), ct)
.ConfigureAwait(false);
return Results.Ok(AnalysisJson(analysis, RedactorFor(options)));
}
private static async Task TopSqlAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct,
DateTimeOffset? from = null, DateTimeOffset? to = null, string? sort = null, int? limit = null)
{
var (instance, start, end, error) = await WindowAsync(name, repository, from, to, TimeSpan.FromHours(24), ct).ConfigureAwait(false);
if (error is not null)
{
return error;
}
var seconds = (end - start).TotalSeconds;
var queries = await repository.GetTopQueriesAsync(instance!.InstanceId, start, end, AnalysisOrder.Parse(sort), Math.Clamp(limit ?? 25, 1, 200), ct).ConfigureAwait(false);
var redact = RedactorFor(options);
return Results.Ok(new
{
Instance = instance.Name,
From = start,
To = end,
Queries = queries.Select(q => TopQueryJson(q, 100.0 * q.WorkerTimeUs / (seconds * 1_000_000), redact)),
});
}
private static async Task DisksAsync(string name, InsightRepository repository, CancellationToken ct, DateTimeOffset? from = null, DateTimeOffset? to = null)
{
var (instance, start, end, error) = await WindowAsync(name, repository, from, to, TimeSpan.FromHours(24), ct).ConfigureAwait(false);
if (error is not null)
{
return error;
}
var hourly = TimeWindow.ChooseResolution(start, DateTimeOffset.UtcNow, new RetentionOptions().RawDays) == WindowResolution.Hourly;
var disks = hourly
? await repository.GetDiskActivityHourlyAsync(instance!.InstanceId, start, end, ct).ConfigureAwait(false)
: await repository.GetDiskActivityAsync(instance!.InstanceId, start, end, ct).ConfigureAwait(false);
var series = await repository.GetDiskSeriesAsync(instance.InstanceId, start, ct).ConfigureAwait(false);
return Results.Ok(new
{
Instance = instance.Name,
From = start,
To = end,
Resolution = hourly ? "Hourly" : "Raw",
Explanation = new { Read = MetricAssessment.Explanation("disk.read"), LogWrite = MetricAssessment.Explanation("disk.write.log"), Write = MetricAssessment.Explanation("disk.write") },
Drives = disks.Select(d => DiskJson(d, null)),
Series = series,
});
}
private static async Task ConfigurationAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct, int? days = null)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
var redact = RedactorFor(options);
var id = instance.InstanceId;
var setup = await repository.GetSnapshotAsync(id, "instance-config", ct).ConfigureAwait(false);
var configuration = await repository.GetSnapshotAsync(id, "configuration", ct).ConfigureAwait(false);
var traceFlags = await repository.GetSnapshotAsync(id, "trace-flags", ct).ConfigureAwait(false);
var now = DateTimeOffset.UtcNow;
var changes = await repository.GetConfigurationChangesAsync(id, now.AddDays(-Math.Clamp(days ?? 90, 1, 366)), now, 500, ct).ConfigureAwait(false);
return Results.Ok(new
{
Instance = instance.Name,
Setup = setup is null ? null : new
{
setup.CapturedAt, setup.ProductVersion, setup.ProductLevel, setup.Edition, setup.Collation, setup.ServicesAvailable,
Services = setup.Services.Select(s => new { s.Name, Account = redact.Value(s.Account), s.StartupType, s.Status, s.InstantFileInitialization }),
setup.InstantFileInitialization, setup.MemoryModel, setup.TempdbFiles, setup.DefaultDataPath, setup.DefaultLogPath,
},
Options = configuration?.Options.Select(o => new { o.Name, o.Value, o.ValueInUse, Default = ConfigurationFingerprint.DefaultValue(o.Name) }),
TraceFlags = traceFlags?.GlobalTraceFlags,
Changes = changes.Select(c => ConfigurationChangeJson(c, redact)),
});
}
private static async Task InstancePrivilegesAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct, int? days = null)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
var now = DateTimeOffset.UtcNow;
var audit = await PrivilegeAudit.LoadAsync(repository, instance, now.AddDays(-Math.Clamp(days ?? 90, 1, 366)), now, ct).ConfigureAwait(false);
return Results.Ok(InstanceAuditJson(audit.Instances[0], RedactorFor(options)));
}
private static async Task AllPrivilegesAsync(InsightRepository repository, IOptions options, CancellationToken ct, int? days = null)
{
var now = DateTimeOffset.UtcNow;
var audit = await PrivilegeAudit.LoadAsync(repository, null, now.AddDays(-Math.Clamp(days ?? 90, 1, 366)), now, ct).ConfigureAwait(false);
return Results.Ok(AuditJson(audit, RedactorFor(options)));
}
private static async Task ActivityAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
var redact = RedactorFor(options);
var a = await repository.GetSnapshotAsync(instance.InstanceId, "activity", ct).ConfigureAwait(false);
return Results.Ok(new
{
Instance = instance.Name,
MeasuredAt = a?.CapturedAt,
Thresholds = a is null ? null : new { a.TransactionMinutes, a.TransactionLogMegabytes, a.RequestMinutes },
Transactions = (a?.Transactions ?? []).Select(t => new
{
t.SessionId, t.BeganAt, AgeMinutes = t.Age(a!.CapturedAt).TotalMinutes, t.DatabaseName, t.Databases, t.LogMegabytes, t.SessionStatus, t.WaitType,
LoginName = redact.Value(t.LoginName), HostName = redact.Value(t.HostName), ProgramName = redact.Value(t.ProgramName), Statement = redact.Value(t.Statement),
}),
LongRequests = (a?.LongRequests ?? []).Select(r => new
{
r.SessionId, r.StartedAt, ElapsedMinutes = r.ElapsedMs / 60000.0, r.Command, r.Status, r.WaitType, r.DatabaseName, r.BlockingSessionId,
LoginName = redact.Value(r.LoginName), HostName = redact.Value(r.HostName), ProgramName = redact.Value(r.ProgramName), Statement = redact.Value(r.Statement),
}),
});
}
private static async Task TempdbAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
var redact = RedactorFor(options);
var t = await repository.GetSnapshotAsync(instance.InstanceId, "tempdb", ct).ConfigureAwait(false);
return Results.Ok(t is null ? new { Instance = instance.Name } : (object)new
{
Instance = instance.Name,
MeasuredAt = t.CapturedAt,
t.SizeMegabytes, t.FreeMegabytes, t.FreePercent, t.CanGrow, t.VersionStoreMegabytes, t.UserObjectsMegabytes, t.InternalObjectsMegabytes,
t.VersionStoreThresholdMegabytes, t.VersionStoreHistory, t.VersionGenerators,
OldestSnapshotTransaction = t.OldestSnapshotTransaction is { } o
? new { o.SessionId, o.ElapsedSeconds, LoginName = redact.Value(o.LoginName), ProgramName = redact.Value(o.ProgramName) }
: null,
TopSessions = t.TopSessions.Select(s => new
{
s.SessionId, s.Since, s.UsedMegabytes, s.Status, LoginName = redact.Value(s.LoginName), ProgramName = redact.Value(s.ProgramName), HostName = redact.Value(s.HostName),
}),
Explanation = new { VersionStore = MetricAssessment.Explanation(Metrics.VersionStoreMegabytes), Used = MetricAssessment.Explanation(Metrics.TempdbUsedMegabytes) },
});
}
private static async Task ErrorLogAsync(string name, InsightRepository repository, IOptions options, CancellationToken ct, int? days = null)
{
if (await repository.GetInstanceAsync(name, ct).ConfigureAwait(false) is not { } instance)
{
return Results.NotFound();
}
var redact = RedactorFor(options);
var now = DateTimeOffset.UtcNow;
var snapshot = await repository.GetSnapshotAsync(instance.InstanceId, "error-log", ct).ConfigureAwait(false);
var events = await repository.GetErrorLogEventsAsync(instance.InstanceId, now.AddDays(-Math.Clamp(days ?? 7, 1, 90)), now, 500, ct).ConfigureAwait(false);
return Results.Ok(new
{
Instance = instance.Name,
Source = snapshot?.Source.ToString(),
snapshot?.Coverage,
ReadAt = snapshot?.CapturedAt,
Events = events.Select(e => new { e.Category, e.Error, e.Severity, e.Count, e.FirstAt, e.LastAt, Sample = redact.Value(e.Sample), e.Source }),
});
}
private static async Task WaitTypeAsync(string name, string waitType, InsightRepository repository, IOptions options, CancellationToken ct,
DateTimeOffset? from = null, DateTimeOffset? to = null)
{
var (instance, start, end, error) = await WindowAsync(name, repository, from, to, TimeSpan.FromHours(24), ct).ConfigureAwait(false);
if (error is not null)
{
return error;
}
var type = waitType.Trim().ToUpperInvariant();
var explanation = WaitKnowledge.Lookup(type);
var hourly = TimeWindow.ChooseResolution(start, DateTimeOffset.UtcNow, new RetentionOptions().RawDays) == WindowResolution.Hourly;
var series = await repository.GetWaitTypeSeriesAsync(instance!.InstanceId, type, start, end, hourly, ct).ConfigureAwait(false);
var statements = await repository.GetWaitTypeStatementsAsync(instance.InstanceId, type, start, end, 25, ct).ConfigureAwait(false);
var redact = RedactorFor(options);
return Results.Ok(new
{
Instance = instance.Name,
WaitType = type,
From = start,
To = end,
Explanation = new { explanation.Title, explanation.Meaning, explanation.Normal, explanation.Causes, explanation.Actions, explanation.DynamicsAx, explanation.Known },
WaitMsPerMinute = series,
Statements = statements.Select(s => new
{
s.QueryKey, s.DatabaseName, Text = redact.Value(s.Text), s.Samples, s.Share, s.EstimatedWaitSeconds, s.AverageWaitMs, s.MaxWaitMs, s.BlockedSamples,
}),
});
}
}