Skip to content

Commit 7c35246

Browse files
committed
feat: add otel metrics
1 parent 766fd9a commit 7c35246

File tree

7 files changed

+70
-9
lines changed

7 files changed

+70
-9
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,20 @@ services.Configure<SqlServerOptions>(Configuration.GetSection("SignalR:SqlServer
7878
## OpenTelemetry Support
7979

8080
This library includes OpenTelemetry instrumentation that wraps background database queries in activities, making them more easily identified and grouped in your collected telemetry.
81-
81+
a
8282
### Setup
8383

84-
To enable OpenTelemetry collection of these trace spans, add the source to your tracing configuration:
84+
To enable OpenTelemetry collection of these trace spans and metrics, add the source and meter to your configuration:
8585

8686
``` cs
8787
builder.Services.AddOpenTelemetry()
8888
.WithTracing(tracing => tracing
8989
.AddSource("IntelliTect.AspNetCore.SignalR.SqlServer")
9090
// ... other instrumentation
91+
)
92+
.WithMetrics(metrics => metrics
93+
.AddMeter("IntelliTect.AspNetCore.SignalR.SqlServer")
94+
// ... other instrumentation
9195
);
9296
```
9397

@@ -100,6 +104,17 @@ The library creates activities for the following operations:
100104
- `SignalR.SqlServer.Poll` - Polling operations (database reads, when Service Broker is not used)
101105
- `SignalR.SqlServer.Publish` - Message publishing operations (database writes)
102106

107+
### Metrics
108+
109+
The library also provides metrics to help monitor the health and performance of the SQL Server backplane:
110+
111+
- `signalr.sqlserver.poll_delay` - Histogram showing the distribution of polling delay intervals, useful for understanding backoff patterns and system health
112+
- `signalr.sqlserver.query_duration` - Histogram tracking the duration of SQL Server query execution for reading messages
113+
- `signalr.sqlserver.rows_read_total` - Counter tracking the total number of message rows read from SQL Server
114+
- `signalr.sqlserver.rows_written_total` - Counter tracking the total number of message rows written to SQL Server
115+
116+
These metrics help you understand polling patterns, database performance, message throughput, and can be useful for tuning performance or identifying when Service Broker fallback to polling occurs.
117+
103118
### Filtering Noise
104119

105120
Since the SQL Server backplane performs frequent polling operations, you may want to filter out successful, fast queries to reduce trace noise.

demo/DemoServer/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
.WithMetrics(metrics => metrics
3535
.AddAspNetCoreInstrumentation()
3636
.AddSqlClientInstrumentation()
37+
.AddMeter("IntelliTect.AspNetCore.SignalR.SqlServer")
3738
)
3839
.WithTracing(tracing => tracing
3940
.AddSource(builder.Environment.ApplicationName)

src/IntelliTect.AspNetCore.SignalR.SqlServer/Internal/SqlServer/SqlReceiver.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Data;
77
using System.Diagnostics;
88
using System.Diagnostics.CodeAnalysis;
9+
using System.Diagnostics.Metrics;
910
using System.Globalization;
1011
using System.Threading;
1112
using System.Threading.Tasks;
@@ -43,6 +44,21 @@ internal class SqlReceiver : IDisposable
4344
private readonly string _maxIdSql = "SELECT [PayloadId] FROM [{0}].[{1}_Id]";
4445
private readonly string _selectSql = "SELECT [PayloadId], [Payload], [InsertedOn] FROM [{0}].[{1}] WHERE [PayloadId] > @PayloadId";
4546
private readonly TimeSpan _activityMaxDuration = TimeSpan.FromMinutes(10);
47+
48+
private static readonly Histogram<double> _pollDelayHistogram = SqlServerOptions.Meter.CreateHistogram<double>(
49+
"signalr.sqlserver.poll_delay",
50+
"ms",
51+
"Distribution of polling delay intervals showing backoff patterns");
52+
53+
private static readonly Counter<long> _rowsReadCounter = SqlServerOptions.Meter.CreateCounter<long>(
54+
"signalr.sqlserver.rows_read_total",
55+
"rows",
56+
"Total number of message rows read from SQL Server");
57+
58+
private static readonly Histogram<double> _queryDurationHistogram = SqlServerOptions.Meter.CreateHistogram<double>(
59+
"signalr.sqlserver.query_duration",
60+
"ms",
61+
"Duration of SQL Server query execution for reading messages");
4662

4763

4864
public SqlReceiver(SqlServerOptions options, ILogger logger, string tableName, string tracePrefix)
@@ -222,7 +238,6 @@ private async Task PollingLoop(CancellationToken cancellationToken)
222238

223239
try
224240
{
225-
226241
var delays = _updateLoopRetryDelays;
227242
for (var retrySetIndex = 0; retrySetIndex < delays.Length; retrySetIndex++)
228243
{
@@ -253,6 +268,10 @@ private async Task PollingLoop(CancellationToken cancellationToken)
253268
}
254269

255270
recordCount = await ReadRows(null);
271+
272+
// Record polling delay distribution to show backoff patterns
273+
_pollDelayHistogram.Record(retryDelay,
274+
new KeyValuePair<string, object?>("signalr.hub", _tracePrefix));
256275
}
257276
catch (TaskCanceledException) { return; }
258277
catch (Exception ex)
@@ -336,6 +355,7 @@ private async Task<int> ReadRows(Action<SqlCommand>? beforeExecute)
336355
beforeExecute?.Invoke(command);
337356

338357
// Fetch any rows that might already be available after the last PayloadId.
358+
var start = DateTime.UtcNow;
339359
var reader = await command.ExecuteReaderAsync();
340360
var recordCount = 0;
341361

@@ -345,6 +365,18 @@ private async Task<int> ReadRows(Action<SqlCommand>? beforeExecute)
345365
await ProcessRecord(reader);
346366
}
347367

368+
// Record query duration and rows read metrics
369+
if (_queryDurationHistogram.Enabled)
370+
{
371+
_queryDurationHistogram.Record((DateTime.UtcNow - start).TotalMilliseconds,
372+
new KeyValuePair<string, object?>("signalr.hub", _tracePrefix));
373+
}
374+
375+
if (recordCount > 0 && _rowsReadCounter.Enabled)
376+
{
377+
_rowsReadCounter.Add(recordCount, new KeyValuePair<string, object?>("signalr.hub", _tracePrefix));
378+
}
379+
348380
return recordCount;
349381
}
350382

src/IntelliTect.AspNetCore.SignalR.SqlServer/Internal/SqlServer/SqlSender.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics.Metrics;
67
using System.Globalization;
78
using System.Threading.Tasks;
89
using Microsoft.Data.SqlClient;
@@ -12,15 +13,22 @@ namespace IntelliTect.AspNetCore.SignalR.SqlServer.Internal
1213
{
1314
internal class SqlSender
1415
{
16+
private static readonly Counter<long> _rowsWrittenCounter = SqlServerOptions.Meter.CreateCounter<long>(
17+
"signalr.sqlserver.rows_written_total",
18+
"rows",
19+
"Total number of message rows written to SQL Server");
20+
1521
private readonly string _insertDml;
1622
private readonly ILogger _logger;
1723
private readonly SqlServerOptions _options;
24+
private readonly string _hubName;
1825

19-
public SqlSender(SqlServerOptions options, ILogger logger, string tableName)
26+
public SqlSender(SqlServerOptions options, ILogger logger, string tableName, string hubName)
2027
{
2128
_options = options;
2229
_insertDml = BuildInsertString(tableName);
2330
_logger = logger;
31+
_hubName = hubName;
2432
}
2533

2634
private string BuildInsertString(string tableName)
@@ -43,6 +51,9 @@ public async Task Send(byte[] message)
4351
await connection.OpenAsync();
4452

4553
await command.ExecuteNonQueryAsync();
54+
55+
// Record rows written metric
56+
_rowsWrittenCounter.Add(1, new KeyValuePair<string, object?>("signalr.hub", _hubName));
4657
}
4758
}
4859
}

src/IntelliTect.AspNetCore.SignalR.SqlServer/Internal/SqlServer/SqlStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public SqlStream(SqlServerOptions options, ILogger logger, int streamIndex, stri
2626
_logger = logger;
2727
_tracePrefix = tracePrefix;
2828

29-
_sender = new SqlSender(options, logger, tableName);
29+
_sender = new SqlSender(options, logger, tableName, _tracePrefix);
3030
_receiver = new SqlReceiver(options, logger, tableName, _tracePrefix);
3131
}
3232

src/IntelliTect.AspNetCore.SignalR.SqlServer/SqlServerOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.ComponentModel.DataAnnotations;
66
using System.Diagnostics;
7+
using System.Diagnostics.Metrics;
78
using System.IO;
89
using System.Net;
910
using System.Threading.Tasks;
@@ -16,6 +17,7 @@ namespace IntelliTect.AspNetCore.SignalR.SqlServer
1617
public class SqlServerOptions
1718
{
1819
internal static readonly ActivitySource ActivitySource = new("IntelliTect.AspNetCore.SignalR.SqlServer");
20+
internal static readonly Meter Meter = new("IntelliTect.AspNetCore.SignalR.SqlServer");
1921

2022
/// <summary>
2123
/// Shared lock to prevent multiple concurrent installs against the same DB.

test/IntelliTect.AspNetCore.SignalR.SqlServer.Tests/SqlServerEndToEndTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public async Task CanSendAndReceivePayloads_WithServiceBroker_UnderHeavyLoad()
6464

6565
var prefix = nameof(CanSendAndReceivePayloads_WithServiceBroker_UnderHeavyLoad);
6666
var installer = new SqlInstaller(options, NullLogger.Instance, prefix, prefix);
67-
var receiver = new SqlReceiver(options, NullLogger.Instance, prefix + "_0", "");
67+
var receiver = new SqlReceiver(options, NullLogger.Instance, prefix + "_0", "TestHub");
6868

6969
var receivedMessages = new ConcurrentBag<byte[]>();
7070
await installer.Install();
@@ -83,7 +83,7 @@ public async Task CanSendAndReceivePayloads_WithServiceBroker_UnderHeavyLoad()
8383
// the hub to be sending messages.
8484
int numSenders = 100;
8585
int numSent = 0;
86-
var sender = new SqlSender(options, NullLogger.Instance, prefix + "_0");
86+
var sender = new SqlSender(options, NullLogger.Instance, prefix + "_0", "TestHub");
8787
for (int i = 0; i < numSenders; i++)
8888
{
8989
_ = Task.Run(async () =>
@@ -116,8 +116,8 @@ public async Task CanSendAndReceivePayloads_WithServiceBroker_UnderHeavyLoad()
116116
private async Task RunCore(SqlServerOptions options, string prefix)
117117
{
118118
var installer = new SqlInstaller(options, NullLogger.Instance, prefix, prefix);
119-
var sender = new SqlSender(options, NullLogger.Instance, prefix + "_0");
120-
var receiver = new SqlReceiver(options, NullLogger.Instance, prefix + "_0", "");
119+
var sender = new SqlSender(options, NullLogger.Instance, prefix + "_0", "TestHub");
120+
var receiver = new SqlReceiver(options, NullLogger.Instance, prefix + "_0", "TestHub");
121121

122122
var receivedMessages = new ConcurrentBag<byte[]>();
123123
var receivedEvent = new SemaphoreSlim(0);

0 commit comments

Comments
 (0)