Skip to content

Commit 7dd65e8

Browse files
committed
Adds .NET distributed tracing instrumentation.
1 parent 256ea2f commit 7dd65e8

File tree

3 files changed

+227
-36
lines changed

3 files changed

+227
-36
lines changed

src/FirebirdSql.Data.FirebirdClient/FirebirdClient/FbCommand.cs

Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
using System.ComponentModel;
2121
using System.Data;
2222
using System.Data.Common;
23+
using System.Diagnostics;
2324
using System.Text;
2425
using System.Threading;
2526
using System.Threading.Tasks;
2627
using FirebirdSql.Data.Common;
2728
using FirebirdSql.Data.Logging;
29+
using FirebirdSql.Data.Trace;
2830
using Microsoft.Extensions.Logging;
2931

3032
namespace FirebirdSql.Data.FirebirdClient;
@@ -50,6 +52,7 @@ public sealed class FbCommand : DbCommand, IFbPreparedCommand, IDescriptorFiller
5052
private int? _commandTimeout;
5153
private int _fetchSize;
5254
private Type[] _expectedColumnTypes;
55+
private Activity _currentActivity;
5356

5457
#endregion
5558

@@ -1064,6 +1067,13 @@ internal void Release()
10641067
_statement.Dispose2();
10651068
_statement = null;
10661069
}
1070+
1071+
if (_currentActivity != null)
1072+
{
1073+
// Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status
1074+
_currentActivity.Dispose();
1075+
_currentActivity = null;
1076+
}
10671077
}
10681078
Task IFbPreparedCommand.ReleaseAsync(CancellationToken cancellationToken) => ReleaseAsync(cancellationToken);
10691079
internal async Task ReleaseAsync(CancellationToken cancellationToken = default)
@@ -1082,6 +1092,13 @@ internal async Task ReleaseAsync(CancellationToken cancellationToken = default)
10821092
await _statement.Dispose2Async(cancellationToken).ConfigureAwait(false);
10831093
_statement = null;
10841094
}
1095+
1096+
if (_currentActivity != null)
1097+
{
1098+
// Do not set status to Ok: https://opentelemetry.io/docs/concepts/signals/traces/#span-status
1099+
_currentActivity.Dispose();
1100+
_currentActivity = null;
1101+
}
10851102
}
10861103

10871104
void IFbPreparedCommand.TransactionCompleted() => TransactionCompleted();
@@ -1302,6 +1319,26 @@ private async ValueTask UpdateParameterValuesAsync(Descriptor descriptor, Cancel
13021319

13031320
#endregion
13041321

1322+
#region Tracing
1323+
1324+
private void TraceCommandStart()
1325+
{
1326+
Debug.Assert(_currentActivity == null);
1327+
if (FbActivitySource.Source.HasListeners())
1328+
_currentActivity = FbActivitySource.CommandStart(this);
1329+
}
1330+
1331+
private void TraceCommandException(Exception e)
1332+
{
1333+
if (_currentActivity != null)
1334+
{
1335+
FbActivitySource.CommandException(_currentActivity, e);
1336+
_currentActivity = null;
1337+
}
1338+
}
1339+
1340+
#endregion Tracing
1341+
13051342
#region Private Methods
13061343

13071344
private void Prepare(bool returnsSet)
@@ -1446,57 +1483,73 @@ private async Task PrepareAsync(bool returnsSet, CancellationToken cancellationT
14461483
private void ExecuteCommand(CommandBehavior behavior, bool returnsSet)
14471484
{
14481485
LogMessages.CommandExecution(Log, this);
1486+
TraceCommandStart();
1487+
try
1488+
{
1489+
Prepare(returnsSet);
14491490

1450-
Prepare(returnsSet);
1491+
if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess ||
1492+
(behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult ||
1493+
(behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow ||
1494+
(behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection ||
1495+
behavior == CommandBehavior.Default)
1496+
{
1497+
// Set the fetch size
1498+
_statement.FetchSize = _fetchSize;
14511499

1452-
if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess ||
1453-
(behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult ||
1454-
(behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow ||
1455-
(behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection ||
1456-
behavior == CommandBehavior.Default)
1457-
{
1458-
// Set the fetch size
1459-
_statement.FetchSize = _fetchSize;
1500+
// Set if it's needed the Records Affected information
1501+
_statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected;
14601502

1461-
// Set if it's needed the Records Affected information
1462-
_statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected;
1503+
// Validate input parameter count
1504+
if (_namedParameters.Count > 0 && !HasParameters)
1505+
{
1506+
throw FbException.Create("Must declare command parameters.");
1507+
}
14631508

1464-
// Validate input parameter count
1465-
if (_namedParameters.Count > 0 && !HasParameters)
1466-
{
1467-
throw FbException.Create("Must declare command parameters.");
1509+
// Execute
1510+
_statement.Execute(CommandTimeout * 1000, this);
14681511
}
1469-
1470-
// Execute
1471-
_statement.Execute(CommandTimeout * 1000, this);
1512+
}
1513+
catch (Exception e)
1514+
{
1515+
TraceCommandException(e);
1516+
throw;
14721517
}
14731518
}
14741519
private async Task ExecuteCommandAsync(CommandBehavior behavior, bool returnsSet, CancellationToken cancellationToken = default)
14751520
{
14761521
LogMessages.CommandExecution(Log, this);
1522+
TraceCommandStart();
1523+
try
1524+
{
1525+
await PrepareAsync(returnsSet, cancellationToken).ConfigureAwait(false);
14771526

1478-
await PrepareAsync(returnsSet, cancellationToken).ConfigureAwait(false);
1527+
if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess ||
1528+
(behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult ||
1529+
(behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow ||
1530+
(behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection ||
1531+
behavior == CommandBehavior.Default)
1532+
{
1533+
// Set the fetch size
1534+
_statement.FetchSize = _fetchSize;
14791535

1480-
if ((behavior & CommandBehavior.SequentialAccess) == CommandBehavior.SequentialAccess ||
1481-
(behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult ||
1482-
(behavior & CommandBehavior.SingleRow) == CommandBehavior.SingleRow ||
1483-
(behavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection ||
1484-
behavior == CommandBehavior.Default)
1485-
{
1486-
// Set the fetch size
1487-
_statement.FetchSize = _fetchSize;
1536+
// Set if it's needed the Records Affected information
1537+
_statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected;
14881538

1489-
// Set if it's needed the Records Affected information
1490-
_statement.ReturnRecordsAffected = _connection.ConnectionOptions.ReturnRecordsAffected;
1539+
// Validate input parameter count
1540+
if (_namedParameters.Count > 0 && !HasParameters)
1541+
{
1542+
throw FbException.Create("Must declare command parameters.");
1543+
}
14911544

1492-
// Validate input parameter count
1493-
if (_namedParameters.Count > 0 && !HasParameters)
1494-
{
1495-
throw FbException.Create("Must declare command parameters.");
1545+
// Execute
1546+
await _statement.ExecuteAsync(CommandTimeout * 1000, this, cancellationToken).ConfigureAwait(false);
14961547
}
1497-
1498-
// Execute
1499-
await _statement.ExecuteAsync(CommandTimeout * 1000, this, cancellationToken).ConfigureAwait(false);
1548+
}
1549+
catch (Exception e)
1550+
{
1551+
TraceCommandException(e);
1552+
throw;
15001553
}
15011554
}
15021555

src/FirebirdSql.Data.FirebirdClient/FirebirdSql.Data.FirebirdClient.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<PrivateAssets>all</PrivateAssets>
5353
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
5454
</PackageReference>
55+
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
5556
</ItemGroup>
5657
<Import Project="..\FirebirdSql.Data.External\FirebirdSql.Data.External.projitems" Label="Shared" />
5758
</Project>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Data;
4+
using System.Diagnostics;
5+
using FirebirdSql.Data.FirebirdClient;
6+
7+
namespace FirebirdSql.Data.Trace
8+
{
9+
internal static class FbActivitySource
10+
{
11+
internal static readonly ActivitySource Source = new("FirebirdSql.Data", "1.0.0");
12+
13+
internal static Activity CommandStart(FbCommand command)
14+
{
15+
// Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md
16+
var dbName = command.Connection.Database;
17+
18+
string dbOperationName = null;
19+
string dbCollectionName = null;
20+
string activityName;
21+
22+
switch (command.CommandType)
23+
{
24+
case CommandType.StoredProcedure:
25+
dbOperationName = "EXECUTE PROCEDURE";
26+
activityName = $"{dbOperationName} {command.CommandText}";
27+
break;
28+
29+
case CommandType.TableDirect:
30+
dbOperationName = "SELECT";
31+
dbCollectionName = command.CommandText;
32+
activityName = $"{dbOperationName} {dbCollectionName}";
33+
break;
34+
35+
case CommandType.Text:
36+
activityName = dbName;
37+
break;
38+
39+
default:
40+
throw new InvalidEnumArgumentException($"Invalid value for 'System.Data.CommandType' ({(int)command.CommandType}).");
41+
}
42+
43+
var activity = Source.StartActivity(activityName, ActivityKind.Client);
44+
if (activity.IsAllDataRequested)
45+
{
46+
activity.SetTag("db.system", "firebird");
47+
48+
if (dbCollectionName != null)
49+
{
50+
activity.SetTag("db.collection.name", dbCollectionName);
51+
}
52+
53+
// db.namespace
54+
55+
if (dbOperationName != null)
56+
{
57+
activity.SetTag("db.operation.name", dbOperationName);
58+
}
59+
60+
// db.response.status_code
61+
62+
// error.type (handled by RecordException)
63+
64+
// server.port
65+
66+
// db.operation.batch.size
67+
68+
// db.query_summary
69+
70+
activity.SetTag("db.query.text", command.CommandText);
71+
72+
// network.peer.address
73+
74+
// network.peer.port
75+
76+
if (command.Connection.DataSource != null)
77+
{
78+
activity.SetTag("server.address", command.Connection.DataSource);
79+
}
80+
81+
foreach (FbParameter p in command.Parameters)
82+
{
83+
var name = p.ParameterName;
84+
var value = NormalizeDbNull(p.InternalValue);
85+
activity.SetTag($"db.query.parameter.{name}", value);
86+
87+
}
88+
89+
// Only for explicit transactions.
90+
if (command.Transaction != null)
91+
{
92+
FbTransactionInfo fbInfo = new FbTransactionInfo(command.Transaction);
93+
94+
var transactionId = fbInfo.GetTransactionId();
95+
activity.SetTag($"db.transaction_id", transactionId);
96+
97+
// TODO: Firebird 4+ only (or remove?)
98+
/*
99+
var snapshotId = fbInfo.GetTransactionSnapshotNumber();
100+
if (snapshotId != 0)
101+
{
102+
activity.SetTag($"db.snapshot_id", snapshotId);
103+
}
104+
*/
105+
}
106+
}
107+
108+
return activity;
109+
}
110+
111+
internal static void CommandException(Activity activity, Exception exception, bool escaped = true)
112+
{
113+
// Reference: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/exceptions/exceptions-spans.md
114+
activity.AddEvent(
115+
new("exception", tags: new()
116+
{
117+
{ "exception.message", exception.Message },
118+
{ "exception.type", exception.GetType().FullName },
119+
{ "exception.escaped", escaped },
120+
{ "exception.stacktrace", exception.ToString() },
121+
})
122+
);
123+
124+
string errorDescription = exception is FbException fbException
125+
? fbException.SQLSTATE
126+
: exception.Message;
127+
128+
activity.SetStatus(ActivityStatusCode.Error, errorDescription);
129+
activity.Dispose();
130+
}
131+
132+
private static object NormalizeDbNull(object value) =>
133+
value == DBNull.Value || value == null
134+
? null
135+
: value;
136+
}
137+
}

0 commit comments

Comments
 (0)