Skip to content

Commit 8837701

Browse files
committed
Create spans for parsing and invocation of S.CL commands
1 parent b7f0d1c commit 8837701

File tree

6 files changed

+132
-8
lines changed

6 files changed

+132
-8
lines changed

Directory.Packages.props

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
2525
<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
2626
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
27-
<PackageVersion Include="System.Memory" Version="4.5.4" />
27+
<PackageVersion Include="System.Memory" Version="4.5.5" />
28+
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
2829
<PackageVersion Include="system.reactive.core" Version="5.0.0" />
2930
</ItemGroup>
3031

src/System.CommandLine/Activities.cs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Diagnostics;
2+
3+
namespace System.CommandLine;
4+
5+
internal static class DiagnosticsStrings
6+
{
7+
internal const string LibraryNamespace = "System.CommandLine";
8+
internal const string ParseMethod = LibraryNamespace + ".Parse";
9+
internal const string InvokeMethod = LibraryNamespace + ".Invoke";
10+
internal const string InvokeType = nameof(InvokeType);
11+
internal const string Async = nameof(Async);
12+
internal const string Sync = nameof(Sync);
13+
internal const string ExitCode = nameof(ExitCode);
14+
internal const string Exception = nameof(Exception);
15+
internal const string Command = nameof(Command);
16+
}
17+
18+
internal static class Activities
19+
{
20+
internal static readonly ActivitySource ActivitySource = new ActivitySource(DiagnosticsStrings.LibraryNamespace);
21+
}
22+
23+
internal static class ActivityExtensions
24+
{
25+
26+
/// <summary>
27+
/// Walks up the command tree to get the build the command name by prepending the parent command names to the 'leaf' command name.
28+
/// </summary>
29+
/// <param name="commandResult"></param>
30+
/// <returns>The full command name, like 'dotnet package add'.</returns>
31+
internal static string FullCommandName(this Parsing.CommandResult commandResult)
32+
{
33+
var command = commandResult.Command;
34+
var path = command.Name;
35+
36+
while (commandResult.Parent is Parsing.CommandResult parent)
37+
{
38+
command = parent.Command;
39+
path = $"{command.Name} {path}";
40+
commandResult = parent;
41+
}
42+
43+
return path;
44+
}
45+
}

src/System.CommandLine/Invocation/InvocationPipeline.cs

+72-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Runtime.CompilerServices;
45
using System.Threading;
56
using System.Threading.Tasks;
67

@@ -10,8 +11,17 @@ internal static class InvocationPipeline
1011
{
1112
internal static async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken)
1213
{
14+
using var invokeActivity = Activities.ActivitySource.StartActivity(DiagnosticsStrings.InvokeMethod);
15+
if (invokeActivity is not null)
16+
{
17+
invokeActivity.DisplayName = parseResult.CommandResult.FullCommandName();
18+
invokeActivity.AddTag(DiagnosticsStrings.Command, parseResult.CommandResult.Command.Name);
19+
invokeActivity.AddTag(DiagnosticsStrings.InvokeType, DiagnosticsStrings.Async);
20+
}
21+
1322
if (parseResult.Action is null)
1423
{
24+
invokeActivity?.SetStatus(Diagnostics.ActivityStatusCode.Error);
1525
return ReturnCodeForMissingAction(parseResult);
1626
}
1727

@@ -41,7 +51,9 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
4151
switch (parseResult.Action)
4252
{
4353
case SynchronousCommandLineAction syncAction:
44-
return syncAction.Invoke(parseResult);
54+
var syncResult = syncAction.Invoke(parseResult);
55+
invokeActivity?.SetExitCode(syncResult);
56+
return syncResult;
4557

4658
case AsynchronousCommandLineAction asyncAction:
4759
var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token);
@@ -52,23 +64,30 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
5264

5365
if (terminationHandler is null)
5466
{
55-
return await startedInvocation;
67+
var asyncResult = await startedInvocation;
68+
invokeActivity?.SetExitCode(asyncResult);
69+
return asyncResult;
5670
}
5771
else
5872
{
5973
// Handlers may not implement cancellation.
6074
// In such cases, when CancelOnProcessTermination is configured and user presses Ctrl+C,
6175
// ProcessTerminationCompletionSource completes first, with the result equal to native exit code for given signal.
6276
Task<int> firstCompletedTask = await Task.WhenAny(startedInvocation, terminationHandler.ProcessTerminationCompletionSource.Task);
63-
return await firstCompletedTask; // return the result or propagate the exception
77+
var asyncResult = await firstCompletedTask; // return the result or propagate the exception
78+
invokeActivity?.SetExitCode(asyncResult);
79+
return asyncResult;
6480
}
6581

6682
default:
67-
throw new ArgumentOutOfRangeException(nameof(parseResult.Action));
83+
var error = new ArgumentOutOfRangeException(nameof(parseResult.Action));
84+
invokeActivity?.Error(error);
85+
throw error;
6886
}
6987
}
7088
catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler)
7189
{
90+
invokeActivity?.Error(ex);
7291
return DefaultExceptionHandler(ex, parseResult.Configuration);
7392
}
7493
finally
@@ -79,9 +98,18 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
7998

8099
internal static int Invoke(ParseResult parseResult)
81100
{
101+
using var invokeActivity = Activities.ActivitySource.StartActivity(DiagnosticsStrings.InvokeMethod);
102+
if (invokeActivity is not null)
103+
{
104+
invokeActivity.DisplayName = parseResult.CommandResult.FullCommandName();
105+
invokeActivity.AddTag(DiagnosticsStrings.Command, parseResult.CommandResult.Command.Name);
106+
invokeActivity.AddTag(DiagnosticsStrings.InvokeType, DiagnosticsStrings.Sync);
107+
}
108+
82109
switch (parseResult.Action)
83110
{
84111
case null:
112+
invokeActivity?.Error();
85113
return ReturnCodeForMissingAction(parseResult);
86114

87115
case SynchronousCommandLineAction syncAction:
@@ -112,15 +140,20 @@ internal static int Invoke(ParseResult parseResult)
112140
}
113141
}
114142

115-
return syncAction.Invoke(parseResult);
143+
var result = syncAction.Invoke(parseResult);
144+
invokeActivity?.SetExitCode(result);
145+
return result;
116146
}
117147
catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler)
118148
{
149+
invokeActivity?.Error(ex);
119150
return DefaultExceptionHandler(ex, parseResult.Configuration);
120151
}
121152

122153
default:
123-
throw new InvalidOperationException($"{nameof(AsynchronousCommandLineAction)} called within non-async invocation.");
154+
var error = new InvalidOperationException($"{nameof(AsynchronousCommandLineAction)} called within non-async invocation.");
155+
invokeActivity?.Error(error);
156+
throw error;
124157
}
125158
}
126159

@@ -150,5 +183,38 @@ private static int ReturnCodeForMissingAction(ParseResult parseResult)
150183
return 0;
151184
}
152185
}
186+
187+
private static void Succeed(this Diagnostics.Activity activity)
188+
{
189+
activity.SetStatus(Diagnostics.ActivityStatusCode.Ok);
190+
activity.AddTag(DiagnosticsStrings.ExitCode, 0);
191+
}
192+
private static void Error(this Diagnostics.Activity activity, int statusCode)
193+
{
194+
activity.SetStatus(Diagnostics.ActivityStatusCode.Error);
195+
activity.AddTag(DiagnosticsStrings.ExitCode, statusCode);
196+
}
197+
198+
private static void Error(this Diagnostics.Activity activity, Exception? exception = null)
199+
{
200+
activity.SetStatus(Diagnostics.ActivityStatusCode.Error);
201+
activity.AddTag(DiagnosticsStrings.ExitCode, 1);
202+
if (exception is not null)
203+
{
204+
activity.AddBaggage(DiagnosticsStrings.Exception, exception.ToString());
205+
}
206+
}
207+
208+
private static void SetExitCode(this Diagnostics.Activity activity, int exitCode)
209+
{
210+
if (exitCode == 0)
211+
{
212+
activity.Succeed();
213+
}
214+
else
215+
{
216+
activity.Error(exitCode);
217+
}
218+
}
153219
}
154220
}

src/System.CommandLine/ParseResult.cs

+1
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ public Task<int> InvokeAsync(CancellationToken cancellationToken = default)
236236
/// <returns>A value that can be used as a process exit code.</returns>
237237
public int Invoke()
238238
{
239+
239240
var useAsync = false;
240241

241242
if (Action is AsynchronousCommandLineAction)

src/System.CommandLine/Parsing/CommandLineParser.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ private static ParseResult Parse(
146146
throw new ArgumentNullException(nameof(arguments));
147147
}
148148

149+
using var parseActivity = Activities.ActivitySource.StartActivity(DiagnosticsStrings.ParseMethod);
150+
149151
configuration ??= new CommandLineConfiguration(command);
150152

151153
arguments.Tokenize(
@@ -160,7 +162,15 @@ private static ParseResult Parse(
160162
tokenizationErrors,
161163
rawInput);
162164

163-
return operation.Parse();
165+
var result = operation.Parse();
166+
parseActivity?.AddBaggage(DiagnosticsStrings.Command, result.CommandResult?.Command.Name);
167+
if (result.Errors.Count == 0)
168+
{
169+
parseActivity?.SetStatus(Diagnostics.ActivityStatusCode.Error);
170+
parseActivity?.AddBaggage("Errors", string.Join("\n", result.Errors.Select(e => e.Message)));
171+
172+
}
173+
return result;
164174
}
165175

166176
private enum Boundary

src/System.CommandLine/System.CommandLine.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
2929
<PackageReference Include="System.Memory" />
30+
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
3031
</ItemGroup>
3132

3233
<ItemGroup>

0 commit comments

Comments
 (0)