-
Notifications
You must be signed in to change notification settings - Fork 395
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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()); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() | |||
{ | |||
|
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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. |
I don't see InvokeType in the diff. |
Sorry @KalleOlaviNiemitalo - I missed a commit. I've pushed it up now! |
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. |
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. |
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? |
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. |
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 :) |
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. |
That's a good question - I'm having darc auth issues so I can't check easily, but it should just be flowing to |
Yes, darc shows flow is just going to main branch. So updating the prebuilt baseline will resolve this. |
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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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
.
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:System.CommandLine.Parse
Command
- the name of the command that ended up being parsedErrors
- newline-delimited list of parse error message stringsInvoke
Activity:System.CommandLine.Invoke
Command
- the name of the command that was invokedInvokeType
- eitherAsync
orSync
based on which kind of method was usedExitCode
- the exit code returnedException
- the rendered exception if one was thrown during invocation