Skip to content

Commit

Permalink
WL#15707 [Add OpenTelemetry tracing]
Browse files Browse the repository at this point in the history
Change-Id: Id1fc09926e37c52c66df6c2d3a00d2fcb78c91ad
  • Loading branch information
Reggie Burnett committed Jun 12, 2023
1 parent 98633c6 commit 2d11201
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 52 deletions.
2 changes: 1 addition & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Remove support for .NET Core and EF Core 3.1 and 5.0 [WL15797]
- Updated non signed libraries (Oracle Bug #35052869)
- Added missing dll to installer (Oracle Bug #35360846)

- Added support for OpenTelemetry tracing (WL15707)

8.0.33
- Added support for OCI ephemeral key-based authentication (WL15489).
Expand Down
27 changes: 27 additions & 0 deletions MySQL.Data.OpenTelemetry/src/MySQL.Data.OpenTelemetry.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>MySql.Data.OpenTelemetry</Description>
<Copyright>Copyright (c) 2023, Oracle and/or its affiliates.</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
<Version>8.1.0</Version>
<Authors>Oracle</Authors>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>MySql.Data.OpenTelemetry</AssemblyName>
<PackageId>MySql.Data.OpenTelemetry</PackageId>
<PackageTags>MySql;.NET Connector;MySql Connector/NET;ado;ado.net;database;sql;opentelemetry;tracing;diagnostics;instrumentation</PackageTags>
<PackageIconUrl>http://www.mysql.com/common/logos/logo-mysql-170x115.png</PackageIconUrl>
<PackageProjectUrl>https://dev.mysql.com/downloads/</PackageProjectUrl>
<PackageLicenseExpression>GPL-2.0-only</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<SignAssembly>True</SignAssembly>
<DelaySign>True</DelaySign>
<AssemblyOriginatorKeyFile>..\..\ConnectorNetPublicKey.snk</AssemblyOriginatorKeyFile>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>MySql.Data.OpenTelemetry</Title>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.API" Version="1.4.0" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions MySQL.Data.OpenTelemetry/src/TraceProviderBuilderExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using OpenTelemetry.Trace;

/// <summary>
/// Extension method for setting up Connector/Net OpenTelemetry tracing.
/// </summary>
public static class TracerProviderBuilderExtensions
{
public static TracerProviderBuilder AddConnectorNet(
this TracerProviderBuilder builder)
=> builder.AddSource("connector-net");
}
107 changes: 107 additions & 0 deletions MySQL.Data/src/MySQLActivitySource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Data;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using MySql.Data.MySqlClient;
using Mysqlx.Notice;

namespace MySql.Data
{
#if NET5_0_OR_GREATER
static class MySQLActivitySource
{
static readonly ActivitySource Source;

static MySQLActivitySource()
{
var assembly = typeof(MySQLActivitySource).Assembly;
var version = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "0.0.0";
Source = new("connector-net", version);
}

private static bool Active => Source.HasListeners();

internal static Activity OpenConnection(MySqlConnectionStringBuilder settings)
{
return InternalOpenConnection(settings, "Connection Open");
}

internal static Activity OpenPooledConnection(MySqlConnectionStringBuilder settings)
{
return InternalOpenConnection(settings, "Connection Open (pooled)");
}

private static Activity InternalOpenConnection(MySqlConnectionStringBuilder settings, string name)
{
if (!Active) return null;

var activity = Source.StartActivity(name, ActivityKind.Client, Activity.Current.Context);
activity?.SetTag("net.transport", settings.ConnectionProtocol);
activity?.SetTag("db.connection_string", settings.GetConnectionString(false));
if (settings.ConnectionProtocol == MySqlConnectionProtocol.Tcp)
activity?.SetTag("net.peer.port", settings.Port);

return activity;
}

internal static void CloseConnection(Activity activity)
{
activity?.SetTag("otel.status_code", "OK");
activity?.Dispose();
}

internal static void SetException(Activity activity, Exception ex)
{
var tags = new ActivityTagsCollection
{
{ "exception.type", ex.GetType().FullName },
{ "exception.message", ex.Message },
{ "exception.stacktrace", ex.ToString() },
};

var activityEvent = new ActivityEvent("exception", tags: tags);
activity?.AddEvent(activityEvent);
activity?.SetTag("otel.status_code", "ERROR");
activity?.SetTag("otel.status_description", ex is MySqlException ? (ex as MySqlException).SqlState : ex.Message);
activity?.Dispose();
}


internal static Activity CommandStart(MySqlCommand command)
{
if (!Active) return null;

var settings = command.Connection.Settings;

var activity = Source.StartActivity("SQL Statement", ActivityKind.Client, Activity.Current.Context);

// passing through this attribute will propagate the context into the server
string query_attr = $"00-{Activity.Current.Context.TraceId}-{Activity.Current.Context.SpanId}-00";
command.Attributes.SetAttribute("traceparent", query_attr);

activity?.SetTag("db.system", "mysql");
activity?.SetTag("db.name", command.Connection.Database);
activity?.SetTag("db.user", command.Connection.Settings.UserID);
activity?.SetTag("thread.id", Thread.CurrentThread.ManagedThreadId);
activity?.SetTag("thread.name", Thread.CurrentThread.Name);
if (command.CommandType == CommandType.TableDirect)
activity?.SetTag("db.sql.table", command.CommandText);
return activity;
}

internal static void ReceivedFirstResponse(Activity activity)
{
var activityEvent = new ActivityEvent("first-packet-received");
activity.AddEvent(activityEvent);
}

internal static void CommandStop(Activity activity)
{
activity?.SetTag("otel.status_code", "OK");
activity?.Dispose();
}
}
#endif
}

5 changes: 3 additions & 2 deletions MySQL.Data/src/MySql.Data.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="ZstdSharp.Port" Version="0.7.1" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="ZstdSharp.Port" Version="0.7.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="7.0.2" />
</ItemGroup>

<ItemGroup>
Expand Down
107 changes: 64 additions & 43 deletions MySQL.Data/src/MySqlCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public sealed class MySqlCommand : DbCommand, IDisposable, ICloneable
private static List<string> keywords = null;
private bool disposed = false;
internal const string ParameterPrefix = "_cnet_param_";
#if NET5_0_OR_GREATER
Activity? CurrentActivity;
#endif

/// <summary>
/// Initializes a new instance of the MySqlCommand class.
Expand Down Expand Up @@ -553,7 +556,10 @@ internal void ClearCommandTimer()

internal async Task CloseAsync(MySqlDataReader reader, bool execAsync)
{
statement?.Close(reader);
#if NET5_0_OR_GREATER
MySQLActivitySource.CommandStop(CurrentActivity);
CurrentActivity = null;
#endif
await ResetSqlSelectLimitAsync(execAsync).ConfigureAwait(false);

if (statement != null && connection?.driver != null)
Expand Down Expand Up @@ -739,6 +745,11 @@ internal async Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior
// command behaviors
await HandleCommandBehaviorsAsync(execAsync, behavior).ConfigureAwait(false);


// Tell whoever is listening that we have started out command
#if NET5_0_OR_GREATER
CurrentActivity = MySQLActivitySource.CommandStart(this);
#endif
try
{
MySqlDataReader reader = new MySqlDataReader(this, statement, behavior);
Expand All @@ -751,47 +762,57 @@ internal async Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior
success = true;
return reader;
}
catch (TimeoutException tex)
{
await connection.HandleTimeoutOrThreadAbortAsync(tex, execAsync).ConfigureAwait(false);
throw; //unreached
}
catch (ThreadAbortException taex)
{
await connection.HandleTimeoutOrThreadAbortAsync(taex, execAsync).ConfigureAwait(false);
throw;
}
catch (IOException ioex)
{
await connection.AbortAsync(execAsync).ConfigureAwait(false); // Closes connection without returning it to the pool
throw new MySqlException(Resources.FatalErrorDuringExecute, ioex);
}
catch (MySqlException ex)
catch (Exception ex)
{
#if NET5_0_OR_GREATER
MySQLActivitySource.SetException(CurrentActivity, ex);
#endif
if (ex is TimeoutException)
{
await connection.HandleTimeoutOrThreadAbortAsync(ex, execAsync).ConfigureAwait(false);
throw; //unreached
}
else if (ex is ThreadAbortException)
{
await connection.HandleTimeoutOrThreadAbortAsync(ex, execAsync).ConfigureAwait(false);
throw;
}
else if (ex is IOException)
{
await connection.AbortAsync(execAsync).ConfigureAwait(false); // Closes connection without returning it to the pool
throw new MySqlException(Resources.FatalErrorDuringExecute, ex);
}
else if (ex is MySqlException)
{
MySqlException mySqlException = ex as MySqlException;
if (mySqlException.InnerException is TimeoutException)
throw; // already handled

if (ex.InnerException is TimeoutException)
throw; // already handled
try
{
await ResetReaderAsync(execAsync).ConfigureAwait(false);
await ResetSqlSelectLimitAsync(execAsync).ConfigureAwait(false);
}
catch (Exception)
{
// Reset SqlLimit did not work, connection is hosed.
await Connection.AbortAsync(execAsync).ConfigureAwait(false);
throw new MySqlException(ex.Message, true, ex);
}

try
{
await ResetReaderAsync(execAsync).ConfigureAwait(false);
await ResetSqlSelectLimitAsync(execAsync).ConfigureAwait(false);
// if we caught an exception because of a cancel, then just return null
if (mySqlException.IsQueryAborted)
return null;
if (mySqlException.IsFatal)
await Connection.CloseAsync(execAsync).ConfigureAwait(false);
if (mySqlException.Number == 0)
throw new MySqlException(Resources.FatalErrorDuringExecute, mySqlException);
throw;
}
catch (Exception)
else
{
// Reset SqlLimit did not work, connection is hosed.
await Connection.AbortAsync(execAsync).ConfigureAwait(false);
throw new MySqlException(ex.Message, true, ex);
throw;
}

// if we caught an exception because of a cancel, then just return null
if (ex.IsQueryAborted)
return null;
if (ex.IsFatal)
await Connection.CloseAsync(execAsync).ConfigureAwait(false);
if (ex.Number == 0)
throw new MySqlException(Resources.FatalErrorDuringExecute, ex);
throw;
}
finally
{
Expand Down Expand Up @@ -956,9 +977,9 @@ public object Clone()
return clone;
}

#endregion
#endregion

#region Async Methods
#region Async Methods
private IAsyncResult asyncResult;

internal delegate object AsyncDelegate(int type, CommandBehavior behavior);
Expand Down Expand Up @@ -1096,15 +1117,15 @@ public int EndExecuteNonQuery(IAsyncResult asyncResult)
throw thrownException;
return (int)c.EndInvoke(asyncResult);
}
#endregion
#endregion

#region Private Methods
#region Private Methods

internal long EstimatedSize() => CommandText.Length + Parameters.Cast<MySqlParameter>().Sum(parameter => parameter.EstimatedSize());

#endregion
#endregion

#region Batching support
#region Batching support

internal void AddToBatch(MySqlCommand command)
{
Expand Down Expand Up @@ -1171,7 +1192,7 @@ internal string GetCommandTextForBatching()
return BatchableCommandText;
}

#endregion
#endregion

// This method is used to throw all exceptions from this class.
private void Throw(Exception ex)
Expand Down
Loading

0 comments on commit 2d11201

Please sign in to comment.