Skip to content

Create System.Diagnostic.Activity spans for parsing and invocation of S.CL commands #2529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

baronfel
Copy link
Member

@baronfel baronfel commented Mar 13, 2025

Based on a chat with @maddymontaquila we think this would be useful :)

Tried to follow some practices I saw in the BCL around defining per-use-case Activities and an Activity Source per-library, otherwise totally open to suggestions here.

Activity Source Name: System.CommandLine

Parse Activity:

  • Name: System.CommandLine.Parse
  • Status: Ok if parsing succeeded, Error if any parse errors were detected
  • Tags:
    • Command - the name of the command that ended up being parsed
  • Baggage:
    • Errors - newline-delimited list of parse error message strings

Invoke Activity:

  • Name: System.CommandLine.Invoke
  • Status: Ok if invocation returned a 0 exit code, Error if any nonzero exit code was returned, or if an exception was thrown during invocation
  • Tags:
    • Command - the name of the command that was invoked
    • InvokeType - either Async or Sync based on which kind of method was used
    • ExitCode - the exit code returned
  • Baggage:
    • Exception - the rendered exception if one was thrown during invocation

Copy link

@KalleOlaviNiemitalo KalleOlaviNiemitalo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new System.Diagnostics.DiagnosticSource package dependency on .NET Standard is a bit annoying.

How do you expect these diagnostics to be processed; what's the scenario? If the application references a telemetry library and initialises it before System.CommandLine, then the diagnostics can be sent out of process, over OTLP or such; but the endpoint for the data would have to be configured with files or environment variables as the command line has not been parsed yet. I think it unlikely that interactive users would set those environment variables. They can be configured in containers but there the diagnostics seem less valuable as the command line will likely be constant across executions.

activity.AddTag(DiagnosticsStrings.ExitCode, 1);
if (exception is not null)
{
activity.AddBaggage(DiagnosticsStrings.Exception, exception.ToString());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activity.AddException from dotnet/runtime#102905 is in System.Diagnostics.DiagnosticSource 9.0.0 or greater, thus not in the 8.0.1 LTS that you specify in Directory.Packages.props, but do you expect to release System.CommandLine before .NET 10 LTS?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prompted me to file dotnet/dotnet-api-docs#11091

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose 8.0 because this repo currently targets net8.0 and I wanted to keep alignment with that, that's all. If we target 10 before .NET 10 releases, I would be happy to use that. I'd also love to follow any guidance from the BCL around tags vs baggage vs events, etc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonsequitur Just chiming in to say if System.CommandLine goes public for .NET 10, it would be great if the versioning aligned with all out-of-box packages and be package versioned at 10.0.0, and match the same AssemblyVersion schema as other out-of-box packages to make binding rules simpler.

@@ -236,6 +236,7 @@ public Task<int> InvokeAsync(CancellationToken cancellationToken = default)
/// <returns>A value that can be used as a process exit code.</returns>
public int Invoke()
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious addition

if (invokeActivity is not null)
{
invokeActivity.DisplayName = parseResult.CommandResult.FullCommandName();
invokeActivity.AddTag(DiagnosticsStrings.Command, parseResult.CommandResult.Command.Name);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag value could be an array of strings

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see that, but I'm not sure how different transports would serialize the data.

@KalleOlaviNiemitalo
Copy link

Create spans for parsing and invocation of S.CL commands

I initially thought this was about method overloads with ReadOnlySpan<char> parameters. Would be good to mention "diagnostic" or "Activity" in case the PR title is ever listed in release notes.

@KalleOlaviNiemitalo
Copy link

InvokeType - either Async or Sync based on which kind of method was used

I don't see InvokeType in the diff.

@baronfel
Copy link
Member Author

Sorry @KalleOlaviNiemitalo - I missed a commit. I've pushed it up now!

@baronfel baronfel changed the title Create spans for parsing and invocation of S.CL commands Create System.Diagnostic.Activity spans for parsing and invocation of S.CL commands Mar 14, 2025
@baronfel
Copy link
Member Author

I see this as being useful for local testing as well as just a general good guideline for libraries in modern .NET - observability of applications is important, and knowing what your libraries are doing in semantic sections is useful. I did basically the equivalent of this in a one-off on the dotnet CLI to prove a point about the behavior of the CLI for workloads downloading, and it was kind of annoying that I had to make my own spans for parsing + invocation to show the 'full' lifecycle of the app.

@KalleOlaviNiemitalo
Copy link

The names of tags and baggage are Capitalized in the PR description but lowercase in the implementation.

I feel uneasy about this use of baggage. Baggage is inherited by child activities. I'm not sure inheriting exceptions makes sense. Could be ActivityEvent instead.

@baronfel
Copy link
Member Author

I like the idea of activity events - do you know of any docs or posts I could read to learn more about their intended use?

@KalleOlaviNiemitalo
Copy link

@baronfel
Copy link
Member Author

hey @dotnet/source-build I've introduced a prebuilt for System.Diagnostics.DiagnosticSource.8.0.1 - is it ok to just add this into the baselines?

@mthalman
Copy link
Member

hey @dotnet/source-build I've introduced a prebuilt for System.Diagnostics.DiagnosticSource.8.0.1 - is it ok to just add this into the baselines?

No, it can't be added to the baseline. That would only be possible if it could rely on the latest version of that package being flowed in by the runtime repo in the VMR build. But command-line-api builds before runtime, so it relies on SBRP or the previously source built packages. So that wouldn't have 8.0.1. I see that 8.0.0 already exists in SBRP. Can you use that version instead? Otherwise, you'll need to add it to SBRP.

@baronfel
Copy link
Member Author

I probably can - I was defaulting to a principle of "keeping patched" assuming that there was some reason to not go back. I'll roll the version back :)

@mthalman
Copy link
Member

I see you still had a prebuilt for 8.0.0.

My bad. I didn't realize the main branch here was consuming the packages from SBRP's release/8.0 branch. So yeah, that version doesn't exist there. Nor can it exist there; you can't have an 8.0 package in the 8.0 SBRP branch, only lower versions are allowed. But that shouldn't be a problem.

In which .NET versions will this change flow to? Will it only be 10.0? In that case, then things are already setup in SBRP to have the necessary 8.0.0 version in which case the prebuilt baseline can be updated to include that version.

@baronfel
Copy link
Member Author

That's a good question - I'm having darc auth issues so I can't check easily, but it should just be flowing to main branches for all the repos, so .NET 10 only.

@mthalman
Copy link
Member

Yes, darc shows flow is just going to main branch. So updating the prebuilt baseline will resolve this.

Comment on lines +7 to +16
internal const string LibraryNamespace = "System.CommandLine";
internal const string ParseMethod = LibraryNamespace + ".Parse";
internal const string InvokeMethod = LibraryNamespace + ".Invoke";
internal const string InvokeType = "invoke.type";
internal const string Async = "async";
internal const string Sync = "sync";
internal const string ExitCode = "exitcode";
internal const string Exception = "exception";
internal const string Errors = "errors";
internal const string Command = "command";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand OpenTelemetry Recommendations for Application Developers and Signal-specific Attributes correctly, the attribute names (tag names in .NET API) should preferably be namespaced even if the namespace is already in new ActivitySource(DiagnosticsStrings.LibraryNamespace) (instrumentation scope) and in the StartActivity calls (span name).

DiagnosticsStrings.InvokeType semantics look specific to this library. DiagnosticsStrings.Async and DiagnosticsStrings.Sync are used only as attribute values and don't need a namespace.

For DiagnosticsStrings.ExitCode, one could perhaps use process.exit.code from OpenTelemetry Semantic Conventions. But I'm not sure it is correct to use that when the process has not exited yet and could even prompt the user for more commands to parse and execute.

For DiagnosticsStrings.Exception, there is the Exception specification in OpenTelemetry Semantic Conventions. There, the event name is "exception", and the event may have tags "exception.message" (Exception.Message in .NET) and "exception.stacktrace" (Exception.ToString() in .NET, rather than Exception.StackTrace), among others.

For DiagnosticsStrings.Errors, see Recording errors on spans. I think it means the library should do activity.AddTag("error.type", "System.CommandLine.Parsing.ParseError") and activity.StatusDescription = string.Join(…), rather than add baggage.

For DiagnosticsStrings.Command, the semantics don't match any of process.command, process.command_args, or process.command_line in OpenTelemetry Semantic Conventions. Nothing suitable in Semantic conventions for CLI (command line interface) programs either.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Event Basics and open-telemetry/opentelemetry-specification#4430, OpenTelemetry intends to deprecate Span Events and recommend Events in a log instead. Which means in .NET that exceptions would be written via Microsoft.Extensions.Logging.ILogger rather than System.Diagnostics.ActivityEvent.

In .NET though, ActivityEvent has not been deprecated. I think it's okay to use in this PR.

Comment on lines +24 to +32
internal static class ActivityExtensions
{

/// <summary>
/// Walks up the command tree to get the build the command name by prepending the parent command names to the 'leaf' command name.
/// </summary>
/// <param name="commandResult"></param>
/// <returns>The full command name, like 'dotnet package add'.</returns>
internal static string FullCommandName(this Parsing.CommandResult commandResult)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit wonky that ActivityExtensions defines an extension method for CommandResult rather than for Activity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants