Skip to content

Commit c6097ae

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

File tree

6 files changed

+124
-8
lines changed

6 files changed

+124
-8
lines changed

Diff for: 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

Diff for: src/System.CommandLine/Activities.cs

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

Diff for: src/System.CommandLine/Invocation/InvocationPipeline.cs

+70-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,16 @@ 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+
}
20+
1321
if (parseResult.Action is null)
1422
{
23+
invokeActivity?.SetStatus(Diagnostics.ActivityStatusCode.Error);
1524
return ReturnCodeForMissingAction(parseResult);
1625
}
1726

@@ -41,7 +50,9 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
4150
switch (parseResult.Action)
4251
{
4352
case SynchronousCommandLineAction syncAction:
44-
return syncAction.Invoke(parseResult);
53+
var syncResult = syncAction.Invoke(parseResult);
54+
invokeActivity?.SetExitCode(syncResult);
55+
return syncResult;
4556

4657
case AsynchronousCommandLineAction asyncAction:
4758
var startedInvocation = asyncAction.InvokeAsync(parseResult, cts.Token);
@@ -52,23 +63,30 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
5263

5364
if (terminationHandler is null)
5465
{
55-
return await startedInvocation;
66+
var asyncResult = await startedInvocation;
67+
invokeActivity?.SetExitCode(asyncResult);
68+
return asyncResult;
5669
}
5770
else
5871
{
5972
// Handlers may not implement cancellation.
6073
// In such cases, when CancelOnProcessTermination is configured and user presses Ctrl+C,
6174
// ProcessTerminationCompletionSource completes first, with the result equal to native exit code for given signal.
6275
Task<int> firstCompletedTask = await Task.WhenAny(startedInvocation, terminationHandler.ProcessTerminationCompletionSource.Task);
63-
return await firstCompletedTask; // return the result or propagate the exception
76+
var asyncResult = await firstCompletedTask; // return the result or propagate the exception
77+
invokeActivity?.SetExitCode(asyncResult);
78+
return asyncResult;
6479
}
6580

6681
default:
67-
throw new ArgumentOutOfRangeException(nameof(parseResult.Action));
82+
var error = new ArgumentOutOfRangeException(nameof(parseResult.Action));
83+
invokeActivity?.Error(error);
84+
throw error;
6885
}
6986
}
7087
catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler)
7188
{
89+
invokeActivity?.Error(ex);
7290
return DefaultExceptionHandler(ex, parseResult.Configuration);
7391
}
7492
finally
@@ -79,9 +97,17 @@ internal static async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
7997

8098
internal static int Invoke(ParseResult parseResult)
8199
{
100+
using var invokeActivity = Activities.ActivitySource.StartActivity(DiagnosticsStrings.InvokeMethod);
101+
if (invokeActivity is not null)
102+
{
103+
invokeActivity.DisplayName = parseResult.CommandResult.FullCommandName();
104+
invokeActivity.AddTag(DiagnosticsStrings.Command, parseResult.CommandResult.Command.Name);
105+
}
106+
82107
switch (parseResult.Action)
83108
{
84109
case null:
110+
invokeActivity?.Error();
85111
return ReturnCodeForMissingAction(parseResult);
86112

87113
case SynchronousCommandLineAction syncAction:
@@ -112,15 +138,20 @@ internal static int Invoke(ParseResult parseResult)
112138
}
113139
}
114140

115-
return syncAction.Invoke(parseResult);
141+
var result = syncAction.Invoke(parseResult);
142+
invokeActivity?.SetExitCode(result);
143+
return result;
116144
}
117145
catch (Exception ex) when (parseResult.Configuration.EnableDefaultExceptionHandler)
118146
{
147+
invokeActivity?.Error(ex);
119148
return DefaultExceptionHandler(ex, parseResult.Configuration);
120149
}
121150

122151
default:
123-
throw new InvalidOperationException($"{nameof(AsynchronousCommandLineAction)} called within non-async invocation.");
152+
var error = new InvalidOperationException($"{nameof(AsynchronousCommandLineAction)} called within non-async invocation.");
153+
invokeActivity?.Error(error);
154+
throw error;
124155
}
125156
}
126157

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

Diff for: 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)

Diff for: src/System.CommandLine/Parsing/CommandLineParser.cs

+8-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,12 @@ private static ParseResult Parse(
160162
tokenizationErrors,
161163
rawInput);
162164

163-
return operation.Parse();
165+
var result = operation.Parse();
166+
if (result.Errors.Count == 0)
167+
{
168+
parseActivity?.SetStatus(Diagnostics.ActivityStatusCode.Error);
169+
}
170+
return result;
164171
}
165172

166173
private enum Boundary

Diff for: 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)