diff --git a/src/MySqlConnector/Core/ResultSet.cs b/src/MySqlConnector/Core/ResultSet.cs index 2b93879ca..dd535c66b 100644 --- a/src/MySqlConnector/Core/ResultSet.cs +++ b/src/MySqlConnector/Core/ResultSet.cs @@ -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; } diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index 27219bb47..62f70bdac 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -1138,6 +1138,8 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint; + internal MySqlDataSource? MySqlDataSource => m_dataSource; + internal void SetState(ConnectionState newState) { if (m_connectionState != newState) diff --git a/src/MySqlConnector/MySqlConnectorTracingOptions.cs b/src/MySqlConnector/MySqlConnectorTracingOptions.cs new file mode 100644 index 000000000..8bc0d71f5 --- /dev/null +++ b/src/MySqlConnector/MySqlConnectorTracingOptions.cs @@ -0,0 +1,11 @@ +namespace MySqlConnector; + +internal sealed class MySqlConnectorTracingOptions +{ + public bool EnableResultSetHeaderEvent { get; set; } + + public static MySqlConnectorTracingOptions Default { get; } = new() + { + EnableResultSetHeaderEvent = true, + }; +} diff --git a/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs b/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs new file mode 100644 index 000000000..8b2ce81a4 --- /dev/null +++ b/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs @@ -0,0 +1,22 @@ +namespace MySqlConnector; + +public sealed class MySqlConnectorTracingOptionsBuilder +{ + /// + /// Gets or sets a value indicating whether to enable the "time-to-first-read" event. + /// Default is true to preserve existing behavior. + /// + 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; +} diff --git a/src/MySqlConnector/MySqlDataSource.cs b/src/MySqlConnector/MySqlDataSource.cs index 56e9718af..5b5b38436 100644 --- a/src/MySqlConnector/MySqlDataSource.cs +++ b/src/MySqlConnector/MySqlDataSource.cs @@ -19,12 +19,13 @@ public sealed class MySqlDataSource : DbDataSource /// The connection string for the MySQL Server. This parameter is required. /// Thrown if is null. 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? clientCertificatesCallback, RemoteCertificateValidationCallback? remoteCertificateValidationCallback, @@ -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; @@ -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!; diff --git a/src/MySqlConnector/MySqlDataSourceBuilder.cs b/src/MySqlConnector/MySqlDataSourceBuilder.cs index 4bceec180..5ea16dce0 100644 --- a/src/MySqlConnector/MySqlDataSourceBuilder.cs +++ b/src/MySqlConnector/MySqlDataSourceBuilder.cs @@ -21,6 +21,23 @@ public MySqlDataSourceBuilder(string? connectionString = null) ConnectionStringBuilder = new(connectionString ?? ""); } + /// + /// Configures OpenTelemetry tracing options. + /// + /// This builder, so that method calls can be chained. + public MySqlDataSourceBuilder ConfigureTracing(Action 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; + } + /// /// Sets the that will be used for logging. /// @@ -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>) []) + callback.Invoke(tracingOptionsBuilder); + var tracingOptions = tracingOptionsBuilder.Build(); + return new(ConnectionStringBuilder.ConnectionString, loggingConfiguration, + tracingOptions, m_name, m_clientCertificatesCallback, m_remoteCertificateValidationCallback, @@ -135,4 +159,5 @@ public MySqlDataSource Build() private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval; private TimeSpan m_periodicPasswordProviderFailureRefreshInterval; private MySqlConnectionOpenedCallback? m_connectionOpenedCallback; + private List>? m_tracingOptionsBuilderCallbacks; } diff --git a/tests/IntegrationTests/ActivityTests.cs b/tests/IntegrationTests/ActivityTests.cs index 60ada3b6f..e486494bf 100644 --- a/tests/IntegrationTests/ActivityTests.cs +++ b/tests/IntegrationTests/ActivityTests.cs @@ -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 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> tags, MySqlConnectionStringBuilder csb) { AssertTag(tags, "db.system", "mysql");