Skip to content

Commit

Permalink
Add telemetry PoC (#6128)
Browse files Browse the repository at this point in the history
* Add .NET generic host

Co-authored-by: Caleb Magiya (from Dev Box) <calebmagiya@microsoft.com>
Co-authored-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
3 people authored Feb 27, 2025
1 parent d529768 commit 7976e7e
Show file tree
Hide file tree
Showing 35 changed files with 1,595 additions and 83 deletions.
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
21 changes: 21 additions & 0 deletions src/kiota/Extension/CollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Diagnostics;

namespace kiota.Extension;

internal static class CollectionExtensions
{
public static TagList AddAll(this TagList tagList, IEnumerable<KeyValuePair<string, object?>> tags)
{
foreach (var tag in tags) tagList.Add(tag);
return tagList;
}

public static T[] OrEmpty<T>(this T[]? source)
{
return source ?? [];
}
public static List<T> OrEmpty<T>(this List<T>? source)
{
return source ?? [];
}
}
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()
// 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<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;
}
}
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
91 changes: 83 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 required Option<bool> SkipGenerationOption

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.OrEmpty();
List<string> excludePatterns = excludePatterns0.OrEmpty();
List<string> disabledValidationRules = disabledValidationRules0.OrEmpty();
List<string> structuredMimeTypes = structuredMimeTypes0.OrEmpty();
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 @@ public override async Task<int> InvokeAsync(InvocationContext context)
{
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 @@ public override async Task<int> 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
Expand All @@ -152,6 +201,32 @@ public override async Task<int> 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<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}.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<string, object?>($"{TelemetryLabels.TagCommandParams}.output", redacted));
if (namespaceName is not null) tags?.Add(new KeyValuePair<string, object?>($"{TelemetryLabels.TagCommandParams}.namespace", 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 (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

0 comments on commit 7976e7e

Please sign in to comment.