Skip to content

Add options to configure tracing #1545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/MySqlConnector/Core/ResultSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)
ContainsCommandParameters = true;
WarningCount = 0;
State = ResultSetState.ReadResultSetHeader;
if (DataReader.Activity is { IsAllDataRequested: true })
if (DataReader.Activity is { IsAllDataRequested: true } && (Command?.Connection!.MySqlDataSource?.TracingOptions.EnableResultSetHeaderEvent ?? MySqlConnectorTracingOptions.Default.EnableResultSetHeaderEvent))
DataReader.Activity.AddEvent(new ActivityEvent("read-result-set-header"));
break;
}
Expand Down
2 changes: 2 additions & 0 deletions src/MySqlConnector/MySqlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,8 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,

internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint;

internal MySqlDataSource? MySqlDataSource => m_dataSource;

internal void SetState(ConnectionState newState)
{
if (m_connectionState != newState)
Expand Down
11 changes: 11 additions & 0 deletions src/MySqlConnector/MySqlConnectorTracingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace MySqlConnector;

internal sealed class MySqlConnectorTracingOptions
{
public bool EnableResultSetHeaderEvent { get; set; }

public static MySqlConnectorTracingOptions Default { get; } = new()
{
EnableResultSetHeaderEvent = true,
};
}
22 changes: 22 additions & 0 deletions src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace MySqlConnector;

public sealed class MySqlConnectorTracingOptionsBuilder
{
/// <summary>
/// Gets or sets a value indicating whether to enable the "time-to-first-read" event.
/// Default is true to preserve existing behavior.
/// </summary>
public MySqlConnectorTracingOptionsBuilder EnableResultSetHeaderEvent(bool enable = true)
{
m_enableResultSetHeaderEvent = enable;
return this;
}

internal MySqlConnectorTracingOptions Build() =>
new()
{
EnableResultSetHeaderEvent = m_enableResultSetHeaderEvent,
};

private bool m_enableResultSetHeaderEvent = MySqlConnectorTracingOptions.Default.EnableResultSetHeaderEvent;
}
6 changes: 5 additions & 1 deletion src/MySqlConnector/MySqlDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ public sealed class MySqlDataSource : DbDataSource
/// <param name="connectionString">The connection string for the MySQL Server. This parameter is required.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="connectionString"/> is <c>null</c>.</exception>
public MySqlDataSource(string connectionString)
: this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default, default)
: this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, null, default, default, default, default)
{
}

internal MySqlDataSource(string connectionString,
MySqlConnectorLoggingConfiguration loggingConfiguration,
MySqlConnectorTracingOptions? tracingOptions,
string? name,
Func<X509CertificateCollection, ValueTask>? clientCertificatesCallback,
RemoteCertificateValidationCallback? remoteCertificateValidationCallback,
Expand All @@ -36,6 +37,7 @@ internal MySqlDataSource(string connectionString,
{
m_connectionString = connectionString;
LoggingConfiguration = loggingConfiguration;
TracingOptions = tracingOptions ?? MySqlConnectorTracingOptions.Default;
Name = name;
m_clientCertificatesCallback = clientCertificatesCallback;
m_remoteCertificateValidationCallback = remoteCertificateValidationCallback;
Expand Down Expand Up @@ -202,6 +204,8 @@ private async Task RefreshPassword()

internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; }

internal MySqlConnectorTracingOptions TracingOptions { get; }

internal string? Name { get; }

private string ProvidePasswordFromField(MySqlProvidePasswordContext context) => m_password!;
Expand Down
25 changes: 25 additions & 0 deletions src/MySqlConnector/MySqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ public MySqlDataSourceBuilder(string? connectionString = null)
ConnectionStringBuilder = new(connectionString ?? "");
}

/// <summary>
/// Configures OpenTelemetry tracing options.
/// </summary>
/// <returns>This builder, so that method calls can be chained.</returns>
public MySqlDataSourceBuilder ConfigureTracing(Action<MySqlConnectorTracingOptionsBuilder> configureAction)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(configureAction);
#else
if (configureAction is null)
throw new ArgumentNullException(nameof(configureAction));
#endif
m_tracingOptionsBuilderCallbacks ??= [];
m_tracingOptionsBuilderCallbacks.Add(configureAction);
return this;
}

/// <summary>
/// Sets the <see cref="ILoggerFactory"/> that will be used for logging.
/// </summary>
Expand Down Expand Up @@ -107,8 +124,15 @@ public MySqlDataSourceBuilder UseConnectionOpenedCallback(MySqlConnectionOpenedC
public MySqlDataSource Build()
{
var loggingConfiguration = m_loggerFactory is null ? MySqlConnectorLoggingConfiguration.NullConfiguration : new(m_loggerFactory);

var tracingOptionsBuilder = new MySqlConnectorTracingOptionsBuilder();
foreach (var callback in m_tracingOptionsBuilderCallbacks ?? (IEnumerable<Action<MySqlConnectorTracingOptionsBuilder>>) [])
callback.Invoke(tracingOptionsBuilder);
var tracingOptions = tracingOptionsBuilder.Build();

return new(ConnectionStringBuilder.ConnectionString,
loggingConfiguration,
tracingOptions,
m_name,
m_clientCertificatesCallback,
m_remoteCertificateValidationCallback,
Expand All @@ -135,4 +159,5 @@ public MySqlDataSource Build()
private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval;
private TimeSpan m_periodicPasswordProviderFailureRefreshInterval;
private MySqlConnectionOpenedCallback? m_connectionOpenedCallback;
private List<Action<MySqlConnectorTracingOptionsBuilder>>? m_tracingOptionsBuilderCallbacks;
}
42 changes: 42 additions & 0 deletions tests/IntegrationTests/ActivityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,48 @@ public void SelectTags()
AssertTag(activity.Tags, "db.statement", "SELECT 1;");
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void ReadResultSetHeaderEvent(bool enableEvent)
{
var dataSourceBuilder = new MySqlDataSourceBuilder(AppConfig.ConnectionString)
.ConfigureTracing(o => o.EnableResultSetHeaderEvent(enableEvent));
using var dataSource = dataSourceBuilder.Build();
using var connection = dataSource.OpenConnection();

using var parentActivity = new Activity(nameof(ReadResultSetHeaderEvent));
parentActivity.Start();

Activity activity = null;
using var listener = new ActivityListener
{
ShouldListenTo = x => x.Name == "MySqlConnector",
Sample = (ref ActivityCreationOptions<ActivityContext> options) =>
options.TraceId == parentActivity.TraceId ? ActivitySamplingResult.AllData : ActivitySamplingResult.None,
ActivityStopped = x => activity = x,
};
ActivitySource.AddActivityListener(listener);

using (var command = new MySqlCommand("SELECT 1;", connection))
{
command.ExecuteScalar();
}

Assert.NotNull(activity);
Assert.Equal(ActivityKind.Client, activity.Kind);
Assert.Equal("Execute", activity.OperationName);
if (enableEvent)
{
var activityEvent = Assert.Single(activity.Events);
Assert.Equal("read-result-set-header", activityEvent.Name);
}
else
{
Assert.Empty(activity.Events);
}
}

private void AssertTags(IEnumerable<KeyValuePair<string, string>> tags, MySqlConnectionStringBuilder csb)
{
AssertTag(tags, "db.system", "mysql");
Expand Down