Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Telemetry PoC #6128

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
31241fa
Add OTel libraries
Feb 10, 2025
27a3107
Add telemetry for client subcommands
calebkiage Feb 11, 2025
c7dafae
Unconditionally add skip_generation parameter tag
calebkiage Feb 11, 2025
b8d62ed
Merge branch 'main' into feat/telemetry
calebkiage Feb 11, 2025
dfa34e5
Format code
calebkiage Feb 11, 2025
a59ed0c
Extract tag creation to function
calebkiage Feb 12, 2025
4754360
Add OpenTelemetry dependabot group
calebkiage Feb 12, 2025
f1d7557
Format code
calebkiage Feb 12, 2025
29e0885
Merge branch 'main' into feat/telemetry
baywet Feb 12, 2025
f85d5d7
Add .NET generic host
calebkiage Feb 12, 2025
4fab314
Merge remote-tracking branch 'origin/feat/telemetry' into feat/telemetry
calebkiage Feb 12, 2025
1631e6c
Update packages
calebkiage Feb 12, 2025
46c86c2
Merge branch 'main' into feat/telemetry
calebkiage Feb 13, 2025
0514e18
Use a counter for the number of generations
calebkiage Feb 13, 2025
4550a38
Fetch telemetry configuration from config file
calebkiage Feb 13, 2025
0903e11
Format doc
calebkiage Feb 14, 2025
5c1c25e
Merge branch 'main' into feat/telemetry
calebkiage Feb 17, 2025
e21208b
Instrument remove client handler
calebkiage Feb 18, 2025
c2baaf7
Merge remote-tracking branch 'origin/feat/telemetry' into feat/telemetry
calebkiage Feb 18, 2025
67f1208
Add instrumentation to plugin add command
calebkiage Feb 18, 2025
ec02e57
Fix plugin add handler activity name
calebkiage Feb 19, 2025
e0f1275
Add more instrumentation for plugin commands
calebkiage Feb 19, 2025
f5de4a8
Instrument legacy generate command
calebkiage Feb 19, 2025
fb0b2e5
Format code
calebkiage Feb 19, 2025
40022f0
Merge branch 'main' into feat/telemetry
calebkiage Feb 19, 2025
dc879e0
Update appinsights connection string
calebkiage Feb 19, 2025
1dc1521
Add telemetry for download command
calebkiage Feb 19, 2025
d2a7c06
Format code
calebkiage Feb 19, 2025
01ed93d
Refactor Enumerable & string extensions
calebkiage Feb 19, 2025
9dd97d3
Instrument GitHub login & logout commands
calebkiage Feb 20, 2025
0753202
Add missing telemetry to plugin add/edit commands
calebkiage Feb 21, 2025
bf14bdf
Add more parameters to command execution metrics
calebkiage Feb 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
dotnet_diagnostic.CA2227.severity = none

[*.csproj]
indent_size = 2
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ updates:
kiota-dependencies:
patterns:
- "*kiota*"
OpenTelemetry:
patterns:
- "OpenTelemetry.*"
- "Azure.Monitor.OpenTelemetry.Exporter"
- package-ecosystem: github-actions
directory: "/"
schedule:
Expand Down
16 changes: 16 additions & 0 deletions src/kiota/Extension/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace kiota.Extension;

internal static class EnumerableExtensions
{
public static IEnumerable<T>? ConcatNullable<T>(this IEnumerable<T>? left, IEnumerable<T>? 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<T> OrEmpty<T>(this IEnumerable<T>? source)
{
return source ?? [];
}
}
92 changes: 92 additions & 0 deletions src/kiota/Extension/KiotaHostExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<string, object>("os.name", osName),
new KeyValuePair<string, object>("os.version", Environment.OSVersion.Version.ToString())
]);
}
});
openTelemetryBuilder.WithMetrics(static mp =>
{
mp.AddMeter($"{TelemetryLabels.ScopeName}*")
.AddHttpClientInstrumentation()
// TODO: Decide if runtime metrics are useful

Check warning on line 46 in src/kiota/Extension/KiotaHostExtensions.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
.AddRuntimeInstrumentation()
.SetExemplarFilter(ExemplarFilterType.TraceBased);
})
.WithTracing(static tp =>
{
tp.AddSource($"{TelemetryLabels.ScopeName}*")
.AddHttpClientInstrumentation();
});
if (cfg.OpenTelemetry.Enabled)
{
// Only register OpenTelemetry exporter if enabled.
Action<OtlpExporterOptions> 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> azureMonitorExporterOptions = options =>
{
options.ConnectionString = cfg.AppInsights.ConnectionString;
};
openTelemetryBuilder
.WithMetrics(mp => mp.AddAzureMonitorMetricExporter(azureMonitorExporterOptions))
.WithTracing(tp => tp.AddAzureMonitorTraceExporter(azureMonitorExporterOptions));
}
services.AddSingleton<Instrumentation>();
}
}
static string? OsName()
{
if (OperatingSystem.IsWindows()) return "windows";
if (OperatingSystem.IsLinux()) return "linux";
if (OperatingSystem.IsMacOS()) return "macos";

return OperatingSystem.IsFreeBSD() ? "freebsd" : null;
}
}
}
10 changes: 10 additions & 0 deletions src/kiota/Extension/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<char>.Empty
return source ?? string.Empty;
}
}
12 changes: 12 additions & 0 deletions src/kiota/Extension/TagListExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics;

namespace kiota.Extension;

internal static class TagListExtensions
{
public static TagList AddAll(this TagList tagList, IEnumerable<KeyValuePair<string, object?>> tags)
{
foreach (var tag in tags) tagList.Add(tag);
return tagList;
}
}
4 changes: 2 additions & 2 deletions src/kiota/Handlers/BaseKiotaCommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ protected async Task<KiotaSearcher> 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))
};
Expand Down Expand Up @@ -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<GenerationConfiguration, string> assignment)
protected void AssignIfNotNullOrEmpty(string? input, Action<GenerationConfiguration, string> assignment)
{
if (!string.IsNullOrEmpty(input))
assignment.Invoke(Configuration.Generation, input);
Expand Down
96 changes: 88 additions & 8 deletions src/kiota/Handlers/Client/AddHandler.cs
Original file line number Diff line number Diff line change
@@ -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<string, object?>[] _commonTags =
[
new(TelemetryLabels.TagGenerationOutputType, "client"),
new(TelemetryLabels.TagCommandName, "add"),
new(TelemetryLabels.TagCommandRevision, 1)
];
public required Option<string> ClassOption
{
get; init;
Expand Down Expand Up @@ -72,21 +83,48 @@

public override async Task<int> 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<string> includePatterns = context.ParseResult.GetValueForOption(IncludePatternsOption) ?? [];
List<string> excludePatterns = context.ParseResult.GetValueForOption(ExcludePatternsOption) ?? [];
List<string> disabledValidationRules = context.ParseResult.GetValueForOption(DisabledValidationRulesOption) ?? [];
List<string> structuredMimeTypes = context.ParseResult.GetValueForOption(StructuredMimeTypesOption) ?? [];
string? className = context.ParseResult.GetValueForOption(ClassOption);
string? namespaceName = context.ParseResult.GetValueForOption(NamespaceOption);
List<string>? includePatterns0 = context.ParseResult.GetValueForOption(IncludePatternsOption);
List<string>? excludePatterns0 = context.ParseResult.GetValueForOption(ExcludePatternsOption);
List<string>? disabledValidationRules0 = context.ParseResult.GetValueForOption(DisabledValidationRulesOption);
List<string>? 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<Instrumentation>();
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<string> includePatterns = includePatterns0 ?? [];
List<string> excludePatterns = excludePatterns0 ?? [];
List<string> disabledValidationRules = disabledValidationRules0 ?? [];
List<string> structuredMimeTypes = structuredMimeTypes0 ?? [];
AssignIfNotNullOrEmpty(output, (c, s) => c.OutputPath = s);
AssignIfNotNullOrEmpty(openapi, (c, s) => c.OpenAPIFilePath = s);
AssignIfNotNullOrEmpty(className, (c, s) => c.ClientClassName = s);
Expand Down Expand Up @@ -131,6 +169,14 @@
{
DisplaySuccess("Generation completed successfully");
DisplayUrlInformation(Configuration.Generation.ApiRootUrl);
var genCounter = instrumentation?.CreateClientGenerationCounter();
var meterTags = new TagList(_commonTags.AsSpan())
{
new KeyValuePair<string, object?>(
TelemetryLabels.TagGeneratorLanguage,
Configuration.Generation.Language.ToString("G"))
};
genCounter?.Add(1, meterTags);
}
else if (skipGeneration)
{
Expand All @@ -140,10 +186,13 @@
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
Expand All @@ -152,6 +201,37 @@
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<string>? includePatterns, List<string>? excludePatterns, List<string>? structuredMimeTypes, LogLevel? logLevel,
out List<KeyValuePair<string, object?>>? tags)
{
// set up telemetry tags
tags = activitySource?.HasListeners() == true ? new List<KeyValuePair<string, object?>>(10)
{
new(TelemetryLabels.TagGeneratorLanguage, language.ToString("G")),
// new($"{TelemetryLabels.TagCommandParams}.type_access_modifier", typeAccessModifier.ToString("G")),
new($"{TelemetryLabels.TagCommandParams}.backing_store", backingStore),
new($"{TelemetryLabels.TagCommandParams}.exclude_backward_compatible", excludeBackwardCompatible),
// new($"{TelemetryLabels.TagCommandParams}.include_additional_data", includeAdditionalData),
new($"{TelemetryLabels.TagCommandParams}.skip_generation", skipGeneration),
} : null;
const string redacted = TelemetryLabels.RedactedValuePlaceholder;
if (output is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.output", redacted));
if (namespaceName is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.namespace", redacted));
// if (className is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.client_name", redacted));

Check warning on line 229 in src/kiota/Handlers/Client/AddHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code. (https://rules.sonarsource.com/csharp/RSPEC-125)
// if (openapi is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.openapi", redacted));
if (includePatterns is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.include_path", redacted));
if (excludePatterns is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.exclude_path", redacted));
// if (disabledValidationRules is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.disable_validation_rules", disabledValidationRules0));

Check warning on line 233 in src/kiota/Handlers/Client/AddHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code. (https://rules.sonarsource.com/csharp/RSPEC-125)
if (structuredMimeTypes is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.structured_media_types", structuredMimeTypes.ToArray()));
if (logLevel is { } ll) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.log_level", ll.ToString("G")));
}
}
Loading
Loading