Skip to content

Commit

Permalink
feat: Add OpenTelemetry Instrumentation Provider (#68)
Browse files Browse the repository at this point in the history
Add OpenTelemetry Instrumentation Provider, and MessageEnvelope.Metadata is now serialized and uses JsonElements explicitly
  • Loading branch information
ashovlin authored Nov 16, 2023
1 parent 3ac714c commit 7a38d82
Show file tree
Hide file tree
Showing 19 changed files with 605 additions and 10 deletions.
7 changes: 7 additions & 0 deletions AWS.Messaging.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Messaging.Lambda", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaMessaging", "sampleapps\LambdaMessaging\LambdaMessaging.csproj", "{F74A4CF0-D814-426E-8149-46758E86AFE3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.Messaging.Telemetry.OpenTelemetry", "src\AWS.Messaging.Telemetry.OpenTelemetry\AWS.Messaging.Telemetry.OpenTelemetry.csproj", "{C529DC6E-72DA-49ED-908A-21DBC40F26C0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -69,6 +71,10 @@ Global
{F74A4CF0-D814-426E-8149-46758E86AFE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F74A4CF0-D814-426E-8149-46758E86AFE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F74A4CF0-D814-426E-8149-46758E86AFE3}.Release|Any CPU.Build.0 = Release|Any CPU
{C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C529DC6E-72DA-49ED-908A-21DBC40F26C0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -82,6 +88,7 @@ Global
{A174942B-AF9C-4935-AD7B-AF651BACE63C} = {80DB2C77-6ADD-4A60-B27D-763BDF9659D3}
{24FA3671-8C2B-4B64-865C-68FB6237E34D} = {2D0A561B-0B97-4259-8603-3AF5437BB652}
{F74A4CF0-D814-426E-8149-46758E86AFE3} = {1AA8985B-897C-4BD5-9735-FD8B33FEBFFB}
{C529DC6E-72DA-49ED-908A-21DBC40F26C0} = {2D0A561B-0B97-4259-8603-3AF5437BB652}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7B2B759D-6455-4089-8173-3F1619567B36}
Expand Down
8 changes: 8 additions & 0 deletions sampleapps/PublisherAPI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using AWS.Messaging.Telemetry.OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using PublisherAPI.Models;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -33,6 +36,11 @@
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("PublisherAPI"))
.WithTracing(tracing => tracing
.AddAWSMessagingInstrumentation()
.AddConsoleExporter());

var app = builder.Build();

Expand Down
3 changes: 3 additions & 0 deletions sampleapps/PublisherAPI/PublisherAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AWS.Messaging.Telemetry.OpenTelemetry\AWS.Messaging.Telemetry.OpenTelemetry.csproj" />
<ProjectReference Include="..\..\src\AWS.Messaging\AWS.Messaging.csproj" />
</ItemGroup>

Expand Down
11 changes: 9 additions & 2 deletions sampleapps/SubscriberService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json;
using AWS.Messaging.Telemetry.OpenTelemetry;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using SubscriberService.MessageHandlers;
using SubscriberService.Models;

Expand Down Expand Up @@ -37,7 +39,12 @@ await Host.CreateDefaultBuilder(args)
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
});
});
})
.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("SubscriberService"))
.WithTracing(tracing => tracing
.AddAWSMessagingInstrumentation()
.AddConsoleExporter());
})
.Build()
.RunAsync();
3 changes: 3 additions & 0 deletions sampleapps/SubscriberService/SubscriberService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AWS.Messaging.Telemetry.OpenTelemetry\AWS.Messaging.Telemetry.OpenTelemetry.csproj" />
<ProjectReference Include="..\..\src\AWS.Messaging\AWS.Messaging.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
<Product>AWS Message Processing Framework Instrumention for OpenTelemetry</Product>
<PackageProjectUrl>https://github.com/awslabs/aws-dotnet-messaging</PackageProjectUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RollForward>Major</RollForward>
<PackageReadmeFile>README.md</PackageReadmeFile>
<WarningsAsErrors>CA1727</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.6.0" />
<PackageReference Include="OpenTelemetry.Api" Version="1.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AWS.Messaging\AWS.Messaging.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" PackagePath="" />
<None Include="..\..\NOTICE" Pack="true" PackagePath="" />
<None Include="..\..\THIRD_PARTY_LICENSES" Pack="true" PackagePath="" />
</ItemGroup>

<ItemGroup>
<None Include=".\README.md" Pack="true" PackagePath="" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions src/AWS.Messaging.Telemetry.OpenTelemetry/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

namespace AWS.Messaging.Telemetry.OpenTelemetry;

/// <summary>
/// Constants related to the OpenTelemetry instrumentation for AWS.Messaging
/// </summary>
public class Constants
{
/// <summary>
/// OpenTelemetry activity source name
/// </summary>
public const string SourceName = "AWS.Messaging";
}
83 changes: 83 additions & 0 deletions src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;

namespace AWS.Messaging.Telemetry.OpenTelemetry;

/// <summary>
/// Creates OpenTelemetry traces
/// </summary>
public class OpenTelemetryProvider : ITelemetryProvider
{
private static readonly ActivitySource _activitySource = new ActivitySource(Constants.SourceName, TelemetryKeys.AWSMessagingAssemblyVersion);

/// <inheritdoc/>
public ITelemetryTrace Trace(string traceName)
{
var activity = _activitySource.StartActivity(traceName, ActivityKind.Producer);
if (activity != null)
{
return new OpenTelemetryTrace(activity);
}

// If we initially failed to create an activity, attempt to force creation with
// a link to the current activity, see https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities
var parentActivity = Activity.Current;
Activity.Current = null;
ActivityLink[]? links = null;
if (parentActivity != null)
{
links = new[] { new ActivityLink(parentActivity.Context) };
}

activity = _activitySource.StartActivity(traceName, ActivityKind.Producer, parentContext: default, links: links);

return new OpenTelemetryTrace(activity, parentActivity);
}

/// <inheritdoc/>
public ITelemetryTrace Trace(string traceName, MessageEnvelope envelope)
{
var propogatedContext = Propagators.DefaultTextMapPropagator.Extract(default, envelope, ExtractTraceContextFromEnvelope);
Baggage.Current = propogatedContext.Baggage;

var activity = _activitySource.StartActivity(traceName, ActivityKind.Consumer, parentContext: propogatedContext.ActivityContext);
if (activity != null)
{
return new OpenTelemetryTrace(activity);
}

// If we initially failed to create an activity, attempt to force creation with
// a link to the current activity, see https://opentelemetry.io/docs/instrumentation/net/manual/#creating-new-root-activities
var parentActivity = Activity.Current;
Activity.Current = null;
ActivityLink[]? links = null;
if (parentActivity != null)
{
links = new[] { new ActivityLink(parentActivity.Context) };
}

activity = _activitySource.StartActivity(traceName, ActivityKind.Consumer, parentContext: propogatedContext.ActivityContext, links: links);

return new OpenTelemetryTrace(activity, parentActivity);
}

/// <summary>
/// Extracts propagation context from a <see cref="MessageEnvelope"/>, meant to be used with <see cref="TextMapPropagator"/>
/// </summary>
/// <param name="envelope">Inbound message envelope</param>
/// <param name="key">Context key</param>
/// <returns>Context value</returns>
private IEnumerable<string> ExtractTraceContextFromEnvelope(MessageEnvelope envelope, string key)
{
if (envelope.Metadata.TryGetValue(key, out var jsonElement))
{
return new string[] { jsonElement.ToString() };
}

return Enumerable.Empty<string>();
}
}
105 changes: 105 additions & 0 deletions src/AWS.Messaging.Telemetry.OpenTelemetry/OpenTelemetryTrace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics;
using System.Text.Json;
using OpenTelemetry;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Trace;

namespace AWS.Messaging.Telemetry.OpenTelemetry;

/// <summary>
/// An OpenTelemetry trace (wrapper around a <see cref="Activity"/>)
/// </summary>
public class OpenTelemetryTrace : ITelemetryTrace
{
private readonly Activity? _activity;
private readonly Activity? _parentToRestore;

/// <summary>
/// Creates a new trace
/// </summary>
/// <param name="activity">New trace</param>
/// <param name="parentToRestore">Optional parent activity that will be set as <see cref="Activity.Current"/> when this trace is disposed</param>
public OpenTelemetryTrace(Activity? activity, Activity? parentToRestore = null)
{
_activity = activity;
_parentToRestore = parentToRestore;
}

/// <inheritdoc/>
public void AddException(Exception exception, bool fatal = true)
{
_activity?.RecordException(exception);

if (fatal)
{
_activity?.SetStatus(ActivityStatusCode.Error, exception.Message);
}
}

/// <inheritdoc/>
public void AddMetadata(string key, object value)
{
if (_activity != null && _activity.IsAllDataRequested)
{
_activity.SetTag(key, value);
}
}

/// <inheritdoc/>
public void RecordTelemetryContext(MessageEnvelope envelope)
{
ActivityContext contextToInject = default;
if (_activity != null)
{
contextToInject = _activity.Context;
}
// Even if an "AWS.Messaging" activity was not created, we still
// propogate the current activity (if it exists) through the message envelope
else if (Activity.Current != null)
{
contextToInject = Activity.Current.Context;
}

Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(contextToInject, Baggage.Current), envelope, InjectTraceContextIntoEnvelope);
}

/// <summary>
/// Stores propagation context in the <see cref="MessageEnvelope"/>, meant to be used with <see cref="TextMapPropagator"/>
/// </summary>
/// <param name="envelope">Outbound message envelope</param>
/// <param name="key">Context key</param>
/// <param name="value">Context value</param>
private void InjectTraceContextIntoEnvelope(MessageEnvelope envelope, string key, string value)
{
envelope.Metadata[key] = JsonSerializer.SerializeToElement(value);
}

private bool _disposed;

/// <summary>
/// Disposes the inner <see cref="Activity"/>, and also restores the parent activity if set
/// </summary>
/// <param name="disposing">Indicates whether the call comes from Dispose (true) or a finalizer (false)</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_activity?.Dispose();
if (_parentToRestore != null)
{
Activity.Current = _parentToRestore;
}
_disposed = true;
}
}

/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Loading

0 comments on commit 7a38d82

Please sign in to comment.