From 0e3cc6dc566937023b9d9db87a512440211c5697 Mon Sep 17 00:00:00 2001 From: moonheart Date: Fri, 28 May 2021 09:59:08 +0800 Subject: [PATCH] Add instrumentation for Mysql.Data. (#115) --- .../package-Instrumentation.MySqlData.yml | 50 ++++ CODEOWNERS | 2 + opentelemetry-dotnet-contrib.sln | 15 ++ .../AssemblyInfo.cs | 23 ++ .../CHANGELOG.md | 5 + .../MySqlActivitySourceHelper.cs | 46 ++++ .../MySqlDataInstrumentation.cs | 225 ++++++++++++++++++ .../MySqlDataInstrumentationEventSource.cs | 41 ++++ .../MySqlDataInstrumentationOptions.cs | 53 +++++ .../MySqlDataTraceCommand.cs | 30 +++ ...y.Contrib.Instrumentation.MySqlData.csproj | 24 ++ .../README.md | 125 ++++++++++ .../TracerProviderBuilderExtensions.cs | 51 ++++ .../MySqlDataTests.cs | 224 +++++++++++++++++ ...rib.Instrumentation.MySqlData.Tests.csproj | 26 ++ 15 files changed, 940 insertions(+) create mode 100644 .github/workflows/package-Instrumentation.MySqlData.yml create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/AssemblyInfo.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/CHANGELOG.md create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlActivitySourceHelper.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentation.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationEventSource.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationOptions.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataTraceCommand.cs create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/OpenTelemetry.Contrib.Instrumentation.MySqlData.csproj create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/README.md create mode 100644 src/OpenTelemetry.Contrib.Instrumentation.MySqlData/TracerProviderBuilderExtensions.cs create mode 100644 test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/MySqlDataTests.cs create mode 100644 test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests.csproj diff --git a/.github/workflows/package-Instrumentation.MySqlData.yml b/.github/workflows/package-Instrumentation.MySqlData.yml new file mode 100644 index 0000000000..5e50855bff --- /dev/null +++ b/.github/workflows/package-Instrumentation.MySqlData.yml @@ -0,0 +1,50 @@ +name: Pack OpenTelemetry.Contrib.Instrumentation.MySqlData + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Instrumentation.MySqlData-*' # trigger when we create a tag with prefix "Instrumentation.MySqlData-" + +jobs: + build-test-pack: + runs-on: ${{ matrix.os }} + env: + PROJECT: OpenTelemetry.Contrib.Instrumentation.MySqlData + + strategy: + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetching all + + - name: Install dependencies + run: dotnet restore + + - name: dotnet build ${{env.PROJECT}} + run: dotnet build src/${{env.PROJECT}} --configuration Release --no-restore -p:Deterministic=true + + - name: dotnet test ${{env.PROJECT}} + run: dotnet test test/${{env.PROJECT}}.Tests + + - name: dotnet pack ${{env.PROJECT}} + run: dotnet pack src/${{env.PROJECT}} --configuration Release --no-build + + - name: Publish Artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{env.PROJECT}}-packages + path: '**/${{env.PROJECT}}/bin/**/*.*nupkg' + + - name: Publish Nuget + run: | + nuget setApiKey ${{ secrets.NUGET_TOKEN }} -Source https://api.nuget.org/v3/index.json + nuget push **/${{env.PROJECT}}/bin/**/*.nupkg -Source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index ae273f03c2..dcd32ecc61 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,6 +11,7 @@ src/OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore/ @ope src/OpenTelemetry.Contrib.Instrumentation.MassTransit/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy src/OpenTelemetry.Contrib.Instrumentation.GrpcCore/ @open-telemetry/dotnet-contrib-approvers @pcwiese src/OpenTelemetry.Contrib.Instrumentation.Wcf/ @open-telemetry/dotnet-contrib-approvers @codeblanch +src/OpenTelemetry.Contrib.Instrumentation.MySqlData/ @open-telemetry/dotnet-contrib-approvers @moonheart test/OpenTelemetry.Contrib.Exporter.Stackdriver.Tests/ @open-telemetry/dotnet-contrib-approvers @SergeyKanzhelev test/OpenTelemetry.Contrib.Extensions.AWSXRay.Tests/ @open-telemetry/dotnet-contrib-approvers @srprash @lupengamzn @@ -19,3 +20,4 @@ test/OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCoreTests/ @ope test/OpenTelemetry.Contrib.Instrumentation.MassTransit.Tests/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy test/OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests/ @open-telemetry/dotnet-contrib-approvers @pcwiese src/OpenTelemetry.Contrib.Instrumentation.Wcf.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch +src/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/ @open-telemetry/dotnet-contrib-approvers @moonheart diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 65b0b10487..86f4ba2de6 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Instrumentation.EntityFrameworkCore.yml = .github\workflows\package-Instrumentation.EntityFrameworkCore.yml .github\workflows\package-Instrumentation.GrpcCore.yml = .github\workflows\package-Instrumentation.GrpcCore.yml .github\workflows\package-Instrumentation.MassTransit.yml = .github\workflows\package-Instrumentation.MassTransit.yml + .github\workflows\package-Instrumentation.MySqlData.yml = .github\workflows\package-Instrumentation.MySqlData.yml .github\workflows\package-Instrumentation.Wcf.yml = .github\workflows\package-Instrumentation.Wcf.yml .github\workflows\pr_build.yml = .github\workflows\pr_build.yml .github\workflows\sanitycheck.yml = .github\workflows\sanitycheck.yml @@ -124,6 +125,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests", "test\OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests\OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests.csproj", "{22E101DD-D6D7-4554-9A98-8963230D6B2F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.MySqlData", "src\OpenTelemetry.Contrib.Instrumentation.MySqlData\OpenTelemetry.Contrib.Instrumentation.MySqlData.csproj", "{7D6175C4-1E8C-43BD-878D-6241E9627A69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests", "test\OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests\OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests.csproj", "{9F837AD0-C410-4001-B002-55090DA584EA}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.AWSLambda", "src\OpenTelemetry.Contrib.Instrumentation.AWSLambda\OpenTelemetry.Contrib.Instrumentation.AWSLambda.csproj", "{87FE0ED4-56A5-4775-9F63-DD532F2200BD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests", "test\OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests\OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests.csproj", "{08EDD935-8B4E-4CF5-8840-200DEBA8E110}" @@ -224,6 +229,14 @@ Global {22E101DD-D6D7-4554-9A98-8963230D6B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {22E101DD-D6D7-4554-9A98-8963230D6B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {22E101DD-D6D7-4554-9A98-8963230D6B2F}.Release|Any CPU.Build.0 = Release|Any CPU + {7D6175C4-1E8C-43BD-878D-6241E9627A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D6175C4-1E8C-43BD-878D-6241E9627A69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D6175C4-1E8C-43BD-878D-6241E9627A69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D6175C4-1E8C-43BD-878D-6241E9627A69}.Release|Any CPU.Build.0 = Release|Any CPU + {9F837AD0-C410-4001-B002-55090DA584EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F837AD0-C410-4001-B002-55090DA584EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F837AD0-C410-4001-B002-55090DA584EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F837AD0-C410-4001-B002-55090DA584EA}.Release|Any CPU.Build.0 = Release|Any CPU {87FE0ED4-56A5-4775-9F63-DD532F2200BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {87FE0ED4-56A5-4775-9F63-DD532F2200BD}.Debug|Any CPU.Build.0 = Debug|Any CPU {87FE0ED4-56A5-4775-9F63-DD532F2200BD}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -268,6 +281,8 @@ Global {F1591DEE-79C0-4161-85C2-1477B261D274} = {58D1DE55-B0A5-4BC4-AB37-09B1C7B26752} {ECBF7E54-3490-4B69-A376-17DC7BC75B0D} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {22E101DD-D6D7-4554-9A98-8963230D6B2F} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {7D6175C4-1E8C-43BD-878D-6241E9627A69} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {9F837AD0-C410-4001-B002-55090DA584EA} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {87FE0ED4-56A5-4775-9F63-DD532F2200BD} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {08EDD935-8B4E-4CF5-8840-200DEBA8E110} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {C33F2D9D-89A6-459C-9A51-79BA5A9EF194} = {2097345F-4DD3-477D-BC54-A922F9B2B402} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/AssemblyInfo.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/AssemblyInfo.cs new file mode 100644 index 0000000000..af3cf46e53 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/AssemblyInfo.cs @@ -0,0 +1,23 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests")] +#endif diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/CHANGELOG.md b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/CHANGELOG.md new file mode 100644 index 0000000000..134621e04d --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +* Initial release diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlActivitySourceHelper.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlActivitySourceHelper.cs new file mode 100644 index 0000000000..a11bdb9499 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlActivitySourceHelper.cs @@ -0,0 +1,46 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData +{ + /// + /// Helper class to hold common properties used by MySqlDataDiagnosticListener. + /// + internal class MySqlActivitySourceHelper + { + public const string MysqlDatabaseSystemName = "mysql"; + + public static readonly AssemblyName AssemblyName = typeof(MySqlActivitySourceHelper).Assembly.GetName(); + public static readonly string ActivitySourceName = AssemblyName.Name; + public static readonly string ActivityName = ActivitySourceName + ".Execute"; + + public static readonly IEnumerable> CreationTags = new[] + { + new KeyValuePair(SemanticConventions.AttributeDbSystem, MysqlDatabaseSystemName), + }; + + private static readonly Version Version = typeof(MySqlActivitySourceHelper).Assembly.GetName().Version; +#pragma warning disable SA1202 // Elements should be ordered by access <- In this case, Version MUST come before ActivitySource otherwise null ref exception is thrown. + internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); +#pragma warning restore SA1202 // Elements should be ordered by access + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentation.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentation.cs new file mode 100644 index 0000000000..b60b930abe --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentation.cs @@ -0,0 +1,225 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using MySql.Data.MySqlClient; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData +{ + /// + /// Mysql.Data instrumentation. + /// + internal class MySqlDataInstrumentation : DefaultTraceListener + { + private readonly ConcurrentDictionary dbConn = new ConcurrentDictionary(); + + private readonly MySqlDataInstrumentationOptions options; + + public MySqlDataInstrumentation(MySqlDataInstrumentationOptions options = null) + { + this.options = options ?? new MySqlDataInstrumentationOptions(); + MySqlTrace.Listeners.Clear(); + MySqlTrace.Listeners.Add(this); + MySqlTrace.Switch.Level = SourceLevels.Information; + MySqlTrace.QueryAnalysisEnabled = true; + } + + /// + public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args) + { + try + { + switch ((MySqlTraceEventType)id) + { + case MySqlTraceEventType.ConnectionOpened: + // args: [driverId, connStr, threadId] + var driverId = (long)args[0]; + var connStr = args[1].ToString(); + this.dbConn[driverId] = new MySqlConnectionStringBuilder(connStr); + break; + case MySqlTraceEventType.ConnectionClosed: + break; + case MySqlTraceEventType.QueryOpened: + // args: [driverId, threadId, cmdText] + this.BeforeExecuteCommand(this.GetCommand(args[0], args[2])); + break; + case MySqlTraceEventType.ResultOpened: + break; + case MySqlTraceEventType.ResultClosed: + break; + case MySqlTraceEventType.QueryClosed: + // args: [driverId] + this.AfterExecuteCommand(); + break; + case MySqlTraceEventType.StatementPrepared: + break; + case MySqlTraceEventType.StatementExecuted: + break; + case MySqlTraceEventType.StatementClosed: + break; + case MySqlTraceEventType.NonQuery: + break; + case MySqlTraceEventType.UsageAdvisorWarning: + break; + case MySqlTraceEventType.Warning: + break; + case MySqlTraceEventType.Error: + // args: [driverId, exNumber, exMessage] + this.ErrorExecuteCommand(this.GetMySqlErrorException(args[2])); + break; + case MySqlTraceEventType.QueryNormalized: + break; + default: + MySqlDataInstrumentationEventSource.Log.UnknownMySqlTraceEventType(id, string.Format(format, args)); + break; + } + } + catch (Exception e) + { + MySqlDataInstrumentationEventSource.Log.ErrorTraceEvent(id, string.Format(format, args), e.ToString()); + } + } + + private void BeforeExecuteCommand(MySqlDataTraceCommand command) + { + var activity = MySqlActivitySourceHelper.ActivitySource.StartActivity( + MySqlActivitySourceHelper.ActivityName, + ActivityKind.Client, + Activity.Current?.Context ?? default(ActivityContext), + MySqlActivitySourceHelper.CreationTags); + if (activity == null) + { + return; + } + + if (activity.IsAllDataRequested) + { + if (this.options.SetDbStatement) + { + activity.SetTag(SemanticConventions.AttributeDbStatement, command.SqlText); + } + + if (command.ConnectionStringBuilder != null) + { + activity.DisplayName = command.ConnectionStringBuilder.Database; + activity.SetTag(SemanticConventions.AttributeDbName, command.ConnectionStringBuilder.Database); + + this.AddConnectionLevelDetailsToActivity(command.ConnectionStringBuilder, activity); + } + } + } + + private void AfterExecuteCommand() + { + var activity = Activity.Current; + if (activity == null) + { + return; + } + + if (activity.Source != MySqlActivitySourceHelper.ActivitySource) + { + return; + } + + try + { + if (activity.IsAllDataRequested) + { + activity.SetStatus(Status.Unset); + } + } + finally + { + activity.Stop(); + } + } + + private void ErrorExecuteCommand(Exception exception) + { + var activity = Activity.Current; + if (activity == null) + { + return; + } + + if (activity.Source != MySqlActivitySourceHelper.ActivitySource) + { + return; + } + + try + { + if (activity.IsAllDataRequested) + { + activity.SetStatus(Status.Error.WithDescription(exception.Message)); + if (this.options.RecordException) + { + activity.RecordException(exception); + } + } + } + finally + { + activity.Stop(); + } + } + + private MySqlDataTraceCommand GetCommand(object driverIdObj, object cmd) + { + var command = new MySqlDataTraceCommand(); + if (this.dbConn.TryGetValue((long)driverIdObj, out var database)) + { + command.ConnectionStringBuilder = database; + } + + command.SqlText = cmd == null ? string.Empty : cmd.ToString(); + return command; + } + + private Exception GetMySqlErrorException(object errorMsg) + { + return new Exception($"{errorMsg}"); + } + + private void AddConnectionLevelDetailsToActivity(MySqlConnectionStringBuilder dataSource, Activity sqlActivity) + { + if (!this.options.EnableConnectionLevelAttributes) + { + sqlActivity.SetTag(SemanticConventions.AttributePeerService, dataSource.Server); + } + else + { + var uriHostNameType = Uri.CheckHostName(dataSource.Server); + + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + sqlActivity.SetTag(SemanticConventions.AttributeNetPeerIp, dataSource.Server); + } + else + { + sqlActivity.SetTag(SemanticConventions.AttributeNetPeerName, dataSource.Server); + } + + sqlActivity.SetTag(SemanticConventions.AttributeNetPeerPort, dataSource.Port); + sqlActivity.SetTag(SemanticConventions.AttributeDbUser, dataSource.UserID); + } + } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationEventSource.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationEventSource.cs new file mode 100644 index 0000000000..9332a60499 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationEventSource.cs @@ -0,0 +1,41 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics.Tracing; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData +{ + /// + /// EventSource events emitted from the project. + /// + [EventSource(Name = "OpenTelemetry-Instrumentation-MySqlData")] + internal class MySqlDataInstrumentationEventSource : EventSource + { + public static readonly MySqlDataInstrumentationEventSource Log = new MySqlDataInstrumentationEventSource(); + + [Event(1, Message = "Unknown MySqlTraceEventType: {0}, Message {1}", Level = EventLevel.Error)] + public void UnknownMySqlTraceEventType(int mysqlEventId, string message) + { + this.WriteEvent(1, mysqlEventId, message); + } + + [Event(2, Message = "Error accured while processing trace event, MySqlTraceEventType: {0}, Message {1}, Exception: {2}", Level = EventLevel.Error)] + public void ErrorTraceEvent(int mysqlEventId, string message, string exception) + { + this.WriteEvent(1, mysqlEventId, message, exception); + } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationOptions.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationOptions.cs new file mode 100644 index 0000000000..3b5ef41653 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataInstrumentationOptions.cs @@ -0,0 +1,53 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Diagnostics; +using System.Text.RegularExpressions; + +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData +{ + /// + /// Options for . + /// + public class MySqlDataInstrumentationOptions + { + /// + /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. Default value: False. + /// + /// + /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md. + /// + public bool RecordException { get; set; } + + /// + /// Gets or sets a value indicating whether or not the should add the text as the tag. Default value: False. + /// + public bool SetDbStatement { get; set; } + + /// + /// Gets or sets a value indicating whether or not the should parse the DataSource on a SqlConnection into server name, instance name, and/or port connection-level attribute tags. Default value: False. + /// + /// + /// The default behavior is to set the MySqlConnection DataSource as the tag. If enabled, MySqlConnection DataSource will be parsed and the server name will be sent as the or tag, the instance name will be sent as the tag, and the port will be sent as the tag if it is not 1433 (the default port). + /// + public bool EnableConnectionLevelAttributes { get; set; } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataTraceCommand.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataTraceCommand.cs new file mode 100644 index 0000000000..fbce7faa39 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/MySqlDataTraceCommand.cs @@ -0,0 +1,30 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using MySql.Data.MySqlClient; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData +{ + /// + /// Informations of current executing command. + /// + internal class MySqlDataTraceCommand + { + public MySqlConnectionStringBuilder ConnectionStringBuilder { get; set; } + + public string SqlText { get; set; } + } +} diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/OpenTelemetry.Contrib.Instrumentation.MySqlData.csproj b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/OpenTelemetry.Contrib.Instrumentation.MySqlData.csproj new file mode 100644 index 0000000000..0d62a592a5 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/OpenTelemetry.Contrib.Instrumentation.MySqlData.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + OpenTelemetry instrumentation for Mysql.Data + $(PackageTags);distributed-tracing;Mysql.Data + Instrumentation.MysqlData- + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/README.md b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/README.md new file mode 100644 index 0000000000..0c6c2cdcd4 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/README.md @@ -0,0 +1,125 @@ +# MySqlData Instrumentation for OpenTelemetry + +This is an +[Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), +which instruments [MySql.Data](https://www.nuget.org/packages/MySql.Data) +and collects telemetry about database operations. + +## Steps to enable OpenTelemetry.Contrib.Instrumentation.MySqlData + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Contrib.Instrumentation.MySqlData`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.MySqlData) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package OpenTelemetry.Contrib.Instrumentation.MySqlData +``` + +### Step 2: Enable MySqlData Instrumentation at application startup + +MySqlData instrumentation must be enabled at application startup. + +The following example demonstrates adding MySqlData instrumentation to a +console application. This example also sets up the OpenTelemetry Console +exporter, which requires adding the package +[`OpenTelemetry.Exporter.Console`](https://www.nuget.org/packages/OpenTelemetry.Exporter.Console) +to the application. + +```csharp +using OpenTelemetry.Trace; + +public class Program +{ + public static void Main(string[] args) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddMySqlDataInstrumentation() + .AddConsoleExporter() + .Build(); + } +} +``` + +For an ASP.NET Core application, adding instrumentation is typically done in +the `ConfigureServices` of your `Startup` class. Refer to documentation for +[OpenTelemetry.Instrumentation.AspNetCore](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Instrumentation.AspNetCore/README.md). + +For an ASP.NET application, adding instrumentation is typically done in the +`Global.asax.cs`. Refer to documentation for [OpenTelemetry.Instrumentation.AspNet](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Instrumentation.AspNet/README.md). + +## Advanced configuration + +This instrumentation can be configured to change the default behavior by using +`MySqlDataInstrumentationOptions`. + +### Capturing 'db.statement' + +The `MySqlDataInstrumentationOptions` class exposes several properties that can be +used to configure how the [`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +attribute is captured upon execution of a query. + +#### SetDbStatement + +The `SetDbStatement` property can be used to control whether this instrumentation +should set the [`db.statement`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md#call-level-attributes) +attribute to the text of the `MySqlCommand` being executed. + +Since `CommandType.Text` might contain sensitive data, SQL capturing is +_disabled_ by default to protect against accidentally sending full query text +to a telemetry backend. If you are only using stored procedures or have no +sensitive data in your `sqlCommand.CommandText`, you can enable SQL capturing +using the options like below: + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddMySqlDataInstrumentation( + options => options.SetDbStatement = true) + .AddConsoleExporter() + .Build(); +``` + +## EnableConnectionLevelAttributes + +By default, `EnabledConnectionLevelAttributes` is disabled and this +instrumentation sets the `peer.service` attribute to the +[`DataSource`](https://docs.microsoft.com/dotnet/api/system.data.common.dbconnection.datasource) +property of the connection. If `EnabledConnectionLevelAttributes` is enabled, +the `DataSource` will be parsed and the server name will be sent as the +`net.peer.name` or `net.peer.ip` attribute, and the port will be sent as the +`net.peer.port` attribute. + +The following example shows how to use `EnableConnectionLevelAttributes`. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddMySqlDataInstrumentation( + options => options.EnableConnectionLevelAttributes = true) + .AddConsoleExporter() + .Build(); +``` + +### RecordException + +This option can be set to instruct the instrumentation to record Exceptions +as Activity [events](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md). + +> Due to the limitation of this library's implementation, We cannot get the raw `MysqlException`, +> only exception message is available. + +The default value is `false` and can be changed by the code like below. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddMySqlDataInstrumentation( + options => options.RecordException = true) + .AddConsoleExporter() + .Build(); +``` + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) + +* [OpenTelemetry semantic conventions for database calls](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md) diff --git a/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/TracerProviderBuilderExtensions.cs new file mode 100644 index 0000000000..e158e26117 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Instrumentation.MySqlData/TracerProviderBuilderExtensions.cs @@ -0,0 +1,51 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Contrib.Instrumentation.MySqlData; + +namespace OpenTelemetry.Trace +{ + /// + /// Extension methods to simplify registering of dependency instrumentation. + /// + public static class TracerProviderBuilderExtensions + { + /// + /// Enables SqlClient instrumentation. + /// + /// being configured. + /// SqlClient configuration options. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddMySqlDataInstrumentation( + this TracerProviderBuilder builder, + Action configureMySqlDataInstrumentationOptions = null) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + var sqlOptions = new MySqlDataInstrumentationOptions(); + configureMySqlDataInstrumentationOptions?.Invoke(sqlOptions); + + builder.AddInstrumentation(() => new MySqlDataInstrumentation(sqlOptions)); + builder.AddSource(MySqlActivitySourceHelper.ActivitySourceName); + + return builder; + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/MySqlDataTests.cs b/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/MySqlDataTests.cs new file mode 100644 index 0000000000..283659195f --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/MySqlDataTests.cs @@ -0,0 +1,224 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Moq; +using MySql.Data.MySqlClient; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests +{ + public class MySqlDataTests + { + private const string ConnStr = "database=mysql;server=127.0.0.1;user id=root;password=123456;port=3306;pooling=False"; + + [Theory] + [InlineData("select 1/1", true, true, true, false)] + [InlineData("select 1/1", true, true, false, false)] + [InlineData("selext 1/1", true, true, true, true)] + public void SuccessTraceEventTest( + string commandText, + bool setDbStatement = false, + bool recordException = false, + bool enableConnectionLevelAttributes = false, + bool isFailure = false) + { + var activityProcessor = new Mock>(); + var sampler = new TestSampler(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .SetSampler(sampler) + .AddMySqlDataInstrumentation(options => + { + options.SetDbStatement = setDbStatement; + options.RecordException = recordException; + options.EnableConnectionLevelAttributes = enableConnectionLevelAttributes; + }) + .Build(); + + var traceListener = (TraceListener)Assert.Single(MySqlTrace.Listeners); + + this.ExecuteSuccessQuery(traceListener, commandText, isFailure); + + Assert.Equal(3, activityProcessor.Invocations.Count); + + var activity = (Activity)activityProcessor.Invocations[1].Arguments[0]; + + VerifyActivityData(commandText, setDbStatement, recordException, enableConnectionLevelAttributes, isFailure, activity); + } + + [Fact] + public void MySqlDataInstrumentationEventSource_test() + { + MySqlDataInstrumentationEventSource.Log.UnknownMySqlTraceEventType(15, "UnknownMySqlTraceEventType"); + MySqlDataInstrumentationEventSource.Log.ErrorTraceEvent(1, "ErrorTraceEvent", "ErrorTraceEvent exception"); + } + + [Theory] + [InlineData(MySqlTraceEventType.ConnectionClosed)] + [InlineData(MySqlTraceEventType.ResultOpened)] + [InlineData(MySqlTraceEventType.ResultClosed)] + [InlineData(MySqlTraceEventType.StatementPrepared)] + [InlineData(MySqlTraceEventType.StatementExecuted)] + [InlineData(MySqlTraceEventType.StatementClosed)] + [InlineData(MySqlTraceEventType.NonQuery)] + [InlineData(MySqlTraceEventType.UsageAdvisorWarning)] + [InlineData(MySqlTraceEventType.Warning)] + [InlineData(MySqlTraceEventType.QueryNormalized)] + [InlineData((MySqlTraceEventType)0)] + public void UnknownMySqlTraceEventType(MySqlTraceEventType eventType) + { + var activityProcessor = new Mock>(); + var sampler = new TestSampler(); + using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .SetSampler(sampler) + .AddMySqlDataInstrumentation() + .Build(); + + var traceListener = (TraceListener)Assert.Single(MySqlTrace.Listeners); + + traceListener?.TraceEvent( + new TraceEventCache(), + "mysql", + TraceEventType.Information, + (int)eventType, + "{0}: Connection Opened: connection string = '{1}'", + 1L, + ConnStr, + 10); + + Assert.Equal(1, activityProcessor.Invocations.Count); + } + + private static void VerifyActivityData( + string commandText, + bool setDbStatement, + bool recordException, + bool enableConnectionLevelAttributes, + bool isFailure, + Activity activity) + { + if (!isFailure) + { + Assert.Equal(Status.Unset, activity.GetStatus()); + } + else + { + var status = activity.GetStatus(); + Assert.Equal(Status.Error.StatusCode, status.StatusCode); + Assert.NotNull(status.Description); + + if (recordException) + { + var events = activity.Events.ToList(); + Assert.Single(events); + + Assert.Equal(SemanticConventions.AttributeExceptionEventName, events[0].Name); + } + else + { + Assert.Empty(activity.Events); + } + } + + Assert.Equal("mysql", activity.GetTagValue(SemanticConventions.AttributeDbName)); + + if (setDbStatement) + { + Assert.Equal(commandText, activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + else + { + Assert.Null(activity.GetTagValue(SemanticConventions.AttributeDbStatement)); + } + + var dataSource = new MySqlConnectionStringBuilder(ConnStr).Server; + + if (!enableConnectionLevelAttributes) + { + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributePeerService)); + } + else + { + var uriHostNameType = Uri.CheckHostName(dataSource); + if (uriHostNameType == UriHostNameType.IPv4 || uriHostNameType == UriHostNameType.IPv6) + { + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributeNetPeerIp)); + } + else + { + Assert.Equal(dataSource, activity.GetTagValue(SemanticConventions.AttributeNetPeerName)); + } + } + } + + private void ExecuteSuccessQuery(TraceListener listener, string query, bool isFailure) + { + // Connection opened + listener.TraceEvent( + new TraceEventCache(), + "mysql", + TraceEventType.Information, + 1, + "{0}: Connection Opened: connection string = '{1}'", + 1L, + ConnStr, + 10); + + // Query opened + listener.TraceEvent( + new TraceEventCache(), + "mysql", + TraceEventType.Information, + 3, + "{0}: Query Opened: {2}", + 1L, + 9, + query); + + if (isFailure) + { + // Query error + listener.TraceEvent( + new TraceEventCache(), + "mysql", + TraceEventType.Information, + 13, + "{0}: Error encountered attempting to open result: Number={1}, Message={2}", + 1L, + 1064, + "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'selext 1/1' at line 1"); + } + else + { + // Query closed + listener.TraceEvent( + new TraceEventCache(), + "mysql", + TraceEventType.Information, + 6, + "{0}: Query Closed", + 1L); + } + } + } +} diff --git a/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests.csproj b/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests.csproj new file mode 100644 index 0000000000..5038c3eb1e --- /dev/null +++ b/test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp3.1 + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + +