From 3fc11c2307e1af713fc34cb70bc3407b4bfb554c Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:05:09 +0300 Subject: [PATCH] Add instrumentation to search & show commands --- ...tExtensions.cs => CollectionExtensions.cs} | 11 ++- src/kiota/Extension/EnumerableExtensions.cs | 4 - src/kiota/Handlers/KiotaInfoCommandHandler.cs | 6 +- .../Handlers/KiotaSearchCommandHandler.cs | 59 +++++++++++++- src/kiota/Handlers/KiotaShowCommandHandler.cs | 76 +++++++++++++++++-- src/kiota/Telemetry/TelemetryLabels.cs | 2 + 6 files changed, 143 insertions(+), 15 deletions(-) rename src/kiota/Extension/{TagListExtensions.cs => CollectionExtensions.cs} (51%) diff --git a/src/kiota/Extension/TagListExtensions.cs b/src/kiota/Extension/CollectionExtensions.cs similarity index 51% rename from src/kiota/Extension/TagListExtensions.cs rename to src/kiota/Extension/CollectionExtensions.cs index 23bf6d06b4..fbcdc44c86 100644 --- a/src/kiota/Extension/TagListExtensions.cs +++ b/src/kiota/Extension/CollectionExtensions.cs @@ -2,11 +2,20 @@ namespace kiota.Extension; -internal static class TagListExtensions +internal static class CollectionExtensions { public static TagList AddAll(this TagList tagList, IEnumerable> tags) { foreach (var tag in tags) tagList.Add(tag); return tagList; } + + public static T[] OrEmpty(this T[]? source) + { + return source ?? []; + } + public static List OrEmpty(this List? source) + { + return source ?? []; + } } diff --git a/src/kiota/Extension/EnumerableExtensions.cs b/src/kiota/Extension/EnumerableExtensions.cs index a8bfc9a7af..fce59e4a48 100644 --- a/src/kiota/Extension/EnumerableExtensions.cs +++ b/src/kiota/Extension/EnumerableExtensions.cs @@ -13,8 +13,4 @@ public static IEnumerable OrEmpty(this IEnumerable? source) { return source ?? []; } - public static T[] OrEmpty(this T[]? source) - { - return source ?? []; - } } diff --git a/src/kiota/Handlers/KiotaInfoCommandHandler.cs b/src/kiota/Handlers/KiotaInfoCommandHandler.cs index 9f77bad124..29c063818d 100644 --- a/src/kiota/Handlers/KiotaInfoCommandHandler.cs +++ b/src/kiota/Handlers/KiotaInfoCommandHandler.cs @@ -5,16 +5,16 @@ using System.CommandLine.Rendering; using System.CommandLine.Rendering.Views; using System.Diagnostics; -using Kiota.Builder; -using Kiota.Builder.Configuration; using kiota.Extension; using kiota.Telemetry; +using Kiota.Builder; +using Kiota.Builder.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Writers; namespace kiota.Handlers; -internal class +internal class KiotaInfoCommandHandler : KiotaSearchBasedCommandHandler { private readonly KeyValuePair[] _commonTags = diff --git a/src/kiota/Handlers/KiotaSearchCommandHandler.cs b/src/kiota/Handlers/KiotaSearchCommandHandler.cs index b559ec257e..52b9d900da 100644 --- a/src/kiota/Handlers/KiotaSearchCommandHandler.cs +++ b/src/kiota/Handlers/KiotaSearchCommandHandler.cs @@ -1,17 +1,27 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; using System.CommandLine.IO; using System.CommandLine.Rendering; using System.CommandLine.Rendering.Views; +using System.Diagnostics; using System.Text.Json; using Kiota.Builder; using Kiota.Builder.SearchProviders; +using kiota.Extension; +using kiota.Telemetry; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaSearchCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "search"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Argument SearchTermArgument { get; init; @@ -26,11 +36,33 @@ public required Option VersionOption } public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options string searchTerm = context.ParseResult.GetValueForArgument(SearchTermArgument); - string version = context.ParseResult.GetValueForOption(VersionOption) ?? string.Empty; + string? version0 = context.ParseResult.GetValueForOption(VersionOption); bool clearCache = context.ParseResult.GetValueForOption(ClearCacheOption); + var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?; CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + var host = context.GetHost(); + var instrumentation = host.Services.GetService(); + var activitySource = instrumentation?.ActivitySource; + + CreateTelemetryTags(activitySource, version0, clearCache, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanSearchCommand, + startTime: startTime, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + // Command duration meter + var meterRuntime = instrumentation?.CreateCommandDurationHistogram(); + if (meterRuntime is null) stopwatch = null; + // Add this run to the command execution counter + var tl = new TagList(_commonTags.AsSpan()).AddAll(tags.OrEmpty()); + instrumentation?.CreateCommandExecutionCounter().Add(1, tl); + + string version = version0.OrEmpty(); Configuration.Search.ClearCache = clearCache; @@ -45,10 +77,13 @@ public override async Task InvokeAsync(InvocationContext context) var searcher = await GetKiotaSearcherAsync(loggerFactory, cancellationToken).ConfigureAwait(false); var results = await searcher.SearchAsync(searchTerm, version, cancellationToken); await DisplayResultsAsync(searchTerm, version, results, logger, cancellationToken); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error searching for a description: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -57,6 +92,10 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } private async Task DisplayResultsAsync(string searchTerm, string version, IDictionary results, ILogger logger, CancellationToken cancellationToken) @@ -91,6 +130,24 @@ private async Task DisplayResultsAsync(string searchTerm, string version, IDicti DisplaySearchAddHint(); } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, string? version, bool clearCache, + LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + tags = activitySource?.HasListeners() == true ? new List>(7) + { + // Search term is required + new($"{TelemetryLabels.TagCommandParams}.search_term", redacted), + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + } : null; + + if (version is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.version", redacted)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } + private const int MaxDescriptionLength = 70; private static string ShortenDescription(string description) { diff --git a/src/kiota/Handlers/KiotaShowCommandHandler.cs b/src/kiota/Handlers/KiotaShowCommandHandler.cs index e268ab55bf..49a51446fb 100644 --- a/src/kiota/Handlers/KiotaShowCommandHandler.cs +++ b/src/kiota/Handlers/KiotaShowCommandHandler.cs @@ -1,14 +1,24 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Extensions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Services; namespace kiota.Handlers; internal class KiotaShowCommandHandler : KiotaSearchBasedCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "show"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option DescriptionOption { get; init; @@ -48,17 +58,44 @@ public required Option DisableSSLValidationOption public override async Task InvokeAsync(InvocationContext context) { - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; - string manifest = context.ParseResult.GetValueForOption(ManifestOption) ?? string.Empty; - string searchTerm = context.ParseResult.GetValueForOption(SearchTermOption) ?? string.Empty; - string version = context.ParseResult.GetValueForOption(VersionOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? openapi0 = context.ParseResult.GetValueForOption(DescriptionOption); + string? manifest0 = context.ParseResult.GetValueForOption(ManifestOption); + string? searchTerm0 = context.ParseResult.GetValueForOption(SearchTermOption); + string? version0 = context.ParseResult.GetValueForOption(VersionOption); uint maxDepth = context.ParseResult.GetValueForOption(MaxDepthOption); - List includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? new List(); - List excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? new List(); + List? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption); + List? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption); bool clearCache = context.ParseResult.GetValueForOption(ClearCacheOption); bool disableSSLValidation = context.ParseResult.GetValueForOption(DisableSSLValidationOption); + var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?; CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; + var host = context.GetHost(); + var instrumentation = host.Services.GetService(); + var activitySource = instrumentation?.ActivitySource; + + CreateTelemetryTags(activitySource, searchTerm0, version0, clearCache, includePatterns0, excludePatterns0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanShowCommand, + startTime: startTime, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + // Command duration meter + var meterRuntime = instrumentation?.CreateCommandDurationHistogram(); + if (meterRuntime is null) stopwatch = null; + // Add this run to the command execution counter + var tl = new TagList(_commonTags.AsSpan()).AddAll(tags.OrEmpty()); + instrumentation?.CreateCommandExecutionCounter().Add(1, tl); + + string openapi = openapi0.OrEmpty(); + string manifest = manifest0.OrEmpty(); + string searchTerm = searchTerm0.OrEmpty(); + string version = version0.OrEmpty(); + var includePatterns = includePatterns0.OrEmpty(); + var excludePatterns = excludePatterns0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context); Configuration.Search.ClearCache = clearCache; @@ -103,6 +140,8 @@ public override async Task InvokeAsync(InvocationContext context) } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error showing the description: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -111,8 +150,13 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } private const string Cross = " ├─"; @@ -152,4 +196,24 @@ private static void RenderChildNode(OpenApiUrlTreeNode node, uint maxDepth, Stri RenderNode(node, maxDepth, builder, indent, nodeDepth + 1); } + + private static void CreateTelemetryTags(ActivitySource? activitySource, string? searchTerm, string? version, + bool clearCache, List? includePatterns, List? excludePatterns, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + tags = activitySource?.HasListeners() == true ? new List>(7) + { + new($"{TelemetryLabels.TagCommandParams}.openapi", redacted), + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + new($"{TelemetryLabels.TagCommandParams}.max_depth", redacted), + } : null; + + if (searchTerm is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.search_key", redacted)); + if (version is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.version", redacted)); + if (includePatterns is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.include_path", redacted)); + if (excludePatterns is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.exclude_path", redacted)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Telemetry/TelemetryLabels.cs b/src/kiota/Telemetry/TelemetryLabels.cs index 0f4b9a9a13..0713f5a71b 100644 --- a/src/kiota/Telemetry/TelemetryLabels.cs +++ b/src/kiota/Telemetry/TelemetryLabels.cs @@ -37,4 +37,6 @@ public static class TelemetryLabels public const string SpanGitHubLogoutCommand = "Logout/GitHub InvokeAsync()"; public const string SpanGitHubPatLoginCommand = "Login/GitHub/Pat InvokeAsync()"; public const string SpanInfoCommand = "Info InvokeAsync()"; + public const string SpanSearchCommand = "Search InvokeAsync()"; + public const string SpanShowCommand = "Show InvokeAsync()"; }