Skip to content

Commit 01532d4

Browse files
authored
[Instrumentation.SqlClient] Implements database metric db.client.operation.duration (open-telemetry#2309)
1 parent 4d7161b commit 01532d4

File tree

7 files changed

+408
-80
lines changed

7 files changed

+408
-80
lines changed

src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.Rec
1111
OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.get -> bool
1212
OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SetDbStatementForText.set -> void
1313
OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions.SqlClientTraceInstrumentationOptions() -> void
14+
OpenTelemetry.Metrics.SqlClientMeterProviderBuilderExtensions
1415
OpenTelemetry.Trace.TracerProviderBuilderExtensions
16+
static OpenTelemetry.Metrics.SqlClientMeterProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder!
1517
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder!
1618
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, string? name, System.Action<OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions!>? configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder!
1719
static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSqlClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action<OpenTelemetry.Instrumentation.SqlClient.SqlClientTraceInstrumentationOptions!>! configureSqlClientTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder!

src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlActivitySourceHelper.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using System.Diagnostics;
5+
using System.Diagnostics.Metrics;
56
using System.Reflection;
67
using OpenTelemetry.Internal;
78
using OpenTelemetry.Trace;
@@ -21,6 +22,14 @@ internal sealed class SqlActivitySourceHelper
2122
public static readonly string ActivitySourceName = AssemblyName.Name!;
2223
public static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion());
2324

25+
public static readonly string MeterName = AssemblyName.Name!;
26+
public static readonly Meter Meter = new(MeterName, Assembly.GetPackageVersion());
27+
28+
public static readonly Histogram<double> DbClientOperationDuration = Meter.CreateHistogram<double>(
29+
"db.client.operation.duration",
30+
"s",
31+
"Duration of database client operations.");
32+
2433
public static TagList GetTagListFromConnectionInfo(string? dataSource, string? databaseName, SqlClientTraceInstrumentationOptions options, out string activityName)
2534
{
2635
activityName = MicrosoftSqlServerDatabaseSystemName;

src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ internal sealed class SqlClientDiagnosticListener : ListenerHandler
2626
public const string SqlDataWriteCommandError = "System.Data.SqlClient.WriteCommandError";
2727
public const string SqlMicrosoftWriteCommandError = "Microsoft.Data.SqlClient.WriteCommandError";
2828

29+
private static readonly string[] SharedTagNames =
30+
[
31+
SemanticConventions.AttributeDbSystem,
32+
SemanticConventions.AttributeDbCollectionName,
33+
SemanticConventions.AttributeDbNamespace,
34+
SemanticConventions.AttributeDbResponseStatusCode,
35+
SemanticConventions.AttributeDbOperationName,
36+
SemanticConventions.AttributeErrorType,
37+
SemanticConventions.AttributeServerPort,
38+
SemanticConventions.AttributeServerAddress,
39+
];
40+
2941
private readonly PropertyFetcher<object> commandFetcher = new("Command");
3042
private readonly PropertyFetcher<object> connectionFetcher = new("Connection");
3143
private readonly PropertyFetcher<string> dataSourceFetcher = new("DataSource");
@@ -34,6 +46,7 @@ internal sealed class SqlClientDiagnosticListener : ListenerHandler
3446
private readonly PropertyFetcher<object> commandTextFetcher = new("CommandText");
3547
private readonly PropertyFetcher<Exception> exceptionFetcher = new("Exception");
3648
private readonly PropertyFetcher<int> exceptionNumberFetcher = new("Number");
49+
private readonly AsyncLocal<long> beginTimestamp = new();
3750

3851
public SqlClientDiagnosticListener(string sourceName)
3952
: base(sourceName)
@@ -44,7 +57,7 @@ public SqlClientDiagnosticListener(string sourceName)
4457

4558
public override void OnEventWritten(string name, object? payload)
4659
{
47-
if (SqlClientInstrumentation.TracingHandles == 0)
60+
if (SqlClientInstrumentation.TracingHandles == 0 && SqlClientInstrumentation.MetricHandles == 0)
4861
{
4962
return;
5063
}
@@ -77,6 +90,7 @@ public override void OnEventWritten(string name, object? payload)
7790
if (activity == null)
7891
{
7992
// There is no listener or it decided not to sample the current request.
93+
this.beginTimestamp.Value = Stopwatch.GetTimestamp();
8094
return;
8195
}
8296

@@ -162,15 +176,18 @@ public override void OnEventWritten(string name, object? payload)
162176
if (activity == null)
163177
{
164178
SqlClientInstrumentationEventSource.Log.NullActivity(name);
179+
this.RecordDuration(null, payload);
165180
return;
166181
}
167182

168183
if (activity.Source != SqlActivitySourceHelper.ActivitySource)
169184
{
185+
this.RecordDuration(null, payload);
170186
return;
171187
}
172188

173189
activity.Stop();
190+
this.RecordDuration(activity, payload);
174191
}
175192

176193
break;
@@ -180,11 +197,13 @@ public override void OnEventWritten(string name, object? payload)
180197
if (activity == null)
181198
{
182199
SqlClientInstrumentationEventSource.Log.NullActivity(name);
200+
this.RecordDuration(null, payload);
183201
return;
184202
}
185203

186204
if (activity.Source != SqlActivitySourceHelper.ActivitySource)
187205
{
206+
this.RecordDuration(null, payload);
188207
return;
189208
}
190209

@@ -217,6 +236,7 @@ public override void OnEventWritten(string name, object? payload)
217236
finally
218237
{
219238
activity.Stop();
239+
this.RecordDuration(activity, payload, hasError: true);
220240
}
221241
}
222242

@@ -225,5 +245,84 @@ public override void OnEventWritten(string name, object? payload)
225245
break;
226246
}
227247
}
248+
249+
private void RecordDuration(Activity? activity, object? payload, bool hasError = false)
250+
{
251+
if (SqlClientInstrumentation.MetricHandles == 0)
252+
{
253+
return;
254+
}
255+
256+
TagList tags = default(TagList);
257+
258+
if (activity != null && activity.IsAllDataRequested)
259+
{
260+
foreach (var name in SharedTagNames)
261+
{
262+
var value = activity.GetTagItem(name);
263+
if (value != null)
264+
{
265+
tags.Add(name, value);
266+
}
267+
}
268+
}
269+
else if (payload != null)
270+
{
271+
if (this.commandFetcher.TryFetch(payload, out var command) && command != null &&
272+
this.connectionFetcher.TryFetch(command, out var connection))
273+
{
274+
this.databaseFetcher.TryFetch(connection, out var databaseName);
275+
this.dataSourceFetcher.TryFetch(connection, out var dataSource);
276+
277+
var connectionTags = SqlActivitySourceHelper.GetTagListFromConnectionInfo(
278+
dataSource,
279+
databaseName,
280+
SqlClientInstrumentation.TracingOptions,
281+
out _);
282+
283+
foreach (var tag in connectionTags)
284+
{
285+
tags.Add(tag.Key, tag.Value);
286+
}
287+
288+
if (this.commandTypeFetcher.TryFetch(command, out var commandType) &&
289+
commandType == CommandType.StoredProcedure)
290+
{
291+
if (this.commandTextFetcher.TryFetch(command, out var commandText))
292+
{
293+
tags.Add(SemanticConventions.AttributeDbOperationName, "EXECUTE");
294+
tags.Add(SemanticConventions.AttributeDbCollectionName, commandText);
295+
}
296+
}
297+
}
298+
299+
if (hasError)
300+
{
301+
if (this.exceptionFetcher.TryFetch(payload, out var exception) && exception != null)
302+
{
303+
tags.Add(SemanticConventions.AttributeErrorType, exception.GetType().FullName);
304+
305+
if (this.exceptionNumberFetcher.TryFetch(exception, out var exceptionNumber))
306+
{
307+
tags.Add(SemanticConventions.AttributeDbResponseStatusCode, exceptionNumber.ToString(CultureInfo.InvariantCulture));
308+
}
309+
}
310+
}
311+
}
312+
313+
var duration = activity?.Duration.TotalSeconds ?? this.CalculateDurationFromTimestamp();
314+
SqlActivitySourceHelper.DbClientOperationDuration.Record(duration, tags);
315+
}
316+
317+
private double CalculateDurationFromTimestamp()
318+
{
319+
var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
320+
var begin = this.beginTimestamp.Value;
321+
var end = Stopwatch.GetTimestamp();
322+
var delta = end - begin;
323+
var ticks = (long)(timestampToTicks * delta);
324+
var duration = new TimeSpan(ticks);
325+
return duration.TotalSeconds;
326+
}
228327
}
229328
#endif

src/OpenTelemetry.Instrumentation.SqlClient/SqlClientInstrumentation.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal sealed class SqlClientInstrumentation : IDisposable
2222
#if NET
2323
internal const string SqlClientTrimmingUnsupportedMessage = "Trimming is not yet supported with SqlClient instrumentation.";
2424
#endif
25+
internal static int MetricHandles;
2526
internal static int TracingHandles;
2627
#if NETFRAMEWORK
2728
private readonly SqlEventSourceListener sqlEventSourceListener;
@@ -61,6 +62,8 @@ private SqlClientInstrumentation()
6162

6263
public static SqlClientTraceInstrumentationOptions TracingOptions { get; set; } = new SqlClientTraceInstrumentationOptions();
6364

65+
public static IDisposable AddMetricHandle() => new MetricHandle();
66+
6467
public static IDisposable AddTracingHandle() => new TracingHandle();
6568

6669
/// <inheritdoc/>
@@ -73,6 +76,28 @@ public void Dispose()
7376
#endif
7477
}
7578

79+
#if NET
80+
[RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)]
81+
#endif
82+
private sealed class MetricHandle : IDisposable
83+
{
84+
private bool disposed;
85+
86+
public MetricHandle()
87+
{
88+
Interlocked.Increment(ref MetricHandles);
89+
}
90+
91+
public void Dispose()
92+
{
93+
if (!this.disposed)
94+
{
95+
Interlocked.Decrement(ref MetricHandles);
96+
this.disposed = true;
97+
}
98+
}
99+
}
100+
76101
#if NET
77102
[RequiresUnreferencedCode(SqlClientTrimmingUnsupportedMessage)]
78103
#endif
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NET
5+
using System.Diagnostics.CodeAnalysis;
6+
#endif
7+
using OpenTelemetry.Instrumentation.SqlClient;
8+
using OpenTelemetry.Instrumentation.SqlClient.Implementation;
9+
using OpenTelemetry.Internal;
10+
11+
namespace OpenTelemetry.Metrics;
12+
13+
/// <summary>
14+
/// Extension methods to simplify registering of dependency instrumentation.
15+
/// </summary>
16+
public static class SqlClientMeterProviderBuilderExtensions
17+
{
18+
/// <summary>
19+
/// Enables SqlClient instrumentation.
20+
/// </summary>
21+
/// <param name="builder"><see cref="MeterProviderBuilder"/> being configured.</param>
22+
/// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns>
23+
#if NET
24+
[RequiresUnreferencedCode(SqlClientInstrumentation.SqlClientTrimmingUnsupportedMessage)]
25+
#endif
26+
public static MeterProviderBuilder AddSqlClientInstrumentation(this MeterProviderBuilder builder)
27+
{
28+
Guard.ThrowIfNull(builder);
29+
30+
builder.AddInstrumentation(sp =>
31+
{
32+
return SqlClientInstrumentation.AddMetricHandle();
33+
});
34+
35+
builder.AddMeter(SqlActivitySourceHelper.MeterName);
36+
37+
return builder;
38+
}
39+
}

0 commit comments

Comments
 (0)