Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Azure: add DTU details #413

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/Opserver.Core/Data/SQL/SQLAzureServer.AzureResourceHistory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Opserver.Data.Dashboard;

namespace Opserver.Data.SQL
{
public partial class SQLInstance
{
private Cache<List<AzureResourceEvent>> _azureResourceHistory;

public Cache<List<AzureResourceEvent>> AzureResourceHistory =>
_azureResourceHistory ??= GetSqlCache(nameof(AzureResourceHistory), async conn =>
{
var sql = GetFetchSQL<AzureResourceEvent>();
var result = await conn.QueryAsync<AzureResourceEvent>(sql);
var lastResult = result.Count > 0 ? result.Last() : null;
CurrentDTUPercent = lastResult?.AvgDTUPercent;
CurrentDTULimit = lastResult?.DTULimit;
return result;
});

public double? CurrentDTUPercent { get; set; }
public int? CurrentDTULimit { get; set; }

public class AzureResourceEvent : ISQLVersioned, IGraphPoint
{
Version IMinVersioned.MinVersion => SQLServerVersions.SQL2012.RTM;
SQLServerEditions ISQLVersioned.SupportedEditions => SQLServerEditions.Azure;

private long? _dateEpoch;
public long DateEpoch => _dateEpoch ??= EventTime.ToEpochTime();
public DateTime EventTime { get; internal set; }
public double AvgDTUPercent { get; internal set; }
public double AvgCPUPercent { get; internal set; }
public double AvgDataIOPercent { get; internal set; }
public double AvgLogWritePercent { get; internal set; }
public double AvgMemoryPercent { get; internal set; }
public double XTPStoragePercent { get; internal set; }
public double MaxWorkerPercent { get; internal set; }
public double MaxSessionPercent { get; internal set; }
public double AvgInstanceCPUPercent { get; internal set; }

public double AvgInstanceMemoryPercent { get; internal set; }
public double AvgLoginRatePercent { get; internal set; }
public double CPULimit { get; internal set; }
public int DTULimit { get; internal set; }
public ReplicaRoleType ReplicaRole { get; internal set; }

public enum ReplicaRoleType
{
Primary = 0,
ReadOnly = 1,
Forwarder = 2,
}

public string GetFetchSQL(in SQLServerEngine e) => @"
Select end_time AS EventTime,
(Select Max(v) From (Values (avg_cpu_percent), (avg_data_io_percent), (avg_log_write_percent)) As value(v)) As AvgDTUPercent,
avg_cpu_percent AvgCPUPercent,
avg_data_io_percent AvgDataIOPercent,
avg_log_write_percent AvgLogWritePercent,
avg_memory_usage_percent AvgMemoryPercent,
xtp_storage_percent XTPStoragePercent,
max_worker_percent MaxWorkerPercent,
max_session_percent MaxSessionPercent,
avg_instance_cpu_percent AvgInstanceCPUPercent,
avg_instance_memory_percent AvgInstanceMemoryPercent,
avg_login_rate_percent AvgLoginRatePercent,
cpu_limit CPULimit,
dtu_limit DTULimit,
replica_role ReplicaRole
From sys.dm_db_resource_stats
Cross Join sys.dm_os_sys_info osi;";
}
}
}
44 changes: 31 additions & 13 deletions src/Opserver.Core/Data/SQL/SQLAzureServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,36 @@ public class SQLAzureServer : SQLInstance
var instances = new List<SQLInstance>();
// grab the list of databases in the SQL Azure instance
// and generate a SQLInstance for each one
var databases = await conn.QueryAsync<string>("Select name From sys.databases");
var databases = await conn.QueryAsync<AzureDatabaseInfo>(@"
Select db.name Name,
dbso.edition Edition,
dbso.service_objective SKU,
dbso.elastic_pool_name ElasticPoolName
From sys.databases db
Join sys.database_service_objectives dbso On db.database_id = dbso.database_id");
foreach (var database in databases)
{
// is there an existing instance?
var key = Settings.Name + ":" + database;
var key = Settings.Name + ":" + database.Name;
var instance = _instancesByKey.GetOrAdd(
key,
key => new SQLInstance(
Module,
new SQLSettings.Instance
{
Name = key,
ConnectionString = new SqlConnectionStringBuilder(ConnectionString)
Module,
new SQLSettings.Instance
{
InitialCatalog = database
}.ConnectionString,
RefreshIntervalSeconds = Settings.RefreshIntervalSeconds,
}
)
Name = key,
ConnectionString = new SqlConnectionStringBuilder(ConnectionString)
{
InitialCatalog = database.Name
}.ConnectionString,
RefreshIntervalSeconds = Settings.RefreshIntervalSeconds,
}
)
{
SKU = database.SKU,
Edition = database.Edition,
ElasticPoolName = database.ElasticPoolName,
}
);

instances.Add(instance);
Expand All @@ -52,9 +63,16 @@ public override IEnumerable<Cache> DataPollers
}
}


public SQLAzureServer(SQLModule module, SQLSettings.Instance settings) : base(module, settings)
{
}

public class AzureDatabaseInfo
{
public string Name { get; set; }
public string Edition { get; set; }
public string SKU { get; set; }
public string ElasticPoolName { get; set; }
}
}
}
7 changes: 7 additions & 0 deletions src/Opserver.Core/Data/SQL/SQLInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public partial class SQLInstance : PollNode<SQLModule>, ISearchableNode
public SQLServerEngine Engine { get; internal set; } = new SQLServerEngine(new Version(), SQLServerEditions.Standard); // default to 0.0
protected SQLSettings.Instance Settings { get; }

// Azure-specific database attributes
public string SKU { get; set; }
public string Edition { get; set; }
public string ElasticPoolName { get; set; }

protected static readonly ConcurrentDictionary<Tuple<string, SQLServerEngine>, string> QueryLookup =
new ConcurrentDictionary<Tuple<string, SQLServerEngine>, string>();

Expand Down Expand Up @@ -77,6 +82,8 @@ public override IEnumerable<Cache> DataPollers
yield return Connections;
if (Supports<SQLConnectionSummaryInfo>())
yield return ConnectionsSummary;
if (Supports<AzureResourceEvent>())
yield return AzureResourceHistory;
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/Opserver.Web/Controllers/GraphController.Spark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,21 @@ public ActionResult SQLCPUSpark(string node)
return SparkSVG(points, 100, p => p.ProcessUtilization, start);
}

[OnlyAllow(SQLRoles.Viewer)]
[ResponseCache(Duration = SparkGraphDuration, VaryByQueryKeys = new string[] { "node" }, Location = ResponseCacheLocation.Client)]
[Route("graph/sql/dtu/spark")]
public ActionResult SQLDTUSpark(string node)
{
var instance = Sql.GetInstance(node);
if (instance == null) return ContentNotFound($"SQLNode not found with name = '{node}'");
var start = DateTime.UtcNow.AddHours(-1);
var points = instance.AzureResourceHistory.Data?.Where(p => p.EventTime >= start).ToList();

if (points == null || points.Count == 0) return EmptySparkSVG();

return SparkSVG(points, 100, p => p.AvgDTUPercent, start);
}

public async Task<ActionResult> SparkSvgAll<T>(string key, Func<Node, Task<List<T>>> getPoints, Func<Node, List<T>, long> getMax, Func<T, double> getVal) where T : IGraphPoint
{
const int width = SparkPoints;
Expand Down
13 changes: 11 additions & 2 deletions src/Opserver.Web/Views/SQL/Servers.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@
</tr>
<tr>
<th>Database</th>
<th>SKU</th>
<th>CPU</th>
<th class="text-muted">(Last Hour)</th>
<th>DTUs</th>
<th class="text-muted">(Last Hour)</th>
<th>Memory</th>
<th title="Connections / Sessions">Conns <span class="text-muted">/ Sess</span></th>
<th>Batches<span class="text-muted">/sec</span></th>
Expand All @@ -97,17 +100,23 @@
@if (!s.Instances.ContainsData)
{
<tr>
<td colspan="7">
<td colspan="10">

</td>
</tr>
}
@foreach (var i in s.Instances.SafeData(true))
{
<tr class="@i.RowClass()">
<td title="@i.Description">@i.IconSpan() <a href="~/sql/[email protected]()">@i.Name</a></td>
<td title="@i.Description (Edition: @i.Edition, Elastic Pool: @(i.ElasticPoolName ?? "None"))">@i.IconSpan() <a href="~/sql/[email protected]()">@i.Name</a></td>
<td title="DTU Limit: @(i.CurrentDTULimit?.ToString("n0") ?? "Unknown")">@i.SKU</td>

<td><a href="~/sql/[email protected]()">@(i.CurrentCPUPercent.HasValue ? i.CurrentCPUPercent.ToString() + "%" : "")</a></td>
<td><img src="~/graph/sql/cpu/[email protected]&[email protected]("yyyy-MM-dd")" width="100" height="16" alt="SQL CPU for @i.Name Last Hour" /></td>

<td title="DTU Limit: @(i.CurrentDTULimit?.ToString("n0") ?? "Unknown")">@(i.CurrentDTUPercent.HasValue ? i.CurrentDTUPercent.ToString() + "%" : "")</td>
<td><img src="~/graph/sql/dtu/[email protected]&[email protected]("yyyy-MM-dd")" width="100" height="16" alt="DTU Usage for @i.Name Last Hour" /></td>

<partial Name="Partials.MemoryCell" Model="@new PartialsMemoryCellModel(i, 2)" />
@ConnectionsCell(i)
<td>@(i.BatchesPerSec?.ToComma())</td>
Expand Down