Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ internal static unsafe uint SniOpenSyncEx(
string connString,
ref IntPtr pConn,
ref string spn,
byte[] instanceName,
ref string instanceName,
bool fOverrideCache,
bool fSync,
int timeout,
Expand All @@ -181,9 +181,13 @@ internal static unsafe uint SniOpenSyncEx(
SQLDNSInfo cachedDnsInfo,
string hostNameInCertificate)
{
fixed (byte* pInstanceName = instanceName)
// Size of this buffer is as specified by netlibs.
ReadOnlySpan<byte> instanceNameBuffer = stackalloc byte[256];

fixed (byte* pInstanceName = instanceNameBuffer)
{
SniClientConsumerInfo clientConsumerInfo = new SniClientConsumerInfo();
uint result;

// initialize client ConsumerInfo part first
MarshalConsumerInfo(consumerInfo, ref clientConsumerInfo.ConsumerInfo);
Expand All @@ -192,7 +196,7 @@ internal static unsafe uint SniOpenSyncEx(
clientConsumerInfo.HostNameInCertificate = hostNameInCertificate;
clientConsumerInfo.networkLibrary = Prefix.UNKNOWN_PREFIX;
clientConsumerInfo.szInstanceName = pInstanceName;
clientConsumerInfo.cchInstanceName = (uint)instanceName.Length;
clientConsumerInfo.cchInstanceName = (uint)instanceNameBuffer.Length;
clientConsumerInfo.fOverrideLastConnectCache = fOverrideCache;
clientConsumerInfo.fSynchronousConnection = fSync;
clientConsumerInfo.timeout = timeout;
Expand Down Expand Up @@ -231,22 +235,21 @@ internal static unsafe uint SniOpenSyncEx(
{
// An empty string implies we need to find the SPN so we supply a buffer for the max size
var array = ArrayPool<byte>.Shared.Rent(SniMaxComposedSpnLength);
array.AsSpan().Clear();
Span<byte> arraySpan = array.AsSpan();

arraySpan.Clear();
try
{
fixed (byte* pin_spnBuffer = array)
fixed (byte* pin_spnBuffer = arraySpan)
{
clientConsumerInfo.szSPN = pin_spnBuffer;
clientConsumerInfo.cchSPN = (uint)SniMaxComposedSpnLength;

var result = s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
if (result == 0)
result = s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
if (result is TdsEnums.SNI_SUCCESS)
{
spn = Encoding.Unicode.GetString(array).TrimEnd('\0');
spn = Encoding.Unicode.CreateStringFromNullTerminated(arraySpan);
}

return result;
}
}
finally
Expand All @@ -270,7 +273,7 @@ internal static unsafe uint SniOpenSyncEx(
{
clientConsumerInfo.szSPN = pin_spnBuffer;
clientConsumerInfo.cchSPN = (uint)writer.WrittenCount;
return s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
result = s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
}
}
finally
Expand All @@ -279,9 +282,18 @@ internal static unsafe uint SniOpenSyncEx(
}
}
}
else
{
// Otherwise leave szSPN null (SQL Auth)
result = s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
}

if (result is TdsEnums.SNI_SUCCESS)
{
instanceName = Encoding.UTF8.CreateStringFromNullTerminated(instanceNameBuffer);
}

// Otherwise leave szSPN null (SQL Auth)
return s_nativeMethods.SniOpenSyncExWrapper(ref clientConsumerInfo, out pConn);
return result;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,6 @@ internal class SqlConnectionInternal : DbConnectionInternal, IDisposable
/// </summary>
private readonly DbConnectionPoolIdentity _identity;

private string _instanceName = string.Empty;

private SqlLoginAck _loginAck;

/// <summary>
Expand Down Expand Up @@ -559,11 +557,9 @@ internal bool IgnoreEnvChange
get => RoutingInfo != null;
}

// @TODO: Make auto-property
internal string InstanceName
{
get => _instanceName;
}
internal string UserInstanceName { get; private set; } = string.Empty;

internal string InstanceName { get; set; }

internal bool Is2008OrNewer
{
Expand Down Expand Up @@ -1211,7 +1207,7 @@ internal void OnEnvChange(SqlEnvChange rec)
break;

case TdsEnums.ENV_USERINSTANCE:
_instanceName = rec._newValue;
UserInstanceName = rec._newValue;
break;

case TdsEnums.ENV_ROUTING:
Expand Down Expand Up @@ -3301,7 +3297,7 @@ private void LoginNoFailover(
_currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage;
CurrentDatabase = _originalDatabase = ConnectionOptions.InitialCatalog;
ServerProvidedFailoverPartner = null;
_instanceName = string.Empty;
UserInstanceName = string.Empty;

routingAttempts++;

Expand Down Expand Up @@ -3615,7 +3611,7 @@ private void LoginWithFailover(
_currentLanguage = _originalLanguage = ConnectionOptions.CurrentLanguage;
CurrentDatabase = _originalDatabase = connectionOptions.InitialCatalog;
ServerProvidedFailoverPartner = null;
_instanceName = string.Empty;
UserInstanceName = string.Empty;

AttemptOneLogin(
currentServerInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,44 @@
using System;
using System.Runtime.CompilerServices;

#nullable enable

namespace Microsoft.Data.SqlClient.Diagnostics
{
/// <summary>
/// Provides a scope for emitting diagnostic events related to SQL command and connection operations. Used to track
/// the start, completion, and error states of database operations for diagnostic listeners.
/// </summary>
/// <remarks>
/// A DiagnosticScope is typically created using the <see cref="SqlDiagnosticListener.CreateCommandScope"/> method
/// and is intended to be used in a using statement to ensure proper disposal. Disposing the scope emits the
/// appropriate completion or error event based on whether an exception was set.
/// </remarks>
internal ref struct DiagnosticScope : IDisposable
{
private const int CommandOperation = 1;
private const int ConnectionOpenOperation = 2;

private readonly object _context1;
private readonly object _context2;
private readonly object? _context2;
private readonly SqlDiagnosticListener _diagnostics;
private readonly int _operation;
private readonly Guid _operationId;
private readonly string _operationName;

private Exception _exception;
private Exception? _exception;

private DiagnosticScope(
SqlDiagnosticListener diagnostics,
int operation,
Guid operationsId,
Guid operationId,
string operationName,
object context1,
object context2)
object? context2)
{
_diagnostics = diagnostics;
_operation = operation;
_operationId = operationsId;
_operationId = operationId;
_operationName = operationName;
_context1 = context1;
_context2 = context2;
Expand All @@ -41,27 +52,25 @@ private DiagnosticScope(
public static DiagnosticScope CreateCommandScope(
SqlDiagnosticListener diagnostics,
SqlCommand command,
SqlTransaction transaction,
SqlTransaction? transaction,
[CallerMemberName]
string operationName = "")
{
Guid operationId = diagnostics.WriteCommandBefore(command, transaction, operationName);
return new DiagnosticScope(diagnostics, CommandOperation, operationId, operationName, command, transaction);
}

// Although ref structs do not allow for inheriting from interfaces (< C#13), but the
// compiler will know to treat this like an IDisposable (> C# 8)
public void Dispose()
public readonly void Dispose()
{
switch (_operation)
{
case CommandOperation:
if (_exception != null)
if (_exception is not null)
{
_diagnostics.WriteCommandError(
_operationId,
(SqlCommand)_context1,
(SqlTransaction)_context2,
(SqlTransaction?)_context2,
_exception,
_operationName);
}
Expand All @@ -70,13 +79,13 @@ public void Dispose()
_diagnostics.WriteCommandAfter(
_operationId,
(SqlCommand)_context1,
(SqlTransaction)_context2,
(SqlTransaction?)_context2,
_operationName);
}
break;

case ConnectionOpenOperation:
if (_exception != null)
if (_exception is not null)
{
_diagnostics.WriteConnectionOpenError(
_operationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable

namespace Microsoft.Data.SqlClient.Diagnostics;

internal static class TelemetryAttributes
{
/// <summary>
/// Attributes prefixed with "db." and "sqlclient.db.".
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/registry/attributes/db/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/db/sql-server/"/>
Comment on lines +14 to +15
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Semantic Conventions are versioned - I would recommend linking to exactly which version you're implementing in all the relevant places. Otherwise it makes datamining the code to see what was implemented more difficult.

public static class Database
{
private const string StandardsPrefix = "db.";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest calling this this the "Semantic Conventions" prefix, rather than "Standards". That's the canonical name for them in OpenTelemetry.


private const string LibrarySpecificPrefix = "sqlclient.db.";

/// <summary>
/// This must always be <see cref="TelemetryAttributeValues.Database.SystemName" />.
/// </summary>
public const string SystemName = $"{StandardsPrefix}system.name";

/// <summary>
/// This is always in the format <c>{instance name}|{database name}</c>.
/// </summary>
/// <remarks>
/// <para>
/// The instance name is not included if the default instance is used.
/// </para>
/// <para>
/// This namespace is a <i>logical</i> construct, and does not necessarily reflect
/// the instance which the connection is currently connected to, or the value specified
/// in the connection string:
/// <list type="number">
/// <item>
/// An AlwaysOn Availability Group between two named SQL Server instances which are accessed
/// through a listener. The namespace will not contain an instance name; it will only be aware
/// of connecting to the default instance on the listener.
/// </item>
/// <item>
/// Database mirroring between two SQL Server instances. The failover partner details supplied
/// by the server will contain a port number, not an instance name. The namespace will contain
/// an instance name prior to failover, and will not contain an instance name after failover.
/// </item>
/// <item>
/// On Windows, configuring a client-side alias for a non-default SQL Server instance. The connection
/// string will not contain an instance name, but alias resolution will discover it. The namespace
/// will contain an instance name.
/// </item>
/// <item>
/// An AlwaysOn Availability Group between two named SQL Server instances, accessed directly and
/// using read-only routing. The namespace will contain an instance name when connected using a
/// connection intent of <c>ReadWrite</c>, and will not contain an instance name when connected
/// using a connection intent of <c>ReadOnly</c> (since read-only routing will re-route the connection
/// to a server based on port number, and will not provide an instance name.)
/// </item>
/// </list>
/// </para>
/// <para>
/// In these more complex scenarios, clients are recommended to use the <see cref="Server.Address"/>
/// and <see cref="Server.Port"/> attributes to uniquely identify the server being connected to, and
/// the <see cref="DatabaseName"/> attribute to identify the database being used.
/// </para>
/// </remarks>
public const string Namespace = $"{StandardsPrefix}namespace";

public const string DatabaseName = $"{LibrarySpecificPrefix}database.name";

public const string OperationName = $"{StandardsPrefix}operation.name";

public const string StoredProcedureName = $"{StandardsPrefix}stored_procedure.name";

public const string ResponseStatusCode = $"{StandardsPrefix}response.status_code";

public const string QueryText = $"{StandardsPrefix}query.text";

public const string OperationBatchSize = $"{StandardsPrefix}operation.batch.size";
}

/// <summary>
/// Attributes prefixed with "error.".
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/db/sql-server/"/>
public static class Error
{
private const string StandardsPrefix = "error.";

public const string Type = $"{StandardsPrefix}type";
}

/// <summary>
/// Attributes prefixed with "exception.".
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/registry/attributes/exception/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/db/sql-server/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/general/recording-errors/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/"/>
public static class Exception
{
private const string StandardsPrefix = "exception.";

public const string Type = $"{StandardsPrefix}type";

public const string Message = $"{StandardsPrefix}message";

public const string StackTrace = $"{StandardsPrefix}stacktrace";
}

/// <summary>
/// Attributes prefixed with "server.".
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/registry/attributes/server/"/>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/db/sql-server/"/>
public static class Server
{
private const string StandardsPrefix = "server.";

public const string Address = $"{StandardsPrefix}address";

public const string Port = $"{StandardsPrefix}port";
}
}

internal static class TelemetryAttributeValues
{
/// <summary>
/// Values for attributes described in <see cref="TelemetryAttributes.Database" />.
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/database/sql-server/"/>
public static class Database
{
public const string SystemName = "microsoft.sql_server";

public const string ExecuteOperationName = "EXECUTE";
}
}

internal static class TelemetryEventNames
{
/// <summary>
/// The name of an exception event.
/// </summary>
/// <seealso href="https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/"/>
public const string Exception = "exception";
}
Loading
Loading