Skip to content

Commit

Permalink
Add XO.Console.Cli.Instrumentation with support for OpenTelemetry (#6)
Browse files Browse the repository at this point in the history
Provide middleware that wraps command execution in an Activity
  • Loading branch information
wjrogers authored Jun 22, 2023
1 parent 130b5fd commit c8c7e30
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Diagnostics;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;

namespace XO.Console.Cli.Instrumentation;

/// <summary>
/// An <see cref="ICommandApp"/> middleware that wraps command execution in an <see cref="Activity"/>.
/// </summary>
public sealed class CommandAppInstrumentationMiddleware : ICommandAppMiddleware
{
/// <summary>
/// The name of the <see cref="ActivitySource"/> for activities started by this middleware.
/// </summary>
public const string ActivitySourceName = ThisAssembly.AssemblyName;

private static readonly ActivitySource Source
= new(ActivitySourceName, version: ThisAssembly.AssemblyInformationalVersion);

private readonly string _defaultName;
private readonly CommandAppInstrumentationOptions _options;

/// <summary>
/// Initializes a new instance of <see cref="CommandAppInstrumentationMiddleware"/>.
/// </summary>
/// <param name="env">The hosting environment.</param>
/// <param name="optionsAccessor">The configuration options.</param>
public CommandAppInstrumentationMiddleware(IHostEnvironment env, IOptions<CommandAppInstrumentationOptions> optionsAccessor)
{
_defaultName = env.ApplicationName;
_options = optionsAccessor.Value;
}

/// <inheritdoc/>
public async Task<int> ExecuteAsync(ExecutorDelegate next, CommandContext context, CancellationToken cancellationToken)
{
int? result = null;
var name = String.Join(' ', from token in context.ParseResult.GetVerbs() select token.Value);
if (String.IsNullOrEmpty(name))
name = _defaultName;

var activity = Source.StartActivity(name, _options.ActivityKind);
try
{
if (activity?.IsAllDataRequested == true)
{
activity.AddTag(TraceSemanticConventions.AttributeCodeFunction, nameof(AsyncCommand.ExecuteAsync));
activity.AddTag(TraceSemanticConventions.AttributeCodeNamespace, context.Command.GetType().FullName);
_options.EnrichWithCommandContext?.Invoke(activity, context);
}

result = await next(context, cancellationToken)
.ConfigureAwait(false);

activity?.AddTag("command.exit_code", result);
}
catch (Exception ex)
{
if (activity?.IsAllDataRequested == true)
{
activity.RecordException(ex);
activity.SetStatus(Status.Error.WithDescription(ex.Message));
}
throw;
}
finally
{
activity?.Dispose();
}

return result.Value;
}
}
23 changes: 23 additions & 0 deletions XO.Console.Cli.Instrumentation/CommandAppInstrumentationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Diagnostics;

namespace XO.Console.Cli.Instrumentation;

/// <summary>
/// Configures OpenTelemetry instrumentation for <see cref="ICommandApp"/>.
/// </summary>
public sealed class CommandAppInstrumentationOptions
{
/// <summary>
/// The <see cref="System.Diagnostics.ActivityKind"/> to assign to command execution activities.
/// </summary>
/// <remarks>
/// Defaults to <see cref="ActivityKind.Producer"/>. Before changing the activity kind, check whether your
/// environment assigns special meaning to certain kinds.
/// </remarks>
public ActivityKind ActivityKind { get; set; } = ActivityKind.Producer;

/// <summary>
/// A delegate that enriches the <see cref="Activity"/> with information from the execution context.
/// </summary>
public Action<Activity, CommandContext>? EnrichWithCommandContext { get; set; }
}
35 changes: 35 additions & 0 deletions XO.Console.Cli.Instrumentation/TracerProviderBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.Extensions.DependencyInjection;
using XO.Console.Cli;
using XO.Console.Cli.Instrumentation;

namespace OpenTelemetry.Trace;

/// <summary>
/// <c>XO.Console.Cli</c> extension methods for <see cref="TracerProviderBuilder"/>.
/// </summary>
public static class TracerProviderBuilderExtensions
{
/// <summary>
/// Enables <see cref="ICommandApp"/> instrumentation.
/// </summary>
/// <param name="builder">The <see cref="TracerProviderBuilder"/> to configure.</param>
/// <param name="configure">A delegate that configures the <see cref="CommandAppInstrumentationOptions"/>.</param>
/// <returns>The same <see cref="TracerProviderBuilder"/> instance.</returns>
public static TracerProviderBuilder AddCommandAppInstrumentation(
this TracerProviderBuilder builder,
Action<CommandAppInstrumentationOptions>? configure = default)
{
return builder
.AddSource(CommandAppInstrumentationMiddleware.ActivitySourceName)
.ConfigureServices(services =>
{
var optionsBuilder = services.AddOptions<CommandAppInstrumentationOptions>();

if (configure != null)
optionsBuilder.Configure(configure);

services.AddCommandAppMiddleware<CommandAppInstrumentationMiddleware>();
})
;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Description>OpenTelemetry instrumentation for XO.Console.Cli</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry" Version="1.5.0" />
<PackageReference Include="OpenTelemetry.SemanticConventions" Version="1.0.0-rc9.9" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\XO.Console.Cli.Extensions\XO.Console.Cli.Extensions.csproj" />
<ProjectReference Include="..\XO.Console.Cli\XO.Console.Cli.csproj" />
</ItemGroup>

</Project>
199 changes: 199 additions & 0 deletions XO.Console.Cli.Instrumentation/packages.lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
{
"version": 1,
"dependencies": {
"net6.0": {
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Nerdbank.GitVersioning": {
"type": "Direct",
"requested": "[3.6.133, )",
"resolved": "3.6.133",
"contentHash": "VZWMd5YAeDxpjWjAP/X6bAxnRMiEf6tES/ITN0X5CHJgkWLLeHGmEALivmTAfYM6P+P/3Szy6VCITUAkqjcHVw=="
},
"OpenTelemetry": {
"type": "Direct",
"requested": "[1.5.0, )",
"resolved": "1.5.0",
"contentHash": "Fx44sVmnPkp/JJQSmUC1iHHWjWQ/lBh6wUIUK6SFeTYRdizn2/K/SaQNNy1dlf0ztpWTB6kfFD+xcjBYgdWPgg==",
"dependencies": {
"Microsoft.Extensions.Logging.Configuration": "3.1.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "1.5.0"
}
},
"OpenTelemetry.SemanticConventions": {
"type": "Direct",
"requested": "[1.0.0-rc9.9, )",
"resolved": "1.0.0-rc9.9",
"contentHash": "Ag1czlfURv4vthJtbHLWUMO7raVX8/IKlcZRd4RFE8CsBRniXT+wtRvh0RRULYyoTbchOey41U8NkPTIZJl7zg=="
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q=="
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "3.1.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "6.0.0"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "3.1.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg=="
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==",
"dependencies": {
"Microsoft.Extensions.Primitives": "6.0.0"
}
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "GcT5l2CYXL6Sa27KCSh0TixsRfADUgth+ojQSD5EkzisZxmGFh7CwzkcYuGwvmXLjr27uWRNrJ2vuuEjMhU05Q==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "6.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.FileProviders.Abstractions": "6.0.0"
}
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "3.1.0",
"Microsoft.Extensions.DependencyInjection": "3.1.0",
"Microsoft.Extensions.Logging.Abstractions": "3.1.0",
"Microsoft.Extensions.Options": "3.1.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA=="
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==",
"dependencies": {
"Microsoft.Extensions.Logging": "3.1.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.0"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0",
"Microsoft.Extensions.Primitives": "6.0.0"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "3.1.0",
"Microsoft.Extensions.Configuration.Binder": "3.1.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0",
"Microsoft.Extensions.Options": "3.1.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg=="
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.5.0",
"contentHash": "aAugEK9E+ono8I2Crjel78mrpEreJtcK1uzCYVooYELnSEPYytrzJYvw5SBxNizXT/qOBbz+EsfO+rkQfW7Mkg==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "7.0.0"
}
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.5.0",
"contentHash": "Wv28j71V1mizHjBfNx/ILhgqkXpbBZbCcZRoQMvqkF+Pp1bHHJpxOuipDXLqpO7KKHTwrTVl0/TAUHzoXXRK0g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0",
"OpenTelemetry.Api": "1.5.0"
}
},
"System.Diagnostics.DiagnosticSource": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "9W0ewWDuAyDqS2PigdTxk6jDKonfgscY/hP8hm7VpxYhNHZHKvZTdRckberlFk3VnCmr3xBUyMBut12Q+T2aOw==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"xo.console.cli": {
"type": "Project"
},
"xo.console.cli.extensions": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[6.0.0, )",
"Microsoft.Extensions.Options": "[6.0.0, )",
"XO.Console.Cli": "[1.0.0, )"
}
}
}
}
}
9 changes: 9 additions & 0 deletions XO.Console.Cli.Instrumentation/version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
"inherit": true,
"pathFilters": [
"../Directory.Build.props",
"../global.json",
"."
]
}
Loading

0 comments on commit c8c7e30

Please sign in to comment.