Description
Proposal
ADO.NET libraries should be instrumented with the ActivitySource
API.
This document is based on the Instrumenting a library/application with .NET Activity API recommendations from OpenTelemetry.
Basics
Install v5.0.1 or later of the System.Diagnostics.DiagnosticSource NuGet package.
Create a single ActivitySource
named after the assembly name of the library being instrumented:
ActivitySource activitySource = new(typeof(ExampleDbConnection).Assembly.GetName();
Activities
The ActivityKind
MUST be set to ActivityKind.Client
.
The standard activity names are:
Open
- establishing a connection to a database server- The actual implementation may simply be retrieving a connection from a pool; this is still recorded as an
Activity
.
- The actual implementation may simply be retrieving a connection from a pool; this is still recorded as an
Execute
- executing a command- OpenTelemetry recommends that this be named
<db.operation> <db.name>.<db.sql.table>
,<db.operation> <db.name>
, or<db.name>
. Since those values will already be set as custom tags on theActivity
, anActivityListener
that is reportingActivity
objects to an OpenTelemetry endpoint should override the span name (with the values of those tags) if desired. - Is it not necessary to record
ExecuteScalar
,ExecuteNonQuery
, andExecuteReader
as different activities. - The
Execute
activity ends when theDbDataReader
returned byExecuteReader
is closed/disposed.AddEvent
will be used to record when the first response is returned by the server. For MySqlConnector, the event name isread-result-set-header
, and can occur multiple times during an activity if multiple result sets are read. For Npgsql, the event name isreceived-first-response
.
- OpenTelemetry recommends that this be named
Commit
- committing a transactionRollback
- rolling back a transaction- NOTE: These are currently represented as their own activities, but could alternatively be events added to a "Transaction" Activity that encompasses the entire transaction. This may change in the future based on developments in the OpenTelemetry database conventions.
Future activity names may include the following. These should not be used until formalized by this specification.
Close
- closing a database connectionPrepare
- preparing a statement
Tags
These tags are taken from the OpenTelemetry Semantic Conventions; that document is the authoritative source and should take precedence in case of a conflict with the recommendations here.
Tag | Type | Description | Examples | Required |
---|---|---|---|---|
db.system |
string | The DBMS. See OTel spec for allowable values. | mysql , postgresql , other_sql |
yes |
db.connection_string |
string | The connection string. Sensitive information should be removed. (In many ADO.NET providers, this is the default, but users can override it with Persist Security Info = true .) |
Server=dbserver; Database=mydb; User ID=myuser |
no |
db.connection_id |
string | The "connection ID" or "server thread" etc. that uniquely identifies this connection on the DB server. (This is an extension to the standard OpenTelemetry attributes.) | 173 |
no |
db.user |
string | User name for accessing the database. | myuser |
no |
net.peer.name |
string | Remote hostname or similar. Usually the same as DbConnection.DataSource . |
dbserver |
[1] |
net.peer.ip |
string | Remote IP address of the peer. | 127.0.0.1 |
[1] |
net.peer.port |
int | Remote port number. | 1433 , 3306 |
[2] |
net.transport |
string | Transport protocol used. | ip_tcp , inproc |
yes |
db.name |
string | The name of the database being accessed. For commands that switch the database, this should be set to the target database. Usually the same as DbConnection.Database . |
customers ; main |
yes |
db.statement |
string | The database statement being executed, i.e., DbCommand.CommandText . [3] |
SELECT * FROM example_table; |
yes [4] |
db.operation |
string | The name of the operation being executed, e.g. the SQL keyword. [5] | SELECT , INSERT |
no |
db.sql.table |
string | The name of the primary table that the operation is acting upon, including the schema name (if applicable). [6] | public.users ; customers |
recommended |
thread.id |
int | Current managed thread ID, i.e., Thread.CurrentThread.ManagedThreadId . |
42 |
no |
thread.name |
string | Current thread name, i.e., Thread.CurrentThread.Name . |
Worker Thread |
no |
exception.type |
string | The type of the exception (its fully-qualified class name, if applicable). | ExampleLib.ExampleDbException |
[7] |
exception.message |
string | The exception message. |
Could not connect to remote server |
[7] |
exception.stacktrace |
string | A managed stack trace | no |
- One of
net.peer.name
ornet.peer.ip
should be specified. - Required if using a non-default port.
- The value may be sanitized to exclude sensitive information. In particular, parameter placeholders SHOULD NOT be replaced with parameter values.
- Users may want to opt in to sending this data (due to confidentiality and size).
- When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of
db.statement
just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted. - It is not recommended to attempt any client-side parsing of
db.statement
just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set. - If an error occurs, at least one of
exception.type
andexception.message
is required.
Exceptions
If an exception occurs, the Status
should be set to ActivityStatusCode.Error
and the exception details should be added as an event named exception
, as per https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#record-exception.
Example
public class ExampleDbConnection : DbConnection
{
static ActivitySource _activitySource = new(typeof(ExampleDbConnection).Assembly.GetName().Name,
typeof(ExampleDbConnection).Assembly.GetName().Version.ToString());
public override void Open()
{
using var activity = _activitySource.StartActivity("Open", ActivityKind.Client);
try
{
if (activity is { IsAllDataRequested: true })
{
activity.SetTag("db.system", "other_sql");
activity.SetTag("db.connection_string", this.ConnectionString);
activity.SetTag("db.user", userId);
activity.SetTag("db.name", this.Database);
activity.SetTag("net.peer.name", this.DataSource);
activity.SetTag("net.transport", "ip_tcp");
activity.SetTag("thread.id", Thread.CurrentThread.ManagedThreadId);
}
// ... actual Open implementation elided ...
}
catch (Exception ex) when (activity is { IsAllDataRequested: true })
{
activity.SetStatus(ActivityStatusCode.Error, ex.Message);
activity.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection
{
{ "exception.type", exception.GetType().FullName },
{ "exception.message", exception.Message },
{ "exception.stacktrace", exception.ToString() },
}));
throw;
}
}
}