diff --git a/.editorconfig b/.editorconfig index 53dfe75c93..efb65160b2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -136,4 +136,7 @@ dotnet_diagnostic.CA1302.severity = none dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1720.severity = none dotnet_diagnostic.CA2007.severity = none -dotnet_diagnostic.CA2227.severity = none \ No newline at end of file +dotnet_diagnostic.CA2227.severity = none + +[*.csproj] +indent_size = 2 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3fd5cefbd5..09ce5ffc86 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,10 @@ updates: kiota-dependencies: patterns: - "*kiota*" + OpenTelemetry: + patterns: + - "OpenTelemetry.*" + - "Azure.Monitor.OpenTelemetry.Exporter" - package-ecosystem: github-actions directory: "/" schedule: diff --git a/src/kiota/Extension/CollectionExtensions.cs b/src/kiota/Extension/CollectionExtensions.cs new file mode 100644 index 0000000000..fbcdc44c86 --- /dev/null +++ b/src/kiota/Extension/CollectionExtensions.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace kiota.Extension; + +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 new file mode 100644 index 0000000000..fce59e4a48 --- /dev/null +++ b/src/kiota/Extension/EnumerableExtensions.cs @@ -0,0 +1,16 @@ +namespace kiota.Extension; + +internal static class EnumerableExtensions +{ + public static IEnumerable? ConcatNullable(this IEnumerable? left, IEnumerable? right) + { + if (left is not null && right is not null) return left.Concat(right); + // At this point, either left is null, right is null or both are null + return left ?? right; + } + + public static IEnumerable OrEmpty(this IEnumerable? source) + { + return source ?? []; + } +} diff --git a/src/kiota/Extension/KiotaHostExtensions.cs b/src/kiota/Extension/KiotaHostExtensions.cs new file mode 100644 index 0000000000..47bd3ae25f --- /dev/null +++ b/src/kiota/Extension/KiotaHostExtensions.cs @@ -0,0 +1,92 @@ +using Azure.Monitor.OpenTelemetry.Exporter; +using kiota.Telemetry; +using kiota.Telemetry.Config; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace kiota.Extension; + +internal static class KiotaHostExtensions +{ + internal static IHostBuilder ConfigureKiotaTelemetryServices(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices(ConfigureServiceContainer); + + static void ConfigureServiceContainer(HostBuilderContext context, IServiceCollection services) + { + TelemetryConfig cfg = new(); + var section = context.Configuration.GetSection(TelemetryConfig.ConfigSectionKey); + section.Bind(cfg); + if (!cfg.Disabled) + { + // Only register if telemetry is enabled. + var openTelemetryBuilder = services.AddOpenTelemetry() + .ConfigureResource(static r => + { + r.AddService(serviceName: "kiota", + serviceNamespace: "microsoft.openapi", + serviceVersion: Kiota.Generated.KiotaVersion.Current()); + if (OsName() is { } osName) + { + r.AddAttributes([ + new KeyValuePair("os.name", osName), + new KeyValuePair("os.version", Environment.OSVersion.Version.ToString()) + ]); + } + }); + openTelemetryBuilder.WithMetrics(static mp => + { + mp.AddMeter($"{TelemetryLabels.ScopeName}*") + .AddHttpClientInstrumentation() + // Decide if runtime metrics are useful + .AddRuntimeInstrumentation() + .SetExemplarFilter(ExemplarFilterType.TraceBased); + }) + .WithTracing(static tp => + { + tp.AddSource($"{TelemetryLabels.ScopeName}*") + .AddHttpClientInstrumentation(); + }); + if (cfg.OpenTelemetry.Enabled) + { + // Only register OpenTelemetry exporter if enabled. + Action exporterOpts = op => + { + if (!string.IsNullOrWhiteSpace(cfg.OpenTelemetry.EndpointAddress)) + { + op.Endpoint = new Uri(cfg.OpenTelemetry.EndpointAddress); + } + }; + openTelemetryBuilder + .WithMetrics(mp => mp.AddOtlpExporter(exporterOpts)) + .WithTracing(tp => tp.AddOtlpExporter(exporterOpts)); + } + if (cfg.AppInsights.Enabled && !string.IsNullOrWhiteSpace(cfg.AppInsights.ConnectionString)) + { + // Only register app insights exporter if it's enabled and we have a connection string. + Action azureMonitorExporterOptions = options => + { + options.ConnectionString = cfg.AppInsights.ConnectionString; + }; + openTelemetryBuilder + .WithMetrics(mp => mp.AddAzureMonitorMetricExporter(azureMonitorExporterOptions)) + .WithTracing(tp => tp.AddAzureMonitorTraceExporter(azureMonitorExporterOptions)); + } + services.AddSingleton(); + } + } + static string? OsName() + { + if (OperatingSystem.IsWindows()) return "windows"; + if (OperatingSystem.IsLinux()) return "linux"; + if (OperatingSystem.IsMacOS()) return "macos"; + + return OperatingSystem.IsFreeBSD() ? "freebsd" : null; + } + } +} diff --git a/src/kiota/Extension/StringExtensions.cs b/src/kiota/Extension/StringExtensions.cs new file mode 100644 index 0000000000..af34cbe234 --- /dev/null +++ b/src/kiota/Extension/StringExtensions.cs @@ -0,0 +1,10 @@ +namespace kiota.Extension; + +internal static class StringExtensions +{ + public static string OrEmpty(this string? source) + { + // Investigate if using spans instead of strings helps perf. i.e. source?.AsSpan() ?? ReadOnlySpan.Empty + return source ?? string.Empty; + } +} diff --git a/src/kiota/Handlers/BaseKiotaCommandHandler.cs b/src/kiota/Handlers/BaseKiotaCommandHandler.cs index a7b857b22a..0d5fe2685f 100644 --- a/src/kiota/Handlers/BaseKiotaCommandHandler.cs +++ b/src/kiota/Handlers/BaseKiotaCommandHandler.cs @@ -109,7 +109,7 @@ protected async Task GetKiotaSearcherAsync(ILoggerFactory loggerF var isPatSignedIn = await patSignInCallBack(cancellationToken).ConfigureAwait(false); var (provider, callback) = (isDeviceCodeSignedIn, isPatSignedIn) switch { - (true, _) => (GetGitHubAuthenticationProvider(logger), deviceCodeSignInCallback), + (true, _) => ((IAuthenticationProvider?)GetGitHubAuthenticationProvider(logger), deviceCodeSignInCallback), (_, true) => (GetGitHubPatAuthenticationProvider(logger), patSignInCallBack), (_, _) => (null, (CancellationToken cts) => Task.FromResult(false)) }; @@ -158,7 +158,7 @@ protected static string GetAbsolutePath(string source) return string.Empty; return Path.IsPathRooted(source) || source.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? source : NormalizeSlashesInPath(Path.Combine(Directory.GetCurrentDirectory(), source)); } - protected void AssignIfNotNullOrEmpty(string input, Action assignment) + protected void AssignIfNotNullOrEmpty(string? input, Action assignment) { if (!string.IsNullOrEmpty(input)) assignment.Invoke(Configuration.Generation, input); diff --git a/src/kiota/Handlers/Client/AddHandler.cs b/src/kiota/Handlers/Client/AddHandler.cs index beec8f8058..a96169cbef 100644 --- a/src/kiota/Handlers/Client/AddHandler.cs +++ b/src/kiota/Handlers/Client/AddHandler.cs @@ -1,17 +1,28 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Client; internal class AddHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "add"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -72,21 +83,48 @@ public required Option SkipGenerationOption public override async Task InvokeAsync(InvocationContext context) { - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? output = context.ParseResult.GetValueForOption(OutputOption); GenerationLanguage language = context.ParseResult.GetValueForOption(LanguageOption); AccessModifier typeAccessModifier = context.ParseResult.GetValueForOption(TypeAccessModifierOption); - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + string? openapi = context.ParseResult.GetValueForOption(DescriptionOption); bool backingStore = context.ParseResult.GetValueForOption(BackingStoreOption); bool excludeBackwardCompatible = context.ParseResult.GetValueForOption(ExcludeBackwardCompatibleOption); bool includeAdditionalData = context.ParseResult.GetValueForOption(AdditionalDataOption); bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; - string namespaceName = context.ParseResult.GetValueForOption(NamespaceOption) ?? string.Empty; - List includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? []; - List excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? []; - List disabledValidationRules = context.ParseResult.GetValueForOption(DisabledValidationRulesOption) ?? []; - List structuredMimeTypes = context.ParseResult.GetValueForOption(StructuredMimeTypesOption) ?? []; + string? className = context.ParseResult.GetValueForOption(ClassOption); + string? namespaceName = context.ParseResult.GetValueForOption(NamespaceOption); + List? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption); + List? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption); + List? disabledValidationRules0 = context.ParseResult.GetValueForOption(DisabledValidationRulesOption); + List? structuredMimeTypes0 = context.ParseResult.GetValueForOption(StructuredMimeTypesOption); + 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, language, backingStore, excludeBackwardCompatible, skipGeneration, output, + namespaceName, includePatterns0, excludePatterns0, structuredMimeTypes0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanAddClientCommand, + 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); + + List includePatterns = includePatterns0.OrEmpty(); + List excludePatterns = excludePatterns0.OrEmpty(); + List disabledValidationRules = disabledValidationRules0.OrEmpty(); + List structuredMimeTypes = structuredMimeTypes0.OrEmpty(); AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s); @@ -131,6 +169,14 @@ public override async Task InvokeAsync(InvocationContext context) { DisplaySuccess("Generation completed successfully"); DisplayUrlInformation(Configuration.Generation.ApiRootUrl); + var genCounter = instrumentation?.CreateClientGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorLanguage, + Configuration.Generation.Language.ToString("G")) + }; + genCounter?.Add(1, meterTags); } else if (skipGeneration) { @@ -140,10 +186,13 @@ public override async Task InvokeAsync(InvocationContext context) var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; DisplayInfoHint(language, string.Empty, manifestPath); DisplayGenerateAdvancedHint(includePatterns, excludePatterns, string.Empty, manifestPath, "client add"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -152,6 +201,32 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, GenerationLanguage language, bool backingStore, + bool excludeBackwardCompatible, bool skipGeneration, string? output, string? namespaceName, + List? includePatterns, List? excludePatterns, List? structuredMimeTypes, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(10) + { + new(TelemetryLabels.TagGeneratorLanguage, language.ToString("G")), + new($"{TelemetryLabels.TagCommandParams}.backing_store", backingStore), + new($"{TelemetryLabels.TagCommandParams}.exclude_backward_compatible", excludeBackwardCompatible), + new($"{TelemetryLabels.TagCommandParams}.skip_generation", skipGeneration), + } : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (namespaceName is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.namespace", 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 (structuredMimeTypes is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.structured_media_types", structuredMimeTypes.ToArray())); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Client/EditHandler.cs b/src/kiota/Handlers/Client/EditHandler.cs index 5c2bee3dfb..cdd266ed5b 100644 --- a/src/kiota/Handlers/Client/EditHandler.cs +++ b/src/kiota/Handlers/Client/EditHandler.cs @@ -1,17 +1,28 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Client; internal class EditHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "edit"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -72,25 +83,50 @@ public required Option SkipGenerationOption public override async Task InvokeAsync(InvocationContext context) { - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? output0 = context.ParseResult.GetValueForOption(OutputOption); GenerationLanguage? language = context.ParseResult.GetValueForOption(LanguageOption); AccessModifier? typeAccessModifier = context.ParseResult.GetValueForOption(TypeAccessModifierOption); - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + string? openapi0 = context.ParseResult.GetValueForOption(DescriptionOption); bool? backingStore = context.ParseResult.GetValueForOption(BackingStoreOption); bool? excludeBackwardCompatible = context.ParseResult.GetValueForOption(ExcludeBackwardCompatibleOption); bool? includeAdditionalData = context.ParseResult.GetValueForOption(AdditionalDataOption); bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; - string namespaceName = context.ParseResult.GetValueForOption(NamespaceOption) ?? string.Empty; + string? className0 = context.ParseResult.GetValueForOption(ClassOption); + string? namespaceName0 = context.ParseResult.GetValueForOption(NamespaceOption); List? includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption); List? excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption); List? disabledValidationRules = context.ParseResult.GetValueForOption(DisabledValidationRulesOption); List? structuredMimeTypes = context.ParseResult.GetValueForOption(StructuredMimeTypesOption); + 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, language, backingStore, excludeBackwardCompatible, skipGeneration, output0, + namespaceName0, includePatterns, excludePatterns, structuredMimeTypes, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanEditClientCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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); + Configuration.Generation.SkipGeneration = skipGeneration; Configuration.Generation.Operation = ConsumerOperation.Edit; + string output = output0.OrEmpty(); + string openapi = openapi0.OrEmpty(); + string className = className0.OrEmpty(); + string namespaceName = namespaceName0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); using (loggerFactory) { @@ -147,6 +183,14 @@ public override async Task InvokeAsync(InvocationContext context) { DisplaySuccess("Generation completed successfully"); DisplayUrlInformation(Configuration.Generation.ApiRootUrl); + var genCounter = instrumentation?.CreateClientGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorLanguage, + Configuration.Generation.Language.ToString("G")) + }; + genCounter?.Add(1, meterTags); } else if (skipGeneration) { @@ -161,10 +205,13 @@ public override async Task InvokeAsync(InvocationContext context) var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; DisplayInfoHint(Configuration.Generation.Language, string.Empty, manifestPath); DisplayGenerateAdvancedHint(includePatterns ?? [], excludePatterns ?? [], string.Empty, manifestPath, "client edit"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -173,6 +220,30 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, GenerationLanguage? language, bool? backingStore, + bool? excludeBackwardCompatible, bool skipGeneration, string? output, string? namespaceName, + List? includePatterns, List? excludePatterns, List? structuredMimeTypes, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(10) : null; + if (language is { } l) tags?.Add(new KeyValuePair(TelemetryLabels.TagGeneratorLanguage, l.ToString("G"))); + if (backingStore is { } bs) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.backing_store", bs)); + if (excludeBackwardCompatible is { } ebc) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.exclude_backward_compatible", ebc)); + tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.skip_generation", skipGeneration)); + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (namespaceName is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.namespace", 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 (structuredMimeTypes is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.structured_media_types", structuredMimeTypes.ToArray())); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Client/GenerateHandler.cs b/src/kiota/Handlers/Client/GenerateHandler.cs index 7c9ce9d01a..cc59b9d0a2 100644 --- a/src/kiota/Handlers/Client/GenerateHandler.cs +++ b/src/kiota/Handlers/Client/GenerateHandler.cs @@ -1,15 +1,26 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Configuration; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Client; internal class GenerateHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "generate"), + new(TelemetryLabels.TagCommandRevision, 2) + ]; public required Option ClassOption { get; init; @@ -20,9 +31,31 @@ public required Option RefreshOption } public override async Task InvokeAsync(InvocationContext context) { - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? className0 = context.ParseResult.GetValueForOption(ClassOption); bool refresh = context.ParseResult.GetValueForOption(RefreshOption); + 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, refresh, className0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanGenerateClientCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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); + + var className = className0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); using (loggerFactory) { @@ -64,6 +97,14 @@ public override async Task InvokeAsync(InvocationContext context) DisplayUrlInformation(generationConfiguration.ApiRootUrl); var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{clientEntry.Key}"; DisplayInfoHint(generationConfiguration.Language, string.Empty, manifestPath); + var genCounter = instrumentation?.CreateClientGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorLanguage, + generationConfiguration.Language.ToString("G")) + }; + genCounter?.Add(1, meterTags); } else { @@ -71,10 +112,14 @@ public override async Task InvokeAsync(InvocationContext context) DisplayCleanHint("client generate", "--refresh"); } } + + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error adding the client: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -83,6 +128,22 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, bool refresh, string? className, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(3) + { + new($"{TelemetryLabels.TagCommandParams}.refresh", refresh), + } : null; + if (className is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.client_name", TelemetryLabels.RedactedValuePlaceholder)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Client/RemoveHandler.cs b/src/kiota/Handlers/Client/RemoveHandler.cs index 38237fc11d..8e7c3636b0 100644 --- a/src/kiota/Handlers/Client/RemoveHandler.cs +++ b/src/kiota/Handlers/Client/RemoveHandler.cs @@ -1,12 +1,23 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Client; internal class RemoveHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "remove"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -17,10 +28,33 @@ public required Option CleanOutputOption } public override async Task InvokeAsync(InvocationContext context) { - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + + // Get options + string? className0 = context.ParseResult.GetValueForOption(ClassOption); bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); + var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?; CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); + + var host = context.GetHost(); + var instrumentation = host.Services.GetService(); + var activitySource = instrumentation?.ActivitySource; + + CreateTelemetryTags(activitySource, cleanOutput, className0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanRemoveClientCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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 className = className0.OrEmpty(); using (loggerFactory) { try @@ -29,10 +63,13 @@ public override async Task InvokeAsync(InvocationContext context) var workspaceManagementService = new WorkspaceManagementService(logger, httpClient, true); await workspaceManagementService.RemoveClientAsync(className, cleanOutput, cancellationToken).ConfigureAwait(false); DisplaySuccess($"Client {className} removed successfully!"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error removing the client: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -41,6 +78,22 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, bool cleanOutput, string? className, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(3) + { + new($"{TelemetryLabels.TagCommandParams}.clean_output", cleanOutput), + } : null; + if (className is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.client_name", TelemetryLabels.RedactedValuePlaceholder)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaDownloadCommandHandler.cs b/src/kiota/Handlers/KiotaDownloadCommandHandler.cs index e61fa83fc3..2ae554e3eb 100644 --- a/src/kiota/Handlers/KiotaDownloadCommandHandler.cs +++ b/src/kiota/Handlers/KiotaDownloadCommandHandler.cs @@ -1,16 +1,26 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Caching; using Kiota.Builder.Configuration; using Kiota.Builder.SearchProviders; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaDownloadCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "download"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Argument SearchTermArgument { get; init; @@ -37,14 +47,38 @@ public required Option DisableSSLValidationOption } 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 outputPath = context.ParseResult.GetValueForOption(OutputPathOption) ?? string.Empty; + string? version0 = context.ParseResult.GetValueForOption(VersionOption); + string? outputPath0 = context.ParseResult.GetValueForOption(OutputPathOption); bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); 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, version0, outputPath0, cleanOutput, clearCache, disableSSLValidation, + logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanDownloadCommand, + 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 outputPath = outputPath0.OrEmpty(); + string version = version0.OrEmpty(); Configuration.Download.ClearCache = clearCache; Configuration.Download.DisableSSLValidation = disableSSLValidation; Configuration.Download.CleanOutput = cleanOutput; @@ -62,10 +96,14 @@ public override async Task InvokeAsync(InvocationContext context) { var searcher = await GetKiotaSearcherAsync(loggerFactory, cancellationToken).ConfigureAwait(false); var results = await searcher.SearchAsync(searchTerm, version, cancellationToken).ConfigureAwait(false); - return await SaveResultsAsync(searchTerm, version, results, logger, cancellationToken); + var result = await SaveResultsAsync(searchTerm, version, results, logger, cancellationToken); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); + return result; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error downloading a description: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -74,6 +112,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 SaveResultsAsync(string searchTerm, string version, IDictionary results, ILogger logger, CancellationToken cancellationToken) @@ -152,4 +194,23 @@ private async Task SaveResultsAsync(string searchTerm, string version, IDic await fileStream.FlushAsync(cancellationToken); return (path, 0); } + + private static void CreateTelemetryTags(ActivitySource? activitySource, string? version, + string? outputPath, bool cleanOutput, bool clearCache, bool disableSslValidation, 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, so it's always available + new($"{TelemetryLabels.TagCommandParams}.search_term", redacted), + new($"{TelemetryLabels.TagCommandParams}.clean_output", cleanOutput), + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + new($"{TelemetryLabels.TagCommandParams}.disable_ssl_validation", disableSslValidation), + } : null; + if (outputPath is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + 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"))); + } } diff --git a/src/kiota/Handlers/KiotaGenerateCommandHandler.cs b/src/kiota/Handlers/KiotaGenerateCommandHandler.cs index 9a429501d0..1f3b92538b 100644 --- a/src/kiota/Handlers/KiotaGenerateCommandHandler.cs +++ b/src/kiota/Handlers/KiotaGenerateCommandHandler.cs @@ -1,17 +1,26 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; - +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; - +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaGenerateCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "generate"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option DescriptionOption { get; init; @@ -66,27 +75,54 @@ public required Option> StructuredMimeTypesOption } public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; WarnShouldUseKiotaConfigClientsCommands(); - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + // Get options + string? output = context.ParseResult.GetValueForOption(OutputOption); GenerationLanguage language = context.ParseResult.GetValueForOption(LanguageOption); - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; - string manifest = context.ParseResult.GetValueForOption(ManifestOption) ?? string.Empty; + string? openapi = context.ParseResult.GetValueForOption(DescriptionOption); + string? manifest = context.ParseResult.GetValueForOption(ManifestOption); bool backingStore = context.ParseResult.GetValueForOption(BackingStoreOption); bool excludeBackwardCompatible = context.ParseResult.GetValueForOption(ExcludeBackwardCompatibleOption); bool clearCache = context.ParseResult.GetValueForOption(ClearCacheOption); bool disableSSLValidation = context.ParseResult.GetValueForOption(DisableSSLValidationOption); bool includeAdditionalData = context.ParseResult.GetValueForOption(AdditionalDataOption); - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + string? className = context.ParseResult.GetValueForOption(ClassOption); AccessModifier typeAccessModifier = context.ParseResult.GetValueForOption(TypeAccessModifierOption); - string namespaceName = context.ParseResult.GetValueForOption(NamespaceOption) ?? string.Empty; - List serializer = context.ParseResult.GetValueForOption(SerializerOption) ?? []; - List deserializer = context.ParseResult.GetValueForOption(DeserializerOption) ?? []; - List includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? []; - List excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? []; - List disabledValidationRules = context.ParseResult.GetValueForOption(DisabledValidationRulesOption) ?? []; + string? namespaceName = context.ParseResult.GetValueForOption(NamespaceOption); + List serializer = context.ParseResult.GetValueForOption(SerializerOption).OrEmpty(); + List deserializer = context.ParseResult.GetValueForOption(DeserializerOption).OrEmpty(); + List? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption); + List? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption); + List? disabledValidationRules0 = context.ParseResult.GetValueForOption(DisabledValidationRulesOption); bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); - List structuredMimeTypes = context.ParseResult.GetValueForOption(StructuredMimeTypesOption) ?? []; + List? structuredMimeTypes0 = context.ParseResult.GetValueForOption(StructuredMimeTypesOption); + 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, language, backingStore, excludeBackwardCompatible, clearCache, disableSSLValidation, cleanOutput, output, + namespaceName, includePatterns0, excludePatterns0, structuredMimeTypes0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanGenerateClientCommand, + 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); + + List includePatterns = includePatterns0.OrEmpty(); + List excludePatterns = excludePatterns0.OrEmpty(); + List disabledValidationRules = disabledValidationRules0.OrEmpty(); + List structuredMimeTypes = structuredMimeTypes0.OrEmpty(); AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); AssignIfNotNullOrEmpty(manifest, (c, s) => c.ApiManifestPath = s); @@ -137,6 +173,14 @@ public override async Task InvokeAsync(InvocationContext context) { DisplaySuccess("Generation completed successfully"); DisplayUrlInformation(Configuration.Generation.ApiRootUrl); + var genCounter = instrumentation?.CreateClientGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorLanguage, + Configuration.Generation.Language.ToString("G")) + }; + genCounter?.Add(1, meterTags); } else { @@ -148,10 +192,13 @@ public override async Task InvokeAsync(InvocationContext context) var manifestPath = manifestResult is null ? string.Empty : Configuration.Generation.ApiManifestPath; DisplayInfoHint(language, Configuration.Generation.OpenAPIFilePath, manifestPath); DisplayGenerateAdvancedHint(includePatterns, excludePatterns, Configuration.Generation.OpenAPIFilePath, manifestPath); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error generating the client: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -160,6 +207,10 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } public required Option> IncludePatternsOption @@ -187,4 +238,28 @@ public required Option DisableSSLValidationOption { get; init; } + + private static void CreateTelemetryTags(ActivitySource? activitySource, GenerationLanguage language, bool backingStore, + bool excludeBackwardCompatible, bool clearCache, bool disableSslValidation, bool cleanOutput, string? output, + string? namespaceName, List? includePatterns, List? excludePatterns, + List? structuredMimeTypes, LogLevel? logLevel, out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(12) + { + new(TelemetryLabels.TagGeneratorLanguage, language.ToString("G")), + new($"{TelemetryLabels.TagCommandParams}.backing_store", backingStore), + new($"{TelemetryLabels.TagCommandParams}.exclude_backward_compatible", excludeBackwardCompatible), + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + new($"{TelemetryLabels.TagCommandParams}.disable_ssl_validation", disableSslValidation), + new($"{TelemetryLabels.TagCommandParams}.clean_output", cleanOutput), + } : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (namespaceName is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.namespace", 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 (structuredMimeTypes is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.structured_media_types", structuredMimeTypes.ToArray())); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaGitHubDeviceLoginCommandHanlder.cs b/src/kiota/Handlers/KiotaGitHubDeviceLoginCommandHanlder.cs index 3eea8ab850..184b1419be 100644 --- a/src/kiota/Handlers/KiotaGitHubDeviceLoginCommandHanlder.cs +++ b/src/kiota/Handlers/KiotaGitHubDeviceLoginCommandHanlder.cs @@ -1,6 +1,11 @@ -using System.CommandLine.Invocation; +using System.CommandLine.Hosting; +using System.CommandLine.Invocation; +using System.Diagnostics; using kiota.Authentication.GitHub.DeviceCode; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder.SearchProviders.GitHub.GitHubClient; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; @@ -10,19 +15,50 @@ namespace kiota.Handlers; internal class KiotaGitHubDeviceLoginCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "login-github-device"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + 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, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanGitHubDeviceLoginCommand, + 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); + var (loggerFactory, logger) = GetLoggerAndFactory(context); using (loggerFactory) { await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); try { - return await LoginAsync(logger, cancellationToken).ConfigureAwait(false); + var result = await LoginAsync(logger, cancellationToken).ConfigureAwait(false); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); + return result; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error signing in to GitHub: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -31,7 +67,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 LoginAsync(ILogger logger, CancellationToken cancellationToken) @@ -79,4 +118,12 @@ private async Task ListOutRepositoriesAsync(IAuthenticationProvider authProvider DisplayWarning("Kiota is not installed to any GitHub organization/account."); } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(1) : null; + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaGitHubLogoutCommandhandler.cs b/src/kiota/Handlers/KiotaGitHubLogoutCommandhandler.cs index 1688e3b4fd..231e97d958 100644 --- a/src/kiota/Handlers/KiotaGitHubLogoutCommandhandler.cs +++ b/src/kiota/Handlers/KiotaGitHubLogoutCommandhandler.cs @@ -1,14 +1,46 @@ -using System.CommandLine.Invocation; +using System.CommandLine.Hosting; +using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder.SearchProviders.GitHub.Authentication; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaGitHubLogoutCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "logout-github"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + 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, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanGitHubLogoutCommand, + 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); + var (loggerFactory, logger) = GetLoggerAndFactory(context); using (loggerFactory) { @@ -22,10 +54,13 @@ public override async Task InvokeAsync(InvocationContext context) DisplaySuccess("Logged out successfully."); else DisplaySuccess("Already logged out."); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error logging out from GitHub: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -34,6 +69,18 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(1) : null; + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaGitHubPatLoginCommandHandler.cs b/src/kiota/Handlers/KiotaGitHubPatLoginCommandHandler.cs index cc01a44030..63e9efaef9 100644 --- a/src/kiota/Handlers/KiotaGitHubPatLoginCommandHandler.cs +++ b/src/kiota/Handlers/KiotaGitHubPatLoginCommandHandler.cs @@ -1,29 +1,65 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder.SearchProviders.GitHub.Authentication; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaGitHubPatLoginCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "login-github-pat"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option PatOption { get; init; } public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string pat = context.ParseResult.GetValueForOption(PatOption).OrEmpty(); + var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?; CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; - string pat = context.ParseResult.GetValueForOption(PatOption) ?? string.Empty; + + var host = context.GetHost(); + var instrumentation = host.Services.GetService(); + var activitySource = instrumentation?.ActivitySource; + + CreateTelemetryTags(activitySource, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanGitHubPatLoginCommand, + 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); + var (loggerFactory, logger) = GetLoggerAndFactory(context); using (loggerFactory) { await CheckForNewVersionAsync(logger, cancellationToken).ConfigureAwait(false); try { - return await LoginAsync(logger, pat, cancellationToken).ConfigureAwait(false); + var result = await LoginAsync(logger, pat, cancellationToken).ConfigureAwait(false); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); + return result; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error signing in to GitHub: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -32,6 +68,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 LoginAsync(ILogger logger, string patValue, CancellationToken cancellationToken) @@ -47,4 +87,12 @@ private async Task LoginAsync(ILogger logger, string patValue, Cancellation DisplayGitHubLogoutHint(); return 0; } + + private static void CreateTelemetryTags(ActivitySource? activitySource, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(1) : null; + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaInfoCommandHandler.cs b/src/kiota/Handlers/KiotaInfoCommandHandler.cs index f60ab7f85e..e3dee7d0e1 100644 --- a/src/kiota/Handlers/KiotaInfoCommandHandler.cs +++ b/src/kiota/Handlers/KiotaInfoCommandHandler.cs @@ -1,16 +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 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 KiotaInfoCommandHandler : KiotaSearchBasedCommandHandler +internal class + KiotaInfoCommandHandler : KiotaSearchBasedCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "info"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option DescriptionOption { get; init; @@ -46,15 +57,42 @@ public required Option DependencyTypesOption public override async Task InvokeAsync(InvocationContext context) { - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; - string manifest = context.ParseResult.GetValueForOption(ManifestOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? openapi0 = context.ParseResult.GetValueForOption(DescriptionOption); + string manifest = context.ParseResult.GetValueForOption(ManifestOption).OrEmpty(); bool clearCache = context.ParseResult.GetValueForOption(ClearCacheOption); - string searchTerm = context.ParseResult.GetValueForOption(SearchTermOption) ?? string.Empty; - string version = context.ParseResult.GetValueForOption(VersionOption) ?? string.Empty; + string? searchTerm0 = context.ParseResult.GetValueForOption(SearchTermOption); + string? version0 = context.ParseResult.GetValueForOption(VersionOption); bool json = context.ParseResult.GetValueForOption(JsonOption); - DependencyType[] dependencyTypes = context.ParseResult.GetValueForOption(DependencyTypesOption) ?? []; + DependencyType[]? dependencyTypes0 = context.ParseResult.GetValueForOption(DependencyTypesOption); GenerationLanguage? language = context.ParseResult.GetValueForOption(GenerationLanguage); + 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, openapi0, version0, language, clearCache, + logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanInfoCommand, + 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 searchTerm = searchTerm0.OrEmpty(); + string version = version0.OrEmpty(); + DependencyType[] dependencyTypes = dependencyTypes0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context); Configuration.Search.ClearCache = clearCache; using (loggerFactory) @@ -83,6 +121,7 @@ public override async Task InvokeAsync(InvocationContext context) var instructions = Configuration.Languages; if (!string.IsNullOrEmpty(openapi)) + { try { var builder = new KiotaBuilder(logger, Configuration.Generation, httpClient); @@ -92,14 +131,22 @@ public override async Task InvokeAsync(InvocationContext context) } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG - logger.LogCritical(ex, "error getting information from the description: {exceptionMessage}", ex.Message); + logger.LogCritical(ex, "error getting information from the description: {exceptionMessage}", + ex.Message); throw; // so debug tools go straight to the source of the exception when attached #else logger.LogCritical("error getting information from the description: {exceptionMessage}", ex.Message); return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } + } ShowLanguageInformation(language.Value, instructions, json, dependencyTypes); return 0; } @@ -166,4 +213,21 @@ private void ShowLanguageInformation(GenerationLanguage language, LanguagesInfor DisplayInfo($"No information for {language}."); } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, string? searchTerm, string? openapi, + string? version, GenerationLanguage? language, bool clearCache, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + tags = activitySource?.HasListeners() == true ? new List>(6) + { + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + } : null; + if (searchTerm is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.search_term", redacted)); + if (openapi is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.openapi", redacted)); + if (version is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.version", redacted)); + if (language is { } lang) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.language", lang.ToString("G"))); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/KiotaSearchCommandHandler.cs b/src/kiota/Handlers/KiotaSearchCommandHandler.cs index b559ec257e..2eee14718b 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.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.SearchProviders; +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>(4) + { + // 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..ef7e06e86f 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>(8) + { + 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/Handlers/KiotaUpdateCommandHandler.cs b/src/kiota/Handlers/KiotaUpdateCommandHandler.cs index 8b94e79bfc..0d0f4ff18f 100644 --- a/src/kiota/Handlers/KiotaUpdateCommandHandler.cs +++ b/src/kiota/Handlers/KiotaUpdateCommandHandler.cs @@ -1,14 +1,25 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Configuration; using Kiota.Builder.Lock; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers; internal class KiotaUpdateCommandHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "client"), + new(TelemetryLabels.TagCommandName, "update"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option OutputOption { get; init; @@ -23,13 +34,36 @@ public required Option ClearCacheOption } public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; WarnShouldUseKiotaConfigClientsCommands(); - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + // Get options + string? output = context.ParseResult.GetValueForOption(OutputOption); bool clearCache = context.ParseResult.GetValueForOption(ClearCacheOption); bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); + 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, output, clearCache, cleanOutput, + logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanUpdateCommand, + 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); + AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); - var searchPath = GetAbsolutePath(output); + var searchPath = GetAbsolutePath(output.OrEmpty()); var lockService = new LockManagementService(); var lockFileDirectoryPaths = lockService.GetDirectoriesContainingLockFile(searchPath); if (!lockFileDirectoryPaths.Any()) @@ -59,8 +93,20 @@ public override async Task InvokeAsync(InvocationContext context) config.OutputPath = x.lockDirectoryPath; return config; }).ToArray(); + var genCounter = instrumentation?.CreateClientGenerationCounter(); var results = await Task.WhenAll(configurations - .Select(x => GenerateClientAsync(context, x, cancellationToken))); + .Select(async x => + { + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorLanguage, + x.Language.ToString("G")) + }; + var result = await GenerateClientAsync(context, x, cancellationToken); + genCounter?.Add(1, meterTags); + return result; + })); foreach (var (lockInfo, lockDirectoryPath) in locks) { DisplaySuccess($"Update of {lockInfo?.ClientClassName} client for {lockInfo?.Language} at {lockDirectoryPath} completed"); @@ -71,10 +117,13 @@ public override async Task InvokeAsync(InvocationContext context) DisplayInfoHint(configuration.Language, configuration.OpenAPIFilePath, string.Empty); if (Array.Exists(results, static x => x) && !cleanOutput) DisplayCleanHint("update"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error updating the client: {ExceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -83,6 +132,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 GenerateClientAsync(InvocationContext context, GenerationConfiguration config, CancellationToken cancellationToken) @@ -93,4 +146,18 @@ private async Task GenerateClientAsync(InvocationContext context, Generati return await new KiotaBuilder(logger, config, httpClient).GenerateClientAsync(cancellationToken); } } + private static void CreateTelemetryTags(ActivitySource? activitySource, string? output, bool clearCache, + bool cleanOutput, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(4) + { + new($"{TelemetryLabels.TagCommandParams}.clear_cache", clearCache), + new($"{TelemetryLabels.TagCommandParams}.clean_output", cleanOutput), + } : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Plugin/AddHandler.cs b/src/kiota/Handlers/Plugin/AddHandler.cs index 7a4a661308..03ca1e2c51 100644 --- a/src/kiota/Handlers/Plugin/AddHandler.cs +++ b/src/kiota/Handlers/Plugin/AddHandler.cs @@ -1,10 +1,15 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; @@ -12,6 +17,12 @@ namespace kiota.Handlers.Plugin; internal class AddHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "plugin"), + new(TelemetryLabels.TagCommandName, "add"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -52,29 +63,52 @@ public required Option SkipGenerationOption } public override async Task InvokeAsync(InvocationContext context) { - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; - List pluginTypes = context.ParseResult.GetValueForOption(PluginTypesOption) ?? []; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? output = context.ParseResult.GetValueForOption(OutputOption); + List? pluginTypes = context.ParseResult.GetValueForOption(PluginTypesOption); SecuritySchemeType? pluginAuthType = context.ParseResult.GetValueForOption(PluginAuthTypeOption); - string pluginAuthRefId = context.ParseResult.GetValueForOption(PluginAuthRefIdOption) ?? string.Empty; - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + string? pluginAuthRefId = context.ParseResult.GetValueForOption(PluginAuthRefIdOption); + string? openapi = context.ParseResult.GetValueForOption(DescriptionOption); bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; - List includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? []; - List excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? []; + string? className = context.ParseResult.GetValueForOption(ClassOption); + List? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption); + List? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption); + 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, pluginTypes, pluginAuthType, pluginAuthRefId, skipGeneration, output, includePatterns0, excludePatterns0, + logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanAddPluginCommand, + 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); + AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s); AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s); AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s); Configuration.Generation.SkipGeneration = skipGeneration; Configuration.Generation.Operation = ConsumerOperation.Add; - if (pluginTypes.Count != 0) + if (pluginTypes is { Count: > 0 }) Configuration.Generation.PluginTypes = pluginTypes.ToHashSet(); if (pluginAuthType.HasValue && !string.IsNullOrWhiteSpace(pluginAuthRefId)) Configuration.Generation.PluginAuthInformation = PluginAuthConfiguration.FromParameters(pluginAuthType, pluginAuthRefId); - if (includePatterns.Count != 0) - Configuration.Generation.IncludePatterns = includePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); - if (excludePatterns.Count != 0) - Configuration.Generation.ExcludePatterns = excludePatterns.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (includePatterns0 is { Count: > 0 }) + Configuration.Generation.IncludePatterns = includePatterns0.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); + if (excludePatterns0 is { Count: > 0 }) + Configuration.Generation.ExcludePatterns = excludePatterns0.Select(static x => x.TrimQuotes()).ToHashSet(StringComparer.OrdinalIgnoreCase); Configuration.Generation.OpenAPIFilePath = GetAbsolutePath(Configuration.Generation.OpenAPIFilePath); Configuration.Generation.OutputPath = NormalizeSlashesInPath(GetAbsolutePath(Configuration.Generation.OutputPath)); var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); @@ -91,6 +125,14 @@ public override async Task InvokeAsync(InvocationContext context) { DisplaySuccess("Generation completed successfully"); DisplayUrlInformation(Configuration.Generation.ApiRootUrl, true); + var genCounter = instrumentation?.CreatePluginGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorPluginTypes, + Configuration.Generation.PluginTypes.Select(static x=> x.ToString("G").ToLowerInvariant()).ToArray()) + }; + genCounter?.Add(1, meterTags); } else if (skipGeneration) { @@ -98,11 +140,14 @@ public override async Task InvokeAsync(InvocationContext context) DisplayGenerateCommandHint(); } // else we get an error because we're adding a client that already exists var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; - DisplayGenerateAdvancedHint(includePatterns, excludePatterns, string.Empty, manifestPath, "plugin add"); + DisplayGenerateAdvancedHint(includePatterns0.OrEmpty(), excludePatterns0.OrEmpty(), string.Empty, manifestPath, "plugin add"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error adding the plugin: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -111,6 +156,30 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, List? pluginTypes, + SecuritySchemeType? pluginAuthType, string? pluginAuthRefId, bool skipGeneration, string? output, + List? includePatterns, List? excludePatterns, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(8) + { + new($"{TelemetryLabels.TagCommandParams}.skip_generation", skipGeneration), + } : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (pluginTypes is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.plugin_types", pluginTypes.Select(static x => x.ToString("G").ToLowerInvariant()).ToArray())); + if (pluginAuthType is { } at) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.auth_type", at.ToString("G"))); + if (pluginAuthRefId is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.auth_ref_id", 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/Handlers/Plugin/EditHandler.cs b/src/kiota/Handlers/Plugin/EditHandler.cs index 3f5a41a4d6..54d7625c6c 100644 --- a/src/kiota/Handlers/Plugin/EditHandler.cs +++ b/src/kiota/Handlers/Plugin/EditHandler.cs @@ -1,10 +1,15 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; @@ -12,6 +17,12 @@ namespace kiota.Handlers.Plugin; internal class EditHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "plugin"), + new(TelemetryLabels.TagCommandName, "edit"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -55,21 +66,46 @@ public required Option PluginAuthRefIdOption public override async Task InvokeAsync(InvocationContext context) { - string output = context.ParseResult.GetValueForOption(OutputOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? output = context.ParseResult.GetValueForOption(OutputOption); List? pluginTypes = context.ParseResult.GetValueForOption(PluginTypesOption); SecuritySchemeType? pluginAuthType = context.ParseResult.GetValueForOption(PluginAuthTypeOption); - string pluginAuthRefId = context.ParseResult.GetValueForOption(PluginAuthRefIdOption) ?? string.Empty; - string openapi = context.ParseResult.GetValueForOption(DescriptionOption) ?? string.Empty; + string? pluginAuthRefId0 = context.ParseResult.GetValueForOption(PluginAuthRefIdOption); + string? openapi = context.ParseResult.GetValueForOption(DescriptionOption); bool skipGeneration = context.ParseResult.GetValueForOption(SkipGenerationOption); - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + string? className0 = context.ParseResult.GetValueForOption(ClassOption); List? includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption); List? excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption); + 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, pluginTypes, pluginAuthType, pluginAuthRefId0, skipGeneration, output, includePatterns, excludePatterns, + logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(ActivityKind.Internal, name: TelemetryLabels.SpanEditPluginCommand, + 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); + + var className = className0.OrEmpty(); + var pluginAuthRefId = pluginAuthRefId0.OrEmpty(); Configuration.Generation.SkipGeneration = skipGeneration; Configuration.Generation.Operation = ConsumerOperation.Edit; - if (pluginAuthType.HasValue && !string.IsNullOrWhiteSpace(pluginAuthRefId)) + if (pluginAuthType.HasValue && !string.IsNullOrWhiteSpace(pluginAuthRefId0)) Configuration.Generation.PluginAuthInformation = PluginAuthConfiguration.FromParameters(pluginAuthType, pluginAuthRefId); + var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); using (loggerFactory) { @@ -110,6 +146,14 @@ public override async Task InvokeAsync(InvocationContext context) { DisplaySuccess("Generation completed successfully"); DisplayUrlInformation(Configuration.Generation.ApiRootUrl, true); + var genCounter = instrumentation?.CreatePluginGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorPluginTypes, + Configuration.Generation.PluginTypes.Select(static x=> x.ToString("G").ToLowerInvariant()).ToArray()) + }; + genCounter?.Add(1, meterTags); } else if (skipGeneration) { @@ -123,11 +167,14 @@ public override async Task InvokeAsync(InvocationContext context) } var manifestPath = $"{GetAbsolutePath(Path.Combine(WorkspaceConfigurationStorageService.KiotaDirectorySegment, WorkspaceConfigurationStorageService.ManifestFileName))}#{Configuration.Generation.ClientClassName}"; DisplayInfoHint(Configuration.Generation.Language, string.Empty, manifestPath); - DisplayGenerateAdvancedHint(includePatterns ?? [], excludePatterns ?? [], string.Empty, manifestPath, "plugin edit"); + DisplayGenerateAdvancedHint(includePatterns.OrEmpty(), excludePatterns.OrEmpty(), string.Empty, manifestPath, "plugin edit"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error editing the plugin: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -136,6 +183,30 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, List? pluginTypes, + SecuritySchemeType? pluginAuthType, string? pluginAuthRefId, bool skipGeneration, string? output, + List? includePatterns, List? excludePatterns, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(8) + { + new($"{TelemetryLabels.TagCommandParams}.skip_generation", skipGeneration), + } : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (output is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.output", redacted)); + if (pluginTypes is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.plugin_types", pluginTypes.Select(static x => x.ToString("G").ToLowerInvariant()).ToArray())); + if (pluginAuthType is { } at) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.auth_type", at.ToString("G"))); + if (pluginAuthRefId is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.auth_ref_id", 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/Handlers/Plugin/GenerateHandler.cs b/src/kiota/Handlers/Plugin/GenerateHandler.cs index 415a14e409..9036ec18dd 100644 --- a/src/kiota/Handlers/Plugin/GenerateHandler.cs +++ b/src/kiota/Handlers/Plugin/GenerateHandler.cs @@ -1,15 +1,26 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; using System.Text.Json; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.Configuration; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Plugin; internal class GenerateHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "plugin"), + new(TelemetryLabels.TagCommandName, "generate"), + new(TelemetryLabels.TagCommandRevision, 2) + ]; public required Option ClassOption { get; init; @@ -20,9 +31,31 @@ public required Option RefreshOption } public override async Task InvokeAsync(InvocationContext context) { - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + // Get options + string? className0 = context.ParseResult.GetValueForOption(ClassOption); bool refresh = context.ParseResult.GetValueForOption(RefreshOption); + 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, refresh, className0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanGeneratePluginCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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); + + var className = className0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); using (loggerFactory) { @@ -62,6 +95,14 @@ public override async Task InvokeAsync(InvocationContext context) if (result) { DisplaySuccess($"Update of {clientEntry.Key} plugin completed"); + var genCounter = instrumentation?.CreatePluginGenerationCounter(); + var meterTags = new TagList(_commonTags.AsSpan()) + { + new KeyValuePair( + TelemetryLabels.TagGeneratorPluginTypes, + generationConfiguration.PluginTypes.Select(static x=> x.ToString("G").ToLowerInvariant()).ToArray()) + }; + genCounter?.Add(1, meterTags); } else { @@ -69,10 +110,14 @@ public override async Task InvokeAsync(InvocationContext context) DisplayCleanHint("client generate", "--refresh"); } } + + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error generating the plugin: {ExceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -81,6 +126,22 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, bool refresh, string? className, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(3) + { + new($"{TelemetryLabels.TagCommandParams}.refresh", refresh), + } : null; + if (className is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.client_name", TelemetryLabels.RedactedValuePlaceholder)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Plugin/RemoveHandler.cs b/src/kiota/Handlers/Plugin/RemoveHandler.cs index 0c0f3c7f5b..65bfa4b5f2 100644 --- a/src/kiota/Handlers/Plugin/RemoveHandler.cs +++ b/src/kiota/Handlers/Plugin/RemoveHandler.cs @@ -1,12 +1,23 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Plugin; internal class RemoveHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagGenerationOutputType, "plugin"), + new(TelemetryLabels.TagCommandName, "remove"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option ClassOption { get; init; @@ -17,9 +28,33 @@ public required Option CleanOutputOption } public override async Task InvokeAsync(InvocationContext context) { - string className = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + + // Get options + string? className0 = context.ParseResult.GetValueForOption(ClassOption); bool cleanOutput = context.ParseResult.GetValueForOption(CleanOutputOption); + 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, cleanOutput, className0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanRemovePluginCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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 className = className0.OrEmpty(); + var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); using (loggerFactory) { @@ -29,10 +64,13 @@ public override async Task InvokeAsync(InvocationContext context) var workspaceManagementService = new WorkspaceManagementService(logger, httpClient, true); await workspaceManagementService.RemovePluginAsync(className, cleanOutput, cancellationToken).ConfigureAwait(false); DisplaySuccess($"Plugin {className} removed successfully!"); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); #if DEBUG logger.LogCritical(ex, "error removing the plugin: {exceptionMessage}", ex.Message); throw; // so debug tools go straight to the source of the exception when attached @@ -41,6 +79,22 @@ public override async Task InvokeAsync(InvocationContext context) return 1; #endif } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, bool cleanOutput, string? className, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(3) + { + new($"{TelemetryLabels.TagCommandParams}.clean_output", cleanOutput), + } : null; + if (className is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.client_name", TelemetryLabels.RedactedValuePlaceholder)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Workspace/InitHandler.cs b/src/kiota/Handlers/Workspace/InitHandler.cs index 8dfcac9767..a2e8d8d38e 100644 --- a/src/kiota/Handlers/Workspace/InitHandler.cs +++ b/src/kiota/Handlers/Workspace/InitHandler.cs @@ -1,14 +1,46 @@ -using System.CommandLine.Invocation; +using System.CommandLine.Hosting; +using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Workspace; internal class InitHandler : BaseKiotaCommandHandler { - public async override Task InvokeAsync(InvocationContext context) + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "workspace-init"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; + public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + + // Get options + 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, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanInitWorkspaceCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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); + var workspaceStorageService = new WorkspaceConfigurationStorageService(Directory.GetCurrentDirectory()); var (loggerFactory, logger) = GetLoggerAndFactory(context, Configuration.Generation.OutputPath); using (loggerFactory) @@ -16,13 +48,28 @@ public async override Task InvokeAsync(InvocationContext context) try { await workspaceStorageService.InitializeAsync(cancellationToken).ConfigureAwait(false); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); logger.LogCritical(ex, "error initializing the workspace configuration"); return 1; } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(1) : null; + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Handlers/Workspace/MigrateHandler.cs b/src/kiota/Handlers/Workspace/MigrateHandler.cs index 066149929a..da7bbd4334 100644 --- a/src/kiota/Handlers/Workspace/MigrateHandler.cs +++ b/src/kiota/Handlers/Workspace/MigrateHandler.cs @@ -1,12 +1,22 @@ using System.CommandLine; +using System.CommandLine.Hosting; using System.CommandLine.Invocation; +using System.Diagnostics; +using kiota.Extension; +using kiota.Telemetry; using Kiota.Builder.WorkspaceManagement; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace kiota.Handlers.Workspace; internal class MigrateHandler : BaseKiotaCommandHandler { + private readonly KeyValuePair[] _commonTags = + [ + new(TelemetryLabels.TagCommandName, "workspace-migrate"), + new(TelemetryLabels.TagCommandRevision, 1) + ]; public required Option LockDirectoryOption { get; @@ -18,11 +28,34 @@ public required Option ClassOption } public override async Task InvokeAsync(InvocationContext context) { + // Span start time + Stopwatch? stopwatch = Stopwatch.StartNew(); + var startTime = DateTimeOffset.UtcNow; + + // Get options var workingDirectory = NormalizeSlashesInPath(Directory.GetCurrentDirectory()); - string lockDirectory = context.ParseResult.GetValueForOption(LockDirectoryOption) ?? workingDirectory; - string clientName = context.ParseResult.GetValueForOption(ClassOption) ?? string.Empty; + string? lockDirectory0 = context.ParseResult.GetValueForOption(LockDirectoryOption); + string? clientName0 = context.ParseResult.GetValueForOption(ClassOption); + var logLevel = context.ParseResult.FindResultFor(LogLevelOption)?.GetValueOrDefault() as LogLevel?; CancellationToken cancellationToken = context.BindingContext.GetService(typeof(CancellationToken)) is CancellationToken token ? token : CancellationToken.None; - lockDirectory = NormalizeSlashesInPath(lockDirectory); + + var host = context.GetHost(); + var instrumentation = host.Services.GetService(); + var activitySource = instrumentation?.ActivitySource; + + CreateTelemetryTags(activitySource, lockDirectory0, clientName0, logLevel, out var tags); + // Start span + using var invokeActivity = activitySource?.StartActivity(TelemetryLabels.SpanMigrateWorkspaceCommand, + ActivityKind.Internal, startTime: startTime, parentContext: default, + tags: _commonTags.ConcatNullable(tags)?.Concat(Telemetry.Telemetry.GetThreadTags())); + 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); + + var lockDirectory = NormalizeSlashesInPath(lockDirectory0 ?? workingDirectory); + var clientName = clientName0.OrEmpty(); var (loggerFactory, logger) = GetLoggerAndFactory(context, $"./{DescriptionStorageService.KiotaDirectorySegment}"); using (loggerFactory) { @@ -37,13 +70,32 @@ public override async Task InvokeAsync(InvocationContext context) } DisplaySuccess($"Client configurations migrated successfully: {string.Join(", ", clientNames)}"); DisplayGenerateAfterMigrateHint(); + invokeActivity?.SetStatus(ActivityStatusCode.Ok); return 0; } catch (Exception ex) { + invokeActivity?.SetStatus(ActivityStatusCode.Error); + invokeActivity?.AddException(ex); logger.LogCritical(ex, "error migrating the workspace configuration"); return 1; } + finally + { + if (stopwatch is not null) meterRuntime?.Record(stopwatch.Elapsed.TotalSeconds, tl); + } } } + + private static void CreateTelemetryTags(ActivitySource? activitySource, string? lockDirectory, string? className, + LogLevel? logLevel, + out List>? tags) + { + // set up telemetry tags + tags = activitySource?.HasListeners() == true ? new List>(3) : null; + const string redacted = TelemetryLabels.RedactedValuePlaceholder; + if (lockDirectory is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.lock_directory", redacted)); + if (className is not null) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.client_name", redacted)); + if (logLevel is { } ll) tags?.Add(new KeyValuePair($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G"))); + } } diff --git a/src/kiota/Program.cs b/src/kiota/Program.cs index 9ddc6255ba..951c93e058 100644 --- a/src/kiota/Program.cs +++ b/src/kiota/Program.cs @@ -1,15 +1,26 @@ using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Hosting; +using System.CommandLine.Parsing; +using kiota.Extension; +using Microsoft.Extensions.Hosting; namespace kiota; + static class Program { static async Task Main(string[] args) { var rootCommand = KiotaHost.GetRootCommand(); - var result = await rootCommand.InvokeAsync(args); + var parser = new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseHost(static args => Host.CreateDefaultBuilder(args).ConfigureKiotaTelemetryServices()) + .Build(); + var result = await parser.InvokeAsync(args); DisposeSubCommands(rootCommand); return result; } + private static void DisposeSubCommands(this Command command) { if (command.Handler is IDisposable disposableHandler) diff --git a/src/kiota/Telemetry/Config/AppInsightsConfig.cs b/src/kiota/Telemetry/Config/AppInsightsConfig.cs new file mode 100644 index 0000000000..8be2e14415 --- /dev/null +++ b/src/kiota/Telemetry/Config/AppInsightsConfig.cs @@ -0,0 +1,15 @@ +namespace kiota.Telemetry.Config; + +public class AppInsightsConfig +{ + public bool Enabled + { + get; + set; + } + public string? ConnectionString + { + get; + set; + } +} diff --git a/src/kiota/Telemetry/Config/OpenTelemetryConfig.cs b/src/kiota/Telemetry/Config/OpenTelemetryConfig.cs new file mode 100644 index 0000000000..72b6f811e6 --- /dev/null +++ b/src/kiota/Telemetry/Config/OpenTelemetryConfig.cs @@ -0,0 +1,15 @@ +namespace kiota.Telemetry.Config; + +public class OpenTelemetryConfig +{ + public bool Enabled + { + get; + set; + } + public string? EndpointAddress + { + get; + set; + } +} diff --git a/src/kiota/Telemetry/Config/TelemetryConfig.cs b/src/kiota/Telemetry/Config/TelemetryConfig.cs new file mode 100644 index 0000000000..95f0e2e7a1 --- /dev/null +++ b/src/kiota/Telemetry/Config/TelemetryConfig.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace kiota.Telemetry.Config; + +public class TelemetryConfig +{ + public const string ConfigSectionKey = "Telemetry"; + + [JsonPropertyName("OptOut")] + public bool Disabled + { + get; + set; + } + + public OpenTelemetryConfig OpenTelemetry + { + get; + set; + } = new(); + + public AppInsightsConfig AppInsights + { + get; + set; + } = new(); +} diff --git a/src/kiota/Telemetry/Instrumentation.cs b/src/kiota/Telemetry/Instrumentation.cs new file mode 100644 index 0000000000..7830838563 --- /dev/null +++ b/src/kiota/Telemetry/Instrumentation.cs @@ -0,0 +1,61 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace kiota.Telemetry; + +internal class Instrumentation(IMeterFactory meterFactory) : IDisposable +{ + private readonly Meter _meter = meterFactory.Create(TelemetryLabels.ScopeName); + + /// + /// An activity source is used to create activities (spans) during manual instrumentation. + /// + public ActivitySource ActivitySource { get; } = new(TelemetryLabels.ScopeName); + + /// + /// Creates a histogram instrument used to time command duration + /// + /// A histogram instrument + public Histogram CreateCommandDurationHistogram() + { + return _meter.CreateHistogram(name: TelemetryLabels.InstrumentCommandDurationName, unit: "s", + description: "Duration of the command"); + } + + /// + /// Creates a counter instrument for client generations + /// + /// A counter instrument + public Counter CreateClientGenerationCounter() + { + return _meter.CreateCounter(name: TelemetryLabels.InstrumentClientGenerationCount, + description: "Count of client generations that have been run"); + } + + /// + /// Creates a counter instrument for plugin generations + /// + /// A counter instrument + public Counter CreatePluginGenerationCounter() + { + return _meter.CreateCounter(name: TelemetryLabels.InstrumentPluginGenerationCount, + description: "Count of plugin generations that have been run"); + } + + /// + /// Creates a counter instrument for command execution + /// + /// A counter instrument + public Counter CreateCommandExecutionCounter() + { + return _meter.CreateCounter(name: TelemetryLabels.InstrumentCommandExecutionsCount, + description: "Count of commands that have been run"); + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + this.ActivitySource.Dispose(); + } +} diff --git a/src/kiota/Telemetry/Telemetry.cs b/src/kiota/Telemetry/Telemetry.cs new file mode 100644 index 0000000000..b073fba89e --- /dev/null +++ b/src/kiota/Telemetry/Telemetry.cs @@ -0,0 +1,26 @@ +using System.Runtime.CompilerServices; + +namespace kiota.Telemetry; + +public class Telemetry +{ + internal static KeyValuePair[] GetThreadTags() + { + var name = Thread.CurrentThread.Name; + var id = Environment.CurrentManagedThreadId; + + if (name is not null) + { + return + [ + new KeyValuePair("thread.id", id), + new KeyValuePair("thread.name", name) + ]; + } + + return + [ + new KeyValuePair("thread.id", id) + ]; + } +} diff --git a/src/kiota/Telemetry/TelemetryLabels.cs b/src/kiota/Telemetry/TelemetryLabels.cs new file mode 100644 index 0000000000..a60d81b542 --- /dev/null +++ b/src/kiota/Telemetry/TelemetryLabels.cs @@ -0,0 +1,46 @@ +namespace kiota.Telemetry; + +public static class TelemetryLabels +{ + public const string ScopeName = "microsoft.openapi.kiota"; + + public const string RedactedValuePlaceholder = "REDACTED"; + + // Meter instruments + public const string InstrumentCommandDurationName = "kiota.command.duration"; + public const string InstrumentCommandExecutionsCount = "kiota.command.executions"; + public const string InstrumentClientGenerationCount = "kiota.client.generations"; + public const string InstrumentPluginGenerationCount = "kiota.plugin.generations"; + + // Tags/Attributes + public const string TagCommandName = "kiota.command.name"; + public const string TagCommandRevision = "kiota.command.revision"; + public const string TagCommandParams = "kiota.command.parameters"; + public const string TagGeneratorLanguage = "kiota.client.language"; + public const string TagGeneratorPluginTypes = "kiota.plugin.types"; + // plugin, client, manifest etc. + public const string TagGenerationOutputType = "kiota.output_type"; + + // Span/Activity names + public const string SpanAddClientCommand = "Client/Add InvokeAsync()"; + public const string SpanEditClientCommand = "Client/Edit InvokeAsync()"; + public const string SpanGenerateClientCommand = "Client/Generate InvokeAsync()"; + public const string SpanRemoveClientCommand = "Client/Remove InvokeAsync()"; + + public const string SpanAddPluginCommand = "Plugin/Add InvokeAsync()"; + public const string SpanEditPluginCommand = "Plugin/Edit InvokeAsync()"; + public const string SpanGeneratePluginCommand = "Plugin/Generate InvokeAsync()"; + public const string SpanRemovePluginCommand = "Plugin/Remove InvokeAsync()"; + + public const string SpanInitWorkspaceCommand = "Workspace/Init InvokeAsync()"; + public const string SpanMigrateWorkspaceCommand = "Workspace/Migrate InvokeAsync()"; + + public const string SpanDownloadCommand = "Download InvokeAsync()"; + public const string SpanGitHubDeviceLoginCommand = "Login/GitHub/Device InvokeAsync()"; + 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()"; + public const string SpanUpdateCommand = "Update InvokeAsync()"; +} diff --git a/src/kiota/appsettings.json b/src/kiota/appsettings.json index 4af690eacf..4ffb85069c 100644 --- a/src/kiota/appsettings.json +++ b/src/kiota/appsettings.json @@ -433,5 +433,15 @@ ], "DependencyInstallCommand": "" } + }, + "Telemetry": { + "OptOut": false, + "OpenTelemetry": { + "Enabled": false + }, + "AppInsights": { + "Enabled": false, + "ConnectionString": "InstrumentationKey=cdc9bddd-eb3f-475b-9511-abb601c1eea9;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=e8bec981-1a1a-40bb-b964-afd917ab2c74" + } } } diff --git a/src/kiota/kiota.csproj b/src/kiota/kiota.csproj index 2ca8b2d58a..ae9c706f3b 100644 --- a/src/kiota/kiota.csproj +++ b/src/kiota/kiota.csproj @@ -38,11 +38,16 @@ + + true + + + @@ -52,9 +57,15 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + + +