diff --git a/.cspell/other.txt b/.cspell/other.txt index 1b3f707f99..0cebfd5e34 100644 --- a/.cspell/other.txt +++ b/.cspell/other.txt @@ -50,6 +50,7 @@ PROCESSRUNTIME proto protobuf protos +RABBITMQ Serilog spdlog SQLCLIENT diff --git a/CHANGELOG.md b/CHANGELOG.md index 333bf69519..20e64f6373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,13 @@ This component adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.h ### Added - Support for Operating System resource detector. +- Support for [RabbitMQ.Client](https://www.nuget.org/packages/RabbitMQ.Client/) + traces instrumentation for versions `6.0.0`-`6.*.*` - Added support for OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_LOGS_EXPORTER to handle comma-separated list. - The environment variables `OTEL_TRACES_EXPORTER`, `OTEL_METRICS_EXPORTER`, and `OTEL_LOGS_EXPORTER` now support configuring console exporters for traces, metrics, and logs, respectively. -- Environment variables `OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED`, - `OTEL_DOTNET_AUTO_METRICS_CONSOLE_EXPORTER_ENABLED`, and - `OTEL_DOTNET_AUTO_LOGS_CONSOLE_EXPORTER_ENABLED` are now marked as deprecated. - Support signal specific OTLP exporter variables (See [docs](/docs/config.md#otlp)): - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, - `OTEL_EXPORTER_OTLP_TRACES_HEADERS`, @@ -54,6 +53,10 @@ This component adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.h ### Deprecated +- Environment variables `OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED`, + `OTEL_DOTNET_AUTO_METRICS_CONSOLE_EXPORTER_ENABLED`, and + `OTEL_DOTNET_AUTO_LOGS_CONSOLE_EXPORTER_ENABLED` are now marked as deprecated. + ### Removed - Support for macOS Big Sur 11 x64. diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln index f16ef8098d..ffe4669d48 100644 --- a/OpenTelemetry.AutoInstrumentation.sln +++ b/OpenTelemetry.AutoInstrumentation.sln @@ -239,6 +239,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.OracleMda.C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-dotnet-monitor-logs", "next-gen\docs\getting-started-dotnet-monitor-logs\getting-started-dotnet-monitor-logs.csproj", "{959764E7-5A0C-4511-8004-48DE6B10F499}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication.RabbitMq", "test\test-applications\integrations\TestApplication.RabbitMq\TestApplication.RabbitMq.csproj", "{91D883EC-069E-46BC-B6F7-67C94299851E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1459,6 +1461,22 @@ Global {959764E7-5A0C-4511-8004-48DE6B10F499}.Release|x64.Build.0 = Release|Any CPU {959764E7-5A0C-4511-8004-48DE6B10F499}.Release|x86.ActiveCfg = Release|Any CPU {959764E7-5A0C-4511-8004-48DE6B10F499}.Release|x86.Build.0 = Release|Any CPU + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|Any CPU.ActiveCfg = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|Any CPU.Build.0 = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|ARM64.ActiveCfg = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|ARM64.Build.0 = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|x64.ActiveCfg = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|x64.Build.0 = Debug|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|x86.ActiveCfg = Debug|x86 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Debug|x86.Build.0 = Debug|x86 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|Any CPU.ActiveCfg = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|Any CPU.Build.0 = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|ARM64.ActiveCfg = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|ARM64.Build.0 = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|x64.ActiveCfg = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|x64.Build.0 = Release|x64 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|x86.ActiveCfg = Release|x86 + {91D883EC-069E-46BC-B6F7-67C94299851E}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1545,6 +1563,7 @@ Global {022A03CE-DD7A-4326-847E-3B750E660845} = {3F051815-8E0D-4356-BC36-55CA642DDF18} {21A915DF-8B9E-4CE8-84DA-1057CDCE117E} = {E409ADD3-9574-465C-AB09-4324D205CC7C} {959764E7-5A0C-4511-8004-48DE6B10F499} = {3F051815-8E0D-4356-BC36-55CA642DDF18} + {91D883EC-069E-46BC-B6F7-67C94299851E} = {E409ADD3-9574-465C-AB09-4324D205CC7C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F} diff --git a/build/LibraryVersions.g.cs b/build/LibraryVersions.g.cs index d6f6890136..ca451cc70a 100644 --- a/build/LibraryVersions.g.cs +++ b/build/LibraryVersions.g.cs @@ -181,5 +181,13 @@ public static partial class LibraryVersion new("2.4.0"), } }, + { + "TestApplication.RabbitMq", + new List + { + new("6.0.0"), + new("6.8.1"), + } + }, }; } diff --git a/docs/config.md b/docs/config.md index 2df32968f5..e7addd6745 100644 --- a/docs/config.md +++ b/docs/config.md @@ -147,6 +147,7 @@ due to lack of stable semantic convention. | `NPGSQL` | [Npgsql](https://www.nuget.org/packages/Npgsql) | ≥6.0.0 | source | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | | `NSERVICEBUS` | [NServiceBus](https://www.nuget.org/packages/NServiceBus) | ≥8.0.0 & < 10.0.0 | source & bytecode | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | | `ORACLEMDA` | [Oracle.ManagedDataAccess.Core](https://www.nuget.org/packages/Oracle.ManagedDataAccess.Core) and [Oracle.ManagedDataAccess](https://www.nuget.org/packages/Oracle.ManagedDataAccess) **Not supported on ARM64** | ≥23.4.0 | source | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | +| `RABBITMQ` | [RabbitMQ.Client](https://www.nuget.org/packages/RabbitMQ.Client/) | ≥6.0.0 & < 7.0.0 | bytecode | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | | `QUARTZ` | [Quartz](https://www.nuget.org/packages/Quartz) **Not supported on .NET Framework 4.7.1 and older** | ≥3.4.0 | source | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | | `SQLCLIENT` | [Microsoft.Data.SqlClient](https://www.nuget.org/packages/Microsoft.Data.SqlClient), [System.Data.SqlClient](https://www.nuget.org/packages/System.Data.SqlClient) and `System.Data` (shipped with .NET Framework) | * \[6\] | source | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | | `STACKEXCHANGEREDIS` | [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis) **Not supported on .NET Framework** | ≥2.6.122 & < 3.0.0 | source & bytecode | [Experimental](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/versioning-and-stability.md) | diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2..e2a17377a0 100644 --- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.AsyncEventingBasicConsumerIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.EventingBasicConsumerIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBaseBasicGetIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBasicPublishIntegration \ No newline at end of file diff --git a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net6.0/PublicAPI.Unshipped.txt index e69de29bb2..e2a17377a0 100644 --- a/src/OpenTelemetry.AutoInstrumentation/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.AutoInstrumentation/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.AsyncEventingBasicConsumerIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.EventingBasicConsumerIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBaseBasicGetIntegration +OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBasicPublishIntegration \ No newline at end of file diff --git a/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerInstrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerInstrumentation.cs index f2e6a0da24..39e56d6307 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerInstrumentation.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Configurations/TracerInstrumentation.cs @@ -127,5 +127,10 @@ internal enum TracerInstrumentation /// /// Oracle Managed Data Access (Core) instrumentation /// - OracleMda = 21 + OracleMda = 21, + + /// + /// RabbitMQ instrumentation + /// + RabbitMq = 22 } diff --git a/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index 534cac38ec..47367e7418 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Generated/net462/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -19,7 +19,7 @@ internal static partial class InstrumentationDefinitions private static NativeCallTargetDefinition[] GetDefinitionsArray() { - var nativeCallTargetDefinitions = new List(16); + var nativeCallTargetDefinitions = new List(20); // Traces var tracerSettings = Instrumentation.TracerSettings.Value; if (tracerSettings.TracesEnabled) @@ -54,6 +54,15 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray() nativeCallTargetDefinitions.Add(new("NServiceBus.Core", "NServiceBus.EndpointConfiguration", ".ctor", new[] {"System.Void", "System.String"}, 8, 0, 0, 8, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NServiceBus.EndpointConfigurationIntegration")); } + // RabbitMq + if (tracerSettings.EnabledInstrumentations.Contains(TracerInstrumentation.RabbitMq)) + { + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Events.AsyncEventingBasicConsumer", "HandleBasicDeliver", new[] {"System.Threading.Tasks.Task", "System.String", "System.UInt64", "System.Boolean", "System.String", "System.String", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.AsyncEventingBasicConsumerIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Events.EventingBasicConsumer", "HandleBasicDeliver", new[] {"System.Void", "System.String", "System.UInt64", "System.Boolean", "System.String", "System.String", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.EventingBasicConsumerIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Impl.ModelBase", "BasicGet", new[] {"RabbitMQ.Client.BasicGetResult", "System.String", "System.Boolean"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBaseBasicGetIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Framing.Impl.Model", "_Private_BasicPublish", new[] {"System.Void", "System.String", "System.String", "System.Boolean", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBasicPublishIntegration")); + } + // WcfClient if (tracerSettings.EnabledInstrumentations.Contains(TracerInstrumentation.WcfClient)) { diff --git a/src/OpenTelemetry.AutoInstrumentation/Generated/net6.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/src/OpenTelemetry.AutoInstrumentation/Generated/net6.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index 5a555a0029..291a29f5b6 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Generated/net6.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Generated/net6.0/SourceGenerators/SourceGenerators.InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -19,7 +19,7 @@ internal static partial class InstrumentationDefinitions private static NativeCallTargetDefinition[] GetDefinitionsArray() { - var nativeCallTargetDefinitions = new List(19); + var nativeCallTargetDefinitions = new List(23); // Traces var tracerSettings = Instrumentation.TracerSettings.Value; if (tracerSettings.TracesEnabled) @@ -48,6 +48,15 @@ private static NativeCallTargetDefinition[] GetDefinitionsArray() nativeCallTargetDefinitions.Add(new("NServiceBus.Core", "NServiceBus.EndpointConfiguration", ".ctor", new[] {"System.Void", "System.String"}, 8, 0, 0, 9, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.NServiceBus.EndpointConfigurationIntegration")); } + // RabbitMq + if (tracerSettings.EnabledInstrumentations.Contains(TracerInstrumentation.RabbitMq)) + { + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Events.AsyncEventingBasicConsumer", "HandleBasicDeliver", new[] {"System.Threading.Tasks.Task", "System.String", "System.UInt64", "System.Boolean", "System.String", "System.String", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.AsyncEventingBasicConsumerIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Events.EventingBasicConsumer", "HandleBasicDeliver", new[] {"System.Void", "System.String", "System.UInt64", "System.Boolean", "System.String", "System.String", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.EventingBasicConsumerIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Impl.ModelBase", "BasicGet", new[] {"RabbitMQ.Client.BasicGetResult", "System.String", "System.Boolean"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBaseBasicGetIntegration")); + nativeCallTargetDefinitions.Add(new("RabbitMQ.Client", "RabbitMQ.Client.Framing.Impl.Model", "_Private_BasicPublish", new[] {"System.Void", "System.String", "System.String", "System.Boolean", "RabbitMQ.Client.IBasicProperties", "System.ReadOnlyMemory`1[System.Byte]"}, 6, 0, 0, 6, 65535, 65535, AssemblyFullName, "OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations.ModelBasicPublishIntegration")); + } + // StackExchangeRedis if (tracerSettings.EnabledInstrumentations.Contains(TracerInstrumentation.StackExchangeRedis)) { diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs index b24f74b596..c521be0552 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentation.cs @@ -375,6 +375,8 @@ private static void AddLazilyLoadedTraceInstrumentations(LazyInstrumentationLoad break; case TracerInstrumentation.Kafka: break; + case TracerInstrumentation.RabbitMq: + break; case TracerInstrumentation.OracleMda: break; default: diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Kafka/MessagingAttributes.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/MessagingAttributes.cs similarity index 64% rename from src/OpenTelemetry.AutoInstrumentation/Instrumentations/Kafka/MessagingAttributes.cs rename to src/OpenTelemetry.AutoInstrumentation/Instrumentations/MessagingAttributes.cs index 632167cb26..4863ae8ffe 100644 --- a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/Kafka/MessagingAttributes.cs +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/MessagingAttributes.cs @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -namespace OpenTelemetry.AutoInstrumentation.Instrumentations.Kafka; +namespace OpenTelemetry.AutoInstrumentation.Instrumentations; // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/messaging/messaging-spans.md#messaging-attributes internal static class MessagingAttributes @@ -12,6 +12,9 @@ internal static class Keys public const string MessagingOperation = "messaging.operation"; public const string DestinationName = "messaging.destination.name"; public const string ClientId = "messaging.client_id"; + public const string MessageBodySize = "messaging.message.body.size"; + public const string MessageId = "messaging.message.id"; + public const string ConversationId = "messaging.message.conversation_id"; // https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/messaging/kafka.md#span-attributes internal static class Kafka @@ -22,6 +25,12 @@ internal static class Kafka public const string PartitionOffset = "messaging.kafka.message.offset"; public const string IsTombstone = "messaging.kafka.message.tombstone"; } + + internal static class RabbitMq + { + public const string RoutingKey = "messaging.rabbitmq.destination.routing_key"; + public const string DeliveryTag = "messaging.rabbitmq.delivery_tag"; + } } internal static class Values @@ -29,5 +38,12 @@ internal static class Values public const string KafkaMessagingSystemName = "kafka"; public const string PublishOperationName = "publish"; public const string ReceiveOperationName = "receive"; + public const string ProcessOperationName = "process"; + + internal static class RabbitMq + { + public const string MessagingSystemName = "rabbitmq"; + public const string DefaultExchangeName = "amq.default"; + } } } diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBasicGetResult.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBasicGetResult.cs new file mode 100644 index 0000000000..3335d91414 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBasicGetResult.cs @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.DuckTyping; + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; + +// wraps https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/a50334f2acb09fd16dc9cbd20ad1c6dd093d1d64/projects/RabbitMQ.Client/client/api/BasicGetResult.cs +internal interface IBasicGetResult : IDuckType +{ + public string? Exchange { get; set; } + + public string? RoutingKey { get; set; } + + public IBasicProperties? BasicProperties { get; set; } + + public ulong DeliveryTag { get; set; } + + public IBody Body { get; set; } +} + +// wraps https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/a50334f2acb09fd16dc9cbd20ad1c6dd093d1d64/projects/RabbitMQ.Client/client/api/IBasicProperties.cs +internal interface IBasicProperties : IDuckType +{ + IDictionary? Headers { get; set; } + + string? CorrelationId { get; set; } + + string? MessageId { get; set; } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBody.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBody.cs new file mode 100644 index 0000000000..25e19cc188 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/DuckTypes/IBody.cs @@ -0,0 +1,11 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.DuckTyping; + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; + +internal interface IBody : IDuckType +{ + public int Length { get; } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/IntegrationConstants.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/IntegrationConstants.cs new file mode 100644 index 0000000000..f6f07a6bb8 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/IntegrationConstants.cs @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6; + +internal static class IntegrationConstants +{ + public const string RabbitMqByteCodeIntegrationName = "RabbitMq"; + public const string RabbitMqAssemblyName = "RabbitMQ.Client"; + public const string ModelBaseTypeName = "RabbitMQ.Client.Impl.ModelBase"; + public const string EventingBasicConsumerTypeName = "RabbitMQ.Client.Events.EventingBasicConsumer"; + public const string AsyncEventingBasicConsumerTypeName = "RabbitMQ.Client.Events.AsyncEventingBasicConsumer"; + public const string ModelGeneratedTypeName = "RabbitMQ.Client.Framing.Impl.Model"; + public const string BasicGetResultTypeName = "RabbitMQ.Client.BasicGetResult"; + public const string BasicPropertiesInterfaceTypeName = "RabbitMQ.Client.IBasicProperties"; + public const string BasicGetMethodName = "BasicGet"; + public const string HandleBasicDeliverMethodName = "HandleBasicDeliver"; + public const string BasicPublishMethodName = "_Private_BasicPublish"; + public const string MinSupportedVersion = "6.0.0"; + public const string MaxSupportedVersion = "6.*.*"; +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/AsyncEventingBasicConsumerIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/AsyncEventingBasicConsumerIntegration.cs new file mode 100644 index 0000000000..3a60874abf --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/AsyncEventingBasicConsumerIntegration.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.CallTarget; +using OpenTelemetry.AutoInstrumentation.DuckTyping; +using OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; +using OpenTelemetry.AutoInstrumentation.Util; +// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations; + +/// +/// RabbitMq AsyncEventingBasicConsumer.HandleBasicDeliver integration. +/// +[InstrumentMethod( + assemblyName: IntegrationConstants.RabbitMqAssemblyName, + typeName: IntegrationConstants.AsyncEventingBasicConsumerTypeName, + methodName: IntegrationConstants.HandleBasicDeliverMethodName, + returnTypeName: ClrNames.Task, + parameterTypeNames: new[] { ClrNames.String, ClrNames.UInt64, ClrNames.Bool, ClrNames.String, ClrNames.String, IntegrationConstants.BasicPropertiesInterfaceTypeName, $"System.ReadOnlyMemory`1[{ClrNames.Byte}]" }, + minimumVersion: IntegrationConstants.MinSupportedVersion, + maximumVersion: IntegrationConstants.MaxSupportedVersion, + integrationName: IntegrationConstants.RabbitMqByteCodeIntegrationName, + type: InstrumentationType.Trace)] +public static class AsyncEventingBasicConsumerIntegration +{ + internal static CallTargetState OnMethodBegin(TTarget instance, string? consumerTag, ulong deliveryTag, bool redelivered, string? exchange, string? routingKey, TBasicProperties properties, TBody body) + where TBasicProperties : IBasicProperties + where TBody : IBody + { + var activity = RabbitMqInstrumentation.StartProcess(properties, exchange, routingKey, body, deliveryTag); + return new CallTargetState(activity, null); + } + + internal static TReturn OnAsyncMethodEnd(TTarget instance, TReturn returnValue, Exception? exception, in CallTargetState state) + { + var activity = state.Activity; + if (activity is null) + { + return returnValue; + } + + if (exception is not null) + { + activity.SetException(exception); + } + + activity.Stop(); + + return returnValue; + } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/EventingBasicConsumerIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/EventingBasicConsumerIntegration.cs new file mode 100644 index 0000000000..62894e3f7b --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/EventingBasicConsumerIntegration.cs @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.CallTarget; +using OpenTelemetry.AutoInstrumentation.DuckTyping; +using OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; +using OpenTelemetry.AutoInstrumentation.Util; +// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations; + +/// +/// RabbitMq EventingBasicConsumer.HandleBasicDeliver integration. +/// +[InstrumentMethod( + assemblyName: IntegrationConstants.RabbitMqAssemblyName, + typeName: IntegrationConstants.EventingBasicConsumerTypeName, + methodName: IntegrationConstants.HandleBasicDeliverMethodName, + returnTypeName: ClrNames.Void, + parameterTypeNames: new[] { ClrNames.String, ClrNames.UInt64, ClrNames.Bool, ClrNames.String, ClrNames.String, IntegrationConstants.BasicPropertiesInterfaceTypeName, $"System.ReadOnlyMemory`1[{ClrNames.Byte}]" }, + minimumVersion: IntegrationConstants.MinSupportedVersion, + maximumVersion: IntegrationConstants.MaxSupportedVersion, + integrationName: IntegrationConstants.RabbitMqByteCodeIntegrationName, + type: InstrumentationType.Trace)] +public static class EventingBasicConsumerIntegration +{ + internal static CallTargetState OnMethodBegin(TTarget instance, string? consumerTag, ulong deliveryTag, bool redelivered, string? exchange, string? routingKey, TBasicProperties properties, TBody body) + where TBasicProperties : IBasicProperties + where TBody : IBody + { + var activity = RabbitMqInstrumentation.StartProcess(properties, exchange, routingKey, body, deliveryTag); + return new CallTargetState(activity, null); + } + + internal static CallTargetReturn OnMethodEnd(TTarget instance, Exception? exception, in CallTargetState state) + { + var activity = state.Activity; + if (activity is null) + { + return CallTargetReturn.GetDefault(); + } + + if (exception is not null) + { + activity.SetException(exception); + } + + activity.Stop(); + + return CallTargetReturn.GetDefault(); + } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBaseBasicGetIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBaseBasicGetIntegration.cs new file mode 100644 index 0000000000..3a84b628ad --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBaseBasicGetIntegration.cs @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Resources; +using OpenTelemetry.AutoInstrumentation.CallTarget; +using OpenTelemetry.AutoInstrumentation.DuckTyping; +using OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; +using OpenTelemetry.AutoInstrumentation.Util; + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations; + +/// +/// RabbitMq ModelBase.BasicGet integration. +/// +[InstrumentMethod( + assemblyName: IntegrationConstants.RabbitMqAssemblyName, + typeName: IntegrationConstants.ModelBaseTypeName, + methodName: IntegrationConstants.BasicGetMethodName, + returnTypeName: IntegrationConstants.BasicGetResultTypeName, + parameterTypeNames: new[] { ClrNames.String, ClrNames.Bool }, + minimumVersion: IntegrationConstants.MinSupportedVersion, + maximumVersion: IntegrationConstants.MaxSupportedVersion, + integrationName: IntegrationConstants.RabbitMqByteCodeIntegrationName, + type: InstrumentationType.Trace)] +public static class ModelBaseBasicGetIntegration +{ + internal static CallTargetState OnMethodBegin(TTarget instance, string queue, bool autoAck) + { + return new CallTargetState(null, null, DateTimeOffset.UtcNow); + } + + internal static CallTargetReturn OnMethodEnd(TTarget instance, TResponse response, Exception? exception, in CallTargetState state) + where TResponse : IBasicGetResult + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (response.Instance is not null) + { + using var activity = RabbitMqInstrumentation.StartReceive(response, state.StartTime!.Value); + if (exception is not null) + { + activity.SetException(exception); + } + } + + return new CallTargetReturn(response); + } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBasicPublishIntegration.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBasicPublishIntegration.cs new file mode 100644 index 0000000000..1144742fd0 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/Integrations/ModelBasicPublishIntegration.cs @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.AutoInstrumentation.CallTarget; +using OpenTelemetry.AutoInstrumentation.DuckTyping; +using OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; +using OpenTelemetry.AutoInstrumentation.Util; +// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.Integrations; + +/// +/// Model BasicPublish integration. +/// +[InstrumentMethod( +assemblyName: IntegrationConstants.RabbitMqAssemblyName, +typeName: IntegrationConstants.ModelGeneratedTypeName, +methodName: IntegrationConstants.BasicPublishMethodName, +returnTypeName: ClrNames.Void, +parameterTypeNames: new[] { ClrNames.String, ClrNames.String, ClrNames.Bool, IntegrationConstants.BasicPropertiesInterfaceTypeName, $"System.ReadOnlyMemory`1[{ClrNames.Byte}]" }, +minimumVersion: IntegrationConstants.MinSupportedVersion, +maximumVersion: IntegrationConstants.MaxSupportedVersion, +integrationName: IntegrationConstants.RabbitMqByteCodeIntegrationName, +type: InstrumentationType.Trace)] +public static class ModelBasicPublishIntegration +{ + internal static CallTargetState OnMethodBegin( + TTarget instance, string? exchange, string? routingKey, bool mandatory, TBasicProperties basicProperties, TBody body) + where TBasicProperties : IBasicProperties + where TBody : IBody + { + var messageBodyLength = body.Instance == null ? 0 : body.Length; + var activity = RabbitMqInstrumentation.StartPublish(basicProperties, exchange, routingKey, messageBodyLength); + if (activity is not null) + { + return new CallTargetState(activity); + } + + return CallTargetState.GetDefault(); + } + + internal static CallTargetReturn OnMethodEnd(TTarget instance, Exception? exception, in CallTargetState state) + { + var activity = state.Activity; + if (activity is null) + { + return CallTargetReturn.GetDefault(); + } + + if (exception is not null) + { + activity.SetException(exception); + } + + activity.Stop(); + return CallTargetReturn.GetDefault(); + } +} diff --git a/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/RabbitMqInstrumentation.cs b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/RabbitMqInstrumentation.cs new file mode 100644 index 0000000000..20ef682a74 --- /dev/null +++ b/src/OpenTelemetry.AutoInstrumentation/Instrumentations/RabbitMq6/RabbitMqInstrumentation.cs @@ -0,0 +1,172 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Text; +using OpenTelemetry.AutoInstrumentation.DuckTyping; +using OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6.DuckTypes; +using OpenTelemetry.Context.Propagation; +// ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + +namespace OpenTelemetry.AutoInstrumentation.Instrumentations.RabbitMq6; + +internal static class RabbitMqInstrumentation +{ + private static readonly ActivitySource Source = new("OpenTelemetry.AutoInstrumentation.RabbitMq"); + + public static Activity? StartReceive(TBasicResult result, DateTimeOffset startTime) + where TBasicResult : IBasicGetResult + { + return StartConsume( + result.BasicProperties?.Headers, + MessagingAttributes.Values.ReceiveOperationName, + result.Exchange, + result.RoutingKey, + result.DeliveryTag, + result.Body.Length, + result.BasicProperties?.MessageId, + result.BasicProperties?.CorrelationId, + startTime); + } + + public static Activity? StartProcess(TBasicProperties properties, string? exchange, string? routingKey, TBody body, ulong deliveryTag) + where TBasicProperties : IBasicProperties + where TBody : IBody + { + var messageBodyLength = body.Instance == null ? 0 : body.Length; + IDictionary? headers = null; + string? messageId = null; + string? correlationId = null; + + if (properties.Instance is not null) + { + headers = properties.Headers; + messageId = properties.MessageId; + correlationId = properties.CorrelationId; + } + + return StartConsume( + headers, + MessagingAttributes.Values.ProcessOperationName, + exchange, + routingKey, + deliveryTag, + messageBodyLength, + messageId, + correlationId); + } + + public static Activity? StartPublish(TBasicProperties basicProperties, string? exchange, string? routingKey, int bodyLength) + where TBasicProperties : IBasicProperties + { + var name = GetActivityName(routingKey, MessagingAttributes.Values.PublishOperationName); + var activity = Source.StartActivity(name, ActivityKind.Producer); + if (activity is not null && basicProperties.Instance is not null) + { + basicProperties.Headers ??= new Dictionary(); + Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activity.Context, Baggage.Current), basicProperties.Headers, MessageHeaderValueSetter); + } + + if (activity is { IsAllDataRequested: true }) + { + SetCommonTags(activity, exchange, routingKey, bodyLength, MessagingAttributes.Values.PublishOperationName); + } + + return activity; + } + + private static string GetActivityName(string? routingKey, string operationType) + { + return string.IsNullOrEmpty(routingKey) ? operationType : $"{routingKey} {operationType}"; + } + + private static Activity? StartConsume( + IDictionary? headers, + string consumeOperationName, + string? exchange, + string? routingKey, + ulong deliveryTag, + int bodyLength, + string? messageId, + string? correlationId, + DateTimeOffset startTime = default) + { + var propagatedContext = Propagators.DefaultTextMapPropagator.Extract(default, headers, MessageHeaderValueGetter); + + var activityLinks = propagatedContext.ActivityContext.IsValid() + ? new[] { new ActivityLink(propagatedContext.ActivityContext) } + : Array.Empty(); + + var name = GetActivityName(routingKey, consumeOperationName); + var activity = Source.StartActivity( + name: name, + kind: ActivityKind.Consumer, + links: activityLinks, + startTime: startTime); + if (activity is { IsAllDataRequested: true }) + { + SetConsumeTags( + activity, + exchange, + routingKey, + consumeOperationName, + deliveryTag, + bodyLength, + messageId, + correlationId); + } + + return activity; + } + + private static void SetCommonTags(Activity activity, string? resultExchange, string? routingKey, int bodyLength, string operationName) + { + var exchange = string.IsNullOrEmpty(resultExchange) ? MessagingAttributes.Values.RabbitMq.DefaultExchangeName : resultExchange; + + activity + .SetTag(MessagingAttributes.Keys.MessagingSystem, MessagingAttributes.Values.RabbitMq.MessagingSystemName) + .SetTag(MessagingAttributes.Keys.MessagingOperation, operationName) + .SetTag(MessagingAttributes.Keys.DestinationName, exchange) + .SetTag(MessagingAttributes.Keys.MessageBodySize, bodyLength); + if (!string.IsNullOrEmpty(routingKey)) + { + activity.SetTag(MessagingAttributes.Keys.RabbitMq.RoutingKey, routingKey); + } + } + + private static void SetConsumeTags(Activity activity, string? exchange, string? routingKey, string operationName, ulong deliveryTag, int bodyLength, string? basicPropertiesMessageId, string? basicPropertiesCorrelationId) + { + SetCommonTags(activity, exchange, routingKey, bodyLength, operationName); + if (!string.IsNullOrEmpty(basicPropertiesMessageId)) + { + activity.SetTag(MessagingAttributes.Keys.MessageId, basicPropertiesMessageId); + } + + if (!string.IsNullOrEmpty(basicPropertiesCorrelationId)) + { + activity.SetTag(MessagingAttributes.Keys.ConversationId, basicPropertiesCorrelationId); + } + + if (deliveryTag > 0) + { + activity.SetTag(MessagingAttributes.Keys.RabbitMq.DeliveryTag, deliveryTag); + } + } + + private static IEnumerable MessageHeaderValueGetter( + IDictionary? headers, + string key) + { + if (headers is not null && headers.TryGetValue(key, out var value) && value is byte[] bytes) + { + return new[] { Encoding.UTF8.GetString(bytes) }; + } + + return Enumerable.Empty(); + } + + private static void MessageHeaderValueSetter(IDictionary headers, string key, string val) + { + headers[key] = val; + } +} diff --git a/test/Directory.Packages.props b/test/Directory.Packages.props index 66258eeccc..d64c6c31d9 100644 --- a/test/Directory.Packages.props +++ b/test/Directory.Packages.props @@ -41,6 +41,7 @@ + diff --git a/test/IntegrationTests/IntegrationTests.csproj b/test/IntegrationTests/IntegrationTests.csproj index 43a0c4314c..a3d3c9cbba 100644 --- a/test/IntegrationTests/IntegrationTests.csproj +++ b/test/IntegrationTests/IntegrationTests.csproj @@ -38,6 +38,7 @@ + diff --git a/test/IntegrationTests/LibraryVersions.g.cs b/test/IntegrationTests/LibraryVersions.g.cs index 56d28394bf..6aa22a15e7 100644 --- a/test/IntegrationTests/LibraryVersions.g.cs +++ b/test/IntegrationTests/LibraryVersions.g.cs @@ -319,6 +319,20 @@ public static TheoryData Kafka_x64 theoryData.Add(string.Empty); #else theoryData.Add("1.4.0"); +#endif + return theoryData; + } + } + public static TheoryData RabbitMq + { + get + { + var theoryData = new TheoryData(); +#if DEFAULT_TEST_PACKAGE_VERSIONS + theoryData.Add(string.Empty); +#else + theoryData.Add("6.0.0"); + theoryData.Add("6.8.1"); #endif return theoryData; } @@ -346,5 +360,6 @@ public static TheoryData Kafka_x64 { "WCFCoreClient", WCFCoreClient }, { "Kafka", Kafka }, { "Kafka_x64", Kafka_x64 }, + { "RabbitMq", RabbitMq }, }; } diff --git a/test/IntegrationTests/RabbitMqCollection.cs b/test/IntegrationTests/RabbitMqCollection.cs new file mode 100644 index 0000000000..d9932a4738 --- /dev/null +++ b/test/IntegrationTests/RabbitMqCollection.cs @@ -0,0 +1,80 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using IntegrationTests.Helpers; +using static IntegrationTests.Helpers.DockerFileHelper; + +namespace IntegrationTests; + +[CollectionDefinition(Name)] +public class RabbitMqCollection : ICollectionFixture +{ + public const string Name = nameof(RabbitMqCollection); +} + +public class RabbitMqFixture : IAsyncLifetime +{ + private const int RabbitMqPort = 5672; + private static readonly string RabbitMqImage = ReadImageFrom("rabbitmq.Dockerfile"); + + private IContainer? _container; + + public RabbitMqFixture() + { + if (IsCurrentArchitectureSupported) + { + Port = TcpPortProvider.GetOpenPort(); + } + } + + public bool IsCurrentArchitectureSupported { get; } = EnvironmentTools.IsX64(); + + public int Port { get; } + + public async Task InitializeAsync() + { + if (!IsCurrentArchitectureSupported) + { + return; + } + + _container = await LaunchRabbitMqContainerAsync(Port); + } + + public async Task DisposeAsync() + { + if (_container != null) + { + await ShutdownRabbitMqContainerAsync(_container); + } + } + + public void SkipIfUnsupportedPlatform() + { + if (!IsCurrentArchitectureSupported) + { + throw new SkipException("RabbitMQ is supported only on AMD64."); + } + } + + private static async Task LaunchRabbitMqContainerAsync(int port) + { + var containersBuilder = new ContainerBuilder() + .WithImage(RabbitMqImage) + .WithName($"rabbitmq-{port}") + .WithPortBinding(port, RabbitMqPort) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(RabbitMqPort)); + + var container = containersBuilder.Build(); + await container.StartAsync(); + + return container; + } + + private static async Task ShutdownRabbitMqContainerAsync(IContainer container) + { + await container.DisposeAsync(); + } +} diff --git a/test/IntegrationTests/RabbitMqTests.cs b/test/IntegrationTests/RabbitMqTests.cs new file mode 100644 index 0000000000..a8be10dc45 --- /dev/null +++ b/test/IntegrationTests/RabbitMqTests.cs @@ -0,0 +1,100 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using FluentAssertions; +using IntegrationTests.Helpers; +using OpenTelemetry.Proto.Common.V1; +using OpenTelemetry.Proto.Trace.V1; +using Xunit.Abstractions; + +namespace IntegrationTests; + +[Collection(RabbitMqCollection.Name)] +public class RabbitMqTests : TestHelper +{ + private const string MessagingSystemAttributeName = "messaging.system"; + private const string MessagingOperationAttributeName = "messaging.operation"; + private const string MessagingDestinationAttributeName = "messaging.destination.name"; + private const string RabbitMqRoutingKeyAttributeName = "messaging.rabbitmq.destination.routing_key"; + private const string MessagingBodySizeAttributeName = "messaging.message.body.size"; + private readonly RabbitMqFixture _rabbitMq; + + public RabbitMqTests(ITestOutputHelper output, RabbitMqFixture rabbitMq) + : base("RabbitMq", output) + { + _rabbitMq = rabbitMq; + } + + [SkippableTheory] + [Trait("Category", "EndToEnd")] + [Trait("Containers", "Linux")] + [MemberData(nameof(LibraryVersion.RabbitMq), MemberType = typeof(LibraryVersion))] + public void SubmitsTraces(string packageVersion) + { + // Skip the test if fixture does not support current platform + _rabbitMq.SkipIfUnsupportedPlatform(); + + using var collector = new MockSpansCollector(Output); + SetExporter(collector); + + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateProducerSpan(span)); + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateProducerSpan(span)); + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateProducerSpan(span)); + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateConsumerSpan(span, "receive")); + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateConsumerSpan(span, "process")); + collector.Expect("OpenTelemetry.AutoInstrumentation.RabbitMq", span => ValidateConsumerSpan(span, "process")); + + collector.ExpectCollected(collected => ValidatePropagation(collected)); + + EnableBytecodeInstrumentation(); + RunTestApplication(new() + { + Arguments = $"--rabbitmq {_rabbitMq.Port}", + PackageVersion = packageVersion + }); + + collector.AssertExpectations(); + } + + private static bool ValidateConsumerSpan(Span span, string operationName) + { + return span.Kind == Span.Types.SpanKind.Consumer && span.Links.Count == 1 && ValidateBasicSpanAttributes(span.Attributes, operationName); + } + + private static bool ValidateProducerSpan(Span span) + { + return span.Kind == Span.Types.SpanKind.Producer && ValidateBasicSpanAttributes(span.Attributes, "publish"); + } + + private static bool ValidatePropagation(ICollection collected) + { + var producerSpans = collected.Where(span => span.Span.Kind == Span.Types.SpanKind.Producer).ToList(); + + producerSpans.Count.Should().Be(3); + + return producerSpans.All(span => VerifySingleMatchingConsumerSpan(span)); + + bool VerifySingleMatchingConsumerSpan(MockSpansCollector.Collected producerSpan) + { + return collected.Count(spans => + spans.Span.Kind == Span.Types.SpanKind.Consumer && + spans.Span.Links[0].TraceId == producerSpan.Span.TraceId && + spans.Span.Links[0].SpanId == producerSpan.Span.SpanId) == 1; + } + } + + private static bool ValidateBasicSpanAttributes(IReadOnlyCollection attributes, string operationName) + { + var messagingSystem = attributes.Single(kv => kv.Key == MessagingSystemAttributeName).Value.StringValue; + var messagingOperation = attributes.Single(kv => kv.Key == MessagingOperationAttributeName).Value.StringValue; + var destinationName = attributes.Single(kv => kv.Key == MessagingDestinationAttributeName).Value.StringValue; + var routingKey = attributes.Single(kv => kv.Key == RabbitMqRoutingKeyAttributeName).Value.StringValue; + var bodySize = attributes.Single(kv => kv.Key == MessagingBodySizeAttributeName).Value.IntValue; + + return messagingSystem == "rabbitmq" && + messagingOperation == operationName && + destinationName == "amq.default" && + routingKey == "hello" && + bodySize == 13; + } +} diff --git a/test/IntegrationTests/docker/rabbitmq.Dockerfile b/test/IntegrationTests/docker/rabbitmq.Dockerfile new file mode 100644 index 0000000000..d5e76ab14d --- /dev/null +++ b/test/IntegrationTests/docker/rabbitmq.Dockerfile @@ -0,0 +1 @@ +FROM rabbitmq:3.13.1 \ No newline at end of file diff --git a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs index 8c34f8f05c..d63525f9ce 100644 --- a/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs +++ b/test/OpenTelemetry.AutoInstrumentation.Tests/Configurations/SettingsTests.cs @@ -286,6 +286,7 @@ internal void Propagators_FailFast() [InlineData("ELASTICTRANSPORT", TracerInstrumentation.ElasticTransport)] [InlineData("KAFKA", TracerInstrumentation.Kafka)] [InlineData("ORACLEMDA", TracerInstrumentation.OracleMda)] + [InlineData("RABBITMQ", TracerInstrumentation.RabbitMq)] internal void TracerSettings_Instrumentations_SupportedValues(string tracerInstrumentation, TracerInstrumentation expectedTracerInstrumentation) { Environment.SetEnvironmentVariable(ConfigurationKeys.Traces.TracesInstrumentationEnabled, "false"); diff --git a/test/test-applications/integrations/TestApplication.RabbitMq/Program.cs b/test/test-applications/integrations/TestApplication.RabbitMq/Program.cs new file mode 100644 index 0000000000..ec9e23fbf3 --- /dev/null +++ b/test/test-applications/integrations/TestApplication.RabbitMq/Program.cs @@ -0,0 +1,145 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace TestApplication.RabbitMq; + +internal static class Program +{ + private const string RoutingKey = "hello"; + private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(10); + private static int _messageNumber; + + // based on tutorials from https://www.rabbitmq.com/tutorials/ + + public static int Main(string[] args) + { + var result = PublishAndConsumeWithSyncDispatcher(args); + if (result != 0) + { + return result; + } + + return PublishAndConsumeAsyncWithAsyncDispatcher(args); + } + + private static int PublishAndConsumeWithSyncDispatcher(string[] args) + { + var syncConsumersConnectionFactory = new ConnectionFactory { HostName = "localhost", Port = int.Parse(GetRabbitMqPort(args)) }; + using var syncConsumersConnection = syncConsumersConnectionFactory.CreateConnection(); + using var syncConsumersModel = syncConsumersConnection.CreateModel(); + + syncConsumersModel.QueueDeclare( + queue: RoutingKey, + durable: false, + exclusive: false, + autoDelete: false, + arguments: null); + + Publish(syncConsumersModel, GetTestMessage(), null); + + for (var i = 0; i < 5; i++) + { + var basicGetResult = syncConsumersModel.BasicGet(queue: RoutingKey, true); + if (basicGetResult is not null) + { + ProcessReceivedMessage(basicGetResult.Body); + break; + } + + Console.WriteLine($"Read nothing, retrying: {i}"); + } + + Publish(syncConsumersModel, GetTestMessage(), syncConsumersModel.CreateBasicProperties()); + + var consumer = new EventingBasicConsumer(syncConsumersModel); + + using var mre = new ManualResetEventSlim(false); + + consumer.Received += (_, ea) => + { + ProcessReceivedMessage(ea.Body); + mre.Set(); + }; + + var consumerTag = syncConsumersModel.BasicConsume(RoutingKey, true, consumer); + if (!mre.Wait(DefaultWaitTimeout)) + { + Console.WriteLine("Timed-out waiting for callback to complete."); + return 1; + } + + syncConsumersModel.BasicCancel(consumerTag); + return 0; + } + + private static int PublishAndConsumeAsyncWithAsyncDispatcher(string[] args) + { + var asyncConsumersConnectionFactory = new ConnectionFactory + { + HostName = "localhost", + Port = int.Parse(GetRabbitMqPort(args)), + DispatchConsumersAsync = true + }; + using var asyncConsumersConnection = asyncConsumersConnectionFactory.CreateConnection(); + using var asyncConsumersModel = asyncConsumersConnection.CreateModel(); + + Publish(asyncConsumersModel, GetTestMessage(), asyncConsumersModel.CreateBasicProperties()); + var asyncConsumer = new AsyncEventingBasicConsumer(asyncConsumersModel); + + using var mre = new ManualResetEventSlim(false); + + asyncConsumer.Received += async (_, ea) => + { + ProcessReceivedMessage(ea.Body); + await Task.Yield(); + mre.Set(); + }; + + var asyncConsumerTag = asyncConsumersModel.BasicConsume(RoutingKey, true, asyncConsumer); + + if (!mre.Wait(DefaultWaitTimeout)) + { + Console.WriteLine("Timed-out waiting for callback to complete."); + return 1; + } + + asyncConsumersModel.BasicCancel(asyncConsumerTag); + + return 0; + } + + private static void ProcessReceivedMessage(ReadOnlyMemory messageBody) + { + var receivedMessageContent = Encoding.UTF8.GetString(messageBody.ToArray()); + Console.WriteLine($" [x] Received {receivedMessageContent}"); + } + + private static void Publish(IModel channel, string message, IBasicProperties? basicProperties) + { + channel.BasicPublish( + exchange: string.Empty, + routingKey: RoutingKey, + basicProperties: basicProperties, + body: Encoding.UTF8.GetBytes(message)); + Console.WriteLine($" [x] Sent {message}"); + } + + private static string GetRabbitMqPort(string[] args) + { + if (args.Length > 1) + { + return args[1]; + } + + return "5672"; + } + + private static string GetTestMessage() + { + return $"Hello World!{_messageNumber++}"; + } +} diff --git a/test/test-applications/integrations/TestApplication.RabbitMq/Properties/launchSettings.json b/test/test-applications/integrations/TestApplication.RabbitMq/Properties/launchSettings.json new file mode 100644 index 0000000000..4c34e8992c --- /dev/null +++ b/test/test-applications/integrations/TestApplication.RabbitMq/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "instrumented": { + "commandName": "Project", + "environmentVariables": { + "COR_ENABLE_PROFILING": "1", + "COR_PROFILER": "{918728DD-259F-4A6A-AC2B-B85E1B658318}", + "COR_PROFILER_PATH": "$(SolutionDir)bin\\tracer-home\\win-x64\\OpenTelemetry.AutoInstrumentation.Native.dll", + "CORECLR_ENABLE_PROFILING": "1", + "CORECLR_PROFILER": "{918728DD-259F-4A6A-AC2B-B85E1B658318}", + "CORECLR_PROFILER_PATH": "$(SolutionDir)bin\\tracer-home\\win-x64\\OpenTelemetry.AutoInstrumentation.Native.dll", + "DOTNET_ADDITIONAL_DEPS": "$(SolutionDir)bin\\tracer-home\\AdditionalDeps", + "DOTNET_SHARED_STORE": "$(SolutionDir)bin\\tracer-home\\store", + "DOTNET_STARTUP_HOOKS": "$(SolutionDir)bin\\tracer-home\\net\\OpenTelemetry.AutoInstrumentation.StartupHook.dll", + "OTEL_DOTNET_AUTO_HOME": "$(SolutionDir)bin\\tracer-home", + "OTEL_SERVICE_NAME": "TestApplication.RabbitMq", + "OTEL_LOG_LEVEL": "debug", + "OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED": "true" + } + } + } +} diff --git a/test/test-applications/integrations/TestApplication.RabbitMq/TestApplication.RabbitMq.csproj b/test/test-applications/integrations/TestApplication.RabbitMq/TestApplication.RabbitMq.csproj new file mode 100644 index 0000000000..37af22accd --- /dev/null +++ b/test/test-applications/integrations/TestApplication.RabbitMq/TestApplication.RabbitMq.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs index bca2a756b5..30672a6a59 100644 --- a/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs +++ b/tools/LibraryVersionsGenerator/PackageVersionDefinitions.cs @@ -241,6 +241,18 @@ internal static class PackageVersionDefinitions new("1.8.2"), // 1.8.0-1.8.1 are known to have issues with arm64 new("*") } + }, + new() + { + IntegrationName = "RabbitMq", + NugetPackageName = "RabbitMQ.Client", + TestApplicationName = "TestApplication.RabbitMq", + Versions = new List + { + new("6.0.0"), + new("6.8.1"), + new("*") + } } };